aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Api/Controllers
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Api/Controllers')
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs41
-rw-r--r--Jellyfin.Api/Controllers/FilterController.cs41
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs8
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs3
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs35
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs3
-rw-r--r--Jellyfin.Api/Controllers/PersonsController.cs16
-rw-r--r--Jellyfin.Api/Controllers/PluginsController.cs1
-rw-r--r--Jellyfin.Api/Controllers/StartupController.cs5
-rw-r--r--Jellyfin.Api/Controllers/UniversalAudioController.cs1
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs52
11 files changed, 151 insertions, 55 deletions
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 838f48949d..034a9dea55 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -20,7 +20,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
-using MediaBrowser.MediaEncoding.Encoder;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
@@ -1456,22 +1455,16 @@ public class DynamicHlsController : BaseJellyfinApiController
var segmentExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
- TranscodingJob? job;
-
- if (System.IO.File.Exists(segmentPath))
- {
- job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- _logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
- return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
- }
-
+ // Keep segment selection and transcoding replacement under the same playlist lock.
+ // An out-of-order request must not replace a job while another request is using its output.
using (await _transcodeManager.LockAsync(playlistPath, cancellationToken).ConfigureAwait(false))
{
+ TranscodingJob? job;
var startTranscoding = false;
if (System.IO.File.Exists(segmentPath))
{
job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- _logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
+ _logger.LogDebug("returning {0} [it exists]", segmentPath);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
@@ -1505,6 +1498,9 @@ public class DynamicHlsController : BaseJellyfinApiController
// If the playlist doesn't already exist, startup ffmpeg
try
{
+ var currentJob = _transcodeManager.GetTranscodingJob(playlistPath, TranscodingJobType);
+ await WaitForActiveTranscodingRequests(currentJob, cancellationToken).ConfigureAwait(false);
+
await _transcodeManager.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false)
.ConfigureAwait(false);
@@ -1540,11 +1536,19 @@ public class DynamicHlsController : BaseJellyfinApiController
await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
}
}
+
+ _logger.LogDebug("returning {0} [general case]", segmentPath);
+ job ??= _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
+ return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
+ }
- _logger.LogDebug("returning {0} [general case]", segmentPath);
- job ??= _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
- return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
+ internal static async Task WaitForActiveTranscodingRequests(TranscodingJob? job, CancellationToken cancellationToken)
+ {
+ while (job?.ActiveRequestCount > 0)
+ {
+ await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+ }
}
private static double[] GetSegmentLengths(StreamState state)
@@ -1607,8 +1611,9 @@ public class DynamicHlsController : BaseJellyfinApiController
if (state.VideoStream is not null && state.IsOutputVideo)
{
- // fMP4 needs this flag to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT
- hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont";
+ // fMP4 needs frag_discont to write the audio packet DTS/PTS including the initial delay into MOOF::TRAF::TFDT
+ // HLS does not use SIDX, and skipping it avoids FFmpeg rewriting open-GOP boundary packet PTS
+ hlsArguments += $" {(useLegacySegmentOption ? "-hls_ts_options" : "-hls_segment_options")} movflags=+frag_discont+skip_sidx";
}
segmentFormat = "fmp4" + outputFmp4HeaderArg;
@@ -1646,9 +1651,9 @@ public class DynamicHlsController : BaseJellyfinApiController
segmentFormat,
startNumber.ToString(CultureInfo.InvariantCulture),
baseUrlParam,
- EncodingUtils.NormalizePath(outputTsArg),
+ outputTsArg.EscapeProcessArgument(),
hlsArguments,
- EncodingUtils.NormalizePath(outputPath)).Trim();
+ outputPath.EscapeProcessArgument()).Trim();
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index cfc8be28ae..b458bd90f3 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -158,13 +158,43 @@ public class FilterController : BaseJellyfinApiController
IsSeries = isSeries
};
+ var streamLanguageQuery = new InternalItemsQuery(user)
+ {
+ // It's possible that different langauges are only available on alternative versions.
+ // To fetch them all, owned items are included.
+ IncludeOwnedItems = true,
+ IncludeItemTypes = includeItemTypes,
+ DtoOptions = new DtoOptions
+ {
+ Fields = Array.Empty<ItemFields>(),
+ EnableImages = false,
+ EnableUserData = false
+ },
+ IsAiring = isAiring,
+ IsMovie = isMovie,
+ IsSports = isSports,
+ IsKids = isKids,
+ IsNews = isNews,
+ IsSeries = isSeries
+ };
+
if ((recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder)
{
- genreQuery.AncestorIds = parentItem is null ? Array.Empty<Guid>() : new[] { parentItem.Id };
+ var ancestorIds = parentItem is null ? Array.Empty<Guid>() : new[] { parentItem.Id };
+ genreQuery.AncestorIds = ancestorIds;
+ streamLanguageQuery.AncestorIds = ancestorIds;
}
else
{
genreQuery.Parent = parentItem;
+ streamLanguageQuery.Parent = parentItem;
+ }
+
+ if ((includeItemTypes.Contains(BaseItemKind.Series) || includeItemTypes.Contains(BaseItemKind.Season))
+ && !includeItemTypes.Contains(BaseItemKind.Episode))
+ {
+ // streams are joined on epsiodes not shows or seasons
+ streamLanguageQuery.IncludeItemTypes = [.. includeItemTypes, BaseItemKind.Episode];
}
if (includeItemTypes.Length == 1
@@ -188,10 +218,13 @@ public class FilterController : BaseJellyfinApiController
}).ToArray();
}
- if (includeItemTypes.Contains(BaseItemKind.Movie) || includeItemTypes.Contains(BaseItemKind.Series))
+ if (includeItemTypes.Contains(BaseItemKind.Movie)
+ || includeItemTypes.Contains(BaseItemKind.Series)
+ || includeItemTypes.Contains(BaseItemKind.Season)
+ || includeItemTypes.Contains(BaseItemKind.Episode))
{
filters.AudioLanguages = _libraryManager
- .GetMediaStreamLanguages(MediaStreamType.Audio)
+ .GetMediaStreamLanguages(MediaStreamType.Audio, streamLanguageQuery)
.Select(language =>
{
var culture = _localization.FindLanguageInfo(language);
@@ -204,7 +237,7 @@ public class FilterController : BaseJellyfinApiController
.OrderBy(l => l.Name)
.ToArray();
filters.SubtitleLanguages = _libraryManager
- .GetMediaStreamLanguages(MediaStreamType.Subtitle)
+ .GetMediaStreamLanguages(MediaStreamType.Subtitle, streamLanguageQuery)
.Select(language =>
{
var culture = _localization.FindLanguageInfo(language);
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 52d8b4dad1..d492a2f5ba 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -125,7 +125,13 @@ public class ImageController : BaseJellyfinApiController
{
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
+ var userConfigurationDirectoryPath = _serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath;
+ var userDataPath = Path.Combine(userConfigurationDirectoryPath, user.Username);
+ if (!PathHelper.IsContainedIn(userConfigurationDirectoryPath, userDataPath))
+ {
+ return BadRequest("Invalid user.");
+ }
+
if (user.ProfileImage is not null)
{
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index d009f80a96..39ba5ab186 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -13,6 +13,7 @@ using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
using Microsoft.AspNetCore.Authorization;
@@ -263,7 +264,7 @@ public class ItemLookupController : BaseJellyfinApiController
searchResult.ProviderIds);
// Since the refresh process won't erase provider Ids, we need to set this explicitly now.
- item.ProviderIds = searchResult.ProviderIds;
+ item.SetProviderIds(searchResult.ProviderIds);
await _providerManager.RefreshFullItem(
item,
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index e26264d292..65fffc4181 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -236,7 +236,7 @@ public class ItemUpdateController : BaseJellyfinApiController
return NoContent();
}
- private async Task UpdateItem(BaseItemDto request, BaseItem item)
+ internal async Task UpdateItem(BaseItemDto request, BaseItem item)
{
item.Name = request.Name;
item.ForcedSortName = request.ForcedSortName;
@@ -250,7 +250,11 @@ public class ItemUpdateController : BaseJellyfinApiController
item.IndexNumber = request.IndexNumber;
item.ParentIndexNumber = request.ParentIndexNumber;
item.Overview = request.Overview;
- item.Genres = request.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+
+ if (request.Genres is not null)
+ {
+ item.Genres = request.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+ }
if (item is Episode episode)
{
@@ -293,10 +297,20 @@ public class ItemUpdateController : BaseJellyfinApiController
item.CustomRating = request.CustomRating;
var currentTags = item.Tags;
- var newTags = request.Tags.Select(t => t.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
- var removedTags = currentTags.Except(newTags).ToList();
- var addedTags = newTags.Except(currentTags).ToList();
- item.Tags = newTags;
+ List<string> removedTags;
+ List<string> addedTags;
+ if (request.Tags is not null)
+ {
+ var newTags = request.Tags.Select(t => t.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+ removedTags = currentTags.Except(newTags).ToList();
+ addedTags = newTags.Except(currentTags).ToList();
+ item.Tags = newTags;
+ }
+ else
+ {
+ removedTags = [];
+ addedTags = [];
+ }
if (item is Series rseries)
{
@@ -412,16 +426,11 @@ public class ItemUpdateController : BaseJellyfinApiController
item.RunTimeTicks = request.RunTimeTicks;
}
- foreach (var pair in request.ProviderIds.ToList())
+ if (request.ProviderIds is not null)
{
- if (string.IsNullOrEmpty(pair.Value))
- {
- request.ProviderIds.Remove(pair.Key);
- }
+ item.SetProviderIds(request.ProviderIds);
}
- item.ProviderIds = request.ProviderIds;
-
if (item is Video video)
{
video.Video3DFormat = request.Video3DFormat;
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index ac7c091f85..aa942e7642 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -84,7 +84,7 @@ public class MediaInfoController : BaseJellyfinApiController
return NotFound();
}
- return await _mediaInfoHelper.GetPlaybackInfo(item, user).ConfigureAwait(false);
+ return await _mediaInfoHelper.GetPlaybackInfo(item, user, Request).ConfigureAwait(false);
}
/// <summary>
@@ -177,6 +177,7 @@ public class MediaInfoController : BaseJellyfinApiController
var info = await _mediaInfoHelper.GetPlaybackInfo(
item,
user,
+ Request,
mediaSourceId,
liveStreamId)
.ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index 9ffccaa9e9..51d4081ecf 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -4,6 +4,7 @@ using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
+using Jellyfin.Data;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
@@ -103,6 +104,7 @@ public class PersonsController : BaseJellyfinApiController
personTypes,
excludePersonTypes)
{
+ AccessFilter = BuildAccessFilter(user),
NameContains = searchTerm,
NameStartsWith = nameStartsWith,
NameLessThan = nameLessThan,
@@ -123,6 +125,20 @@ public class PersonsController : BaseJellyfinApiController
.ToArray());
}
+ // People are not owned by a library, so nothing in the Peoples table says which of them a user is
+ // allowed to see; that only follows from the items they are credited on.
+ private InternalItemsQuery? BuildAccessFilter(User? user)
+ {
+ if (user is null || !user.HasContentRestrictions())
+ {
+ return null;
+ }
+
+ var accessFilter = new InternalItemsQuery(user) { IncludeOwnedItems = true };
+ _libraryManager.ConfigureUserAccess(accessFilter, user);
+ return accessFilter;
+ }
+
/// <summary>
/// Get person by name.
/// </summary>
diff --git a/Jellyfin.Api/Controllers/PluginsController.cs b/Jellyfin.Api/Controllers/PluginsController.cs
index 2c84fde972..79b8b60cb7 100644
--- a/Jellyfin.Api/Controllers/PluginsController.cs
+++ b/Jellyfin.Api/Controllers/PluginsController.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
+using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Plugins;
diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs
index fa6d9efe36..47c8f21241 100644
--- a/Jellyfin.Api/Controllers/StartupController.cs
+++ b/Jellyfin.Api/Controllers/StartupController.cs
@@ -140,6 +140,11 @@ public class StartupController : BaseJellyfinApiController
return NotFound();
}
+ if (!string.IsNullOrEmpty(user.Password))
+ {
+ return Forbid();
+ }
+
if (string.IsNullOrWhiteSpace(startupUserDto.Password))
{
return BadRequest("Password must not be empty");
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index e53d15acfd..cdbd1ee7aa 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -133,6 +133,7 @@ public class UniversalAudioController : BaseJellyfinApiController
var info = await _mediaInfoHelper.GetPlaybackInfo(
item,
user,
+ Request,
mediaSourceId)
.ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index a718035528..da03032249 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -34,12 +34,15 @@ namespace Jellyfin.Api.Controllers;
[Tags("Library")]
public class UserLibraryController : BaseJellyfinApiController
{
+ private static readonly TimeSpan RefreshOnDemandTimeout = TimeSpan.FromSeconds(3);
+
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataRepository;
private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService;
private readonly IUserViewManager _userViewManager;
private readonly IFileSystem _fileSystem;
+ private readonly IProviderManager _providerManager;
/// <summary>
/// Initializes a new instance of the <see cref="UserLibraryController"/> class.
@@ -50,13 +53,15 @@ public class UserLibraryController : BaseJellyfinApiController
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
public UserLibraryController(
IUserManager userManager,
IUserDataManager userDataRepository,
ILibraryManager libraryManager,
IDtoService dtoService,
IUserViewManager userViewManager,
- IFileSystem fileSystem)
+ IFileSystem fileSystem,
+ IProviderManager providerManager)
{
_userManager = userManager;
_userDataRepository = userDataRepository;
@@ -64,6 +69,7 @@ public class UserLibraryController : BaseJellyfinApiController
_dtoService = dtoService;
_userViewManager = userViewManager;
_fileSystem = fileSystem;
+ _providerManager = providerManager;
}
/// <summary>
@@ -94,7 +100,7 @@ public class UserLibraryController : BaseJellyfinApiController
return NotFound();
}
- await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
+ await RefreshOnDemandIfNeeded(item).ConfigureAwait(false);
var dtoOptions = new DtoOptions();
@@ -639,24 +645,36 @@ public class UserLibraryController : BaseJellyfinApiController
limit,
groupItems);
- private async Task RefreshItemOnDemandIfNeeded(BaseItem item)
+ private async Task RefreshOnDemandIfNeeded(BaseItem item)
{
- if (item is Person)
+ if (item is not Person)
{
- var hasMetadata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
- var performFullRefresh = !hasMetadata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
+ return;
+ }
- if (performFullRefresh)
- {
- var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
- ImageRefreshMode = MetadataRefreshMode.FullRefresh,
- ForceSave = true
- };
-
- await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
- }
+ var hasMetadata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
+ if (hasMetadata || (DateTime.UtcNow - item.DateLastRefreshed).TotalDays < 3)
+ {
+ return;
+ }
+
+ var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
+ ImageRefreshMode = MetadataRefreshMode.FullRefresh,
+ ForceSave = true
+ };
+
+ using var timeout = CancellationTokenSource.CreateLinkedTokenSource(HttpContext.RequestAborted);
+ timeout.CancelAfter(RefreshOnDemandTimeout);
+
+ try
+ {
+ await item.RefreshMetadata(options, timeout.Token).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) when (!HttpContext.RequestAborted.IsCancellationRequested)
+ {
+ _providerManager.QueueRefresh(item.Id, options, RefreshPriority.High);
}
}