diff options
12 files changed, 55 insertions, 28 deletions
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 6730751d5..858c10030 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -131,6 +131,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun await taskCompletionSource.Task.ConfigureAwait(false); } + public string GetFilePath() + { + return TempFilePath; + } + private Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) { return Task.Run(async () => diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 8107bc427..4b170b2e4 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -65,7 +65,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var channelIdPrefix = GetFullChannelIdPrefix(info); - return await new M3uParser(Logger, _httpClientFactory, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false); + return await new M3uParser(Logger, _httpClientFactory, _appHost) + .Parse(info, channelIdPrefix, cancellationToken) + .ConfigureAwait(false); } public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken) @@ -126,7 +128,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public async Task Validate(TunerHostInfo info) { - using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false)) + using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false)) { } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index f066a749e..c064e2fe6 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -13,6 +13,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.LiveTv; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.TunerHosts @@ -30,12 +31,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts _appHost = appHost; } - public async Task<List<ChannelInfo>> Parse(string url, string channelIdPrefix, string tunerHostId, CancellationToken cancellationToken) + public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken) { // Read the file and display it line by line. - using (var reader = new StreamReader(await GetListingsStream(url, cancellationToken).ConfigureAwait(false))) + using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false))) { - return GetChannels(reader, channelIdPrefix, tunerHostId); + return GetChannels(reader, channelIdPrefix, info.Id); } } @@ -48,15 +49,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } } - public Task<Stream> GetListingsStream(string url, CancellationToken cancellationToken) + public async Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken) { - if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { - return _httpClientFactory.CreateClient(NamedClient.Default) - .GetStreamAsync(url); + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url); + if (!string.IsNullOrEmpty(info.UserAgent)) + { + requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent); + } + + var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .SendAsync(requestMessage, cancellationToken) + .ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); } - return Task.FromResult((Stream)File.OpenRead(url)); + return File.OpenRead(info.Url); } private const string ExtInfPrefix = "#EXTINF:"; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index 6c10fca8c..2e1b89509 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -55,7 +55,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts var typeName = GetType().Name; Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url); - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + // Response stream is disposed manually. + var response = await _httpClientFactory.CreateClient(NamedClient.Default) .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) .ConfigureAwait(false); @@ -121,6 +122,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } } + public string GetFilePath() + { + return TempFilePath; + } + private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) { return Task.Run(async () => diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index f4ca8575a..cc9243f37 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -113,5 +113,7 @@ "TaskCleanCache": "Vider le répertoire cache", "TasksApplicationCategory": "Application", "TasksLibraryCategory": "Bibliothèque", - "TasksMaintenanceCategory": "Maintenance" + "TasksMaintenanceCategory": "Maintenance", + "TaskCleanActivityLogDescription": "Supprime les entrées du journal d'activité antérieures à l'âge configuré.", + "TaskCleanActivityLog": "Nettoyer le journal d'activité" } diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 74380c2ef..b3e3490c2 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Devices Controller. /// </summary> - [Authorize(Policy = Policies.DefaultAuthorization)] + [Authorize(Policy = Policies.RequiresElevation)] public class DevicesController : BaseJellyfinApiController { private readonly IDeviceManager _deviceManager; @@ -46,7 +46,6 @@ namespace Jellyfin.Api.Controllers /// <response code="200">Devices retrieved.</response> /// <returns>An <see cref="OkResult"/> containing the list of devices.</returns> [HttpGet] - [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { @@ -62,7 +61,6 @@ namespace Jellyfin.Api.Controllers /// <response code="404">Device not found.</response> /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns> [HttpGet("Info")] - [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id) @@ -84,7 +82,6 @@ namespace Jellyfin.Api.Controllers /// <response code="404">Device not found.</response> /// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns> [HttpGet("Options")] - [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id) @@ -107,7 +104,6 @@ namespace Jellyfin.Api.Controllers /// <response code="404">Device not found.</response> /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns> [HttpPost("Options")] - [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateDeviceOptions( diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index f89c31e8b..88a7542ce 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -1220,11 +1220,8 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - await using var memoryStream = new MemoryStream(); - await new ProgressiveFileCopier(liveStreamInfo, null, _transcodingJobHelper, CancellationToken.None) - .WriteToAsync(memoryStream, CancellationToken.None) - .ConfigureAwait(false); - return File(memoryStream, MimeTypes.GetMimeType("file." + container)); + var liveStream = new ProgressiveFileStream(liveStreamInfo.GetFilePath(), null, _transcodingJobHelper); + return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container)); } private void AssertUserCanManageLiveTv() diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index d7c81a3ab..ad64adfba 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; +using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; @@ -9,6 +10,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -18,6 +20,7 @@ namespace Jellyfin.Api.Controllers /// The suggestions controller. /// </summary> [Route("")] + [Authorize(Policy = Policies.DefaultAuthorization)] public class SuggestionsController : BaseJellyfinApiController { private readonly IDtoService _dtoService; diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs index b3566b6f8..824870c7e 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs +++ b/Jellyfin.Api/Helpers/ProgressiveFileStream.cs @@ -82,20 +82,23 @@ namespace Jellyfin.Api.Helpers int totalBytesRead = 0; int remainingBytesToRead = count; + int newOffset = offset; while (remainingBytesToRead > 0) { cancellationToken.ThrowIfCancellationRequested(); int bytesRead; if (_allowAsyncFileRead) { - bytesRead = await _fileStream.ReadAsync(buffer, offset, remainingBytesToRead, cancellationToken).ConfigureAwait(false); + bytesRead = await _fileStream.ReadAsync(buffer, newOffset, remainingBytesToRead, cancellationToken).ConfigureAwait(false); } else { - bytesRead = _fileStream.Read(buffer, offset, remainingBytesToRead); + bytesRead = _fileStream.Read(buffer, newOffset, remainingBytesToRead); } remainingBytesToRead -= bytesRead; + newOffset += bytesRead; + if (bytesRead > 0) { _bytesWritten += bytesRead; @@ -108,12 +111,13 @@ namespace Jellyfin.Api.Helpers } else { - if (_job == null || _job.HasExited) + // If the job is null it's a live stream and will require user action to close + if (_job?.HasExited ?? false) { break; } - await Task.Delay(100, cancellationToken).ConfigureAwait(false); + await Task.Delay(50, cancellationToken).ConfigureAwait(false); } } diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs index 208566dc8..4f012cab2 100644 --- a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs +++ b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs @@ -15,7 +15,7 @@ namespace Jellyfin.Api.ModelBinders public Task BindModelAsync(ModelBindingContext bindingContext) { var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); - var elementType = bindingContext.ModelType.GetElementType(); + var elementType = bindingContext.ModelType.GetElementType() ?? bindingContext.ModelType.GenericTypeArguments[0]; var converter = TypeDescriptor.GetConverter(elementType); if (valueProviderResult.Length > 1) diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index 22bf9488f..21c6ef2af 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -115,5 +115,7 @@ namespace MediaBrowser.Controller.Library public interface IDirectStreamProvider { Task CopyToAsync(Stream stream, CancellationToken cancellationToken); + + string GetFilePath(); } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 33256e4bf..5846a603a 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -3085,7 +3085,7 @@ namespace MediaBrowser.Controller.MediaEncoding } } - var whichCodec = videoStream.Codec.ToLowerInvariant(); + var whichCodec = videoStream.Codec?.ToLowerInvariant(); switch (whichCodec) { case "avc": |
