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; /// /// Manages active playback sessions between Jellyfin and LMS. /// public class LmsSessionManager : IHostedService { private readonly ILogger _logger; private readonly ILmsApiClient _lmsClient; private readonly LmsPlayerManager _playerManager; private readonly ILibraryManager _libraryManager; private readonly ConcurrentDictionary _sessions = new(); /// /// Initializes a new instance of the class. /// /// The logger instance. /// The LMS API client. /// The player manager. /// The Jellyfin library manager. public LmsSessionManager( ILogger logger, ILmsApiClient lmsClient, LmsPlayerManager playerManager, ILibraryManager libraryManager) { _logger = logger; _lmsClient = lmsClient; _playerManager = playerManager; _libraryManager = libraryManager; } private PluginConfiguration Config => Plugin.Instance?.Configuration ?? new PluginConfiguration(); /// /// Gets all active sessions. /// /// List of active sessions. public List GetActiveSessions() { return _sessions.Values.Where(s => s.State != PlaybackState.Stopped).ToList(); } /// /// Gets a session by ID. /// /// The session ID. /// The session, or null if not found. public LmsPlaybackSession? GetSession(string sessionId) { return _sessions.GetValueOrDefault(sessionId); } /// /// Starts playback of a Jellyfin item on LMS players. /// /// The Jellyfin item ID. /// The LMS player MAC addresses. /// Optional user ID. /// The created session. public async Task StartPlaybackAsync( Guid itemId, IEnumerable 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; } /// /// Pauses a playback session. /// /// The session ID. /// True if successful. public async Task 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; } /// /// Resumes a paused playback session. /// /// The session ID. /// True if successful. public async Task 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; } /// /// Stops a playback session. /// /// The session ID. /// True if successful. public async Task 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; } /// /// Seeks to a position in the playback session. /// /// The session ID. /// The position in ticks. /// True if successful. public async Task 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; } /// /// Sets the volume for all players in a session. /// /// The session ID. /// The volume level (0-100). /// True if successful. public async Task 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; } /// /// Updates the session state from LMS. /// /// The session ID. /// A task representing the operation. 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"; } /// public Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("LMS Session Manager started"); return Task.CompletedTask; } /// 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; } }