314 lines
9.5 KiB
C#
314 lines
9.5 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Jellyfin.Plugin.JellyLMS.Configuration;
|
|
using Jellyfin.Plugin.JellyLMS.Models;
|
|
using MediaBrowser.Controller.Library;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Jellyfin.Plugin.JellyLMS.Services;
|
|
|
|
/// <summary>
|
|
/// Manages active playback sessions between Jellyfin and LMS.
|
|
/// </summary>
|
|
public class LmsSessionManager : IHostedService
|
|
{
|
|
private readonly ILogger<LmsSessionManager> _logger;
|
|
private readonly ILmsApiClient _lmsClient;
|
|
private readonly LmsPlayerManager _playerManager;
|
|
private readonly ILibraryManager _libraryManager;
|
|
private readonly ConcurrentDictionary<string, LmsPlaybackSession> _sessions = new();
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="LmsSessionManager"/> class.
|
|
/// </summary>
|
|
/// <param name="logger">The logger instance.</param>
|
|
/// <param name="lmsClient">The LMS API client.</param>
|
|
/// <param name="playerManager">The player manager.</param>
|
|
/// <param name="libraryManager">The Jellyfin library manager.</param>
|
|
public LmsSessionManager(
|
|
ILogger<LmsSessionManager> logger,
|
|
ILmsApiClient lmsClient,
|
|
LmsPlayerManager playerManager,
|
|
ILibraryManager libraryManager)
|
|
{
|
|
_logger = logger;
|
|
_lmsClient = lmsClient;
|
|
_playerManager = playerManager;
|
|
_libraryManager = libraryManager;
|
|
}
|
|
|
|
private PluginConfiguration Config => Plugin.Instance?.Configuration ?? new PluginConfiguration();
|
|
|
|
/// <summary>
|
|
/// Gets all active sessions.
|
|
/// </summary>
|
|
/// <returns>List of active sessions.</returns>
|
|
public List<LmsPlaybackSession> GetActiveSessions()
|
|
{
|
|
return _sessions.Values.Where(s => s.State != PlaybackState.Stopped).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a session by ID.
|
|
/// </summary>
|
|
/// <param name="sessionId">The session ID.</param>
|
|
/// <returns>The session, or null if not found.</returns>
|
|
public LmsPlaybackSession? GetSession(string sessionId)
|
|
{
|
|
return _sessions.GetValueOrDefault(sessionId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts playback of a Jellyfin item on LMS players.
|
|
/// </summary>
|
|
/// <param name="itemId">The Jellyfin item ID.</param>
|
|
/// <param name="playerMacs">The LMS player MAC addresses.</param>
|
|
/// <param name="userId">Optional user ID.</param>
|
|
/// <returns>The created session.</returns>
|
|
public async Task<LmsPlaybackSession?> StartPlaybackAsync(
|
|
Guid itemId,
|
|
IEnumerable<string> playerMacs,
|
|
Guid? userId = null)
|
|
{
|
|
var macList = playerMacs.ToList();
|
|
if (macList.Count == 0)
|
|
{
|
|
_logger.LogWarning("No players specified for playback");
|
|
return null;
|
|
}
|
|
|
|
var item = _libraryManager.GetItemById(itemId);
|
|
if (item == null)
|
|
{
|
|
_logger.LogWarning("Item {ItemId} not found", itemId);
|
|
return null;
|
|
}
|
|
|
|
// Build the audio stream URL
|
|
var streamUrl = BuildStreamUrl(itemId);
|
|
|
|
var session = new LmsPlaybackSession
|
|
{
|
|
ItemId = itemId,
|
|
ItemName = item.Name,
|
|
PlayerMacs = macList,
|
|
State = PlaybackState.Playing,
|
|
StreamUrl = streamUrl,
|
|
UserId = userId,
|
|
RuntimeTicks = item.RunTimeTicks ?? 0
|
|
};
|
|
|
|
// If multiple players, sync them first
|
|
if (macList.Count > 1)
|
|
{
|
|
var masterMac = macList[0];
|
|
var slaveMacs = macList.Skip(1);
|
|
await _playerManager.CreateSyncGroupAsync(masterMac, slaveMacs).ConfigureAwait(false);
|
|
}
|
|
|
|
// Start playback on the first player (others will sync)
|
|
var targetMac = macList[0];
|
|
var success = await _lmsClient.PlayUrlAsync(targetMac, streamUrl, item.Name).ConfigureAwait(false);
|
|
|
|
if (!success)
|
|
{
|
|
_logger.LogError("Failed to start playback on player {Mac}", targetMac);
|
|
return null;
|
|
}
|
|
|
|
_sessions[session.SessionId] = session;
|
|
_logger.LogInformation(
|
|
"Started playback session {SessionId} for {Item} on {Count} players",
|
|
session.SessionId,
|
|
item.Name,
|
|
macList.Count);
|
|
|
|
return session;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pauses a playback session.
|
|
/// </summary>
|
|
/// <param name="sessionId">The session ID.</param>
|
|
/// <returns>True if successful.</returns>
|
|
public async Task<bool> PauseSessionAsync(string sessionId)
|
|
{
|
|
if (!_sessions.TryGetValue(sessionId, out var session))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Pause the master player (synced players will follow)
|
|
var success = await _lmsClient.PauseAsync(session.PlayerMacs[0]).ConfigureAwait(false);
|
|
|
|
if (success)
|
|
{
|
|
session.State = PlaybackState.Paused;
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resumes a paused playback session.
|
|
/// </summary>
|
|
/// <param name="sessionId">The session ID.</param>
|
|
/// <returns>True if successful.</returns>
|
|
public async Task<bool> ResumeSessionAsync(string sessionId)
|
|
{
|
|
if (!_sessions.TryGetValue(sessionId, out var session))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var success = await _lmsClient.PlayAsync(session.PlayerMacs[0]).ConfigureAwait(false);
|
|
|
|
if (success)
|
|
{
|
|
session.State = PlaybackState.Playing;
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops a playback session.
|
|
/// </summary>
|
|
/// <param name="sessionId">The session ID.</param>
|
|
/// <returns>True if successful.</returns>
|
|
public async Task<bool> StopSessionAsync(string sessionId)
|
|
{
|
|
if (!_sessions.TryGetValue(sessionId, out var session))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var success = await _lmsClient.StopAsync(session.PlayerMacs[0]).ConfigureAwait(false);
|
|
|
|
if (success)
|
|
{
|
|
session.State = PlaybackState.Stopped;
|
|
|
|
// Unsync players if there were multiple
|
|
if (session.PlayerMacs.Count > 1)
|
|
{
|
|
await _playerManager.DissolveSyncGroupAsync(session.PlayerMacs[0]).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
// Remove the session
|
|
_sessions.TryRemove(sessionId, out _);
|
|
|
|
return success;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seeks to a position in the playback session.
|
|
/// </summary>
|
|
/// <param name="sessionId">The session ID.</param>
|
|
/// <param name="positionTicks">The position in ticks.</param>
|
|
/// <returns>True if successful.</returns>
|
|
public async Task<bool> SeekAsync(string sessionId, long positionTicks)
|
|
{
|
|
if (!_sessions.TryGetValue(sessionId, out var session))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var positionSeconds = positionTicks / TimeSpan.TicksPerSecond;
|
|
var success = await _lmsClient.SeekAsync(session.PlayerMacs[0], positionSeconds).ConfigureAwait(false);
|
|
|
|
if (success)
|
|
{
|
|
session.PositionTicks = positionTicks;
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the volume for all players in a session.
|
|
/// </summary>
|
|
/// <param name="sessionId">The session ID.</param>
|
|
/// <param name="volume">The volume level (0-100).</param>
|
|
/// <returns>True if successful.</returns>
|
|
public async Task<bool> SetVolumeAsync(string sessionId, int volume)
|
|
{
|
|
if (!_sessions.TryGetValue(sessionId, out var session))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var success = true;
|
|
foreach (var mac in session.PlayerMacs)
|
|
{
|
|
if (!await _lmsClient.SetVolumeAsync(mac, volume).ConfigureAwait(false))
|
|
{
|
|
success = false;
|
|
}
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the session state from LMS.
|
|
/// </summary>
|
|
/// <param name="sessionId">The session ID.</param>
|
|
/// <returns>A task representing the operation.</returns>
|
|
public async Task RefreshSessionStateAsync(string sessionId)
|
|
{
|
|
if (!_sessions.TryGetValue(sessionId, out var session))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var status = await _lmsClient.GetPlayerStatusAsync(session.PlayerMacs[0]).ConfigureAwait(false);
|
|
if (status == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
session.PositionTicks = (long)(status.Time * TimeSpan.TicksPerSecond);
|
|
session.State = status.Mode switch
|
|
{
|
|
"play" => PlaybackState.Playing,
|
|
"pause" => PlaybackState.Paused,
|
|
_ => PlaybackState.Stopped
|
|
};
|
|
}
|
|
|
|
private string BuildStreamUrl(Guid itemId)
|
|
{
|
|
var baseUrl = Config.JellyfinServerUrl.TrimEnd('/');
|
|
// Direct stream URL - LMS will pull audio from Jellyfin
|
|
return $"{baseUrl}/Audio/{itemId}/stream.mp3";
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
_logger.LogInformation("LMS Session Manager started");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public Task StopAsync(CancellationToken cancellationToken)
|
|
{
|
|
_logger.LogInformation("LMS Session Manager stopping");
|
|
|
|
// Stop all active sessions
|
|
foreach (var sessionId in _sessions.Keys.ToList())
|
|
{
|
|
_ = StopSessionAsync(sessionId);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|