package monitor import ( "context" "fmt" "log/slog" "sync" "time" "ym-dl/internal/config" "ym-dl/internal/downloader" "ym-dl/internal/storage" "ym-dl/internal/yandex" ) // Monitor periodically checks for new releases from configured artists type Monitor struct { cfg *config.Config yandexClient *yandex.YandexClient storage *storage.Storage downloader *downloader.Downloader logger *slog.Logger mu sync.Mutex stopCh chan struct{} wg sync.WaitGroup running bool } // NewMonitor creates a new monitor func NewMonitor(cfg *config.Config, yandexClient *yandex.YandexClient, storage *storage.Storage, downloader *downloader.Downloader, logger *slog.Logger) *Monitor { return &Monitor{ cfg: cfg, yandexClient: yandexClient, storage: storage, downloader: downloader, logger: logger, stopCh: make(chan struct{}), } } // Start starts the monitor loop func (m *Monitor) Start(ctx context.Context) error { m.mu.Lock() if m.running { m.mu.Unlock() return fmt.Errorf("monitor already running") } m.running = true m.mu.Unlock() m.wg.Add(1) go m.runLoop(ctx) return nil } // Stop stops the monitor func (m *Monitor) Stop() { m.mu.Lock() if !m.running { m.mu.Unlock() return } m.running = false close(m.stopCh) m.mu.Unlock() m.wg.Wait() } // runLoop is the main monitoring loop func (m *Monitor) runLoop(ctx context.Context) { defer m.wg.Done() interval, err := m.cfg.GetMonitorInterval() if err != nil { m.logger.Error("invalid monitor interval", "error", err) return } ticker := time.NewTicker(interval) defer ticker.Stop() // Run initial check m.checkAllArtists(ctx) for { select { case <-ctx.Done(): return case <-m.stopCh: return case <-ticker.C: m.checkAllArtists(ctx) } } } // checkAllArtists checks all configured artists for new releases func (m *Monitor) checkAllArtists(ctx context.Context) { m.logger.Info("starting artist check", "artist_count", len(m.cfg.Artists)) for _, artist := range m.cfg.Artists { select { case <-ctx.Done(): return case <-m.stopCh: return default: } m.checkArtist(ctx, artist.ID) } } // checkArtist checks a single artist for new tracks and albums func (m *Monitor) checkArtist(ctx context.Context, artistID int) { m.logger.Info("checking artist", "artist_id", artistID) // Get artist info artist, err := m.yandexClient.GetArtistInfo(ctx, artistID) if err != nil { m.logger.Error("failed to get artist info", "artist_id", artistID, "error", err) return } m.logger.Info("artist info", "artist_id", artistID, "name", artist.Name) // Check for new tracks m.checkArtistTracks(ctx, artistID, artist.Name) // Check for new albums m.checkArtistAlbums(ctx, artistID, artist.Name) } // checkArtistTracks checks for new tracks from an artist func (m *Monitor) checkArtistTracks(ctx context.Context, artistID int, artistName string) { page := 0 pageSize := 500 for { tracks, err := m.yandexClient.GetArtistTracks(ctx, artistID, page, pageSize) if err != nil { m.logger.Error("failed to get artist tracks", "artist_id", artistID, "page", page, "error", err) return } if len(tracks) == 0 { break } newTracks := 0 for _, track := range tracks { if !m.storage.IsTrackDownloaded(track.ID) { m.logger.Info("new track found", "track_id", track.ID, "artist", artistName) result := m.downloader.DownloadTrack(ctx, track.ID) if result.Success { newTracks++ } else { m.logger.Error("failed to download track", "track_id", track.ID, "error", result.Error) } } } m.logger.Info("track check complete", "artist_id", artistID, "new_tracks", newTracks, "page", page) if len(tracks) < pageSize { break } page++ } } // checkArtistAlbums checks for new albums from an artist func (m *Monitor) checkArtistAlbums(ctx context.Context, artistID int, artistName string) { page := 0 pageSize := 500 for { albums, err := m.yandexClient.GetArtistAlbums(ctx, artistID, page, pageSize) if err != nil { m.logger.Error("failed to get artist albums", "artist_id", artistID, "page", page, "error", err) return } if len(albums) == 0 { break } newAlbums := 0 for _, album := range albums { albumIDStr := album.ID if !m.isAlbumDownloaded(albumIDStr, album.TrackCount) { m.logger.Info("new album found", "album_id", albumIDStr, "title", album.Title, "artist", artistName) results := m.downloader.DownloadAlbum(ctx, album.ID) success := false for _, r := range results { if r.Success { success = true break } } if success { newAlbums++ } else { m.logger.Error("failed to download album", "album_id", albumIDStr, "title", album.Title) } } } m.logger.Info("album check complete", "artist_id", artistID, "new_albums", newAlbums, "page", page) if len(albums) < pageSize { break } page++ } } // isAlbumDownloaded checks if an album has been fully downloaded. // An album is considered downloaded only if all its expected tracks (by TrackCount) // are present in the storage index. If TrackCount is 0 (unknown), the album is // never considered pre-downloaded to avoid skipping compilation albums // where only some tracks were fetched individually. func (m *Monitor) isAlbumDownloaded(albumID string, expectedTrackCount int) bool { if expectedTrackCount <= 0 { return false // Unknown track count — download anyway to be safe } downloadedCount := 0 for _, trackID := range m.storage.GetDownloadedTracks() { meta, ok := m.storage.GetTrackMeta(trackID) if ok && meta.AlbumID == albumID { downloadedCount++ } } return downloadedCount >= expectedTrackCount }