Merge pull request #80 from york9675/main

Add post-download conversion feature
This commit is contained in:
ZHAAREY
2025-09-23 00:10:03 +08:00
committed by GitHub
3 changed files with 160 additions and 2 deletions

View File

@@ -50,3 +50,10 @@ mv-max: 2160
# 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.
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)

148
main.go
View File

@@ -635,14 +635,131 @@ func handleSearch(searchType string, queryParts []string, token string) (string,
// 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) {
var err error
counter.Total++
fmt.Printf("Track %d of %d: %s\n", track.TaskNum, track.TaskTotal, track.Type)
//提前获取到的播放列表下track所在的专辑信息
if track.PreType == "playlists" && Config.UseSongInfoForPlaylist {
track.GetAlbumData(token)
}
//mv dl dev
if track.Type == "music-videos" {
if len(mediaUserToken) <= 50 {
@@ -664,6 +781,7 @@ func ripTrack(track *task.Track, token string, mediaUserToken string) {
counter.Success++
return
}
needDlAacLc := false
if dl_aac && Config.AacType == "aac-lc" {
needDlAacLc = true
@@ -743,6 +861,17 @@ func ripTrack(track *task.Track, token string, mediaUserToken string) {
trackPath := filepath.Join(track.SaveDir, track.SaveName)
lrcFilename := fmt.Sprintf("%s.%s", forbiddenNames.ReplaceAllString(songName, "_"), Config.LrcFormat)
// Determine possible post-conversion target file (so we can skip re-download)
var convertedPath string
considerConverted := false
if Config.ConvertAfterDownload &&
Config.ConvertFormat != "" &&
strings.ToLower(Config.ConvertFormat) != "copy" &&
!Config.ConvertKeepOriginal {
convertedPath = strings.TrimSuffix(trackPath, filepath.Ext(trackPath)) + "." + strings.ToLower(Config.ConvertFormat)
considerConverted = true
}
//get lrc
var lrc string = ""
if Config.EmbedLrc || Config.SaveLrcFile {
@@ -762,16 +891,27 @@ func ripTrack(track *task.Track, token string, mediaUserToken string) {
}
}
exists, err := fileExists(trackPath)
// Existence check now considers converted output (if original was deleted)
existsOriginal, err := fileExists(trackPath)
if err != nil {
fmt.Println("Failed to check if track exists.")
}
if exists {
if existsOriginal {
fmt.Println("Track already exists locally.")
counter.Success++
okDict[track.PreID] = append(okDict[track.PreID], track.TaskNum)
return
}
if considerConverted {
existsConverted, err2 := fileExists(convertedPath)
if err2 == nil && existsConverted {
fmt.Println("Converted track already exists locally.")
counter.Success++
okDict[track.PreID] = append(okDict[track.PreID], track.TaskNum)
return
}
}
if needDlAacLc {
if len(mediaUserToken) <= 50 {
fmt.Println("Invalid media-user-token")
@@ -837,6 +977,10 @@ func ripTrack(track *task.Track, token string, mediaUserToken string) {
counter.Unavailable++
return
}
// CONVERSION FEATURE hook
convertIfNeeded(track)
counter.Success++
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"`
MVAudioType string `yaml:"mv-audio-type"`
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 {