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
|
cover-format: jpg #jpg png or original
|
||||||
alac-save-folder: AM-DL downloads
|
alac-save-folder: AM-DL downloads
|
||||||
atmos-save-folder: AM-DL-Atmos downloads
|
atmos-save-folder: AM-DL-Atmos downloads
|
||||||
|
aac-save-folder: AM-DL-AAC downloads
|
||||||
max-memory-limit: 256 # MB
|
max-memory-limit: 256 # MB
|
||||||
decrypt-m3u8-port: "127.0.0.1:10020"
|
decrypt-m3u8-port: "127.0.0.1:10020"
|
||||||
get-m3u8-port: "127.0.0.1:20020"
|
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"`
|
Type string `json:"type"`
|
||||||
Attributes struct {
|
Attributes struct {
|
||||||
Ttml string `json:"ttml"`
|
Ttml string `json:"ttml"`
|
||||||
|
TtmlLocalizations string `json:"ttmlLocalizations"`
|
||||||
PlayParams struct {
|
PlayParams struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Kind string `json:"kind"`
|
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) {
|
func getSongLyrics(songId string, storefront string, token string, userToken string, lrcType string, language string) (string, error) {
|
||||||
req, err := http.NewRequest("GET",
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -66,7 +67,10 @@ func getSongLyrics(songId string, storefront string, token string, userToken str
|
|||||||
obj := new(SongLyrics)
|
obj := new(SongLyrics)
|
||||||
_ = json.NewDecoder(do.Body).Decode(&obj)
|
_ = json.NewDecoder(do.Body).Decode(&obj)
|
||||||
if obj.Data != nil {
|
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 {
|
} else {
|
||||||
return "", errors.New("failed to get lyrics")
|
return "", errors.New("failed to get lyrics")
|
||||||
}
|
}
|
||||||
@@ -165,7 +169,7 @@ func conventSyllableTTMLToLRC(ttml string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
var lrcLines []string
|
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
|
var h, m, s, ms int
|
||||||
if strings.Contains(timeValue, ":") {
|
if strings.Contains(timeValue, ":") {
|
||||||
_, err = fmt.Sscanf(timeValue, "%d:%d:%d.%d", &h, &m, &s, &ms)
|
_, 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
|
m += h * 60
|
||||||
ms = ms / 10
|
ms = ms / 10
|
||||||
if newLine {
|
if newLine == 0 {
|
||||||
return fmt.Sprintf("[%02d:%02d.%02d]<%02d:%02d.%02d>", m, s, ms, m, s, ms), nil
|
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 {
|
} else {
|
||||||
return fmt.Sprintf("<%02d:%02d.%02d>", m, s, ms), nil
|
return fmt.Sprintf("<%02d:%02d.%02d>", m, s, ms), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
divs := parsedTTML.FindElement("tt").FindElement("body").FindElements("div")
|
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 _, div := range divs {
|
||||||
for _, item := range div.ChildElements() {
|
for _, item := range div.ChildElements() { //LINES
|
||||||
var lrcSyllables []string
|
var lrcSyllables []string
|
||||||
var i int = 0
|
var i int = 0
|
||||||
var endTime string
|
var endTime, transLine string
|
||||||
for _, lyrics := range item.Child {
|
for _, lyrics := range item.Child { //WORDS
|
||||||
if _, ok := lyrics.(*etree.CharData); ok {
|
if _, ok := lyrics.(*etree.CharData); ok { //是否为span之间的空格
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
lrcSyllables = append(lrcSyllables, " ")
|
lrcSyllables = append(lrcSyllables, " ")
|
||||||
continue
|
continue
|
||||||
@@ -220,11 +212,12 @@ func conventSyllableTTMLToLRC(ttml string) (string, error) {
|
|||||||
if lyric.SelectAttr("begin") == nil {
|
if lyric.SelectAttr("begin") == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
beginTime, err := parseTime(lyric.SelectAttr("begin").Value, i == 0)
|
beginTime, err := parseTime(lyric.SelectAttr("begin").Value, i)
|
||||||
if err != nil {
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -243,6 +236,39 @@ func conventSyllableTTMLToLRC(ttml string) (string, error) {
|
|||||||
text = lyric.SelectAttr("text").Value
|
text = lyric.SelectAttr("text").Value
|
||||||
}
|
}
|
||||||
lrcSyllables = append(lrcSyllables, fmt.Sprintf("%s%s", beginTime, text))
|
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
|
i += 1
|
||||||
}
|
}
|
||||||
//endTime, err := parseTime(item.SelectAttr("end").Value)
|
//endTime, err := parseTime(item.SelectAttr("end").Value)
|
||||||
@@ -250,7 +276,10 @@ func conventSyllableTTMLToLRC(ttml string) (string, error) {
|
|||||||
// return "", err
|
// return "", err
|
||||||
//}
|
//}
|
||||||
lrcLines = append(lrcLines, strings.Join(lrcSyllables, "")+endTime)
|
lrcLines = append(lrcLines, strings.Join(lrcSyllables, "")+endTime)
|
||||||
|
if len(transLine) > 0 {
|
||||||
|
lrcLines = append(lrcLines, transLine)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return strings.Join(lrcLines, "\n"), nil
|
return strings.Join(lrcLines, "\n"), nil
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/gospider007/requests"
|
"github.com/gospider007/requests"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
@@ -24,6 +25,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
//"time"
|
||||||
|
|
||||||
"github.com/grafov/m3u8"
|
"github.com/grafov/m3u8"
|
||||||
"github.com/schollz/progressbar/v3"
|
"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
|
return obj.List[0].HlsPlaylistUrl, "", nil
|
||||||
}
|
}
|
||||||
// 遍历 Assets
|
// 遍历 Assets
|
||||||
for i, _ := range obj.List[0].Assets {
|
for i := range obj.List[0].Assets {
|
||||||
if obj.List[0].Assets[i].Flavor == "28:ctrp256" {
|
if obj.List[0].Assets[i].Flavor == "28:ctrp256" {
|
||||||
kidBase64, fileurl, err := extractKidBase64(obj.List[0].Assets[i].URL, false)
|
kidBase64, fileurl, err := extractKidBase64(obj.List[0].Assets[i].URL, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -165,7 +168,7 @@ func GetWebplayback(adamId string, authtoken string, mutoken string, mvmode bool
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "", "", nil
|
return "", "", errors.New("Unavailable")
|
||||||
}
|
}
|
||||||
|
|
||||||
type Songlist struct {
|
type Songlist struct {
|
||||||
@@ -298,10 +301,19 @@ func Run(adamId string, trackpath string, authtoken string, mutoken string, mvmo
|
|||||||
AfterRequest: AfterRequest,
|
AfterRequest: AfterRequest,
|
||||||
}
|
}
|
||||||
key.CdmInit()
|
key.CdmInit()
|
||||||
keystr, keybt, err := key.GetKey(ctx, "https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/acquireWebPlaybackLicense", pssh, nil)
|
var keybt []byte
|
||||||
if err != nil {
|
if strings.Contains(adamId, "ra.") {
|
||||||
fmt.Println(err)
|
keystr, keybt, err = key.GetKey(ctx, "https://play.itunes.apple.com/WebObjects/MZPlay.woa/web/radio/versions/1/license", pssh, nil)
|
||||||
return "", err
|
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 {
|
if mvmode {
|
||||||
keyAndUrls := "1:" + keystr + ";" + fileurl
|
keyAndUrls := "1:" + keystr + ";" + fileurl
|
||||||
@@ -335,6 +347,95 @@ func Run(adamId string, trackpath string, authtoken string, mutoken string, mvmo
|
|||||||
return "", nil
|
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 {
|
func ExtMvData(keyAndUrls string, savePath string) error {
|
||||||
segments := strings.Split(keyAndUrls, ";")
|
segments := strings.Split(keyAndUrls, ";")
|
||||||
key := segments[0]
|
key := segments[0]
|
||||||
@@ -345,36 +446,51 @@ func ExtMvData(keyAndUrls string, savePath string) error {
|
|||||||
fmt.Printf("创建文件失败:%v\n", err)
|
fmt.Printf("创建文件失败:%v\n", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer tempFile.Close()
|
|
||||||
defer os.Remove(tempFile.Name())
|
defer os.Remove(tempFile.Name())
|
||||||
|
defer tempFile.Close()
|
||||||
|
|
||||||
// 依次下载每个链接并写入文件
|
var downloadWg, writerWg sync.WaitGroup
|
||||||
bar := progressbar.DefaultBytes(
|
segmentsChan := make(chan Segment, len(urls))
|
||||||
-1,
|
// --- 新增代码: 定义最大并发数 ---
|
||||||
"Downloading...",
|
const maxConcurrency = 10
|
||||||
)
|
// --- 新增代码: 创建带缓冲的 Channel 作为信号量 ---
|
||||||
|
limiter := make(chan struct{}, maxConcurrency)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
// 初始化进度条
|
||||||
|
bar := progressbar.DefaultBytes(-1, "Downloading...")
|
||||||
barWriter := io.MultiWriter(tempFile, bar)
|
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.")
|
fmt.Println("\nDownloaded.")
|
||||||
|
|
||||||
cmd1 := exec.Command("mp4decrypt", "--key", key, tempFile.Name(), filepath.Base(savePath))
|
cmd1 := exec.Command("mp4decrypt", "--key", key, tempFile.Name(), filepath.Base(savePath))
|
||||||
|
|||||||
@@ -1,523 +1,95 @@
|
|||||||
package structs
|
package structs
|
||||||
|
|
||||||
type ConfigSet struct {
|
type ConfigSet struct {
|
||||||
MediaUserToken string `yaml:"media-user-token"`
|
MediaUserToken string `yaml:"media-user-token"`
|
||||||
AuthorizationToken string `yaml:"authorization-token"`
|
AuthorizationToken string `yaml:"authorization-token"`
|
||||||
Language string `yaml:"language"`
|
Language string `yaml:"language"`
|
||||||
SaveLrcFile bool `yaml:"save-lrc-file"`
|
SaveLrcFile bool `yaml:"save-lrc-file"`
|
||||||
LrcType string `yaml:"lrc-type"`
|
LrcType string `yaml:"lrc-type"`
|
||||||
LrcFormat string `yaml:"lrc-format"`
|
LrcFormat string `yaml:"lrc-format"`
|
||||||
SaveAnimatedArtwork bool `yaml:"save-animated-artwork"`
|
SaveAnimatedArtwork bool `yaml:"save-animated-artwork"`
|
||||||
EmbyAnimatedArtwork bool `yaml:"emby-animated-artwork"`
|
EmbyAnimatedArtwork bool `yaml:"emby-animated-artwork"`
|
||||||
EmbedLrc bool `yaml:"embed-lrc"`
|
EmbedLrc bool `yaml:"embed-lrc"`
|
||||||
EmbedCover bool `yaml:"embed-cover"`
|
EmbedCover bool `yaml:"embed-cover"`
|
||||||
SaveArtistCover bool `yaml:"save-artist-cover"`
|
SaveArtistCover bool `yaml:"save-artist-cover"`
|
||||||
CoverSize string `yaml:"cover-size"`
|
CoverSize string `yaml:"cover-size"`
|
||||||
CoverFormat string `yaml:"cover-format"`
|
CoverFormat string `yaml:"cover-format"`
|
||||||
AlacSaveFolder string `yaml:"alac-save-folder"`
|
AlacSaveFolder string `yaml:"alac-save-folder"`
|
||||||
AtmosSaveFolder string `yaml:"atmos-save-folder"`
|
AtmosSaveFolder string `yaml:"atmos-save-folder"`
|
||||||
AlbumFolderFormat string `yaml:"album-folder-format"`
|
AacSaveFolder string `yaml:"aac-save-folder"`
|
||||||
PlaylistFolderFormat string `yaml:"playlist-folder-format"`
|
AlbumFolderFormat string `yaml:"album-folder-format"`
|
||||||
ArtistFolderFormat string `yaml:"artist-folder-format"`
|
PlaylistFolderFormat string `yaml:"playlist-folder-format"`
|
||||||
SongFileFormat string `yaml:"song-file-format"`
|
ArtistFolderFormat string `yaml:"artist-folder-format"`
|
||||||
ExplicitChoice string `yaml:"explicit-choice"`
|
SongFileFormat string `yaml:"song-file-format"`
|
||||||
CleanChoice string `yaml:"clean-choice"`
|
ExplicitChoice string `yaml:"explicit-choice"`
|
||||||
AppleMasterChoice string `yaml:"apple-master-choice"`
|
CleanChoice string `yaml:"clean-choice"`
|
||||||
MaxMemoryLimit int `yaml:"max-memory-limit"`
|
AppleMasterChoice string `yaml:"apple-master-choice"`
|
||||||
DecryptM3u8Port string `yaml:"decrypt-m3u8-port"`
|
MaxMemoryLimit int `yaml:"max-memory-limit"`
|
||||||
GetM3u8Port string `yaml:"get-m3u8-port"`
|
DecryptM3u8Port string `yaml:"decrypt-m3u8-port"`
|
||||||
GetM3u8Mode string `yaml:"get-m3u8-mode"`
|
GetM3u8Port string `yaml:"get-m3u8-port"`
|
||||||
GetM3u8FromDevice bool `yaml:"get-m3u8-from-device"`
|
GetM3u8Mode string `yaml:"get-m3u8-mode"`
|
||||||
AacType string `yaml:"aac-type"`
|
GetM3u8FromDevice bool `yaml:"get-m3u8-from-device"`
|
||||||
AlacMax int `yaml:"alac-max"`
|
AacType string `yaml:"aac-type"`
|
||||||
AtmosMax int `yaml:"atmos-max"`
|
AlacMax int `yaml:"alac-max"`
|
||||||
LimitMax int `yaml:"limit-max"`
|
AtmosMax int `yaml:"atmos-max"`
|
||||||
UseSongInfoForPlaylist bool `yaml:"use-songinfo-for-playlist"`
|
LimitMax int `yaml:"limit-max"`
|
||||||
DlAlbumcoverForPlaylist bool `yaml:"dl-albumcover-for-playlist"`
|
UseSongInfoForPlaylist bool `yaml:"use-songinfo-for-playlist"`
|
||||||
MVAudioType string `yaml:"mv-audio-type"`
|
DlAlbumcoverForPlaylist bool `yaml:"dl-albumcover-for-playlist"`
|
||||||
MVMax int `yaml:"mv-max"`
|
MVAudioType string `yaml:"mv-audio-type"`
|
||||||
}
|
MVMax int `yaml:"mv-max"`
|
||||||
|
}
|
||||||
type Counter struct {
|
|
||||||
Unavailable int
|
type Counter struct {
|
||||||
NotSong int
|
Unavailable int
|
||||||
Error int
|
NotSong int
|
||||||
Success int
|
Error int
|
||||||
Total int
|
Success int
|
||||||
}
|
Total int
|
||||||
|
}
|
||||||
type ApiResult struct {
|
|
||||||
Data []SongData `json:"data"`
|
// 艺术家页面
|
||||||
}
|
type AutoGeneratedArtist struct {
|
||||||
|
Next string `json:"next"`
|
||||||
type SongAttributes struct {
|
Data []struct {
|
||||||
ArtistName string `json:"artistName"`
|
ID string `json:"id"`
|
||||||
DiscNumber int `json:"discNumber"`
|
Type string `json:"type"`
|
||||||
GenreNames []string `json:"genreNames"`
|
Href string `json:"href"`
|
||||||
ExtendedAssetUrls struct {
|
Attributes struct {
|
||||||
EnhancedHls string `json:"enhancedHls"`
|
Previews []struct {
|
||||||
} `json:"extendedAssetUrls"`
|
URL string `json:"url"`
|
||||||
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
} `json:"previews"`
|
||||||
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
|
Artwork struct {
|
||||||
ContentRating string `json:"contentRating"`
|
Width int `json:"width"`
|
||||||
ReleaseDate string `json:"releaseDate"`
|
Height int `json:"height"`
|
||||||
Name string `json:"name"`
|
URL string `json:"url"`
|
||||||
Isrc string `json:"isrc"`
|
BgColor string `json:"bgColor"`
|
||||||
AlbumName string `json:"albumName"`
|
TextColor1 string `json:"textColor1"`
|
||||||
TrackNumber int `json:"trackNumber"`
|
TextColor2 string `json:"textColor2"`
|
||||||
ComposerName string `json:"composerName"`
|
TextColor3 string `json:"textColor3"`
|
||||||
}
|
TextColor4 string `json:"textColor4"`
|
||||||
|
} `json:"artwork"`
|
||||||
type AlbumAttributes struct {
|
ArtistName string `json:"artistName"`
|
||||||
ArtistName string `json:"artistName"`
|
URL string `json:"url"`
|
||||||
IsSingle bool `json:"isSingle"`
|
DiscNumber int `json:"discNumber"`
|
||||||
IsComplete bool `json:"isComplete"`
|
GenreNames []string `json:"genreNames"`
|
||||||
GenreNames []string `json:"genreNames"`
|
HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"`
|
||||||
TrackCount int `json:"trackCount"`
|
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
||||||
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
|
||||||
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
|
ContentRating string `json:"contentRating"`
|
||||||
ContentRating string `json:"contentRating"`
|
DurationInMillis int `json:"durationInMillis"`
|
||||||
ReleaseDate string `json:"releaseDate"`
|
ReleaseDate string `json:"releaseDate"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
RecordLabel string `json:"recordLabel"`
|
Isrc string `json:"isrc"`
|
||||||
Upc string `json:"upc"`
|
AudioTraits []string `json:"audioTraits"`
|
||||||
Copyright string `json:"copyright"`
|
HasLyrics bool `json:"hasLyrics"`
|
||||||
IsCompilation bool `json:"isCompilation"`
|
AlbumName string `json:"albumName"`
|
||||||
}
|
PlayParams struct {
|
||||||
|
ID string `json:"id"`
|
||||||
type SongData struct {
|
Kind string `json:"kind"`
|
||||||
ID string `json:"id"`
|
} `json:"playParams"`
|
||||||
Attributes SongAttributes `json:"attributes"`
|
TrackNumber int `json:"trackNumber"`
|
||||||
Relationships struct {
|
AudioLocale string `json:"audioLocale"`
|
||||||
Albums struct {
|
ComposerName string `json:"composerName"`
|
||||||
Data []struct {
|
} `json:"attributes"`
|
||||||
ID string `json:"id"`
|
} `json:"data"`
|
||||||
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 {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Href string `json:"href"`
|
|
||||||
Attributes struct {
|
|
||||||
Previews []struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
} `json:"previews"`
|
|
||||||
Artwork struct {
|
|
||||||
Width int `json:"width"`
|
|
||||||
Height int `json:"height"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
BgColor string `json:"bgColor"`
|
|
||||||
TextColor1 string `json:"textColor1"`
|
|
||||||
TextColor2 string `json:"textColor2"`
|
|
||||||
TextColor3 string `json:"textColor3"`
|
|
||||||
TextColor4 string `json:"textColor4"`
|
|
||||||
} `json:"artwork"`
|
|
||||||
ArtistName string `json:"artistName"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
DiscNumber int `json:"discNumber"`
|
|
||||||
GenreNames []string `json:"genreNames"`
|
|
||||||
HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"`
|
|
||||||
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
|
||||||
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
|
|
||||||
ContentRating string `json:"contentRating"`
|
|
||||||
DurationInMillis int `json:"durationInMillis"`
|
|
||||||
ReleaseDate string `json:"releaseDate"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Isrc string `json:"isrc"`
|
|
||||||
AudioTraits []string `json:"audioTraits"`
|
|
||||||
HasLyrics bool `json:"hasLyrics"`
|
|
||||||
AlbumName string `json:"albumName"`
|
|
||||||
PlayParams struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Kind string `json:"kind"`
|
|
||||||
} `json:"playParams"`
|
|
||||||
TrackNumber int `json:"trackNumber"`
|
|
||||||
AudioLocale string `json:"audioLocale"`
|
|
||||||
ComposerName string `json:"composerName"`
|
|
||||||
} `json:"attributes"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type 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