Merge pull request #50 from itouakirai/main

Add: mv tags;artist mv dl
This commit is contained in:
ZHAAREY 2025-02-12 09:19:56 +08:00 committed by GitHub
commit 96748d0df9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 123 additions and 38 deletions

152
main.go
View File

@ -155,11 +155,11 @@ func getUrlSong(songUrl string, token string) (string, error) {
songAlbumUrl := fmt.Sprintf("https://music.apple.com/%s/album/1/%s?i=%s", storefront, albumId, songId) songAlbumUrl := fmt.Sprintf("https://music.apple.com/%s/album/1/%s?i=%s", storefront, albumId, songId)
return songAlbumUrl, nil return songAlbumUrl, nil
} }
func getUrlArtistName(artistUrl string, token string) (string, error) { func getUrlArtistName(artistUrl string, token string) (string, string, error) {
storefront, artistId := checkUrlArtist(artistUrl) storefront, artistId := checkUrlArtist(artistUrl)
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/artists/%s", storefront, artistId), nil) req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/artists/%s", storefront, artistId), nil)
if err != nil { if err != nil {
return "", err return "", "", err
} }
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
@ -168,21 +168,21 @@ func getUrlArtistName(artistUrl string, token string) (string, error) {
query.Set("l", Config.Language) query.Set("l", Config.Language)
do, err := http.DefaultClient.Do(req) do, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return "", err return "", "", err
} }
defer do.Body.Close() defer do.Body.Close()
if do.StatusCode != http.StatusOK { if do.StatusCode != http.StatusOK {
return "", errors.New(do.Status) return "", "", errors.New(do.Status)
} }
obj := new(structs.AutoGeneratedArtist) obj := new(structs.AutoGeneratedArtist)
err = json.NewDecoder(do.Body).Decode(&obj) err = json.NewDecoder(do.Body).Decode(&obj)
if err != nil { if err != nil {
return "", err return "", "", err
} }
return obj.Data[0].Attributes.Name, nil return obj.Data[0].Attributes.Name, obj.Data[0].ID , nil
} }
func checkArtist(artistUrl string, token string) ([]string, error) { func checkArtist(artistUrl string, token string, relationship string) ([]string, error) {
storefront, artistId := checkUrlArtist(artistUrl) storefront, artistId := checkUrlArtist(artistUrl)
Num := 0 Num := 0
@ -190,7 +190,7 @@ func checkArtist(artistUrl string, token string) ([]string, error) {
var urls []string var urls []string
var options []string var options []string
for { for {
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/artists/%s/albums?limit=100&offset=%d&l=%s", storefront, artistId, Num, Config.Language), nil) req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/artists/%s/%s?limit=100&offset=%d&l=%s", storefront, artistId, relationship, Num, Config.Language), nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -227,7 +227,7 @@ func checkArtist(artistUrl string, token string) ([]string, error) {
return urls, nil return urls, nil
} }
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
fmt.Println("Please select from the following options (multiple options separated by commas, ranges supported, or type 'all' to select all)") fmt.Println("Please select from the following " + relationship + " options (multiple options separated by commas, ranges supported, or type 'all' to select all)")
fmt.Print("Enter your choice: ") fmt.Print("Enter your choice: ")
input, _ := reader.ReadString('\n') input, _ := reader.ReadString('\n')
@ -479,7 +479,7 @@ func downloadTrack(trackNum int, trackTotal int, meta *structs.AutoGenerated, tr
counter.Success++ counter.Success++
return return
} }
err := mvDownloader(track.ID, sanAlbumFolder, token, storefront, mediaUserToken) err := mvDownloader(track.ID, sanAlbumFolder, token, storefront, mediaUserToken, meta)
if err != nil { if err != nil {
fmt.Println("\u26A0 Failed to dl MV:", err) fmt.Println("\u26A0 Failed to dl MV:", err)
counter.Error++ counter.Error++
@ -1132,7 +1132,7 @@ func main() {
} }
os.Args = args os.Args = args
if strings.Contains(os.Args[0], "/artist/") { if strings.Contains(os.Args[0], "/artist/") {
urlArtistName, err := getUrlArtistName(os.Args[0], token) urlArtistName, urlArtistID, err := getUrlArtistName(os.Args[0], token)
if err != nil { if err != nil {
fmt.Println("Failed to get artistname.") fmt.Println("Failed to get artistname.")
return return
@ -1140,13 +1140,19 @@ func main() {
//fmt.Println("get artistname:", urlArtistName) //fmt.Println("get artistname:", urlArtistName)
Config.ArtistFolderFormat = strings.NewReplacer( Config.ArtistFolderFormat = strings.NewReplacer(
"{UrlArtistName}", LimitString(urlArtistName), "{UrlArtistName}", LimitString(urlArtistName),
"{ArtistId}", urlArtistID,
).Replace(Config.ArtistFolderFormat) ).Replace(Config.ArtistFolderFormat)
newArgs, err := checkArtist(os.Args[0], token) albumArgs, err := checkArtist(os.Args[0], token, "albums")
if err != nil { if err != nil {
fmt.Println("Failed to get artist.") fmt.Println("Failed to get artist albums.")
return return
} }
os.Args = newArgs mvArgs, err := checkArtist(os.Args[0], token, "music-videos")
if err != nil {
fmt.Println("Failed to get artist music-videos.")
return
}
os.Args = append(albumArgs, mvArgs...)
} }
albumTotal := len(os.Args) albumTotal := len(os.Args)
for { for {
@ -1169,8 +1175,18 @@ func main() {
counter.Success++ counter.Success++
continue continue
} }
mvSaveDir := strings.NewReplacer(
"{ArtistName}", "",
"{UrlArtistName}", "",
"{ArtistId}", "",
).Replace(Config.ArtistFolderFormat)
if mvSaveDir != "" {
mvSaveDir = filepath.Join(Config.AlacSaveFolder, mvSaveDir)
} else {
mvSaveDir = Config.AlacSaveFolder
}
storefront, albumId = checkUrlMv(urlRaw) storefront, albumId = checkUrlMv(urlRaw)
err := mvDownloader(albumId, Config.AlacSaveFolder, token, storefront, Config.MediaUserToken) err := mvDownloader(albumId, mvSaveDir, token, storefront, Config.MediaUserToken, nil)
if err != nil { if err != nil {
fmt.Println("\u26A0 Failed to dl MV:", err) fmt.Println("\u26A0 Failed to dl MV:", err)
counter.Error++ counter.Error++
@ -1216,48 +1232,109 @@ func main() {
counter = structs.Counter{} counter = structs.Counter{}
} }
} }
func mvDownloader(adamID string, saveDir string, token string, storefront string, mediaUserToken string) error { func mvDownloader(adamID string, saveDir string, token string, storefront string, mediaUserToken string, meta *structs.AutoGenerated) error {
MVInfo, err := getMVInfoFromAdam(adamID, token, storefront) MVInfo, err := getMVInfoFromAdam(adamID, token, storefront)
if err != nil { if err != nil {
fmt.Println("\u26A0 Failed to get MV manifest:", err) fmt.Println("\u26A0 Failed to get MV manifest:", err)
counter.NotSong++
return nil return nil
} }
vidPath := filepath.Join(saveDir, fmt.Sprintf("%s_vid.mp4", adamID)) vidPath := filepath.Join(saveDir, fmt.Sprintf("%s_vid.mp4", adamID))
audPath := filepath.Join(saveDir, fmt.Sprintf("%s_aud.mp4", adamID)) audPath := filepath.Join(saveDir, fmt.Sprintf("%s_aud.mp4", adamID))
mvOutPath := filepath.Join(saveDir, fmt.Sprintf("%s.mp4", forbiddenNames.ReplaceAllString(MVInfo.Data[0].Attributes.Name, "_"))) mvOutPath := filepath.Join(saveDir, fmt.Sprintf("%s.mp4", forbiddenNames.ReplaceAllString(MVInfo.Data[0].Attributes.Name, "_")))
fmt.Println(MVInfo.Data[0].Attributes.Name)
exists, _ := fileExists(mvOutPath) exists, _ := fileExists(mvOutPath)
if exists { if exists {
fmt.Println("MV already exists locally.") fmt.Println("MV already exists locally.")
return nil return nil
} }
mvm3u8url, _, _ := runv3.GetWebplayback(adamID, token, mediaUserToken, true)
//videom3u8url, audiom3u8url , _ := mvQualitySelect(mvm3u8url)
videom3u8url, _ := extractVideo(mvm3u8url)
audiom3u8url, _ := extractMvAudio(mvm3u8url)
//fmt.Println(audiom3u8url)
videokeyAndUrls, _ := runv3.Run(adamID, videom3u8url, token, mediaUserToken, true) mvm3u8url, _, _ := runv3.GetWebplayback(adamID, token, mediaUserToken, true)
audiokeyAndUrls, _ := runv3.Run(adamID, audiom3u8url, token, mediaUserToken, true)
//fmt.Println(videokeyAndUrls)
//fmt.Println(audiokeyAndUrls)
os.MkdirAll(saveDir, os.ModePerm) os.MkdirAll(saveDir, os.ModePerm)
fmt.Println(MVInfo.Data[0].Attributes.Name + "-VIDEO") //video
videom3u8url, _ := extractVideo(mvm3u8url)
videokeyAndUrls, _ := runv3.Run(adamID, videom3u8url, token, mediaUserToken, true)
_ = runv3.ExtMvData(videokeyAndUrls, vidPath) _ = runv3.ExtMvData(videokeyAndUrls, vidPath)
//audio
fmt.Println(MVInfo.Data[0].Attributes.Name + "-AUDIO") audiom3u8url, _ := extractMvAudio(mvm3u8url)
audiokeyAndUrls, _ := runv3.Run(adamID, audiom3u8url, token, mediaUserToken, true)
_ = runv3.ExtMvData(audiokeyAndUrls, audPath) _ = runv3.ExtMvData(audiokeyAndUrls, audPath)
muxCmd := exec.Command("MP4Box", "-quiet", "-add", vidPath, "-add", audPath, "-keep-utc", "-new", mvOutPath)
//tags
tags := []string{
"tool=",
fmt.Sprintf("artist=%s", MVInfo.Data[0].Attributes.ArtistName),
fmt.Sprintf("title=%s", MVInfo.Data[0].Attributes.Name),
fmt.Sprintf("genre=%s", MVInfo.Data[0].Attributes.GenreNames),
fmt.Sprintf("created=%s", MVInfo.Data[0].Attributes.ReleaseDate),
fmt.Sprintf("ISRC=%s", MVInfo.Data[0].Attributes.Isrc),
}
// ContentRating tag
if MVInfo.Data[0].Attributes.ContentRating == "explicit" {
tags = append(tags, "rating=1")
} else if MVInfo.Data[0].Attributes.ContentRating == "clean" {
tags = append(tags, "rating=2")
} else {
tags = append(tags, "rating=0")
}
//获取传入的专辑信息当中该mv所在的位置
var trackTotal int
var trackNum int
var index int
if meta !=nil {
trackTotal = len(meta.Data[0].Relationships.Tracks.Data)
for i, track := range meta.Data[0].Relationships.Tracks.Data {
if adamID == track.ID {
index = i
trackNum = i + 1
}
}
}
//根据情况额外添加可使用的tags
if meta != nil {
if meta.Data[0].Type == "playlists" && !Config.UseSongInfoForPlaylist {
tags = append(tags, "disk=1/1")
tags = append(tags, fmt.Sprintf("album=%s", meta.Data[0].Attributes.Name))
tags = append(tags, fmt.Sprintf("track=%d", trackNum))
tags = append(tags, fmt.Sprintf("tracknum=%d/%d", trackNum, trackTotal))
tags = append(tags, fmt.Sprintf("album_artist=%s", meta.Data[0].Attributes.ArtistName))
tags = append(tags, fmt.Sprintf("performer=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName))
tags = append(tags, fmt.Sprintf("copyright=%s", meta.Data[0].Attributes.Copyright))
tags = append(tags, fmt.Sprintf("UPC=%s", meta.Data[0].Attributes.Upc))
} else {
tags = append(tags, fmt.Sprintf("album=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.AlbumName))
tags = append(tags, fmt.Sprintf("disk=%d/%d", meta.Data[0].Relationships.Tracks.Data[index].Attributes.DiscNumber, meta.Data[0].Relationships.Tracks.Data[trackTotal-1].Attributes.DiscNumber))
tags = append(tags, fmt.Sprintf("track=%d", meta.Data[0].Relationships.Tracks.Data[index].Attributes.TrackNumber))
tags = append(tags, fmt.Sprintf("tracknum=%d/%d", meta.Data[0].Relationships.Tracks.Data[index].Attributes.TrackNumber, trackTotal))
tags = append(tags, fmt.Sprintf("album_artist=%s", meta.Data[0].Attributes.ArtistName))
tags = append(tags, fmt.Sprintf("performer=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName))
tags = append(tags, fmt.Sprintf("copyright=%s", meta.Data[0].Attributes.Copyright))
tags = append(tags, fmt.Sprintf("UPC=%s", meta.Data[0].Attributes.Upc))
}
} else {
tags = append(tags, fmt.Sprintf("album=%s", MVInfo.Data[0].Attributes.AlbumName))
tags = append(tags, fmt.Sprintf("disk=%d", MVInfo.Data[0].Attributes.DiscNumber))
tags = append(tags, fmt.Sprintf("track=%d", MVInfo.Data[0].Attributes.TrackNumber))
tags = append(tags, fmt.Sprintf("tracknum=%d", MVInfo.Data[0].Attributes.TrackNumber))
//tags = append(tags, fmt.Sprintf("album_artist=%s", MVInfo.Data[0].Attributes.ArtistName))
tags = append(tags, fmt.Sprintf("performer=%s", MVInfo.Data[0].Attributes.ArtistName))
}
//mux and add tag
tagsString := strings.Join(tags, ":")
muxCmd := exec.Command("MP4Box", "-itags", tagsString, "-quiet", "-add", vidPath, "-add", audPath, "-keep-utc", "-new", mvOutPath)
fmt.Printf("MV Remuxing...") fmt.Printf("MV Remuxing...")
if err := muxCmd.Run(); err != nil { if err := muxCmd.Run(); err != nil {
fmt.Printf("MV mux failed: %v\n", err) fmt.Printf("MV mux failed: %v\n", err)
return err return err
} }
fmt.Printf("\rMV Remuxed. \n") fmt.Printf("\rMV Remuxed. \n")
_ = os.Remove(vidPath) defer os.Remove(vidPath)
_ = os.Remove(audPath) defer os.Remove(audPath)
return nil return nil
} }
@ -1283,13 +1360,13 @@ func extractMvAudio(c string) (string, error) {
return "", err return "", err
} }
videoString := string(body) audioString := string(body)
from, listType, err := m3u8.DecodeFrom(strings.NewReader(videoString), true) from, listType, err := m3u8.DecodeFrom(strings.NewReader(audioString), true)
if err != nil || listType != m3u8.MASTER { if err != nil || listType != m3u8.MASTER {
return "", errors.New("m3u8 not of media type") return "", errors.New("m3u8 not of media type")
} }
video := from.(*m3u8.MasterPlaylist) audio := from.(*m3u8.MasterPlaylist)
var audioPriority = []string{"audio-atmos", "audio-ac3", "audio-stereo-256"} var audioPriority = []string{"audio-atmos", "audio-ac3", "audio-stereo-256"}
if Config.MVAudioType == "ac3" { if Config.MVAudioType == "ac3" {
@ -1307,7 +1384,7 @@ func extractMvAudio(c string) (string, error) {
} }
var audioStreams []AudioStream var audioStreams []AudioStream
for _, variant := range video.Variants { for _, variant := range audio.Variants {
for _, audiov := range variant.Alternatives { for _, audiov := range variant.Alternatives {
if audiov.URI != "" { if audiov.URI != "" {
for _, priority := range audioPriority { for _, priority := range audioPriority {
@ -1336,7 +1413,7 @@ func extractMvAudio(c string) (string, error) {
sort.Slice(audioStreams, func(i, j int) bool { sort.Slice(audioStreams, func(i, j int) bool {
return audioStreams[i].Rank > audioStreams[j].Rank return audioStreams[i].Rank > audioStreams[j].Rank
}) })
fmt.Println("Audio: " + audioStreams[0].GroupID)
return audioStreams[0].URL, nil return audioStreams[0].URL, nil
} }
@ -1874,6 +1951,7 @@ func extractVideo(c string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
fmt.Println("Video: " + variant.Resolution + "-" + variant.VideoRange)
break break
} }
} }

View File

@ -355,7 +355,10 @@ func ExtMvData (keyAndUrls string, savePath string)(error) {
fmt.Printf("下载链接 %s 失败:%v\n", url, err) fmt.Printf("下载链接 %s 失败:%v\n", url, err)
return err return err
} }
if resp.StatusCode != http.StatusOK {
fmt.Printf("链接 %s 响应失败:%v\n", url, resp.Status)
return errors.New(resp.Status)
}
// 将响应体写入输出文件 // 将响应体写入输出文件
_, err = io.Copy(barWriter, resp.Body) _, err = io.Copy(barWriter, resp.Body)
defer resp.Body.Close() // 注意及时关闭响应体,避免资源泄露 defer resp.Body.Close() // 注意及时关闭响应体,避免资源泄露

View File

@ -437,11 +437,15 @@ type AutoGeneratedMusicVideo struct {
TextColor3 string `json:"textColor3"` TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"` TextColor4 string `json:"textColor4"`
} `json:"artwork"` } `json:"artwork"`
AlbumName string `json:"albumName"`
ArtistName string `json:"artistName"` ArtistName string `json:"artistName"`
URL string `json:"url"` URL string `json:"url"`
GenreNames []string `json:"genreNames"` GenreNames []string `json:"genreNames"`
DurationInMillis int `json:"durationInMillis"` DurationInMillis int `json:"durationInMillis"`
Isrc string `json:"isrc"` Isrc string `json:"isrc"`
TrackNumber int `json:"trackNumber"`
DiscNumber int `json:"discNumber"`
ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"` ReleaseDate string `json:"releaseDate"`
Name string `json:"name"` Name string `json:"name"`
Has4K bool `json:"has4K"` Has4K bool `json:"has4K"`