diff --git a/cmd/rip/main.go b/cmd/rip/main.go
index b0a6108..461147a 100644
--- a/cmd/rip/main.go
+++ b/cmd/rip/main.go
@@ -552,6 +552,7 @@ func main() {
os.Exit(1)
}
defer func() { _ = mainApp.Close() }()
+ mainApp.IgnoreDB = gopts.noDB
title, tracks, err := fetchLastFMPlaylist(ctx, cfg.Session.Downloads.VerifySSL, opts.PlaylistURL)
if err != nil {
@@ -1422,12 +1423,13 @@ type resolvedLastFMTrack struct {
}
var (
- lastFMTitleTagsRe = regexp.MustCompile(`]*\btitle=(?:"([^"]+)"|'([^']+)')`)
- lastFMTotalTracksRe = regexp.MustCompile(`data-playlisting-entry-count="(\d+)"`)
- lastFMPlaylistTitleRe = regexp.MustCompile(`]*class="[^"]*playlisting-playlist-header-title[^"]*"[^>]*>([^<]+)
`)
- lastFMMirrorTitleRe = regexp.MustCompile(`^Title:\s*(.+?)\s+\|`)
- lastFMMirrorLinkTextRe = regexp.MustCompile(`\[([^\]]+)\]\(`)
- errLastFMInvalidSource = "unsupported source"
+ lastFMTitleTagsRe = regexp.MustCompile(`]*\btitle=(?:"([^"]+)"|'([^']+)')`)
+ lastFMDataTrackArtistRe = regexp.MustCompile(`data-track-name=(?:"([^"]+)"|'([^']+)')[^>]*data-artist-name=(?:"([^"]+)"|'([^']+)')`)
+ lastFMTotalTracksRe = regexp.MustCompile(`data-playlisting-entry-count="(\d+)"`)
+ lastFMPlaylistTitleRe = regexp.MustCompile(`]*class="[^"]*playlisting-playlist-header-title[^"]*"[^>]*>([^<]+)
`)
+ lastFMMirrorTitleRe = regexp.MustCompile(`^Title:\s*(.+?)\s+\|`)
+ lastFMMirrorLinkTextRe = regexp.MustCompile(`\[([^\]]+)\]\(`)
+ errLastFMInvalidSource = "unsupported source"
)
func addURLToQueue(ctx context.Context, mainApp *app.Main, raw string) bool {
@@ -1828,11 +1830,30 @@ func extractLastFMPlaylistInfo(page string) (string, int, error) {
}
func extractLastFMTitleArtistPairs(page string) []lastFMTrack {
+ dataPairs := lastFMDataTrackArtistRe.FindAllStringSubmatch(page, -1)
+ if len(dataPairs) > 0 {
+ out := make([]lastFMTrack, 0, len(dataPairs))
+ for _, m := range dataPairs {
+ title := html.UnescapeString(strings.TrimSpace(firstNonEmpty(m[1], m[2])))
+ artist := html.UnescapeString(strings.TrimSpace(firstNonEmpty(m[3], m[4])))
+ if title == "" || artist == "" {
+ continue
+ }
+ out = append(out, lastFMTrack{Title: title, Artist: artist})
+ }
+ if len(out) > 0 {
+ return out
+ }
+ }
+
titles := lastFMTitleTagsRe.FindAllStringSubmatch(page, -1)
out := make([]lastFMTrack, 0, len(titles)/2)
for i := 0; i+1 < len(titles); i += 2 {
titleRaw := strings.TrimSpace(firstNonEmpty(titles[i][1], titles[i][2]))
artistRaw := strings.TrimSpace(firstNonEmpty(titles[i+1][1], titles[i+1][2]))
+ if strings.EqualFold(titleRaw, "Play on YouTube") || strings.EqualFold(artistRaw, "Play on YouTube") {
+ continue
+ }
title := html.UnescapeString(titleRaw)
artist := html.UnescapeString(artistRaw)
if title == "" || artist == "" {
diff --git a/cmd/rip/main_test.go b/cmd/rip/main_test.go
index 2e15266..d19335d 100644
--- a/cmd/rip/main_test.go
+++ b/cmd/rip/main_test.go
@@ -171,6 +171,25 @@ func TestExtractLastFMTitleArtistPairsSingleQuotes(t *testing.T) {
}
}
+func TestExtractLastFMTitleArtistPairsSkipsPlayOnYouTubeNoise(t *testing.T) {
+ html := `Play track
+
+
+Play track
+
+`
+ pairs := extractLastFMTitleArtistPairs(html)
+ if len(pairs) != 2 {
+ t.Fatalf("pairs len = %d, want 2", len(pairs))
+ }
+ if pairs[0].Title != "Won't Forget You" || pairs[0].Artist != "Shouse" {
+ t.Fatalf("unexpected first pair: %+v", pairs[0])
+ }
+ if pairs[1].Title != "EYES" || pairs[1].Artist != "The Blaze" {
+ t.Fatalf("unexpected second pair: %+v", pairs[1])
+ }
+}
+
func TestParseGlobalArgsNoDBBeforeCommand(t *testing.T) {
opts, err := parseGlobalArgs([]string{"-ndb", "url", "https://play.qobuz.com/album/0004228000522"})
if err != nil {
diff --git a/internal/download/downloader.go b/internal/download/downloader.go
index 06ed1ff..7a548c2 100644
--- a/internal/download/downloader.go
+++ b/internal/download/downloader.go
@@ -14,6 +14,7 @@ import (
"strconv"
"strings"
"sync/atomic"
+ "time"
"github.com/vbauerster/mpb/v8"
"github.com/vbauerster/mpb/v8/decor"
@@ -90,24 +91,40 @@ func (d *Downloader) FileDeezerEncrypted(ctx context.Context, sourceURL, outputP
}()
var bar *mpb.Bar
- if d.ProgressEnabled() && resp.ContentLength > 0 {
+ if d.ProgressEnabled() {
d.barStarted.Store(1)
desc := shortenName(filepath.Base(outputPath), 54)
- bar = d.progress.AddBar(
- resp.ContentLength,
- mpb.PrependDecorators(
- decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
- decor.Percentage(decor.WCSyncWidthR),
- ),
- mpb.AppendDecorators(
- decor.CountersKibiByte("% .1f / % .1f", decor.WCSyncWidthR),
- decor.Name(" | ", decor.WCSyncWidth),
- decor.AverageSpeed(decor.SizeB1024(0), "% .1f", decor.WCSyncWidthR),
- decor.Name(" | ETA ", decor.WCSyncWidth),
- decor.AverageETA(decor.ET_STYLE_GO, decor.WCSyncWidthR),
- ),
- mpb.BarRemoveOnComplete(),
- )
+ if resp.ContentLength > 0 {
+ bar = d.progress.AddBar(
+ resp.ContentLength,
+ mpb.PrependDecorators(
+ decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
+ decor.Percentage(decor.WCSyncWidthR),
+ ),
+ mpb.AppendDecorators(
+ decor.CountersKibiByte("% .1f / % .1f", decor.WCSyncWidthR),
+ decor.Name(" | ", decor.WCSyncWidth),
+ decor.AverageSpeed(decor.SizeB1024(0), "% .1f", decor.WCSyncWidthR),
+ decor.Name(" | ETA ", decor.WCSyncWidth),
+ decor.AverageETA(decor.ET_STYLE_GO, decor.WCSyncWidthR),
+ ),
+ mpb.BarRemoveOnComplete(),
+ )
+ } else {
+ bar = d.progress.AddSpinner(
+ 0,
+ mpb.PrependDecorators(
+ decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
+ ),
+ mpb.AppendDecorators(
+ decor.CurrentKibiByte("% .1f", decor.WCSyncWidthR),
+ decor.Name(" | ", decor.WCSyncWidth),
+ decor.Elapsed(decor.ET_STYLE_GO, decor.WCSyncWidthR),
+ ),
+ mpb.BarRemoveOnComplete(),
+ )
+ defer bar.SetTotal(-1, true)
+ }
}
block, err := blowfish.NewCipher(deriveDeezerBlowfishKey(trackID))
@@ -193,24 +210,41 @@ func (d *Downloader) file(ctx context.Context, sourceURL, outputPath string, all
}
}()
- if d.ProgressEnabled() && allowProgress && resp.ContentLength > 0 {
+ if d.ProgressEnabled() && allowProgress {
d.barStarted.Store(1)
desc := shortenName(filepath.Base(outputPath), 54)
- bar := d.progress.AddBar(
- resp.ContentLength,
- mpb.PrependDecorators(
- decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
- decor.Percentage(decor.WCSyncWidthR),
- ),
- mpb.AppendDecorators(
- decor.CountersKibiByte("% .1f / % .1f", decor.WCSyncWidthR),
- decor.Name(" | ", decor.WCSyncWidth),
- decor.AverageSpeed(decor.SizeB1024(0), "% .1f", decor.WCSyncWidthR),
- decor.Name(" | ETA ", decor.WCSyncWidth),
- decor.AverageETA(decor.ET_STYLE_GO, decor.WCSyncWidthR),
- ),
- mpb.BarRemoveOnComplete(),
- )
+ var bar *mpb.Bar
+ if resp.ContentLength > 0 {
+ bar = d.progress.AddBar(
+ resp.ContentLength,
+ mpb.PrependDecorators(
+ decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
+ decor.Percentage(decor.WCSyncWidthR),
+ ),
+ mpb.AppendDecorators(
+ decor.CountersKibiByte("% .1f / % .1f", decor.WCSyncWidthR),
+ decor.Name(" | ", decor.WCSyncWidth),
+ decor.AverageSpeed(decor.SizeB1024(0), "% .1f", decor.WCSyncWidthR),
+ decor.Name(" | ETA ", decor.WCSyncWidth),
+ decor.AverageETA(decor.ET_STYLE_GO, decor.WCSyncWidthR),
+ ),
+ mpb.BarRemoveOnComplete(),
+ )
+ } else {
+ bar = d.progress.AddSpinner(
+ 0,
+ mpb.PrependDecorators(
+ decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
+ ),
+ mpb.AppendDecorators(
+ decor.CurrentKibiByte("% .1f", decor.WCSyncWidthR),
+ decor.Name(" | ", decor.WCSyncWidth),
+ decor.Elapsed(decor.ET_STYLE_GO, decor.WCSyncWidthR),
+ ),
+ mpb.BarRemoveOnComplete(),
+ )
+ defer bar.SetTotal(-1, true)
+ }
buf := make([]byte, 256*1024)
totalWritten := int64(0)
for {
@@ -286,6 +320,41 @@ func shortenName(name string, max int) string {
}
func (d *Downloader) streamManifestWithFFmpeg(ctx context.Context, sourceURL, outputPath string, includeVideo bool) error {
+ stopSpinner := func() {}
+ if d.ProgressEnabled() {
+ d.barStarted.Store(1)
+ desc := shortenName(filepath.Base(outputPath), 54)
+ spin := d.progress.AddSpinner(
+ 0,
+ mpb.PrependDecorators(
+ decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
+ ),
+ mpb.AppendDecorators(
+ decor.Elapsed(decor.ET_STYLE_GO, decor.WCSyncWidthR),
+ decor.Name(" | ffmpeg stream", decor.WCSyncWidth),
+ ),
+ mpb.BarRemoveOnComplete(),
+ )
+ done := make(chan struct{})
+ go func() {
+ ticker := time.NewTicker(120 * time.Millisecond)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ticker.C:
+ spin.IncrBy(1)
+ case <-done:
+ return
+ }
+ }
+ }()
+ stopSpinner = func() {
+ close(done)
+ spin.SetTotal(-1, true)
+ }
+ }
+ defer stopSpinner()
+
if _, err := exec.LookPath("ffmpeg"); err != nil {
return fmt.Errorf("ffmpeg not found for manifest stream: %w", err)
}