< Summary - Jellyfin

Information
Class: Jellyfin.Api.Controllers.PlaystateController
Assembly: Jellyfin.Api
File(s): /srv/git/jellyfin/Jellyfin.Api/Controllers/PlaystateController.cs
Line coverage
40%
Covered lines: 30
Uncovered lines: 44
Coverable lines: 74
Total lines: 540
Line coverage: 40.5%
Branch coverage
21%
Covered branches: 6
Total branches: 28
Branch coverage: 21.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 2/14/2026 - 12:11:17 AM Line coverage: 56% (14/25) Branch coverage: 0% (0/8) Total lines: 5354/19/2026 - 12:14:27 AM Line coverage: 40.5% (30/74) Branch coverage: 21.4% (6/28) Total lines: 5354/30/2026 - 12:14:58 AM Line coverage: 40.5% (30/74) Branch coverage: 21.4% (6/28) Total lines: 5385/13/2026 - 12:15:27 AM Line coverage: 40.5% (30/74) Branch coverage: 21.4% (6/28) Total lines: 540 2/14/2026 - 12:11:17 AM Line coverage: 56% (14/25) Branch coverage: 0% (0/8) Total lines: 5354/19/2026 - 12:14:27 AM Line coverage: 40.5% (30/74) Branch coverage: 21.4% (6/28) Total lines: 5354/30/2026 - 12:14:58 AM Line coverage: 40.5% (30/74) Branch coverage: 21.4% (6/28) Total lines: 5385/13/2026 - 12:15:27 AM Line coverage: 40.5% (30/74) Branch coverage: 21.4% (6/28) Total lines: 540

Coverage delta

Coverage delta 22 -22

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
MarkPlayedItem()37.5%16850%
MarkUnplayedItem()37.5%16850%
ReportPlaybackStart()100%210%
ReportPlaybackProgress()100%210%
PingPlaybackSession(...)100%210%
ReportPlaybackStopped()0%2040%
UpdatePlayedStatus(...)0%620%
ValidatePlayMethod(...)0%4260%

File(s)

/srv/git/jellyfin/Jellyfin.Api/Controllers/PlaystateController.cs

#LineLine coverage
 1using System;
 2using System.ComponentModel.DataAnnotations;
 3using System.Diagnostics.CodeAnalysis;
 4using System.Threading.Tasks;
 5using Jellyfin.Api.Extensions;
 6using Jellyfin.Api.Helpers;
 7using Jellyfin.Api.ModelBinders;
 8using Jellyfin.Database.Implementations.Entities;
 9using MediaBrowser.Controller.Entities;
 10using MediaBrowser.Controller.Library;
 11using MediaBrowser.Controller.MediaEncoding;
 12using MediaBrowser.Controller.Session;
 13using MediaBrowser.Model.Dto;
 14using MediaBrowser.Model.Session;
 15using Microsoft.AspNetCore.Authorization;
 16using Microsoft.AspNetCore.Http;
 17using Microsoft.AspNetCore.Mvc;
 18using Microsoft.Extensions.Logging;
 19
 20namespace Jellyfin.Api.Controllers;
 21
 22/// <summary>
 23/// Playstate controller.
 24/// </summary>
 25[Route("")]
 26[Authorize]
 27[Tags("Session")]
 28public class PlaystateController : BaseJellyfinApiController
 29{
 30    private readonly IUserManager _userManager;
 31    private readonly IUserDataManager _userDataRepository;
 32    private readonly ILibraryManager _libraryManager;
 33    private readonly ISessionManager _sessionManager;
 34    private readonly ILogger<PlaystateController> _logger;
 35    private readonly ITranscodeManager _transcodeManager;
 36
 37    /// <summary>
 38    /// Initializes a new instance of the <see cref="PlaystateController"/> class.
 39    /// </summary>
 40    /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
 41    /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
 42    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 43    /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
 44    /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
 45    /// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
 446    public PlaystateController(
 447        IUserManager userManager,
 448        IUserDataManager userDataRepository,
 449        ILibraryManager libraryManager,
 450        ISessionManager sessionManager,
 451        ILoggerFactory loggerFactory,
 452        ITranscodeManager transcodeManager)
 53    {
 454        _userManager = userManager;
 455        _userDataRepository = userDataRepository;
 456        _libraryManager = libraryManager;
 457        _sessionManager = sessionManager;
 458        _logger = loggerFactory.CreateLogger<PlaystateController>();
 59
 460        _transcodeManager = transcodeManager;
 461    }
 62
 63    /// <summary>
 64    /// Marks an item as played for user.
 65    /// </summary>
 66    /// <param name="userId">User id.</param>
 67    /// <param name="itemId">Item id.</param>
 68    /// <param name="datePlayed">Optional. The date the item was played.</param>
 69    /// <response code="200">Item marked as played.</response>
 70    /// <response code="404">Item not found.</response>
 71    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>, or a <see cref="NotFoundResult"
 72    [HttpPost("UserPlayedItems/{itemId}")]
 73    [ProducesResponseType(StatusCodes.Status200OK)]
 74    [ProducesResponseType(StatusCodes.Status404NotFound)]
 75    [Tags("UserData")]
 76    public async Task<ActionResult<UserItemDataDto?>> MarkPlayedItem(
 77        [FromQuery] Guid? userId,
 78        [FromRoute, Required] Guid itemId,
 79        [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
 80    {
 281        userId = RequestHelpers.GetUserId(User, userId);
 282        var user = _userManager.GetUserById(userId.Value);
 283        if (user is null)
 84        {
 185            return NotFound();
 86        }
 87
 188        var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
 189        if (item is null)
 90        {
 191            return NotFound();
 92        }
 93
 094        var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext, userId).ConfigureAwait
 95
 096        var dto = UpdatePlayedStatus(user, item, true, datePlayed);
 097        foreach (var additionalUserInfo in session.AdditionalUsers)
 98        {
 099            var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
 0100            if (additionalUser is null)
 101            {
 0102                return NotFound();
 103            }
 104
 0105            UpdatePlayedStatus(additionalUser, item, true, datePlayed);
 106        }
 107
 0108        return dto;
 2109    }
 110
 111    /// <summary>
 112    /// Marks an item as played for user.
 113    /// </summary>
 114    /// <param name="userId">User id.</param>
 115    /// <param name="itemId">Item id.</param>
 116    /// <param name="datePlayed">Optional. The date the item was played.</param>
 117    /// <response code="200">Item marked as played.</response>
 118    /// <response code="404">Item not found.</response>
 119    /// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>, or a <see cref="NotFoundResult"
 120    [HttpPost("Users/{userId}/PlayedItems/{itemId}")]
 121    [ProducesResponseType(StatusCodes.Status200OK)]
 122    [ProducesResponseType(StatusCodes.Status404NotFound)]
 123    [Obsolete("Kept for backwards compatibility")]
 124    [ApiExplorerSettings(IgnoreApi = true)]
 125    public Task<ActionResult<UserItemDataDto?>> MarkPlayedItemLegacy(
 126        [FromRoute, Required] Guid userId,
 127        [FromRoute, Required] Guid itemId,
 128        [FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
 129        => MarkPlayedItem(userId, itemId, datePlayed);
 130
 131    /// <summary>
 132    /// Marks an item as unplayed for user.
 133    /// </summary>
 134    /// <param name="userId">User id.</param>
 135    /// <param name="itemId">Item id.</param>
 136    /// <response code="200">Item marked as unplayed.</response>
 137    /// <response code="404">Item not found.</response>
 138    /// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>, or a <see cref="NotFoundResult"/
 139    [HttpDelete("UserPlayedItems/{itemId}")]
 140    [ProducesResponseType(StatusCodes.Status200OK)]
 141    [ProducesResponseType(StatusCodes.Status404NotFound)]
 142    [Tags("UserData")]
 143    public async Task<ActionResult<UserItemDataDto?>> MarkUnplayedItem(
 144        [FromQuery] Guid? userId,
 145        [FromRoute, Required] Guid itemId)
 146    {
 2147        userId = RequestHelpers.GetUserId(User, userId);
 2148        var user = _userManager.GetUserById(userId.Value);
 2149        if (user is null)
 150        {
 1151            return NotFound();
 152        }
 153
 1154        var item = _libraryManager.GetItemById<BaseItem>(itemId, user);
 1155        if (item is null)
 156        {
 1157            return NotFound();
 158        }
 159
 0160        var session = await RequestHelpers.GetSession(_sessionManager, _userManager, HttpContext, userId).ConfigureAwait
 161
 0162        var dto = UpdatePlayedStatus(user, item, false, null);
 0163        foreach (var additionalUserInfo in session.AdditionalUsers)
 164        {
 0165            var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
 0166            if (additionalUser is null)
 167            {
 0168                return NotFound();
 169            }
 170
 0171            UpdatePlayedStatus(additionalUser, item, false, null);
 172        }
 173
 0174        return dto;
 2175    }
 176
 177    /// <summary>
 178    /// Marks an item as unplayed for user.
 179    /// </summary>
 180    /// <param name="userId">User id.</param>
 181    /// <param name="itemId">Item id.</param>
 182    /// <response code="200">Item marked as unplayed.</response>
 183    /// <response code="404">Item not found.</response>
 184    /// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>, or a <see cref="NotFoundResult"/
 185    [HttpDelete("Users/{userId}/PlayedItems/{itemId}")]
 186    [ProducesResponseType(StatusCodes.Status200OK)]
 187    [ProducesResponseType(StatusCodes.Status404NotFound)]
 188    [Obsolete("Kept for backwards compatibility")]
 189    [ApiExplorerSettings(IgnoreApi = true)]
 190    public Task<ActionResult<UserItemDataDto?>> MarkUnplayedItemLegacy(
 191        [FromRoute, Required] Guid userId,
 192        [FromRoute, Required] Guid itemId)
 193        => MarkUnplayedItem(userId, itemId);
 194
 195    /// <summary>
 196    /// Reports playback has started within a session.
 197    /// </summary>
 198    /// <param name="playbackStartInfo">The playback start info.</param>
 199    /// <response code="204">Playback start recorded.</response>
 200    /// <returns>A <see cref="NoContentResult"/>.</returns>
 201    [HttpPost("Sessions/Playing")]
 202    [ProducesResponseType(StatusCodes.Status204NoContent)]
 203    public async Task<ActionResult> ReportPlaybackStart([FromBody] PlaybackStartInfo playbackStartInfo)
 204    {
 0205        playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId)
 0206        playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).Conf
 0207        await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false);
 0208        return NoContent();
 0209    }
 210
 211    /// <summary>
 212    /// Reports playback progress within a session.
 213    /// </summary>
 214    /// <param name="playbackProgressInfo">The playback progress info.</param>
 215    /// <response code="204">Playback progress recorded.</response>
 216    /// <returns>A <see cref="NoContentResult"/>.</returns>
 217    [HttpPost("Sessions/Playing/Progress")]
 218    [ProducesResponseType(StatusCodes.Status204NoContent)]
 219    public async Task<ActionResult> ReportPlaybackProgress([FromBody] PlaybackProgressInfo playbackProgressInfo)
 220    {
 0221        playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlayS
 0222        playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).C
 0223        await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false);
 0224        return NoContent();
 0225    }
 226
 227    /// <summary>
 228    /// Pings a playback session.
 229    /// </summary>
 230    /// <param name="playSessionId">Playback session id.</param>
 231    /// <response code="204">Playback session pinged.</response>
 232    /// <returns>A <see cref="NoContentResult"/>.</returns>
 233    [HttpPost("Sessions/Playing/Ping")]
 234    [ProducesResponseType(StatusCodes.Status204NoContent)]
 235    public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId)
 236    {
 0237        _transcodeManager.PingTranscodingJob(playSessionId, null);
 0238        return NoContent();
 239    }
 240
 241    /// <summary>
 242    /// Reports playback has stopped within a session.
 243    /// </summary>
 244    /// <param name="playbackStopInfo">The playback stop info.</param>
 245    /// <response code="204">Playback stop recorded.</response>
 246    /// <returns>A <see cref="NoContentResult"/>.</returns>
 247    [HttpPost("Sessions/Playing/Stopped")]
 248    [ProducesResponseType(StatusCodes.Status204NoContent)]
 249    public async Task<ActionResult> ReportPlaybackStopped([FromBody] PlaybackStopInfo playbackStopInfo)
 250    {
 0251        _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
 0252        if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
 253        {
 0254            await _transcodeManager.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).
 255        }
 256
 0257        playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).Confi
 0258        await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
 0259        return NoContent();
 0260    }
 261
 262    /// <summary>
 263    /// Reports that a session has begun playing an item.
 264    /// </summary>
 265    /// <param name="itemId">Item id.</param>
 266    /// <param name="mediaSourceId">The id of the MediaSource.</param>
 267    /// <param name="audioStreamIndex">The audio stream index.</param>
 268    /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
 269    /// <param name="playMethod">The play method.</param>
 270    /// <param name="liveStreamId">The live stream id.</param>
 271    /// <param name="playSessionId">The play session id.</param>
 272    /// <param name="canSeek">Indicates if the client can seek.</param>
 273    /// <response code="204">Play start recorded.</response>
 274    /// <returns>A <see cref="NoContentResult"/>.</returns>
 275    [HttpPost("PlayingItems/{itemId}")]
 276    [ProducesResponseType(StatusCodes.Status204NoContent)]
 277    [Obsolete("This endpoint is obsolete. Use ReportPlaybackStart instead")]
 278    [ApiExplorerSettings(IgnoreApi = true)]
 279    public async Task<ActionResult> OnPlaybackStart(
 280        [FromRoute, Required] Guid itemId,
 281        [FromQuery] string? mediaSourceId,
 282        [FromQuery] int? audioStreamIndex,
 283        [FromQuery] int? subtitleStreamIndex,
 284        [FromQuery] PlayMethod? playMethod,
 285        [FromQuery] string? liveStreamId,
 286        [FromQuery] string? playSessionId,
 287        [FromQuery] bool canSeek = false)
 288    {
 289        var playbackStartInfo = new PlaybackStartInfo
 290        {
 291            CanSeek = canSeek,
 292            ItemId = itemId,
 293            MediaSourceId = mediaSourceId,
 294            AudioStreamIndex = audioStreamIndex,
 295            SubtitleStreamIndex = subtitleStreamIndex,
 296            PlayMethod = playMethod ?? PlayMethod.Transcode,
 297            PlaySessionId = playSessionId,
 298            LiveStreamId = liveStreamId
 299        };
 300
 301        playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId)
 302        playbackStartInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).Conf
 303        await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false);
 304        return NoContent();
 305    }
 306
 307    /// <summary>
 308    /// Reports that a user has begun playing an item.
 309    /// </summary>
 310    /// <param name="userId">User id.</param>
 311    /// <param name="itemId">Item id.</param>
 312    /// <param name="mediaSourceId">The id of the MediaSource.</param>
 313    /// <param name="audioStreamIndex">The audio stream index.</param>
 314    /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
 315    /// <param name="playMethod">The play method.</param>
 316    /// <param name="liveStreamId">The live stream id.</param>
 317    /// <param name="playSessionId">The play session id.</param>
 318    /// <param name="canSeek">Indicates if the client can seek.</param>
 319    /// <response code="204">Play start recorded.</response>
 320    /// <returns>A <see cref="NoContentResult"/>.</returns>
 321    [HttpPost("Users/{userId}/PlayingItems/{itemId}")]
 322    [ProducesResponseType(StatusCodes.Status204NoContent)]
 323    [Obsolete("Kept for backwards compatibility")]
 324    [ApiExplorerSettings(IgnoreApi = true)]
 325    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Re
 326    public Task<ActionResult> OnPlaybackStartLegacy(
 327        [FromRoute, Required] Guid userId,
 328        [FromRoute, Required] Guid itemId,
 329        [FromQuery] string? mediaSourceId,
 330        [FromQuery] int? audioStreamIndex,
 331        [FromQuery] int? subtitleStreamIndex,
 332        [FromQuery] PlayMethod? playMethod,
 333        [FromQuery] string? liveStreamId,
 334        [FromQuery] string? playSessionId,
 335        [FromQuery] bool canSeek = false)
 336        => OnPlaybackStart(itemId, mediaSourceId, audioStreamIndex, subtitleStreamIndex, playMethod, liveStreamId, playS
 337
 338    /// <summary>
 339    /// Reports a session's playback progress.
 340    /// </summary>
 341    /// <param name="itemId">Item id.</param>
 342    /// <param name="mediaSourceId">The id of the MediaSource.</param>
 343    /// <param name="positionTicks">Optional. The current position, in ticks. 1 tick = 10000 ms.</param>
 344    /// <param name="audioStreamIndex">The audio stream index.</param>
 345    /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
 346    /// <param name="volumeLevel">Scale of 0-100.</param>
 347    /// <param name="playMethod">The play method.</param>
 348    /// <param name="liveStreamId">The live stream id.</param>
 349    /// <param name="playSessionId">The play session id.</param>
 350    /// <param name="repeatMode">The repeat mode.</param>
 351    /// <param name="isPaused">Indicates if the player is paused.</param>
 352    /// <param name="isMuted">Indicates if the player is muted.</param>
 353    /// <response code="204">Play progress recorded.</response>
 354    /// <returns>A <see cref="NoContentResult"/>.</returns>
 355    [HttpPost("PlayingItems/{itemId}/Progress")]
 356    [ProducesResponseType(StatusCodes.Status204NoContent)]
 357    [Obsolete("This endpoint is obsolete. Use ReportPlaybackProgress instead")]
 358    [ApiExplorerSettings(IgnoreApi = true)]
 359    public async Task<ActionResult> OnPlaybackProgress(
 360        [FromRoute, Required] Guid itemId,
 361        [FromQuery] string? mediaSourceId,
 362        [FromQuery] long? positionTicks,
 363        [FromQuery] int? audioStreamIndex,
 364        [FromQuery] int? subtitleStreamIndex,
 365        [FromQuery] int? volumeLevel,
 366        [FromQuery] PlayMethod? playMethod,
 367        [FromQuery] string? liveStreamId,
 368        [FromQuery] string? playSessionId,
 369        [FromQuery] RepeatMode? repeatMode,
 370        [FromQuery] bool isPaused = false,
 371        [FromQuery] bool isMuted = false)
 372    {
 373        var playbackProgressInfo = new PlaybackProgressInfo
 374        {
 375            ItemId = itemId,
 376            PositionTicks = positionTicks,
 377            IsMuted = isMuted,
 378            IsPaused = isPaused,
 379            MediaSourceId = mediaSourceId,
 380            AudioStreamIndex = audioStreamIndex,
 381            SubtitleStreamIndex = subtitleStreamIndex,
 382            VolumeLevel = volumeLevel,
 383            PlayMethod = playMethod ?? PlayMethod.Transcode,
 384            PlaySessionId = playSessionId,
 385            LiveStreamId = liveStreamId,
 386            RepeatMode = repeatMode ?? RepeatMode.RepeatNone
 387        };
 388
 389        playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlayS
 390        playbackProgressInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).C
 391        await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false);
 392        return NoContent();
 393    }
 394
 395    /// <summary>
 396    /// Reports a user's playback progress.
 397    /// </summary>
 398    /// <param name="userId">User id.</param>
 399    /// <param name="itemId">Item id.</param>
 400    /// <param name="mediaSourceId">The id of the MediaSource.</param>
 401    /// <param name="positionTicks">Optional. The current position, in ticks. 1 tick = 10000 ms.</param>
 402    /// <param name="audioStreamIndex">The audio stream index.</param>
 403    /// <param name="subtitleStreamIndex">The subtitle stream index.</param>
 404    /// <param name="volumeLevel">Scale of 0-100.</param>
 405    /// <param name="playMethod">The play method.</param>
 406    /// <param name="liveStreamId">The live stream id.</param>
 407    /// <param name="playSessionId">The play session id.</param>
 408    /// <param name="repeatMode">The repeat mode.</param>
 409    /// <param name="isPaused">Indicates if the player is paused.</param>
 410    /// <param name="isMuted">Indicates if the player is muted.</param>
 411    /// <response code="204">Play progress recorded.</response>
 412    /// <returns>A <see cref="NoContentResult"/>.</returns>
 413    [HttpPost("Users/{userId}/PlayingItems/{itemId}/Progress")]
 414    [ProducesResponseType(StatusCodes.Status204NoContent)]
 415    [Obsolete("Kept for backwards compatibility")]
 416    [ApiExplorerSettings(IgnoreApi = true)]
 417    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Re
 418    public Task<ActionResult> OnPlaybackProgressLegacy(
 419        [FromRoute, Required] Guid userId,
 420        [FromRoute, Required] Guid itemId,
 421        [FromQuery] string? mediaSourceId,
 422        [FromQuery] long? positionTicks,
 423        [FromQuery] int? audioStreamIndex,
 424        [FromQuery] int? subtitleStreamIndex,
 425        [FromQuery] int? volumeLevel,
 426        [FromQuery] PlayMethod? playMethod,
 427        [FromQuery] string? liveStreamId,
 428        [FromQuery] string? playSessionId,
 429        [FromQuery] RepeatMode? repeatMode,
 430        [FromQuery] bool isPaused = false,
 431        [FromQuery] bool isMuted = false)
 432        => OnPlaybackProgress(itemId, mediaSourceId, positionTicks, audioStreamIndex, subtitleStreamIndex, volumeLevel, 
 433
 434    /// <summary>
 435    /// Reports that a session has stopped playing an item.
 436    /// </summary>
 437    /// <param name="itemId">Item id.</param>
 438    /// <param name="mediaSourceId">The id of the MediaSource.</param>
 439    /// <param name="nextMediaType">The next media type that will play.</param>
 440    /// <param name="positionTicks">Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms.</param>
 441    /// <param name="liveStreamId">The live stream id.</param>
 442    /// <param name="playSessionId">The play session id.</param>
 443    /// <response code="204">Playback stop recorded.</response>
 444    /// <returns>A <see cref="NoContentResult"/>.</returns>
 445    [HttpDelete("PlayingItems/{itemId}")]
 446    [ProducesResponseType(StatusCodes.Status204NoContent)]
 447    [Obsolete("This endpoint is obsolete. Use ReportPlaybackStop instead")]
 448    [ApiExplorerSettings(IgnoreApi = true)]
 449    public async Task<ActionResult> OnPlaybackStopped(
 450        [FromRoute, Required] Guid itemId,
 451        [FromQuery] string? mediaSourceId,
 452        [FromQuery] string? nextMediaType,
 453        [FromQuery] long? positionTicks,
 454        [FromQuery] string? liveStreamId,
 455        [FromQuery] string? playSessionId)
 456    {
 457        var playbackStopInfo = new PlaybackStopInfo
 458        {
 459            ItemId = itemId,
 460            PositionTicks = positionTicks,
 461            MediaSourceId = mediaSourceId,
 462            PlaySessionId = playSessionId,
 463            LiveStreamId = liveStreamId,
 464            NextMediaType = nextMediaType
 465        };
 466
 467        _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
 468        if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
 469        {
 470            await _transcodeManager.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).
 471        }
 472
 473        playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).Confi
 474        await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
 475        return NoContent();
 476    }
 477
 478    /// <summary>
 479    /// Reports that a user has stopped playing an item.
 480    /// </summary>
 481    /// <param name="userId">User id.</param>
 482    /// <param name="itemId">Item id.</param>
 483    /// <param name="mediaSourceId">The id of the MediaSource.</param>
 484    /// <param name="nextMediaType">The next media type that will play.</param>
 485    /// <param name="positionTicks">Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms.</param>
 486    /// <param name="liveStreamId">The live stream id.</param>
 487    /// <param name="playSessionId">The play session id.</param>
 488    /// <response code="204">Playback stop recorded.</response>
 489    /// <returns>A <see cref="NoContentResult"/>.</returns>
 490    [HttpDelete("Users/{userId}/PlayingItems/{itemId}")]
 491    [ProducesResponseType(StatusCodes.Status204NoContent)]
 492    [Obsolete("Kept for backwards compatibility")]
 493    [ApiExplorerSettings(IgnoreApi = true)]
 494    [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Re
 495    public Task<ActionResult> OnPlaybackStoppedLegacy(
 496        [FromRoute, Required] Guid userId,
 497        [FromRoute, Required] Guid itemId,
 498        [FromQuery] string? mediaSourceId,
 499        [FromQuery] string? nextMediaType,
 500        [FromQuery] long? positionTicks,
 501        [FromQuery] string? liveStreamId,
 502        [FromQuery] string? playSessionId)
 503        => OnPlaybackStopped(itemId, mediaSourceId, nextMediaType, positionTicks, liveStreamId, playSessionId);
 504
 505    /// <summary>
 506    /// Updates the played status.
 507    /// </summary>
 508    /// <param name="user">The user.</param>
 509    /// <param name="item">The item.</param>
 510    /// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
 511    /// <param name="datePlayed">The date played.</param>
 512    /// <returns>Task.</returns>
 513    private UserItemDataDto? UpdatePlayedStatus(User user, BaseItem item, bool wasPlayed, DateTime? datePlayed)
 514    {
 0515        if (wasPlayed)
 516        {
 0517            item.MarkPlayed(user, datePlayed, true);
 518        }
 519        else
 520        {
 0521            item.MarkUnplayed(user);
 522        }
 523
 0524        return _userDataRepository.GetUserDataDto(item, user);
 525    }
 526
 527    private PlayMethod ValidatePlayMethod(PlayMethod method, string? playSessionId)
 528    {
 0529        if (method == PlayMethod.Transcode)
 530        {
 0531            var job = string.IsNullOrWhiteSpace(playSessionId) ? null : _transcodeManager.GetTranscodingJob(playSessionI
 0532            if (job is null)
 533            {
 0534                return PlayMethod.DirectPlay;
 535            }
 536        }
 537
 0538        return method;
 539    }
 540}