|
|
@ -18,6 +18,7 @@ import ( |
|
|
|
"sort" |
|
|
|
"sort" |
|
|
|
"strings" |
|
|
|
"strings" |
|
|
|
"time" |
|
|
|
"time" |
|
|
|
|
|
|
|
"strconv" |
|
|
|
|
|
|
|
|
|
|
|
"github.com/abema/go-mp4" |
|
|
|
"github.com/abema/go-mp4" |
|
|
|
"github.com/grafov/m3u8" |
|
|
|
"github.com/grafov/m3u8" |
|
|
@ -909,8 +910,26 @@ func checkUrl(url string) (string, string) { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func checkUrlPlaylist(url string) (string, string) { |
|
|
|
|
|
|
|
pat := regexp.MustCompile(`^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w{2})(?:\/playlist|\/playlist\/.+))\/(?:id)?(pl\.[\w-]+)(?:$|\?)`) |
|
|
|
|
|
|
|
matches := pat.FindAllStringSubmatch(url, -1) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if matches == nil { |
|
|
|
|
|
|
|
return "", "" |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
return matches[0][1], matches[0][2] |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func getMeta(albumId string, token string, storefront string) (*AutoGenerated, error) { |
|
|
|
func getMeta(albumId string, token string, storefront string) (*AutoGenerated, error) { |
|
|
|
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/albums/%s", storefront, albumId), nil) |
|
|
|
var mtype string |
|
|
|
|
|
|
|
var page int |
|
|
|
|
|
|
|
if strings.Contains(albumId, "pl.") { |
|
|
|
|
|
|
|
mtype = "playlists" |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
mtype = "albums" |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s", storefront,mtype, albumId), nil) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
return nil, err |
|
|
|
return nil, err |
|
|
|
} |
|
|
|
} |
|
|
@ -939,6 +958,42 @@ func getMeta(albumId string, token string, storefront string) (*AutoGenerated, e |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
return nil, err |
|
|
|
return nil, err |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if strings.Contains(albumId, "pl.") { |
|
|
|
|
|
|
|
obj.Data[0].Attributes.ArtistName="Apple Music" |
|
|
|
|
|
|
|
if len(obj.Data[0].Relationships.Tracks.Next) > 0 { |
|
|
|
|
|
|
|
page=0 |
|
|
|
|
|
|
|
for{ |
|
|
|
|
|
|
|
page=page+100 |
|
|
|
|
|
|
|
pageStr := strconv.Itoa(page) |
|
|
|
|
|
|
|
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s/tracks?offset=%s", storefront, mtype, albumId, pageStr), 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) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
obj2 := new(AutoGeneratedTrack) |
|
|
|
|
|
|
|
err = json.NewDecoder(do.Body).Decode(&obj2) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return nil, err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
for _, value := range obj2.Data { |
|
|
|
|
|
|
|
obj.Data[0].Relationships.Tracks.Data = append(obj.Data[0].Relationships.Tracks.Data, value) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if len(obj2.Next)==0{ |
|
|
|
|
|
|
|
break |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
return obj, nil |
|
|
|
return obj, nil |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -985,6 +1040,9 @@ func rip(albumId string, token string, storefront string) error { |
|
|
|
return err |
|
|
|
return err |
|
|
|
} |
|
|
|
} |
|
|
|
albumFolder := fmt.Sprintf("%s - %s", meta.Data[0].Attributes.ArtistName, meta.Data[0].Attributes.Name) |
|
|
|
albumFolder := fmt.Sprintf("%s - %s", meta.Data[0].Attributes.ArtistName, meta.Data[0].Attributes.Name) |
|
|
|
|
|
|
|
if strings.HasSuffix(albumFolder, ".") { |
|
|
|
|
|
|
|
albumFolder = strings.ReplaceAll(albumFolder, ".", "") |
|
|
|
|
|
|
|
} |
|
|
|
sanAlbumFolder := filepath.Join("AM-DL downloads", forbiddenNames.ReplaceAllString(albumFolder, "_")) |
|
|
|
sanAlbumFolder := filepath.Join("AM-DL downloads", forbiddenNames.ReplaceAllString(albumFolder, "_")) |
|
|
|
os.MkdirAll(sanAlbumFolder, os.ModePerm) |
|
|
|
os.MkdirAll(sanAlbumFolder, os.ModePerm) |
|
|
|
fmt.Println(albumFolder) |
|
|
|
fmt.Println(albumFolder) |
|
|
@ -1056,7 +1114,12 @@ func main() { |
|
|
|
albumTotal := len(os.Args[1:]) |
|
|
|
albumTotal := len(os.Args[1:]) |
|
|
|
for albumNum, url := range os.Args[1:] { |
|
|
|
for albumNum, url := range os.Args[1:] { |
|
|
|
fmt.Printf("Album %d of %d:\n", albumNum+1, albumTotal) |
|
|
|
fmt.Printf("Album %d of %d:\n", albumNum+1, albumTotal) |
|
|
|
storefront, albumId := checkUrl(url) |
|
|
|
var storefront, albumId string |
|
|
|
|
|
|
|
if strings.Contains(url, "playlist") { |
|
|
|
|
|
|
|
storefront, albumId = checkUrlPlaylist(url) |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
storefront, albumId = checkUrl(url) |
|
|
|
|
|
|
|
} |
|
|
|
if albumId == "" { |
|
|
|
if albumId == "" { |
|
|
|
fmt.Printf("Invalid URL: %s\n", url) |
|
|
|
fmt.Printf("Invalid URL: %s\n", url) |
|
|
|
continue |
|
|
|
continue |
|
|
@ -1586,6 +1649,7 @@ type AutoGenerated struct { |
|
|
|
} `json:"artists"` |
|
|
|
} `json:"artists"` |
|
|
|
Tracks struct { |
|
|
|
Tracks struct { |
|
|
|
Href string `json:"href"` |
|
|
|
Href string `json:"href"` |
|
|
|
|
|
|
|
Next string `json:"next"` |
|
|
|
Data []struct { |
|
|
|
Data []struct { |
|
|
|
ID string `json:"id"` |
|
|
|
ID string `json:"id"` |
|
|
|
Type string `json:"type"` |
|
|
|
Type string `json:"type"` |
|
|
@ -1643,3 +1707,61 @@ type AutoGenerated struct { |
|
|
|
} `json:"relationships"` |
|
|
|
} `json:"relationships"` |
|
|
|
} `json:"data"` |
|
|
|
} `json:"data"` |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type AutoGeneratedTrack struct { |
|
|
|
|
|
|
|
Href string `json:"href"` |
|
|
|
|
|
|
|
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"` |
|
|
|
|
|
|
|
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"` |
|
|
|
|
|
|
|
Relationships struct { |
|
|
|
|
|
|
|
Artists struct { |
|
|
|
|
|
|
|
Href string `json:"href"` |
|
|
|
|
|
|
|
Data []struct { |
|
|
|
|
|
|
|
ID string `json:"id"` |
|
|
|
|
|
|
|
Type string `json:"type"` |
|
|
|
|
|
|
|
Href string `json:"href"` |
|
|
|
|
|
|
|
Attributes struct { |
|
|
|
|
|
|
|
Name string `json:"name"` |
|
|
|
|
|
|
|
} `json:"attributes"` |
|
|
|
|
|
|
|
} `json:"data"` |
|
|
|
|
|
|
|
} `json:"artists"` |
|
|
|
|
|
|
|
} `json:"relationships"` |
|
|
|
|
|
|
|
} `json:"data"` |
|
|
|
|
|
|
|
} |