Add post-download conversion feature

This commit is contained in:
York
2025-09-20 13:46:30 +08:00
committed by GitHub
parent 5578600359
commit 90f97e0d14
3 changed files with 133 additions and 0 deletions

View File

@@ -50,3 +50,10 @@ mv-max: 2160
# if your account is from Japan, you must use jp. # if your account is from Japan, you must use jp.
# if the storefront is different from your account, you will see a "failed to get lyrics" error in most of the songs. By default the storefront is set to US if not set. # if the storefront is different from your account, you will see a "failed to get lyrics" error in most of the songs. By default the storefront is set to US if not set.
storefront: "enter your account storefront" storefront: "enter your account storefront"
# Conversion settings
convert-after-download: false # Enable post-download conversion (requires ffmpeg)
convert-format: "flac" # flac | mp3 | opus | wav | copy (no re-encode)
convert-keep-original: false # Keep original file after successful conversion
convert-skip-if-source-matches: true # If already in target format, skip
ffmpeg-path: "ffmpeg" # Override if ffmpeg is not in PATH
convert-extra-args: "" # Additional raw args appended (advanced)

119
main.go
View File

@@ -635,6 +635,121 @@ func handleSearch(searchType string, queryParts []string, token string) (string,
// END: New functions for search functionality // END: New functions for search functionality
// CONVERSION FEATURE: Determine if source codec is lossy (rough heuristic by extension/codec name).
func isLossySource(ext string, codec string) bool {
ext = strings.ToLower(ext)
if ext == ".m4a" && (codec == "AAC" || strings.Contains(codec, "AAC") || strings.Contains(codec, "ATMOS")) {
return true
}
if ext == ".mp3" || ext == ".opus" || ext == ".ogg" {
return true
}
return false
}
// CONVERSION FEATURE: Build ffmpeg arguments for desired target.
func buildFFmpegArgs(ffmpegPath, inPath, outPath, targetFmt, extraArgs string) ([]string, error) {
args := []string{"-y", "-i", inPath, "-vn"}
switch targetFmt {
case "flac":
args = append(args, "-c:a", "flac")
case "mp3":
// VBR quality 2 ~ high quality
args = append(args, "-c:a", "libmp3lame", "-qscale:a", "2")
case "opus":
// Medium/high quality
args = append(args, "-c:a", "libopus", "-b:a", "192k", "-vbr", "on")
case "wav":
args = append(args, "-c:a", "pcm_s16le")
case "copy":
// Just container copy (probably pointless for same container)
args = append(args, "-c", "copy")
default:
return nil, fmt.Errorf("unsupported convert-format: %s", targetFmt)
}
if extraArgs != "" {
// naive split; for complex quoting you could enhance
args = append(args, strings.Fields(extraArgs)...)
}
args = append(args, outPath)
return args, nil
}
// CONVERSION FEATURE: Perform conversion if enabled.
func convertIfNeeded(track *task.Track) {
if !Config.ConvertAfterDownload {
return
}
if Config.ConvertFormat == "" {
return
}
srcPath := track.SavePath
if srcPath == "" {
return
}
ext := strings.ToLower(filepath.Ext(srcPath))
targetFmt := strings.ToLower(Config.ConvertFormat)
// Map extension for output
if targetFmt == "copy" {
fmt.Println("Convert (copy) requested; skipping because it produces no new format.")
return
}
if Config.ConvertSkipIfSourceMatch {
if ext == "."+targetFmt {
fmt.Printf("Conversion skipped (already %s)\n", targetFmt)
return
}
}
outBase := strings.TrimSuffix(srcPath, ext)
outPath := outBase + "." + targetFmt
// Warn about lossy -> lossless
if Config.ConvertWarnLossyToLossless && (targetFmt == "flac" || targetFmt == "wav") &&
isLossySource(ext, track.Codec) {
fmt.Println("Warning: Converting lossy source to lossless container will not improve quality.")
}
if _, err := exec.LookPath(Config.FFmpegPath); err != nil {
fmt.Printf("ffmpeg not found at '%s'; skipping conversion.\n", Config.FFmpegPath)
return
}
args, err := buildFFmpegArgs(Config.FFmpegPath, srcPath, outPath, targetFmt, Config.ConvertExtraArgs)
if err != nil {
fmt.Println("Conversion config error:", err)
return
}
fmt.Printf("Converting -> %s ...\n", targetFmt)
cmd := exec.Command(Config.FFmpegPath, args...)
cmd.Stdout = nil
cmd.Stderr = nil
start := time.Now()
if err := cmd.Run(); err != nil {
fmt.Println("Conversion failed:", err)
// leave original
return
}
fmt.Printf("Conversion completed in %s: %s\n", time.Since(start).Truncate(time.Millisecond), filepath.Base(outPath))
if !Config.ConvertKeepOriginal {
if err := os.Remove(srcPath); err != nil {
fmt.Println("Failed to remove original after conversion:", err)
} else {
track.SavePath = outPath
track.SaveName = filepath.Base(outPath)
fmt.Println("Original removed.")
}
} else {
// Keep both but point track to new file (optional decision)
track.SavePath = outPath
track.SaveName = filepath.Base(outPath)
}
}
func ripTrack(track *task.Track, token string, mediaUserToken string) { func ripTrack(track *task.Track, token string, mediaUserToken string) {
var err error var err error
counter.Total++ counter.Total++
@@ -837,6 +952,10 @@ func ripTrack(track *task.Track, token string, mediaUserToken string) {
counter.Unavailable++ counter.Unavailable++
return return
} }
// CONVERSION FEATURE hook
convertIfNeeded(track)
counter.Success++ counter.Success++
okDict[track.PreID] = append(okDict[track.PreID], track.TaskNum) okDict[track.PreID] = append(okDict[track.PreID], track.TaskNum)
} }

View File

@@ -38,6 +38,13 @@ type ConfigSet struct {
DlAlbumcoverForPlaylist bool `yaml:"dl-albumcover-for-playlist"` DlAlbumcoverForPlaylist bool `yaml:"dl-albumcover-for-playlist"`
MVAudioType string `yaml:"mv-audio-type"` MVAudioType string `yaml:"mv-audio-type"`
MVMax int `yaml:"mv-max"` MVMax int `yaml:"mv-max"`
ConvertAfterDownload bool `yaml:"convert-after-download"`
ConvertFormat string `yaml:"convert-format"`
ConvertKeepOriginal bool `yaml:"convert-keep-original"`
ConvertSkipIfSourceMatch bool `yaml:"convert-skip-if-source-matches"`
FFmpegPath string `yaml:"ffmpeg-path"`
ConvertExtraArgs string `yaml:"convert-extra-args"`
ConvertWarnLossyToLossless bool `yaml:"convert-warn-lossy-to-lossless"`
} }
type Counter struct { type Counter struct {