From 93cfd3f7f7d126da7850a971240731c53be51b11 Mon Sep 17 00:00:00 2001 From: zhaarey <157944548+zhaarey@users.noreply.github.com> Date: Sun, 9 Jun 2024 17:46:36 +0800 Subject: [PATCH] add support for artist --- README.md | 2 + main.go | 164 +++++++++++++++++++++++++++++++++++++++++++++++++ main_atmos.go | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 331 insertions(+) diff --git a/README.md b/README.md index ca813ea..82daba1 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ 7. main 支持使用 go run main.go "txt文件地址" txt文件名需要指定格式 例如 cn_1707581102_THE BOOK 3.txt 建议使用这个[Reqable 脚本代码](https://telegra.ph/Reqable-For-Apple-Music-05-01) 自动生成 8. main 支持check 可以填入文本地址 或API数据库. 9. 新增get-m3u8-from-device 改为true 且设置端口`adb forward tcp:20020 tcp:20020`即从模拟器获取m3u8 +10. 文件夹和文件支持模板 +11. 支持下载歌手 `go run main.go https://music.apple.com/us/artist/taylor-swift/159260351` 本项目仅支持ALAC和Atmos - `alac (audio-alac-stereo)` diff --git a/main.go b/main.go index d4fdd2c..40e2b6d 100644 --- a/main.go +++ b/main.go @@ -1019,6 +1019,116 @@ func checkUrlPlaylist(url string) (string, string) { } } +func checkUrlArtist(url string) (string, string) { + pat := regexp.MustCompile(`^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w{2})(?:\/artist|\/artist\/.+))\/(?:id)?(\d[^\D]+)(?:$|\?)`) + matches := pat.FindAllStringSubmatch(url, -1) + + if matches == nil { + return "", "" + } else { + return matches[0][1], matches[0][2] + } +} + +func checkArtist(artistUrl string, token string) ([]string, error) { + storefront, artistId := checkUrlArtist(artistUrl) + Num := 0 + + var args []string + var urls []string + var options []string + for { + req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/artists/%s/albums?limit=100&offset=%d", storefront, artistId, Num), nil) + if err != nil { + return nil, err + } + 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("Origin", "https://music.apple.com") + do, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer do.Body.Close() + if do.StatusCode != http.StatusOK { + return nil, errors.New(do.Status) + } + obj := new(AutoGeneratedArtist) + err = json.NewDecoder(do.Body).Decode(&obj) + if err != nil { + return nil, err + } + for _, album := range obj.Data { + urls = append(urls, album.Attributes.URL) + options = append(options, fmt.Sprintf("AlbumName: %s(%s)", album.Attributes.Name, album.ID)) + } + Num = Num + 100 + if len(obj.Next) == 0 { + break + } + } + for i, option := range options { + fmt.Printf("%02d: %s\n", i+1, option) + } + 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.Print("Enter your choice: ") + input, _ := reader.ReadString('\n') + + // Remove newline and whitespace + input = strings.TrimSpace(input) + if input == "all" { + fmt.Println("You have selected all options:") + return urls, nil + } + + // Split input into string slices + selectedOptions := [][]string{} + parts := strings.Split(input, ",") + for _, part := range parts { + if strings.Contains(part, "-") { // Range setting + rangeParts := strings.Split(part, "-") + selectedOptions = append(selectedOptions, rangeParts) + } else { // Single option + selectedOptions = append(selectedOptions, []string{part}) + } + } + + // Print selected options + fmt.Println("You have selected the following options:") + for _, opt := range selectedOptions { + if len(opt) == 1 { // Single option + num, err := strconv.Atoi(opt[0]) + if err != nil { + fmt.Println("Invalid option:", opt[0]) + continue + } + if num > 0 && num <= len(options) { + args = append(args, urls[num-1]) + } else { + fmt.Println("Option out of range:", opt[0]) + } + } else if len(opt) == 2 { // Range + start, err1 := strconv.Atoi(opt[0]) + end, err2 := strconv.Atoi(opt[1]) + if err1 != nil || err2 != nil { + fmt.Println("Invalid range:", opt) + continue + } + if start < 1 || end > len(options) || start > end { + fmt.Println("Range out of range:", opt) + continue + } + for i := start; i <= end; i++ { + args = append(args, urls[i-1]) + } + } else { + fmt.Println("Invalid option:", opt) + } + } + return args, nil +} + func getMeta(albumId string, token string, storefront string) (*AutoGenerated, error) { var mtype string var page int @@ -1439,6 +1549,14 @@ func main() { fmt.Println("Failed to get token.") return } + if strings.Contains(os.Args[1], "/artist/") { + newArgs, err := checkArtist(os.Args[1], token) + if err != nil { + fmt.Println("Failed to get artist.") + return + } + os.Args = append([]string{os.Args[0]}, newArgs...) + } albumTotal := len(os.Args[1:]) for albumNum, url := range os.Args[1:] { fmt.Printf("Album %d of %d:\n", albumNum+1, albumTotal) @@ -2322,6 +2440,52 @@ type AutoGeneratedTrack struct { } `json:"data"` } +type AutoGeneratedArtist struct { + Next string `json:"next"` + Data []struct { + ID string `json:"id"` + Type string `json:"type"` + Href string `json:"href"` + Attributes struct { + Previews []struct { + URL string `json:"url"` + } `json:"previews"` + Artwork struct { + Width int `json:"width"` + Height int `json:"height"` + URL string `json:"url"` + BgColor string `json:"bgColor"` + TextColor1 string `json:"textColor1"` + TextColor2 string `json:"textColor2"` + TextColor3 string `json:"textColor3"` + TextColor4 string `json:"textColor4"` + } `json:"artwork"` + ArtistName string `json:"artistName"` + URL string `json:"url"` + DiscNumber int `json:"discNumber"` + GenreNames []string `json:"genreNames"` + HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` + IsMasteredForItunes bool `json:"isMasteredForItunes"` + IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` + ContentRating string `json:"contentRating"` + DurationInMillis int `json:"durationInMillis"` + ReleaseDate string `json:"releaseDate"` + Name string `json:"name"` + Isrc string `json:"isrc"` + AudioTraits []string `json:"audioTraits"` + HasLyrics bool `json:"hasLyrics"` + AlbumName string `json:"albumName"` + PlayParams struct { + ID string `json:"id"` + Kind string `json:"kind"` + } `json:"playParams"` + TrackNumber int `json:"trackNumber"` + AudioLocale string `json:"audioLocale"` + ComposerName string `json:"composerName"` + } `json:"attributes"` + } `json:"data"` +} + type SongLyrics struct { Data []struct { Id string `json:"id"` diff --git a/main_atmos.go b/main_atmos.go index 37d984a..f1a53db 100644 --- a/main_atmos.go +++ b/main_atmos.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "bytes" "encoding/binary" "encoding/json" @@ -964,6 +965,116 @@ func checkUrlPlaylist(url string) (string, string) { } } +func checkUrlArtist(url string) (string, string) { + pat := regexp.MustCompile(`^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w{2})(?:\/artist|\/artist\/.+))\/(?:id)?(\d[^\D]+)(?:$|\?)`) + matches := pat.FindAllStringSubmatch(url, -1) + + if matches == nil { + return "", "" + } else { + return matches[0][1], matches[0][2] + } +} + +func checkArtist(artistUrl string, token string) ([]string, error) { + storefront, artistId := checkUrlArtist(artistUrl) + Num := 0 + + var args []string + var urls []string + var options []string + for { + req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/artists/%s/albums?limit=100&offset=%d", storefront, artistId, Num), nil) + if err != nil { + return nil, err + } + 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("Origin", "https://music.apple.com") + do, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer do.Body.Close() + if do.StatusCode != http.StatusOK { + return nil, errors.New(do.Status) + } + obj := new(AutoGeneratedArtist) + err = json.NewDecoder(do.Body).Decode(&obj) + if err != nil { + return nil, err + } + for _, album := range obj.Data { + urls = append(urls, album.Attributes.URL) + options = append(options, fmt.Sprintf("AlbumName: %s(%s)", album.Attributes.Name, album.ID)) + } + Num = Num + 100 + if len(obj.Next) == 0 { + break + } + } + for i, option := range options { + fmt.Printf("%02d: %s\n", i+1, option) + } + 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.Print("Enter your choice: ") + input, _ := reader.ReadString('\n') + + // Remove newline and whitespace + input = strings.TrimSpace(input) + if input == "all" { + fmt.Println("You have selected all options:") + return urls, nil + } + + // Split input into string slices + selectedOptions := [][]string{} + parts := strings.Split(input, ",") + for _, part := range parts { + if strings.Contains(part, "-") { // Range setting + rangeParts := strings.Split(part, "-") + selectedOptions = append(selectedOptions, rangeParts) + } else { // Single option + selectedOptions = append(selectedOptions, []string{part}) + } + } + + // Print selected options + fmt.Println("You have selected the following options:") + for _, opt := range selectedOptions { + if len(opt) == 1 { // Single option + num, err := strconv.Atoi(opt[0]) + if err != nil { + fmt.Println("Invalid option:", opt[0]) + continue + } + if num > 0 && num <= len(options) { + args = append(args, urls[num-1]) + } else { + fmt.Println("Option out of range:", opt[0]) + } + } else if len(opt) == 2 { // Range + start, err1 := strconv.Atoi(opt[0]) + end, err2 := strconv.Atoi(opt[1]) + if err1 != nil || err2 != nil { + fmt.Println("Invalid range:", opt) + continue + } + if start < 1 || end > len(options) || start > end { + fmt.Println("Range out of range:", opt) + continue + } + for i := start; i <= end; i++ { + args = append(args, urls[i-1]) + } + } else { + fmt.Println("Invalid option:", opt) + } + } + return args, nil +} + func getMeta(albumId string, token string, storefront string) (*AutoGenerated, error) { var mtype string var page int @@ -1391,6 +1502,14 @@ func main() { fmt.Println("Failed to get token.") return } + if strings.Contains(os.Args[1], "/artist/") { + newArgs, err := checkArtist(os.Args[1], token) + if err != nil { + fmt.Println("Failed to get artist.") + return + } + os.Args = append([]string{os.Args[0]}, newArgs...) + } albumTotal := len(os.Args[1:]) for albumNum, url := range os.Args[1:] { fmt.Printf("Album %d of %d:\n", albumNum+1, albumTotal) @@ -2113,6 +2232,52 @@ type AutoGeneratedTrack struct { } `json:"data"` } +type AutoGeneratedArtist struct { + Next string `json:"next"` + Data []struct { + ID string `json:"id"` + Type string `json:"type"` + Href string `json:"href"` + Attributes struct { + Previews []struct { + URL string `json:"url"` + } `json:"previews"` + Artwork struct { + Width int `json:"width"` + Height int `json:"height"` + URL string `json:"url"` + BgColor string `json:"bgColor"` + TextColor1 string `json:"textColor1"` + TextColor2 string `json:"textColor2"` + TextColor3 string `json:"textColor3"` + TextColor4 string `json:"textColor4"` + } `json:"artwork"` + ArtistName string `json:"artistName"` + URL string `json:"url"` + DiscNumber int `json:"discNumber"` + GenreNames []string `json:"genreNames"` + HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` + IsMasteredForItunes bool `json:"isMasteredForItunes"` + IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` + ContentRating string `json:"contentRating"` + DurationInMillis int `json:"durationInMillis"` + ReleaseDate string `json:"releaseDate"` + Name string `json:"name"` + Isrc string `json:"isrc"` + AudioTraits []string `json:"audioTraits"` + HasLyrics bool `json:"hasLyrics"` + AlbumName string `json:"albumName"` + PlayParams struct { + ID string `json:"id"` + Kind string `json:"kind"` + } `json:"playParams"` + TrackNumber int `json:"trackNumber"` + AudioLocale string `json:"audioLocale"` + ComposerName string `json:"composerName"` + } `json:"attributes"` + } `json:"data"` +} + type SongLyrics struct { Data []struct { Id string `json:"id"`