Files
Mrixs 4e6bef40e3
CI/CD / test (push) Successful in 48s
CI/CD / build (push) Successful in 54s
CI/CD / docker (push) Successful in 2m31s
Fix JSONID type conversions for yandex-music-api-go v0.0.0-20261018072517-d0173f795d2a
The new library version uses models.JSONID (custom string type) for ID
fields instead of plain string. Convert to string where IDs are passed
to functions expecting string or compared with string values.

Affected: client.go (GetTrackInfoWithPosition), downloader.go
(track.ID usage), monitor.go (track.ID, album.ID usage), storage.go
(album/track ID assignment).
2026-10-18 10:39:43 +03:00

240 lines
5.7 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(string(track.ID)) {
m.logger.Info("new track found", "track_id", string(track.ID), "artist", artistName)
result := m.downloader.DownloadTrack(ctx, string(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 := string(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, string(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
}