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); }