mirror of
https://github.com/zhaarey/apple-music-downloader.git
synced 2025-10-23 15:11:05 +00:00
Add post-download conversion feature
This commit is contained in:
@@ -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)
|
||||
|
||||
119
main.go
119
main.go
@@ -635,6 +635,121 @@ 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++
|
||||
@@ -837,6 +952,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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user