Migrate ym-dl from custom oapi-codegen Yandex Music API client to external gitea.mrixs.me/Mrixs/yandex-music-api-go library with token-only authentication. Key changes: - Replace custom Yandex API client with yandexmusic library wrapper - Token-based OAuth authentication only (no username/password fallback) - SQLite storage index with JSON index migration path - Extended ID3 tag support: genre, comment, lyrics, publisher, ISRC, copyright - High-resolution album covers (1000x1000) - Fixed album download check (requires all tracks, not just one)
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 := 50
|
|
|
|
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 := 50
|
|
|
|
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
|
|
} |