fix lastfm extraction regression and honor no-db

This commit is contained in:
2026-04-21 20:40:28 +02:00
parent 4c7e6f5792
commit dfa8095e1d
3 changed files with 147 additions and 38 deletions

View File

@@ -552,6 +552,7 @@ func main() {
os.Exit(1) os.Exit(1)
} }
defer func() { _ = mainApp.Close() }() defer func() { _ = mainApp.Close() }()
mainApp.IgnoreDB = gopts.noDB
title, tracks, err := fetchLastFMPlaylist(ctx, cfg.Session.Downloads.VerifySSL, opts.PlaylistURL) title, tracks, err := fetchLastFMPlaylist(ctx, cfg.Session.Downloads.VerifySSL, opts.PlaylistURL)
if err != nil { if err != nil {
@@ -1422,12 +1423,13 @@ type resolvedLastFMTrack struct {
} }
var ( var (
lastFMTitleTagsRe = regexp.MustCompile(`<a\b[^>]*\btitle=(?:"([^"]+)"|'([^']+)')`) lastFMTitleTagsRe = regexp.MustCompile(`<a\b[^>]*\btitle=(?:"([^"]+)"|'([^']+)')`)
lastFMTotalTracksRe = regexp.MustCompile(`data-playlisting-entry-count="(\d+)"`) lastFMDataTrackArtistRe = regexp.MustCompile(`data-track-name=(?:"([^"]+)"|'([^']+)')[^>]*data-artist-name=(?:"([^"]+)"|'([^']+)')`)
lastFMPlaylistTitleRe = regexp.MustCompile(`<h1[^>]*class="[^"]*playlisting-playlist-header-title[^"]*"[^>]*>([^<]+)</h1>`) lastFMTotalTracksRe = regexp.MustCompile(`data-playlisting-entry-count="(\d+)"`)
lastFMMirrorTitleRe = regexp.MustCompile(`^Title:\s*(.+?)\s+\|`) lastFMPlaylistTitleRe = regexp.MustCompile(`<h1[^>]*class="[^"]*playlisting-playlist-header-title[^"]*"[^>]*>([^<]+)</h1>`)
lastFMMirrorLinkTextRe = regexp.MustCompile(`\[([^\]]+)\]\(`) lastFMMirrorTitleRe = regexp.MustCompile(`^Title:\s*(.+?)\s+\|`)
errLastFMInvalidSource = "unsupported source" lastFMMirrorLinkTextRe = regexp.MustCompile(`\[([^\]]+)\]\(`)
errLastFMInvalidSource = "unsupported source"
) )
func addURLToQueue(ctx context.Context, mainApp *app.Main, raw string) bool { 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 { 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) titles := lastFMTitleTagsRe.FindAllStringSubmatch(page, -1)
out := make([]lastFMTrack, 0, len(titles)/2) out := make([]lastFMTrack, 0, len(titles)/2)
for i := 0; i+1 < len(titles); i += 2 { for i := 0; i+1 < len(titles); i += 2 {
titleRaw := strings.TrimSpace(firstNonEmpty(titles[i][1], 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])) 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) title := html.UnescapeString(titleRaw)
artist := html.UnescapeString(artistRaw) artist := html.UnescapeString(artistRaw)
if title == "" || artist == "" { if title == "" || artist == "" {

View File

@@ -171,6 +171,25 @@ func TestExtractLastFMTitleArtistPairsSingleQuotes(t *testing.T) {
} }
} }
func TestExtractLastFMTitleArtistPairsSkipsPlayOnYouTubeNoise(t *testing.T) {
html := `<a href="https://www.youtube.com/watch?v=1" data-track-name="Won&#39;t Forget You" data-artist-name="Shouse" title="Play on YouTube">Play track</a>
<a href="/music/Shouse/_/Won%27t+Forget+You" title="Won&#39;t Forget You"></a>
<a href="/music/Shouse" title="Shouse"></a>
<a href="https://www.youtube.com/watch?v=2" data-track-name="EYES" data-artist-name="The Blaze" title="Play on YouTube">Play track</a>
<a href="/music/The+Blaze/_/EYES" title="EYES"></a>
<a href="/music/The+Blaze" title="The Blaze"></a>`
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) { func TestParseGlobalArgsNoDBBeforeCommand(t *testing.T) {
opts, err := parseGlobalArgs([]string{"-ndb", "url", "https://play.qobuz.com/album/0004228000522"}) opts, err := parseGlobalArgs([]string{"-ndb", "url", "https://play.qobuz.com/album/0004228000522"})
if err != nil { if err != nil {

View File

@@ -14,6 +14,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync/atomic" "sync/atomic"
"time"
"github.com/vbauerster/mpb/v8" "github.com/vbauerster/mpb/v8"
"github.com/vbauerster/mpb/v8/decor" "github.com/vbauerster/mpb/v8/decor"
@@ -90,24 +91,40 @@ func (d *Downloader) FileDeezerEncrypted(ctx context.Context, sourceURL, outputP
}() }()
var bar *mpb.Bar var bar *mpb.Bar
if d.ProgressEnabled() && resp.ContentLength > 0 { if d.ProgressEnabled() {
d.barStarted.Store(1) d.barStarted.Store(1)
desc := shortenName(filepath.Base(outputPath), 54) desc := shortenName(filepath.Base(outputPath), 54)
bar = d.progress.AddBar( if resp.ContentLength > 0 {
resp.ContentLength, bar = d.progress.AddBar(
mpb.PrependDecorators( resp.ContentLength,
decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}), mpb.PrependDecorators(
decor.Percentage(decor.WCSyncWidthR), decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
), decor.Percentage(decor.WCSyncWidthR),
mpb.AppendDecorators( ),
decor.CountersKibiByte("% .1f / % .1f", decor.WCSyncWidthR), mpb.AppendDecorators(
decor.Name(" | ", decor.WCSyncWidth), decor.CountersKibiByte("% .1f / % .1f", decor.WCSyncWidthR),
decor.AverageSpeed(decor.SizeB1024(0), "% .1f", decor.WCSyncWidthR), decor.Name(" | ", decor.WCSyncWidth),
decor.Name(" | ETA ", decor.WCSyncWidth), decor.AverageSpeed(decor.SizeB1024(0), "% .1f", decor.WCSyncWidthR),
decor.AverageETA(decor.ET_STYLE_GO, decor.WCSyncWidthR), decor.Name(" | ETA ", decor.WCSyncWidth),
), decor.AverageETA(decor.ET_STYLE_GO, decor.WCSyncWidthR),
mpb.BarRemoveOnComplete(), ),
) 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)) 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) d.barStarted.Store(1)
desc := shortenName(filepath.Base(outputPath), 54) desc := shortenName(filepath.Base(outputPath), 54)
bar := d.progress.AddBar( var bar *mpb.Bar
resp.ContentLength, if resp.ContentLength > 0 {
mpb.PrependDecorators( bar = d.progress.AddBar(
decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}), resp.ContentLength,
decor.Percentage(decor.WCSyncWidthR), mpb.PrependDecorators(
), decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
mpb.AppendDecorators( decor.Percentage(decor.WCSyncWidthR),
decor.CountersKibiByte("% .1f / % .1f", decor.WCSyncWidthR), ),
decor.Name(" | ", decor.WCSyncWidth), mpb.AppendDecorators(
decor.AverageSpeed(decor.SizeB1024(0), "% .1f", decor.WCSyncWidthR), decor.CountersKibiByte("% .1f / % .1f", decor.WCSyncWidthR),
decor.Name(" | ETA ", decor.WCSyncWidth), decor.Name(" | ", decor.WCSyncWidth),
decor.AverageETA(decor.ET_STYLE_GO, decor.WCSyncWidthR), decor.AverageSpeed(decor.SizeB1024(0), "% .1f", decor.WCSyncWidthR),
), decor.Name(" | ETA ", decor.WCSyncWidth),
mpb.BarRemoveOnComplete(), 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) buf := make([]byte, 256*1024)
totalWritten := int64(0) totalWritten := int64(0)
for { 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 { 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 { if _, err := exec.LookPath("ffmpeg"); err != nil {
return fmt.Errorf("ffmpeg not found for manifest stream: %w", err) return fmt.Errorf("ffmpeg not found for manifest stream: %w", err)
} }