mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
render manifest ffmpeg progress like standard download bars
This commit is contained in:
@@ -343,8 +343,9 @@ func (d *Downloader) streamManifestWithFFmpeg(ctx context.Context, sourceURL, ou
|
|||||||
decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
|
decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
|
||||||
),
|
),
|
||||||
mpb.AppendDecorators(
|
mpb.AppendDecorators(
|
||||||
|
decor.CurrentKibiByte("% .1f", decor.WCSyncWidthR),
|
||||||
|
decor.Name(" | ", decor.WCSyncWidth),
|
||||||
decor.Elapsed(decor.ET_STYLE_GO, decor.WCSyncWidthR),
|
decor.Elapsed(decor.ET_STYLE_GO, decor.WCSyncWidthR),
|
||||||
decor.Name(" | probing stream", decor.WCSyncWidth),
|
|
||||||
),
|
),
|
||||||
mpb.BarRemoveOnComplete(),
|
mpb.BarRemoveOnComplete(),
|
||||||
)
|
)
|
||||||
@@ -364,18 +365,21 @@ func (d *Downloader) streamManifestWithFFmpeg(ctx context.Context, sourceURL, ou
|
|||||||
scanDone := make(chan scanState, 1)
|
scanDone := make(chan scanState, 1)
|
||||||
go func() {
|
go func() {
|
||||||
state := scanState{}
|
state := scanState{}
|
||||||
shownMS := int64(0)
|
shownBytes := int64(0)
|
||||||
newFFmpegBar := func(totalMS int64) *mpb.Bar {
|
barTotalBytes := int64(0)
|
||||||
|
newEstimatedBar := func(totalBytes int64) *mpb.Bar {
|
||||||
return d.progress.AddBar(
|
return d.progress.AddBar(
|
||||||
totalMS,
|
totalBytes,
|
||||||
mpb.PrependDecorators(
|
mpb.PrependDecorators(
|
||||||
decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
|
decor.Name(desc+" ", decor.WC{W: 56, C: decor.DSyncWidth | decor.DindentRight}),
|
||||||
decor.Percentage(decor.WCSyncWidthR),
|
decor.Percentage(decor.WCSyncWidthR),
|
||||||
),
|
),
|
||||||
mpb.AppendDecorators(
|
mpb.AppendDecorators(
|
||||||
|
decor.CountersKibiByte("% .1f / % .1f", decor.WCSyncWidthR),
|
||||||
decor.Name(" | ", decor.WCSyncWidth),
|
decor.Name(" | ", decor.WCSyncWidth),
|
||||||
decor.Elapsed(decor.ET_STYLE_GO, decor.WCSyncWidthR),
|
decor.AverageSpeed(decor.SizeB1024(0), "% .1f", decor.WCSyncWidthR),
|
||||||
decor.Name(" | ffmpeg stream", decor.WCSyncWidth),
|
decor.Name(" | ETA ", decor.WCSyncWidth),
|
||||||
|
decor.AverageETA(decor.ET_STYLE_GO, decor.WCSyncWidthR),
|
||||||
),
|
),
|
||||||
mpb.BarRemoveOnComplete(),
|
mpb.BarRemoveOnComplete(),
|
||||||
)
|
)
|
||||||
@@ -389,45 +393,65 @@ func (d *Downloader) streamManifestWithFFmpeg(ctx context.Context, sourceURL, ou
|
|||||||
ffOut.WriteString(line)
|
ffOut.WriteString(line)
|
||||||
ffOut.WriteByte('\n')
|
ffOut.WriteByte('\n')
|
||||||
}
|
}
|
||||||
if state.totalMS == 0 {
|
|
||||||
if totalMS, ok := parseFFmpegDurationLine(line); ok && totalMS > 0 {
|
if totalMS, ok := parseFFmpegDurationLine(line); ok && totalMS > 0 {
|
||||||
state.totalMS = totalMS
|
state.totalMS = totalMS
|
||||||
spinner.SetTotal(-1, true)
|
|
||||||
bar = newFFmpegBar(state.totalMS)
|
|
||||||
if state.currentMS > state.totalMS {
|
|
||||||
state.currentMS = state.totalMS
|
|
||||||
}
|
}
|
||||||
if state.currentMS > shownMS {
|
if bitrateBPS, ok := parseFFmpegDurationBitrateBPS(line); ok && bitrateBPS > 0 {
|
||||||
bar.IncrBy(int(state.currentMS - shownMS))
|
state.bitrateBPS = bitrateBPS
|
||||||
shownMS = state.currentMS
|
|
||||||
}
|
}
|
||||||
|
if bitrateBPS, ok := parseFFmpegProgressBitrateBPS(line); ok && bitrateBPS > 0 {
|
||||||
|
state.bitrateBPS = bitrateBPS
|
||||||
}
|
}
|
||||||
|
if totalSize, ok := parseFFmpegTotalSize(line); ok && totalSize > state.currentBytes {
|
||||||
|
state.currentBytes = totalSize
|
||||||
}
|
}
|
||||||
if currentMS, ok := parseFFmpegOutTime(line); ok {
|
if currentMS, ok := parseFFmpegOutTime(line); ok {
|
||||||
if currentMS > state.currentMS {
|
if currentMS > state.currentMS {
|
||||||
state.currentMS = currentMS
|
state.currentMS = currentMS
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bar == nil {
|
||||||
|
estimatedTotal := int64(0)
|
||||||
|
if state.totalMS > 0 && state.bitrateBPS > 0 {
|
||||||
|
estimatedTotal = estimateTotalBytesFromBitrate(state.totalMS, state.bitrateBPS)
|
||||||
|
}
|
||||||
|
if estimatedTotal == 0 {
|
||||||
|
estimatedTotal = estimateTotalBytesFromProgress(state.totalMS, state.currentMS, state.currentBytes)
|
||||||
|
}
|
||||||
|
if estimatedTotal > 0 {
|
||||||
|
if estimatedTotal < state.currentBytes {
|
||||||
|
estimatedTotal = state.currentBytes
|
||||||
|
}
|
||||||
|
barTotalBytes = estimatedTotal
|
||||||
|
spinner.SetTotal(-1, true)
|
||||||
|
bar = newEstimatedBar(barTotalBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.currentBytes > shownBytes {
|
||||||
|
delta := state.currentBytes - shownBytes
|
||||||
if bar != nil {
|
if bar != nil {
|
||||||
targetMS := state.currentMS
|
bar.IncrBy(int(delta))
|
||||||
if targetMS > state.totalMS {
|
if state.currentBytes > barTotalBytes {
|
||||||
targetMS = state.totalMS
|
barTotalBytes = state.currentBytes
|
||||||
}
|
bar.SetTotal(barTotalBytes, false)
|
||||||
if targetMS > shownMS {
|
|
||||||
bar.IncrBy(int(targetMS - shownMS))
|
|
||||||
shownMS = targetMS
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
spinner.IncrBy(int(delta))
|
||||||
}
|
}
|
||||||
|
shownBytes = state.currentBytes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if bar != nil {
|
if bar != nil {
|
||||||
if shownMS < state.totalMS {
|
if shownBytes < barTotalBytes {
|
||||||
bar.IncrBy(int(state.totalMS - shownMS))
|
bar.IncrBy(int(barTotalBytes - shownBytes))
|
||||||
}
|
}
|
||||||
bar.SetTotal(state.totalMS, true)
|
bar.SetTotal(barTotalBytes, true)
|
||||||
} else {
|
} else {
|
||||||
spinner.SetTotal(-1, true)
|
spinner.SetTotal(-1, true)
|
||||||
}
|
}
|
||||||
state.currentMS = shownMS
|
state.currentBytes = shownBytes
|
||||||
state.scanErr = scanner.Err()
|
state.scanErr = scanner.Err()
|
||||||
scanDone <- state
|
scanDone <- state
|
||||||
}()
|
}()
|
||||||
@@ -469,6 +493,8 @@ func buildFFmpegStreamArgs(sourceURL, outputPath string, includeVideo bool) []st
|
|||||||
type scanState struct {
|
type scanState struct {
|
||||||
totalMS int64
|
totalMS int64
|
||||||
currentMS int64
|
currentMS int64
|
||||||
|
bitrateBPS int64
|
||||||
|
currentBytes int64
|
||||||
scanErr error
|
scanErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,6 +539,118 @@ func parseFFmpegOutTime(line string) (int64, bool) {
|
|||||||
return parseClockDurationMS(raw)
|
return parseClockDurationMS(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseFFmpegTotalSize(line string) (int64, bool) {
|
||||||
|
if !strings.HasPrefix(line, "total_size=") {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
raw := strings.TrimSpace(strings.TrimPrefix(line, "total_size="))
|
||||||
|
if raw == "" || strings.EqualFold(raw, "N/A") {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
v, err := strconv.ParseInt(raw, 10, 64)
|
||||||
|
if err != nil || v < 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFFmpegDurationBitrateBPS(line string) (int64, bool) {
|
||||||
|
idx := strings.Index(strings.ToLower(line), "bitrate:")
|
||||||
|
if idx < 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
raw := strings.TrimSpace(line[idx+len("bitrate:"):])
|
||||||
|
if raw == "" {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return parseFFmpegBitrateBPS(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFFmpegProgressBitrateBPS(line string) (int64, bool) {
|
||||||
|
if !strings.HasPrefix(line, "bitrate=") {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
raw := strings.TrimSpace(strings.TrimPrefix(line, "bitrate="))
|
||||||
|
if raw == "" || strings.EqualFold(raw, "N/A") {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return parseFFmpegBitrateBPS(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFFmpegBitrateBPS(raw string) (int64, bool) {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" || strings.EqualFold(raw, "N/A") {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
parts := strings.Fields(strings.ToLower(raw))
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
numStr := parts[0]
|
||||||
|
unit := ""
|
||||||
|
if len(parts) > 1 {
|
||||||
|
unit = parts[1]
|
||||||
|
} else {
|
||||||
|
i := 0
|
||||||
|
for i < len(numStr) {
|
||||||
|
c := numStr[i]
|
||||||
|
if (c >= '0' && c <= '9') || c == '.' {
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if i < len(numStr) {
|
||||||
|
unit = numStr[i:]
|
||||||
|
numStr = numStr[:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
num, err := strconv.ParseFloat(strings.TrimSpace(numStr), 64)
|
||||||
|
if err != nil || num <= 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
unit = strings.TrimSpace(strings.Trim(unit, ","))
|
||||||
|
mult := float64(1)
|
||||||
|
switch unit {
|
||||||
|
case "kb/s", "kbits/s", "kbit/s", "kbps":
|
||||||
|
mult = 1000
|
||||||
|
case "mb/s", "mbits/s", "mbit/s", "mbps":
|
||||||
|
mult = 1000 * 1000
|
||||||
|
case "gb/s", "gbits/s", "gbit/s", "gbps":
|
||||||
|
mult = 1000 * 1000 * 1000
|
||||||
|
case "b/s", "bit/s", "bits/s", "":
|
||||||
|
mult = 1
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
bps := int64(num * mult)
|
||||||
|
if bps <= 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return bps, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func estimateTotalBytesFromBitrate(totalMS, bitrateBPS int64) int64 {
|
||||||
|
if totalMS <= 0 || bitrateBPS <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return (totalMS * bitrateBPS) / 8000
|
||||||
|
}
|
||||||
|
|
||||||
|
func estimateTotalBytesFromProgress(totalMS, currentMS, currentBytes int64) int64 {
|
||||||
|
if totalMS <= 0 || currentMS <= 0 || currentBytes <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if currentMS > totalMS {
|
||||||
|
currentMS = totalMS
|
||||||
|
}
|
||||||
|
est := (currentBytes * totalMS) / currentMS
|
||||||
|
if est < currentBytes {
|
||||||
|
est = currentBytes
|
||||||
|
}
|
||||||
|
return est
|
||||||
|
}
|
||||||
|
|
||||||
func parseClockDurationMS(raw string) (int64, bool) {
|
func parseClockDurationMS(raw string) (int64, bool) {
|
||||||
parts := strings.Split(strings.TrimSpace(raw), ":")
|
parts := strings.Split(strings.TrimSpace(raw), ":")
|
||||||
if len(parts) != 3 {
|
if len(parts) != 3 {
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ func TestDeezerBlowfishKeyDerivation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func TestFileDeezerEncrypted(t *testing.T) {
|
func TestFileDeezerEncrypted(t *testing.T) {
|
||||||
trackID := "3135556"
|
trackID := "3135556"
|
||||||
plain := make([]byte, deezerBFChunkSize+777)
|
plain := make([]byte, deezerBFChunkSize+777)
|
||||||
@@ -221,6 +220,36 @@ func TestParseFFmpegDurationLine(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseFFmpegDurationBitrateBPS(t *testing.T) {
|
||||||
|
bps, ok := parseFFmpegDurationBitrateBPS(" Duration: 00:04:52.57, start: 0.000000, bitrate: 975 kb/s")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected bitrate parse to succeed")
|
||||||
|
}
|
||||||
|
if want := int64(975000); bps != want {
|
||||||
|
t.Fatalf("unexpected bitrate: got=%d want=%d", bps, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseFFmpegProgressBitrateBPS(t *testing.T) {
|
||||||
|
bps, ok := parseFFmpegProgressBitrateBPS("bitrate=1706.8kbits/s")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected progress bitrate parse to succeed")
|
||||||
|
}
|
||||||
|
if want := int64(1706800); bps != want {
|
||||||
|
t.Fatalf("unexpected bitrate: got=%d want=%d", bps, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseFFmpegTotalSize(t *testing.T) {
|
||||||
|
size, ok := parseFFmpegTotalSize("total_size=1234567")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected total_size parse to succeed")
|
||||||
|
}
|
||||||
|
if want := int64(1234567); size != want {
|
||||||
|
t.Fatalf("unexpected total_size: got=%d want=%d", size, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseFFmpegOutTime(t *testing.T) {
|
func TestParseFFmpegOutTime(t *testing.T) {
|
||||||
currentMS, ok := parseFFmpegOutTime("out_time=00:01:02.340000")
|
currentMS, ok := parseFFmpegOutTime("out_time=00:01:02.340000")
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -240,6 +269,20 @@ func TestParseClockDurationMSInvalid(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEstimateTotalBytesFromBitrate(t *testing.T) {
|
||||||
|
total := estimateTotalBytesFromBitrate(10000, 1600000)
|
||||||
|
if want := int64(2000000); total != want {
|
||||||
|
t.Fatalf("unexpected estimate from bitrate: got=%d want=%d", total, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEstimateTotalBytesFromProgress(t *testing.T) {
|
||||||
|
total := estimateTotalBytesFromProgress(10000, 2500, 500000)
|
||||||
|
if want := int64(2000000); total != want {
|
||||||
|
t.Fatalf("unexpected estimate from progress: got=%d want=%d", total, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildFFmpegStreamArgsAudioOnly(t *testing.T) {
|
func TestBuildFFmpegStreamArgsAudioOnly(t *testing.T) {
|
||||||
args := buildFFmpegStreamArgs("https://example.com/master.m3u8", "/tmp/out.m4a", false)
|
args := buildFFmpegStreamArgs("https://example.com/master.m3u8", "/tmp/out.m4a", false)
|
||||||
if !containsArgPair(args, "-map", "0:a:0") {
|
if !containsArgPair(args, "-map", "0:a:0") {
|
||||||
|
|||||||
Reference in New Issue
Block a user