diff --git a/config.yaml b/config.yaml index 2dffba7..8556566 100644 --- a/config.yaml +++ b/config.yaml @@ -42,3 +42,5 @@ apple-master-choice : "[M]" use-songinfo-for-playlist: false #if set true,will download album cover for playlist dl-albumcover-for-playlist: false +mv-audio-type: atmos #atmos ac3 aac +mv-max: 2160 diff --git a/main.go b/main.go index 045dc1b..dd83367 100644 --- a/main.go +++ b/main.go @@ -44,6 +44,8 @@ var ( debug_mode bool alac_max *int atmos_max *int + mv_max *int + mv_audio_type *string aac_type *string Config structs.ConfigSet counter structs.Counter @@ -100,7 +102,7 @@ func checkUrl(url string) (string, string) { return matches[0][1], matches[0][2] } } -func checkUrlMv(url string) (string) { +func checkUrlMv(url string) string { pat := regexp.MustCompile(`^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w{2})(?:\/music-video|\/music-video\/.+))\/(?:id)?(\d[^\D]+)(?:$|\?)`) matches := pat.FindAllStringSubmatch(url, -1) @@ -478,7 +480,7 @@ func downloadTrack(trackNum int, trackTotal int, meta *structs.AutoGenerated, tr return } err := mvDownloader(track.ID, sanAlbumFolder, token, mediaUserToken) - if err !=nil { + if err != nil { fmt.Println("\u26A0 Failed to dl MV:", err) counter.Error++ return @@ -1104,6 +1106,8 @@ func main() { 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") + mv_audio_type = pflag.String("mv-audio-type", Config.MVAudioType, "Select MV audio type, atmos ac3 aac") + mv_max = pflag.Int("mv-max", Config.MVMax, "Specify the max quality for download MV") // Custom usage message for help pflag.Usage = func() { @@ -1117,6 +1121,8 @@ func main() { Config.AlacMax = *alac_max Config.AtmosMax = *atmos_max Config.AacType = *aac_type + Config.MVAudioType = *mv_audio_type + Config.MVMax = *mv_max args := pflag.Args() if len(args) == 0 { @@ -1164,7 +1170,7 @@ func main() { continue } err := mvDownloader(checkUrlMv(urlRaw), Config.AlacSaveFolder, token, Config.MediaUserToken) - if err !=nil { + if err != nil { fmt.Println("\u26A0 Failed to dl MV:", err) counter.Error++ continue @@ -1209,10 +1215,10 @@ func main() { counter = structs.Counter{} } } -func mvDownloader(adamID string, saveDir string, token string, mediaUserToken string)(error){ +func mvDownloader(adamID string, saveDir string, token string, mediaUserToken string) error { vidPath := filepath.Join(saveDir, fmt.Sprintf("%s_vid.mp4", adamID)) audPath := filepath.Join(saveDir, fmt.Sprintf("%s_aud.mp4", adamID)) - mvOutPath := filepath.Join(saveDir, fmt.Sprintf("%s.mkv", adamID)) + mvOutPath := filepath.Join(saveDir, fmt.Sprintf("%s.mp4", adamID)) exists, _ := fileExists(mvOutPath) if exists { fmt.Println("MV already exists locally.") @@ -1222,7 +1228,6 @@ func mvDownloader(adamID string, saveDir string, token string, mediaUserToken st //videom3u8url, audiom3u8url , _ := mvQualitySelect(mvm3u8url) videom3u8url, _ := extractVideo(mvm3u8url) audiom3u8url, _ := extractMvAudio(mvm3u8url) - //fmt.Println(videom3u8url) //fmt.Println(audiom3u8url) videokeyAndUrls, _ := runv3.Run(adamID, videom3u8url, token, mediaUserToken, true) @@ -1255,37 +1260,77 @@ func extractMvAudio(c string) (string, error) { if err != nil { return "", err } + resp, err := http.Get(c) if err != nil { return "", err } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { return "", errors.New(resp.Status) } + body, err := io.ReadAll(resp.Body) if err != nil { return "", err } + videoString := string(body) from, listType, err := m3u8.DecodeFrom(strings.NewReader(videoString), true) if err != nil || listType != m3u8.MASTER { return "", errors.New("m3u8 not of media type") } + video := from.(*m3u8.MasterPlaylist) - var streamUrl *url.URL + + var audioPriority = []string{"audio-atmos", "audio-ac3", "audio-stereo-256"} + if Config.MVAudioType == "ac3" { + audioPriority = []string{"audio-ac3", "audio-stereo-256"} + } else if Config.MVAudioType == "aac" { + audioPriority = []string{"audio-stereo-256"} + } + + re := regexp.MustCompile(`_gr(\d+)_`) + + type AudioStream struct { + URL string + Rank int + GroupID string + } + var audioStreams []AudioStream + for _, variant := range video.Variants { for _, audiov := range variant.Alternatives { - if audiov.GroupId == "audio-stereo-256" { - streamUrl, _ = MediaUrl.Parse(audiov.URI) - break + if audiov.URI != "" { + for _, priority := range audioPriority { + if audiov.GroupId == priority { + matches := re.FindStringSubmatch(audiov.URI) + if len(matches) == 2 { + var rank int + fmt.Sscanf(matches[1], "%d", &rank) + streamUrl, _ := MediaUrl.Parse(audiov.URI) + audioStreams = append(audioStreams, AudioStream{ + URL: streamUrl.String(), + Rank: rank, + GroupID: audiov.GroupId, + }) + } + } + } } } } - if streamUrl == nil { - return "", errors.New("no video codec found") + + if len(audioStreams) == 0 { + return "", errors.New("no suitable audio stream found") } - return streamUrl.String(), nil + + sort.Slice(audioStreams, func(i, j int) bool { + return audioStreams[i].Rank > audioStreams[j].Rank + }) + + return audioStreams[0].URL, nil } func conventSyllableTTMLToLRC(ttml string) (string, error) { @@ -1775,38 +1820,62 @@ func extractVideo(c string) (string, error) { if err != nil { return "", err } + resp, err := http.Get(c) if err != nil { return "", err } defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { return "", errors.New(resp.Status) } + body, err := io.ReadAll(resp.Body) if err != nil { return "", err } videoString := string(body) + from, listType, err := m3u8.DecodeFrom(strings.NewReader(videoString), true) if err != nil || listType != m3u8.MASTER { return "", errors.New("m3u8 not of media type") } + video := from.(*m3u8.MasterPlaylist) + + re := regexp.MustCompile(`_(\d+)x(\d+)_`) + var streamUrl *url.URL sort.Slice(video.Variants, func(i, j int) bool { return video.Variants[i].AverageBandwidth > video.Variants[j].AverageBandwidth }) - if len(video.Variants) > 0 { - highestBandwidthVariant := video.Variants[0] - streamUrl, err = MediaUrl.Parse(highestBandwidthVariant.URI) - if err != nil { - return "", err + + maxHeight := Config.MVMax + + for _, variant := range video.Variants { + matches := re.FindStringSubmatch(variant.URI) + if len(matches) == 3 { + height := matches[2] + var h int + _, err := fmt.Sscanf(height, "%d", &h) + if err != nil { + continue + } + if h <= maxHeight { + streamUrl, err = MediaUrl.Parse(variant.URI) + if err != nil { + return "", err + } + break + } } } + if streamUrl == nil { - return "", errors.New("no video codec found") + return "", errors.New("no suitable video stream found") } + return streamUrl.String(), nil } diff --git a/utils/structs/structs.go b/utils/structs/structs.go index 535d1cf..82902a5 100644 --- a/utils/structs/structs.go +++ b/utils/structs/structs.go @@ -34,6 +34,8 @@ type ConfigSet struct { LimitMax int `yaml:"limit-max"` UseSongInfoForPlaylist bool `yaml:"use-songinfo-for-playlist"` DlAlbumcoverForPlaylist bool `yaml:"dl-albumcover-for-playlist"` + MVAudioType string `yaml:"mv-audio-type"` + MVMax int `yaml:"mv-max"` } type Counter struct {