aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Api/Controllers
diff options
context:
space:
mode:
Diffstat (limited to 'Jellyfin.Api/Controllers')
-rw-r--r--Jellyfin.Api/Controllers/DisplayPreferencesController.cs15
-rw-r--r--Jellyfin.Api/Controllers/InstantMixController.cs40
-rw-r--r--Jellyfin.Api/Controllers/LibraryController.cs1
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs2
-rw-r--r--Jellyfin.Api/Controllers/LyricsController.cs267
-rw-r--r--Jellyfin.Api/Controllers/MediaInfoController.cs3
-rw-r--r--Jellyfin.Api/Controllers/PlaylistsController.cs7
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs46
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs45
9 files changed, 332 insertions, 94 deletions
diff --git a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
index 6f0006832b..1cad663264 100644
--- a/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
+++ b/Jellyfin.Api/Controllers/DisplayPreferencesController.cs
@@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
+using Jellyfin.Api.Helpers;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
@@ -48,15 +49,17 @@ public class DisplayPreferencesController : BaseJellyfinApiController
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
public ActionResult<DisplayPreferencesDto> GetDisplayPreferences(
[FromRoute, Required] string displayPreferencesId,
- [FromQuery, Required] Guid userId,
+ [FromQuery] Guid? userId,
[FromQuery, Required] string client)
{
+ userId = RequestHelpers.GetUserId(User, userId);
+
if (!Guid.TryParse(displayPreferencesId, out var itemId))
{
itemId = displayPreferencesId.GetMD5();
}
- var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
+ var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId.Value, itemId, client);
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
itemPreferences.ItemId = itemId;
@@ -113,10 +116,12 @@ public class DisplayPreferencesController : BaseJellyfinApiController
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
public ActionResult UpdateDisplayPreferences(
[FromRoute, Required] string displayPreferencesId,
- [FromQuery, Required] Guid userId,
+ [FromQuery] Guid? userId,
[FromQuery, Required] string client,
[FromBody, Required] DisplayPreferencesDto displayPreferences)
{
+ userId = RequestHelpers.GetUserId(User, userId);
+
HomeSectionType[] defaults =
{
HomeSectionType.SmallLibraryTiles,
@@ -134,7 +139,7 @@ public class DisplayPreferencesController : BaseJellyfinApiController
itemId = displayPreferencesId.GetMD5();
}
- var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
+ var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId.Value, itemId, client);
existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : null;
existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop;
existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar;
@@ -204,7 +209,7 @@ public class DisplayPreferencesController : BaseJellyfinApiController
itemPrefs.ItemId = itemId;
// Set all remaining custom preferences.
- _displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
+ _displayPreferencesManager.SetCustomItemDisplayPreferences(userId.Value, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
_displayPreferencesManager.SaveChanges();
return NoContent();
diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs
index e7ff1f9868..3cf4852995 100644
--- a/Jellyfin.Api/Controllers/InstantMixController.cs
+++ b/Jellyfin.Api/Controllers/InstantMixController.cs
@@ -53,7 +53,7 @@ public class InstantMixController : BaseJellyfinApiController
/// <summary>
/// Creates an instant playlist based on a given song.
/// </summary>
- /// <param name="id">The item id.</param>
+ /// <param name="itemId">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
@@ -63,10 +63,10 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
- [HttpGet("Songs/{id}/InstantMix")]
+ [HttpGet("Songs/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
- [FromRoute, Required] Guid id,
+ [FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
@@ -75,7 +75,7 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var item = _libraryManager.GetItemById(id);
+ var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
@@ -90,7 +90,7 @@ public class InstantMixController : BaseJellyfinApiController
/// <summary>
/// Creates an instant playlist based on a given album.
/// </summary>
- /// <param name="id">The item id.</param>
+ /// <param name="itemId">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
@@ -100,10 +100,10 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
- [HttpGet("Albums/{id}/InstantMix")]
+ [HttpGet("Albums/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
- [FromRoute, Required] Guid id,
+ [FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
@@ -112,7 +112,7 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var album = _libraryManager.GetItemById(id);
+ var album = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
@@ -127,7 +127,7 @@ public class InstantMixController : BaseJellyfinApiController
/// <summary>
/// Creates an instant playlist based on a given playlist.
/// </summary>
- /// <param name="id">The item id.</param>
+ /// <param name="itemId">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
@@ -137,10 +137,10 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
- [HttpGet("Playlists/{id}/InstantMix")]
+ [HttpGet("Playlists/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
- [FromRoute, Required] Guid id,
+ [FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
@@ -149,7 +149,7 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var playlist = (Playlist)_libraryManager.GetItemById(id);
+ var playlist = (Playlist)_libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
@@ -200,7 +200,7 @@ public class InstantMixController : BaseJellyfinApiController
/// <summary>
/// Creates an instant playlist based on a given artist.
/// </summary>
- /// <param name="id">The item id.</param>
+ /// <param name="itemId">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
@@ -210,10 +210,10 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
- [HttpGet("Artists/{id}/InstantMix")]
+ [HttpGet("Artists/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
- [FromRoute, Required] Guid id,
+ [FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
@@ -222,7 +222,7 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var item = _libraryManager.GetItemById(id);
+ var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
@@ -237,7 +237,7 @@ public class InstantMixController : BaseJellyfinApiController
/// <summary>
/// Creates an instant playlist based on a given item.
/// </summary>
- /// <param name="id">The item id.</param>
+ /// <param name="itemId">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
@@ -247,10 +247,10 @@ public class InstantMixController : BaseJellyfinApiController
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
- [HttpGet("Items/{id}/InstantMix")]
+ [HttpGet("Items/{itemId}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
- [FromRoute, Required] Guid id,
+ [FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
@@ -259,7 +259,7 @@ public class InstantMixController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
- var item = _libraryManager.GetItemById(id);
+ var item = _libraryManager.GetItemById(itemId);
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index e357588d1d..984dc77896 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -913,6 +913,7 @@ public class LibraryController : BaseJellyfinApiController
User.GetUserId())
{
ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), User.GetClient(), User.GetDevice()),
+ ItemId = item.Id.ToString("N", CultureInfo.InvariantCulture)
}).ConfigureAwait(false);
}
catch
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 78dd7a71cb..7768b3c45f 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -636,7 +636,7 @@ public class LiveTvController : BaseJellyfinApiController
[Authorize(Policy = Policies.LiveTvAccess)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
{
- var user = body.UserId.IsEmpty() ? null : _userManager.GetUserById(body.UserId);
+ var user = body.UserId.IsNullOrEmpty() ? null : _userManager.GetUserById(body.UserId.Value);
var query = new InternalItemsQuery(user)
{
diff --git a/Jellyfin.Api/Controllers/LyricsController.cs b/Jellyfin.Api/Controllers/LyricsController.cs
new file mode 100644
index 0000000000..4fccf2cb42
--- /dev/null
+++ b/Jellyfin.Api/Controllers/LyricsController.cs
@@ -0,0 +1,267 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.IO;
+using System.Net.Mime;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
+using Jellyfin.Api.Extensions;
+using Jellyfin.Extensions;
+using MediaBrowser.Common.Api;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Lyrics;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Lyrics;
+using MediaBrowser.Model.Providers;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Lyrics controller.
+/// </summary>
+[Route("")]
+public class LyricsController : BaseJellyfinApiController
+{
+ private readonly ILibraryManager _libraryManager;
+ private readonly ILyricManager _lyricManager;
+ private readonly IProviderManager _providerManager;
+ private readonly IFileSystem _fileSystem;
+ private readonly IUserManager _userManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="LyricsController"/> class.
+ /// </summary>
+ /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
+ /// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
+ /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
+ public LyricsController(
+ ILibraryManager libraryManager,
+ ILyricManager lyricManager,
+ IProviderManager providerManager,
+ IFileSystem fileSystem,
+ IUserManager userManager)
+ {
+ _libraryManager = libraryManager;
+ _lyricManager = lyricManager;
+ _providerManager = providerManager;
+ _fileSystem = fileSystem;
+ _userManager = userManager;
+ }
+
+ /// <summary>
+ /// Gets an item's lyrics.
+ /// </summary>
+ /// <param name="itemId">Item id.</param>
+ /// <response code="200">Lyrics returned.</response>
+ /// <response code="404">Something went wrong. No Lyrics will be returned.</response>
+ /// <returns>An <see cref="OkResult"/> containing the item's lyrics.</returns>
+ [HttpGet("Audio/{itemId}/Lyrics")]
+ [Authorize]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ActionResult<LyricDto>> GetLyrics([FromRoute, Required] Guid itemId)
+ {
+ var isApiKey = User.GetIsApiKey();
+ var userId = User.GetUserId();
+ if (!isApiKey && userId.IsEmpty())
+ {
+ return BadRequest();
+ }
+
+ var audio = _libraryManager.GetItemById<Audio>(itemId);
+ if (audio is null)
+ {
+ return NotFound();
+ }
+
+ if (!isApiKey)
+ {
+ var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ return NotFound();
+ }
+
+ // Check the item is visible for the user
+ if (!audio.IsVisible(user))
+ {
+ return Unauthorized($"{user.Username} is not permitted to access item {audio.Name}.");
+ }
+ }
+
+ var result = await _lyricManager.GetLyricsAsync(audio, CancellationToken.None).ConfigureAwait(false);
+ if (result is not null)
+ {
+ return Ok(result);
+ }
+
+ return NotFound();
+ }
+
+ /// <summary>
+ /// Upload an external lyric file.
+ /// </summary>
+ /// <param name="itemId">The item the lyric belongs to.</param>
+ /// <param name="fileName">Name of the file being uploaded.</param>
+ /// <response code="200">Lyrics uploaded.</response>
+ /// <response code="400">Error processing upload.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>The uploaded lyric.</returns>
+ [HttpPost("Audio/{itemId}/Lyrics")]
+ [Authorize(Policy = Policies.LyricManagement)]
+ [AcceptsFile(MediaTypeNames.Text.Plain)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<LyricDto>> UploadLyrics(
+ [FromRoute, Required] Guid itemId,
+ [FromQuery, Required] string fileName)
+ {
+ var audio = _libraryManager.GetItemById<Audio>(itemId);
+ if (audio is null)
+ {
+ return NotFound();
+ }
+
+ if (Request.ContentLength.GetValueOrDefault(0) == 0)
+ {
+ return BadRequest("No lyrics uploaded");
+ }
+
+ // Utilize Path.GetExtension as it provides extra path validation.
+ var format = Path.GetExtension(fileName.AsSpan()).RightPart('.').ToString();
+ if (string.IsNullOrEmpty(format))
+ {
+ return BadRequest("Extension is required on filename");
+ }
+
+ var stream = new MemoryStream();
+ await using (stream.ConfigureAwait(false))
+ {
+ await Request.Body.CopyToAsync(stream).ConfigureAwait(false);
+ var uploadedLyric = await _lyricManager.UploadLyricAsync(
+ audio,
+ new LyricResponse
+ {
+ Format = format,
+ Stream = stream
+ }).ConfigureAwait(false);
+
+ if (uploadedLyric is null)
+ {
+ return BadRequest();
+ }
+
+ _providerManager.QueueRefresh(audio.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
+ return Ok(uploadedLyric);
+ }
+ }
+
+ /// <summary>
+ /// Deletes an external lyric file.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <response code="204">Lyric deleted.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpDelete("Audio/{itemId}/Lyrics")]
+ [Authorize(Policy = Policies.LyricManagement)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult> DeleteLyrics(
+ [FromRoute, Required] Guid itemId)
+ {
+ var audio = _libraryManager.GetItemById<Audio>(itemId);
+ if (audio is null)
+ {
+ return NotFound();
+ }
+
+ await _lyricManager.DeleteLyricsAsync(audio).ConfigureAwait(false);
+ return NoContent();
+ }
+
+ /// <summary>
+ /// Search remote lyrics.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <response code="200">Lyrics retrieved.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>An array of <see cref="RemoteLyricInfo"/>.</returns>
+ [HttpGet("Audio/{itemId}/RemoteSearch/Lyrics")]
+ [Authorize(Policy = Policies.LyricManagement)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<IReadOnlyList<RemoteLyricInfoDto>>> SearchRemoteLyrics([FromRoute, Required] Guid itemId)
+ {
+ var audio = _libraryManager.GetItemById<Audio>(itemId);
+ if (audio is null)
+ {
+ return NotFound();
+ }
+
+ var results = await _lyricManager.SearchLyricsAsync(audio, false, CancellationToken.None).ConfigureAwait(false);
+ return Ok(results);
+ }
+
+ /// <summary>
+ /// Downloads a remote lyric.
+ /// </summary>
+ /// <param name="itemId">The item id.</param>
+ /// <param name="lyricId">The lyric id.</param>
+ /// <response code="200">Lyric downloaded.</response>
+ /// <response code="404">Item not found.</response>
+ /// <returns>A <see cref="NoContentResult"/>.</returns>
+ [HttpPost("Audio/{itemId}/RemoteSearch/Lyrics/{lyricId}")]
+ [Authorize(Policy = Policies.LyricManagement)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<LyricDto>> DownloadRemoteLyrics(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] string lyricId)
+ {
+ var audio = _libraryManager.GetItemById<Audio>(itemId);
+ if (audio is null)
+ {
+ return NotFound();
+ }
+
+ var downloadedLyrics = await _lyricManager.DownloadLyricsAsync(audio, lyricId, CancellationToken.None).ConfigureAwait(false);
+ if (downloadedLyrics is null)
+ {
+ return NotFound();
+ }
+
+ _providerManager.QueueRefresh(audio.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
+ return Ok(downloadedLyrics);
+ }
+
+ /// <summary>
+ /// Gets the remote lyrics.
+ /// </summary>
+ /// <param name="lyricId">The remote provider item id.</param>
+ /// <response code="200">File returned.</response>
+ /// <response code="404">Lyric not found.</response>
+ /// <returns>A <see cref="FileStreamResult"/> with the lyric file.</returns>
+ [HttpGet("Providers/Lyrics/{lyricId}")]
+ [Authorize(Policy = Policies.LyricManagement)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task<ActionResult<LyricDto>> GetRemoteLyrics([FromRoute, Required] string lyricId)
+ {
+ var result = await _lyricManager.GetRemoteLyricsAsync(lyricId, CancellationToken.None).ConfigureAwait(false);
+ if (result is null)
+ {
+ return NotFound();
+ }
+
+ return Ok(result);
+ }
+}
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index bea545cfda..742012b71e 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -64,8 +64,9 @@ public class MediaInfoController : BaseJellyfinApiController
/// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
[HttpGet("Items/{itemId}/PlaybackInfo")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery, Required] Guid userId)
+ public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
{
+ userId = RequestHelpers.GetUserId(User, userId);
return await _mediaInfoHelper.GetPlaybackInfo(
itemId,
userId)
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 921cc6031f..0e7c3f1556 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -174,7 +174,7 @@ public class PlaylistsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems(
[FromRoute, Required] Guid playlistId,
- [FromQuery, Required] Guid userId,
+ [FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
@@ -183,15 +183,16 @@ public class PlaylistsController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
+ userId = RequestHelpers.GetUserId(User, userId);
var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
if (playlist is null)
{
return NotFound();
}
- var user = userId.IsEmpty()
+ var user = userId.IsNullOrEmpty()
? null
- : _userManager.GetUserById(userId);
+ : _userManager.GetUserById(userId.Value);
var items = playlist.GetManageableItems().ToArray();
var count = items.Length;
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 49ca058bd4..cc2a630e1d 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -11,7 +11,6 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.SubtitleDtos;
using MediaBrowser.Common.Api;
@@ -162,17 +161,17 @@ public class SubtitleController : BaseJellyfinApiController
/// <summary>
/// Gets the remote subtitles.
/// </summary>
- /// <param name="id">The item id.</param>
+ /// <param name="subtitleId">The item id.</param>
/// <response code="200">File returned.</response>
/// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns>
- [HttpGet("Providers/Subtitles/Subtitles/{id}")]
+ [HttpGet("Providers/Subtitles/Subtitles/{subtitleId}")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[Produces(MediaTypeNames.Application.Octet)]
[ProducesFile("text/*")]
- public async Task<ActionResult> GetRemoteSubtitles([FromRoute, Required] string id)
+ public async Task<ActionResult> GetRemoteSubtitles([FromRoute, Required] string subtitleId)
{
- var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false);
+ var result = await _subtitleManager.GetRemoteSubtitles(subtitleId, CancellationToken.None).ConfigureAwait(false);
return File(result.Stream, MimeTypes.GetMimeType("file." + result.Format));
}
@@ -407,22 +406,29 @@ public class SubtitleController : BaseJellyfinApiController
[FromBody, Required] UploadSubtitleDto body)
{
var video = (Video)_libraryManager.GetItemById(itemId);
- var stream = new CryptoStream(Request.Body, new FromBase64Transform(), CryptoStreamMode.Read);
- await using (stream.ConfigureAwait(false))
- {
- await _subtitleManager.UploadSubtitle(
- video,
- new SubtitleResponse
- {
- Format = body.Format,
- Language = body.Language,
- IsForced = body.IsForced,
- IsHearingImpaired = body.IsHearingImpaired,
- Stream = stream
- }).ConfigureAwait(false);
- _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
- return NoContent();
+ var bytes = Encoding.UTF8.GetBytes(body.Data);
+ var memoryStream = new MemoryStream(bytes, 0, bytes.Length, false, true);
+ await using (memoryStream.ConfigureAwait(false))
+ {
+ using var transform = new FromBase64Transform();
+ var stream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Read);
+ await using (stream.ConfigureAwait(false))
+ {
+ await _subtitleManager.UploadSubtitle(
+ video,
+ new SubtitleResponse
+ {
+ Format = body.Format,
+ Language = body.Language,
+ IsForced = body.IsForced,
+ IsHearingImpaired = body.IsHearingImpaired,
+ Stream = stream
+ }).ConfigureAwait(false);
+ _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
+
+ return NoContent();
+ }
}
}
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index 264e0a3dbd..e3bfd4ea9c 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -18,6 +18,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Lyrics;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -539,48 +540,4 @@ public class UserLibraryController : BaseJellyfinApiController
return _userDataRepository.GetUserDataDto(item, user);
}
-
- /// <summary>
- /// Gets an item's lyrics.
- /// </summary>
- /// <param name="userId">User id.</param>
- /// <param name="itemId">Item id.</param>
- /// <response code="200">Lyrics returned.</response>
- /// <response code="404">Something went wrong. No Lyrics will be returned.</response>
- /// <returns>An <see cref="OkResult"/> containing the item's lyrics.</returns>
- [HttpGet("Users/{userId}/Items/{itemId}/Lyrics")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ActionResult<LyricResponse>> GetLyrics([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
- {
- var user = _userManager.GetUserById(userId);
-
- if (user is null)
- {
- return NotFound();
- }
-
- var item = itemId.IsEmpty()
- ? _libraryManager.GetUserRootFolder()
- : _libraryManager.GetItemById(itemId);
-
- if (item is null)
- {
- return NotFound();
- }
-
- if (item is not UserRootFolder
- // Check the item is visible for the user
- && !item.IsVisible(user))
- {
- return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
- }
-
- var result = await _lyricManager.GetLyrics(item).ConfigureAwait(false);
- if (result is not null)
- {
- return Ok(result);
- }
-
- return NotFound();
- }
}