diff --git a/main.go b/main.go index e90584a..677098b 100644 --- a/main.go +++ b/main.go @@ -41,6 +41,7 @@ var ( dl_select bool dl_song bool artist_select bool + debug_mode bool alac_max *int atmos_max *int aac_type *string @@ -428,7 +429,7 @@ func contains(slice []string, item string) bool { } // 下载单曲逻辑 -func downloadTrack(trackNum int, trackTotal int, meta *structs.AutoGenerated, track structs.TrackData, albumId, token, storefront, userToken, sanAlbumFolder, Codec string, counter *structs.Counter) { +func downloadTrack(trackNum int, trackTotal int, meta *structs.AutoGenerated, track structs.TrackData, albumId, token, storefront, mediaUserToken, sanAlbumFolder, Codec string, counter *structs.Counter) { counter.Total++ fmt.Printf("Track %d of %d:\n", trackNum, trackTotal) manifest, err := getInfoFromAdam(track.ID, token, storefront) @@ -514,8 +515,8 @@ func downloadTrack(trackNum int, trackTotal int, meta *structs.AutoGenerated, tr //get lrc var lrc string = "" - if userToken != "your-media-user-token" && (Config.EmbedLrc || Config.SaveLrcFile) { - ttml, err := getSongLyrics(track.ID, storefront, token, userToken) + if mediaUserToken != "" && len(mediaUserToken) > 10 { + ttml, err := getSongLyrics(track.ID, storefront, token, mediaUserToken) if err != nil { fmt.Println("Failed to get lyrics") } else if Config.LrcFormat == "ttml" { @@ -555,12 +556,12 @@ func downloadTrack(trackNum int, trackTotal int, meta *structs.AutoGenerated, tr return } if needDlAacLc { - if userToken == "your-media-user-token" { - fmt.Println("media-user-token Unset!") + if mediaUserToken == "" || len(mediaUserToken) <= 10 { + fmt.Println("Invalid media-user-token") counter.Error++ return } - err := runv3.Run(track.ID, trackPath, token, userToken) + err := runv3.Run(track.ID, trackPath, token, mediaUserToken) if err != nil { fmt.Println("Failed to dl aac-lc:", err) counter.Error++ @@ -621,7 +622,52 @@ func downloadTrack(trackNum int, trackTotal int, meta *structs.AutoGenerated, tr okDict[albumId] = append(okDict[albumId], trackNum) } -func rip(albumId string, token string, storefront string, userToken string, urlArg_i string) error { +func rip(albumId string, token string, storefront string, mediaUserToken string, urlArg_i string) error { + meta, err := getMeta(albumId, token, storefront) + if err != nil { + return err + } + + if debug_mode { + // Print album info + fmt.Println(meta.Data[0].Attributes.ArtistName) + fmt.Println(meta.Data[0].Attributes.Name) + + for trackNum, track := range meta.Data[0].Relationships.Tracks.Data { + trackNum++ + fmt.Printf("\nTrack %d of %d:\n", trackNum, len(meta.Data[0].Relationships.Tracks.Data)) + fmt.Printf("%02d. %s\n", trackNum, track.Attributes.Name) + + manifest, err := getInfoFromAdam(track.ID, token, storefront) + if err != nil { + fmt.Printf("Failed to get manifest for track %d: %v\n", trackNum, err) + continue + } + + var m3u8Url string + if manifest.Attributes.ExtendedAssetUrls.EnhancedHls != "" { + m3u8Url = manifest.Attributes.ExtendedAssetUrls.EnhancedHls + } else if mediaUserToken != "" && len(mediaUserToken) > 10 { + // Try to get m3u8 from device if media-user-token is set + m3u8Url, err = checkM3u8(track.ID, "song") + if err != nil { + fmt.Printf("Failed to get m3u8 from device for track %d: %v\n", trackNum, err) + continue + } + } + + if m3u8Url != "" { + _, err = extractMediaQuality(m3u8Url) + if err != nil { + fmt.Printf("Failed to extract quality info for track %d: %v\n", trackNum, err) + continue + } + } else { + fmt.Println("\nNo audio formats available - valid media-user-token may be required") + } + } + return nil // Return directly without showing statistics + } var Codec string if dl_atmos { Codec = "ATMOS" @@ -630,11 +676,6 @@ func rip(albumId string, token string, storefront string, userToken string, urlA } else { Codec = "ALAC" } - meta, err := getMeta(albumId, token, storefront) - if err != nil { - fmt.Println("Failed to get album metadata.\n") - return err - } var singerFoldername string if Config.ArtistFolderFormat != "" { if strings.Contains(albumId, "pl.") { @@ -770,31 +811,63 @@ func rip(albumId string, token string, storefront string, userToken string, urlA //get animated artwork if Config.SaveAnimatedArtwork && meta.Data[0].Attributes.EditorialVideo.MotionDetailSquare.Video != "" { fmt.Println("Found Animation Artwork.") - motionvideoUrl, err := extractVideo(meta.Data[0].Attributes.EditorialVideo.MotionDetailSquare.Video) + + // Download tall version + motionvideoUrlTall, err := extractVideo(meta.Data[0].Attributes.EditorialVideo.MotionDetailTall.Video) if err != nil { - fmt.Println("no motion video.\n", err) - } - exists, err := fileExists(filepath.Join(sanAlbumFolder, "animated_artwork.mp4")) - if err != nil { - fmt.Println("Failed to check if animated artwork exists.") - } - if exists { - fmt.Println("Animated artwork already exists locally.") + fmt.Println("no motion video tall.\n", err) } else { - fmt.Println("Animation Artwork Downloading...") - cmd := exec.Command("ffmpeg", "-loglevel", "quiet", "-y", "-i", motionvideoUrl, "-c", "copy", filepath.Join(sanAlbumFolder, "animated_artwork.mp4")) - if err := cmd.Run(); err != nil { - fmt.Printf("animated artwork dl err: %v\n", err) - } else { - fmt.Println("Animation Artwork Downloaded") + exists, err := fileExists(filepath.Join(sanAlbumFolder, "animated_artwork_tall.mp4")) + if err != nil { + fmt.Println("Failed to check if animated artwork tall exists.") } - if Config.EmbyAnimatedArtwork { - cmd2 := exec.Command("ffmpeg", "-i", filepath.Join(sanAlbumFolder, "animated_artwork.mp4"), "-vf", "scale=440:-1", "-r", "24", "-f", "gif", filepath.Join(sanAlbumFolder, "folder.jpg")) - if err := cmd2.Run(); err != nil { - fmt.Printf("animated artwork to gif err: %v\n", err) + if exists { + fmt.Println("Animated artwork tall already exists locally.") + } else { + fmt.Println("Animation Artwork Tall Downloading...") + cmd := exec.Command("ffmpeg", "-loglevel", "quiet", "-y", "-i", motionvideoUrlTall, "-c", "copy", filepath.Join(sanAlbumFolder, "animated_artwork_tall.mp4")) + if err := cmd.Run(); err != nil { + fmt.Printf("animated artwork tall dl err: %v\n", err) + } else { + fmt.Println("Animation Artwork Tall Downloaded") } } + } + // Download square version + motionvideoUrlSquare, err := extractVideo(meta.Data[0].Attributes.EditorialVideo.MotionDetailSquare.Video) + if err != nil { + fmt.Println("no motion video square.\n", err) + } else { + exists, err := fileExists(filepath.Join(sanAlbumFolder, "animated_artwork_square.mp4")) + if err != nil { + fmt.Println("Failed to check if animated artwork square exists.") + } + if exists { + fmt.Println("Animated artwork square already exists locally.") + } else { + fmt.Println("Animation Artwork Square Downloading...") + cmd := exec.Command("ffmpeg", "-loglevel", "quiet", "-y", "-i", motionvideoUrlSquare, "-c", "copy", filepath.Join(sanAlbumFolder, "animated_artwork_square.mp4")) + if err := cmd.Run(); err != nil { + fmt.Printf("animated artwork square dl err: %v\n", err) + } else { + fmt.Println("Animation Artwork Square Downloaded") + } + } + } + + if Config.EmbyAnimatedArtwork { + // Convert tall version to gif + cmd2 := exec.Command("ffmpeg", "-i", filepath.Join(sanAlbumFolder, "animated_artwork_tall.mp4"), "-vf", "scale=440:-1", "-r", "24", "-f", "gif", filepath.Join(sanAlbumFolder, "folder_tall.jpg")) + if err := cmd2.Run(); err != nil { + fmt.Printf("animated artwork tall to gif err: %v\n", err) + } + + // Convert square version to gif + cmd3 := exec.Command("ffmpeg", "-i", filepath.Join(sanAlbumFolder, "animated_artwork_square.mp4"), "-vf", "scale=440:-1", "-r", "24", "-f", "gif", filepath.Join(sanAlbumFolder, "folder_square.jpg")) + if err := cmd3.Run(); err != nil { + fmt.Printf("animated artwork square to gif err: %v\n", err) + } } } trackTotal := len(meta.Data[0].Relationships.Tracks.Data) @@ -812,7 +885,7 @@ func rip(albumId string, token string, storefront string, userToken string, urlA for trackNum, track := range meta.Data[0].Relationships.Tracks.Data { trackNum++ if urlArg_i == track.ID { - downloadTrack(trackNum, trackTotal, meta, track, albumId, token, storefront, userToken, sanAlbumFolder, Codec, &counter) + downloadTrack(trackNum, trackTotal, meta, track, albumId, token, storefront, mediaUserToken, sanAlbumFolder, Codec, &counter) return nil } } @@ -866,7 +939,7 @@ func rip(albumId string, token string, storefront string, userToken string, urlA } if isInArray(selected, trackNum) { counter.Total++ - downloadTrack(trackNum, trackTotal, meta, track, albumId, token, storefront, userToken, sanAlbumFolder, Codec, &counter) + downloadTrack(trackNum, trackTotal, meta, track, albumId, token, storefront, mediaUserToken, sanAlbumFolder, Codec, &counter) } } return nil @@ -973,6 +1046,7 @@ func main() { pflag.BoolVar(&dl_select, "select", false, "Enable selective download") pflag.BoolVar(&dl_song, "song", false, "Enable single song download mode") pflag.BoolVar(&artist_select, "all-album", false, "Download all artist albums") + pflag.BoolVar(&debug_mode, "debug", false, "Enable debug mode to show audio quality information") alac_max = pflag.Int("alac-max", Config.AlacMax, "Specify the max quality for download alac") atmos_max = pflag.Int("atmos-max", Config.AtmosMax, "Specify the max quality for download atmos") aac_type = pflag.String("aac-type", Config.AacType, "Select AAC type, aac aac-binaural aac-downmix") @@ -1265,36 +1339,120 @@ func extractMediaQuality(b string) (string, error) { return "", errors.New("m3u8 not of master type") } master := from.(*m3u8.MasterPlaylist) - sort.Slice(master.Variants, func(i, j int) bool { - return master.Variants[i].AverageBandwidth > master.Variants[j].AverageBandwidth - }) + if debug_mode { + fmt.Println("\nDebug: All Available Variants:") + fmt.Println("-----------------------------") + for _, variant := range master.Variants { + fmt.Printf("Codec: %s, Audio: %s, Bandwidth: %d\n", + variant.Codecs, variant.Audio, variant.Bandwidth) + } + fmt.Println("-----------------------------") + + var hasAAC, hasLossless, hasHiRes, hasAtmos, hasDolbyAudio bool + var aacQuality, losslessQuality, hiResQuality, atmosQuality, dolbyAudioQuality string + + // Check for all formats + for _, variant := range master.Variants { + if variant.Codecs == "mp4a.40.2" { // AAC + hasAAC = true + split := strings.Split(variant.Audio, "-") + if len(split) >= 3 { + bitrate, _ := strconv.Atoi(split[2]) + currentBitrate := 0 + if aacQuality != "" { + current := strings.Split(aacQuality, " | ")[2] + current = strings.Split(current, " ")[0] + currentBitrate, _ = strconv.Atoi(current) + } + if bitrate > currentBitrate { + aacQuality = fmt.Sprintf("AAC | 2 Channel | %d kbps", bitrate) + } + } + } else if variant.Codecs == "ec-3" && strings.Contains(variant.Audio, "atmos") { // Dolby Atmos + hasAtmos = true + split := strings.Split(variant.Audio, "-") + if len(split) > 0 { + bitrateStr := split[len(split)-1] + // Remove leading "2" if present in "2768" + if len(bitrateStr) == 4 && bitrateStr[0] == '2' { + bitrateStr = bitrateStr[1:] + } + bitrate, _ := strconv.Atoi(bitrateStr) + currentBitrate := 0 + if atmosQuality != "" { + current := strings.Split(strings.Split(atmosQuality, " | ")[2], " ")[0] + currentBitrate, _ = strconv.Atoi(current) + } + if bitrate > currentBitrate { + atmosQuality = fmt.Sprintf("E-AC-3 | 16 Channel | %d kbps", bitrate) + } + } + } else if variant.Codecs == "alac" { // ALAC (Lossless or Hi-Res) + split := strings.Split(variant.Audio, "-") + if len(split) >= 3 { + bitDepth := split[len(split)-1] + sampleRate := split[len(split)-2] + sampleRateInt, _ := strconv.Atoi(sampleRate) + if sampleRateInt > 48000 { // Hi-Res + hasHiRes = true + hiResQuality = fmt.Sprintf("ALAC | 2 Channel | %s-bit/%d kHz", bitDepth, sampleRateInt/1000) + } else { // Standard Lossless + hasLossless = true + losslessQuality = fmt.Sprintf("ALAC | 2 Channel | %s-bit/%d kHz", bitDepth, sampleRateInt/1000) + } + } + } else if variant.Codecs == "ac-3" { // Dolby Audio + hasDolbyAudio = true + split := strings.Split(variant.Audio, "-") + if len(split) > 0 { + bitrate, _ := strconv.Atoi(split[len(split)-1]) + dolbyAudioQuality = fmt.Sprintf("AC-3 | 16 Channel | %d kbps", bitrate) + } + } + } + + fmt.Println("Available Audio Formats:") + fmt.Println("------------------------") + fmt.Printf("AAC : %s\n", formatAvailability(hasAAC, aacQuality)) + fmt.Printf("Lossless : %s\n", formatAvailability(hasLossless, losslessQuality)) + fmt.Printf("Hi-Res Lossless : %s\n", formatAvailability(hasHiRes, hiResQuality)) + fmt.Printf("Dolby Atmos : %s\n", formatAvailability(hasAtmos, atmosQuality)) + fmt.Printf("Dolby Audio : %s\n", formatAvailability(hasDolbyAudio, dolbyAudioQuality)) + fmt.Println("------------------------") + + return "", nil + } var Quality string for _, variant := range master.Variants { - if dl_aac { - if variant.Codecs == "mp4a.40.2" { - aacregex := regexp.MustCompile(`audio-stereo-\d+`) - replaced := aacregex.ReplaceAllString(variant.Audio, "aac") + if dl_atmos { + if variant.Codecs == "ec-3" && strings.Contains(variant.Audio, "atmos") { split := strings.Split(variant.Audio, "-") - if replaced == Config.AacType { - Quality = fmt.Sprintf("%skbps", split[2]) + if len(split) > 0 { + Quality = fmt.Sprintf("%s kbps", split[len(split)-1]) + break + } + } else if variant.Codecs == "ac-3" { // Add Dolby Audio support for --atmos flag + split := strings.Split(variant.Audio, "-") + if len(split) > 0 { + Quality = fmt.Sprintf("%s kbps", split[len(split)-1]) + break + } + } + } else if dl_aac { + if variant.Codecs == "mp4a.40.2" { + split := strings.Split(variant.Audio, "-") + if len(split) >= 3 { + Quality = fmt.Sprintf("%s kbps", split[2]) break } } } else { if variant.Codecs == "alac" { split := strings.Split(variant.Audio, "-") - length := len(split) - length_int, err := strconv.Atoi(split[length-2]) - if err != nil { - return "", err - } - if length_int <= Config.AlacMax { - HZ, err := strconv.Atoi(split[length-2]) - if err != nil { - fmt.Println(err) - } - KHZ := float64(HZ) / 1000.0 - Quality = fmt.Sprintf("%sB-%.1fkHz", split[length-1], KHZ) + if len(split) >= 3 { + bitDepth := split[len(split)-1] + sampleRate := split[len(split)-2] + Quality = fmt.Sprintf("%s-bit / %s Hz", bitDepth, sampleRate) break } } @@ -1303,6 +1461,13 @@ func extractMediaQuality(b string) (string, error) { return Quality, nil } +func formatAvailability(available bool, quality string) string { + if !available { + return "Not Available" + } + return quality +} + func extractMedia(b string) (string, error) { masterUrl, err := url.Parse(b) if err != nil { @@ -1332,30 +1497,37 @@ func extractMedia(b string) (string, error) { }) for _, variant := range master.Variants { if dl_atmos { - if variant.Codecs == "ec-3" { - split := strings.Split(variant.Audio, "-") - length := len(split) - length_int, err := strconv.Atoi(split[length-1]) + if variant.Codecs == "ec-3" && strings.Contains(variant.Audio, "atmos") { + if debug_mode { + fmt.Printf("Debug: Found Dolby Atmos variant - %s (Bitrate: %d kbps)\n", + variant.Audio, variant.Bandwidth/1000) + } + streamUrlTemp, err := masterUrl.Parse(variant.URI) if err != nil { return "", err } - if length_int <= Config.AtmosMax { - fmt.Printf("%s\n", variant.Audio) - streamUrlTemp, err := masterUrl.Parse(variant.URI) - if err != nil { - panic(err) - } - streamUrl = streamUrlTemp - break + streamUrl = streamUrlTemp + break + } else if variant.Codecs == "ac-3" { // Add Dolby Audio support + if debug_mode { + fmt.Printf("Debug: Found Dolby Audio variant - %s (Bitrate: %d kbps)\n", + variant.Audio, variant.Bandwidth/1000) } + streamUrlTemp, err := masterUrl.Parse(variant.URI) + if err != nil { + return "", err + } + streamUrl = streamUrlTemp + break } } else if dl_aac { if variant.Codecs == "mp4a.40.2" { + if debug_mode { + fmt.Printf("Debug: Found AAC variant - %s (Bitrate: %d)\n", variant.Audio, variant.Bandwidth) + } aacregex := regexp.MustCompile(`audio-stereo-\d+`) replaced := aacregex.ReplaceAllString(variant.Audio, "aac") - //split := strings.Split(variant.Audio, "-") if replaced == Config.AacType { - fmt.Printf("%s\n", variant.Audio) streamUrlTemp, err := masterUrl.Parse(variant.URI) if err != nil { panic(err) @@ -1373,7 +1545,6 @@ func extractMedia(b string) (string, error) { return "", err } if length_int <= Config.AlacMax { - fmt.Printf("%s-bit / %s Hz\n", split[length-1], split[length-2]) streamUrlTemp, err := masterUrl.Parse(variant.URI) if err != nil { panic(err) diff --git a/utils/structs/structs.go b/utils/structs/structs.go index 0d31af7..535d1cf 100644 --- a/utils/structs/structs.go +++ b/utils/structs/structs.go @@ -274,12 +274,12 @@ type AutoGenerated struct { } `json:"playParams"` IsCompilation bool `json:"isCompilation"` EditorialVideo struct { + MotionDetailTall struct { + Video string `json:"video"` + } `json:"motionDetailTall"` MotionDetailSquare struct { Video string `json:"video"` } `json:"motionDetailSquare"` - MotionSquareVideo1x1 struct { - Video string `json:"video"` - } `json:"motionSquareVideo1x1"` } `json:"editorialVideo"` } `json:"attributes"` Relationships struct {