Initial commit: ym-dl with yandex-music-api-go library migration
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)
This commit is contained in:
commit
fe62910311
16 files changed
+6688
No files matched your search
@@ -0,0 +1,240 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user