mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-07-19 12:44:19 +01:00
Merge branch 'master' into trickplay
This commit is contained in:
@@ -30,7 +30,7 @@ namespace Jellyfin.Api.Auth.AnonymousLanAccessPolicy
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AnonymousLanAccessRequirement requirement)
|
||||
{
|
||||
var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIp();
|
||||
var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIP();
|
||||
|
||||
// Loopback will be on LAN, so we can accept null.
|
||||
if (ip is null || _networkManager.IsInLocalNetwork(ip))
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
|
||||
}
|
||||
|
||||
var isInLocalNetwork = _httpContextAccessor.HttpContext is not null
|
||||
&& _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIp());
|
||||
&& _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIP());
|
||||
var user = _userManager.GetUserById(userId);
|
||||
if (user is null)
|
||||
{
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement)
|
||||
{
|
||||
var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIp();
|
||||
var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIP();
|
||||
|
||||
// Loopback will be on LAN, so we can accept null.
|
||||
if (ip is null || _networkManager.IsInLocalNetwork(ip))
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.IO;
|
||||
using System.Net.Mime;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna;
|
||||
using Emby.Dlna.Main;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
@@ -33,12 +32,19 @@ public class DlnaServerController : BaseJellyfinApiController
|
||||
/// Initializes a new instance of the <see cref="DlnaServerController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
||||
public DlnaServerController(IDlnaManager dlnaManager)
|
||||
/// <param name="contentDirectory">Instance of the <see cref="IContentDirectory"/> interface.</param>
|
||||
/// <param name="connectionManager">Instance of the <see cref="IConnectionManager"/> interface.</param>
|
||||
/// <param name="mediaReceiverRegistrar">Instance of the <see cref="IMediaReceiverRegistrar"/> interface.</param>
|
||||
public DlnaServerController(
|
||||
IDlnaManager dlnaManager,
|
||||
IContentDirectory contentDirectory,
|
||||
IConnectionManager connectionManager,
|
||||
IMediaReceiverRegistrar mediaReceiverRegistrar)
|
||||
{
|
||||
_dlnaManager = dlnaManager;
|
||||
_contentDirectory = DlnaEntryPoint.Current.ContentDirectory;
|
||||
_connectionManager = DlnaEntryPoint.Current.ConnectionManager;
|
||||
_mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar;
|
||||
_contentDirectory = contentDirectory;
|
||||
_connectionManager = connectionManager;
|
||||
_mediaReceiverRegistrar = mediaReceiverRegistrar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -45,6 +45,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
private const string DefaultEventEncoderPreset = "superfast";
|
||||
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
|
||||
|
||||
private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
|
||||
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
@@ -1654,7 +1656,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
_encodingHelper.GetInputArgument(state, _encodingOptions, segmentContainer),
|
||||
threads,
|
||||
mapArgs,
|
||||
GetVideoArguments(state, startNumber, isEventPlaylist),
|
||||
GetVideoArguments(state, startNumber, isEventPlaylist, segmentContainer),
|
||||
GetAudioArguments(state),
|
||||
maxMuxingQueueSize,
|
||||
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
|
||||
@@ -1706,19 +1708,33 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
}
|
||||
|
||||
var audioCodec = _encodingHelper.GetAudioEncoder(state);
|
||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
||||
|
||||
// opus, dts, truehd and flac (in FFmpeg 5 and older) are experimental in mp4 muxer
|
||||
var strictArgs = string.Empty;
|
||||
var actualOutputAudioCodec = state.ActualOutputAudioCodec;
|
||||
if (string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)
|
||||
|| (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|
||||
&& _mediaEncoder.EncoderVersion < _minFFmpegFlacInMp4))
|
||||
{
|
||||
strictArgs = " -strict -2";
|
||||
}
|
||||
|
||||
if (!state.IsOutputVideo)
|
||||
{
|
||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||
{
|
||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
||||
|
||||
return "-acodec copy -strict -2" + bitStreamArgs;
|
||||
}
|
||||
|
||||
var audioTranscodeParams = string.Empty;
|
||||
|
||||
audioTranscodeParams += "-acodec " + audioCodec;
|
||||
// -vn to drop any video streams
|
||||
audioTranscodeParams += "-vn";
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||
{
|
||||
return audioTranscodeParams + " -acodec copy" + bitStreamArgs + strictArgs;
|
||||
}
|
||||
|
||||
audioTranscodeParams += " -acodec " + audioCodec + bitStreamArgs + strictArgs;
|
||||
|
||||
var audioBitrate = state.OutputAudioBitrate;
|
||||
var audioChannels = state.OutputAudioChannels;
|
||||
@@ -1746,25 +1762,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
audioTranscodeParams += " -vn";
|
||||
return audioTranscodeParams;
|
||||
}
|
||||
|
||||
// dts, flac, opus and truehd are experimental in mp4 muxer
|
||||
var strictArgs = string.Empty;
|
||||
var actualOutputAudioCodec = state.ActualOutputAudioCodec;
|
||||
if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(actualOutputAudioCodec, "truehd", 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))
|
||||
@@ -1775,7 +1778,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
return copyArgs;
|
||||
}
|
||||
|
||||
var args = "-codec:a:0 " + audioCodec + strictArgs;
|
||||
var args = "-codec:a:0 " + audioCodec + bitStreamArgs + strictArgs;
|
||||
|
||||
var channels = state.OutputAudioChannels;
|
||||
|
||||
@@ -1819,8 +1822,9 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
/// <param name="state">The <see cref="StreamState"/>.</param>
|
||||
/// <param name="startNumber">The first number in the hls sequence.</param>
|
||||
/// <param name="isEventPlaylist">Whether the playlist is EVENT or VOD.</param>
|
||||
/// <param name="segmentContainer">The segment container.</param>
|
||||
/// <returns>The command line arguments for video transcoding.</returns>
|
||||
private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist)
|
||||
private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist, string segmentContainer)
|
||||
{
|
||||
if (state.VideoStream is null)
|
||||
{
|
||||
@@ -1912,7 +1916,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
}
|
||||
|
||||
// TODO why was this not enabled for VOD?
|
||||
if (isEventPlaylist)
|
||||
if (isEventPlaylist && string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " -flags -global_header";
|
||||
}
|
||||
@@ -2045,9 +2049,9 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
return null;
|
||||
}
|
||||
|
||||
var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
|
||||
var playlistFilename = Path.GetFileNameWithoutExtension(playlist.AsSpan());
|
||||
|
||||
var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
|
||||
var indexString = Path.GetFileNameWithoutExtension(file.Name.AsSpan()).Slice(playlistFilename.Length);
|
||||
|
||||
return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ public class HlsSegmentController : BaseJellyfinApiController
|
||||
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
|
||||
{
|
||||
// TODO: Deprecate with new iOS app
|
||||
var file = segmentId + Path.GetExtension(Request.Path);
|
||||
var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
|
||||
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
||||
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
||||
var fileDir = Path.GetDirectoryName(file);
|
||||
@@ -85,11 +85,12 @@ public class HlsSegmentController : BaseJellyfinApiController
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
||||
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
|
||||
{
|
||||
var file = playlistId + Path.GetExtension(Request.Path);
|
||||
var file = string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan()));
|
||||
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
||||
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
||||
var fileDir = Path.GetDirectoryName(file);
|
||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8")
|
||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)
|
||||
|| Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return BadRequest("Invalid segment.");
|
||||
}
|
||||
@@ -138,7 +139,7 @@ public class HlsSegmentController : BaseJellyfinApiController
|
||||
[FromRoute, Required] string segmentId,
|
||||
[FromRoute, Required] string segmentContainer)
|
||||
{
|
||||
var file = segmentId + Path.GetExtension(Request.Path);
|
||||
var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
|
||||
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
|
||||
|
||||
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
@@ -78,6 +79,9 @@ public class ImageController : BaseJellyfinApiController
|
||||
_appPaths = appPaths;
|
||||
}
|
||||
|
||||
private static Stream GetFromBase64Stream(Stream inputStream)
|
||||
=> new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the user image.
|
||||
/// </summary>
|
||||
@@ -116,8 +120,8 @@ public class ImageController : BaseJellyfinApiController
|
||||
return BadRequest("Incorrect ContentType.");
|
||||
}
|
||||
|
||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
var stream = GetFromBase64Stream(Request.Body);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
// Handle image/png; charset=utf-8
|
||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||
@@ -130,7 +134,7 @@ public class ImageController : BaseJellyfinApiController
|
||||
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
|
||||
|
||||
await _providerManager
|
||||
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
|
||||
.SaveImage(stream, mimeType, user.ProfileImage.Path)
|
||||
.ConfigureAwait(false);
|
||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||
|
||||
@@ -176,8 +180,8 @@ public class ImageController : BaseJellyfinApiController
|
||||
return BadRequest("Incorrect ContentType.");
|
||||
}
|
||||
|
||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
var stream = GetFromBase64Stream(Request.Body);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
// Handle image/png; charset=utf-8
|
||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||
@@ -190,7 +194,7 @@ public class ImageController : BaseJellyfinApiController
|
||||
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
|
||||
|
||||
await _providerManager
|
||||
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
|
||||
.SaveImage(stream, mimeType, user.ProfileImage.Path)
|
||||
.ConfigureAwait(false);
|
||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||
|
||||
@@ -372,12 +376,12 @@ public class ImageController : BaseJellyfinApiController
|
||||
return BadRequest("Incorrect ContentType.");
|
||||
}
|
||||
|
||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
var stream = GetFromBase64Stream(Request.Body);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
// Handle image/png; charset=utf-8
|
||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
@@ -416,12 +420,12 @@ public class ImageController : BaseJellyfinApiController
|
||||
return BadRequest("Incorrect ContentType.");
|
||||
}
|
||||
|
||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
var stream = GetFromBase64Stream(Request.Body);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
// Handle image/png; charset=utf-8
|
||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
@@ -1792,8 +1796,8 @@ public class ImageController : BaseJellyfinApiController
|
||||
return BadRequest("Incorrect ContentType.");
|
||||
}
|
||||
|
||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
var stream = GetFromBase64Stream(Request.Body);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
|
||||
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||
@@ -1803,7 +1807,7 @@ public class ImageController : BaseJellyfinApiController
|
||||
var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||
await using (fs.ConfigureAwait(false))
|
||||
{
|
||||
await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
|
||||
await stream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
@@ -1833,15 +1837,6 @@ public class ImageController : BaseJellyfinApiController
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
|
||||
{
|
||||
using var reader = new StreamReader(inputStream);
|
||||
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
|
||||
var bytes = Convert.FromBase64String(text);
|
||||
return new MemoryStream(bytes, 0, bytes.Length, false, true);
|
||||
}
|
||||
|
||||
private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
|
||||
{
|
||||
int? width = null;
|
||||
|
||||
@@ -294,8 +294,8 @@ public class LibraryController : BaseJellyfinApiController
|
||||
|
||||
return new AllThemeMediaResult
|
||||
{
|
||||
ThemeSongsResult = themeSongs?.Value,
|
||||
ThemeVideosResult = themeVideos?.Value,
|
||||
ThemeSongsResult = themeSongs.Value,
|
||||
ThemeVideosResult = themeVideos.Value,
|
||||
SoundtrackSongsResult = new ThemeMediaResult()
|
||||
};
|
||||
}
|
||||
@@ -490,7 +490,7 @@ public class LibraryController : BaseJellyfinApiController
|
||||
|
||||
baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
|
||||
|
||||
parent = parent?.GetParent();
|
||||
parent = parent.GetParent();
|
||||
}
|
||||
|
||||
return baseItemDtos;
|
||||
|
||||
@@ -23,7 +23,6 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
@@ -48,7 +47,6 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IConfigurationManager _configurationManager;
|
||||
private readonly TranscodingJobHelper _transcodingJobHelper;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LiveTvController"/> class.
|
||||
@@ -61,7 +59,6 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
||||
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
|
||||
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
|
||||
public LiveTvController(
|
||||
ILiveTvManager liveTvManager,
|
||||
IUserManager userManager,
|
||||
@@ -70,8 +67,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
IDtoService dtoService,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IConfigurationManager configurationManager,
|
||||
TranscodingJobHelper transcodingJobHelper,
|
||||
ISessionManager sessionManager)
|
||||
TranscodingJobHelper transcodingJobHelper)
|
||||
{
|
||||
_liveTvManager = liveTvManager;
|
||||
_userManager = userManager;
|
||||
@@ -81,7 +77,6 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_configurationManager = configurationManager;
|
||||
_transcodingJobHelper = transcodingJobHelper;
|
||||
_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -184,7 +184,7 @@ public class MediaInfoController : BaseJellyfinApiController
|
||||
enableTranscoding.Value,
|
||||
allowVideoStreamCopy.Value,
|
||||
allowAudioStreamCopy.Value,
|
||||
Request.HttpContext.GetNormalizedRemoteIp());
|
||||
Request.HttpContext.GetNormalizedRemoteIP());
|
||||
}
|
||||
|
||||
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -90,7 +91,7 @@ public class SubtitleController : BaseJellyfinApiController
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<Task> DeleteSubtitle(
|
||||
public async Task<ActionResult> DeleteSubtitle(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] int index)
|
||||
{
|
||||
@@ -101,7 +102,7 @@ public class SubtitleController : BaseJellyfinApiController
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
_subtitleManager.DeleteSubtitles(item, index);
|
||||
await _subtitleManager.DeleteSubtitles(item, index).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -405,9 +406,8 @@ public class SubtitleController : BaseJellyfinApiController
|
||||
[FromBody, Required] UploadSubtitleDto body)
|
||||
{
|
||||
var video = (Video)_libraryManager.GetItemById(itemId);
|
||||
var data = Convert.FromBase64String(body.Data);
|
||||
var memoryStream = new MemoryStream(data, 0, data.Length, false, true);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
var stream = new CryptoStream(Request.Body, new FromBase64Transform(), CryptoStreamMode.Read);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
await _subtitleManager.UploadSubtitle(
|
||||
video,
|
||||
@@ -416,7 +416,8 @@ public class SubtitleController : BaseJellyfinApiController
|
||||
Format = body.Format,
|
||||
Language = body.Language,
|
||||
IsForced = body.IsForced,
|
||||
Stream = memoryStream
|
||||
IsHearingImpaired = body.IsHearingImpaired,
|
||||
Stream = stream
|
||||
}).ConfigureAwait(false);
|
||||
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
||||
|
||||
|
||||
@@ -4,14 +4,12 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.System;
|
||||
@@ -27,32 +25,36 @@ namespace Jellyfin.Api.Controllers;
|
||||
/// </summary>
|
||||
public class SystemController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ILogger<SystemController> _logger;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly INetworkManager _network;
|
||||
private readonly ILogger<SystemController> _logger;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly ISystemManager _systemManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SystemController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
|
||||
/// <param name="appPaths">Instance of <see cref="IServerApplicationPaths"/> interface.</param>
|
||||
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="systemManager">Instance of <see cref="ISystemManager"/> interface.</param>
|
||||
public SystemController(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<SystemController> logger,
|
||||
IServerApplicationHost appHost,
|
||||
IServerApplicationPaths appPaths,
|
||||
IFileSystem fileSystem,
|
||||
INetworkManager network,
|
||||
ILogger<SystemController> logger)
|
||||
INetworkManager networkManager,
|
||||
ISystemManager systemManager)
|
||||
{
|
||||
_appPaths = serverConfigurationManager.ApplicationPaths;
|
||||
_appHost = appHost;
|
||||
_fileSystem = fileSystem;
|
||||
_network = network;
|
||||
_logger = logger;
|
||||
_appHost = appHost;
|
||||
_appPaths = appPaths;
|
||||
_fileSystem = fileSystem;
|
||||
_networkManager = networkManager;
|
||||
_systemManager = systemManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -66,9 +68,7 @@ public class SystemController : BaseJellyfinApiController
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public ActionResult<SystemInfo> GetSystemInfo()
|
||||
{
|
||||
return _appHost.GetSystemInfo(Request);
|
||||
}
|
||||
=> _systemManager.GetSystemInfo(Request);
|
||||
|
||||
/// <summary>
|
||||
/// Gets public information about the server.
|
||||
@@ -78,9 +78,7 @@ public class SystemController : BaseJellyfinApiController
|
||||
[HttpGet("Info/Public")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
|
||||
{
|
||||
return _appHost.GetPublicSystemInfo(Request);
|
||||
}
|
||||
=> _systemManager.GetPublicSystemInfo(Request);
|
||||
|
||||
/// <summary>
|
||||
/// Pings the system.
|
||||
@@ -91,9 +89,7 @@ public class SystemController : BaseJellyfinApiController
|
||||
[HttpPost("Ping", Name = "PostPingSystem")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<string> PingSystem()
|
||||
{
|
||||
return _appHost.Name;
|
||||
}
|
||||
=> _appHost.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Restarts the application.
|
||||
@@ -107,11 +103,7 @@ public class SystemController : BaseJellyfinApiController
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public ActionResult RestartApplication()
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
_appHost.Restart();
|
||||
});
|
||||
_systemManager.Restart();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -127,11 +119,7 @@ public class SystemController : BaseJellyfinApiController
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public ActionResult ShutdownApplication()
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
await _appHost.Shutdown().ConfigureAwait(false);
|
||||
});
|
||||
_systemManager.Shutdown();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -189,7 +177,7 @@ public class SystemController : BaseJellyfinApiController
|
||||
return new EndPointInfo
|
||||
{
|
||||
IsLocal = HttpContext.IsLocal(),
|
||||
IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp())
|
||||
IsInNetwork = _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
|
||||
};
|
||||
}
|
||||
|
||||
@@ -227,7 +215,7 @@ public class SystemController : BaseJellyfinApiController
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
|
||||
{
|
||||
var result = _network.GetMacAddresses()
|
||||
var result = _networkManager.GetMacAddresses()
|
||||
.Select(i => new WakeOnLanInfo(i));
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,8 @@ public class TvShowsController : BaseJellyfinApiController
|
||||
/// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param>
|
||||
/// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
|
||||
/// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param>
|
||||
/// <param name="enableRewatching">Whether to include watched episode in next up results.</param>
|
||||
/// <param name="enableResumable">Whether to include resumable episodes in next up results.</param>
|
||||
/// <param name="enableRewatching">Whether to include watched episodes in next up results.</param>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
|
||||
[HttpGet("NextUp")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
@@ -86,6 +87,7 @@ public class TvShowsController : BaseJellyfinApiController
|
||||
[FromQuery] DateTime? nextUpDateCutoff,
|
||||
[FromQuery] bool enableTotalRecordCount = true,
|
||||
[FromQuery] bool disableFirstEpisode = false,
|
||||
[FromQuery] bool enableResumable = true,
|
||||
[FromQuery] bool enableRewatching = false)
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
@@ -104,6 +106,7 @@ public class TvShowsController : BaseJellyfinApiController
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
DisableFirstEpisode = disableFirstEpisode,
|
||||
NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue,
|
||||
EnableResumable = enableResumable,
|
||||
EnableRewatching = enableRewatching
|
||||
},
|
||||
options);
|
||||
|
||||
@@ -138,7 +138,7 @@ public class UniversalAudioController : BaseJellyfinApiController
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
Request.HttpContext.GetNormalizedRemoteIp());
|
||||
Request.HttpContext.GetNormalizedRemoteIP());
|
||||
}
|
||||
|
||||
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
|
||||
|
||||
@@ -134,7 +134,7 @@ public class UserController : BaseJellyfinApiController
|
||||
return NotFound("User not found");
|
||||
}
|
||||
|
||||
var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp().ToString());
|
||||
var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIP().ToString());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ public class UserController : BaseJellyfinApiController
|
||||
DeviceId = auth.DeviceId,
|
||||
DeviceName = auth.Device,
|
||||
Password = request.Pw,
|
||||
RemoteEndPoint = HttpContext.GetNormalizedRemoteIp().ToString(),
|
||||
RemoteEndPoint = HttpContext.GetNormalizedRemoteIP().ToString(),
|
||||
Username = request.Username
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
@@ -226,7 +226,7 @@ public class UserController : BaseJellyfinApiController
|
||||
catch (SecurityException e)
|
||||
{
|
||||
// rethrow adding IP address to message
|
||||
throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e);
|
||||
throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIP()}] {e.Message}", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ public class UserController : BaseJellyfinApiController
|
||||
catch (SecurityException e)
|
||||
{
|
||||
// rethrow adding IP address to message
|
||||
throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e);
|
||||
throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIP()}] {e.Message}", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ public class UserController : BaseJellyfinApiController
|
||||
user.Username,
|
||||
request.CurrentPw ?? string.Empty,
|
||||
request.CurrentPw ?? string.Empty,
|
||||
HttpContext.GetNormalizedRemoteIp().ToString(),
|
||||
HttpContext.GetNormalizedRemoteIP().ToString(),
|
||||
false).ConfigureAwait(false);
|
||||
|
||||
if (success is null)
|
||||
@@ -475,7 +475,7 @@ public class UserController : BaseJellyfinApiController
|
||||
await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp().ToString());
|
||||
var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIP().ToString());
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -490,11 +490,11 @@ public class UserController : BaseJellyfinApiController
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest)
|
||||
{
|
||||
var ip = HttpContext.GetNormalizedRemoteIp();
|
||||
var ip = HttpContext.GetNormalizedRemoteIP();
|
||||
var isLocal = HttpContext.IsLocal()
|
||||
|| _networkManager.IsInLocalNetwork(ip);
|
||||
|
||||
if (isLocal)
|
||||
if (!isLocal)
|
||||
{
|
||||
_logger.LogWarning("Password reset process initiated from outside the local network with IP: {IP}", ip);
|
||||
}
|
||||
@@ -571,7 +571,7 @@ public class UserController : BaseJellyfinApiController
|
||||
|
||||
if (filterByNetwork)
|
||||
{
|
||||
if (!_networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()))
|
||||
if (!_networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP()))
|
||||
{
|
||||
users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess));
|
||||
}
|
||||
@@ -579,7 +579,7 @@ public class UserController : BaseJellyfinApiController
|
||||
|
||||
var result = users
|
||||
.OrderBy(u => u.Username)
|
||||
.Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp().ToString()));
|
||||
.Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIP().ToString()));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -206,13 +206,6 @@ public class DynamicHlsHelper
|
||||
|
||||
if (state.VideoStream is not null && state.VideoRequest is not null)
|
||||
{
|
||||
// Provide a workaround for the case issue between flac and fLaC.
|
||||
var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString());
|
||||
if (!string.IsNullOrEmpty(flacWaPlaylist))
|
||||
{
|
||||
builder.Append(flacWaPlaylist);
|
||||
}
|
||||
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
|
||||
// Provide SDR HEVC entrance for backward compatibility.
|
||||
@@ -242,14 +235,7 @@ public class DynamicHlsHelper
|
||||
}
|
||||
|
||||
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
|
||||
var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
|
||||
|
||||
// Provide a workaround for the case issue between flac and fLaC.
|
||||
flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString());
|
||||
if (!string.IsNullOrEmpty(flacWaPlaylist))
|
||||
{
|
||||
builder.Append(flacWaPlaylist);
|
||||
}
|
||||
AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
|
||||
|
||||
// Restore the video codec
|
||||
state.OutputVideoCodec = "copy";
|
||||
@@ -280,17 +266,10 @@ public class DynamicHlsHelper
|
||||
state.VideoStream.Level = originalLevel;
|
||||
var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
|
||||
builder.Append(newPlaylist);
|
||||
|
||||
// Provide a workaround for the case issue between flac and fLaC.
|
||||
flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist);
|
||||
if (!string.IsNullOrEmpty(flacWaPlaylist))
|
||||
{
|
||||
builder.Append(flacWaPlaylist);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp()))
|
||||
if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIP()))
|
||||
{
|
||||
var requestedVideoBitrate = state.VideoRequest is null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
|
||||
|
||||
@@ -741,7 +720,7 @@ public class DynamicHlsHelper
|
||||
// Currently we only transcode to 8 bits AV1
|
||||
int bitDepth = 8;
|
||||
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||
&& state.VideoStream != null
|
||||
&& state.VideoStream is not null
|
||||
&& state.VideoStream.BitDepth.HasValue)
|
||||
{
|
||||
bitDepth = state.VideoStream.BitDepth.Value;
|
||||
@@ -815,16 +794,4 @@ public class DynamicHlsHelper
|
||||
newValue.ToString(),
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist)
|
||||
{
|
||||
if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal);
|
||||
|
||||
return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ using System.Text;
|
||||
namespace Jellyfin.Api.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Hls Codec string helpers.
|
||||
/// Helpers to generate HLS codec strings according to
|
||||
/// <a href="https://datatracker.ietf.org/doc/html/rfc6381#section-3.3">RFC 6381 section 3.3</a>
|
||||
/// and the <a href="https://mp4ra.org">MP4 Registration Authority</a>.
|
||||
/// </summary>
|
||||
public static class HlsCodecStringHelpers
|
||||
{
|
||||
@@ -27,7 +29,7 @@ public static class HlsCodecStringHelpers
|
||||
/// <summary>
|
||||
/// Codec name for FLAC.
|
||||
/// </summary>
|
||||
public const string FLAC = "flac";
|
||||
public const string FLAC = "fLaC";
|
||||
|
||||
/// <summary>
|
||||
/// Codec name for ALAC.
|
||||
@@ -37,7 +39,7 @@ public static class HlsCodecStringHelpers
|
||||
/// <summary>
|
||||
/// Codec name for OPUS.
|
||||
/// </summary>
|
||||
public const string OPUS = "opus";
|
||||
public const string OPUS = "Opus";
|
||||
|
||||
/// <summary>
|
||||
/// Gets a MP3 codec string.
|
||||
|
||||
@@ -421,7 +421,7 @@ public class MediaInfoHelper
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
httpContext.GetNormalizedRemoteIp());
|
||||
httpContext.GetNormalizedRemoteIP());
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -487,7 +487,7 @@ public class MediaInfoHelper
|
||||
{
|
||||
var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress);
|
||||
|
||||
_logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork);
|
||||
_logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIP: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork);
|
||||
if (!isInLocalNetwork)
|
||||
{
|
||||
maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
|
||||
|
||||
@@ -125,7 +125,7 @@ public static class RequestHelpers
|
||||
httpContext.User.GetVersion(),
|
||||
httpContext.User.GetDeviceId(),
|
||||
httpContext.User.GetDevice(),
|
||||
httpContext.GetNormalizedRemoteIp().ToString(),
|
||||
httpContext.GetNormalizedRemoteIP().ToString(),
|
||||
user).ConfigureAwait(false);
|
||||
|
||||
if (session is null)
|
||||
|
||||
@@ -191,6 +191,11 @@ public static class StreamingHelpers
|
||||
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0;
|
||||
}
|
||||
|
||||
if (outputAudioCodec.StartsWith("pcm_", StringComparison.Ordinal))
|
||||
{
|
||||
containerInternal = ".pcm";
|
||||
}
|
||||
|
||||
state.OutputAudioCodec = outputAudioCodec;
|
||||
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
|
||||
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
|
||||
@@ -243,7 +248,7 @@ public static class StreamingHelpers
|
||||
? GetOutputFileExtension(state, mediaSource)
|
||||
: ("." + state.OutputContainer);
|
||||
|
||||
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
|
||||
state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
|
||||
|
||||
return state;
|
||||
}
|
||||
@@ -416,10 +421,9 @@ public static class StreamingHelpers
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="mediaSource">The mediaSource.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
|
||||
private static string GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
|
||||
{
|
||||
var ext = Path.GetExtension(state.RequestedUrl);
|
||||
|
||||
if (!string.IsNullOrEmpty(ext))
|
||||
{
|
||||
return ext;
|
||||
@@ -458,10 +462,9 @@ public static class StreamingHelpers
|
||||
return ".asf";
|
||||
}
|
||||
}
|
||||
|
||||
// Try to infer based on the desired audio codec
|
||||
if (!state.IsVideoRequest)
|
||||
else
|
||||
{
|
||||
// Try to infer based on the desired audio codec
|
||||
var audioCodec = state.Request.AudioCodec;
|
||||
|
||||
if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -492,7 +495,7 @@ public static class StreamingHelpers
|
||||
return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
throw new InvalidOperationException("Failed to find an appropriate file extension");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -509,7 +512,7 @@ public static class StreamingHelpers
|
||||
var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
|
||||
|
||||
var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
var ext = outputFileExtension?.ToLowerInvariant();
|
||||
var ext = outputFileExtension.ToLowerInvariant();
|
||||
var folder = serverConfigurationManager.GetTranscodePath();
|
||||
|
||||
return Path.Combine(folder, filename + ext);
|
||||
|
||||
@@ -538,7 +538,7 @@ public class TranscodingJobHelper : IDisposable
|
||||
await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase))
|
||||
if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string subtitlePath = state.SubtitleStream.Path;
|
||||
string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
|
||||
@@ -620,7 +620,7 @@ public class TranscodingJobHelper : IDisposable
|
||||
state.TranscodingJob = transcodingJob;
|
||||
|
||||
// Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback
|
||||
_ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream);
|
||||
_ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError, logStream);
|
||||
|
||||
// Wait for the file to exist before proceeding
|
||||
var ffmpegTargetFile = state.WaitForPath ?? outputPath;
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
|
||||
<NoWarn>AD0001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -122,17 +122,17 @@ public class ExceptionMiddleware
|
||||
|
||||
private static int GetStatusCode(Exception ex)
|
||||
{
|
||||
switch (ex)
|
||||
return ex switch
|
||||
{
|
||||
case ArgumentException _: return StatusCodes.Status400BadRequest;
|
||||
case AuthenticationException _: return StatusCodes.Status401Unauthorized;
|
||||
case SecurityException _: return StatusCodes.Status403Forbidden;
|
||||
case DirectoryNotFoundException _:
|
||||
case FileNotFoundException _:
|
||||
case ResourceNotFoundException _: return StatusCodes.Status404NotFound;
|
||||
case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed;
|
||||
default: return StatusCodes.Status500InternalServerError;
|
||||
}
|
||||
ArgumentException => StatusCodes.Status400BadRequest,
|
||||
AuthenticationException => StatusCodes.Status401Unauthorized,
|
||||
SecurityException => StatusCodes.Status403Forbidden,
|
||||
DirectoryNotFoundException => StatusCodes.Status404NotFound,
|
||||
FileNotFoundException => StatusCodes.Status404NotFound,
|
||||
ResourceNotFoundException => StatusCodes.Status404NotFound,
|
||||
MethodNotAllowedException => StatusCodes.Status405MethodNotAllowed,
|
||||
_ => StatusCodes.Status500InternalServerError
|
||||
};
|
||||
}
|
||||
|
||||
private string NormalizeExceptionMessage(string msg)
|
||||
|
||||
@@ -9,15 +9,15 @@ namespace Jellyfin.Api.Middleware;
|
||||
/// <summary>
|
||||
/// Validates the IP of requests coming from local networks wrt. remote access.
|
||||
/// </summary>
|
||||
public class IpBasedAccessValidationMiddleware
|
||||
public class IPBasedAccessValidationMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IpBasedAccessValidationMiddleware"/> class.
|
||||
/// Initializes a new instance of the <see cref="IPBasedAccessValidationMiddleware"/> class.
|
||||
/// </summary>
|
||||
/// <param name="next">The next delegate in the pipeline.</param>
|
||||
public IpBasedAccessValidationMiddleware(RequestDelegate next)
|
||||
public IPBasedAccessValidationMiddleware(RequestDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
}
|
||||
@@ -37,9 +37,9 @@ public class IpBasedAccessValidationMiddleware
|
||||
return;
|
||||
}
|
||||
|
||||
var remoteIp = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback;
|
||||
var remoteIP = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback;
|
||||
|
||||
if (!networkManager.HasRemoteAccess(remoteIp))
|
||||
if (!networkManager.HasRemoteAccess(remoteIP))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public class LanFilteringMiddleware
|
||||
return;
|
||||
}
|
||||
|
||||
var host = httpContext.GetNormalizedRemoteIp();
|
||||
var host = httpContext.GetNormalizedRemoteIP();
|
||||
if (!networkManager.IsInLocalNetwork(host))
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -51,9 +51,9 @@ public class ResponseTimeMiddleware
|
||||
if (enableWarning && responseTimeMs > warningThreshold && _logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}",
|
||||
"Slow HTTP Response from {Url} to {RemoteIP} in {Elapsed:g} with Status Code {StatusCode}",
|
||||
context.Request.GetDisplayUrl(),
|
||||
context.GetNormalizedRemoteIp(),
|
||||
context.GetNormalizedRemoteIP(),
|
||||
responseTime,
|
||||
context.Response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -33,8 +33,7 @@ public class RobotsRedirectionMiddleware
|
||||
/// <returns>The async task.</returns>
|
||||
public async Task Invoke(HttpContext httpContext)
|
||||
{
|
||||
var localPath = httpContext.Request.Path.ToString();
|
||||
if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase))
|
||||
if (httpContext.Request.Path.Equals("/robots.txt", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("Redirecting robots.txt request to web/robots.txt");
|
||||
httpContext.Response.Redirect("web/robots.txt");
|
||||
|
||||
@@ -77,7 +77,7 @@ public class CommaDelimitedArrayModelBinder : IModelBinder
|
||||
var typedValueIndex = 0;
|
||||
for (var i = 0; i < parsedValues.Length; i++)
|
||||
{
|
||||
if (parsedValues[i] != null)
|
||||
if (parsedValues[i] is not null)
|
||||
{
|
||||
typedValues.SetValue(parsedValues[i], typedValueIndex);
|
||||
typedValueIndex++;
|
||||
|
||||
@@ -77,7 +77,7 @@ public class PipeDelimitedArrayModelBinder : IModelBinder
|
||||
var typedValueIndex = 0;
|
||||
for (var i = 0; i < parsedValues.Length; i++)
|
||||
{
|
||||
if (parsedValues[i] != null)
|
||||
if (parsedValues[i] is not null)
|
||||
{
|
||||
typedValues.SetValue(parsedValues[i], typedValueIndex);
|
||||
typedValueIndex++;
|
||||
|
||||
@@ -25,6 +25,12 @@ public class UploadSubtitleDto
|
||||
[Required]
|
||||
public bool IsForced { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the subtitle is for hearing impaired.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool IsHearingImpaired { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the subtitle data.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Activity;
|
||||
using MediaBrowser.Model.Session;
|
||||
@@ -9,7 +11,7 @@ using Microsoft.Extensions.Logging;
|
||||
namespace Jellyfin.Api.WebSocketListeners;
|
||||
|
||||
/// <summary>
|
||||
/// Class SessionInfoWebSocketListener.
|
||||
/// Class ActivityLogWebSocketListener.
|
||||
/// </summary>
|
||||
public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<ActivityLogEntry[], WebSocketListenerState>
|
||||
{
|
||||
@@ -56,6 +58,20 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
|
||||
base.Dispose(dispose);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts sending messages over an activity log web socket.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
protected override void Start(WebSocketMessageInfo message)
|
||||
{
|
||||
if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
|
||||
{
|
||||
throw new AuthenticationException("Only admin users can retrieve the activity log.");
|
||||
}
|
||||
|
||||
base.Start(message);
|
||||
}
|
||||
|
||||
private async void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
|
||||
{
|
||||
await SendData(true).ConfigureAwait(false);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Session;
|
||||
@@ -66,6 +68,20 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
|
||||
base.Dispose(dispose);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts sending messages over a session info web socket.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
protected override void Start(WebSocketMessageInfo message)
|
||||
{
|
||||
if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
|
||||
{
|
||||
throw new AuthenticationException("Only admin users can subscribe to session information.");
|
||||
}
|
||||
|
||||
base.Start(message);
|
||||
}
|
||||
|
||||
private async void OnSessionManagerSessionActivity(object? sender, SessionEventArgs e)
|
||||
{
|
||||
await SendData(false).ConfigureAwait(false);
|
||||
|
||||
Reference in New Issue
Block a user