aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Api/Controllers
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Api/Controllers')
-rw-r--r--Jellyfin.Api/Controllers/BackupController.cs127
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs7
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs12
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs137
-rw-r--r--Jellyfin.Api/Controllers/ItemUpdateController.cs13
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs3
-rw-r--r--Jellyfin.Api/Controllers/LocalizationController.cs11
-rw-r--r--Jellyfin.Api/Controllers/MediaSegmentsController.cs5
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs39
-rw-r--r--Jellyfin.Api/Controllers/PlaystateController.cs3
-rw-r--r--Jellyfin.Api/Controllers/SyncPlayController.cs2
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs1
-rw-r--r--Jellyfin.Api/Controllers/YearsController.cs7
13 files changed, 228 insertions, 139 deletions
diff --git a/Jellyfin.Api/Controllers/BackupController.cs b/Jellyfin.Api/Controllers/BackupController.cs
new file mode 100644
index 000000000..aa908ee30
--- /dev/null
+++ b/Jellyfin.Api/Controllers/BackupController.cs
@@ -0,0 +1,127 @@
+using System.IO;
+using System.Threading.Tasks;
+using Jellyfin.Server.Implementations.SystemBackupService;
+using MediaBrowser.Common.Api;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.SystemBackupService;
+using Microsoft.AspNetCore.Authentication.OAuth.Claims;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// The backup controller.
+/// </summary>
+[Authorize(Policy = Policies.RequiresElevation)]
+public class BackupController : BaseJellyfinApiController
+{
+ private readonly IBackupService _backupService;
+ private readonly IApplicationPaths _applicationPaths;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BackupController"/> class.
+ /// </summary>
+ /// <param name="backupService">Instance of the <see cref="IBackupService"/> interface.</param>
+ /// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
+ public BackupController(IBackupService backupService, IApplicationPaths applicationPaths)
+ {
+ _backupService = backupService;
+ _applicationPaths = applicationPaths;
+ }
+
+ /// <summary>
+ /// Creates a new Backup.
+ /// </summary>
+ /// <param name="backupOptions">The backup options.</param>
+ /// <response code="200">Backup created.</response>
+ /// <response code="403">User does not have permission to retrieve information.</response>
+ /// <returns>The created backup manifest.</returns>
+ [HttpPost("Create")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult<BackupManifestDto>> CreateBackup([FromBody] BackupOptionsDto backupOptions)
+ {
+ return Ok(await _backupService.CreateBackupAsync(backupOptions ?? new()).ConfigureAwait(false));
+ }
+
+ /// <summary>
+ /// Restores to a backup by restarting the server and applying the backup.
+ /// </summary>
+ /// <param name="archiveRestoreDto">The data to start a restore process.</param>
+ /// <response code="204">Backup restore started.</response>
+ /// <response code="403">User does not have permission to retrieve information.</response>
+ /// <returns>No-Content.</returns>
+ [HttpPost("Restore")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public IActionResult StartRestoreBackup([FromBody, BindRequired] BackupRestoreRequestDto archiveRestoreDto)
+ {
+ var archivePath = SanitizePath(archiveRestoreDto.ArchiveFileName);
+ if (!System.IO.File.Exists(archivePath))
+ {
+ return NotFound();
+ }
+
+ _backupService.ScheduleRestoreAndRestartServer(archivePath);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Gets a list of all currently present backups in the backup directory.
+ /// </summary>
+ /// <response code="200">Backups available.</response>
+ /// <response code="403">User does not have permission to retrieve information.</response>
+ /// <returns>The list of backups.</returns>
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult<BackupManifestDto[]>> ListBackups()
+ {
+ return Ok(await _backupService.EnumerateBackups().ConfigureAwait(false));
+ }
+
+ /// <summary>
+ /// Gets the descriptor from an existing archive is present.
+ /// </summary>
+ /// <param name="path">The data to start a restore process.</param>
+ /// <response code="200">Backup archive manifest.</response>
+ /// <response code="204">Not a valid jellyfin Archive.</response>
+ /// <response code="404">Not a valid path.</response>
+ /// <response code="403">User does not have permission to retrieve information.</response>
+ /// <returns>The backup manifest.</returns>
+ [HttpGet("Manifest")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task<ActionResult<BackupManifestDto>> GetBackup([BindRequired] string path)
+ {
+ var backupPath = SanitizePath(path);
+
+ if (!System.IO.File.Exists(backupPath))
+ {
+ return NotFound();
+ }
+
+ var manifest = await _backupService.GetBackupManifest(backupPath).ConfigureAwait(false);
+ if (manifest is null)
+ {
+ return NoContent();
+ }
+
+ return Ok(manifest);
+ }
+
+ [NonAction]
+ private string SanitizePath(string path)
+ {
+ // sanitize path
+ var archiveRestorePath = Path.GetFileName(Path.GetFullPath(path));
+ var archivePath = Path.Combine(_applicationPaths.BackupPath, archiveRestorePath);
+ return archivePath;
+ }
+}
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 13064882c..585318d24 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -96,9 +96,6 @@ public class DisplayPreferencesController : BaseJellyfinApiController
dto.CustomPrefs.TryAdd(key, value);
}
- // This will essentially be a noop if no changes have been made, but new prefs must be saved at least.
- _displayPreferencesManager.SaveChanges();
-
return dto;
}
@@ -210,8 +207,8 @@ public class DisplayPreferencesController : BaseJellyfinApiController
// Set all remaining custom preferences.
_displayPreferencesManager.SetCustomItemDisplayPreferences(userId.Value, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
- _displayPreferencesManager.SaveChanges();
-
+ _displayPreferencesManager.UpdateItemDisplayPreferences(itemPrefs);
+ _displayPreferencesManager.UpdateDisplayPreferences(existingDisplayPreferences);
return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 4cac8ed67..fe6f855b5 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -46,6 +46,7 @@ public class DynamicHlsController : BaseJellyfinApiController
private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
private readonly Version _minFFmpegX265BframeInFmp4 = new Version(7, 0, 1);
+ private readonly Version _minFFmpegHlsSegmentOptions = new Version(5, 0);
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
@@ -1606,6 +1607,7 @@ public class DynamicHlsController : BaseJellyfinApiController
var segmentFormat = string.Empty;
var segmentContainer = outputExtension.TrimStart('.');
var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions, segmentContainer);
+ var hlsArguments = $"-hls_playlist_type {(isEventPlaylist ? "event" : "vod")} -hls_list_size 0";
if (string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase))
{
@@ -1621,6 +1623,14 @@ public class DynamicHlsController : BaseJellyfinApiController
false => " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\""
};
+ var useLegacySegmentOption = _mediaEncoder.EncoderVersion < _minFFmpegHlsSegmentOptions;
+
+ 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";
+ }
+
segmentFormat = "fmp4" + outputFmp4HeaderArg;
}
else
@@ -1642,8 +1652,6 @@ public class DynamicHlsController : BaseJellyfinApiController
Path.GetFileNameWithoutExtension(outputPath));
}
- var hlsArguments = $"-hls_playlist_type {(isEventPlaylist ? "event" : "vod")} -hls_list_size 0";
-
return string.Format(
CultureInfo.InvariantCulture,
"{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9}{10} -hls_segment_filename \"{11}\" {12} -y \"{13}\"",
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index abda053d3..cd8132d21 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
+using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -1458,19 +1459,6 @@ public class ImageController : BaseJellyfinApiController
/// <param name="userId">User id.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
- /// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
- /// <param name="width">The fixed image width to return.</param>
- /// <param name="height">The fixed image height to return.</param>
- /// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
- /// <param name="fillWidth">Width of box to fill.</param>
- /// <param name="fillHeight">Height of box to fill.</param>
- /// <param name="blur">Optional. Blur image.</param>
- /// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
- /// <param name="imageIndex">Image index.</param>
/// <response code="200">Image stream returned.</response>
/// <response code="400">User id not provided.</response>
/// <response code="404">Item not found.</response>
@@ -1487,20 +1475,7 @@ public class ImageController : BaseJellyfinApiController
public async Task<ActionResult> GetUserImage(
[FromQuery] Guid? userId,
[FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] double? percentPlayed,
- [FromQuery] int? unplayedCount,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? quality,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromQuery] int? imageIndex)
+ [FromQuery] ImageFormat? format)
{
var requestUserId = userId ?? User.GetUserId();
if (requestUserId.IsEmpty())
@@ -1521,34 +1496,24 @@ public class ImageController : BaseJellyfinApiController
DateModified = user.ProfileImage.LastModified
};
- if (width.HasValue)
- {
- info.Width = width.Value;
- }
-
- if (height.HasValue)
- {
- info.Height = height.Value;
- }
-
return await GetImageInternal(
user.Id,
ImageType.Profile,
- imageIndex,
+ null,
tag,
format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- blur,
- backgroundColor,
- foregroundLayer,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 90,
+ null,
+ null,
+ null,
+ null,
+ null,
null,
info)
.ConfigureAwait(false);
@@ -1608,20 +1573,7 @@ public class ImageController : BaseJellyfinApiController
=> GetUserImage(
userId,
tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- blur,
- backgroundColor,
- foregroundLayer,
- imageIndex);
+ format);
/// <summary>
/// Get user profile image.
@@ -1677,36 +1629,13 @@ public class ImageController : BaseJellyfinApiController
=> GetUserImage(
userId,
tag,
- format,
- maxWidth,
- maxHeight,
- percentPlayed,
- unplayedCount,
- width,
- height,
- quality,
- fillWidth,
- fillHeight,
- blur,
- backgroundColor,
- foregroundLayer,
- imageIndex);
+ format);
/// <summary>
/// Generates or gets the splashscreen.
/// </summary>
/// <param name="tag">Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
- /// <param name="maxWidth">The maximum image width to return.</param>
- /// <param name="maxHeight">The maximum image height to return.</param>
- /// <param name="width">The fixed image width to return.</param>
- /// <param name="height">The fixed image height to return.</param>
- /// <param name="fillWidth">Width of box to fill.</param>
- /// <param name="fillHeight">Height of box to fill.</param>
- /// <param name="blur">Blur image.</param>
- /// <param name="backgroundColor">Apply a background color for transparent images.</param>
- /// <param name="foregroundLayer">Apply a foreground layer on top of the image.</param>
- /// <param name="quality">Quality setting, from 0-100.</param>
/// <response code="200">Splashscreen returned successfully.</response>
/// <returns>The splashscreen.</returns>
[HttpGet("Branding/Splashscreen")]
@@ -1714,17 +1643,7 @@ public class ImageController : BaseJellyfinApiController
[ProducesImageFile]
public async Task<ActionResult> GetSplashscreen(
[FromQuery] string? tag,
- [FromQuery] ImageFormat? format,
- [FromQuery] int? maxWidth,
- [FromQuery] int? maxHeight,
- [FromQuery] int? width,
- [FromQuery] int? height,
- [FromQuery] int? fillWidth,
- [FromQuery] int? fillHeight,
- [FromQuery] int? blur,
- [FromQuery] string? backgroundColor,
- [FromQuery] string? foregroundLayer,
- [FromQuery, Range(0, 100)] int quality = 90)
+ [FromQuery] ImageFormat? format)
{
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
var isAdmin = User.IsInRole(Constants.UserRoles.Administrator);
@@ -1763,16 +1682,16 @@ public class ImageController : BaseJellyfinApiController
{
Path = splashscreenPath
},
- Height = height,
- MaxHeight = maxHeight,
- MaxWidth = maxWidth,
- FillHeight = fillHeight,
- FillWidth = fillWidth,
- Quality = quality,
- Width = width,
- Blur = blur,
- BackgroundColor = backgroundColor,
- ForegroundLayer = foregroundLayer,
+ Height = null,
+ MaxHeight = null,
+ MaxWidth = null,
+ FillHeight = null,
+ FillWidth = null,
+ Quality = 90,
+ Width = null,
+ Blur = null,
+ BackgroundColor = null,
+ ForegroundLayer = null,
SupportedOutputFormats = outputFormats
};
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index d49e0753e..e1d9b6bba 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -158,7 +158,10 @@ public class ItemUpdateController : BaseJellyfinApiController
ParentalRatingOptions = _localizationManager.GetParentalRatings().ToList(),
ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
Countries = _localizationManager.GetCountries().ToArray(),
- Cultures = _localizationManager.GetCultures().ToArray()
+ Cultures = _localizationManager.GetCultures()
+ .DistinctBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase)
+ .OrderBy(c => c.DisplayName)
+ .ToArray()
};
if (!item.IsVirtualItem
@@ -299,7 +302,7 @@ public class ItemUpdateController : BaseJellyfinApiController
if (!season.LockedFields.Contains(MetadataField.Tags))
{
- season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+ season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
season.OnMetadataChanged();
@@ -316,7 +319,7 @@ public class ItemUpdateController : BaseJellyfinApiController
if (!ep.LockedFields.Contains(MetadataField.Tags))
{
- ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+ ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
ep.OnMetadataChanged();
@@ -337,7 +340,7 @@ public class ItemUpdateController : BaseJellyfinApiController
if (!ep.LockedFields.Contains(MetadataField.Tags))
{
- ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+ ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
ep.OnMetadataChanged();
@@ -357,7 +360,7 @@ public class ItemUpdateController : BaseJellyfinApiController
if (!track.LockedFields.Contains(MetadataField.Tags))
{
- track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
+ track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
track.OnMetadataChanged();
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index bde1758e9..4c9cc2b1e 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -779,11 +779,14 @@ public class LibraryController : BaseJellyfinApiController
var query = new InternalItemsQuery(user)
{
Genres = item.Genres,
+ Tags = item.Tags,
Limit = limit,
IncludeItemTypes = includeItemTypes.ToArray(),
DtoOptions = dtoOptions,
EnableTotalRecordCount = !isMovie ?? true,
EnableGroupByMetadataKey = isMovie ?? false,
+ ExcludeItemIds = [itemId],
+ OrderBy = [(ItemSortBy.Random, SortOrder.Ascending)]
};
// ExcludeArtistIds
diff --git a/Jellyfin.Api/Controllers/LocalizationController.cs b/Jellyfin.Api/Controllers/LocalizationController.cs
index bbce5a9e1..dd8f935dc 100644
--- a/Jellyfin.Api/Controllers/LocalizationController.cs
+++ b/Jellyfin.Api/Controllers/LocalizationController.cs
@@ -1,4 +1,6 @@
+using System;
using System.Collections.Generic;
+using System.Linq;
using MediaBrowser.Common.Api;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
@@ -34,7 +36,14 @@ public class LocalizationController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<CultureDto>> GetCultures()
{
- return Ok(_localization.GetCultures());
+ var allCultures = _localization.GetCultures();
+
+ var distinctCultures = allCultures
+ .DistinctBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase)
+ .OrderBy(c => c.DisplayName)
+ .AsEnumerable();
+
+ return Ok(distinctCultures);
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/MediaSegmentsController.cs b/Jellyfin.Api/Controllers/MediaSegmentsController.cs
index e30e2b54e..b8836d7cf 100644
--- a/Jellyfin.Api/Controllers/MediaSegmentsController.cs
+++ b/Jellyfin.Api/Controllers/MediaSegmentsController.cs
@@ -5,9 +5,9 @@ using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Database.Implementations.Enums;
-using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Model.MediaSegments;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
@@ -55,7 +55,8 @@ public class MediaSegmentsController : BaseJellyfinApiController
return NotFound();
}
- var items = await _mediaSegmentManager.GetSegmentsAsync(item, includeSegmentTypes).ConfigureAwait(false);
+ var libraryOptions = _libraryManager.GetLibraryOptions(item);
+ var items = await _mediaSegmentManager.GetSegmentsAsync(item, includeSegmentTypes, libraryOptions).ConfigureAwait(false);
return Ok(new QueryResult<MediaSegmentDto>(items.ToArray()));
}
}
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index ec5fdab38..79c71d23a 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -450,22 +450,41 @@ public class PlaylistsController : BaseJellyfinApiController
{
var callingUserId = User.GetUserId();
- var playlist = _playlistManager.GetPlaylistForUser(Guid.Parse(playlistId), callingUserId);
- if (playlist is null)
+ if (!callingUserId.IsEmpty())
{
- return NotFound("Playlist not found");
+ var playlist = _playlistManager.GetPlaylistForUser(Guid.Parse(playlistId), callingUserId);
+ if (playlist is null)
+ {
+ return NotFound("Playlist not found");
+ }
+
+ var isPermitted = playlist.OwnerUserId.Equals(callingUserId)
+ || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId));
+
+ if (!isPermitted)
+ {
+ return Forbid();
+ }
}
+ else
+ {
+ var isApiKey = User.GetIsApiKey();
- var isPermitted = playlist.OwnerUserId.Equals(callingUserId)
- || playlist.Shares.Any(s => s.CanEdit && s.UserId.Equals(callingUserId));
+ if (!isApiKey)
+ {
+ return Forbid();
+ }
+ }
- if (!isPermitted)
+ try
{
- return Forbid();
+ await _playlistManager.RemoveItemFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false);
+ return NoContent();
+ }
+ catch (ArgumentException)
+ {
+ return NotFound();
}
-
- await _playlistManager.RemoveItemFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false);
- return NoContent();
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs
index 1577b4594..ade0906b3 100644
--- a/Jellyfin.Api/Controllers/PlaystateController.cs
+++ b/Jellyfin.Api/Controllers/PlaystateController.cs
@@ -272,6 +272,7 @@ public class PlaystateController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("PlayingItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Obsolete("This endpoint is obsolete. Use ReportPlaybackStart instead")]
public async Task<ActionResult> OnPlaybackStart(
[FromRoute, Required] Guid itemId,
[FromQuery] string? mediaSourceId,
@@ -350,6 +351,7 @@ public class PlaystateController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("PlayingItems/{itemId}/Progress")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Obsolete("This endpoint is obsolete. Use ReportPlaybackProgress instead")]
public async Task<ActionResult> OnPlaybackProgress(
[FromRoute, Required] Guid itemId,
[FromQuery] string? mediaSourceId,
@@ -438,6 +440,7 @@ public class PlaystateController : BaseJellyfinApiController
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("PlayingItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
+ [Obsolete("This endpoint is obsolete. Use ReportPlaybackStop instead")]
public async Task<ActionResult> OnPlaybackStopped(
[FromRoute, Required] Guid itemId,
[FromQuery] string? mediaSourceId,
diff --git a/Jellyfin.Api/Controllers/SyncPlayController.cs b/Jellyfin.Api/Controllers/SyncPlayController.cs
index fbab2a784..3d6874079 100644
--- a/Jellyfin.Api/Controllers/SyncPlayController.cs
+++ b/Jellyfin.Api/Controllers/SyncPlayController.cs
@@ -125,7 +125,7 @@ public class SyncPlayController : BaseJellyfinApiController
{
var currentSession = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
var group = _syncPlayManager.GetGroup(currentSession, id);
- return group == null ? NotFound() : Ok(group);
+ return group is null ? NotFound() : Ok(group);
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index 07a1f7650..450225c37 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -5,7 +5,6 @@ using System.IO;
using System.Linq;
using System.Net.Mime;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.SystemInfoDtos;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Configuration;
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index ebf98da45..5495f60d8 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -108,6 +108,7 @@ public class YearsController : BaseJellyfinApiController
bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
IReadOnlyList<BaseItem> items;
+ int totalCount = -1;
if (parentItem.IsFolder)
{
var folder = (Folder)parentItem;
@@ -118,7 +119,7 @@ public class YearsController : BaseJellyfinApiController
}
else
{
- items = recursive ? folder.GetRecursiveChildren(user, query) : folder.GetChildren(user, true).Where(Filter).ToArray();
+ items = recursive ? folder.GetRecursiveChildren(user, query, out totalCount) : folder.GetChildren(user, true).Where(Filter).ToArray();
}
}
else
@@ -153,7 +154,7 @@ public class YearsController : BaseJellyfinApiController
var result = new QueryResult<BaseItemDto>(
startIndex,
- ibnItemsArray.Count,
+ totalCount == -1 ? ibnItemsArray.Count : totalCount,
dtos.Where(i => i is not null).ToArray());
return result;
}
@@ -223,6 +224,6 @@ public class YearsController : BaseJellyfinApiController
.Select(i => i.ProductionYear ?? 0)
.Where(i => i > 0)
.Distinct()
- .Select(year => _libraryManager.GetYear(year));
+ .Select(_libraryManager.GetYear);
}
}