mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-12-12 03:53:01 +03:00
Compare commits
149 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93941f9728 | ||
|
|
aa0f6cb5eb | ||
|
|
69cc1e0bd8 | ||
|
|
007856e61a | ||
|
|
b4954985be | ||
|
|
6b16d90b9b | ||
|
|
2a89683e80 | ||
|
|
8595a979a8 | ||
|
|
1900096012 | ||
|
|
0e8da3e805 | ||
|
|
84c9e7a22b | ||
|
|
be28f940b7 | ||
|
|
fb95fb1a73 | ||
|
|
910995f922 | ||
|
|
4f0666ac5c | ||
|
|
4bfadbc636 | ||
|
|
2cc896251f | ||
|
|
5e343d30e1 | ||
|
|
df6c5b6d42 | ||
|
|
754bda8f73 | ||
|
|
3721b5e985 | ||
|
|
77c73e241f | ||
|
|
9954cbd550 | ||
|
|
d8f1a87c85 | ||
|
|
bf0a7c374c | ||
|
|
8a6b26cd42 | ||
|
|
b369194710 | ||
|
|
293bcfb342 | ||
|
|
1c5571b24e | ||
|
|
b507d1a780 | ||
|
|
d471be8d92 | ||
|
|
3c5b4b9a27 | ||
|
|
c9491cf317 | ||
|
|
c5dae18034 | ||
|
|
ff4f624850 | ||
|
|
d29a423475 | ||
|
|
4c0510ee6d | ||
|
|
492c6bbd7e | ||
|
|
84878f537c | ||
|
|
825e6460c9 | ||
|
|
760b021032 | ||
|
|
a532a866e3 | ||
|
|
71bf567045 | ||
|
|
a82e378da9 | ||
|
|
884a59da07 | ||
|
|
5a9e5e0d5d | ||
|
|
85cfea4c50 | ||
|
|
3ea67374ae | ||
|
|
5da4bcc782 | ||
|
|
b46d61dfdf | ||
|
|
8119e4a573 | ||
|
|
de3c68d474 | ||
|
|
7aa0db24d8 | ||
|
|
be832e82cc | ||
|
|
368d10d042 | ||
|
|
9523a1682b | ||
|
|
e67d8ce077 | ||
|
|
39196bb5e2 | ||
|
|
5386f06095 | ||
|
|
5a9afb0874 | ||
|
|
029be321d1 | ||
|
|
96b7c46df4 | ||
|
|
f7ef7d9eda | ||
|
|
c69e79f64d | ||
|
|
4b1256e67b | ||
|
|
8d1d973438 | ||
|
|
60affd0965 | ||
|
|
8a1eca0913 | ||
|
|
a4e4b761d5 | ||
|
|
2be9a34b26 | ||
|
|
0c8b9091a5 | ||
|
|
128d54622a | ||
|
|
21ce0e58c6 | ||
|
|
3229ba4918 | ||
|
|
105f057512 | ||
|
|
b6a2640f22 | ||
|
|
a2abae3014 | ||
|
|
7084541508 | ||
|
|
e87240b374 | ||
|
|
057d5dfc25 | ||
|
|
12f9132975 | ||
|
|
4a1aa619d2 | ||
|
|
1002d1d193 | ||
|
|
7c91543694 | ||
|
|
b04211a707 | ||
|
|
fcb65ac38d | ||
|
|
727402f7f7 | ||
|
|
2a1ca4badc | ||
|
|
7f52f77ef5 | ||
|
|
1d5961126e | ||
|
|
ec6f7bdcff | ||
|
|
65c47c79da | ||
|
|
5d9df10e27 | ||
|
|
d45d228b36 | ||
|
|
2b7d139b5b | ||
|
|
a280ff603f | ||
|
|
9beb3aff4e | ||
|
|
066bdc1e72 | ||
|
|
5833c70725 | ||
|
|
cd93f49fa8 | ||
|
|
93009682b3 | ||
|
|
56573f14b0 | ||
|
|
9001eaa67e | ||
|
|
76010e80dd | ||
|
|
5778541d2f | ||
|
|
cb0baddde3 | ||
|
|
0ff37413b0 | ||
|
|
d5434988d7 | ||
|
|
847518701d | ||
|
|
c5212a20a3 | ||
|
|
385a0b9437 | ||
|
|
884ba4f3ed | ||
|
|
cba6a4e3f3 | ||
|
|
d5f44f7a5c | ||
|
|
bf1ccf7493 | ||
|
|
d7c548f3db | ||
|
|
a7abdca47a | ||
|
|
7a8eaa8811 | ||
|
|
045dca49d0 | ||
|
|
e9ce82445e | ||
|
|
2133c6e348 | ||
|
|
5de2db9f52 | ||
|
|
620625c4c1 | ||
|
|
e0b035e34e | ||
|
|
1946414e14 | ||
|
|
132c85e554 | ||
|
|
9d1049e83f | ||
|
|
72aca15191 | ||
|
|
577325b788 | ||
|
|
fec2cf5060 | ||
|
|
53b06ce4e3 | ||
|
|
bdb85aeecf | ||
|
|
e299adc819 | ||
|
|
0674f84e9e | ||
|
|
b6086398d3 | ||
|
|
1d585146d6 | ||
|
|
aa1b1c6bbb | ||
|
|
bebe1808ce | ||
|
|
fb2e4c2e1a | ||
|
|
784ed796ce | ||
|
|
579155a571 | ||
|
|
ca16a55a47 | ||
|
|
e2ffd41141 | ||
|
|
ca67a48140 | ||
|
|
d2ce315c1d | ||
|
|
6a6874aa16 | ||
|
|
6f45848b51 | ||
|
|
23ba15ccfb | ||
|
|
5376c37d42 |
@@ -13,7 +13,7 @@ namespace Emby.Dlna.Configuration
|
||||
public DlnaOptions()
|
||||
{
|
||||
EnablePlayTo = true;
|
||||
EnableServer = true;
|
||||
EnableServer = false;
|
||||
BlastAliveMessages = true;
|
||||
SendOnlyMatchedHost = true;
|
||||
ClientDiscoveryIntervalSeconds = 60;
|
||||
|
||||
@@ -6,8 +6,8 @@ using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Diacritics.Extensions;
|
||||
using Emby.Dlna.Didl;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ namespace Emby.Naming.Common
|
||||
".mkv",
|
||||
".mk3d",
|
||||
".mov",
|
||||
".mp2",
|
||||
".mp4",
|
||||
".mpe",
|
||||
".mpeg",
|
||||
@@ -315,7 +314,7 @@ namespace Emby.Naming.Common
|
||||
// This isn't a Kodi naming rule, but the expression below causes false positives,
|
||||
// so we make sure this one gets tested first.
|
||||
// "Foo Bar 889"
|
||||
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/x]*$")
|
||||
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$")
|
||||
{
|
||||
IsNamed = true
|
||||
},
|
||||
|
||||
@@ -398,6 +398,12 @@ namespace Emby.Server.Implementations.AppBase
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ConfigurationStore[] GetConfigurationStores()
|
||||
{
|
||||
return _configurationStores;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Type GetConfigurationType(string key)
|
||||
{
|
||||
|
||||
@@ -11,7 +11,6 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using Diacritics.Extensions;
|
||||
using Emby.Server.Implementations.Playlists;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
@@ -5763,7 +5762,7 @@ AND Type = @InternalPersonType)");
|
||||
{
|
||||
var itemIdBlob = id.ToByteArray();
|
||||
|
||||
// First delete chapters
|
||||
// Delete existing mediastreams
|
||||
db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob);
|
||||
|
||||
InsertMediaStreams(itemIdBlob, streams, db);
|
||||
@@ -5867,10 +5866,10 @@ AND Type = @InternalPersonType)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the chapter.
|
||||
/// Gets the media stream.
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader.</param>
|
||||
/// <returns>ChapterInfo.</returns>
|
||||
/// <returns>MediaStream.</returns>
|
||||
private MediaStream GetMediaStream(IReadOnlyList<ResultSetValue> reader)
|
||||
{
|
||||
var item = new MediaStream
|
||||
|
||||
@@ -1094,7 +1094,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
{
|
||||
if (item is IHasTrailers hasTrailers)
|
||||
{
|
||||
dto.LocalTrailerCount = hasTrailers.GetTrailerCount();
|
||||
dto.LocalTrailerCount = hasTrailers.LocalTrailers.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -24,15 +24,15 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
|
||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.3" />
|
||||
<PackageReference Include="Mono.Nat" Version="3.0.2" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.3" />
|
||||
<PackageReference Include="sharpcompress" Version="0.30.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.5" />
|
||||
<PackageReference Include="Mono.Nat" Version="3.0.3" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.4" />
|
||||
<PackageReference Include="sharpcompress" Version="0.31.0" />
|
||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
||||
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -262,6 +262,10 @@ namespace Emby.Server.Implementations.IO
|
||||
_logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
|
||||
result.Exists = false;
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2840,10 +2840,12 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
var existingNameCount = 1; // first numbered name will be 2
|
||||
var virtualFolderPath = Path.Combine(rootFolderPath, name);
|
||||
var originalName = name;
|
||||
while (Directory.Exists(virtualFolderPath))
|
||||
{
|
||||
existingNameCount++;
|
||||
virtualFolderPath = Path.Combine(rootFolderPath, name + " " + existingNameCount);
|
||||
name = originalName + existingNameCount;
|
||||
virtualFolderPath = Path.Combine(rootFolderPath, name);
|
||||
}
|
||||
|
||||
var mediaPathInfos = options.PathInfos;
|
||||
|
||||
@@ -151,7 +151,11 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
|
||||
|
||||
if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio || i.Type == MediaStreamType.Video))
|
||||
// If file is strm or main media stream is missing, force a metadata refresh with remote probing
|
||||
if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder
|
||||
&& (item.Path.EndsWith(".strm", StringComparison.OrdinalIgnoreCase)
|
||||
|| (item.MediaType == MediaType.Video && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Video))
|
||||
|| (item.MediaType == MediaType.Audio && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio))))
|
||||
{
|
||||
await item.RefreshMetadata(
|
||||
new MetadataRefreshOptions(_directoryService)
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
await RecordFromFile(mediaSource, mediaSource.Path, targetFile, onStarted, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("Recording completed to file {0}", targetFile);
|
||||
_logger.LogInformation("Recording completed to file {Path}", targetFile);
|
||||
}
|
||||
|
||||
private async Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, Action onStarted, CancellationToken cancellationToken)
|
||||
@@ -115,7 +115,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
|
||||
_ = StartStreamingLog(_process.StandardError.BaseStream, _logFileStream);
|
||||
|
||||
_logger.LogInformation("ffmpeg recording process started for {0}", _targetPath);
|
||||
_logger.LogInformation("ffmpeg recording process started for {Path}", _targetPath);
|
||||
|
||||
// Block until ffmpeg exits
|
||||
await _taskCompletionSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private string GetCommandLineArgs(MediaSourceInfo mediaSource, string inputTempFile, string targetFile)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using System.Text;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
@@ -48,12 +49,18 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(info.EpisodeTitle))
|
||||
{
|
||||
var tmpName = name;
|
||||
if (addHyphen)
|
||||
{
|
||||
name += " -";
|
||||
tmpName += " -";
|
||||
}
|
||||
|
||||
name += " " + info.EpisodeTitle;
|
||||
tmpName += " " + info.EpisodeTitle;
|
||||
// Since the filename will be used with file ext. (.mp4, .ts, etc)
|
||||
if (Encoding.UTF8.GetByteCount(tmpName) < 250)
|
||||
{
|
||||
name = tmpName;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (info.IsMovie && info.ProductionYear != null)
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
@@ -26,6 +27,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
{
|
||||
public class XmlTvListingsProvider : IListingsProvider
|
||||
{
|
||||
private static readonly TimeSpan _maxCacheAge = TimeSpan.FromHours(1);
|
||||
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILogger<XmlTvListingsProvider> _logger;
|
||||
@@ -69,13 +72,19 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
return UnzipIfNeeded(info.Path, info.Path);
|
||||
}
|
||||
|
||||
string cacheFilename = DateTime.UtcNow.DayOfYear.ToString(CultureInfo.InvariantCulture) + "-" + DateTime.UtcNow.Hour.ToString(CultureInfo.InvariantCulture) + "-" + info.Id + ".xml";
|
||||
string cacheFilename = info.Id + ".xml";
|
||||
string cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename);
|
||||
if (File.Exists(cacheFile))
|
||||
if (File.Exists(cacheFile) && File.GetLastWriteTimeUtc(cacheFile) >= DateTime.UtcNow.Subtract(_maxCacheAge))
|
||||
{
|
||||
return UnzipIfNeeded(info.Path, cacheFile);
|
||||
}
|
||||
|
||||
// Must check if file exists as parent directory may not exist.
|
||||
if (File.Exists(cacheFile))
|
||||
{
|
||||
File.Delete(cacheFile);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Downloading xmltv listings from {Path}", info.Path);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFile));
|
||||
@@ -124,7 +133,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
{
|
||||
using (var stream = File.OpenRead(file))
|
||||
{
|
||||
string tempFolder = Path.Combine(_config.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString());
|
||||
string tempFolder = GetTempFolderPath(stream);
|
||||
Directory.CreateDirectory(tempFolder);
|
||||
|
||||
_zipClient.ExtractFirstFileFromGz(stream, tempFolder, "data.xml");
|
||||
@@ -137,7 +146,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
{
|
||||
using (var stream = File.OpenRead(file))
|
||||
{
|
||||
string tempFolder = Path.Combine(_config.ApplicationPaths.TempDirectory, Guid.NewGuid().ToString());
|
||||
string tempFolder = GetTempFolderPath(stream);
|
||||
Directory.CreateDirectory(tempFolder);
|
||||
|
||||
_zipClient.ExtractAllFromGz(stream, tempFolder, true);
|
||||
@@ -146,6 +155,16 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
}
|
||||
}
|
||||
|
||||
private string GetTempFolderPath(Stream stream)
|
||||
{
|
||||
#pragma warning disable CA5351
|
||||
using var md5 = MD5.Create();
|
||||
#pragma warning restore CA5351
|
||||
var checksum = Convert.ToHexString(md5.ComputeHash(stream));
|
||||
stream.Position = 0;
|
||||
return Path.Combine(_config.ApplicationPaths.TempDirectory, checksum);
|
||||
}
|
||||
|
||||
private string FindXmlFile(string directory)
|
||||
{
|
||||
return _fileSystem.GetFiles(directory, true)
|
||||
|
||||
@@ -104,6 +104,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
||||
{
|
||||
_logger.LogError(ex, "Error updating {0}", package.Name);
|
||||
}
|
||||
catch (InvalidDataException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error updating {0}", package.Name);
|
||||
}
|
||||
|
||||
// Update progress
|
||||
lock (progress)
|
||||
|
||||
@@ -329,13 +329,17 @@ namespace Emby.Server.Implementations.Session
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CloseIfNeeded(SessionInfo session)
|
||||
public async Task CloseIfNeededAsync(SessionInfo session)
|
||||
{
|
||||
if (!session.SessionControllers.Any(i => i.IsSessionActive))
|
||||
{
|
||||
var key = GetSessionKey(session.Client, session.DeviceId);
|
||||
|
||||
_activeConnections.TryRemove(key, out _);
|
||||
if (!string.IsNullOrEmpty(session.PlayState?.LiveStreamId))
|
||||
{
|
||||
await _mediaSourceManager.CloseLiveStream(session.PlayState.LiveStreamId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
OnSessionEnded(session);
|
||||
}
|
||||
@@ -413,6 +417,7 @@ namespace Emby.Server.Implementations.Session
|
||||
session.PlayState.IsPaused = info.IsPaused;
|
||||
session.PlayState.PositionTicks = info.PositionTicks;
|
||||
session.PlayState.MediaSourceId = info.MediaSourceId;
|
||||
session.PlayState.LiveStreamId = info.LiveStreamId;
|
||||
session.PlayState.CanSeek = info.CanSeek;
|
||||
session.PlayState.IsMuted = info.IsMuted;
|
||||
session.PlayState.VolumeLevel = info.VolumeLevel;
|
||||
@@ -699,7 +704,9 @@ namespace Emby.Server.Implementations.Session
|
||||
DeviceName = session.DeviceName,
|
||||
ClientName = session.Client,
|
||||
DeviceId = session.DeviceId,
|
||||
Session = session
|
||||
Session = session,
|
||||
PlaybackPositionTicks = info.PositionTicks,
|
||||
PlaySessionId = info.PlaySessionId
|
||||
};
|
||||
|
||||
await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
|
||||
@@ -768,6 +775,11 @@ namespace Emby.Server.Implementations.Session
|
||||
|
||||
await UpdateNowPlayingItem(session, info, libraryItem, !isAutomated).ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(session.DeviceId) && info.PlayMethod != PlayMethod.Transcode)
|
||||
{
|
||||
ClearTranscodingInfo(session.DeviceId);
|
||||
}
|
||||
|
||||
var users = GetUsers(session);
|
||||
|
||||
// only update saved user data on actual check-ins, not automated ones
|
||||
@@ -985,7 +997,8 @@ namespace Emby.Server.Implementations.Session
|
||||
DeviceName = session.DeviceName,
|
||||
ClientName = session.Client,
|
||||
DeviceId = session.DeviceId,
|
||||
Session = session
|
||||
Session = session,
|
||||
PlaySessionId = info.PlaySessionId
|
||||
};
|
||||
|
||||
await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
|
||||
|
||||
@@ -53,13 +53,13 @@ namespace Emby.Server.Implementations.Session
|
||||
connection.Closed += OnConnectionClosed;
|
||||
}
|
||||
|
||||
private void OnConnectionClosed(object? sender, EventArgs e)
|
||||
private async void OnConnectionClosed(object? sender, EventArgs e)
|
||||
{
|
||||
var connection = sender as IWebSocketConnection ?? throw new ArgumentException($"{nameof(sender)} is not of type {nameof(IWebSocketConnection)}", nameof(sender));
|
||||
_logger.LogDebug("Removing websocket from session {Session}", _session.Id);
|
||||
_sockets.Remove(connection);
|
||||
connection.Closed -= OnConnectionClosed;
|
||||
_sessionManager.CloseIfNeeded(_session);
|
||||
await _sessionManager.CloseIfNeededAsync(_session).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -34,6 +34,11 @@ namespace Emby.Server.Implementations.Sorting
|
||||
throw new ArgumentNullException(nameof(y));
|
||||
}
|
||||
|
||||
if (!x.IndexNumber.HasValue && !y.IndexNumber.HasValue)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!x.IndexNumber.HasValue)
|
||||
{
|
||||
return -1;
|
||||
|
||||
@@ -34,6 +34,11 @@ namespace Emby.Server.Implementations.Sorting
|
||||
throw new ArgumentNullException(nameof(y));
|
||||
}
|
||||
|
||||
if (!x.ParentIndexNumber.HasValue && !y.ParentIndexNumber.HasValue)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!x.ParentIndexNumber.HasValue)
|
||||
{
|
||||
return -1;
|
||||
|
||||
25
Jellyfin.Api/Attributes/DlnaEnabledAttribute.cs
Normal file
25
Jellyfin.Api/Attributes/DlnaEnabledAttribute.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Emby.Dlna;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Jellyfin.Api.Attributes;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class DlnaEnabledAttribute : ActionFilterAttribute
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var serverConfigurationManager = context.HttpContext.RequestServices.GetRequiredService<IServerConfigurationManager>();
|
||||
|
||||
var enabled = serverConfigurationManager.GetDlnaConfiguration().EnableServer;
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
context.Result = new StatusCodeResult(StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,21 +86,23 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Updates named configuration.
|
||||
/// </summary>
|
||||
/// <param name="key">Configuration key.</param>
|
||||
/// <param name="configuration">Configuration.</param>
|
||||
/// <response code="204">Named configuration updated.</response>
|
||||
/// <returns>Update status.</returns>
|
||||
[HttpPost("Configuration/{key}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> UpdateNamedConfiguration([FromRoute, Required] string key)
|
||||
public ActionResult UpdateNamedConfiguration([FromRoute, Required] string key, [FromBody, Required] JsonDocument configuration)
|
||||
{
|
||||
var configurationType = _configurationManager.GetConfigurationType(key);
|
||||
var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, _serializerOptions).ConfigureAwait(false);
|
||||
if (configuration == null)
|
||||
var deserializedConfiguration = configuration.Deserialize(configurationType, _serializerOptions);
|
||||
|
||||
if (deserializedConfiguration == null)
|
||||
{
|
||||
throw new ArgumentException("Body doesn't contain a valid configuration");
|
||||
}
|
||||
|
||||
_configurationManager.SaveConfiguration(key, configuration);
|
||||
_configurationManager.SaveConfiguration(key, deserializedConfiguration);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Dlna Server Controller.
|
||||
/// </summary>
|
||||
[Route("Dlna")]
|
||||
[DlnaEnabled]
|
||||
[Authorize(Policy = Policies.AnonymousLanAccessPolicy)]
|
||||
public class DlnaServerController : BaseJellyfinApiController
|
||||
{
|
||||
@@ -55,15 +56,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public ActionResult GetDescriptionXml([FromRoute, Required] string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
var url = GetAbsoluteUri();
|
||||
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
|
||||
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
|
||||
return Ok(xml);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
var url = GetAbsoluteUri();
|
||||
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
|
||||
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
|
||||
return Ok(xml);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -83,12 +79,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
public ActionResult GetContentDirectory([FromRoute, Required] string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return Ok(_contentDirectory.GetServiceXml());
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return Ok(_contentDirectory.GetServiceXml());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -108,12 +99,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return Ok(_mediaReceiverRegistrar.GetServiceXml());
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return Ok(_mediaReceiverRegistrar.GetServiceXml());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -133,12 +119,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
public ActionResult GetConnectionManager([FromRoute, Required] string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return Ok(_connectionManager.GetServiceXml());
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return Ok(_connectionManager.GetServiceXml());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -155,12 +136,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -177,12 +153,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -199,12 +170,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -224,12 +190,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return ProcessEventRequest(_mediaReceiverRegistrar);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return ProcessEventRequest(_mediaReceiverRegistrar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -249,12 +210,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return ProcessEventRequest(_contentDirectory);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return ProcessEventRequest(_contentDirectory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -274,12 +230,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return ProcessEventRequest(_connectionManager);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return ProcessEventRequest(_connectionManager);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -299,12 +250,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesImageFile]
|
||||
public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return GetIconInternal(fileName);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return GetIconInternal(fileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -322,12 +268,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesImageFile]
|
||||
public ActionResult GetIcon([FromRoute, Required] string fileName)
|
||||
{
|
||||
if (DlnaEntryPoint.Enabled)
|
||||
{
|
||||
return GetIconInternal(fileName);
|
||||
}
|
||||
|
||||
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
return GetIconInternal(fileName);
|
||||
}
|
||||
|
||||
private ActionResult GetIconInternal(string fileName)
|
||||
|
||||
@@ -1711,20 +1711,30 @@ namespace Jellyfin.Api.Controllers
|
||||
return audioTranscodeParams;
|
||||
}
|
||||
|
||||
// flac and opus are experimental in mp4 muxer
|
||||
var strictArgs = string.Empty;
|
||||
|
||||
if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
strictArgs = " -strict -2";
|
||||
}
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||
{
|
||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
||||
var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs;
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
|
||||
{
|
||||
return "-codec:a:0 copy -strict -2 -copypriorss:a:0 0" + bitStreamArgs;
|
||||
return copyArgs + " -copypriorss:a:0 0";
|
||||
}
|
||||
|
||||
return "-codec:a:0 copy -strict -2" + bitStreamArgs;
|
||||
return copyArgs;
|
||||
}
|
||||
|
||||
var args = "-codec:a:0 " + audioCodec;
|
||||
var args = "-codec:a:0 " + audioCodec + strictArgs;
|
||||
|
||||
var channels = state.OutputAudioChannels;
|
||||
|
||||
@@ -1773,13 +1783,24 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var args = "-codec:v:0 " + codec;
|
||||
|
||||
// Prefer hvc1 to hev1.
|
||||
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " -tag:v:0 hvc1";
|
||||
if (EncodingHelper.IsCopyCodec(codec)
|
||||
&& (string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
// Prefer dvh1 to dvhe
|
||||
args += " -tag:v:0 dvh1 -strict -2";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Prefer hvc1 to hev1
|
||||
args += " -tag:v:0 hvc1";
|
||||
}
|
||||
}
|
||||
|
||||
// if (state.EnableMpegtsM2TsMode)
|
||||
|
||||
@@ -89,6 +89,11 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
|
||||
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
|
||||
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
|
||||
/// <param name="isMovie">Optional filter for live tv movies.</param>
|
||||
/// <param name="isSeries">Optional filter for live tv series.</param>
|
||||
/// <param name="isNews">Optional filter for live tv news.</param>
|
||||
/// <param name="isKids">Optional filter for live tv kids.</param>
|
||||
/// <param name="isSports">Optional filter for live tv sports.</param>
|
||||
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
@@ -173,6 +178,11 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? hasImdbId,
|
||||
[FromQuery] bool? hasTmdbId,
|
||||
[FromQuery] bool? hasTvdbId,
|
||||
[FromQuery] bool? isMovie,
|
||||
[FromQuery] bool? isSeries,
|
||||
[FromQuery] bool? isNews,
|
||||
[FromQuery] bool? isKids,
|
||||
[FromQuery] bool? isSports,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
@@ -316,6 +326,11 @@ namespace Jellyfin.Api.Controllers
|
||||
Is3D = is3D,
|
||||
HasTvdbId = hasTvdbId,
|
||||
HasTmdbId = hasTmdbId,
|
||||
IsMovie = isMovie,
|
||||
IsSeries = isSeries,
|
||||
IsNews = isNews,
|
||||
IsKids = isKids,
|
||||
IsSports = isSports,
|
||||
HasOverview = hasOverview,
|
||||
HasOfficialRating = hasOfficialRating,
|
||||
HasParentalRating = hasParentalRating,
|
||||
@@ -515,8 +530,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
|
||||
/// <param name="isHd">Optional filter by items that are HD or not.</param>
|
||||
/// <param name="is4K">Optional filter by items that are 4K or not.</param>
|
||||
/// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
|
||||
/// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
|
||||
/// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
|
||||
/// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
|
||||
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
|
||||
@@ -529,42 +544,47 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
|
||||
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
|
||||
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
|
||||
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="isMovie">Optional filter for live tv movies.</param>
|
||||
/// <param name="isSeries">Optional filter for live tv series.</param>
|
||||
/// <param name="isNews">Optional filter for live tv news.</param>
|
||||
/// <param name="isKids">Optional filter for live tv kids.</param>
|
||||
/// <param name="isSports">Optional filter for live tv sports.</param>
|
||||
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
|
||||
/// <param name="searchTerm">Optional. Filter based on a search term.</param>
|
||||
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
|
||||
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
|
||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
|
||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
|
||||
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
|
||||
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
|
||||
/// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
|
||||
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
|
||||
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
|
||||
/// <param name="isPlayed">Optional filter by items that are played, or not.</param>
|
||||
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
|
||||
/// <param name="enableUserData">Optional, include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
|
||||
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
|
||||
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
|
||||
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
|
||||
/// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
|
||||
/// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
|
||||
/// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
|
||||
/// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.</param>
|
||||
/// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
|
||||
/// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
|
||||
/// <param name="isLocked">Optional filter by items that are locked.</param>
|
||||
/// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
|
||||
@@ -575,12 +595,12 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
|
||||
/// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
|
||||
/// <param name="is3D">Optional filter by items that are 3D, or not.</param>
|
||||
/// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimeted.</param>
|
||||
/// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
|
||||
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
||||
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
||||
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
||||
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
|
||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
|
||||
@@ -613,6 +633,11 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? hasImdbId,
|
||||
[FromQuery] bool? hasTmdbId,
|
||||
[FromQuery] bool? hasTvdbId,
|
||||
[FromQuery] bool? isMovie,
|
||||
[FromQuery] bool? isSeries,
|
||||
[FromQuery] bool? isNews,
|
||||
[FromQuery] bool? isKids,
|
||||
[FromQuery] bool? isSports,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
@@ -695,6 +720,11 @@ namespace Jellyfin.Api.Controllers
|
||||
hasImdbId,
|
||||
hasTmdbId,
|
||||
hasTvdbId,
|
||||
isMovie,
|
||||
isSeries,
|
||||
isNews,
|
||||
isKids,
|
||||
isSports,
|
||||
excludeItemIds,
|
||||
startIndex,
|
||||
limit,
|
||||
|
||||
@@ -155,7 +155,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Package repositories saved.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Repositories")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult SetRepositories([FromBody, Required] List<RepositoryInfo> repositoryInfos)
|
||||
{
|
||||
|
||||
@@ -57,6 +57,11 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
|
||||
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
|
||||
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
|
||||
/// <param name="isMovie">Optional filter for live tv movies.</param>
|
||||
/// <param name="isSeries">Optional filter for live tv series.</param>
|
||||
/// <param name="isNews">Optional filter for live tv news.</param>
|
||||
/// <param name="isKids">Optional filter for live tv kids.</param>
|
||||
/// <param name="isSports">Optional filter for live tv sports.</param>
|
||||
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
@@ -140,6 +145,11 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? hasImdbId,
|
||||
[FromQuery] bool? hasTmdbId,
|
||||
[FromQuery] bool? hasTvdbId,
|
||||
[FromQuery] bool? isMovie,
|
||||
[FromQuery] bool? isSeries,
|
||||
[FromQuery] bool? isNews,
|
||||
[FromQuery] bool? isKids,
|
||||
[FromQuery] bool? isSports,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
@@ -224,6 +234,11 @@ namespace Jellyfin.Api.Controllers
|
||||
hasImdbId,
|
||||
hasTmdbId,
|
||||
hasTvdbId,
|
||||
isMovie,
|
||||
isSeries,
|
||||
isNews,
|
||||
isKids,
|
||||
isSports,
|
||||
excludeItemIds,
|
||||
startIndex,
|
||||
limit,
|
||||
|
||||
@@ -256,9 +256,17 @@ namespace Jellyfin.Api.Helpers
|
||||
streamInfo.StartPositionTicks = startTimeTicks;
|
||||
|
||||
mediaSource.SupportsDirectPlay = streamInfo.PlayMethod == PlayMethod.DirectPlay;
|
||||
|
||||
// Players do not handle this being set according to PlayMethod
|
||||
mediaSource.SupportsDirectStream = options.EnableDirectStream ? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream : streamInfo.PlayMethod == PlayMethod.DirectPlay;
|
||||
mediaSource.SupportsTranscoding = streamInfo.PlayMethod == PlayMethod.DirectStream || mediaSource.TranscodingContainer != null;
|
||||
mediaSource.SupportsDirectStream =
|
||||
options.EnableDirectStream
|
||||
? streamInfo.PlayMethod == PlayMethod.DirectPlay || streamInfo.PlayMethod == PlayMethod.DirectStream
|
||||
: streamInfo.PlayMethod == PlayMethod.DirectPlay;
|
||||
|
||||
mediaSource.SupportsTranscoding =
|
||||
streamInfo.PlayMethod == PlayMethod.DirectStream
|
||||
|| mediaSource.TranscodingContainer != null
|
||||
|| profile.TranscodingProfiles.Any(i => i.Type == streamInfo.MediaType && i.Context == options.Context);
|
||||
|
||||
if (item is Audio)
|
||||
{
|
||||
@@ -290,7 +298,7 @@ namespace Jellyfin.Api.Helpers
|
||||
}
|
||||
else
|
||||
{
|
||||
if (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream)
|
||||
if (!mediaSource.SupportsDirectPlay && (mediaSource.SupportsTranscoding || mediaSource.SupportsDirectStream))
|
||||
{
|
||||
streamInfo.PlayMethod = PlayMethod.Transcode;
|
||||
mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.3.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -362,7 +362,7 @@ namespace Jellyfin.Data.Entities
|
||||
/// <returns><c>True</c> if the user has the specified permission.</returns>
|
||||
public bool HasPermission(PermissionKind kind)
|
||||
{
|
||||
return Permissions.First(p => p.Kind == kind).Value;
|
||||
return Permissions.FirstOrDefault(p => p.Kind == kind)?.Value ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -372,7 +372,15 @@ namespace Jellyfin.Data.Entities
|
||||
/// <param name="value">The value to set.</param>
|
||||
public void SetPermission(PermissionKind kind, bool value)
|
||||
{
|
||||
Permissions.First(p => p.Kind == kind).Value = value;
|
||||
var currentPermission = Permissions.FirstOrDefault(p => p.Kind == kind);
|
||||
if (currentPermission == null)
|
||||
{
|
||||
Permissions.Add(new Permission(kind, value));
|
||||
}
|
||||
else
|
||||
{
|
||||
currentPermission.Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -382,9 +390,9 @@ namespace Jellyfin.Data.Entities
|
||||
/// <returns>A string array containing the user's preferences.</returns>
|
||||
public string[] GetPreference(PreferenceKind preference)
|
||||
{
|
||||
var val = Preferences.First(p => p.Kind == preference).Value;
|
||||
var val = Preferences.FirstOrDefault(p => p.Kind == preference)?.Value;
|
||||
|
||||
return Equals(val, string.Empty) ? Array.Empty<string>() : val.Split(Delimiter);
|
||||
return string.IsNullOrEmpty(val) ? Array.Empty<string>() : val.Split(Delimiter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -395,7 +403,7 @@ namespace Jellyfin.Data.Entities
|
||||
/// <returns>A {T} array containing the user's preference.</returns>
|
||||
public T[] GetPreferenceValues<T>(PreferenceKind preference)
|
||||
{
|
||||
var val = Preferences.First(p => p.Kind == preference).Value;
|
||||
var val = Preferences.FirstOrDefault(p => p.Kind == preference)?.Value;
|
||||
if (string.IsNullOrEmpty(val))
|
||||
{
|
||||
return Array.Empty<T>();
|
||||
@@ -432,8 +440,16 @@ namespace Jellyfin.Data.Entities
|
||||
/// <param name="values">The values.</param>
|
||||
public void SetPreference(PreferenceKind preference, string[] values)
|
||||
{
|
||||
Preferences.First(p => p.Kind == preference).Value
|
||||
= string.Join(Delimiter, values);
|
||||
var value = string.Join(Delimiter, values);
|
||||
var currentPreference = Preferences.FirstOrDefault(p => p.Kind == preference);
|
||||
if (currentPreference == null)
|
||||
{
|
||||
Preferences.Add(new Preference(preference, value));
|
||||
}
|
||||
else
|
||||
{
|
||||
currentPreference.Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -444,8 +460,16 @@ namespace Jellyfin.Data.Entities
|
||||
/// <typeparam name="T">The type of value.</typeparam>
|
||||
public void SetPreference<T>(PreferenceKind preference, T[] values)
|
||||
{
|
||||
Preferences.First(p => p.Kind == preference).Value
|
||||
= string.Join(Delimiter, values);
|
||||
var value = string.Join(Delimiter, values);
|
||||
var currentPreference = Preferences.FirstOrDefault(p => p.Kind == preference);
|
||||
if (currentPreference == null)
|
||||
{
|
||||
Preferences.Add(new Preference(preference, value));
|
||||
}
|
||||
else
|
||||
{
|
||||
currentPreference.Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BlurHashSharp" Version="1.2.0" />
|
||||
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.80.3" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.3" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.1" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.1-preview.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using BlurHashSharp.SkiaSharp;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
|
||||
@@ -463,6 +463,18 @@ namespace Jellyfin.Networking.Manager
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInLocalNetwork(IPObject address)
|
||||
{
|
||||
return IsInLocalNetwork(address.Address);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInLocalNetwork(string address)
|
||||
{
|
||||
return IPHost.TryParse(address, out IPHost ipHost) && IsInLocalNetwork(ipHost);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInLocalNetwork(IPAddress address)
|
||||
{
|
||||
if (address == null)
|
||||
{
|
||||
@@ -481,36 +493,7 @@ namespace Jellyfin.Networking.Manager
|
||||
}
|
||||
|
||||
// As private addresses can be redefined by Configuration.LocalNetworkAddresses
|
||||
return address.IsLoopback() || (_lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInLocalNetwork(string address)
|
||||
{
|
||||
if (IPHost.TryParse(address, out IPHost ep))
|
||||
{
|
||||
return _lanSubnets.ContainsAddress(ep) && !_excludedSubnets.ContainsAddress(ep);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInLocalNetwork(IPAddress address)
|
||||
{
|
||||
if (address == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
}
|
||||
|
||||
// See conversation at https://github.com/jellyfin/jellyfin/pull/3515.
|
||||
if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// As private addresses can be redefined by Configuration.LocalNetworkAddresses
|
||||
return _lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address);
|
||||
return IPAddress.IsLoopback(address) || (_lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -27,13 +27,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.3">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.3">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -16,11 +17,16 @@ namespace Jellyfin.Server.Implementations.Security
|
||||
{
|
||||
private readonly JellyfinDbProvider _jellyfinDbProvider;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IServerApplicationHost _serverApplicationHost;
|
||||
|
||||
public AuthorizationContext(JellyfinDbProvider jellyfinDb, IUserManager userManager)
|
||||
public AuthorizationContext(
|
||||
JellyfinDbProvider jellyfinDb,
|
||||
IUserManager userManager,
|
||||
IServerApplicationHost serverApplicationHost)
|
||||
{
|
||||
_jellyfinDbProvider = jellyfinDb;
|
||||
_userManager = userManager;
|
||||
_serverApplicationHost = serverApplicationHost;
|
||||
}
|
||||
|
||||
public Task<AuthorizationInfo> GetAuthorizationInfo(HttpContext requestContext)
|
||||
@@ -187,17 +193,17 @@ namespace Jellyfin.Server.Implementations.Security
|
||||
authInfo.Token = key.AccessToken;
|
||||
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
|
||||
{
|
||||
authInfo.DeviceId = string.Empty;
|
||||
authInfo.DeviceId = _serverApplicationHost.SystemId;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Device))
|
||||
{
|
||||
authInfo.Device = string.Empty;
|
||||
authInfo.Device = _serverApplicationHost.Name;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Version))
|
||||
{
|
||||
authInfo.Version = string.Empty;
|
||||
authInfo.Version = _serverApplicationHost.ApplicationVersionString;
|
||||
}
|
||||
|
||||
authInfo.IsApiKey = true;
|
||||
|
||||
@@ -440,6 +440,12 @@ namespace Jellyfin.Server.Extensions
|
||||
.Cast<IOpenApiAny>()
|
||||
.ToArray()
|
||||
});
|
||||
|
||||
// Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it.
|
||||
options.MapType<Version>(() => new OpenApiSchema
|
||||
{
|
||||
Type = "string"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using System;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Server.Migrations;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.ApiClient;
|
||||
using MediaBrowser.Model.Entities;
|
||||
@@ -14,6 +18,19 @@ namespace Jellyfin.Server.Filters
|
||||
/// </summary>
|
||||
public class AdditionalModelFilter : IDocumentFilter
|
||||
{
|
||||
// Array of options that should not be visible in the api spec.
|
||||
private static readonly Type[] _ignoredConfigurations = { typeof(MigrationOptions) };
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AdditionalModelFilter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
public AdditionalModelFilter(IServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
@@ -29,6 +46,16 @@ namespace Jellyfin.Server.Filters
|
||||
|
||||
context.SchemaGenerator.GenerateSchema(typeof(SessionMessageType), context.SchemaRepository);
|
||||
context.SchemaGenerator.GenerateSchema(typeof(ServerDiscoveryInfo), context.SchemaRepository);
|
||||
|
||||
foreach (var configuration in _serverConfigurationManager.GetConfigurationStores())
|
||||
{
|
||||
if (_ignoredConfigurations.IndexOf(configuration.ConfigurationType) != -1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
context.SchemaGenerator.GenerateSchema(configuration.ConfigurationType, context.SchemaRepository);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.5" />
|
||||
<PackageReference Include="prometheus-net" Version="6.0.0" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
|
||||
@@ -48,7 +48,7 @@
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Graylog" Version="2.3.0" />
|
||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.7" />
|
||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -5,6 +5,7 @@ using Emby.Server.Implementations.Data;
|
||||
using Jellyfin.Data.Entities.Security;
|
||||
using Jellyfin.Server.Implementations;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLitePCL.pretty;
|
||||
|
||||
@@ -20,6 +21,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
private readonly ILogger<MigrateAuthenticationDb> _logger;
|
||||
private readonly JellyfinDbProvider _dbProvider;
|
||||
private readonly IServerApplicationPaths _appPaths;
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MigrateAuthenticationDb"/> class.
|
||||
@@ -27,11 +29,17 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="dbProvider">The database provider.</param>
|
||||
/// <param name="appPaths">The server application paths.</param>
|
||||
public MigrateAuthenticationDb(ILogger<MigrateAuthenticationDb> logger, JellyfinDbProvider dbProvider, IServerApplicationPaths appPaths)
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
public MigrateAuthenticationDb(
|
||||
ILogger<MigrateAuthenticationDb> logger,
|
||||
JellyfinDbProvider dbProvider,
|
||||
IServerApplicationPaths appPaths,
|
||||
IUserManager userManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_dbProvider = dbProvider;
|
||||
_appPaths = appPaths;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -74,6 +82,14 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
}
|
||||
else
|
||||
{
|
||||
var userId = new Guid(row[6].ToString());
|
||||
var user = _userManager.GetUserById(userId);
|
||||
if (user is null)
|
||||
{
|
||||
// User doesn't exist, don't bring over the device.
|
||||
continue;
|
||||
}
|
||||
|
||||
dbContext.Devices.Add(new Device(
|
||||
new Guid(row[6].ToString()),
|
||||
row[3].ToString(),
|
||||
|
||||
@@ -545,12 +545,14 @@ namespace Jellyfin.Server
|
||||
const string ResourcePath = "Jellyfin.Server.Resources.Configuration.logging.json";
|
||||
Stream resource = typeof(Program).Assembly.GetManifestResourceStream(ResourcePath)
|
||||
?? throw new InvalidOperationException($"Invalid resource path: '{ResourcePath}'");
|
||||
Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||
await using (resource.ConfigureAwait(false))
|
||||
await using (dst.ConfigureAwait(false))
|
||||
{
|
||||
// Copy the resource contents to the expected file path for the config file
|
||||
await resource.CopyToAsync(dst).ConfigureAwait(false);
|
||||
Stream dst = new FileStream(configPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||
await using (dst.ConfigureAwait(false))
|
||||
{
|
||||
// Copy the resource contents to the expected file path for the config file
|
||||
await resource.CopyToAsync(dst).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,12 @@ namespace MediaBrowser.Common.Configuration
|
||||
/// <returns>System.Object.</returns>
|
||||
object GetConfiguration(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the array of coniguration stores.
|
||||
/// </summary>
|
||||
/// <returns>Array of ConfigurationStore.</returns>
|
||||
ConfigurationStore[] GetConfigurationStores();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the configuration.
|
||||
/// </summary>
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace MediaBrowser.Common.Net
|
||||
address = address.MapToIPv4();
|
||||
}
|
||||
|
||||
if (IsLoopback(address))
|
||||
if (IPAddress.IsLoopback(address))
|
||||
{
|
||||
return (address, prefixLength);
|
||||
}
|
||||
@@ -102,31 +102,6 @@ namespace MediaBrowser.Common.Net
|
||||
return (new IPAddress(addressBytes), prefixLength);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests to see if the ip address is a Loopback address.
|
||||
/// </summary>
|
||||
/// <param name="address">Value to test.</param>
|
||||
/// <returns>True if it is.</returns>
|
||||
public static bool IsLoopback(IPAddress address)
|
||||
{
|
||||
if (address == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
}
|
||||
|
||||
if (!address.Equals(IPAddress.None))
|
||||
{
|
||||
if (address.IsIPv4MappedToIPv6)
|
||||
{
|
||||
address = address.MapToIPv4();
|
||||
}
|
||||
|
||||
return address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests to see if the ip address is an IP6 address.
|
||||
/// </summary>
|
||||
@@ -295,7 +270,7 @@ namespace MediaBrowser.Common.Net
|
||||
/// <returns>True if it is.</returns>
|
||||
public virtual bool IsLoopback()
|
||||
{
|
||||
return IsLoopback(Address);
|
||||
return IPAddress.IsLoopback(Address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -8,9 +8,9 @@ using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Audio
|
||||
|
||||
@@ -11,7 +11,6 @@ using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
namespace MediaBrowser.Controller.Library
|
||||
{
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Diacritics" Version="3.3.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||
|
||||
@@ -13,11 +13,13 @@ using System.Threading;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
@@ -32,6 +34,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly ISubtitleEncoder _subtitleEncoder;
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
private static readonly string[] _videoProfilesH264 = new[]
|
||||
{
|
||||
@@ -54,11 +57,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
public EncodingHelper(
|
||||
IApplicationPaths appPaths,
|
||||
IMediaEncoder mediaEncoder,
|
||||
ISubtitleEncoder subtitleEncoder)
|
||||
ISubtitleEncoder subtitleEncoder,
|
||||
IConfiguration config)
|
||||
{
|
||||
_appPaths = appPaths;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_subtitleEncoder = subtitleEncoder;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
|
||||
@@ -144,15 +149,28 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
private bool IsHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
|
||||
{
|
||||
if (state.VideoStream == null)
|
||||
if (state.VideoStream == null
|
||||
|| !options.EnableTonemapping
|
||||
|| GetVideoColorBitDepth(state) != 10)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return options.EnableTonemapping
|
||||
&& (string.Equals(state.VideoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.VideoStream.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
|
||||
&& GetVideoColorBitDepth(state) == 10;
|
||||
if (string.Equals(state.VideoStream.CodecTag, "dovi", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.VideoStream.CodecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.VideoStream.CodecTag, "dvhe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Only native SW decoder and HW accelerator can parse dovi rpu.
|
||||
var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
|
||||
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
|
||||
var isNvdecDecoder = vidDecoder.Contains("cuda", StringComparison.OrdinalIgnoreCase);
|
||||
var isVaapiDecoder = vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase);
|
||||
var isD3d11vaDecoder = vidDecoder.Contains("d3d11va", StringComparison.OrdinalIgnoreCase);
|
||||
return isSwDecoder || isNvdecDecoder || isVaapiDecoder || isD3d11vaDecoder;
|
||||
}
|
||||
|
||||
return string.Equals(state.VideoStream.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.VideoStream.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private bool IsVaapiVppTonemapAvailable(EncodingJobInfo state, EncodingOptions options)
|
||||
@@ -516,8 +534,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
if (string.Equals(codec, "flac", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// flac is experimental in mp4 muxer
|
||||
return "flac -strict -2";
|
||||
return "flac";
|
||||
}
|
||||
|
||||
return codec.ToLowerInvariant();
|
||||
@@ -1024,7 +1041,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return FormattableString.Invariant($" -qmin 18 -qmax 32 -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
|
||||
// Override the too high default qmin 18 in transcoding preset
|
||||
return FormattableString.Invariant($" -rc cbr -qmin 0 -qmax 32 -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
|
||||
}
|
||||
|
||||
if (string.Equals(videoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||
@@ -1062,10 +1080,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Clients may direct play higher than level 41, but there's no reason to transcode higher.
|
||||
if (requestLevel >= 41)
|
||||
// Transcode to level 5.1 and lower for maximum compatibility.
|
||||
// h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4.
|
||||
// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
|
||||
if (requestLevel >= 51)
|
||||
{
|
||||
return "41";
|
||||
return "51";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1220,10 +1240,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
// Example: we encoded half of desired length, then codec detected
|
||||
// scene cut and inserted a keyframe; next forced keyframe would
|
||||
// be created outside of segment, which breaks seeking.
|
||||
// -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe.
|
||||
gopArg = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -g:v:0 {0} -keyint_min:v:0 {0} -sc_threshold:v:0 0",
|
||||
" -g:v:0 {0} -keyint_min:v:0 {0}",
|
||||
Math.Ceiling(segmentLength * framerate.Value));
|
||||
}
|
||||
|
||||
@@ -1243,6 +1262,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|| string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += keyFrameArg;
|
||||
|
||||
// prevent the libx264 from post processing to break the set keyframe.
|
||||
if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " -sc_threshold:v:0 0";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2213,13 +2238,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return state.IsInputVideo ? "-sn" : string.Empty;
|
||||
}
|
||||
|
||||
// We have media info, but we don't know the stream indexes
|
||||
// We have media info, but we don't know the stream index
|
||||
if (state.VideoStream != null && state.VideoStream.Index == -1)
|
||||
{
|
||||
return "-sn";
|
||||
}
|
||||
|
||||
// We have media info, but we don't know the stream indexes
|
||||
// We have media info, but we don't know the stream index
|
||||
if (state.AudioStream != null && state.AudioStream.Index == -1)
|
||||
{
|
||||
return state.IsInputVideo ? "-sn" : string.Empty;
|
||||
@@ -2229,10 +2254,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
if (state.VideoStream != null)
|
||||
{
|
||||
int videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream);
|
||||
|
||||
args += string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-map 0:{0}",
|
||||
state.VideoStream.Index);
|
||||
videoStreamIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2242,23 +2269,24 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
if (state.AudioStream != null)
|
||||
{
|
||||
int audioStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.AudioStream);
|
||||
if (state.AudioStream.IsExternal)
|
||||
{
|
||||
int externalAudioMapIndex = state.SubtitleStream != null && state.SubtitleStream.IsExternal ? 2 : 1;
|
||||
int externalAudioStream = state.MediaSource.MediaStreams.Where(i => i.Path == state.AudioStream.Path).ToList().IndexOf(state.AudioStream);
|
||||
bool hasExternalGraphicsSubs = state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream;
|
||||
int externalAudioMapIndex = hasExternalGraphicsSubs ? 2 : 1;
|
||||
|
||||
args += string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -map {0}:{1}",
|
||||
externalAudioMapIndex,
|
||||
externalAudioStream);
|
||||
audioStreamIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
args += string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -map 0:{0}",
|
||||
state.AudioStream.Index);
|
||||
audioStreamIndex);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -2273,14 +2301,21 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
else if (subtitleMethod == SubtitleDeliveryMethod.Embed)
|
||||
{
|
||||
int subtitleStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream);
|
||||
|
||||
args += string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -map 0:{0}",
|
||||
state.SubtitleStream.Index);
|
||||
subtitleStreamIndex);
|
||||
}
|
||||
else if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
|
||||
{
|
||||
args += " -map 1:0 -sn";
|
||||
int externalSubtitleStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream);
|
||||
|
||||
args += string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -map 1:{0} -sn",
|
||||
externalSubtitleStreamIndex);
|
||||
}
|
||||
|
||||
return args;
|
||||
@@ -2509,7 +2544,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/{2})*{2}:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2",
|
||||
"scale=trunc(min(max(iw\\,ih*a)\\,min({0}\\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\\,ih)\\,min({0}/a\\,{1}))/2)*2",
|
||||
maxWidthParam,
|
||||
maxHeightParam,
|
||||
scaleVal);
|
||||
@@ -2553,7 +2588,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"scale=trunc(min(max(iw\\,ih*dar)\\,{0})/{1})*{1}:trunc(ow/dar/2)*2",
|
||||
"scale=trunc(min(max(iw\\,ih*a)\\,{0})/{1})*{1}:trunc(ow/a/2)*2",
|
||||
maxWidthParam,
|
||||
scaleVal);
|
||||
}
|
||||
@@ -2565,7 +2600,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"scale=trunc(oh*a/{1})*{1}:min(max(iw/dar\\,ih)\\,{0})",
|
||||
"scale=trunc(oh*a/{1})*{1}:min(max(iw/a\\,ih)\\,{0})",
|
||||
maxHeightParam,
|
||||
scaleVal);
|
||||
}
|
||||
@@ -2614,7 +2649,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
else
|
||||
{
|
||||
filter = "scale={0}:trunc({0}/dar/2)*2";
|
||||
filter = "scale={0}:trunc({0}/a/2)*2";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2768,8 +2803,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
else if (hasGraphicalSubs)
|
||||
{
|
||||
// [0:s]scale=s=1280x720
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
// [0:s]scale=expr
|
||||
var subSwScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
|
||||
}
|
||||
@@ -2955,7 +2990,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var subSwScaleFilter = isSwDecoder
|
||||
? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
|
||||
: GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
|
||||
}
|
||||
@@ -3153,7 +3190,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var subSwScaleFilter = isSwDecoder
|
||||
? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
|
||||
: GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
|
||||
}
|
||||
@@ -3381,7 +3420,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
|
||||
// qsv requires a fixed pool size.
|
||||
subFilters.Add("hwupload=extra_hw_frames=32");
|
||||
// default to 64 otherwise it will fail on certain iGPU.
|
||||
subFilters.Add("hwupload=extra_hw_frames=64");
|
||||
|
||||
var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var overlaySize = (overlayW.HasValue && overlayH.HasValue)
|
||||
@@ -3398,7 +3438,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var subSwScaleFilter = isSwDecoder
|
||||
? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
|
||||
: GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0");
|
||||
}
|
||||
@@ -3589,7 +3631,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
|
||||
// qsv requires a fixed pool size.
|
||||
subFilters.Add("hwupload=extra_hw_frames=32");
|
||||
// default to 64 otherwise it will fail on certain iGPU.
|
||||
subFilters.Add("hwupload=extra_hw_frames=64");
|
||||
|
||||
var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var overlaySize = (overlayW.HasValue && overlayH.HasValue)
|
||||
@@ -3606,7 +3649,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var subSwScaleFilter = isSwDecoder
|
||||
? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
|
||||
: GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
|
||||
}
|
||||
@@ -3853,7 +3898,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var subSwScaleFilter = isSwDecoder
|
||||
? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
|
||||
: GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
|
||||
|
||||
@@ -4028,7 +4075,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var subSwScaleFilter = isSwDecoder
|
||||
? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH)
|
||||
: GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0");
|
||||
|
||||
@@ -4124,9 +4173,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
string.Join(',', overlayFilters));
|
||||
|
||||
var mapPrefix = Convert.ToInt32(state.SubtitleStream.IsExternal);
|
||||
var subtitleStreamIndex = state.SubtitleStream.IsExternal
|
||||
? 0
|
||||
: state.SubtitleStream.Index;
|
||||
var subtitleStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.SubtitleStream);
|
||||
var videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream);
|
||||
|
||||
if (hasSubs)
|
||||
{
|
||||
@@ -4147,7 +4195,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
filterStr,
|
||||
mapPrefix,
|
||||
subtitleStreamIndex,
|
||||
state.VideoStream.Index,
|
||||
videoStreamIndex,
|
||||
mainStr,
|
||||
subStr,
|
||||
overlayStr);
|
||||
@@ -4840,22 +4888,21 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions, string segmentContainer)
|
||||
{
|
||||
var inputModifier = string.Empty;
|
||||
var probeSizeArgument = string.Empty;
|
||||
var analyzeDurationArgument = string.Empty;
|
||||
|
||||
string analyzeDurationArgument;
|
||||
if (state.MediaSource.AnalyzeDurationMs.HasValue)
|
||||
// Apply -analyzeduration as per the environment variable,
|
||||
// otherwise ffmpeg will break on certain files due to default value is 0.
|
||||
// The default value of -probesize is more than enough, so leave it as is.
|
||||
var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
|
||||
|
||||
if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
|
||||
{
|
||||
analyzeDurationArgument = "-analyzeduration " + ffmpegAnalyzeDuration;
|
||||
}
|
||||
else if (state.MediaSource.AnalyzeDurationMs.HasValue)
|
||||
{
|
||||
analyzeDurationArgument = "-analyzeduration " + (state.MediaSource.AnalyzeDurationMs.Value * 1000).ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
analyzeDurationArgument = string.Empty;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(probeSizeArgument))
|
||||
{
|
||||
inputModifier += " " + probeSizeArgument;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(analyzeDurationArgument))
|
||||
{
|
||||
@@ -5357,12 +5404,22 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
// opus will fail on 44100
|
||||
if (!string.Equals(state.OutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (state.OutputAudioSampleRate.HasValue)
|
||||
// opus only supports specific sampling rates
|
||||
var sampleRate = state.OutputAudioSampleRate;
|
||||
if (sampleRate.HasValue)
|
||||
{
|
||||
audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture));
|
||||
var sampleRateValue = sampleRate.Value switch
|
||||
{
|
||||
<= 8000 => 8000,
|
||||
<= 12000 => 12000,
|
||||
<= 16000 => 16000,
|
||||
<= 24000 => 24000,
|
||||
_ => 48000
|
||||
};
|
||||
|
||||
audioTranscodeParams.Add("-ar " + sampleRateValue.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5384,6 +5441,28 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
string.Empty).Trim();
|
||||
}
|
||||
|
||||
public static int FindIndex(IReadOnlyList<MediaStream> mediaStreams, MediaStream streamToFind)
|
||||
{
|
||||
var index = 0;
|
||||
var length = mediaStreams.Count;
|
||||
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
var currentMediaStream = mediaStreams[i];
|
||||
if (currentMediaStream == streamToFind)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
|
||||
if (string.Equals(currentMediaStream.Path, streamToFind.Path, StringComparison.Ordinal))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static bool IsCopyCodec(string codec)
|
||||
{
|
||||
return string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
@@ -141,6 +141,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/// <returns>System.String.</returns>
|
||||
string GetInputArgument(string inputFile, MediaSourceInfo mediaSource);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the input argument for an external subtitle file.
|
||||
/// </summary>
|
||||
/// <param name="inputFile">The input file.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
string GetExternalSubtitleInputArgument(string inputFile);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the time parameter.
|
||||
/// </summary>
|
||||
|
||||
@@ -5,6 +5,11 @@ namespace MediaBrowser.Controller.Resolvers
|
||||
/// </summary>
|
||||
public enum ResolverPriority
|
||||
{
|
||||
/// <summary>
|
||||
/// The highest priority. Used by plugins to bypass the default server resolvers.
|
||||
/// </summary>
|
||||
Plugin = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The first.
|
||||
/// </summary>
|
||||
|
||||
@@ -352,6 +352,6 @@ namespace MediaBrowser.Controller.Session
|
||||
/// <returns>Task.</returns>
|
||||
Task RevokeUserTokens(Guid userId, string currentAccessToken);
|
||||
|
||||
void CloseIfNeeded(SessionInfo session);
|
||||
Task CloseIfNeededAsync(SessionInfo session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.MediaEncoding.Probing;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
@@ -49,6 +50,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
private readonly IServerConfigurationManager _configurationManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly string _startupOptionFFmpegPath;
|
||||
|
||||
private readonly SemaphoreSlim _thumbnailResourcePool = new SemaphoreSlim(2, 2);
|
||||
@@ -85,6 +87,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
_configurationManager = configurationManager;
|
||||
_fileSystem = fileSystem;
|
||||
_localization = localization;
|
||||
_config = config;
|
||||
_startupOptionFFmpegPath = config.GetValue<string>(Controller.Extensions.ConfigurationExtensions.FfmpegPathKey) ?? string.Empty;
|
||||
_jsonSerializerOptions = JsonDefaults.Options;
|
||||
}
|
||||
@@ -371,8 +374,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
var inputFile = request.MediaSource.Path;
|
||||
|
||||
string analyzeDuration = string.Empty;
|
||||
string ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
|
||||
|
||||
if (request.MediaSource.AnalyzeDurationMs > 0)
|
||||
if (!string.IsNullOrEmpty(ffmpegAnalyzeDuration))
|
||||
{
|
||||
analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
|
||||
}
|
||||
else if (request.MediaSource.AnalyzeDurationMs > 0)
|
||||
{
|
||||
analyzeDuration = "-analyzeduration " +
|
||||
(request.MediaSource.AnalyzeDurationMs * 1000).ToString();
|
||||
@@ -411,6 +419,19 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
return EncodingUtils.GetInputArgument(prefix, inputFile, mediaSource.Protocol);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the input argument for an external subtitle file.
|
||||
/// </summary>
|
||||
/// <param name="inputFile">The input file.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
/// <exception cref="ArgumentException">Unrecognized InputType.</exception>
|
||||
public string GetExternalSubtitleInputArgument(string inputFile)
|
||||
{
|
||||
const string Prefix = "file";
|
||||
|
||||
return EncodingUtils.GetInputArgument(Prefix, inputFile, MediaProtocol.File);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the media info internal.
|
||||
/// </summary>
|
||||
@@ -616,10 +637,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
filters.Add("thumbnail=n=" + (useLargerBatchSize ? "50" : "24"));
|
||||
}
|
||||
|
||||
// Use SW tonemap on HDR video stream only when the zscale filter is available.
|
||||
var enableHdrExtraction = string.Equals(videoStream?.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) && SupportsFilter("zscale");
|
||||
if (enableHdrExtraction)
|
||||
// Use SW tonemap on HDR10/HLG video stream only when the zscale filter is available.
|
||||
var enableHdrExtraction = false;
|
||||
|
||||
if ((string.Equals(videoStream?.ColorTransfer, "smpte2084", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoStream?.ColorTransfer, "arib-std-b67", StringComparison.OrdinalIgnoreCase))
|
||||
&& SupportsFilter("zscale"))
|
||||
{
|
||||
enableHdrExtraction = true;
|
||||
|
||||
filters.Add("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0:peak=100,zscale=t=bt709:m=bt709,format=yuv420p");
|
||||
}
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
MediaStream subtitleStream,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!subtitleStream.IsExternal)
|
||||
if (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string outputFormat;
|
||||
string outputCodec;
|
||||
@@ -224,7 +224,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
// Extract
|
||||
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFormat);
|
||||
|
||||
await ExtractTextSubtitle(mediaSource, subtitleStream.Index, outputCodec, outputPath, cancellationToken)
|
||||
await ExtractTextSubtitle(mediaSource, subtitleStream, outputCodec, outputPath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return new SubtitleInfo(outputPath, MediaProtocol.File, outputFormat, false);
|
||||
@@ -494,7 +494,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
/// Extracts the text subtitle.
|
||||
/// </summary>
|
||||
/// <param name="mediaSource">The mediaSource.</param>
|
||||
/// <param name="subtitleStreamIndex">Index of the subtitle stream.</param>
|
||||
/// <param name="subtitleStream">The subtitle stream.</param>
|
||||
/// <param name="outputCodec">The output codec.</param>
|
||||
/// <param name="outputPath">The output path.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
@@ -502,7 +502,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
/// <exception cref="ArgumentException">Must use inputPath list overload.</exception>
|
||||
private async Task ExtractTextSubtitle(
|
||||
MediaSourceInfo mediaSource,
|
||||
int subtitleStreamIndex,
|
||||
MediaStream subtitleStream,
|
||||
string outputCodec,
|
||||
string outputPath,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -511,12 +511,21 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
|
||||
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(outputPath))
|
||||
{
|
||||
var args = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource);
|
||||
|
||||
if (subtitleStream.IsExternal)
|
||||
{
|
||||
args = _mediaEncoder.GetExternalSubtitleInputArgument(subtitleStream.Path);
|
||||
}
|
||||
|
||||
await ExtractTextSubtitleInternal(
|
||||
_mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource),
|
||||
args,
|
||||
subtitleStreamIndex,
|
||||
outputCodec,
|
||||
outputPath,
|
||||
@@ -672,11 +681,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
if (!string.Equals(text, newText, StringComparison.Ordinal))
|
||||
{
|
||||
var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||
var writer = new StreamWriter(fileStream, encoding);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
await using (writer.ConfigureAwait(false))
|
||||
{
|
||||
await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false);
|
||||
var writer = new StreamWriter(fileStream, encoding);
|
||||
await using (writer.ConfigureAwait(false))
|
||||
{
|
||||
await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace MediaBrowser.Model.Configuration
|
||||
EnableHardwareEncoding = true;
|
||||
AllowHevcEncoding = false;
|
||||
EnableSubtitleExtraction = true;
|
||||
AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = Array.Empty<string>();
|
||||
AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" };
|
||||
HardwareDecodingCodecs = new string[] { "h264", "vc1" };
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace MediaBrowser.Model.Configuration
|
||||
RequirePerfectSubtitleMatch = true;
|
||||
AllowEmbeddedSubtitles = EmbeddedSubtitleOptions.AllowAll;
|
||||
|
||||
AutomaticallyAddToCollection = true;
|
||||
AutomaticallyAddToCollection = false;
|
||||
EnablePhotos = true;
|
||||
SaveSubtitlesWithMedia = true;
|
||||
EnableRealtimeMonitor = true;
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace MediaBrowser.Model.Configuration
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether quick connect is available for use on this server.
|
||||
/// </summary>
|
||||
public bool QuickConnectAvailable { get; set; } = false;
|
||||
public bool QuickConnectAvailable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [enable case sensitive item ids].
|
||||
|
||||
@@ -385,7 +385,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
// If device requirements are satisfied then allow both direct stream and direct play
|
||||
if (item.SupportsDirectPlay)
|
||||
{
|
||||
if (IsItemBitrateEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectPlay))
|
||||
if (IsItemBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectPlay))
|
||||
{
|
||||
if (options.EnableDirectPlay)
|
||||
{
|
||||
@@ -401,7 +401,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
// While options takes the network and other factors into account. Only applies to direct stream
|
||||
if (item.SupportsDirectStream)
|
||||
{
|
||||
if (IsItemBitrateEligibleForDirectPlay(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream))
|
||||
if (IsItemBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(true) ?? 0, PlayMethod.DirectStream))
|
||||
{
|
||||
if (options.EnableDirectStream)
|
||||
{
|
||||
@@ -604,11 +604,11 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
var videoStream = item.VideoStream;
|
||||
|
||||
var directPlayEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay);
|
||||
var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectStream);
|
||||
bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult == 0);
|
||||
bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directPlayEligibilityResult == 0);
|
||||
var transcodeReasons = directPlayEligibilityResult | directStreamEligibilityResult;
|
||||
var directPlayBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectPlay);
|
||||
var directStreamBitrateEligibility = IsBitrateEligibleForDirectPlayback(item, options.GetMaxBitrate(false) ?? 0, options, PlayMethod.DirectStream);
|
||||
bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayBitrateEligibility == 0);
|
||||
bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamBitrateEligibility == 0);
|
||||
var transcodeReasons = directPlayBitrateEligibility | directStreamBitrateEligibility;
|
||||
|
||||
_logger.LogDebug(
|
||||
"Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
|
||||
@@ -625,7 +625,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
var directPlay = directPlayInfo.PlayMethod;
|
||||
transcodeReasons |= directPlayInfo.TranscodeReasons;
|
||||
|
||||
if (directPlay != null)
|
||||
if (directPlay.HasValue)
|
||||
{
|
||||
directPlayProfile = directPlayInfo.Profile;
|
||||
playlistItem.PlayMethod = directPlay.Value;
|
||||
@@ -676,7 +676,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
playlistItem.TranscodeReasons = transcodeReasons;
|
||||
|
||||
if (playlistItem.PlayMethod != PlayMethod.DirectStream || !options.EnableDirectStream)
|
||||
if (playlistItem.PlayMethod != PlayMethod.DirectStream && playlistItem.PlayMethod != PlayMethod.DirectPlay)
|
||||
{
|
||||
// Can't direct play, find the transcoding profile
|
||||
// If we do this for direct-stream we will overwrite the info
|
||||
@@ -687,6 +687,8 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
BuildStreamVideoItem(playlistItem, options, item, videoStream, audioStream, candidateAudioStreams, transcodingProfile.Container, transcodingProfile.VideoCodec, transcodingProfile.AudioCodec);
|
||||
|
||||
playlistItem.PlayMethod = PlayMethod.Transcode;
|
||||
|
||||
if (subtitleStream != null)
|
||||
{
|
||||
var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Container, transcodingProfile.Protocol);
|
||||
@@ -696,14 +698,9 @@ namespace MediaBrowser.Model.Dlna
|
||||
playlistItem.SubtitleCodecs = new[] { subtitleProfile.Format };
|
||||
}
|
||||
|
||||
if (playlistItem.PlayMethod != PlayMethod.DirectPlay)
|
||||
if ((playlistItem.TranscodeReasons & (VideoReasons | TranscodeReason.ContainerBitrateExceedsLimit)) != 0)
|
||||
{
|
||||
playlistItem.PlayMethod = PlayMethod.Transcode;
|
||||
|
||||
if ((playlistItem.TranscodeReasons & (VideoReasons | TranscodeReason.ContainerBitrateExceedsLimit)) != 0)
|
||||
{
|
||||
ApplyTranscodingConditions(playlistItem, transcodingProfile.Conditions, null, true, true);
|
||||
}
|
||||
ApplyTranscodingConditions(playlistItem, transcodingProfile.Conditions, null, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -744,16 +741,19 @@ namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
var videoCodecs = ContainerProfile.SplitValue(transcodingProfile.VideoCodec);
|
||||
|
||||
if (ContainerProfile.ContainsContainer(videoCodecs, item.VideoStream.Codec))
|
||||
if (ContainerProfile.ContainsContainer(videoCodecs, item.VideoStream?.Codec))
|
||||
{
|
||||
var videoCodec = transcodingProfile.VideoCodec;
|
||||
var container = transcodingProfile.Container;
|
||||
var appliedVideoConditions = options.Profile.CodecProfiles
|
||||
.Where(i => i.Type == CodecType.Video &&
|
||||
i.ContainsAnyCodec(videoCodec, container))
|
||||
i.ContainsAnyCodec(videoCodec, container) &&
|
||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC)))
|
||||
.Select(i =>
|
||||
i.ApplyConditions.Any(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC)));
|
||||
var conditionsSatisfied = !appliedVideoConditions.Any() || !appliedVideoConditions.Any(satisfied => !satisfied);
|
||||
i.Conditions.All(condition => ConditionProcessor.IsVideoConditionSatisfied(condition, videoStream?.Width, videoStream?.Height, videoStream?.BitDepth, videoStream?.BitRate, videoStream?.Profile, videoStream?.Level, videoFramerate, videoStream?.PacketLength, timestamp, videoStream?.IsAnamorphic, videoStream?.IsInterlaced, videoStream?.RefFrames, numVideoStreams, numAudioStreams, videoStream?.CodecTag, videoStream?.IsAVC)));
|
||||
|
||||
// An empty appliedVideoConditions means that the codec has no conditions for the current video stream
|
||||
var conditionsSatisfied = appliedVideoConditions.All(satisfied => satisfied);
|
||||
return conditionsSatisfied ? 1 : 2;
|
||||
}
|
||||
|
||||
@@ -768,35 +768,42 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
private void BuildStreamVideoItem(StreamInfo playlistItem, VideoOptions options, MediaSourceInfo item, MediaStream videoStream, MediaStream audioStream, IEnumerable<MediaStream> candidateAudioStreams, string container, string videoCodec, string audioCodec)
|
||||
{
|
||||
// prefer matching video codecs
|
||||
// Prefer matching video codecs
|
||||
var videoCodecs = ContainerProfile.SplitValue(videoCodec);
|
||||
var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream.Codec) ? videoStream.Codec : null;
|
||||
playlistItem.VideoCodecs = directVideoCodec != null ? new[] { directVideoCodec } : videoCodecs;
|
||||
var directVideoCodec = ContainerProfile.ContainsContainer(videoCodecs, videoStream?.Codec) ? videoStream?.Codec : null;
|
||||
if (directVideoCodec != null)
|
||||
{
|
||||
// merge directVideoCodec to videoCodecs
|
||||
Array.Resize(ref videoCodecs, videoCodecs.Length + 1);
|
||||
videoCodecs[^1] = directVideoCodec;
|
||||
}
|
||||
|
||||
// copy video codec options as a starting point, this applies to transcode and direct-stream
|
||||
playlistItem.MaxFramerate = videoStream.AverageFrameRate;
|
||||
var qualifier = videoStream.Codec;
|
||||
if (videoStream.Level.HasValue)
|
||||
playlistItem.VideoCodecs = videoCodecs;
|
||||
|
||||
// Copy video codec options as a starting point, this applies to transcode and direct-stream
|
||||
playlistItem.MaxFramerate = videoStream?.AverageFrameRate;
|
||||
var qualifier = videoStream?.Codec;
|
||||
if (videoStream?.Level != null)
|
||||
{
|
||||
playlistItem.SetOption(qualifier, "level", videoStream.Level.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
if (videoStream.BitDepth.HasValue)
|
||||
if (videoStream?.BitDepth != null)
|
||||
{
|
||||
playlistItem.SetOption(qualifier, "videobitdepth", videoStream.BitDepth.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(videoStream.Profile))
|
||||
if (!string.IsNullOrEmpty(videoStream?.Profile))
|
||||
{
|
||||
playlistItem.SetOption(qualifier, "profile", videoStream.Profile.ToLowerInvariant());
|
||||
}
|
||||
|
||||
if (videoStream.Level != 0)
|
||||
if (videoStream != null && videoStream.Level != 0)
|
||||
{
|
||||
playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString());
|
||||
}
|
||||
|
||||
// prefer matching audio codecs, could do better here
|
||||
// Prefer matching audio codecs, could do better here
|
||||
var audioCodecs = ContainerProfile.SplitValue(audioCodec);
|
||||
var directAudioStream = candidateAudioStreams.FirstOrDefault(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec));
|
||||
playlistItem.AudioCodecs = audioCodecs;
|
||||
@@ -806,7 +813,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
playlistItem.AudioStreamIndex = audioStream.Index;
|
||||
playlistItem.AudioCodecs = new[] { audioStream.Codec };
|
||||
|
||||
// copy matching audio codec options
|
||||
// Copy matching audio codec options
|
||||
playlistItem.AudioSampleRate = audioStream.SampleRate;
|
||||
playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString());
|
||||
|
||||
@@ -843,7 +850,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
var appliedVideoConditions = options.Profile.CodecProfiles
|
||||
.Where(i => i.Type == CodecType.Video &&
|
||||
i.ContainsAnyCodec(videoCodec, container) &&
|
||||
i.ApplyConditions.Any(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)));
|
||||
i.ApplyConditions.All(applyCondition => ConditionProcessor.IsVideoConditionSatisfied(applyCondition, width, height, bitDepth, videoBitrate, videoProfile, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)));
|
||||
var isFirstAppliedCodecProfile = true;
|
||||
foreach (var i in appliedVideoConditions)
|
||||
{
|
||||
@@ -873,9 +880,9 @@ namespace MediaBrowser.Model.Dlna
|
||||
int? inputAudioBitDepth = audioStream == null ? null : audioStream.BitDepth;
|
||||
|
||||
var appliedAudioConditions = options.Profile.CodecProfiles
|
||||
.Where(i => i.Type == CodecType.Video &&
|
||||
.Where(i => i.Type == CodecType.VideoAudio &&
|
||||
i.ContainsAnyCodec(audioCodec, container) &&
|
||||
i.ApplyConditions.Any(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;
|
||||
foreach (var i in appliedAudioConditions)
|
||||
{
|
||||
@@ -1067,7 +1074,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
DeviceProfile profile = options.Profile;
|
||||
string container = mediaSource.Container;
|
||||
|
||||
// video
|
||||
// Video
|
||||
int? width = videoStream?.Width;
|
||||
int? height = videoStream?.Height;
|
||||
int? bitDepth = videoStream?.BitDepth;
|
||||
@@ -1079,7 +1086,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
bool? isInterlaced = videoStream?.IsInterlaced;
|
||||
string videoCodecTag = videoStream?.CodecTag;
|
||||
bool? isAvc = videoStream?.IsAVC;
|
||||
// audio
|
||||
// Audio
|
||||
var defaultLanguage = audioStream?.Language ?? string.Empty;
|
||||
var defaultMarked = audioStream?.IsDefault ?? false;
|
||||
|
||||
@@ -1108,18 +1115,9 @@ namespace MediaBrowser.Model.Dlna
|
||||
profile,
|
||||
"VideoCodecProfile",
|
||||
profile.CodecProfiles
|
||||
.Where(codecProfile => codecProfile.Type == CodecType.Video && codecProfile.ContainsAnyCodec(videoStream?.Codec, container))
|
||||
.SelectMany(codecProfile =>
|
||||
{
|
||||
var failedApplyConditions = checkVideoConditions(codecProfile.ApplyConditions);
|
||||
if (!failedApplyConditions.Any())
|
||||
{
|
||||
return Array.Empty<ProfileCondition>();
|
||||
}
|
||||
|
||||
var failedConditions = checkVideoConditions(codecProfile.Conditions);
|
||||
return failedApplyConditions.Concat(failedConditions);
|
||||
}));
|
||||
.Where(codecProfile => codecProfile.Type == CodecType.Video && codecProfile.ContainsAnyCodec(videoStream?.Codec, container) &&
|
||||
!checkVideoConditions(codecProfile.ApplyConditions).Any())
|
||||
.SelectMany(codecProfile => checkVideoConditions(codecProfile.Conditions)));
|
||||
|
||||
// Check audiocandidates profile conditions
|
||||
var audioStreamMatches = candidateAudioStreams.ToDictionary(s => s, audioStream => CheckVideoAudioStreamDirectPlay(options, mediaSource, container, audioStream, defaultLanguage, defaultMarked));
|
||||
@@ -1189,7 +1187,18 @@ namespace MediaBrowser.Model.Dlna
|
||||
audioCodecProfileReasons = audioStreamMatches.GetValueOrDefault(selectedAudioStream);
|
||||
}
|
||||
|
||||
var failureReasons = directPlayProfileReasons | containerProfileReasons | videoCodecProfileReasons | audioCodecProfileReasons | subtitleProfileReasons;
|
||||
var failureReasons = directPlayProfileReasons | containerProfileReasons | subtitleProfileReasons;
|
||||
|
||||
if ((failureReasons & TranscodeReason.VideoCodecNotSupported) == 0)
|
||||
{
|
||||
failureReasons |= videoCodecProfileReasons;
|
||||
}
|
||||
|
||||
if ((failureReasons & TranscodeReason.AudioCodecNotSupported) == 0)
|
||||
{
|
||||
failureReasons |= audioCodecProfileReasons;
|
||||
}
|
||||
|
||||
var directStreamFailureReasons = failureReasons & (~DirectStreamReasons);
|
||||
|
||||
PlayMethod? playMethod = null;
|
||||
@@ -1206,6 +1215,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
return (Result: (Profile: directPlayProfile, PlayMethod: playMethod, AudioStreamIndex: selectedAudioStream?.Index, TranscodeReason: failureReasons), Order: order, Rank: ranked);
|
||||
})
|
||||
.OrderByDescending(analysis => analysis.Result.PlayMethod)
|
||||
.ThenByDescending(analysis => analysis.Rank)
|
||||
.ThenBy(analysis => analysis.Order)
|
||||
.ToArray()
|
||||
.ToLookup(analysis => analysis.Result.PlayMethod != null);
|
||||
@@ -1218,7 +1228,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
return profileMatch;
|
||||
}
|
||||
|
||||
var failureReasons = analyzedProfiles[false].OrderBy(a => a.Result.TranscodeReason).ThenBy(analysis => analysis.Order).FirstOrDefault().Result.TranscodeReason;
|
||||
var failureReasons = analyzedProfiles[false].Select(analysis => analysis.Result).FirstOrDefault().TranscodeReason;
|
||||
if (failureReasons == 0)
|
||||
{
|
||||
failureReasons = TranscodeReason.DirectPlayError;
|
||||
@@ -1264,13 +1274,13 @@ namespace MediaBrowser.Model.Dlna
|
||||
mediaSource.Path ?? "Unknown path");
|
||||
}
|
||||
|
||||
private TranscodeReason IsEligibleForDirectPlay(
|
||||
private TranscodeReason IsBitrateEligibleForDirectPlayback(
|
||||
MediaSourceInfo item,
|
||||
long maxBitrate,
|
||||
VideoOptions options,
|
||||
PlayMethod playMethod)
|
||||
{
|
||||
bool result = IsItemBitrateEligibleForDirectPlay(item, maxBitrate, playMethod);
|
||||
bool result = IsItemBitrateEligibleForDirectPlayback(item, maxBitrate, playMethod);
|
||||
if (!result)
|
||||
{
|
||||
return TranscodeReason.ContainerBitrateExceedsLimit;
|
||||
@@ -1438,7 +1448,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsItemBitrateEligibleForDirectPlay(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod)
|
||||
private bool IsItemBitrateEligibleForDirectPlayback(MediaSourceInfo item, long maxBitrate, PlayMethod playMethod)
|
||||
{
|
||||
// Don't restrict by bitrate if coming from an external domain
|
||||
if (item.IsRemote)
|
||||
|
||||
@@ -121,8 +121,7 @@ namespace MediaBrowser.Model.Entities
|
||||
|
||||
var codecTag = CodecTag;
|
||||
|
||||
if (string.Equals(codecTag, "dva1", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codecTag, "dvav", StringComparison.OrdinalIgnoreCase)
|
||||
if (string.Equals(codecTag, "dovi", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codecTag, "dvh1", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codecTag, "dvhe", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codecTag, "dav1", StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Model.Entities
|
||||
{
|
||||
@@ -9,6 +10,16 @@ namespace MediaBrowser.Model.Entities
|
||||
/// </summary>
|
||||
public static class ProviderIdsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Case insensitive dictionary of <see cref="MetadataProvider"/> string representation.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, string> _metadataProviderEnumDictionary =
|
||||
Enum.GetValues<MetadataProvider>()
|
||||
.ToDictionary(
|
||||
enumValue => enumValue.ToString(),
|
||||
enumValue => enumValue.ToString(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if this instance has an id for the given provider.
|
||||
/// </summary>
|
||||
@@ -108,7 +119,7 @@ namespace MediaBrowser.Model.Entities
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
public static void SetProviderId(this IHasProviderIds instance, string name, string value)
|
||||
public static void SetProviderId(this IHasProviderIds instance, string name, string? value)
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
@@ -125,7 +136,15 @@ namespace MediaBrowser.Model.Entities
|
||||
// Ensure it exists
|
||||
instance.ProviderIds ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
instance.ProviderIds[name] = value;
|
||||
// Match on internal MetadataProvider enum string values before adding arbitrary providers
|
||||
if (_metadataProviderEnumDictionary.TryGetValue(name, out var enumValue))
|
||||
{
|
||||
instance.ProviderIds[enumValue] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
instance.ProviderIds[name] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,12 +35,12 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
|
||||
<PackageReference Include="MimeTypes" Version="2.3.0">
|
||||
<PackageReference Include="MimeTypes" Version="2.4.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Globalization" Version="4.3.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.2" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -64,5 +64,11 @@ namespace MediaBrowser.Model.Session
|
||||
/// </summary>
|
||||
/// <value>The repeat mode.</value>
|
||||
public RepeatMode RepeatMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the now playing live stream identifier.
|
||||
/// </summary>
|
||||
/// <value>The live stream identifier.</value>
|
||||
public string LiveStreamId { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
|
||||
@@ -30,6 +30,7 @@ using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Priority_Queue;
|
||||
@@ -188,6 +189,12 @@ namespace MediaBrowser.Providers.Manager
|
||||
throw new HttpRequestException("Invalid image received.", null, HttpStatusCode.NotFound);
|
||||
}
|
||||
|
||||
// some iptv/epg providers don't correctly report media type, extract from url if no extension found
|
||||
if (string.IsNullOrWhiteSpace(MimeTypes.ToExtension(contentType)))
|
||||
{
|
||||
contentType = MimeTypes.GetMimeType(url);
|
||||
}
|
||||
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await SaveImage(
|
||||
item,
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" />
|
||||
<PackageReference Include="PlaylistsNET" Version="1.1.3" />
|
||||
<PackageReference Include="TMDbLib" Version="1.9.1" />
|
||||
<PackageReference Include="OptimizedPriorityQueue" Version="5.1.0" />
|
||||
<PackageReference Include="PlaylistsNET" Version="1.2.1" />
|
||||
<PackageReference Include="TMDbLib" Version="1.9.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -173,16 +173,30 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
IReadOnlyList<MediaAttachment> mediaAttachments;
|
||||
ChapterInfo[] chapters;
|
||||
|
||||
mediaStreams = new List<MediaStream>();
|
||||
|
||||
// Add external streams before adding the streams from the file to preserve stream IDs on remote videos
|
||||
await AddExternalSubtitlesAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await AddExternalAudioAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var startIndex = mediaStreams.Count == 0 ? 0 : (mediaStreams.Max(i => i.Index) + 1);
|
||||
|
||||
if (mediaInfo != null)
|
||||
{
|
||||
mediaStreams = mediaInfo.MediaStreams.ToList();
|
||||
foreach (var mediaStream in mediaInfo.MediaStreams)
|
||||
{
|
||||
mediaStream.Index = startIndex++;
|
||||
mediaStreams.Add(mediaStream);
|
||||
}
|
||||
|
||||
mediaAttachments = mediaInfo.MediaAttachments;
|
||||
|
||||
video.TotalBitrate = mediaInfo.Bitrate;
|
||||
// video.FormatName = (mediaInfo.Container ?? string.Empty)
|
||||
// .Replace("matroska", "mkv", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// For dvd's this may not always be accurate, so don't set the runtime if the item already has one
|
||||
// For DVDs this may not always be accurate, so don't set the runtime if the item already has one
|
||||
var needToSetRuntime = video.VideoType != VideoType.Dvd || video.RunTimeTicks == null || video.RunTimeTicks.Value == 0;
|
||||
|
||||
if (needToSetRuntime)
|
||||
@@ -213,15 +227,20 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
}
|
||||
else
|
||||
{
|
||||
mediaStreams = new List<MediaStream>();
|
||||
var currentMediaStreams = video.GetMediaStreams();
|
||||
foreach (var mediaStream in currentMediaStreams)
|
||||
{
|
||||
if (!mediaStream.IsExternal)
|
||||
{
|
||||
mediaStream.Index = startIndex++;
|
||||
mediaStreams.Add(mediaStream);
|
||||
}
|
||||
}
|
||||
|
||||
mediaAttachments = Array.Empty<MediaAttachment>();
|
||||
chapters = Array.Empty<ChapterInfo>();
|
||||
}
|
||||
|
||||
await AddExternalSubtitlesAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await AddExternalAudioAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var libraryOptions = _libraryManager.GetLibraryOptions(video);
|
||||
|
||||
if (mediaInfo != null)
|
||||
@@ -254,7 +273,11 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
video.HasSubtitles = mediaStreams.Any(i => i.Type == MediaStreamType.Subtitle);
|
||||
|
||||
_itemRepo.SaveMediaStreams(video.Id, mediaStreams, cancellationToken);
|
||||
_itemRepo.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken);
|
||||
|
||||
if (mediaAttachments.Any())
|
||||
{
|
||||
_itemRepo.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken);
|
||||
}
|
||||
|
||||
if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh ||
|
||||
options.MetadataRefreshMode == MetadataRefreshMode.Default)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -107,19 +106,28 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
if (mediaInfo.MediaStreams.Count == 1)
|
||||
{
|
||||
MediaStream mediaStream = mediaInfo.MediaStreams[0];
|
||||
mediaStream.Index = startIndex++;
|
||||
mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault;
|
||||
mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced;
|
||||
|
||||
mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
|
||||
if ((mediaStream.Type == MediaStreamType.Audio && _type == DlnaProfileType.Audio)
|
||||
|| (mediaStream.Type == MediaStreamType.Subtitle && _type == DlnaProfileType.Subtitle))
|
||||
{
|
||||
mediaStream.Index = startIndex++;
|
||||
mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault;
|
||||
mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced;
|
||||
|
||||
mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (MediaStream mediaStream in mediaInfo.MediaStreams)
|
||||
{
|
||||
mediaStream.Index = startIndex++;
|
||||
if ((mediaStream.Type == MediaStreamType.Audio && _type == DlnaProfileType.Audio)
|
||||
|| (mediaStream.Type == MediaStreamType.Subtitle && _type == DlnaProfileType.Subtitle))
|
||||
{
|
||||
mediaStream.Index = startIndex++;
|
||||
|
||||
mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
|
||||
mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,6 +161,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
}
|
||||
|
||||
var files = directoryService.GetFilePaths(folder, clearCache).ToList();
|
||||
files.Remove(video.Path);
|
||||
var internalMetadataPath = video.GetInternalMetadataPath();
|
||||
if (_fileSystem.DirectoryExists(internalMetadataPath))
|
||||
{
|
||||
@@ -222,13 +231,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
mediaStream.Title = string.IsNullOrEmpty(mediaStream.Title) ? (string.IsNullOrEmpty(pathInfo.Title) ? null : pathInfo.Title) : mediaStream.Title;
|
||||
mediaStream.Language = string.IsNullOrEmpty(mediaStream.Language) ? (string.IsNullOrEmpty(pathInfo.Language) ? null : pathInfo.Language) : mediaStream.Language;
|
||||
|
||||
mediaStream.Type = _type switch
|
||||
{
|
||||
DlnaProfileType.Audio => MediaStreamType.Audio,
|
||||
DlnaProfileType.Subtitle => MediaStreamType.Subtitle,
|
||||
_ => mediaStream.Type
|
||||
};
|
||||
|
||||
return mediaStream;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
using MediaBrowser.Model.Plugins;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.StudioImages
|
||||
namespace MediaBrowser.Providers.Plugins.StudioImages.Configuration
|
||||
{
|
||||
public class PluginConfiguration : BasePluginConfiguration
|
||||
{
|
||||
@@ -12,12 +12,19 @@ namespace MediaBrowser.Providers.Plugins.StudioImages
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(_repository))
|
||||
{
|
||||
_repository = Plugin.DefaultServer;
|
||||
}
|
||||
|
||||
return _repository;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_repository = value.TrimEnd('/');
|
||||
_repository = string.IsNullOrEmpty(value)
|
||||
? Plugin.DefaultServer
|
||||
: value.TrimEnd('/');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<div class="content-primary">
|
||||
<form class="configForm">
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="text" id="repository" required label="Repository" />
|
||||
<div class="fieldDescription">This can be any Jellyfin-compatible artwork repository.</div>
|
||||
<input is="emby-input" type="text" id="repository" label="Repository" />
|
||||
<div class="fieldDescription">This can be any Jellyfin-compatible artwork repository. Leave blank to use default repository.</div>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
@@ -44,7 +44,7 @@
|
||||
Dashboard.showLoadingMsg();
|
||||
|
||||
ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
|
||||
config.RepositoryUrl = document.querySelector('#server').value;
|
||||
config.RepositoryUrl = document.querySelector('#repository').value;
|
||||
|
||||
ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Providers.Plugins.StudioImages.Configuration;
|
||||
|
||||
namespace MediaBrowser.Providers.Plugins.StudioImages
|
||||
{
|
||||
|
||||
@@ -18,29 +18,24 @@ using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using MediaBrowser.Providers.Plugins.StudioImages;
|
||||
|
||||
namespace MediaBrowser.Providers.Studios
|
||||
namespace MediaBrowser.Providers.Plugins.StudioImages
|
||||
{
|
||||
public class StudiosImageProvider : IRemoteImageProvider
|
||||
{
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly string repositoryUrl;
|
||||
|
||||
public StudiosImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory, IFileSystem fileSystem)
|
||||
{
|
||||
_config = config;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_fileSystem = fileSystem;
|
||||
repositoryUrl = Plugin.Instance.Configuration.RepositoryUrl;
|
||||
}
|
||||
|
||||
public string Name => "Artwork Repository";
|
||||
|
||||
public int Order => 0;
|
||||
|
||||
public bool Supports(BaseItem item)
|
||||
{
|
||||
return item is Studio;
|
||||
@@ -98,12 +93,12 @@ namespace MediaBrowser.Providers.Studios
|
||||
|
||||
private string GetUrl(string image, string filename)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0}/images/{1}/{2}.jpg", repositoryUrl, image, filename);
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0}/images/{1}/{2}.jpg", GetRepositoryUrl(), image, filename);
|
||||
}
|
||||
|
||||
private Task<string> EnsureThumbsList(string file, CancellationToken cancellationToken)
|
||||
{
|
||||
string url = string.Format(CultureInfo.InvariantCulture, "{0}/thumbs.txt", repositoryUrl);
|
||||
string url = string.Format(CultureInfo.InvariantCulture, "{0}/thumbs.txt", GetRepositoryUrl());
|
||||
|
||||
return EnsureList(url, file, _fileSystem, cancellationToken);
|
||||
}
|
||||
@@ -169,5 +164,8 @@ namespace MediaBrowser.Providers.Studios
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetRepositoryUrl()
|
||||
=> Plugin.Instance.Configuration.RepositoryUrl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,8 +84,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
|
||||
public static bool IsTrailerType(Video video)
|
||||
{
|
||||
return video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase)
|
||||
&& (!video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase)
|
||||
|| !video.Type.Equals("teaser", StringComparison.OrdinalIgnoreCase));
|
||||
&& (video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase)
|
||||
|| video.Type.Equals("teaser", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
{
|
||||
var formatString = ConfigurationManager.GetNfoConfiguration().ReleaseDateFormat;
|
||||
|
||||
writer.WriteElementString("disbanded", artist.EndDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture));
|
||||
writer.WriteElementString("disbanded", artist.EndDate.Value.ToString(formatString, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
var albums = artist
|
||||
|
||||
@@ -473,7 +473,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
writer.WriteElementString("lockedfields", string.Join('|', item.LockedFields));
|
||||
}
|
||||
|
||||
writer.WriteElementString("dateadded", item.DateCreated.ToLocalTime().ToString(DateAddedFormat, CultureInfo.InvariantCulture));
|
||||
writer.WriteElementString("dateadded", item.DateCreated.ToString(DateAddedFormat, CultureInfo.InvariantCulture));
|
||||
|
||||
writer.WriteElementString("title", item.Name ?? string.Empty);
|
||||
|
||||
@@ -601,16 +601,16 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
{
|
||||
writer.WriteElementString(
|
||||
"formed",
|
||||
item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture));
|
||||
item.PremiereDate.Value.ToString(formatString, CultureInfo.InvariantCulture));
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteElementString(
|
||||
"premiered",
|
||||
item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture));
|
||||
item.PremiereDate.Value.ToString(formatString, CultureInfo.InvariantCulture));
|
||||
writer.WriteElementString(
|
||||
"releasedate",
|
||||
item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture));
|
||||
item.PremiereDate.Value.ToString(formatString, CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,7 +622,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
|
||||
writer.WriteElementString(
|
||||
"enddate",
|
||||
item.EndDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture));
|
||||
item.EndDate.Value.ToString(formatString, CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -891,7 +891,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
{
|
||||
writer.WriteElementString(
|
||||
"lastplayed",
|
||||
userdata.LastPlayedDate.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture).ToLowerInvariant());
|
||||
userdata.LastPlayedDate.Value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture).ToLowerInvariant());
|
||||
}
|
||||
|
||||
writer.WriteStartElement("resume");
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
{
|
||||
var formatString = ConfigurationManager.GetNfoConfiguration().ReleaseDateFormat;
|
||||
|
||||
writer.WriteElementString("aired", episode.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture));
|
||||
writer.WriteElementString("aired", episode.PremiereDate.Value.ToString(formatString, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
if (!episode.ParentIndexNumber.HasValue || episode.ParentIndexNumber.Value == 0)
|
||||
|
||||
4
debian/changelog
vendored
4
debian/changelog
vendored
@@ -1,8 +1,8 @@
|
||||
jellyfin-server (10.8.0-1) unstable; urgency=medium
|
||||
|
||||
* Forthcoming stable release
|
||||
* New upstream version 10.8.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.0
|
||||
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 04 Dec 2020 21:55:12 -0500
|
||||
-- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 10 Jun 2022 22:15:12 -0400
|
||||
|
||||
jellyfin-server (10.7.0-1) unstable; urgency=medium
|
||||
|
||||
|
||||
2
debian/control
vendored
2
debian/control
vendored
@@ -22,7 +22,7 @@ Depends: at,
|
||||
libsqlite3-0,
|
||||
libfontconfig1,
|
||||
libfreetype6,
|
||||
libssl1.1
|
||||
libssl1.1 | libssl3
|
||||
Recommends: jellyfin-web, sudo
|
||||
Description: Jellyfin is the Free Software Media System.
|
||||
This package provides the Jellyfin server backend and API.
|
||||
|
||||
2
debian/metapackage/jellyfin
vendored
2
debian/metapackage/jellyfin
vendored
@@ -5,7 +5,7 @@ Homepage: https://jellyfin.org
|
||||
Standards-Version: 3.9.2
|
||||
|
||||
Package: jellyfin
|
||||
Version: 10.8.0
|
||||
Version: 10.8.0~beta3
|
||||
Maintainer: Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
Depends: jellyfin-server, jellyfin-web
|
||||
Description: Provides the Jellyfin Free Software Media System
|
||||
|
||||
@@ -13,7 +13,7 @@ RUN yum update -yq \
|
||||
&& yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget
|
||||
|
||||
# Install DotNET SDK
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c505a449-9ecf-4352-8629-56216f521616/bd6807340faae05b61de340c8bf161e8/dotnet-sdk-6.0.201-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/dc930bff-ef3d-4f6f-8799-6eb60390f5b4/1efee2a8ea0180c94aff8f15eb3af981/dotnet-sdk-6.0.300-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -12,7 +12,7 @@ RUN dnf update -yq \
|
||||
&& dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget
|
||||
|
||||
# Install DotNET SDK
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c505a449-9ecf-4352-8629-56216f521616/bd6807340faae05b61de340c8bf161e8/dotnet-sdk-6.0.201-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/dc930bff-ef3d-4f6f-8799-6eb60390f5b4/1efee2a8ea0180c94aff8f15eb3af981/dotnet-sdk-6.0.300-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -17,7 +17,7 @@ RUN apt-get update -yqq \
|
||||
libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0
|
||||
|
||||
# Install dotnet repository
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c505a449-9ecf-4352-8629-56216f521616/bd6807340faae05b61de340c8bf161e8/dotnet-sdk-6.0.201-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/dc930bff-ef3d-4f6f-8799-6eb60390f5b4/1efee2a8ea0180c94aff8f15eb3af981/dotnet-sdk-6.0.300-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
|
||||
mmv build-essential lsb-release
|
||||
|
||||
# Install dotnet repository
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c505a449-9ecf-4352-8629-56216f521616/bd6807340faae05b61de340c8bf161e8/dotnet-sdk-6.0.201-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/dc930bff-ef3d-4f6f-8799-6eb60390f5b4/1efee2a8ea0180c94aff8f15eb3af981/dotnet-sdk-6.0.300-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN apt-get update -yqq \
|
||||
mmv build-essential lsb-release
|
||||
|
||||
# Install dotnet repository
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/c505a449-9ecf-4352-8629-56216f521616/bd6807340faae05b61de340c8bf161e8/dotnet-sdk-6.0.201-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
RUN wget -q https://download.visualstudio.microsoft.com/download/pr/dc930bff-ef3d-4f6f-8799-6eb60390f5b4/1efee2a8ea0180c94aff8f15eb3af981/dotnet-sdk-6.0.300-linux-x64.tar.gz -O dotnet-sdk.tar.gz \
|
||||
&& mkdir -p dotnet-sdk \
|
||||
&& tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \
|
||||
&& ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet
|
||||
|
||||
@@ -153,6 +153,8 @@ fi
|
||||
%systemd_postun_with_restart jellyfin.service
|
||||
|
||||
%changelog
|
||||
* Fri Jun 10 2022 Jellyfin Packaging Team <packaging@jellyfin.org>
|
||||
- New upstream version 10.8.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.8.0
|
||||
* Mon Nov 29 2021 Brian J. Murrell <brian@interlinx.bc.ca>
|
||||
- Add jellyfin-server-lowports.service drop-in in a server-lowports
|
||||
subpackage to allow binding to low ports
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Jellyfin.Extensions
|
||||
{
|
||||
@@ -7,6 +11,44 @@ namespace Jellyfin.Extensions
|
||||
/// </summary>
|
||||
public static class StringExtensions
|
||||
{
|
||||
// Matches non-conforming unicode chars
|
||||
// https://mnaoumov.wordpress.com/2014/06/14/stripping-invalid-characters-from-utf-16-strings/
|
||||
private static readonly Regex _nonConformingUnicode = new Regex("([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])|(\ufffd)");
|
||||
|
||||
/// <summary>
|
||||
/// Removes the diacritics character from the strings.
|
||||
/// </summary>
|
||||
/// <param name="text">The string to act on.</param>
|
||||
/// <returns>The string without diacritics character.</returns>
|
||||
public static string RemoveDiacritics(this string text)
|
||||
{
|
||||
string withDiactritics = _nonConformingUnicode
|
||||
.Replace(text, string.Empty)
|
||||
.Normalize(NormalizationForm.FormD);
|
||||
|
||||
var withoutDiactritics = new StringBuilder();
|
||||
foreach (char c in withDiactritics)
|
||||
{
|
||||
UnicodeCategory uc = CharUnicodeInfo.GetUnicodeCategory(c);
|
||||
if (uc != UnicodeCategory.NonSpacingMark)
|
||||
{
|
||||
withoutDiactritics.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return withoutDiactritics.ToString().Normalize(NormalizationForm.FormC);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks wether or not the specified string has diacritics in it.
|
||||
/// </summary>
|
||||
/// <param name="text">The string to check.</param>
|
||||
/// <returns>True if the string has diacritics, false otherwise.</returns>
|
||||
public static bool HasDiacritics(this string text)
|
||||
{
|
||||
return !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Counts the number of occurrences of [needle] in the string.
|
||||
/// </summary>
|
||||
|
||||
@@ -15,13 +15,16 @@
|
||||
<PackageReference Include="AutoFixture" Version="4.17.0" />
|
||||
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
|
||||
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2" />
|
||||
<PackageReference Include="Moq" Version="4.17.2" />
|
||||
<PackageReference Include="Moq" Version="4.18.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Code Analyzers -->
|
||||
|
||||
@@ -12,11 +12,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2" />
|
||||
<PackageReference Include="FsCheck.Xunit" Version="2.16.4" />
|
||||
<PackageReference Include="FsCheck.Xunit" Version="2.16.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Code Analyzers -->
|
||||
|
||||
@@ -12,10 +12,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Moq" Version="4.17.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.1" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -7,10 +7,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Moq" Version="4.17.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.1" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
@@ -17,7 +17,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FsCheck.Xunit" Version="2.16.4" />
|
||||
<PackageReference Include="FsCheck.Xunit" Version="2.16.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Code Analyzers -->
|
||||
|
||||
@@ -5,6 +5,38 @@ namespace Jellyfin.Extensions.Tests
|
||||
{
|
||||
public class StringExtensionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("", "")] // Identity edge-case (no diactritics)
|
||||
[InlineData("Indiana Jones", "Indiana Jones")] // Identity (no diactritics)
|
||||
[InlineData("a\ud800b", "ab")] // Invalid UTF-16 char stripping
|
||||
[InlineData("Jön", "Jon")] // Issue #7484
|
||||
[InlineData("Jönssonligan", "Jonssonligan")] // Issue #7484
|
||||
[InlineData("Kieślowski", "Kieslowski")] // Issue #7450
|
||||
[InlineData("Cidadão Kane", "Cidadao Kane")] // Issue #7560
|
||||
[InlineData("운명처럼 널 사랑해", "운명처럼 널 사랑해")] // Issue #6393 (Korean language support)
|
||||
[InlineData("애타는 로맨스", "애타는 로맨스")] // Issue #6393
|
||||
public void RemoveDiacritics_ValidInput_Corrects(string input, string expectedResult)
|
||||
{
|
||||
string result = input.RemoveDiacritics();
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("", false)] // Identity edge-case (no diactritics)
|
||||
[InlineData("Indiana Jones", false)] // Identity (no diactritics)
|
||||
[InlineData("a\ud800b", true)] // Invalid UTF-16 char stripping
|
||||
[InlineData("Jön", true)] // Issue #7484
|
||||
[InlineData("Jönssonligan", true)] // Issue #7484
|
||||
[InlineData("Kieślowski", true)] // Issue #7450
|
||||
[InlineData("Cidadão Kane", true)] // Issue #7560
|
||||
[InlineData("운명처럼 널 사랑해", false)] // Issue #6393 (Korean language support)
|
||||
[InlineData("애타는 로맨스", false)] // Issue #6393
|
||||
public void HasDiacritics_ValidInput_Corrects(string input, bool expectedResult)
|
||||
{
|
||||
bool result = input.HasDiacritics();
|
||||
Assert.Equal(expectedResult, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("", '_', 0)]
|
||||
[InlineData("___", '_', 3)]
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -22,10 +22,13 @@
|
||||
<PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
|
||||
<PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Moq" Version="4.17.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.1" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Code Analyzers -->
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace Jellyfin.Model.Tests
|
||||
[InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
|
||||
[InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
|
||||
[InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
|
||||
[InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
|
||||
[InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450
|
||||
[InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
|
||||
[InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
|
||||
// Firefox
|
||||
@@ -38,7 +38,7 @@ namespace Jellyfin.Model.Tests
|
||||
[InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
|
||||
[InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
|
||||
[InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
|
||||
[InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
|
||||
[InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450
|
||||
[InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
|
||||
[InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
|
||||
// Safari
|
||||
@@ -89,7 +89,7 @@ namespace Jellyfin.Model.Tests
|
||||
[InlineData("Chrome-NoHLS", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
|
||||
[InlineData("Chrome-NoHLS", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")]
|
||||
[InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")]
|
||||
[InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
|
||||
[InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450
|
||||
[InlineData("Chrome-NoHLS", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
|
||||
[InlineData("Chrome-NoHLS", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
|
||||
// TranscodeMedia
|
||||
@@ -131,6 +131,37 @@ namespace Jellyfin.Model.Tests
|
||||
[InlineData("Null", "mkv-vp9-aac-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)]
|
||||
[InlineData("Null", "mkv-vp9-ac3-srt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)]
|
||||
[InlineData("Null", "mkv-vp9-vorbis-vtt-2600k", null, TranscodeReason.ContainerBitrateExceedsLimit)]
|
||||
// AndroidTV
|
||||
[InlineData("AndroidTVExoPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("AndroidTVExoPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("AndroidTVExoPlayer", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("AndroidTVExoPlayer", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)]
|
||||
[InlineData("AndroidTVExoPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)]
|
||||
[InlineData("AndroidTVExoPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("AndroidTVExoPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("AndroidTVExoPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
|
||||
// Tizen 3 Stereo
|
||||
[InlineData("Tizen3-stereo", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen3-stereo", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen3-stereo", "mp4-h264-dts-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen3-stereo", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen3-stereo", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen3-stereo", "mp4-hevc-truehd-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
|
||||
[InlineData("Tizen3-stereo", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen3-stereo", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen3-stereo", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
|
||||
// Tizen 4 4K 5.1
|
||||
[InlineData("Tizen4-4K-5.1", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen4-4K-5.1", "mp4-h264-dts-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
|
||||
[InlineData("Tizen4-4K-5.1", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen4-4K-5.1", "mp4-hevc-truehd-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
|
||||
[InlineData("Tizen4-4K-5.1", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen4-4K-5.1", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
|
||||
public async Task BuildVideoItemSimple(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "")
|
||||
{
|
||||
var options = await GetVideoOptions(deviceName, mediaSource);
|
||||
@@ -146,7 +177,7 @@ namespace Jellyfin.Model.Tests
|
||||
[InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
|
||||
[InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
|
||||
[InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
|
||||
[InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
|
||||
[InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450
|
||||
[InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
|
||||
[InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
|
||||
// Firefox
|
||||
@@ -156,7 +187,7 @@ namespace Jellyfin.Model.Tests
|
||||
[InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
|
||||
[InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
|
||||
[InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")]
|
||||
[InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
|
||||
[InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450
|
||||
[InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450
|
||||
[InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
|
||||
// Safari
|
||||
@@ -198,6 +229,37 @@ namespace Jellyfin.Model.Tests
|
||||
[InlineData("JellyfinMediaPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
|
||||
[InlineData("JellyfinMediaPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
|
||||
[InlineData("JellyfinMediaPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)] // #6450
|
||||
// AndroidTV
|
||||
[InlineData("AndroidTVExoPlayer", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("AndroidTVExoPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("AndroidTVExoPlayer", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("AndroidTVExoPlayer", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)]
|
||||
[InlineData("AndroidTVExoPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)]
|
||||
[InlineData("AndroidTVExoPlayer", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("AndroidTVExoPlayer", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("AndroidTVExoPlayer", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
|
||||
// Tizen 3 Stereo
|
||||
[InlineData("Tizen3-stereo", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen3-stereo", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen3-stereo", "mp4-h264-dts-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen3-stereo", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen3-stereo", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen3-stereo", "mp4-hevc-truehd-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
|
||||
[InlineData("Tizen3-stereo", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen3-stereo", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen3-stereo", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
|
||||
// Tizen 4 4K 5.1
|
||||
[InlineData("Tizen4-4K-5.1", "mp4-h264-aac-vtt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen4-4K-5.1", "mp4-h264-dts-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
|
||||
[InlineData("Tizen4-4K-5.1", "mp4-hevc-aac-srt-15200k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen4-4K-5.1", "mp4-hevc-truehd-srt-15200k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)]
|
||||
[InlineData("Tizen4-4K-5.1", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen4-4K-5.1", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectPlay)]
|
||||
[InlineData("Tizen4-4K-5.1", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay)]
|
||||
public async Task BuildVideoItemWithFirstExplicitStream(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "")
|
||||
{
|
||||
var options = await GetVideoOptions(deviceName, mediaSource);
|
||||
@@ -223,12 +285,26 @@ namespace Jellyfin.Model.Tests
|
||||
// RokuSSPlus
|
||||
[InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
|
||||
[InlineData("RokuSSPlus", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450
|
||||
// no streams
|
||||
[InlineData("Chrome", "no-streams", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] // #6450
|
||||
// AndroidTV
|
||||
[InlineData("AndroidTVExoPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
|
||||
[InlineData("AndroidTVExoPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
|
||||
// Tizen 3 Stereo
|
||||
[InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
|
||||
[InlineData("Tizen3-stereo", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
|
||||
// Tizen 4 4K 5.1
|
||||
[InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
|
||||
[InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")]
|
||||
public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = (TranscodeReason)0, string transcodeMode = "DirectStream", string transcodeProtocol = "")
|
||||
{
|
||||
var options = await GetVideoOptions(deviceName, mediaSource);
|
||||
var streamCount = options.MediaSources[0].MediaStreams.Count;
|
||||
options.AudioStreamIndex = streamCount - 2;
|
||||
options.SubtitleStreamIndex = streamCount - 1;
|
||||
if (streamCount > 0)
|
||||
{
|
||||
options.AudioStreamIndex = streamCount - 2;
|
||||
options.SubtitleStreamIndex = streamCount - 1;
|
||||
}
|
||||
|
||||
var streamInfo = BuildVideoItemSimpleTest(options, playMethod, why, transcodeMode, transcodeProtocol);
|
||||
Assert.Equal(streamInfo?.AudioStreamIndex, options.AudioStreamIndex);
|
||||
@@ -262,23 +338,23 @@ namespace Jellyfin.Model.Tests
|
||||
Assert.NotNull(mediaSource);
|
||||
var videoStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Video);
|
||||
var audioStreams = mediaSource.MediaStreams.Where(stream => stream.Type == MediaStreamType.Audio);
|
||||
// TODO: check AudioStreamIndex vs options.AudioStreamIndex
|
||||
// TODO: Check AudioStreamIndex vs options.AudioStreamIndex
|
||||
var inputAudioStream = mediaSource.GetDefaultAudioStream(audioStreamIndexInput ?? mediaSource.DefaultAudioStreamIndex);
|
||||
|
||||
var uri = ParseUri(val);
|
||||
|
||||
if (playMethod == PlayMethod.DirectPlay)
|
||||
{
|
||||
// check expected container
|
||||
// Check expected container
|
||||
var containers = ContainerProfile.SplitValue(mediaSource.Container);
|
||||
// TODO: test transcode too
|
||||
// TODO: Test transcode too
|
||||
// Assert.Contains(uri.Extension, containers);
|
||||
|
||||
// check expected video codec (1)
|
||||
// Check expected video codec (1)
|
||||
Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec);
|
||||
Assert.Single(val.TargetVideoCodec);
|
||||
|
||||
// check expected audio codecs (1)
|
||||
// Check expected audio codecs (1)
|
||||
Assert.Contains(targetAudioStream.Codec, val.TargetAudioCodec);
|
||||
Assert.Single(val.TargetAudioCodec);
|
||||
// Assert.Single(val.AudioCodecs);
|
||||
@@ -294,7 +370,7 @@ namespace Jellyfin.Model.Tests
|
||||
Assert.NotEmpty(val.VideoCodecs);
|
||||
Assert.NotEmpty(val.AudioCodecs);
|
||||
|
||||
// check expected container (todo: this could be a test param)
|
||||
// Check expected container (todo: this could be a test param)
|
||||
if (transcodeProtocol == "http")
|
||||
{
|
||||
// Assert.Equal("webm", val.Container);
|
||||
@@ -327,32 +403,39 @@ namespace Jellyfin.Model.Tests
|
||||
stream => Assert.DoesNotContain(stream.Codec, val.VideoCodecs));
|
||||
}
|
||||
|
||||
// todo: fill out tests here
|
||||
// TODO: Fill out tests here
|
||||
}
|
||||
|
||||
// DirectStream and Remux
|
||||
else
|
||||
{
|
||||
// check expected video codec (1)
|
||||
// Check expected video codec (1)
|
||||
Assert.Contains(targetVideoStream.Codec, val.TargetVideoCodec);
|
||||
Assert.Single(val.TargetVideoCodec);
|
||||
|
||||
if (transcodeMode == "DirectStream")
|
||||
{
|
||||
// Check expected audio codecs (1)
|
||||
if (!targetAudioStream.IsExternal)
|
||||
{
|
||||
// check expected audio codecs (1)
|
||||
Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs);
|
||||
if (val.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported))
|
||||
{
|
||||
Assert.Contains(targetAudioStream.Codec, val.AudioCodecs);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.DoesNotContain(targetAudioStream.Codec, val.AudioCodecs);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (transcodeMode == "Remux")
|
||||
{
|
||||
// check expected audio codecs (1)
|
||||
// Check expected audio codecs (1)
|
||||
Assert.Contains(targetAudioStream.Codec, val.AudioCodecs);
|
||||
Assert.Single(val.AudioCodecs);
|
||||
}
|
||||
|
||||
// video details
|
||||
// Video details
|
||||
var videoStream = targetVideoStream;
|
||||
Assert.False(val.EstimateContentLength);
|
||||
Assert.Equal(TranscodeSeekInfo.Auto, val.TranscodeSeekInfo);
|
||||
@@ -361,10 +444,10 @@ namespace Jellyfin.Model.Tests
|
||||
Assert.Equal(videoStream.BitDepth, val.TargetVideoBitDepth);
|
||||
Assert.InRange(val.VideoBitrate.GetValueOrDefault(), videoStream.BitRate.GetValueOrDefault(), int.MaxValue);
|
||||
|
||||
// audio codec not supported
|
||||
// Audio codec not supported
|
||||
if ((why & TranscodeReason.AudioCodecNotSupported) != 0)
|
||||
{
|
||||
// audio stream specified
|
||||
// Audio stream specified
|
||||
if (options.AudioStreamIndex >= 0)
|
||||
{
|
||||
// TODO:fixme
|
||||
@@ -374,10 +457,10 @@ namespace Jellyfin.Model.Tests
|
||||
}
|
||||
}
|
||||
|
||||
// audio stream not specified
|
||||
// Audio stream not specified
|
||||
else
|
||||
{
|
||||
// TODO:fixme
|
||||
// TODO: Fixme
|
||||
Assert.All(audioStreams, stream =>
|
||||
{
|
||||
if (!stream.IsExternal)
|
||||
|
||||
@@ -7,12 +7,15 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Moq" Version="4.17.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.1" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2" />
|
||||
<PackageReference Include="FsCheck.Xunit" Version="2.16.4" />
|
||||
<PackageReference Include="FsCheck.Xunit" Version="2.16.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
{
|
||||
"Name": "Jellyfin AndroidTV-ExoPlayer",
|
||||
"EnableAlbumArtInDidl": false,
|
||||
"EnableSingleAlbumArtLimit": false,
|
||||
"EnableSingleSubtitleLimit": false,
|
||||
"SupportedMediaTypes": "Audio,Photo,Video",
|
||||
"MaxAlbumArtWidth": 0,
|
||||
"MaxAlbumArtHeight": 0,
|
||||
"MaxStreamingBitrate": 120000000,
|
||||
"MaxStaticBitrate": 100000000,
|
||||
"MusicStreamingTranscodingBitrate": 192000,
|
||||
"TimelineOffsetSeconds": 0,
|
||||
"RequiresPlainVideoItems": false,
|
||||
"RequiresPlainFolders": false,
|
||||
"EnableMSMediaReceiverRegistrar": false,
|
||||
"IgnoreTranscodeByteRangeRequests": false,
|
||||
"DirectPlayProfiles": [
|
||||
{
|
||||
"Container": "m4v,mov,xvid,vob,mkv,wmv,asf,ogm,ogv,mp4,webm",
|
||||
"AudioCodec": "aac,mp3,mp2,aac_latm,alac,ac3,eac3,dca,dts,mlp,truehd,pcm_alaw,pcm_mulaw",
|
||||
"VideoCodec": "h264,hevc,vp8,vp9,mpeg,mpeg2video",
|
||||
"Type": "Video",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "aac,mp3,mp2,aac_latm,alac,ac3,eac3,dca,dts,mlp,truehd,pcm_alaw,pcm_mulaw,,pa,flac,wav,wma,ogg,oga,webma,ape,opus",
|
||||
"Type": "Audio",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "jpg,jpeg,png,gif,web",
|
||||
"Type": "Photo",
|
||||
"$type": "DirectPlayProfile"
|
||||
}
|
||||
],
|
||||
"CodecProfiles": [
|
||||
{
|
||||
"Type": "Video",
|
||||
"Conditions": [
|
||||
{
|
||||
"Condition": "EqualsAny",
|
||||
"Property": "VideoProfile",
|
||||
"Value": "high|main|baseline|constrained baseline",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
},
|
||||
{
|
||||
"Condition": "LessThanEqual",
|
||||
"Property": "VideoLevel",
|
||||
"Value": "51",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
}
|
||||
],
|
||||
"Codec": "h264",
|
||||
"$type": "CodecProfile"
|
||||
},
|
||||
{
|
||||
"Type": "Video",
|
||||
"Conditions": [
|
||||
{
|
||||
"Condition": "LessThanEqual",
|
||||
"Property": "RefFrames",
|
||||
"Value": "12",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
}
|
||||
],
|
||||
"ApplyConditions": [
|
||||
{
|
||||
"Condition": "GreaterThanEqual",
|
||||
"Property": "Width",
|
||||
"Value": "1200",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
}
|
||||
],
|
||||
"Codec": "h264",
|
||||
"$type": "CodecProfile"
|
||||
},
|
||||
{
|
||||
"Type": "Video",
|
||||
"Conditions": [
|
||||
{
|
||||
"Condition": "LessThanEqual",
|
||||
"Property": "RefFrames",
|
||||
"Value": "4",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
}
|
||||
],
|
||||
"ApplyConditions": [
|
||||
{
|
||||
"Condition": "GreaterThanEqual",
|
||||
"Property": "Width",
|
||||
"Value": "1900",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
}
|
||||
],
|
||||
"Codec": "h264",
|
||||
"$type": "CodecProfile"
|
||||
},
|
||||
{
|
||||
"Type": "VideoAudio",
|
||||
"Conditions": [
|
||||
{
|
||||
"Condition": "LessThanEqual",
|
||||
"Property": "AudioChannels",
|
||||
"Value": "6",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
}
|
||||
],
|
||||
"$type": "CodecProfile"
|
||||
}
|
||||
],
|
||||
"TranscodingProfiles": [
|
||||
{
|
||||
"Container": "ts",
|
||||
"Type": "Video",
|
||||
"VideoCodec": "h264",
|
||||
"AudioCodec": "aac,mp3",
|
||||
"Protocol": "hls",
|
||||
"EstimateContentLength": false,
|
||||
"EnableMpegtsM2TsMode": false,
|
||||
"TranscodeSeekInfo": "Auto",
|
||||
"CopyTimestamps": false,
|
||||
"Context": "Streaming",
|
||||
"EnableSubtitlesInManifest": false,
|
||||
"MinSegments": 0,
|
||||
"SegmentLength": 0,
|
||||
"BreakOnNonKeyFrames": false,
|
||||
"$type": "TranscodingProfile"
|
||||
},
|
||||
{
|
||||
"Container": "mp3",
|
||||
"Type": "Audio",
|
||||
"AudioCodec": "mp3",
|
||||
"Protocol": "http",
|
||||
"EstimateContentLength": false,
|
||||
"EnableMpegtsM2TsMode": false,
|
||||
"TranscodeSeekInfo": "Auto",
|
||||
"CopyTimestamps": false,
|
||||
"Context": "Streaming",
|
||||
"EnableSubtitlesInManifest": false,
|
||||
"MinSegments": 0,
|
||||
"SegmentLength": 0,
|
||||
"BreakOnNonKeyFrames": false,
|
||||
"$type": "TranscodingProfile"
|
||||
}
|
||||
],
|
||||
"SubtitleProfiles": [
|
||||
{
|
||||
"Format": "srt",
|
||||
"Method": "Embed",
|
||||
"$type": "SubtitleProfile"
|
||||
},
|
||||
{
|
||||
"Format": "srt",
|
||||
"Method": "External",
|
||||
"$type": "SubtitleProfile"
|
||||
},
|
||||
{
|
||||
"Format": "subrip",
|
||||
"Method": "Embed",
|
||||
"$type": "SubtitleProfile"
|
||||
},
|
||||
{
|
||||
"Format": "subrip",
|
||||
"Method": "External",
|
||||
"$type": "SubtitleProfile"
|
||||
},
|
||||
{
|
||||
"Format": "ass",
|
||||
"Method": "Encode",
|
||||
"$type": "SubtitleProfile"
|
||||
},
|
||||
{
|
||||
"Format": "ssa",
|
||||
"Method": "Encode",
|
||||
"$type": "SubtitleProfile"
|
||||
},
|
||||
{
|
||||
"Format": "pgs",
|
||||
"Method": "Encode",
|
||||
"$type": "SubtitleProfile"
|
||||
},
|
||||
{
|
||||
"Format": "pgssub",
|
||||
"Method": "Encode",
|
||||
"$type": "SubtitleProfile"
|
||||
},
|
||||
{
|
||||
"Format": "dvdsub",
|
||||
"Method": "Encode",
|
||||
"$type": "SubtitleProfile"
|
||||
},
|
||||
{
|
||||
"Format": "vtt",
|
||||
"Method": "Embed",
|
||||
"$type": "SubtitleProfile"
|
||||
},
|
||||
{
|
||||
"Format": "sub",
|
||||
"Method": "Embed",
|
||||
"$type": "SubtitleProfile"
|
||||
},
|
||||
{
|
||||
"Format": "idx",
|
||||
"Method": "Embed",
|
||||
"$type": "SubtitleProfile"
|
||||
}
|
||||
],
|
||||
"$type": "DeviceProfile"
|
||||
}
|
||||
@@ -0,0 +1,526 @@
|
||||
{
|
||||
"Name": "Jellyfin Tizen 3 Stereo",
|
||||
"EnableAlbumArtInDidl": false,
|
||||
"EnableSingleAlbumArtLimit": false,
|
||||
"EnableSingleSubtitleLimit": false,
|
||||
"SupportedMediaTypes": "Audio,Photo,Video",
|
||||
"MaxAlbumArtWidth": 0,
|
||||
"MaxAlbumArtHeight": 0,
|
||||
"MaxStreamingBitrate": 120000000,
|
||||
"MaxStaticBitrate": 100000000,
|
||||
"MusicStreamingTranscodingBitrate": 384000,
|
||||
"TimelineOffsetSeconds": 0,
|
||||
"RequiresPlainVideoItems": false,
|
||||
"RequiresPlainFolders": false,
|
||||
"EnableMSMediaReceiverRegistrar": false,
|
||||
"IgnoreTranscodeByteRangeRequests": false,
|
||||
"DirectPlayProfiles": [
|
||||
{
|
||||
"Container": "webm",
|
||||
"AudioCodec": "vorbis,opus",
|
||||
"VideoCodec": "vp8,vp9",
|
||||
"Type": "Video",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "mp4,m4v",
|
||||
"AudioCodec": "aac,mp3,ac3,eac3,mp2,dca,dts,pcm_s16le,pcm_s24le,aac_latm,opus,flac,vorbis",
|
||||
"VideoCodec": "h264,hevc,mpeg2video,vc1,msmpeg4v2,vp8,vp9",
|
||||
"Type": "Video",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "mkv",
|
||||
"AudioCodec": "aac,mp3,ac3,eac3,mp2,dca,dts,pcm_s16le,pcm_s24le,aac_latm,opus,flac,vorbis",
|
||||
"VideoCodec": "h264,hevc,mpeg2video,vc1,msmpeg4v2,vp8,vp9",
|
||||
"Type": "Video",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "m2ts",
|
||||
"AudioCodec": "aac,mp3,ac3,eac3,mp2,dca,dts,pcm_s16le,pcm_s24le,aac_latm,opus,flac,vorbis",
|
||||
"VideoCodec": "h264,vc1,mpeg2video",
|
||||
"Type": "Video",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "wmv",
|
||||
"AudioCodec": "wma",
|
||||
"VideoCodec": "wmv,vc1",
|
||||
"Type": "Video",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "ts,mpegts",
|
||||
"AudioCodec": "aac,mp3,ac3,eac3,mp2,dca,dts,pcm_s16le,pcm_s24le,aac_latm,opus,flac,vorbis",
|
||||
"VideoCodec": "h264,hevc,vc1,mpeg2video",
|
||||
"Type": "Video",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "asf",
|
||||
"AudioCodec": "wma",
|
||||
"VideoCodec": "wmv,vc1",
|
||||
"Type": "Video",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "avi",
|
||||
"AudioCodec": "aac,mp3,ac3,eac3,mp2,dca,dts,pcm_s16le,pcm_s24le,aac_latm,opus,flac,vorbis",
|
||||
"VideoCodec": "",
|
||||
"Type": "Video",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "mpg,mpeg,flv,3gp,mts,trp,vob,vro",
|
||||
"AudioCodec": "aac,mp3,ac3,eac3,mp2,dca,dts,pcm_s16le,pcm_s24le,aac_latm,opus,flac,vorbis",
|
||||
"VideoCodec": "",
|
||||
"Type": "Video",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "mov",
|
||||
"AudioCodec": "aac,mp3,ac3,eac3,mp2,dca,dts,pcm_s16le,pcm_s24le,aac_latm,opus,flac,vorbis",
|
||||
"VideoCodec": "h264",
|
||||
"Type": "Video",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "opus",
|
||||
"Type": "Audio",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "webm",
|
||||
"AudioCodec": "opus",
|
||||
"Type": "Audio",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "mp3",
|
||||
"Type": "Audio",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "aac",
|
||||
"Type": "Audio",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "m4a",
|
||||
"AudioCodec": "aac",
|
||||
"Type": "Audio",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "m4b",
|
||||
"AudioCodec": "aac",
|
||||
"Type": "Audio",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "flac",
|
||||
"Type": "Audio",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "webma",
|
||||
"Type": "Audio",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "webm",
|
||||
"AudioCodec": "webma",
|
||||
"Type": "Audio",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "wma",
|
||||
"Type": "Audio",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "wav",
|
||||
"Type": "Audio",
|
||||
"$type": "DirectPlayProfile"
|
||||
},
|
||||
{
|
||||
"Container": "ogg",
|
||||
"Type": "Audio",
|
||||
"$type": "DirectPlayProfile"
|
||||
}
|
||||
],
|
||||
"TranscodingProfiles": [
|
||||
{
|
||||
"Container": "aac",
|
||||
"Type": "Audio",
|
||||
"AudioCodec": "aac",
|
||||
"Protocol": "hls",
|
||||
"EstimateContentLength": false,
|
||||
"EnableMpegtsM2TsMode": false,
|
||||
"TranscodeSeekInfo": "Auto",
|
||||
"CopyTimestamps": false,
|
||||
"Context": "Streaming",
|
||||
"EnableSubtitlesInManifest": false,
|
||||
"MaxAudioChannels": "2",
|
||||
"MinSegments": 1,
|
||||
"SegmentLength": 0,
|
||||
"BreakOnNonKeyFrames": true,
|
||||
"$type": "TranscodingProfile"
|
||||
},
|
||||
{
|
||||
"Container": "aac",
|
||||
"Type": "Audio",
|
||||
"AudioCodec": "aac",
|
||||
"Protocol": "http",
|
||||
"EstimateContentLength": false,
|
||||
"EnableMpegtsM2TsMode": false,
|
||||
"TranscodeSeekInfo": "Auto",
|
||||
"CopyTimestamps": false,
|
||||
"Context": "Streaming",
|
||||
"EnableSubtitlesInManifest": false,
|
||||
"MaxAudioChannels": "2",
|
||||
"MinSegments": 0,
|
||||
"SegmentLength": 0,
|
||||
"BreakOnNonKeyFrames": false,
|
||||
"$type": "TranscodingProfile"
|
||||
},
|
||||
{
|
||||
"Container": "mp3",
|
||||
"Type": "Audio",
|
||||
"AudioCodec": "mp3",
|
||||
"Protocol": "http",
|
||||
"EstimateContentLength": false,
|
||||
"EnableMpegtsM2TsMode": false,
|
||||
"TranscodeSeekInfo": "Auto",
|
||||
"CopyTimestamps": false,
|
||||
"Context": "Streaming",
|
||||
"EnableSubtitlesInManifest": false,
|
||||
"MaxAudioChannels": "2",
|
||||
"MinSegments": 0,
|
||||
"SegmentLength": 0,
|
||||
"BreakOnNonKeyFrames": false,
|
||||
"$type": "TranscodingProfile"
|
||||
},
|
||||
{
|
||||
"Container": "opus",
|
||||
"Type": "Audio",
|
||||
"AudioCodec": "opus",
|
||||
"Protocol": "http",
|
||||
"EstimateContentLength": false,
|
||||
"EnableMpegtsM2TsMode": false,
|
||||
"TranscodeSeekInfo": "Auto",
|
||||
"CopyTimestamps": false,
|
||||
"Context": "Streaming",
|
||||
"EnableSubtitlesInManifest": false,
|
||||
"MaxAudioChannels": "2",
|
||||
"MinSegments": 0,
|
||||
"SegmentLength": 0,
|
||||
"BreakOnNonKeyFrames": false,
|
||||
"$type": "TranscodingProfile"
|
||||
},
|
||||
{
|
||||
"Container": "wav",
|
||||
"Type": "Audio",
|
||||
"AudioCodec": "wav",
|
||||
"Protocol": "http",
|
||||
"EstimateContentLength": false,
|
||||
"EnableMpegtsM2TsMode": false,
|
||||
"TranscodeSeekInfo": "Auto",
|
||||
"CopyTimestamps": false,
|
||||
"Context": "Streaming",
|
||||
"EnableSubtitlesInManifest": false,
|
||||
"MaxAudioChannels": "2",
|
||||
"MinSegments": 0,
|
||||
"SegmentLength": 0,
|
||||
"BreakOnNonKeyFrames": false,
|
||||
"$type": "TranscodingProfile"
|
||||
},
|
||||
{
|
||||
"Container": "opus",
|
||||
"Type": "Audio",
|
||||
"AudioCodec": "opus",
|
||||
"Protocol": "http",
|
||||
"EstimateContentLength": false,
|
||||
"EnableMpegtsM2TsMode": false,
|
||||
"TranscodeSeekInfo": "Auto",
|
||||
"CopyTimestamps": false,
|
||||
"Context": "Static",
|
||||
"EnableSubtitlesInManifest": false,
|
||||
"MaxAudioChannels": "2",
|
||||
"MinSegments": 0,
|
||||
"SegmentLength": 0,
|
||||
"BreakOnNonKeyFrames": false,
|
||||
"$type": "TranscodingProfile"
|
||||
},
|
||||
{
|
||||
"Container": "mp3",
|
||||
"Type": "Audio",
|
||||
"AudioCodec": "mp3",
|
||||
"Protocol": "http",
|
||||
"EstimateContentLength": false,
|
||||
"EnableMpegtsM2TsMode": false,
|
||||
"TranscodeSeekInfo": "Auto",
|
||||
"CopyTimestamps": false,
|
||||
"Context": "Static",
|
||||
"EnableSubtitlesInManifest": false,
|
||||
"MaxAudioChannels": "2",
|
||||
"MinSegments": 0,
|
||||
"SegmentLength": 0,
|
||||
"BreakOnNonKeyFrames": false,
|
||||
"$type": "TranscodingProfile"
|
||||
},
|
||||
{
|
||||
"Container": "aac",
|
||||
"Type": "Audio",
|
||||
"AudioCodec": "aac",
|
||||
"Protocol": "http",
|
||||
"EstimateContentLength": false,
|
||||
"EnableMpegtsM2TsMode": false,
|
||||
"TranscodeSeekInfo": "Auto",
|
||||
"CopyTimestamps": false,
|
||||
"Context": "Static",
|
||||
"EnableSubtitlesInManifest": false,
|
||||
"MaxAudioChannels": "2",
|
||||
"MinSegments": 0,
|
||||
"SegmentLength": 0,
|
||||
"BreakOnNonKeyFrames": false,
|
||||
"$type": "TranscodingProfile"
|
||||
},
|
||||
{
|
||||
"Container": "wav",
|
||||
"Type": "Audio",
|
||||
"AudioCodec": "wav",
|
||||
"Protocol": "http",
|
||||
"EstimateContentLength": false,
|
||||
"EnableMpegtsM2TsMode": false,
|
||||
"TranscodeSeekInfo": "Auto",
|
||||
"CopyTimestamps": false,
|
||||
"Context": "Static",
|
||||
"EnableSubtitlesInManifest": false,
|
||||
"MaxAudioChannels": "2",
|
||||
"MinSegments": 0,
|
||||
"SegmentLength": 0,
|
||||
"BreakOnNonKeyFrames": false,
|
||||
"$type": "TranscodingProfile"
|
||||
},
|
||||
{
|
||||
"Container": "mkv",
|
||||
"Type": "Video",
|
||||
"VideoCodec": "h264,hevc,mpeg2video,vc1,msmpeg4v2,vp8,vp9",
|
||||
"AudioCodec": "aac,mp3,ac3,eac3,mp2,dca,dts,pcm_s16le,pcm_s24le,aac_latm,opus,flac,vorbis",
|
||||
"Protocol": "",
|
||||
"EstimateContentLength": false,
|
||||
"EnableMpegtsM2TsMode": false,
|
||||
"TranscodeSeekInfo": "Auto",
|
||||
"CopyTimestamps": true,
|
||||
"Context": "Static",
|
||||
"EnableSubtitlesInManifest": false,
|
||||
"MaxAudioChannels": "2",
|
||||
"MinSegments": 0,
|
||||
"SegmentLength": 0,
|
||||
"BreakOnNonKeyFrames": false,
|
||||
"Conditions": [
|
||||
{
|
||||
"Condition": "LessThanEqual",
|
||||
"Property": "Width",
|
||||
"Value": "1920",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
}
|
||||
],
|
||||
"$type": "TranscodingProfile"
|
||||
},
|
||||
{
|
||||
"Container": "ts",
|
||||
"Type": "Video",
|
||||
"VideoCodec": "h264,hevc",
|
||||
"AudioCodec": "aac,mp3,ac3,eac3,opus",
|
||||
"Protocol": "hls",
|
||||
"EstimateContentLength": false,
|
||||
"EnableMpegtsM2TsMode": false,
|
||||
"TranscodeSeekInfo": "Auto",
|
||||
"CopyTimestamps": false,
|
||||
"Context": "Streaming",
|
||||
"EnableSubtitlesInManifest": false,
|
||||
"MaxAudioChannels": "2",
|
||||
"MinSegments": 1,
|
||||
"SegmentLength": 0,
|
||||
"BreakOnNonKeyFrames": false,
|
||||
"Conditions": [
|
||||
{
|
||||
"Condition": "LessThanEqual",
|
||||
"Property": "Width",
|
||||
"Value": "1920",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
}
|
||||
],
|
||||
"$type": "TranscodingProfile"
|
||||
},
|
||||
{
|
||||
"Container": "webm",
|
||||
"Type": "Video",
|
||||
"VideoCodec": "vp8,vp9,vpx",
|
||||
"AudioCodec": "vorbis,opus",
|
||||
"Protocol": "http",
|
||||
"EstimateContentLength": false,
|
||||
"EnableMpegtsM2TsMode": false,
|
||||
"TranscodeSeekInfo": "Auto",
|
||||
"CopyTimestamps": false,
|
||||
"Context": "Streaming",
|
||||
"EnableSubtitlesInManifest": false,
|
||||
"MaxAudioChannels": "2",
|
||||
"MinSegments": 0,
|
||||
"SegmentLength": 0,
|
||||
"BreakOnNonKeyFrames": false,
|
||||
"Conditions": [
|
||||
{
|
||||
"Condition": "LessThanEqual",
|
||||
"Property": "Width",
|
||||
"Value": "1920",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
}
|
||||
],
|
||||
"$type": "TranscodingProfile"
|
||||
},
|
||||
{
|
||||
"Container": "mp4",
|
||||
"Type": "Video",
|
||||
"VideoCodec": "h264",
|
||||
"AudioCodec": "aac,mp3,ac3,eac3,mp2,dca,dts,pcm_s16le,pcm_s24le,aac_latm,opus,flac,vorbis",
|
||||
"Protocol": "http",
|
||||
"EstimateContentLength": false,
|
||||
"EnableMpegtsM2TsMode": false,
|
||||
"TranscodeSeekInfo": "Auto",
|
||||
"CopyTimestamps": false,
|
||||
"Context": "Static",
|
||||
"EnableSubtitlesInManifest": false,
|
||||
"MinSegments": 0,
|
||||
"SegmentLength": 0,
|
||||
"BreakOnNonKeyFrames": false,
|
||||
"Conditions": [
|
||||
{
|
||||
"Condition": "LessThanEqual",
|
||||
"Property": "Width",
|
||||
"Value": "1920",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
}
|
||||
],
|
||||
"$type": "TranscodingProfile"
|
||||
}
|
||||
],
|
||||
"CodecProfiles": [
|
||||
{
|
||||
"Type": "Video",
|
||||
"Conditions": [
|
||||
{
|
||||
"Condition": "NotEquals",
|
||||
"Property": "IsAnamorphic",
|
||||
"Value": "true",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
},
|
||||
{
|
||||
"Condition": "EqualsAny",
|
||||
"Property": "VideoProfile",
|
||||
"Value": "high|main|baseline|constrained baseline|high 10",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
},
|
||||
{
|
||||
"Condition": "LessThanEqual",
|
||||
"Property": "VideoLevel",
|
||||
"Value": "52",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
},
|
||||
{
|
||||
"Condition": "LessThanEqual",
|
||||
"Property": "VideoBitrate",
|
||||
"Value": "20000000",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
}
|
||||
],
|
||||
"Codec": "h264",
|
||||
"$type": "CodecProfile"
|
||||
},
|
||||
{
|
||||
"Type": "Video",
|
||||
"Conditions": [
|
||||
{
|
||||
"Condition": "NotEquals",
|
||||
"Property": "IsAnamorphic",
|
||||
"Value": "true",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
},
|
||||
{
|
||||
"Condition": "EqualsAny",
|
||||
"Property": "VideoProfile",
|
||||
"Value": "main|main 10",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
},
|
||||
{
|
||||
"Condition": "LessThanEqual",
|
||||
"Property": "VideoLevel",
|
||||
"Value": "183",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
},
|
||||
{
|
||||
"Condition": "LessThanEqual",
|
||||
"Property": "VideoBitrate",
|
||||
"Value": "20000000",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
}
|
||||
],
|
||||
"Codec": "hevc",
|
||||
"$type": "CodecProfile"
|
||||
},
|
||||
{
|
||||
"Type": "Video",
|
||||
"Conditions": [
|
||||
{
|
||||
"Condition": "LessThanEqual",
|
||||
"Property": "VideoBitrate",
|
||||
"Value": "20000000",
|
||||
"IsRequired": false,
|
||||
"$type": "ProfileCondition"
|
||||
}
|
||||
],
|
||||
"$type": "CodecProfile"
|
||||
}
|
||||
],
|
||||
"ResponseProfiles": [
|
||||
{
|
||||
"Container": "m4v",
|
||||
"Type": "Video",
|
||||
"MimeType": "video/mp4",
|
||||
"$type": "ResponseProfile"
|
||||
}
|
||||
],
|
||||
"SubtitleProfiles": [
|
||||
{
|
||||
"Format": "vtt",
|
||||
"Method": "External",
|
||||
"$type": "SubtitleProfile"
|
||||
},
|
||||
{
|
||||
"Format": "ass",
|
||||
"Method": "External",
|
||||
"$type": "SubtitleProfile"
|
||||
},
|
||||
{
|
||||
"Format": "ssa",
|
||||
"Method": "External",
|
||||
"$type": "SubtitleProfile"
|
||||
}
|
||||
],
|
||||
"$type": "DeviceProfile"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user