mirror of
https://github.com/zhaarey/apple-music-downloader.git
synced 2025-10-23 15:11:05 +00:00
Compare commits
21 Commits
a086da99c0
...
4a4d3c993f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a4d3c993f | ||
|
|
3f173c1187 | ||
|
|
a4b4ee17b3 | ||
|
|
480acb7d7c | ||
|
|
040716e93a | ||
|
|
be89d9e4c1 | ||
|
|
db3f537e9e | ||
|
|
85c1c9fc60 | ||
|
|
b6bc55ce1c | ||
|
|
beb95b8730 | ||
|
|
421db91731 | ||
|
|
7c590213ef | ||
|
|
65172a3d3d | ||
|
|
8f284cadea | ||
|
|
56d37ccce7 | ||
|
|
c9ecc16a3b | ||
|
|
7dcd164885 | ||
|
|
be1467340d | ||
|
|
acb5a7ce1e | ||
|
|
21728cabb6 | ||
|
|
b2bcdfde88 |
@@ -13,6 +13,7 @@ cover-size: 5000x5000
|
||||
cover-format: jpg #jpg png or original
|
||||
alac-save-folder: AM-DL downloads
|
||||
atmos-save-folder: AM-DL-Atmos downloads
|
||||
aac-save-folder: AM-DL-AAC downloads
|
||||
max-memory-limit: 256 # MB
|
||||
decrypt-m3u8-port: "127.0.0.1:10020"
|
||||
get-m3u8-port: "127.0.0.1:20020"
|
||||
|
||||
243
utils/ampapi/album.go
Normal file
243
utils/ampapi/album.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package ampapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetAlbumResp(storefront string, id string, language string, token string) (*AlbumResp, error) {
|
||||
var err error
|
||||
if token == "" {
|
||||
token, err = GetToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/albums/%s", storefront, id), 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")
|
||||
query := url.Values{}
|
||||
query.Set("omit[resource]", "autos")
|
||||
query.Set("include", "tracks,artists,record-labels")
|
||||
query.Set("include[songs]", "artists")
|
||||
//query.Set("fields[artists]", "name,artwork")
|
||||
//query.Set("fields[albums:albums]", "artistName,artwork,name,releaseDate,url")
|
||||
//query.Set("fields[record-labels]", "name")
|
||||
query.Set("extend", "editorialVideo,extendedAssetUrls")
|
||||
query.Set("l", language)
|
||||
req.URL.RawQuery = query.Encode()
|
||||
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(AlbumResp)
|
||||
err = json.NewDecoder(do.Body).Decode(&obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(obj.Data[0].Relationships.Tracks.Next) > 0 {
|
||||
next := obj.Data[0].Relationships.Tracks.Next
|
||||
for {
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com%s", next), 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")
|
||||
query := req.URL.Query()
|
||||
query.Set("omit[resource]", "autos")
|
||||
query.Set("include", "artists")
|
||||
query.Set("extend", "editorialVideo,extendedAssetUrls")
|
||||
req.URL.RawQuery = query.Encode()
|
||||
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(TrackResp)
|
||||
err = json.NewDecoder(do.Body).Decode(&obj2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
obj.Data[0].Relationships.Tracks.Data = append(obj.Data[0].Relationships.Tracks.Data, obj2.Data...)
|
||||
next = obj2.Next
|
||||
if len(next) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func GetAlbumRespByHref(href string, language string, token string) (*AlbumResp, error) {
|
||||
var err error
|
||||
if token == "" {
|
||||
token, err = GetToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
href = strings.Split(href, "?")[0]
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com%s/albums", href), 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")
|
||||
query := url.Values{}
|
||||
query.Set("omit[resource]", "autos")
|
||||
query.Set("include", "tracks,artists,record-labels")
|
||||
query.Set("include[songs]", "artists")
|
||||
//query.Set("fields[artists]", "name,artwork")
|
||||
//query.Set("fields[albums:albums]", "artistName,artwork,name,releaseDate,url")
|
||||
//query.Set("fields[record-labels]", "name")
|
||||
query.Set("extend", "editorialVideo,extendedAssetUrls")
|
||||
query.Set("l", language)
|
||||
req.URL.RawQuery = query.Encode()
|
||||
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(AlbumResp)
|
||||
err = json.NewDecoder(do.Body).Decode(&obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(obj.Data[0].Relationships.Tracks.Next) > 0 {
|
||||
next := obj.Data[0].Relationships.Tracks.Next
|
||||
for {
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com%s", next), 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")
|
||||
query := req.URL.Query()
|
||||
query.Set("omit[resource]", "autos")
|
||||
query.Set("include", "artists")
|
||||
query.Set("extend", "editorialVideo,extendedAssetUrls")
|
||||
req.URL.RawQuery = query.Encode()
|
||||
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(TrackResp)
|
||||
err = json.NewDecoder(do.Body).Decode(&obj2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
obj.Data[0].Relationships.Tracks.Data = append(obj.Data[0].Relationships.Tracks.Data, obj2.Data...)
|
||||
next = obj2.Next
|
||||
if len(next) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
type AlbumResp struct {
|
||||
Href string `json:"href"`
|
||||
Next string `json:"next"`
|
||||
Data []AlbumRespData `json:"data"`
|
||||
}
|
||||
|
||||
type AlbumRespData struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Href string `json:"href"`
|
||||
Attributes struct {
|
||||
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"`
|
||||
IsSingle bool `json:"isSingle"`
|
||||
URL string `json:"url"`
|
||||
IsComplete bool `json:"isComplete"`
|
||||
GenreNames []string `json:"genreNames"`
|
||||
TrackCount int `json:"trackCount"`
|
||||
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
||||
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
|
||||
ContentRating string `json:"contentRating"`
|
||||
ReleaseDate string `json:"releaseDate"`
|
||||
Name string `json:"name"`
|
||||
RecordLabel string `json:"recordLabel"`
|
||||
Upc string `json:"upc"`
|
||||
AudioTraits []string `json:"audioTraits"`
|
||||
Copyright string `json:"copyright"`
|
||||
PlayParams struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
} `json:"playParams"`
|
||||
IsCompilation bool `json:"isCompilation"`
|
||||
EditorialVideo struct {
|
||||
MotionTall struct {
|
||||
Video string `json:"video"`
|
||||
} `json:"motionTallVideo3x4"`
|
||||
MotionSquare struct {
|
||||
Video string `json:"video"`
|
||||
} `json:"motionSquareVideo1x1"`
|
||||
MotionDetailTall struct {
|
||||
Video string `json:"video"`
|
||||
} `json:"motionDetailTall"`
|
||||
MotionDetailSquare struct {
|
||||
Video string `json:"video"`
|
||||
} `json:"motionDetailSquare"`
|
||||
} `json:"editorialVideo"`
|
||||
} `json:"attributes"`
|
||||
Relationships struct {
|
||||
RecordLabels struct {
|
||||
Href string `json:"href"`
|
||||
Data []interface{} `json:"data"`
|
||||
} `json:"record-labels"`
|
||||
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"`
|
||||
Artwork struct {
|
||||
Url string `json:"url"`
|
||||
} `json:"artwork"`
|
||||
} `json:"attributes"`
|
||||
} `json:"data"`
|
||||
} `json:"artists"`
|
||||
Tracks TrackResp `json:"tracks"`
|
||||
} `json:"relationships"`
|
||||
}
|
||||
1
utils/ampapi/artist.go
Normal file
1
utils/ampapi/artist.go
Normal file
@@ -0,0 +1 @@
|
||||
package ampapi
|
||||
145
utils/ampapi/musicvideo.go
Normal file
145
utils/ampapi/musicvideo.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package ampapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func GetMusicVideoResp(storefront string, id string, language string, token string) (*MusicVideoResp, error) {
|
||||
var err error
|
||||
if token == "" {
|
||||
token, err = GetToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/music-videos/%s", storefront, id), 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")
|
||||
query := url.Values{}
|
||||
//query.Set("omit[resource]", "autos")
|
||||
query.Set("include", "albums,artists")
|
||||
//query.Set("extend", "extendedAssetUrls")
|
||||
//query.Set("include[songs]", "artists")
|
||||
//query.Set("fields[artists]", "name,artwork")
|
||||
//query.Set("fields[albums:albums]", "artistName,artwork,name,releaseDate,url")
|
||||
//query.Set("fields[record-labels]", "name")
|
||||
//query.Set("extend", "editorialVideo")
|
||||
query.Set("l", language)
|
||||
req.URL.RawQuery = query.Encode()
|
||||
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(MusicVideoResp)
|
||||
err = json.NewDecoder(do.Body).Decode(&obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
type MusicVideoResp struct {
|
||||
Href string `json:"href"`
|
||||
Next string `json:"next"`
|
||||
Data []MusicVideoRespData `json:"data"`
|
||||
}
|
||||
|
||||
type MusicVideoRespData 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"`
|
||||
AlbumName string `json:"albumName"`
|
||||
ArtistName string `json:"artistName"`
|
||||
URL string `json:"url"`
|
||||
GenreNames []string `json:"genreNames"`
|
||||
DurationInMillis int `json:"durationInMillis"`
|
||||
Isrc string `json:"isrc"`
|
||||
TrackNumber int `json:"trackNumber"`
|
||||
DiscNumber int `json:"discNumber"`
|
||||
ContentRating string `json:"contentRating"`
|
||||
ReleaseDate string `json:"releaseDate"`
|
||||
Name string `json:"name"`
|
||||
Has4K bool `json:"has4K"`
|
||||
HasHDR bool `json:"hasHDR"`
|
||||
PlayParams struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
} `json:"playParams"`
|
||||
} `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"`
|
||||
Albums struct {
|
||||
Href string `json:"href"`
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Href string `json:"href"`
|
||||
Attributes struct {
|
||||
ArtistName string `json:"artistName"`
|
||||
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"`
|
||||
GenreNames []string `json:"genreNames"`
|
||||
IsCompilation bool `json:"isCompilation"`
|
||||
IsComplete bool `json:"isComplete"`
|
||||
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
||||
IsPrerelease bool `json:"isPrerelease"`
|
||||
IsSingle bool `json:"isSingle"`
|
||||
Name string `json:"name"`
|
||||
PlayParams struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
} `json:"playParams"`
|
||||
ReleaseDate string `json:"releaseDate"`
|
||||
TrackCount int `json:"trackCount"`
|
||||
Upc string `json:"upc"`
|
||||
URL string `json:"url"`
|
||||
} `json:"attributes"`
|
||||
} `json:"data"`
|
||||
} `json:"albums"`
|
||||
} `json:"relationships"`
|
||||
}
|
||||
165
utils/ampapi/playlist.go
Normal file
165
utils/ampapi/playlist.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package ampapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func GetPlaylistResp(storefront string, id string, language string, token string) (*PlaylistResp, error) {
|
||||
var err error
|
||||
if token == "" {
|
||||
token, err = GetToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/playlists/%s", storefront, id), 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")
|
||||
query := url.Values{}
|
||||
query.Set("omit[resource]", "autos")
|
||||
query.Set("include", "tracks,artists,record-labels")
|
||||
query.Set("include[songs]", "artists")
|
||||
//query.Set("fields[artists]", "name,artwork")
|
||||
//query.Set("fields[albums:albums]", "artistName,artwork,name,releaseDate,url")
|
||||
//query.Set("fields[record-labels]", "name")
|
||||
query.Set("extend", "editorialVideo,extendedAssetUrls")
|
||||
query.Set("l", language)
|
||||
req.URL.RawQuery = query.Encode()
|
||||
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(PlaylistResp)
|
||||
err = json.NewDecoder(do.Body).Decode(&obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(obj.Data[0].Relationships.Tracks.Next) > 0 {
|
||||
next := obj.Data[0].Relationships.Tracks.Next
|
||||
for {
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com%s", next), 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")
|
||||
query := req.URL.Query()
|
||||
query.Set("omit[resource]", "autos")
|
||||
query.Set("include", "artists")
|
||||
query.Set("extend", "editorialVideo,extendedAssetUrls")
|
||||
req.URL.RawQuery = query.Encode()
|
||||
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(TrackResp)
|
||||
err = json.NewDecoder(do.Body).Decode(&obj2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
obj.Data[0].Relationships.Tracks.Data = append(obj.Data[0].Relationships.Tracks.Data, obj2.Data...)
|
||||
next = obj2.Next
|
||||
if len(next) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
type PlaylistResp struct {
|
||||
Href string `json:"href"`
|
||||
Next string `json:"next"`
|
||||
Data []PlaylistRespData `json:"data"`
|
||||
}
|
||||
|
||||
type PlaylistRespData struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Href string `json:"href"`
|
||||
Attributes struct {
|
||||
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"`
|
||||
IsSingle bool `json:"isSingle"`
|
||||
URL string `json:"url"`
|
||||
IsComplete bool `json:"isComplete"`
|
||||
GenreNames []string `json:"genreNames"`
|
||||
TrackCount int `json:"trackCount"`
|
||||
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
||||
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
|
||||
ContentRating string `json:"contentRating"`
|
||||
ReleaseDate string `json:"releaseDate"`
|
||||
Name string `json:"name"`
|
||||
RecordLabel string `json:"recordLabel"`
|
||||
Upc string `json:"upc"`
|
||||
AudioTraits []string `json:"audioTraits"`
|
||||
Copyright string `json:"copyright"`
|
||||
PlayParams struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
} `json:"playParams"`
|
||||
IsCompilation bool `json:"isCompilation"`
|
||||
EditorialVideo struct {
|
||||
MotionTall struct {
|
||||
Video string `json:"video"`
|
||||
} `json:"motionTallVideo3x4"`
|
||||
MotionSquare struct {
|
||||
Video string `json:"video"`
|
||||
} `json:"motionSquareVideo1x1"`
|
||||
MotionDetailTall struct {
|
||||
Video string `json:"video"`
|
||||
} `json:"motionDetailTall"`
|
||||
MotionDetailSquare struct {
|
||||
Video string `json:"video"`
|
||||
} `json:"motionDetailSquare"`
|
||||
} `json:"editorialVideo"`
|
||||
} `json:"attributes"`
|
||||
Relationships struct {
|
||||
RecordLabels struct {
|
||||
Href string `json:"href"`
|
||||
Data []interface{} `json:"data"`
|
||||
} `json:"record-labels"`
|
||||
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"`
|
||||
Artwork struct {
|
||||
Url string `json:"url"`
|
||||
} `json:"artwork"`
|
||||
} `json:"attributes"`
|
||||
} `json:"data"`
|
||||
} `json:"artists"`
|
||||
Tracks TrackResp `json:"tracks"`
|
||||
} `json:"relationships"`
|
||||
}
|
||||
153
utils/ampapi/song.go
Normal file
153
utils/ampapi/song.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package ampapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func GetSongResp(storefront string, id string, language string, token string) (*SongResp, error) {
|
||||
var err error
|
||||
if token == "" {
|
||||
token, err = GetToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s", storefront, id), 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")
|
||||
query := url.Values{}
|
||||
//query.Set("omit[resource]", "autos")
|
||||
query.Set("include", "albums,artists")
|
||||
query.Set("extend", "extendedAssetUrls")
|
||||
//query.Set("include[songs]", "artists")
|
||||
//query.Set("fields[artists]", "name,artwork")
|
||||
//query.Set("fields[albums:albums]", "artistName,artwork,name,releaseDate,url")
|
||||
//query.Set("fields[record-labels]", "name")
|
||||
//query.Set("extend", "editorialVideo")
|
||||
query.Set("l", language)
|
||||
req.URL.RawQuery = query.Encode()
|
||||
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(SongResp)
|
||||
err = json.NewDecoder(do.Body).Decode(&obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
type SongResp struct {
|
||||
Href string `json:"href"`
|
||||
Next string `json:"next"`
|
||||
Data []SongRespData `json:"data"`
|
||||
}
|
||||
|
||||
type SongRespData 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"`
|
||||
ExtendedAssetUrls struct {
|
||||
EnhancedHls string `json:"enhancedHls"`
|
||||
} `json:"extendedAssetUrls"`
|
||||
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"`
|
||||
Albums struct {
|
||||
Href string `json:"href"`
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Href string `json:"href"`
|
||||
Attributes struct {
|
||||
ArtistName string `json:"artistName"`
|
||||
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"`
|
||||
GenreNames []string `json:"genreNames"`
|
||||
IsCompilation bool `json:"isCompilation"`
|
||||
IsComplete bool `json:"isComplete"`
|
||||
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
||||
IsPrerelease bool `json:"isPrerelease"`
|
||||
IsSingle bool `json:"isSingle"`
|
||||
Name string `json:"name"`
|
||||
PlayParams struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
} `json:"playParams"`
|
||||
ReleaseDate string `json:"releaseDate"`
|
||||
TrackCount int `json:"trackCount"`
|
||||
Upc string `json:"upc"`
|
||||
URL string `json:"url"`
|
||||
} `json:"attributes"`
|
||||
} `json:"data"`
|
||||
} `json:"albums"`
|
||||
} `json:"relationships"`
|
||||
}
|
||||
185
utils/ampapi/station.go
Normal file
185
utils/ampapi/station.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package ampapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func GetStationResp(storefront string, id string, language string, token string) (*StationResp, error) {
|
||||
var err error
|
||||
if token == "" {
|
||||
token, err = GetToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/stations/%s", storefront, id), 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")
|
||||
query := url.Values{}
|
||||
query.Set("omit[resource]", "autos")
|
||||
query.Set("extend", "editorialVideo")
|
||||
query.Set("l", language)
|
||||
req.URL.RawQuery = query.Encode()
|
||||
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(StationResp)
|
||||
err = json.NewDecoder(do.Body).Decode(&obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func GetStationAssetsUrl(id string, mutoken string, token string) (string, error) {
|
||||
var err error
|
||||
if token == "" {
|
||||
token, err = GetToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "https://amp-api.music.apple.com/v1/play/assets", nil)
|
||||
if err != nil {
|
||||
return "", 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")
|
||||
req.Header.Set("Media-User-Token", mutoken)
|
||||
query := url.Values{}
|
||||
//query.Set("omit[resource]", "autos")
|
||||
//query.Set("extend", "editorialVideo")
|
||||
query.Set("id", id)
|
||||
query.Set("kind", "radioStation")
|
||||
query.Set("keyFormat", "web")
|
||||
req.URL.RawQuery = query.Encode()
|
||||
do, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer do.Body.Close()
|
||||
if do.StatusCode != http.StatusOK {
|
||||
return "", errors.New(do.Status)
|
||||
}
|
||||
obj := new(StationAssets)
|
||||
err = json.NewDecoder(do.Body).Decode(&obj)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return obj.Results.Assets[0].Url, nil
|
||||
}
|
||||
|
||||
func GetStationNextTracks(id, mutoken, language, token string) (*TrackResp, error) {
|
||||
var err error
|
||||
if token == "" {
|
||||
token, err = GetToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("https://amp-api.music.apple.com/v1/me/stations/next-tracks/%s", id), 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")
|
||||
req.Header.Set("Media-User-Token", mutoken)
|
||||
query := url.Values{}
|
||||
query.Set("omit[resource]", "autos")
|
||||
//query.Set("include", "tracks,artists,record-labels")
|
||||
query.Set("include[songs]", "artists,albums")
|
||||
query.Set("limit", "10")
|
||||
query.Set("extend", "editorialVideo,extendedAssetUrls")
|
||||
query.Set("l", language)
|
||||
req.URL.RawQuery = query.Encode()
|
||||
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(TrackResp)
|
||||
err = json.NewDecoder(do.Body).Decode(&obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
type StationResp struct {
|
||||
Href string `json:"href"`
|
||||
Next string `json:"next"`
|
||||
Data []StationRespData `json:"data"`
|
||||
}
|
||||
|
||||
type StationAssets struct {
|
||||
Results struct {
|
||||
Assets []struct {
|
||||
KeyServerUrl string `json:"keyServerUrl"`
|
||||
Url string `json:"url"`
|
||||
WidevineKeyCertificateUrl string `json:"widevineKeyCertificateUrl"`
|
||||
FairPlayKeyCertificateUrl string `json:"fairPlayKeyCertificateUrl"`
|
||||
} `json:"assets"`
|
||||
} `json:"results"`
|
||||
}
|
||||
|
||||
type StationRespData struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Href string `json:"href"`
|
||||
Attributes struct {
|
||||
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"`
|
||||
IsLive bool `json:"isLive"`
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
EditorialVideo struct {
|
||||
MotionTall struct {
|
||||
Video string `json:"video"`
|
||||
} `json:"motionTallVideo3x4"`
|
||||
MotionSquare struct {
|
||||
Video string `json:"video"`
|
||||
} `json:"motionSquareVideo1x1"`
|
||||
MotionDetailTall struct {
|
||||
Video string `json:"video"`
|
||||
} `json:"motionDetailTall"`
|
||||
MotionDetailSquare struct {
|
||||
Video string `json:"video"`
|
||||
} `json:"motionDetailSquare"`
|
||||
} `json:"editorialVideo"`
|
||||
PlayParams struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Format string `json:"format"`
|
||||
StationHash string `json:"stationHash"`
|
||||
} `json:"playParams"`
|
||||
} `json:"attributes"`
|
||||
}
|
||||
49
utils/ampapi/token.go
Normal file
49
utils/ampapi/token.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package ampapi
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func GetToken() (string, error) {
|
||||
req, err := http.NewRequest("GET", "https://beta.music.apple.com", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
regex := regexp.MustCompile(`/assets/index-legacy-[^/]+\.js`)
|
||||
indexJsUri := regex.FindString(string(body))
|
||||
|
||||
req, err = http.NewRequest("GET", "https://beta.music.apple.com"+indexJsUri, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
regex = regexp.MustCompile(`eyJh([^"]*)`)
|
||||
token := regex.FindString(string(body))
|
||||
|
||||
return token, nil
|
||||
}
|
||||
103
utils/ampapi/track.go
Normal file
103
utils/ampapi/track.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package ampapi
|
||||
|
||||
type TrackResp struct {
|
||||
Href string `json:"href"`
|
||||
Next string `json:"next"`
|
||||
Data []TrackRespData `json:"data"`
|
||||
}
|
||||
|
||||
// 类型为song 或者 music-video
|
||||
type TrackRespData 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"`
|
||||
ExtendedAssetUrls struct {
|
||||
EnhancedHls string `json:"enhancedHls"`
|
||||
} `json:"extendedAssetUrls"`
|
||||
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"`
|
||||
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"`
|
||||
Albums struct {
|
||||
Href string `json:"href"`
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Href string `json:"href"`
|
||||
Attributes struct {
|
||||
ArtistName string `json:"artistName"`
|
||||
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"`
|
||||
GenreNames []string `json:"genreNames"`
|
||||
IsCompilation bool `json:"isCompilation"`
|
||||
IsComplete bool `json:"isComplete"`
|
||||
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
||||
IsPrerelease bool `json:"isPrerelease"`
|
||||
IsSingle bool `json:"isSingle"`
|
||||
Name string `json:"name"`
|
||||
PlayParams struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
} `json:"playParams"`
|
||||
ReleaseDate string `json:"releaseDate"`
|
||||
TrackCount int `json:"trackCount"`
|
||||
Upc string `json:"upc"`
|
||||
URL string `json:"url"`
|
||||
} `json:"attributes"`
|
||||
} `json:"data"`
|
||||
} `json:"albums"`
|
||||
} `json:"relationships"`
|
||||
}
|
||||
@@ -16,6 +16,7 @@ type SongLyrics struct {
|
||||
Type string `json:"type"`
|
||||
Attributes struct {
|
||||
Ttml string `json:"ttml"`
|
||||
TtmlLocalizations string `json:"ttmlLocalizations"`
|
||||
PlayParams struct {
|
||||
Id string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
@@ -49,7 +50,7 @@ func Get(storefront, songId, lrcType, language, lrcFormat, token, mediaUserToken
|
||||
}
|
||||
func getSongLyrics(songId string, storefront string, token string, userToken string, lrcType string, language string) (string, error) {
|
||||
req, err := http.NewRequest("GET",
|
||||
fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s/%s?l=%s", storefront, songId, lrcType, language), nil)
|
||||
fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s/%s?l=%s&extend=ttmlLocalizations", storefront, songId, lrcType, language), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -66,7 +67,10 @@ func getSongLyrics(songId string, storefront string, token string, userToken str
|
||||
obj := new(SongLyrics)
|
||||
_ = json.NewDecoder(do.Body).Decode(&obj)
|
||||
if obj.Data != nil {
|
||||
return obj.Data[0].Attributes.Ttml, nil
|
||||
if len(obj.Data[0].Attributes.Ttml) > 0 {
|
||||
return obj.Data[0].Attributes.Ttml, nil
|
||||
}
|
||||
return obj.Data[0].Attributes.TtmlLocalizations, nil
|
||||
} else {
|
||||
return "", errors.New("failed to get lyrics")
|
||||
}
|
||||
@@ -165,7 +169,7 @@ func conventSyllableTTMLToLRC(ttml string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
var lrcLines []string
|
||||
parseTime := func(timeValue string, newLine bool) (string, error) {
|
||||
parseTime := func(timeValue string, newLine int) (string, error) {
|
||||
var h, m, s, ms int
|
||||
if strings.Contains(timeValue, ":") {
|
||||
_, err = fmt.Sscanf(timeValue, "%d:%d:%d.%d", &h, &m, &s, &ms)
|
||||
@@ -182,34 +186,22 @@ func conventSyllableTTMLToLRC(ttml string) (string, error) {
|
||||
}
|
||||
m += h * 60
|
||||
ms = ms / 10
|
||||
if newLine {
|
||||
if newLine == 0 {
|
||||
return fmt.Sprintf("[%02d:%02d.%02d]<%02d:%02d.%02d>", m, s, ms, m, s, ms), nil
|
||||
} else if newLine == -1 {
|
||||
return fmt.Sprintf("[%02d:%02d.%02d]", m, s, ms), nil
|
||||
} else {
|
||||
return fmt.Sprintf("<%02d:%02d.%02d>", m, s, ms), nil
|
||||
}
|
||||
}
|
||||
divs := parsedTTML.FindElement("tt").FindElement("body").FindElements("div")
|
||||
//get trans
|
||||
if len(parsedTTML.FindElement("tt").FindElements("head")) > 0 {
|
||||
if len(parsedTTML.FindElement("tt").FindElement("head").FindElements("metadata")) > 0 {
|
||||
Metadata := parsedTTML.FindElement("tt").FindElement("head").FindElement("metadata")
|
||||
if len(Metadata.FindElements("iTunesMetadata")) > 0 {
|
||||
iTunesMetadata := Metadata.FindElement("iTunesMetadata")
|
||||
if len(iTunesMetadata.FindElements("translations")) > 0 {
|
||||
if len(iTunesMetadata.FindElement("translations").FindElements("translation")) > 0 {
|
||||
divs = iTunesMetadata.FindElement("translations").FindElements("translation")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, div := range divs {
|
||||
for _, item := range div.ChildElements() {
|
||||
for _, item := range div.ChildElements() { //LINES
|
||||
var lrcSyllables []string
|
||||
var i int = 0
|
||||
var endTime string
|
||||
for _, lyrics := range item.Child {
|
||||
if _, ok := lyrics.(*etree.CharData); ok {
|
||||
var endTime, transLine string
|
||||
for _, lyrics := range item.Child { //WORDS
|
||||
if _, ok := lyrics.(*etree.CharData); ok { //是否为span之间的空格
|
||||
if i > 0 {
|
||||
lrcSyllables = append(lrcSyllables, " ")
|
||||
continue
|
||||
@@ -220,11 +212,12 @@ func conventSyllableTTMLToLRC(ttml string) (string, error) {
|
||||
if lyric.SelectAttr("begin") == nil {
|
||||
continue
|
||||
}
|
||||
beginTime, err := parseTime(lyric.SelectAttr("begin").Value, i == 0)
|
||||
beginTime, err := parseTime(lyric.SelectAttr("begin").Value, i)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", err
|
||||
}
|
||||
endTime, err = parseTime(lyric.SelectAttr("end").Value, false)
|
||||
|
||||
endTime, err = parseTime(lyric.SelectAttr("end").Value, 1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -243,6 +236,39 @@ func conventSyllableTTMLToLRC(ttml string) (string, error) {
|
||||
text = lyric.SelectAttr("text").Value
|
||||
}
|
||||
lrcSyllables = append(lrcSyllables, fmt.Sprintf("%s%s", beginTime, text))
|
||||
if i == 0 {
|
||||
transBeginTime, _ := parseTime(lyric.SelectAttr("begin").Value, -1)
|
||||
if len(parsedTTML.FindElement("tt").FindElements("head")) > 0 {
|
||||
if len(parsedTTML.FindElement("tt").FindElement("head").FindElements("metadata")) > 0 {
|
||||
Metadata := parsedTTML.FindElement("tt").FindElement("head").FindElement("metadata")
|
||||
if len(Metadata.FindElements("iTunesMetadata")) > 0 {
|
||||
iTunesMetadata := Metadata.FindElement("iTunesMetadata")
|
||||
if len(iTunesMetadata.FindElements("translations")) > 0 {
|
||||
if len(iTunesMetadata.FindElement("translations").FindElements("translation")) > 0 {
|
||||
xpath := fmt.Sprintf("//text[@for='%s']", item.SelectAttr("itunes:key").Value)
|
||||
trans := iTunesMetadata.FindElement("translations").FindElement("translation").FindElement(xpath)
|
||||
var transTxt string
|
||||
if trans.SelectAttr("text") == nil {
|
||||
var textTmp []string
|
||||
for _, span := range trans.Child {
|
||||
if _, ok := span.(*etree.CharData); ok {
|
||||
textTmp = append(textTmp, span.(*etree.CharData).Data)
|
||||
} /*else {
|
||||
textTmp = append(textTmp, span.(*etree.Element).Text())
|
||||
}*/
|
||||
}
|
||||
transTxt = strings.Join(textTmp, "")
|
||||
} else {
|
||||
transTxt = trans.SelectAttr("text").Value
|
||||
}
|
||||
//fmt.Println(transTxt)
|
||||
transLine = transBeginTime + transTxt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
//endTime, err := parseTime(item.SelectAttr("end").Value)
|
||||
@@ -250,6 +276,9 @@ func conventSyllableTTMLToLRC(ttml string) (string, error) {
|
||||
// return "", err
|
||||
//}
|
||||
lrcLines = append(lrcLines, strings.Join(lrcSyllables, "")+endTime)
|
||||
if len(transLine) > 0 {
|
||||
lrcLines = append(lrcLines, transLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(lrcLines, "\n"), nil
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gospider007/requests"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
@@ -24,6 +25,8 @@ import (
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
//"time"
|
||||
|
||||
"github.com/grafov/m3u8"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
@@ -154,7 +157,7 @@ func GetWebplayback(adamId string, authtoken string, mutoken string, mvmode bool
|
||||
return obj.List[0].HlsPlaylistUrl, "", nil
|
||||
}
|
||||
// 遍历 Assets
|
||||
for i, _ := range obj.List[0].Assets {
|
||||
for i := range obj.List[0].Assets {
|
||||
if obj.List[0].Assets[i].Flavor == "28:ctrp256" {
|
||||
kidBase64, fileurl, err := extractKidBase64(obj.List[0].Assets[i].URL, false)
|
||||
if err != nil {
|
||||
@@ -165,7 +168,7 @@ func GetWebplayback(adamId string, authtoken string, mutoken string, mvmode bool
|
||||
continue
|
||||
}
|
||||
}
|
||||
return "", "", nil
|
||||
return "", "", errors.New("Unavailable")
|
||||
}
|
||||
|
||||
type Songlist struct {
|
||||
@@ -298,10 +301,19 @@ func Run(adamId string, trackpath string, authtoken string, mutoken string, mvmo
|
||||
AfterRequest: AfterRequest,
|
||||
}
|
||||
key.CdmInit()
|
||||
keystr, keybt, err := key.GetKey(ctx, "https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/acquireWebPlaybackLicense", pssh, nil)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return "", err
|
||||
var keybt []byte
|
||||
if strings.Contains(adamId, "ra.") {
|
||||
keystr, keybt, err = key.GetKey(ctx, "https://play.itunes.apple.com/WebObjects/MZPlay.woa/web/radio/versions/1/license", pssh, nil)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
keystr, keybt, err = key.GetKey(ctx, "https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/acquireWebPlaybackLicense", pssh, nil)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if mvmode {
|
||||
keyAndUrls := "1:" + keystr + ";" + fileurl
|
||||
@@ -335,6 +347,95 @@ func Run(adamId string, trackpath string, authtoken string, mutoken string, mvmo
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Segment 结构体用于在 Channel 中传递分段数据
|
||||
type Segment struct {
|
||||
Index int
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func downloadSegment(url string, index int, wg *sync.WaitGroup, segmentsChan chan<- Segment, client *http.Client, limiter chan struct{}) {
|
||||
// 函数退出时,从 limiter 中接收一个值,释放一个并发槽位
|
||||
defer func() {
|
||||
<-limiter
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("错误(分段 %d): 创建请求失败: %v\n", index, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("错误(分段 %d): 下载失败: %v\n", index, err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("错误(分段 %d): 服务器返回状态码 %d\n", index, resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Printf("错误(分段 %d): 读取数据失败: %v\n", index, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 将下载好的分段(包含序号和数据)发送到 Channel
|
||||
segmentsChan <- Segment{Index: index, Data: data}
|
||||
}
|
||||
|
||||
// fileWriter 从 Channel 接收分段并按顺序写入文件
|
||||
func fileWriter(wg *sync.WaitGroup, segmentsChan <-chan Segment, outputFile io.Writer, totalSegments int) {
|
||||
defer wg.Done()
|
||||
|
||||
// 缓冲区,用于存放乱序到达的分段
|
||||
// key 是分段序号,value 是分段数据
|
||||
segmentBuffer := make(map[int][]byte)
|
||||
nextIndex := 0 // 期望写入的下一个分段的序号
|
||||
|
||||
for segment := range segmentsChan {
|
||||
// 检查收到的分段是否是当前期望的
|
||||
if segment.Index == nextIndex {
|
||||
//fmt.Printf("写入分段 %d\n", segment.Index)
|
||||
_, err := outputFile.Write(segment.Data)
|
||||
if err != nil {
|
||||
fmt.Printf("错误(分段 %d): 写入文件失败: %v\n", segment.Index, err)
|
||||
}
|
||||
nextIndex++
|
||||
|
||||
// 检查缓冲区中是否有下一个连续的分段
|
||||
for {
|
||||
data, ok := segmentBuffer[nextIndex]
|
||||
if !ok {
|
||||
break // 缓冲区里没有下一个,跳出循环,等待下一个分段到达
|
||||
}
|
||||
|
||||
//fmt.Printf("从缓冲区写入分段 %d\n", nextIndex)
|
||||
_, err := outputFile.Write(data)
|
||||
if err != nil {
|
||||
fmt.Printf("错误(分段 %d): 从缓冲区写入文件失败: %v\n", nextIndex, err)
|
||||
}
|
||||
// 从缓冲区删除已写入的分段,释放内存
|
||||
delete(segmentBuffer, nextIndex)
|
||||
nextIndex++
|
||||
}
|
||||
} else {
|
||||
// 如果不是期望的分段,先存入缓冲区
|
||||
//fmt.Printf("缓冲分段 %d (等待 %d)\n", segment.Index, nextIndex)
|
||||
segmentBuffer[segment.Index] = segment.Data
|
||||
}
|
||||
}
|
||||
|
||||
// 确保所有分段都已写入
|
||||
if nextIndex != totalSegments {
|
||||
fmt.Printf("警告: 写入完成,但似乎有分段丢失。期望 %d 个, 实际写入 %d 个。\n", totalSegments, nextIndex)
|
||||
}
|
||||
}
|
||||
|
||||
func ExtMvData(keyAndUrls string, savePath string) error {
|
||||
segments := strings.Split(keyAndUrls, ";")
|
||||
key := segments[0]
|
||||
@@ -345,36 +446,51 @@ func ExtMvData(keyAndUrls string, savePath string) error {
|
||||
fmt.Printf("创建文件失败:%v\n", err)
|
||||
return err
|
||||
}
|
||||
defer tempFile.Close()
|
||||
defer os.Remove(tempFile.Name())
|
||||
defer tempFile.Close()
|
||||
|
||||
// 依次下载每个链接并写入文件
|
||||
bar := progressbar.DefaultBytes(
|
||||
-1,
|
||||
"Downloading...",
|
||||
)
|
||||
var downloadWg, writerWg sync.WaitGroup
|
||||
segmentsChan := make(chan Segment, len(urls))
|
||||
// --- 新增代码: 定义最大并发数 ---
|
||||
const maxConcurrency = 10
|
||||
// --- 新增代码: 创建带缓冲的 Channel 作为信号量 ---
|
||||
limiter := make(chan struct{}, maxConcurrency)
|
||||
client := &http.Client{}
|
||||
|
||||
// 初始化进度条
|
||||
bar := progressbar.DefaultBytes(-1, "Downloading...")
|
||||
barWriter := io.MultiWriter(tempFile, bar)
|
||||
for _, url := range urls {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
fmt.Printf("下载链接 %s 失败:%v\n", url, 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)
|
||||
defer resp.Body.Close() // 注意及时关闭响应体,避免资源泄露
|
||||
if err != nil {
|
||||
fmt.Printf("写入文件失败:%v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
//fmt.Printf("第 %d 个链接 %s 下载并写入完成\n", idx+1, url)
|
||||
// 启动写入 Goroutine
|
||||
writerWg.Add(1)
|
||||
go fileWriter(&writerWg, segmentsChan, barWriter, len(urls))
|
||||
|
||||
// 启动下载 Goroutines
|
||||
for i, url := range urls {
|
||||
// 在启动 Goroutine 前,向 limiter 发送一个值来“获取”一个槽位
|
||||
// 如果 limiter 已满 (达到10个),这里会阻塞,直到有其他任务完成并释放槽位
|
||||
//fmt.Printf("请求启动任务 %d...\n", i)
|
||||
limiter <- struct{}{}
|
||||
//fmt.Printf("...任务 %d 已启动\n", i)
|
||||
|
||||
downloadWg.Add(1)
|
||||
// 将 limiter 传递给下载函数
|
||||
go downloadSegment(url, i, &downloadWg, segmentsChan, client, limiter)
|
||||
}
|
||||
|
||||
// 等待所有下载任务完成
|
||||
downloadWg.Wait()
|
||||
// 下载完成后,关闭 Channel。写入 Goroutine 会在处理完 Channel 中所有数据后退出。
|
||||
close(segmentsChan)
|
||||
|
||||
// 等待写入 Goroutine 完成所有写入和缓冲处理
|
||||
writerWg.Wait()
|
||||
|
||||
// 显式关闭文件(defer会再次调用,但重复关闭是安全的)
|
||||
if err := tempFile.Close(); err != nil {
|
||||
fmt.Printf("关闭临时文件失败: %v\n", err)
|
||||
return err
|
||||
}
|
||||
tempFile.Close()
|
||||
fmt.Println("\nDownloaded.")
|
||||
|
||||
cmd1 := exec.Command("mp4decrypt", "--key", key, tempFile.Name(), filepath.Base(savePath))
|
||||
|
||||
@@ -16,6 +16,7 @@ type ConfigSet struct {
|
||||
CoverFormat string `yaml:"cover-format"`
|
||||
AlacSaveFolder string `yaml:"alac-save-folder"`
|
||||
AtmosSaveFolder string `yaml:"atmos-save-folder"`
|
||||
AacSaveFolder string `yaml:"aac-save-folder"`
|
||||
AlbumFolderFormat string `yaml:"album-folder-format"`
|
||||
PlaylistFolderFormat string `yaml:"playlist-folder-format"`
|
||||
ArtistFolderFormat string `yaml:"artist-folder-format"`
|
||||
@@ -46,380 +47,7 @@ type Counter struct {
|
||||
Total int
|
||||
}
|
||||
|
||||
type ApiResult struct {
|
||||
Data []SongData `json:"data"`
|
||||
}
|
||||
|
||||
type SongAttributes struct {
|
||||
ArtistName string `json:"artistName"`
|
||||
DiscNumber int `json:"discNumber"`
|
||||
GenreNames []string `json:"genreNames"`
|
||||
ExtendedAssetUrls struct {
|
||||
EnhancedHls string `json:"enhancedHls"`
|
||||
} `json:"extendedAssetUrls"`
|
||||
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
||||
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
|
||||
ContentRating string `json:"contentRating"`
|
||||
ReleaseDate string `json:"releaseDate"`
|
||||
Name string `json:"name"`
|
||||
Isrc string `json:"isrc"`
|
||||
AlbumName string `json:"albumName"`
|
||||
TrackNumber int `json:"trackNumber"`
|
||||
ComposerName string `json:"composerName"`
|
||||
}
|
||||
|
||||
type AlbumAttributes struct {
|
||||
ArtistName string `json:"artistName"`
|
||||
IsSingle bool `json:"isSingle"`
|
||||
IsComplete bool `json:"isComplete"`
|
||||
GenreNames []string `json:"genreNames"`
|
||||
TrackCount int `json:"trackCount"`
|
||||
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
||||
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
|
||||
ContentRating string `json:"contentRating"`
|
||||
ReleaseDate string `json:"releaseDate"`
|
||||
Name string `json:"name"`
|
||||
RecordLabel string `json:"recordLabel"`
|
||||
Upc string `json:"upc"`
|
||||
Copyright string `json:"copyright"`
|
||||
IsCompilation bool `json:"isCompilation"`
|
||||
}
|
||||
|
||||
type SongData struct {
|
||||
ID string `json:"id"`
|
||||
Attributes SongAttributes `json:"attributes"`
|
||||
Relationships struct {
|
||||
Albums struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Href string `json:"href"`
|
||||
Attributes AlbumAttributes `json:"attributes"`
|
||||
} `json:"data"`
|
||||
} `json:"albums"`
|
||||
Artists struct {
|
||||
Href string `json:"href"`
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Href string `json:"href"`
|
||||
} `json:"data"`
|
||||
} `json:"artists"`
|
||||
} `json:"relationships"`
|
||||
}
|
||||
|
||||
type SongResult struct {
|
||||
Artwork struct {
|
||||
Width int `json:"width"`
|
||||
URL string `json:"url"`
|
||||
Height int `json:"height"`
|
||||
TextColor3 string `json:"textColor3"`
|
||||
TextColor2 string `json:"textColor2"`
|
||||
TextColor4 string `json:"textColor4"`
|
||||
HasAlpha bool `json:"hasAlpha"`
|
||||
TextColor1 string `json:"textColor1"`
|
||||
BgColor string `json:"bgColor"`
|
||||
HasP3 bool `json:"hasP3"`
|
||||
SupportsLayeredImage bool `json:"supportsLayeredImage"`
|
||||
} `json:"artwork"`
|
||||
ArtistName string `json:"artistName"`
|
||||
CollectionID string `json:"collectionId"`
|
||||
DiscNumber int `json:"discNumber"`
|
||||
GenreNames []string `json:"genreNames"`
|
||||
ID string `json:"id"`
|
||||
DurationInMillis int `json:"durationInMillis"`
|
||||
ReleaseDate string `json:"releaseDate"`
|
||||
ContentRatingsBySystem struct {
|
||||
} `json:"contentRatingsBySystem"`
|
||||
Name string `json:"name"`
|
||||
Composer struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
} `json:"composer"`
|
||||
EditorialArtwork struct {
|
||||
} `json:"editorialArtwork"`
|
||||
CollectionName string `json:"collectionName"`
|
||||
AssetUrls struct {
|
||||
Plus string `json:"plus"`
|
||||
Lightweight string `json:"lightweight"`
|
||||
SuperLightweight string `json:"superLightweight"`
|
||||
LightweightPlus string `json:"lightweightPlus"`
|
||||
EnhancedHls string `json:"enhancedHls"`
|
||||
} `json:"assetUrls"`
|
||||
AudioTraits []string `json:"audioTraits"`
|
||||
Kind string `json:"kind"`
|
||||
Copyright string `json:"copyright"`
|
||||
ArtistID string `json:"artistId"`
|
||||
Genres []struct {
|
||||
GenreID string `json:"genreId"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
MediaType string `json:"mediaType"`
|
||||
} `json:"genres"`
|
||||
TrackNumber int `json:"trackNumber"`
|
||||
AudioLocale string `json:"audioLocale"`
|
||||
Offers []struct {
|
||||
ActionText struct {
|
||||
Short string `json:"short"`
|
||||
Medium string `json:"medium"`
|
||||
Long string `json:"long"`
|
||||
Downloaded string `json:"downloaded"`
|
||||
Downloading string `json:"downloading"`
|
||||
} `json:"actionText"`
|
||||
Type string `json:"type"`
|
||||
PriceFormatted string `json:"priceFormatted"`
|
||||
Price float64 `json:"price"`
|
||||
BuyParams string `json:"buyParams"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
Assets []struct {
|
||||
Flavor string `json:"flavor"`
|
||||
Preview struct {
|
||||
Duration int `json:"duration"`
|
||||
URL string `json:"url"`
|
||||
} `json:"preview"`
|
||||
Size int `json:"size"`
|
||||
Duration int `json:"duration"`
|
||||
} `json:"assets"`
|
||||
} `json:"offers"`
|
||||
}
|
||||
|
||||
type TrackData 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"`
|
||||
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"`
|
||||
Albums struct {
|
||||
Href string `json:"href"`
|
||||
Data []AlbumData `json:"data"`
|
||||
}
|
||||
} `json:"relationships"`
|
||||
}
|
||||
|
||||
type AlbumData struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Href string `json:"href"`
|
||||
Attributes struct {
|
||||
ArtistName string `json:"artistName"`
|
||||
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"`
|
||||
GenreNames []string `json:"genreNames"`
|
||||
IsCompilation bool `json:"isCompilation"`
|
||||
IsComplete bool `json:"isComplete"`
|
||||
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
||||
IsPrerelease bool `json:"isPrerelease"`
|
||||
IsSingle bool `json:"isSingle"`
|
||||
Name string `json:"name"`
|
||||
PlayParams struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
} `json:"playParams"`
|
||||
ReleaseDate string `json:"releaseDate"`
|
||||
TrackCount int `json:"trackCount"`
|
||||
Upc string `json:"upc"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
}
|
||||
|
||||
type AutoGenerated struct {
|
||||
Data []struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Href string `json:"href"`
|
||||
Attributes struct {
|
||||
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"`
|
||||
IsSingle bool `json:"isSingle"`
|
||||
URL string `json:"url"`
|
||||
IsComplete bool `json:"isComplete"`
|
||||
GenreNames []string `json:"genreNames"`
|
||||
TrackCount int `json:"trackCount"`
|
||||
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
||||
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
|
||||
ContentRating string `json:"contentRating"`
|
||||
ReleaseDate string `json:"releaseDate"`
|
||||
Name string `json:"name"`
|
||||
RecordLabel string `json:"recordLabel"`
|
||||
Upc string `json:"upc"`
|
||||
AudioTraits []string `json:"audioTraits"`
|
||||
Copyright string `json:"copyright"`
|
||||
PlayParams struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
} `json:"playParams"`
|
||||
IsCompilation bool `json:"isCompilation"`
|
||||
EditorialVideo struct {
|
||||
MotionTall struct {
|
||||
Video string `json:"video"`
|
||||
} `json:"motionTallVideo3x4"`
|
||||
MotionSquare struct {
|
||||
Video string `json:"video"`
|
||||
} `json:"motionSquareVideo1x1"`
|
||||
MotionDetailTall struct {
|
||||
Video string `json:"video"`
|
||||
} `json:"motionDetailTall"`
|
||||
MotionDetailSquare struct {
|
||||
Video string `json:"video"`
|
||||
} `json:"motionDetailSquare"`
|
||||
} `json:"editorialVideo"`
|
||||
} `json:"attributes"`
|
||||
Relationships struct {
|
||||
RecordLabels struct {
|
||||
Href string `json:"href"`
|
||||
Data []interface{} `json:"data"`
|
||||
} `json:"record-labels"`
|
||||
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"`
|
||||
Artwork struct {
|
||||
Url string `json:"url"`
|
||||
} `json:"artwork"`
|
||||
} `json:"attributes"`
|
||||
} `json:"data"`
|
||||
} `json:"artists"`
|
||||
Tracks struct {
|
||||
Href string `json:"href"`
|
||||
Next string `json:"next"`
|
||||
Data []TrackData `json:"data"`
|
||||
} `json:"tracks"`
|
||||
} `json:"relationships"`
|
||||
} `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"`
|
||||
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"`
|
||||
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"`
|
||||
Albums struct {
|
||||
Href string `json:"href"`
|
||||
Data []AlbumData `json:"data"`
|
||||
}
|
||||
} `json:"relationships"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// 艺术家页面
|
||||
type AutoGeneratedArtist struct {
|
||||
Next string `json:"next"`
|
||||
Data []struct {
|
||||
@@ -465,59 +93,3 @@ type AutoGeneratedArtist struct {
|
||||
} `json:"attributes"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type AutoGeneratedMusicVideo struct {
|
||||
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"`
|
||||
AlbumName string `json:"albumName"`
|
||||
ArtistName string `json:"artistName"`
|
||||
URL string `json:"url"`
|
||||
GenreNames []string `json:"genreNames"`
|
||||
DurationInMillis int `json:"durationInMillis"`
|
||||
Isrc string `json:"isrc"`
|
||||
TrackNumber int `json:"trackNumber"`
|
||||
DiscNumber int `json:"discNumber"`
|
||||
ContentRating string `json:"contentRating"`
|
||||
ReleaseDate string `json:"releaseDate"`
|
||||
Name string `json:"name"`
|
||||
Has4K bool `json:"has4K"`
|
||||
HasHDR bool `json:"hasHDR"`
|
||||
PlayParams struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
} `json:"playParams"`
|
||||
} `json:"attributes"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type SongLyrics struct {
|
||||
Data []struct {
|
||||
Id string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Attributes struct {
|
||||
Ttml string `json:"ttml"`
|
||||
PlayParams struct {
|
||||
Id string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
CatalogId string `json:"catalogId"`
|
||||
DisplayType int `json:"displayType"`
|
||||
} `json:"playParams"`
|
||||
} `json:"attributes"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
193
utils/task/album.go
Normal file
193
utils/task/album.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
|
||||
"main/utils/ampapi"
|
||||
)
|
||||
|
||||
type Album struct {
|
||||
Storefront string
|
||||
ID string
|
||||
|
||||
SaveDir string
|
||||
SaveName string
|
||||
Codec string
|
||||
CoverPath string
|
||||
|
||||
Language string
|
||||
Resp ampapi.AlbumResp
|
||||
Name string
|
||||
Tracks []Track
|
||||
}
|
||||
|
||||
func NewAlbum(st string, id string) *Album {
|
||||
a := new(Album)
|
||||
a.Storefront = st
|
||||
a.ID = id
|
||||
|
||||
//fmt.Println("Album created")
|
||||
return a
|
||||
|
||||
}
|
||||
|
||||
func (a *Album) GetResp(token, l string) error {
|
||||
var err error
|
||||
a.Language = l
|
||||
resp, err := ampapi.GetAlbumResp(a.Storefront, a.ID, a.Language, token)
|
||||
if err != nil {
|
||||
return errors.New("error getting album response")
|
||||
}
|
||||
a.Resp = *resp
|
||||
//简化高频调用名称
|
||||
a.Name = a.Resp.Data[0].Attributes.Name
|
||||
//fmt.Println("Getting album response")
|
||||
//从resp中的Tracks数据中提取trackData信息到新的Track结构体中
|
||||
for i, trackData := range a.Resp.Data[0].Relationships.Tracks.Data {
|
||||
len := len(a.Resp.Data[0].Relationships.Tracks.Data)
|
||||
a.Tracks = append(a.Tracks, Track{
|
||||
ID: trackData.ID,
|
||||
Type: trackData.Type,
|
||||
Name: trackData.Attributes.Name,
|
||||
Language: a.Language,
|
||||
Storefront: a.Storefront,
|
||||
|
||||
//SaveDir: filepath.Join(a.SaveDir, a.SaveName),
|
||||
//Codec: a.Codec,
|
||||
TaskNum: i + 1,
|
||||
TaskTotal: len,
|
||||
M3u8: trackData.Attributes.ExtendedAssetUrls.EnhancedHls,
|
||||
WebM3u8: trackData.Attributes.ExtendedAssetUrls.EnhancedHls,
|
||||
//CoverPath: a.CoverPath,
|
||||
|
||||
Resp: trackData,
|
||||
PreType: "albums",
|
||||
DiscTotal: a.Resp.Data[0].Relationships.Tracks.Data[len-1].Attributes.DiscNumber,
|
||||
PreID: a.ID,
|
||||
AlbumData: a.Resp.Data[0],
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Album) GetArtwork() string {
|
||||
return a.Resp.Data[0].Attributes.Artwork.URL
|
||||
}
|
||||
|
||||
func (a *Album) ShowSelect() []int {
|
||||
meta := a.Resp
|
||||
trackTotal := len(meta.Data[0].Relationships.Tracks.Data)
|
||||
arr := make([]int, trackTotal)
|
||||
for i := 0; i < trackTotal; i++ {
|
||||
arr[i] = i + 1
|
||||
}
|
||||
selected := []int{}
|
||||
var data [][]string
|
||||
for trackNum, track := range meta.Data[0].Relationships.Tracks.Data {
|
||||
trackNum++
|
||||
trackName := fmt.Sprintf("%02d. %s", track.Attributes.TrackNumber, track.Attributes.Name)
|
||||
data = append(data, []string{fmt.Sprint(trackNum),
|
||||
trackName,
|
||||
track.Attributes.ContentRating,
|
||||
track.Type})
|
||||
|
||||
}
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"", "Track Name", "Rating", "Type"})
|
||||
//table.SetFooter([]string{"", "", "Footer", "Footer4"})
|
||||
table.SetRowLine(false)
|
||||
//table.SetAutoMergeCells(true)
|
||||
table.SetCaption(true, fmt.Sprintf("Storefront: %s, %d tracks missing", strings.ToUpper(a.Storefront), meta.Data[0].Attributes.TrackCount-trackTotal))
|
||||
table.SetHeaderColor(tablewriter.Colors{},
|
||||
tablewriter.Colors{tablewriter.FgRedColor, tablewriter.Bold},
|
||||
tablewriter.Colors{tablewriter.FgBlackColor, tablewriter.Bold},
|
||||
tablewriter.Colors{tablewriter.FgBlackColor, tablewriter.Bold})
|
||||
|
||||
table.SetColumnColor(tablewriter.Colors{tablewriter.FgCyanColor},
|
||||
tablewriter.Colors{tablewriter.Bold, tablewriter.FgRedColor},
|
||||
tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor},
|
||||
tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor})
|
||||
for _, row := range data {
|
||||
if row[2] == "explicit" {
|
||||
row[2] = "E"
|
||||
} else if row[2] == "clean" {
|
||||
row[2] = "C"
|
||||
} else {
|
||||
row[2] = "None"
|
||||
}
|
||||
if row[3] == "music-videos" {
|
||||
row[3] = "MV"
|
||||
} else if row[3] == "songs" {
|
||||
row[3] = "SONG"
|
||||
}
|
||||
table.Append(row)
|
||||
}
|
||||
//table.AppendBulk(data)
|
||||
table.Render()
|
||||
fmt.Println("Please select from the track options above (multiple options separated by commas, ranges supported, or type 'all' to select all)")
|
||||
cyanColor := color.New(color.FgCyan)
|
||||
cyanColor.Print("select: ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
input, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "all" {
|
||||
fmt.Println("You have selected all options:")
|
||||
selected = arr
|
||||
} else {
|
||||
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})
|
||||
}
|
||||
}
|
||||
//
|
||||
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(arr) {
|
||||
selected = append(selected, num)
|
||||
//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(arr) || start > end {
|
||||
fmt.Println("Range out of range:", opt)
|
||||
continue
|
||||
}
|
||||
for i := start; i <= end; i++ {
|
||||
//fmt.Println(options[i-1])
|
||||
selected = append(selected, i)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Invalid option:", opt)
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected
|
||||
}
|
||||
195
utils/task/playlist.go
Normal file
195
utils/task/playlist.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
|
||||
"main/utils/ampapi"
|
||||
)
|
||||
|
||||
type Playlist struct {
|
||||
Storefront string
|
||||
ID string
|
||||
|
||||
SaveDir string
|
||||
SaveName string
|
||||
Codec string
|
||||
CoverPath string
|
||||
|
||||
Language string
|
||||
Resp ampapi.PlaylistResp
|
||||
Name string
|
||||
Tracks []Track
|
||||
}
|
||||
|
||||
func NewPlaylist(st string, id string) *Playlist {
|
||||
a := new(Playlist)
|
||||
a.Storefront = st
|
||||
a.ID = id
|
||||
|
||||
//fmt.Println("Album created")
|
||||
return a
|
||||
|
||||
}
|
||||
|
||||
func (a *Playlist) GetResp(token, l string) error {
|
||||
var err error
|
||||
a.Language = l
|
||||
resp, err := ampapi.GetPlaylistResp(a.Storefront, a.ID, a.Language, token)
|
||||
if err != nil {
|
||||
return errors.New("error getting album response")
|
||||
}
|
||||
a.Resp = *resp
|
||||
|
||||
a.Resp.Data[0].Attributes.ArtistName = "Apple Music"
|
||||
//简化高频调用名称
|
||||
a.Name = a.Resp.Data[0].Attributes.Name
|
||||
//fmt.Println("Getting album response")
|
||||
//从resp中的Tracks数据中提取trackData信息到新的Track结构体中
|
||||
for i, trackData := range a.Resp.Data[0].Relationships.Tracks.Data {
|
||||
len := len(a.Resp.Data[0].Relationships.Tracks.Data)
|
||||
a.Tracks = append(a.Tracks, Track{
|
||||
ID: trackData.ID,
|
||||
Type: trackData.Type,
|
||||
Name: trackData.Attributes.Name,
|
||||
Language: a.Language,
|
||||
Storefront: a.Storefront,
|
||||
|
||||
//SaveDir: filepath.Join(a.SaveDir, a.SaveName),
|
||||
//Codec: a.Codec,
|
||||
TaskNum: i + 1,
|
||||
TaskTotal: len,
|
||||
M3u8: trackData.Attributes.ExtendedAssetUrls.EnhancedHls,
|
||||
WebM3u8: trackData.Attributes.ExtendedAssetUrls.EnhancedHls,
|
||||
//CoverPath: a.CoverPath,
|
||||
|
||||
Resp: trackData,
|
||||
PreType: "playlists",
|
||||
//DiscTotal: a.Resp.Data[0].Relationships.Tracks.Data[len-1].Attributes.DiscNumber, 在它处获取
|
||||
PreID: a.ID,
|
||||
PlaylistData: a.Resp.Data[0],
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Playlist) GetArtwork() string {
|
||||
return a.Resp.Data[0].Attributes.Artwork.URL
|
||||
}
|
||||
|
||||
func (a *Playlist) ShowSelect() []int {
|
||||
meta := a.Resp
|
||||
trackTotal := len(meta.Data[0].Relationships.Tracks.Data)
|
||||
arr := make([]int, trackTotal)
|
||||
for i := 0; i < trackTotal; i++ {
|
||||
arr[i] = i + 1
|
||||
}
|
||||
selected := []int{}
|
||||
var data [][]string
|
||||
for trackNum, track := range meta.Data[0].Relationships.Tracks.Data {
|
||||
trackNum++
|
||||
trackName := fmt.Sprintf("%s - %s", track.Attributes.Name, track.Attributes.ArtistName)
|
||||
data = append(data, []string{fmt.Sprint(trackNum),
|
||||
trackName,
|
||||
track.Attributes.ContentRating,
|
||||
track.Type})
|
||||
|
||||
}
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"", "Track Name", "Rating", "Type"})
|
||||
//table.SetFooter([]string{"", "", "Footer", "Footer4"})
|
||||
table.SetRowLine(false)
|
||||
//table.SetAutoMergeCells(true)
|
||||
table.SetCaption(true, fmt.Sprintf("Playlists: %d tracks", trackTotal))
|
||||
table.SetHeaderColor(tablewriter.Colors{},
|
||||
tablewriter.Colors{tablewriter.FgRedColor, tablewriter.Bold},
|
||||
tablewriter.Colors{tablewriter.FgBlackColor, tablewriter.Bold},
|
||||
tablewriter.Colors{tablewriter.FgBlackColor, tablewriter.Bold})
|
||||
|
||||
table.SetColumnColor(tablewriter.Colors{tablewriter.FgCyanColor},
|
||||
tablewriter.Colors{tablewriter.Bold, tablewriter.FgRedColor},
|
||||
tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor},
|
||||
tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor})
|
||||
for _, row := range data {
|
||||
if row[2] == "explicit" {
|
||||
row[2] = "E"
|
||||
} else if row[2] == "clean" {
|
||||
row[2] = "C"
|
||||
} else {
|
||||
row[2] = "None"
|
||||
}
|
||||
if row[3] == "music-videos" {
|
||||
row[3] = "MV"
|
||||
} else if row[3] == "songs" {
|
||||
row[3] = "SONG"
|
||||
}
|
||||
table.Append(row)
|
||||
}
|
||||
//table.AppendBulk(data)
|
||||
table.Render()
|
||||
fmt.Println("Please select from the track options above (multiple options separated by commas, ranges supported, or type 'all' to select all)")
|
||||
cyanColor := color.New(color.FgCyan)
|
||||
cyanColor.Print("select: ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
input, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "all" {
|
||||
fmt.Println("You have selected all options:")
|
||||
selected = arr
|
||||
} else {
|
||||
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})
|
||||
}
|
||||
}
|
||||
//
|
||||
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(arr) {
|
||||
selected = append(selected, num)
|
||||
//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(arr) || start > end {
|
||||
fmt.Println("Range out of range:", opt)
|
||||
continue
|
||||
}
|
||||
for i := start; i <= end; i++ {
|
||||
//fmt.Println(options[i-1])
|
||||
selected = append(selected, i)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Invalid option:", opt)
|
||||
}
|
||||
}
|
||||
}
|
||||
return selected
|
||||
}
|
||||
209
utils/task/station.go
Normal file
209
utils/task/station.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
//"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
//"os"
|
||||
//"strconv"
|
||||
//"strings"
|
||||
|
||||
//"github.com/fatih/color"
|
||||
//"github.com/olekukonko/tablewriter"
|
||||
|
||||
"main/utils/ampapi"
|
||||
)
|
||||
|
||||
type Station struct {
|
||||
Storefront string
|
||||
ID string
|
||||
|
||||
SaveDir string
|
||||
SaveName string
|
||||
Codec string
|
||||
CoverPath string
|
||||
|
||||
Language string
|
||||
Resp ampapi.StationResp
|
||||
Type string
|
||||
Name string
|
||||
Tracks []Track
|
||||
}
|
||||
|
||||
func NewStation(st string, id string) *Station {
|
||||
a := new(Station)
|
||||
a.Storefront = st
|
||||
a.ID = id
|
||||
//fmt.Println("Album created")
|
||||
return a
|
||||
|
||||
}
|
||||
|
||||
func (a *Station) GetResp(mutoken, token, l string) error {
|
||||
var err error
|
||||
a.Language = l
|
||||
resp, err := ampapi.GetStationResp(a.Storefront, a.ID, a.Language, token)
|
||||
if err != nil {
|
||||
return errors.New("error getting station response")
|
||||
}
|
||||
a.Resp = *resp
|
||||
//简化高频调用名称
|
||||
a.Type = a.Resp.Data[0].Attributes.PlayParams.Format
|
||||
a.Name = a.Resp.Data[0].Attributes.Name
|
||||
if a.Type != "tracks" {
|
||||
return nil
|
||||
}
|
||||
tracksResp, err := ampapi.GetStationNextTracks(a.ID, mutoken, a.Language, token)
|
||||
if err != nil {
|
||||
return errors.New("error getting station tracks response")
|
||||
}
|
||||
//fmt.Println("Getting album response")
|
||||
//从resp中的Tracks数据中提取trackData信息到新的Track结构体中
|
||||
for i, trackData := range tracksResp.Data {
|
||||
albumResp, err := ampapi.GetAlbumRespByHref(trackData.Href, a.Language, token)
|
||||
if err != nil {
|
||||
fmt.Println("Error getting album response:", err)
|
||||
continue
|
||||
}
|
||||
albumLen := len(albumResp.Data[0].Relationships.Tracks.Data)
|
||||
a.Tracks = append(a.Tracks, Track{
|
||||
ID: trackData.ID,
|
||||
Type: trackData.Type,
|
||||
Name: trackData.Attributes.Name,
|
||||
Language: a.Language,
|
||||
Storefront: a.Storefront,
|
||||
|
||||
//SaveDir: filepath.Join(a.SaveDir, a.SaveName),
|
||||
//Codec: a.Codec,
|
||||
TaskNum: i + 1,
|
||||
TaskTotal: len(tracksResp.Data),
|
||||
M3u8: trackData.Attributes.ExtendedAssetUrls.EnhancedHls,
|
||||
WebM3u8: trackData.Attributes.ExtendedAssetUrls.EnhancedHls,
|
||||
//CoverPath: a.CoverPath,
|
||||
|
||||
Resp: trackData,
|
||||
PreType: "stations",
|
||||
DiscTotal: albumResp.Data[0].Relationships.Tracks.Data[albumLen-1].Attributes.DiscNumber,
|
||||
PreID: a.ID,
|
||||
AlbumData: albumResp.Data[0],
|
||||
})
|
||||
a.Tracks[i].PlaylistData.Attributes.Name = a.Name
|
||||
a.Tracks[i].PlaylistData.Attributes.ArtistName = "Apple Music Station"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Station) GetArtwork() string {
|
||||
return a.Resp.Data[0].Attributes.Artwork.URL
|
||||
}
|
||||
|
||||
// func (a *Album) ShowSelect() []int {
|
||||
// meta := a.Resp
|
||||
// trackTotal := len(meta.Data[0].Relationships.Tracks.Data)
|
||||
// arr := make([]int, trackTotal)
|
||||
// for i := 0; i < trackTotal; i++ {
|
||||
// arr[i] = i + 1
|
||||
// }
|
||||
// selected := []int{}
|
||||
// var data [][]string
|
||||
// for trackNum, track := range meta.Data[0].Relationships.Tracks.Data {
|
||||
// trackNum++
|
||||
// trackName := fmt.Sprintf("%02d. %s", track.Attributes.TrackNumber, track.Attributes.Name)
|
||||
// data = append(data, []string{fmt.Sprint(trackNum),
|
||||
// trackName,
|
||||
// track.Attributes.ContentRating,
|
||||
// track.Type})
|
||||
|
||||
// }
|
||||
// table := tablewriter.NewWriter(os.Stdout)
|
||||
// table.SetHeader([]string{"", "Track Name", "Rating", "Type"})
|
||||
// //table.SetFooter([]string{"", "", "Footer", "Footer4"})
|
||||
// table.SetRowLine(false)
|
||||
// //table.SetAutoMergeCells(true)
|
||||
// table.SetCaption(true, fmt.Sprintf("Storefront: %s, %d tracks missing", strings.ToUpper(a.Storefront), meta.Data[0].Attributes.TrackCount-trackTotal))
|
||||
// table.SetHeaderColor(tablewriter.Colors{},
|
||||
// tablewriter.Colors{tablewriter.FgRedColor, tablewriter.Bold},
|
||||
// tablewriter.Colors{tablewriter.FgBlackColor, tablewriter.Bold},
|
||||
// tablewriter.Colors{tablewriter.FgBlackColor, tablewriter.Bold})
|
||||
|
||||
// table.SetColumnColor(tablewriter.Colors{tablewriter.FgCyanColor},
|
||||
// tablewriter.Colors{tablewriter.Bold, tablewriter.FgRedColor},
|
||||
// tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor},
|
||||
// tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor})
|
||||
// for _, row := range data {
|
||||
// if row[2] == "explicit" {
|
||||
// row[2] = "E"
|
||||
// } else if row[2] == "clean" {
|
||||
// row[2] = "C"
|
||||
// } else {
|
||||
// row[2] = "None"
|
||||
// }
|
||||
// if row[3] == "music-videos" {
|
||||
// row[3] = "MV"
|
||||
// } else if row[3] == "songs" {
|
||||
// row[3] = "SONG"
|
||||
// }
|
||||
// table.Append(row)
|
||||
// }
|
||||
// //table.AppendBulk(data)
|
||||
// table.Render()
|
||||
// fmt.Println("Please select from the track options above (multiple options separated by commas, ranges supported, or type 'all' to select all)")
|
||||
// cyanColor := color.New(color.FgCyan)
|
||||
// cyanColor.Print("select: ")
|
||||
// reader := bufio.NewReader(os.Stdin)
|
||||
// input, err := reader.ReadString('\n')
|
||||
// if err != nil {
|
||||
// fmt.Println(err)
|
||||
// }
|
||||
// input = strings.TrimSpace(input)
|
||||
// if input == "all" {
|
||||
// fmt.Println("You have selected all options:")
|
||||
// selected = arr
|
||||
// } else {
|
||||
// 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})
|
||||
// }
|
||||
// }
|
||||
// //
|
||||
// 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(arr) {
|
||||
// selected = append(selected, num)
|
||||
// //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(arr) || start > end {
|
||||
// fmt.Println("Range out of range:", opt)
|
||||
// continue
|
||||
// }
|
||||
// for i := start; i <= end; i++ {
|
||||
// //fmt.Println(options[i-1])
|
||||
// selected = append(selected, i)
|
||||
// }
|
||||
// } else {
|
||||
// fmt.Println("Invalid option:", opt)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return selected
|
||||
// }
|
||||
50
utils/task/track.go
Normal file
50
utils/task/track.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"main/utils/ampapi"
|
||||
)
|
||||
|
||||
type Track struct {
|
||||
ID string
|
||||
Type string
|
||||
Name string
|
||||
Storefront string
|
||||
Language string
|
||||
|
||||
SaveDir string
|
||||
SaveName string
|
||||
SavePath string
|
||||
Codec string
|
||||
TaskNum int
|
||||
TaskTotal int
|
||||
M3u8 string
|
||||
WebM3u8 string
|
||||
DeviceM3u8 string
|
||||
Quality string
|
||||
CoverPath string
|
||||
|
||||
Resp ampapi.TrackRespData
|
||||
PreType string // 上级类型 专辑或者歌单
|
||||
PreID string // 上级ID
|
||||
DiscTotal int
|
||||
AlbumData ampapi.AlbumRespData
|
||||
PlaylistData ampapi.PlaylistRespData
|
||||
}
|
||||
|
||||
func (t *Track) GetAlbumData(token string) error {
|
||||
var err error
|
||||
resp, err := ampapi.GetAlbumRespByHref(t.Resp.Href, t.Language, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.AlbumData = resp.Data[0]
|
||||
//尝试获取该track所在album的disk总数
|
||||
if len(resp.Data) > 0 {
|
||||
len := len(resp.Data[0].Relationships.Tracks.Data)
|
||||
if len > 0 {
|
||||
t.DiscTotal = resp.Data[0].Relationships.Tracks.Data[len-1].Attributes.DiscNumber
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user