using Microsoft.Extensions.Logging;
using SameBeat.Application.DTO;
using SameBeat.Application.Interfejsy;
using SameBeat.Application.Komendy;
using SameBeat.Domain.Encje;
using SameBeat.Domain.Enumy;
using SameBeat.Domain.Wyjatki;
namespace SameBeat.Application.Serwisy;
///
/// Logika muzyki (ETAP 4): biblioteka utworow, playlista biezacego wydarzenia i "zegar" odtwarzania.
/// Muzyka NIE idzie przez LiveKit/SignalR jako dzwiek -- backend trzyma tylko zegar (kiedy sie
/// zaczelo, od ktorego miejsca), a kazda przegladarka sama odtwarza plik audio spod Utwor.Url i
/// synchronizuje do niego swoja pozycje. To jak radio internetowe.
///
public sealed class SerwisMuzyki : ISerwisMuzyki
{
private const string BladBrakWydarzenia = "Brak aktywnego wydarzenia, sprobuj pozniej.";
private readonly IRepozytoriumWydarzen _repozytoriumWydarzen;
private readonly IRepozytoriumUtworow _repozytoriumUtworow;
private readonly IRepozytoriumPlaylisty _repozytoriumPlaylisty;
private readonly IJednostkaPracy _jednostkaPracy;
private readonly ILogger _logger;
public SerwisMuzyki(
IRepozytoriumWydarzen repozytoriumWydarzen,
IRepozytoriumUtworow repozytoriumUtworow,
IRepozytoriumPlaylisty repozytoriumPlaylisty,
IJednostkaPracy jednostkaPracy,
ILogger logger)
{
_repozytoriumWydarzen = repozytoriumWydarzen;
_repozytoriumUtworow = repozytoriumUtworow;
_repozytoriumPlaylisty = repozytoriumPlaylisty;
_jednostkaPracy = jednostkaPracy;
_logger = logger;
}
public async Task> DodajUtworAsync(DodajUtworKomenda komenda, CancellationToken token = default)
{
if (string.IsNullOrWhiteSpace(komenda.Tytul))
{
return WynikOperacji.Niepowodzenie("Tytul utworu jest wymagany.");
}
if (string.IsNullOrWhiteSpace(komenda.Url) || !Uri.TryCreate(komenda.Url, UriKind.Absolute, out _))
{
return WynikOperacji.Niepowodzenie("Url utworu musi byc poprawnym, pelnym adresem.");
}
if (komenda.CzasTrwaniaMs <= 0)
{
return WynikOperacji.Niepowodzenie("Czas trwania utworu musi byc dodatni.");
}
Utwor utwor;
try
{
utwor = Utwor.Utworz(komenda.Tytul, komenda.Wykonawca, komenda.Url, komenda.CzasTrwaniaMs);
}
catch (WyjatekDomenowy wyjatek)
{
return WynikOperacji.Niepowodzenie(wyjatek.Message);
}
_repozytoriumUtworow.Dodaj(utwor);
await _jednostkaPracy.ZapiszZmianyAsync(token);
_logger.LogInformation("Dodano utwor {UtworId} ({Tytul}) do biblioteki.", utwor.Id, utwor.Tytul);
return WynikOperacji.Ok(MapujUtwor(utwor));
}
public async Task>> PobierzUtworyAsync(CancellationToken token = default)
{
var utwory = await _repozytoriumUtworow.PobierzWszystkieAsync(token);
return WynikOperacji>.Ok(utwory.Select(MapujUtwor).ToList());
}
public async Task>> DodajDoPlaylistyAsync(Guid utworId, CancellationToken token = default)
{
var wydarzenie = await _repozytoriumWydarzen.PobierzOstatnieAsync(token);
if (wydarzenie is null)
{
return WynikOperacji>.Niepowodzenie(BladBrakWydarzenia);
}
var utwor = await _repozytoriumUtworow.ZnajdzPoIdAsync(utworId, token);
if (utwor is null)
{
return WynikOperacji>.Niepowodzenie("Nie znaleziono utworu o podanym identyfikatorze.");
}
var aktualnaPlaylista = await _repozytoriumPlaylisty.PobierzUporzadkowanaDlaWydarzeniaAsync(wydarzenie.Id, token);
var nastepnaKolejnosc = aktualnaPlaylista.Count == 0 ? 0 : aktualnaPlaylista[^1].Kolejnosc + 1;
var pozycja = PozycjaPlaylisty.Utworz(wydarzenie.Id, utworId, nastepnaKolejnosc);
_repozytoriumPlaylisty.Dodaj(pozycja);
await _jednostkaPracy.ZapiszZmianyAsync(token);
_logger.LogInformation("Dodano utwor {UtworId} na koniec playlisty wydarzenia {WydarzenieId} (kolejnosc {Kolejnosc}).",
utworId, wydarzenie.Id, nastepnaKolejnosc);
return await ZbudujPlaylisteDtoAsync(wydarzenie, token);
}
public async Task>> UsunZPlaylistyAsync(Guid pozycjaId, CancellationToken token = default)
{
var wydarzenie = await _repozytoriumWydarzen.PobierzOstatnieAsync(token);
if (wydarzenie is null)
{
return WynikOperacji>.Niepowodzenie(BladBrakWydarzenia);
}
var pozycja = await _repozytoriumPlaylisty.ZnajdzPoIdAsync(pozycjaId, token);
// Usuniecie nieistniejacej/juz usunietej pozycji jest idempotentne -- to sukces, nie blad,
// tak samo jak ZwolnijSlotAsync w SerwisSceny.
if (pozycja is not null && pozycja.WydarzenieId == wydarzenie.Id)
{
_repozytoriumPlaylisty.Usun(pozycja);
await _jednostkaPracy.ZapiszZmianyAsync(token);
_logger.LogInformation("Usunieto pozycje {PozycjaId} z playlisty wydarzenia {WydarzenieId}.", pozycjaId, wydarzenie.Id);
}
return await ZbudujPlaylisteDtoAsync(wydarzenie, token);
}
public async Task>> PobierzPlaylisteAsync(CancellationToken token = default)
{
var wydarzenie = await _repozytoriumWydarzen.PobierzOstatnieAsync(token);
if (wydarzenie is null)
{
// Brak wydarzenia to normalny stan (host jeszcze niczego nie utworzyl) -- pusta playlista, nie blad.
return WynikOperacji>.Ok(new List());
}
return await ZbudujPlaylisteDtoAsync(wydarzenie, token);
}
public async Task> PobierzStanAsync(CancellationToken token = default)
{
var wydarzenie = await _repozytoriumWydarzen.PobierzOstatnieAsync(token);
if (wydarzenie is null)
{
// Brak wydarzenia to normalny stan -- "cisza" (Zatrzymana, brak utworu), nie blad.
return WynikOperacji.Ok(new StanMuzykiDto(null, null, null, null, null, 0, StanOdtwarzania.Zatrzymana, 0.9f));
}
return WynikOperacji.Ok(await BudowniczyStanuMuzyki.ZbudujAsync(wydarzenie, _repozytoriumUtworow, token));
}
public async Task> OdtworzAsync(Guid? utworId, CancellationToken token = default)
{
var wydarzenie = await _repozytoriumWydarzen.PobierzOstatnieAsync(token);
if (wydarzenie is null)
{
return WynikOperacji.Niepowodzenie(BladBrakWydarzenia);
}
if (utworId is Guid podanyUtworId)
{
var utwor = await _repozytoriumUtworow.ZnajdzPoIdAsync(podanyUtworId, token);
if (utwor is null)
{
return WynikOperacji.Niepowodzenie("Nie znaleziono utworu o podanym identyfikatorze.");
}
wydarzenie.OdtworzOd(podanyUtworId, 0);
_logger.LogInformation("Wydarzenie {WydarzenieId} przelaczylo odtwarzanie na utwor {UtworId}.", wydarzenie.Id, podanyUtworId);
}
else if (wydarzenie.StanOdtwarzania == StanOdtwarzania.Wstrzymana)
{
wydarzenie.Wznow();
_logger.LogInformation("Wydarzenie {WydarzenieId} wznowilo odtwarzanie od pozycji {PozycjaMs}ms.", wydarzenie.Id, wydarzenie.PozycjaStartowaMs);
}
else if (wydarzenie.StanOdtwarzania != StanOdtwarzania.Odtwarzana)
{
// Zatrzymana i brak utworId -- nie ma czego odtworzyc, host musi wskazac utwor.
return WynikOperacji.Niepowodzenie("Brak utworu do odtworzenia -- podaj utworId albo najpierw dodaj utwor do playlisty.");
}
// Juz Odtwarzana i brak utworId -- no-op, sukces bez zmian (idempotentne).
await _jednostkaPracy.ZapiszZmianyAsync(token);
return WynikOperacji.Ok(await BudowniczyStanuMuzyki.ZbudujAsync(wydarzenie, _repozytoriumUtworow, token));
}
public async Task> WstrzymajAsync(CancellationToken token = default)
{
var wydarzenie = await _repozytoriumWydarzen.PobierzOstatnieAsync(token);
if (wydarzenie is null)
{
return WynikOperacji.Niepowodzenie(BladBrakWydarzenia);
}
wydarzenie.Wstrzymaj();
await _jednostkaPracy.ZapiszZmianyAsync(token);
_logger.LogInformation("Wydarzenie {WydarzenieId} wstrzymalo odtwarzanie na pozycji {PozycjaMs}ms.", wydarzenie.Id, wydarzenie.PozycjaStartowaMs);
return WynikOperacji.Ok(await BudowniczyStanuMuzyki.ZbudujAsync(wydarzenie, _repozytoriumUtworow, token));
}
public Task> NastepnyAsync(CancellationToken token = default) => PrzejdzWKolejceAsync(krok: 1, token);
public Task> PoprzedniAsync(CancellationToken token = default) => PrzejdzWKolejceAsync(krok: -1, token);
public async Task> UstawGlosnoscAsync(float wartosc, CancellationToken token = default)
{
if (wartosc < 0f || wartosc > 1f)
{
return WynikOperacji.Niepowodzenie($"Glosnosc musi byc w zakresie 0.0-1.0 (podano {wartosc}).");
}
var wydarzenie = await _repozytoriumWydarzen.PobierzOstatnieAsync(token);
if (wydarzenie is null)
{
return WynikOperacji.Niepowodzenie(BladBrakWydarzenia);
}
wydarzenie.UstawGlosnosc(wartosc);
await _jednostkaPracy.ZapiszZmianyAsync(token);
return WynikOperacji.Ok(await BudowniczyStanuMuzyki.ZbudujAsync(wydarzenie, _repozytoriumUtworow, token));
}
///
/// Wspolna logika Nastepny/Poprzedni -- krok +1/-1 po pozycji biezacego utworu w kolejce.
/// Gdy nic teraz nie gra z kolejki (albo grany utwor juz z niej zniknal), startuje od pierwszej
/// pozycji. Gdy krok wychodzi poza kolejke (koniec przy Nastepny, poczatek przy Poprzedni) --
/// zatrzymuje odtwarzanie calkowicie (Zatrzymana, UtworId=null).
///
private async Task> PrzejdzWKolejceAsync(int krok, CancellationToken token)
{
var wydarzenie = await _repozytoriumWydarzen.PobierzOstatnieAsync(token);
if (wydarzenie is null)
{
return WynikOperacji.Niepowodzenie(BladBrakWydarzenia);
}
var playlista = await _repozytoriumPlaylisty.PobierzUporzadkowanaDlaWydarzeniaAsync(wydarzenie.Id, token);
if (playlista.Count == 0)
{
return WynikOperacji.Niepowodzenie("Playlista jest pusta.");
}
var indeksBiezacego = wydarzenie.UtworId is Guid biezacyUtworId
? playlista.FindIndex(p => p.UtworId == biezacyUtworId)
: -1;
if (indeksBiezacego == -1)
{
wydarzenie.OdtworzOd(playlista[0].UtworId, 0);
}
else
{
var docelowyIndeks = indeksBiezacego + krok;
if (docelowyIndeks < 0 || docelowyIndeks >= playlista.Count)
{
wydarzenie.Zatrzymaj();
}
else
{
wydarzenie.OdtworzOd(playlista[docelowyIndeks].UtworId, 0);
}
}
await _jednostkaPracy.ZapiszZmianyAsync(token);
_logger.LogInformation("Wydarzenie {WydarzenieId} przeszlo w kolejce (krok {Krok}) -- biezacy utwor: {UtworId}.",
wydarzenie.Id, krok, wydarzenie.UtworId);
return WynikOperacji.Ok(await BudowniczyStanuMuzyki.ZbudujAsync(wydarzenie, _repozytoriumUtworow, token));
}
private async Task>> ZbudujPlaylisteDtoAsync(Wydarzenie wydarzenie, CancellationToken token)
{
var playlista = await _repozytoriumPlaylisty.PobierzUporzadkowanaDlaWydarzeniaAsync(wydarzenie.Id, token);
var dto = new List(playlista.Count);
foreach (var pozycja in playlista)
{
var utwor = await _repozytoriumUtworow.ZnajdzPoIdAsync(pozycja.UtworId, token);
if (utwor is null)
{
// Nie powinno sie zdarzyc (utwor jest wskazywany tylko przy dodaniu, gdy istnial) --
// ale gdyby jednak zniknal z biblioteki, pomijamy pozycje zamiast rzucac NRE.
_logger.LogWarning("Pozycja playlisty {PozycjaId} wskazuje na nieistniejacy utwor {UtworId} -- pomijam w odpowiedzi.",
pozycja.Id, pozycja.UtworId);
continue;
}
var czyAktualnieGrana = wydarzenie.UtworId == pozycja.UtworId;
dto.Add(new PozycjaPlaylistyDto(pozycja.Id, pozycja.Kolejnosc, MapujUtwor(utwor), czyAktualnieGrana));
}
return WynikOperacji>.Ok(dto);
}
private static UtworDto MapujUtwor(Utwor utwor) => new(utwor.Id, utwor.Tytul, utwor.Wykonawca, utwor.Url, utwor.CzasTrwaniaMs);
}