Fetch up to 500 tracks/albums per page instead of 50, reducing the number of API calls needed when checking artists with large discographies.
240 lines
5.6 KiB
Go
240 lines
5.6 KiB
Go
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
|
|
} |