diff --git a/src/config/config.go b/src/config/config.go index 756a4df..5bf5864 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -5,92 +5,94 @@ import ( "fmt" "log" "os" - "time" "strings" + "time" + "github.com/ilyakaznacheev/cleanenv" ) type Config struct { - DownloadCfg DownloadConfig + DownloadCfg DownloadConfig DiscoveryCfg DiscoveryConfig - ClientCfg ClientConfig - Persist bool `env:"PERSIST" env-default:"true"` - System string `env:"EXPLO_SYSTEM"` - Debug bool `env:"DEBUG" env-default:"false"` + ClientCfg ClientConfig + Persist bool `env:"PERSIST" env-default:"true"` + System string `env:"EXPLO_SYSTEM"` + Debug bool `env:"DEBUG" env-default:"false"` } type ClientConfig struct { - ClientID string `env:"CLIENT_ID" env-default:"explo"` - LibraryName string `env:"LIBRARY_NAME" env-default:"Explo"` - URL string `env:"SYSTEM_URL"` - DownloadDir string `env:"DOWNLOAD_DIR" env-default:"/data/"` - PlaylistDir string `env:"PLAYLIST_DIR"` + ClientID string `env:"CLIENT_ID" env-default:"explo"` + LibraryName string `env:"LIBRARY_NAME" env-default:"Explo"` + URL string `env:"SYSTEM_URL"` + DownloadDir string `env:"DOWNLOAD_DIR" env-default:"/data/"` + PlaylistDir string `env:"PLAYLIST_DIR"` PlaylistName string - PlaylistID string - Sleep int `env:"SLEEP" env-default:"2"` - Creds Credentials - Subsonic SubsonicConfig + PlaylistID string + Sleep int `env:"SLEEP" env-default:"2"` + Creds Credentials + Subsonic SubsonicConfig } type Credentials struct { - APIKey string `env:"API_KEY"` - User string `env:"SYSTEM_USERNAME"` + APIKey string `env:"API_KEY"` + User string `env:"SYSTEM_USERNAME"` Password string `env:"SYSTEM_PASSWORD"` - Headers map[string]string - Token string - Salt string + Headers map[string]string + Token string + Salt string } - -type SubsonicConfig struct { - Version string `env:"SUBSONIC_VERSION" env-default:"1.16.1"` - ID string `env:"CLIENT" env-default:"explo"` - URL string `env:"SUBSONIC_URL" env-default:"http://127.0.0.1:4533"` - User string `env:"SUBSONIC_USER"` - Password string `env:"SUBSONIC_PASSWORD"` +type DiscoveryConfig struct { + Discovery string `env:"DISCOVERY_SERVICE" env-default:"listenbrainz"` + Separator string `env:"FILENAME_SEPARATOR" env-default:" "` + Listenbrainz Listenbrainz } type DownloadConfig struct { - DownloadDir string `env:"DOWNLOAD_DIR" env-default:"/data/"` - Youtube Youtube - Slskd Slskd - Discovery string `env:"LISTENBRAINZ_DISCOVERY" env-default:"playlist"` - Services []string `env:"DOWNLOAD_SERVICES" env-default:"youtube"` + DownloadDir string `env:"DOWNLOAD_DIR" env-default:"/data/"` + Slskd Slskd + Youtube Youtube + Discovery string `env:"LISTENBRAINZ_DISCOVERY" env-default:"playlist"` + Services []string `env:"DOWNLOAD_SERVICES" env-default:"youtube"` } type Filters struct { - Extensions []string `env:"EXTENSIONS" env-default:"flac,mp3"` - MinBitDepth int `env:"MIN_BIT_DEPTH" env-default:"8"` - MinBitRate int `env:"MIN_BITRATE" env-default:"256"` - FilterList []string `env:"FILTER_LIST" env-default:"live,remix,instrumental,extended"` + Extensions []string `env:"EXTENSIONS" env-default:"flac,mp3"` + MinBitDepth int `env:"MIN_BIT_DEPTH" env-default:"8"` + MinBitRate int `env:"MIN_BITRATE" env-default:"256"` + FilterList []string `env:"FILTER_LIST" env-default:"live,remix,instrumental,extended"` } -type Youtube struct { - APIKey string `env:"YOUTUBE_API_KEY"` - FfmpegPath string `env:"FFMPEG_PATH"` - YtdlpPath string `env:"YTDLP_PATH"` - Filters Filters +type Listenbrainz struct { + Discovery string `env:"LISTENBRAINZ_DISCOVERY" env-default:"playlist"` + User string `env:"LISTENBRAINZ_USER"` + SingleArtist bool `env:"SINGLE_ARTIST" env-default:"true"` } -type Slskd struct { - APIKey string `env:"SLSKD_API_KEY"` - URL string `env:"SLSKD_URL"` - Retry int `env:"SLSKD_RETRY" env-default:"5"` // Number of times to check search status before skipping the track - DownloadAttempts int `env:"SLSKD_DL_ATTEMPTS" env-default:"3"` // Max number of files to attempt downloading per track - SlskdDir string `env:"SLSKD_DIR" env-default:"/slskd/"` - MigrateDL bool `env:"MIGRATE_DOWNLOADS" env-default:"false"` // Move downloads from SlskdDir to DownloadDir - Timeout time.Duration `env:"SLSKD_TIMEOUT" env-default:"20s"` - Filters Filters +type SubsonicConfig struct { + Version string `env:"SUBSONIC_VERSION" env-default:"1.16.1"` + ID string `env:"CLIENT" env-default:"explo"` + URL string `env:"SUBSONIC_URL" env-default:"http://127.0.0.1:4533"` + User string `env:"SUBSONIC_USER"` + Password string `env:"SUBSONIC_PASSWORD"` } -type DiscoveryConfig struct { - Discovery string `env:"DISCOVERY_SERVICE" env-default:"listenbrainz"` - Listenbrainz Listenbrainz +type Youtube struct { + APIKey string `env:"YOUTUBE_API_KEY"` + FfmpegPath string `env:"FFMPEG_PATH"` + YtdlpPath string `env:"YTDLP_PATH"` + Filters Filters } -type Listenbrainz struct { - Discovery string `env:"LISTENBRAINZ_DISCOVERY" env-default:"playlist"` - User string `env:"LISTENBRAINZ_USER"` - SingleArtist bool `env:"SINGLE_ARTIST" env-default:"true"` + +type Slskd struct { + APIKey string `env:"SLSKD_API_KEY"` + URL string `env:"SLSKD_URL"` + Retry int `env:"SLSKD_RETRY" env-default:"5"` // Number of times to check search status before skipping the track + DownloadAttempts int `env:"SLSKD_DL_ATTEMPTS" env-default:"3"` // Max number of files to attempt downloading per track + SlskdDir string `env:"SLSKD_DIR" env-default:"/slskd/"` + MigrateDL bool `env:"MIGRATE_DOWNLOADS" env-default:"false"` // Move downloads from SlskdDir to DownloadDir + Timeout time.Duration `env:"SLSKD_TIMEOUT" env-default:"20s"` + Filters Filters } func ReadEnv() Config { @@ -128,24 +130,25 @@ func fixDir(dir string) string { return dir } -/* func (cfg *Config) HandleDeprecation() { // no deprecations at the moment (keeping this for reference) - switch cfg.System { - case "subsonic": - if cfg.Subsonic.User != "" && cfg.Creds.User == "" { - log.Println("Warning: 'SUBSONIC_USER' is deprecated. Please use 'SYSTEM_USERNAME'.") - cfg.Creds.User = cfg.Subsonic.User - } - if cfg.Subsonic.Password != "" && cfg.Creds.Password == "" { - log.Println("Warning: 'SUBSONIC_PASSWORD' is deprecated. Please use 'SYSTEM_PASSWORD'.") - cfg.Creds.Password = cfg.Subsonic.Password - } - if cfg.Subsonic.URL != "" && cfg.URL == "" { - log.Println("Warning: 'SUBSONIC_URL' is deprecated. Please use 'SYSTEM_URL'.") - cfg.URL = cfg.Subsonic.URL +/* + func (cfg *Config) HandleDeprecation() { // no deprecations at the moment (keeping this for reference) + switch cfg.System { + case "subsonic": + if cfg.Subsonic.User != "" && cfg.Creds.User == "" { + log.Println("Warning: 'SUBSONIC_USER' is deprecated. Please use 'SYSTEM_USERNAME'.") + cfg.Creds.User = cfg.Subsonic.User + } + if cfg.Subsonic.Password != "" && cfg.Creds.Password == "" { + log.Println("Warning: 'SUBSONIC_PASSWORD' is deprecated. Please use 'SYSTEM_PASSWORD'.") + cfg.Creds.Password = cfg.Subsonic.Password + } + if cfg.Subsonic.URL != "" && cfg.URL == "" { + log.Println("Warning: 'SUBSONIC_URL' is deprecated. Please use 'SYSTEM_URL'.") + cfg.URL = cfg.Subsonic.URL + } } } -} - */ +*/ func (cfg *Config) GetPlaylistName() { // Generate playlist name depending if user wants to keep it or not playlistName := "Discover-Weekly" if cfg.Persist { diff --git a/src/downloader/downloader.go b/src/downloader/downloader.go index 4bba759..80dde93 100644 --- a/src/downloader/downloader.go +++ b/src/downloader/downloader.go @@ -1,14 +1,15 @@ package downloader import ( + "fmt" + "io" + "log" "os" "path" - "log" - "strings" - "regexp" - "fmt" "path/filepath" - "io" + "regexp" + "strings" + "golang.org/x/sync/errgroup" cfg "explo/src/config" @@ -17,7 +18,7 @@ import ( ) type DownloadClient struct { - Cfg *cfg.DownloadConfig + Cfg *cfg.DownloadConfig Downloaders []Downloader } @@ -27,7 +28,6 @@ type Downloader interface { MonitorDownloads([]*models.Track) error } - func NewDownloader(cfg *cfg.DownloadConfig, httpClient *util.HttpClient) *DownloadClient { // get download services from config and append them to DownloadClient var downloader []Downloader for _, service := range cfg.Services { @@ -44,39 +44,39 @@ func NewDownloader(cfg *cfg.DownloadConfig, httpClient *util.HttpClient) *Downlo } return &DownloadClient{ - Cfg: cfg, + Cfg: cfg, Downloaders: downloader} } - func (c *DownloadClient) StartDownload(tracks *[]*models.Track) { - for _, d := range c.Downloaders { - var g errgroup.Group - g.SetLimit(5) - - for _, track := range *tracks { - if track.Present { - continue +func (c *DownloadClient) StartDownload(tracks *[]*models.Track) { + for _, d := range c.Downloaders { + var g errgroup.Group + g.SetLimit(5) + + for _, track := range *tracks { + if track.Present { + continue + } + + g.Go(func() error { + + if err := d.QueryTrack(track); err != nil { + log.Println(err.Error()) + return nil } - - g.Go(func() error { - - if err := d.QueryTrack(track); err != nil { - log.Println(err.Error()) - return nil - } - if err := d.GetTrack(track); err != nil { - log.Println(err.Error()) - return nil - } + if err := d.GetTrack(track); err != nil { + log.Println(err.Error()) return nil - }) + } + return nil + }) } if err := g.Wait(); err != nil { return } - + if err := d.MonitorDownloads(*tracks); err != nil { - log.Printf("track monitoring failed: %s", err.Error()) + log.Printf("track monitoring failed: %s", err.Error()) } } filterTracks(tracks) @@ -90,7 +90,7 @@ func (c *DownloadClient) DeleteSongs() { for _, entry := range entries { if !(entry.IsDir()) { err = os.Remove(path.Join(c.Cfg.DownloadDir, entry.Name())) - + if err != nil { log.Printf("failed to remove file: %s", err.Error()) } @@ -112,9 +112,9 @@ func filterTracks(tracks *[]*models.Track) { // only keep tracks that were downl func containsLower(str string, substr string) bool { return strings.Contains( - strings.ToLower(str), - strings.ToLower(substr), - ) + strings.ToLower(str), + strings.ToLower(substr), + ) } func sanitizeName(s string) string { // return string with only letters and digits @@ -129,7 +129,7 @@ func getFilename(title, artist string) string { t := re.ReplaceAllString(title, "_") a := re.ReplaceAllString(artist, "_") - return fmt.Sprintf("%s-%s",t,a) + return fmt.Sprintf("%s-%s", t, a) } func moveDownload(srcDir, destDir, trackPath, file string) error { // Move download from the source dir to the dest dir (download dir) @@ -186,4 +186,4 @@ func moveDownload(srcDir, destDir, trackPath, file string) error { // Move downl } return nil -} \ No newline at end of file +} diff --git a/src/downloader/monitor.go b/src/downloader/monitor.go new file mode 100644 index 0000000..b3e6d5f --- /dev/null +++ b/src/downloader/monitor.go @@ -0,0 +1,148 @@ +package downloader + +import ( + "explo/src/debug" + "explo/src/models" + "fmt" + "log" + "strings" + "time" +) + +type DownloadStatusFetcher func() (DownloadStatus, error) +type CleanupFunc func(track *models.Track, fileID string) +type MoveFunc func(from, to, path, file string) error + +type MonitorConfig struct { + CheckInterval time.Duration + MonitorDuration time.Duration + MigrateDownload bool + FromDir string + ToDir string +} + +func Monitor(tracks []*models.Track, + fetchStatus DownloadStatusFetcher, + cleanup CleanupFunc, + move MoveFunc, + cfg MonitorConfig) error { + const checkInterval = 1 * time.Minute + const monitorDuration = 15 * time.Minute + var successDownloads int + + progressMap := make(map[string]*DownloadMonitor) + + ticker := time.NewTicker(checkInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + status, err := fetchStatus() + if err != nil { + log.Printf("Error fetching download status: %s", err.Error()) + continue + } + + currentTime := time.Now().Local() + + for _, track := range tracks { + + key := fmt.Sprintf("%s|%s", track.MainArtistID, track.File) + + if track.Present || track.MainArtistID == "" || (progressMap[key] != nil && progressMap[key].Skipped) { + continue + } + + // Initialize tracker if not present + if _, exists := progressMap[key]; !exists { + progressMap[key] = &DownloadMonitor{ + LastBytesTransferred: 0, + Counter: 0, + LastUpdated: currentTime, + } + } + + // Find the corresponding file in the download status + fileStatus := findFile(status, *track) + + tracker := progressMap[key] + if fileStatus.Size == 0 { + tracker.Counter++ + + if tracker.Counter >= 2 { + log.Printf("[monitor] %s by %s not found in queue after retries, skipping track", track.CleanTitle, track.MainArtist) + tracker.Skipped = true + } + continue + } + + if fileStatus.BytesRemaining == 0 || fileStatus.PercentComplete == 100 || strings.Contains(fileStatus.State, "Succeeded") { + track.Present = true + log.Printf("[monitor] %s downloaded successfully", track.File) + file, path := parsePath(track.File) + if cfg.MigrateDownload { + if err = moveDownload(cfg.FromDir, cfg.ToDir, path, file); err != nil { + debug.Debug(err.Error()) + } else { + debug.Debug("track moved successfully") + } + } + delete(progressMap, key) + track.File = file + successDownloads += 1 + cleanup(track, fileStatus.ID) + continue + + } else if fileStatus.BytesTransferred > tracker.LastBytesTransferred { + tracker.LastBytesTransferred = fileStatus.BytesTransferred + tracker.LastUpdated = currentTime + log.Printf("[monitor] progress updated for %s: %d bytes transferred", track.File, fileStatus.BytesTransferred) + continue + + } else if currentTime.Sub(tracker.LastUpdated) > monitorDuration || strings.Contains(fileStatus.State, "Errored") || strings.Contains(fileStatus.State, "Cancelled") { + log.Printf("[monitor] no progress on %s in %v, skipping track", track.File, monitorDuration) + tracker.Skipped = true + cleanup(track, fileStatus.ID) + continue + } + } + + // Exit condition: all tracks have been processed or skipped + if tracksProcessed(tracks, progressMap) { + log.Printf("[monitor] %d out of %d tracks have been downloaded", successDownloads, len(tracks)) + return nil + } + default: + continue + } + } +} + +func findFile(status DownloadStatus, track models.Track) DownloadFiles { + for _, userStatus := range status { + if userStatus.Username != track.MainArtistID { + continue + } + for _, dir := range userStatus.Directories { + for _, file := range dir.Files { + if string(file.Filename) == track.File { + return file + } + } + } + } + return DownloadFiles{} +} + +func tracksProcessed(tracks []*models.Track, progressMap map[string]*DownloadMonitor) bool { // Checks if all tracks are processed (either downloaded or skipped) + for _, track := range tracks { + key := fmt.Sprintf("%s|%s", track.MainArtistID, track.File) + tracker, exists := progressMap[key] + if !track.Present && exists && !tracker.Skipped { + log.Printf("%s still present", track.File) + return false + } + } + return true +} diff --git a/src/downloader/slskd.go b/src/downloader/slskd.go index f5c0900..8f82e4d 100644 --- a/src/downloader/slskd.go +++ b/src/downloader/slskd.go @@ -2,11 +2,11 @@ package downloader import ( "bytes" // Could be moved to util for all clients + "encoding/json" "explo/src/config" "explo/src/debug" "explo/src/models" "explo/src/util" - "encoding/json" "fmt" "log" "path/filepath" @@ -40,15 +40,15 @@ type SearchResults []struct { Username string `json:"username"` } type File struct { - BitRate int `json:"bitRate"` - BitDepth int `json:"bitDepth"` - Code int `json:"code"` - Extension string `json:"extension"` - Name string `json:"filename"` - Length int `json:"length"` - Size int `json:"size"` - IsLocked bool `json:"isLocked"` - Username string // Save user from SearchResults to here during collection + BitRate int `json:"bitRate"` + BitDepth int `json:"bitDepth"` + Code int `json:"code"` + Extension string `json:"extension"` + Name string `json:"filename"` + Length int `json:"length"` + Size int `json:"size"` + IsLocked bool `json:"isLocked"` + Username string // Save user from SearchResults to here during collection } type DownloadPayload struct { @@ -61,23 +61,23 @@ type DownloadStatus []struct { Directories []Directories `json:"directories"` } type DownloadFiles struct { - ID string `json:"id"` - Username string `json:"username"` - Direction string `json:"direction"` - Filename string `json:"filename"` - Size int `json:"size"` - StartOffset int `json:"startOffset"` - State string `json:"state"` - RequestedAt string `json:"requestedAt"` - EnqueuedAt string `json:"enqueuedAt"` - StartedAt time.Time `json:"startedAt"` - EndedAt time.Time `json:"endedAt"` - BytesTransferred int `json:"bytesTransferred"` - AverageSpeed float64 `json:"averageSpeed"` - BytesRemaining int `json:"bytesRemaining"` - ElapsedTime string `json:"elapsedTime"` - PercentComplete float64 `json:"percentComplete"` - RemainingTime string `json:"remainingTime"` + ID string `json:"id"` + Username string `json:"username"` + Direction string `json:"direction"` + Filename string `json:"filename"` + Size int `json:"size"` + StartOffset int `json:"startOffset"` + State string `json:"state"` + RequestedAt string `json:"requestedAt"` + EnqueuedAt string `json:"enqueuedAt"` + StartedAt time.Time `json:"startedAt"` + EndedAt time.Time `json:"endedAt"` + BytesTransferred int `json:"bytesTransferred"` + AverageSpeed float64 `json:"averageSpeed"` + BytesRemaining int `json:"bytesRemaining"` + ElapsedTime string `json:"elapsedTime"` + PercentComplete float64 `json:"percentComplete"` + RemainingTime string `json:"remainingTime"` } type Directories struct { Directory string `json:"directory"` @@ -94,16 +94,16 @@ type DownloadMonitor struct { } type Slskd struct { - Headers map[string]string - HttpClient *util.HttpClient + Headers map[string]string + HttpClient *util.HttpClient DownloadDir string - Cfg config.Slskd + Cfg config.Slskd } func NewSlskd(cfg config.Slskd, downloadDir string) *Slskd { return &Slskd{Cfg: cfg, - HttpClient: util.NewHttp(util.HttpClientConfig{Timeout: cfg.Timeout}), - DownloadDir: downloadDir,} + HttpClient: util.NewHttp(util.HttpClientConfig{Timeout: cfg.Timeout}), + DownloadDir: downloadDir} } func (c *Slskd) AddHeader() { @@ -304,7 +304,7 @@ func (c Slskd) queueDownload(files []File, track *models.Track) error { payload := []DownloadPayload{ { Filename: file.Name, - Size: file.Size, + Size: file.Size, }, } @@ -321,7 +321,7 @@ func (c Slskd) queueDownload(files []File, track *models.Track) error { return nil } - log.Printf("[%d/%d] failed to queue download for '%s - %s': %s", i + 1, len(files), track.CleanTitle, track.Artist, err.Error()) + log.Printf("[%d/%d] failed to queue download for '%s - %s': %s", i+1, len(files), track.CleanTitle, track.Artist, err.Error()) continue } if err := c.deleteSearch(track.ID); err != nil { @@ -330,7 +330,6 @@ func (c Slskd) queueDownload(files []File, track *models.Track) error { return fmt.Errorf("couldn't download track: %s - %s", track.CleanTitle, track.Artist) } - func (c Slskd) getDownloadStatus() (DownloadStatus, error) { reqParams := "/api/v0/transfers/downloads" @@ -347,125 +346,24 @@ func (c Slskd) getDownloadStatus() (DownloadStatus, error) { } func (c *Slskd) MonitorDownloads(tracks []*models.Track) error { - const checkInterval = 1 * time.Minute - const monitorDuration = 15 * time.Minute - var successDownloads int - - progressMap := make(map[string]*DownloadMonitor) - - ticker := time.NewTicker(checkInterval) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - status, err := c.getDownloadStatus() - if err != nil { - log.Printf("Error fetching download status: %s", err.Error()) - continue - } - - currentTime := time.Now().Local() - - for _, track := range tracks { - - key := fmt.Sprintf("%s|%s", track.MainArtistID, track.File) - - if track.Present || track.MainArtistID == "" || (progressMap[key] != nil && progressMap[key].Skipped) { - continue - } - - // Initialize tracker if not present - if _, exists := progressMap[key]; !exists { - progressMap[key] = &DownloadMonitor{ - LastBytesTransferred: 0, - Counter: 0, - LastUpdated: currentTime, - } - } - - // Find the corresponding file in the download status - fileStatus := c.findFile(status, *track) - - tracker := progressMap[key] - if fileStatus.Size == 0 { - tracker.Counter++ - - if tracker.Counter >= 2 { - log.Printf("[slskd] %s by %s not found in queue after retries, skipping track", track.CleanTitle, track.MainArtist) - tracker.Skipped = true - } - continue - } - - if fileStatus.BytesRemaining == 0 || fileStatus.PercentComplete == 100 || strings.Contains(fileStatus.State, "Succeeded") { - track.Present = true - log.Printf("[slskd] %s downloaded successfully", track.File) - file, path := parsePath(track.File) - if c.Cfg.MigrateDL { - if err = moveDownload(c.Cfg.SlskdDir, c.DownloadDir, path, file); err != nil { - debug.Debug(err.Error()) - } else { - debug.Debug("track moved successfully") - } - } - delete(progressMap, key) - track.File = file - successDownloads += 1 - c.cleanupTrack(track, fileStatus.ID) - continue - - } else if fileStatus.BytesTransferred > tracker.LastBytesTransferred { - tracker.LastBytesTransferred = fileStatus.BytesTransferred - tracker.LastUpdated = currentTime - log.Printf("[slskd] progress updated for %s: %d bytes transferred", track.File, fileStatus.BytesTransferred) - continue - - } else if currentTime.Sub(tracker.LastUpdated) > monitorDuration || strings.Contains(fileStatus.State, "Errored") || strings.Contains(fileStatus.State, "Cancelled") { - log.Printf("[slskd] no progress on %s in %v, skipping track", track.File, monitorDuration) - tracker.Skipped = true - c.cleanupTrack(track, fileStatus.ID) - continue - } - } - - // Exit condition: all tracks have been processed or skipped - if c.tracksProcessed(tracks, progressMap) { - log.Printf("[slskd] %d out of %d tracks have been downloaded", successDownloads, len(tracks)) - return nil - } - default: - continue - } + monitorCfg := MonitorConfig{ + CheckInterval: 1 * time.Minute, + MonitorDuration: 15 * time.Minute, + MigrateDownload: c.Cfg.MigrateDL, + FromDir: c.Cfg.SlskdDir, + ToDir: c.DownloadDir, } -} - -func (c Slskd) findFile(status DownloadStatus, track models.Track) DownloadFiles { - for _, userStatus := range status { - if userStatus.Username != track.MainArtistID { - continue - } - for _, dir := range userStatus.Directories { - for _, file := range dir.Files { - if string(file.Filename) == track.File { - return file - } - } - } - } - return DownloadFiles{} -} - -func (c Slskd) tracksProcessed(tracks []*models.Track, progressMap map[string]*DownloadMonitor) bool { // Checks if all tracks are processed (either downloaded or skipped) - for _, track := range tracks { - key := fmt.Sprintf("%s|%s", track.MainArtistID, track.File) - tracker, exists := progressMap[key] - if !track.Present && exists && !tracker.Skipped { - log.Printf("%s still present", track.File) - return false - } + err := Monitor( + tracks, + c.getDownloadStatus, + func(t *models.Track, id string) { c.cleanupTrack(t, id) }, + moveDownload, + monitorCfg, + ) + if err != nil { + return err } - return true + return nil } func (c Slskd) deleteDownload(user, ID string) error { @@ -485,16 +383,16 @@ func (c Slskd) deleteDownload(user, ID string) error { } func (c *Slskd) cleanupTrack(track *models.Track, fileID string) { - if err := c.deleteSearch(track.ID); err != nil { - debug.Debug(fmt.Sprintf("[slskd] failed to delete search request: %v", err)) - } - if err := c.deleteDownload(track.MainArtistID, fileID); err != nil { - debug.Debug(fmt.Sprintf("[slskd] failed to delete download: %v", err)) - } + if err := c.deleteSearch(track.ID); err != nil { + debug.Debug(fmt.Sprintf("[slskd] failed to delete search request: %v", err)) + } + if err := c.deleteDownload(track.MainArtistID, fileID); err != nil { + debug.Debug(fmt.Sprintf("[slskd] failed to delete download: %v", err)) + } } func parsePath(p string) (string, string) { // parse filepath to downloaded format, return filename and parent dir p = strings.ReplaceAll(p, `\`, `/`) return filepath.Base(p), filepath.Base(filepath.Dir(p)) -} \ No newline at end of file +}