Merge remote-tracking branch 'upstream/main'

This commit is contained in:
PurelyAndy
2025-08-24 18:19:49 -04:00
21 changed files with 3227 additions and 1187 deletions

View File

@@ -9,6 +9,7 @@ English / [简体中文](./README-CN.md)
3. Support downloading singers `go run main.go https://music.apple.com/us/artist/taylor-swift/159260351` `--all-album` Automatically select all albums of the artist
4. The download decryption part is replaced with Sendy McSenderson to decrypt while downloading, and solve the lack of memory when decrypting large files
5. MV Download, installation required[mp4decrypt](https://www.bento4.com/downloads/)
6. Add interactive search with arrow-key navigation `go run main.go --search [song/album/artist] "search_term"`
### Special thanks to `chocomint` for creating `agent-arm64.js`
@@ -45,3 +46,19 @@ Original script by Sorrow. Modified by me to include some fixes and improvements
3. Find the cookie named `media-user-token` and copy its value
4. Paste the cookie value obtained in step 3 into the config.yaml and save it
5. Start the script as usual
## Get translation and pronunciation lyrics (Beta)
1. Open [Apple Music](https://beta.music.apple.com) and log in.
2. Open the Developer tools, click `Network` tab.
3. Search a song which is available for translation and pronunciation lyrics (recommend K-Pop songs).
4. Press Ctrl+R and let Developer tools sniff network data.
5. Play a song and then click lyric button, sniff will show a data called `syllable-lyrics`.
6. Stop sniff (small red circles button on top left), then click `Fetch/XHR` tabs.
7. Click `syllable-lyrics` data, see requested URL.
8. Find this line `.../syllable-lyrics?l=<copy all the language value from here>&extend=ttmlLocalizations`.
9. Paste the language value obtained in step 8 into the config.yaml and save it.
10. If don't need pronunciation, do this `...%5D=<remove this value>&extend...` on config.yaml and save it.
11. Start the script as usual.
Noted: These features are only in beta version right now.

View File

@@ -13,6 +13,7 @@ cover-size: 5000x5000
cover-format: jpg #jpg png or original
alac-save-folder: AM-DL downloads
atmos-save-folder: AM-DL-Atmos downloads
aac-save-folder: AM-DL-AAC downloads
max-memory-limit: 256 # MB
decrypt-m3u8-port: "127.0.0.1:10020"
get-m3u8-port: "127.0.0.1:20020"
@@ -44,3 +45,8 @@ use-songinfo-for-playlist: false
dl-albumcover-for-playlist: false
mv-audio-type: atmos #atmos ac3 aac
mv-max: 2160
# storefront will be used only in searching.
# storefront is the 2-letter country code that are available in the urls (jp, ca, us etc.).
# if your account is from Japan, you must use jp.
# if the storefront is different from your account, you will see a "failed to get lyrics" error in most of the songs. By default the storefront is set to US if not set.
storefront: "enter your account storefront"

3
go.mod
View File

@@ -46,6 +46,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
@@ -54,6 +55,7 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/acmez/v3 v3.0.0 // indirect
github.com/mholt/archives v0.1.0 // indirect
github.com/miekg/dns v1.1.62 // indirect
@@ -95,6 +97,7 @@ require (
)
require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/beevik/etree v1.3.0
github.com/fatih/color v1.18.0
github.com/olekukonko/tablewriter v0.0.5

18
go.sum
View File

@@ -15,10 +15,14 @@ cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Eyevinn/mp4ff v0.46.0 h1:A8oJA4A3C9fDbX38jEw/26utjNdvmRmrO37tVI5pDk0=
github.com/Eyevinn/mp4ff v0.46.0/go.mod h1:hJNUUqOBryLAzUW9wpCJyw2HaI+TCd2rUPhafoS5lgg=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
@@ -48,6 +52,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -143,12 +149,16 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
@@ -167,13 +177,18 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/acmez/v3 v3.0.0 h1:r1NcjuWR0VaKP2BTjDK9LRFBw/WvURx3jlaEUl9Ht8E=
github.com/mholt/acmez/v3 v3.0.0/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/mholt/archives v0.1.0 h1:FacgJyrjiuyomTuNA92X5GyRBRZjE43Y/lrzKIlF35Q=
@@ -224,6 +239,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
@@ -365,6 +381,7 @@ golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -408,6 +425,7 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=

1548
main.go

File diff suppressed because it is too large Load Diff

243
utils/ampapi/album.go Normal file
View 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
View File

@@ -0,0 +1 @@
package ampapi

145
utils/ampapi/musicvideo.go Normal file
View 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
View 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"`
}

95
utils/ampapi/search.go Normal file
View File

@@ -0,0 +1,95 @@
package ampapi
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)
// SearchResp represents the top-level response from the search API.
type SearchResp struct {
Results SearchResults `json:"results"`
}
// SearchResults contains the different types of search results.
type SearchResults struct {
Songs *SongResults `json:"songs,omitempty"`
Albums *AlbumResults `json:"albums,omitempty"`
Artists *ArtistResults `json:"artists,omitempty"`
}
// SongResults contains a list of song search results.
type SongResults struct {
Href string `json:"href"`
Next string `json:"next"`
Data []SongRespData `json:"data"`
}
// AlbumResults contains a list of album search results.
type AlbumResults struct {
Href string `json:"href"`
Next string `json:"next"`
Data []AlbumRespData `json:"data"`
}
// ArtistResults contains a list of artist search results.
type ArtistResults 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 {
Name string `json:"name"`
GenreNames []string `json:"genreNames"`
URL string `json:"url"`
} `json:"attributes"`
} `json:"data"`
}
// Search performs a search query against the Apple Music API.
func Search(storefront, term, types, language, token string, limit, offset int) (*SearchResp, 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/search", storefront), 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("term", term)
query.Set("types", types)
query.Set("limit", fmt.Sprintf("%d", limit))
query.Set("offset", fmt.Sprintf("%d", offset))
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, fmt.Errorf("API request failed with status: %s", do.Status)
}
obj := new(SearchResp)
err = json.NewDecoder(do.Body).Decode(&obj)
if err != nil {
return nil, err
}
return obj, nil
}

153
utils/ampapi/song.go Normal file
View 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
View 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
View 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
View 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"`
}

View File

@@ -16,6 +16,7 @@ type SongLyrics struct {
Type string `json:"type"`
Attributes struct {
Ttml string `json:"ttml"`
TtmlLocalizations string `json:"ttmlLocalizations"`
PlayParams struct {
Id string `json:"id"`
Kind string `json:"kind"`
@@ -47,9 +48,10 @@ func Get(storefront, songId, lrcType, language, lrcFormat, token, mediaUserToken
return lrc, nil
}
func getSongLyrics(songId string, storefront string, token string, userToken string, lrcType string, language string) (string, error) {
req, err := http.NewRequest("GET",
fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s/%s?l=%s", storefront, songId, lrcType, language), nil)
fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s/%s?l=%s&extend=ttmlLocalizations", storefront, songId, lrcType, language), nil)
if err != nil {
return "", err
}
@@ -66,12 +68,59 @@ func getSongLyrics(songId string, storefront string, token string, userToken str
obj := new(SongLyrics)
_ = json.NewDecoder(do.Body).Decode(&obj)
if obj.Data != nil {
return obj.Data[0].Attributes.Ttml, nil
if len(obj.Data[0].Attributes.Ttml) > 0 {
return obj.Data[0].Attributes.Ttml, nil
}
return obj.Data[0].Attributes.TtmlLocalizations, nil
} else {
return "", errors.New("failed to get lyrics")
}
}
// Use for detect if lyrics have CJK, will be replaced by transliteration if exist.
func containsCJK(s string) bool {
for _, r := range s {
if (r >= 0x1100 && r <= 0x11FF) || // Hangul Jamo
(r >= 0x2E80 && r <= 0x2EFF) || // CJK Radicals Supplement
(r >= 0x2F00 && r <= 0x2FDF) || // Kangxi Radicals
(r >= 0x2FF0 && r <= 0x2FFF) || // Ideographic Description Characters
(r >= 0x3000 && r <= 0x303F) || // CJK Symbols and Punctuation
(r >= 0x3040 && r <= 0x309F) || // Hiragana
(r >= 0x30A0 && r <= 0x30FF) || // Katakana
(r >= 0x3130 && r <= 0x318F) || // Hangul Compatibility Jamo
(r >= 0x31C0 && r <= 0x31EF) || // CJK Strokes
(r >= 0x31F0 && r <= 0x31FF) || // Katakana Phonetic Extensions
(r >= 0x3200 && r <= 0x32FF) || // Enclosed CJK Letters and Months
(r >= 0x3300 && r <= 0x33FF) || // CJK Compatibility
(r >= 0x3400 && r <= 0x4DBF) || // CJK Unified Ideographs Extension A
(r >= 0x4E00 && r <= 0x9FFF) || // CJK Unified Ideographs
(r >= 0xA960 && r <= 0xA97F) || // Hangul Jamo Extended-A
(r >= 0xAC00 && r <= 0xD7AF) || // Hangul Syllables
(r >= 0xD7B0 && r <= 0xD7FF) || // Hangul Jamo Extended-B
(r >= 0xF900 && r <= 0xFAFF) || // CJK Compatibility Ideographs
(r >= 0xFE30 && r <= 0xFE4F) || // CJK Compatibility Forms
(r >= 0xFF65 && r <= 0xFF9F) || // Halfwidth Katakana
(r >= 0xFFA0 && r <= 0xFFDC) || // Halfwidth Jamo
(r >= 0x1AFF0 && r <= 0x1AFFF) || // Kana Extended-B
(r >= 0x1B000 && r <= 0x1B0FF) || // Kana Supplement
(r >= 0x1B100 && r <= 0x1B12F) || // Kana Extended-A
(r >= 0x1B130 && r <= 0x1B16F) || // Small Kana Extension
(r >= 0x1F200 && r <= 0x1F2FF) || // Enclosed Ideographic Supplement
(r >= 0x20000 && r <= 0x2A6DF) || // CJK Unified Ideographs Extension B
(r >= 0x2A700 && r <= 0x2B73F) || // CJK Unified Ideographs Extension C
(r >= 0x2B740 && r <= 0x2B81F) || // CJK Unified Ideographs Extension D
(r >= 0x2B820 && r <= 0x2CEAF) || // CJK Unified Ideographs Extension E
(r >= 0x2CEB0 && r <= 0x2EBEF) || // CJK Unified Ideographs Extension F
(r >= 0x2EBF0 && r <= 0x2EE5F) || // CJK Unified Ideographs Extension I
(r >= 0x2F800 && r <= 0x2FA1F) || // CJK Compatibility Ideographs Supplement
(r >= 0x30000 && r <= 0x3134F) || // CJK Unified Ideographs Extension G
(r >= 0x31350 && r <= 0x323AF) { // CJK Unified Ideographs Extension H
return true
}
}
return false
}
func TtmlToLrc(ttml string) (string, error) {
parsedTTML := etree.NewDocument()
err := parsedTTML.ReadFromString(ttml)
@@ -101,37 +150,76 @@ func TtmlToLrc(ttml string) (string, error) {
for _, item := range parsedTTML.FindElement("tt").FindElement("body").ChildElements() {
for _, lyric := range item.ChildElements() {
var h, m, s, ms int
if lyric.SelectAttr("begin") == nil {
beginAttr := lyric.SelectAttr("begin")
if beginAttr == nil {
return "", errors.New("no synchronised lyrics")
}
if strings.Contains(lyric.SelectAttr("begin").Value, ":") {
_, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d:%d.%d", &h, &m, &s, &ms)
beginValue := beginAttr.Value
if strings.Contains(beginValue, ":") {
_, err = fmt.Sscanf(beginValue, "%d:%d:%d.%d", &h, &m, &s, &ms)
if err != nil {
_, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d.%d", &m, &s, &ms)
_, err = fmt.Sscanf(beginValue, "%d:%d.%d", &m, &s, &ms)
if err != nil {
_, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d", &m, &s)
_, err = fmt.Sscanf(beginValue, "%d:%d", &m, &s)
}
h = 0
}
} else {
_, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d.%d", &s, &ms)
_, err = fmt.Sscanf(beginValue, "%d.%d", &s, &ms)
h, m = 0, 0
}
if err != nil {
return "", err
}
var text string
//GET trans
m += h * 60
ms = ms / 10
var text, transText, translitText string
//GET trans and translit
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("transliterations")) > 0 {
if len(iTunesMetadata.FindElement("transliterations").FindElements("transliteration")) > 0 {
xpath := fmt.Sprintf("text[@for='%s']", lyric.SelectAttr("itunes:key").Value)
translit := iTunesMetadata.FindElement("transliterations").FindElement("transliteration").FindElement(xpath)
if translit != nil {
if translit.SelectAttr("text") != nil {
translitText = translit.SelectAttr("text").Value
} else {
var translitTmp []string
for _, span := range translit.Child {
if c, ok := span.(*etree.CharData); ok {
translitTmp = append(translitTmp, c.Data)
} else if e, ok := span.(*etree.Element); ok {
translitTmp = append(translitTmp, e.Text())
}
}
translitText = strings.Join(translitTmp, "")
}
}
}
}
if len(iTunesMetadata.FindElements("translations")) > 0 {
if len(iTunesMetadata.FindElement("translations").FindElements("translation")) > 0 {
xpath := fmt.Sprintf("//text[@for='%s']", lyric.SelectAttr("itunes:key").Value)
trans := iTunesMetadata.FindElement("translations").FindElement("translation").FindElement(xpath)
lyric = trans
if trans != nil {
if trans.SelectAttr("text") != nil {
transText = trans.SelectAttr("text").Value
} else {
var transTmp []string
for _, span := range trans.Child {
if c, ok := span.(*etree.CharData); ok {
transTmp = append(transTmp, c.Data)
} else if e, ok := span.(*etree.Element); ok {
transTmp = append(transTmp, e.Text())
}
}
transText = strings.Join(transTmp, "")
}
}
}
}
}
@@ -150,9 +238,14 @@ func TtmlToLrc(ttml string) (string, error) {
} else {
text = lyric.SelectAttr("text").Value
}
m += h * 60
ms = ms / 10
lrcLines = append(lrcLines, fmt.Sprintf("[%02d:%02d.%02d]%s", m, s, ms, text))
if len(transText) > 0 {
lrcLines = append(lrcLines, fmt.Sprintf("[%02d:%02d.%02d]%s", m, s, ms, transText))
}
if len(translitText) > 0 && containsCJK(text) {
lrcLines = append(lrcLines, fmt.Sprintf("[%02d:%02d.%02d]%s", m, s, ms, translitText))
} else {
lrcLines = append(lrcLines, fmt.Sprintf("[%02d:%02d.%02d]%s", m, s, ms, text))
}
}
}
return strings.Join(lrcLines, "\n"), nil
@@ -165,7 +258,7 @@ func conventSyllableTTMLToLRC(ttml string) (string, error) {
return "", err
}
var lrcLines []string
parseTime := func(timeValue string, newLine bool) (string, error) {
parseTime := func(timeValue string, newLine int) (string, error) {
var h, m, s, ms int
if strings.Contains(timeValue, ":") {
_, err = fmt.Sscanf(timeValue, "%d:%d:%d.%d", &h, &m, &s, &ms)
@@ -182,34 +275,22 @@ func conventSyllableTTMLToLRC(ttml string) (string, error) {
}
m += h * 60
ms = ms / 10
if newLine {
if newLine == 0 {
return fmt.Sprintf("[%02d:%02d.%02d]<%02d:%02d.%02d>", m, s, ms, m, s, ms), nil
} else if newLine == -1 {
return fmt.Sprintf("[%02d:%02d.%02d]", m, s, ms), nil
} else {
return fmt.Sprintf("<%02d:%02d.%02d>", m, s, ms), nil
}
}
divs := parsedTTML.FindElement("tt").FindElement("body").FindElements("div")
//get trans
if len(parsedTTML.FindElement("tt").FindElements("head")) > 0 {
if len(parsedTTML.FindElement("tt").FindElement("head").FindElements("metadata")) > 0 {
Metadata := parsedTTML.FindElement("tt").FindElement("head").FindElement("metadata")
if len(Metadata.FindElements("iTunesMetadata")) > 0 {
iTunesMetadata := Metadata.FindElement("iTunesMetadata")
if len(iTunesMetadata.FindElements("translations")) > 0 {
if len(iTunesMetadata.FindElement("translations").FindElements("translation")) > 0 {
divs = iTunesMetadata.FindElement("translations").FindElements("translation")
}
}
}
}
}
for _, div := range divs {
for _, item := range div.ChildElements() {
for _, item := range div.ChildElements() { //LINES
var lrcSyllables []string
var i int = 0
var endTime string
for _, lyrics := range item.Child {
if _, ok := lyrics.(*etree.CharData); ok {
var endTime, translitLine, transLine string
for _, lyrics := range item.Child { //WORDS
if _, ok := lyrics.(*etree.CharData); ok { //是否为span之间的空格
if i > 0 {
lrcSyllables = append(lrcSyllables, " ")
continue
@@ -220,11 +301,12 @@ func conventSyllableTTMLToLRC(ttml string) (string, error) {
if lyric.SelectAttr("begin") == nil {
continue
}
beginTime, err := parseTime(lyric.SelectAttr("begin").Value, i == 0)
beginTime, err := parseTime(lyric.SelectAttr("begin").Value, i)
if err != nil {
return "", err
return "", err
}
endTime, err = parseTime(lyric.SelectAttr("end").Value, false)
endTime, err = parseTime(lyric.SelectAttr("end").Value, 1)
if err != nil {
return "", err
}
@@ -243,13 +325,88 @@ func conventSyllableTTMLToLRC(ttml string) (string, error) {
text = lyric.SelectAttr("text").Value
}
lrcSyllables = append(lrcSyllables, fmt.Sprintf("%s%s", beginTime, text))
if i == 0 {
transBeginTime, _ := parseTime(lyric.SelectAttr("begin").Value, -1)
sharedTimestamp := ""
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("transliterations")) > 0 {
if len(iTunesMetadata.FindElement("transliterations").FindElements("transliteration")) > 0 {
xpath := fmt.Sprintf("text[@for='%s']", item.SelectAttr("itunes:key").Value)
trans := iTunesMetadata.FindElement("transliterations").FindElement("transliteration").FindElement(xpath)
// Get text content
var transTxtParts []string
var transStartTime string
for i, span := range trans.ChildElements() {
if span.Tag == "span" {
spanBegin := span.SelectAttrValue("begin", "")
spanText := span.Text()
if spanBegin == "" {
continue
}
// Get timestamp
timestamp, err := parseTime(spanBegin, 2)
if err != nil {
return "", err
}
if i == 0 {
// For [mm:ss.xx] prefix
transStartTime, _ = parseTime(spanBegin, -1)
sharedTimestamp = transStartTime
}
transTxtParts = append(transTxtParts, fmt.Sprintf("%s%s", timestamp, spanText))
}
}
translitLine = fmt.Sprintf("%s%s", transStartTime, strings.Join(transTxtParts, " "))
}
}
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)
if sharedTimestamp != "" {
transLine = sharedTimestamp + transTxt
} else {
transLine = transBeginTime + transTxt
}
}
}
}
}
}
}
i += 1
}
//endTime, err := parseTime(item.SelectAttr("end").Value)
//if err != nil {
// return "", err
//}
lrcLines = append(lrcLines, strings.Join(lrcSyllables, "")+endTime)
if len(transLine) > 0 {
lrcLines = append(lrcLines, transLine)
}
if len(translitLine) > 0 && containsCJK(strings.Join(lrcSyllables, "")) {
lrcLines = append(lrcLines, translitLine)
} else {
lrcLines = append(lrcLines, strings.Join(lrcSyllables, "")+endTime)
}
}
}
return strings.Join(lrcLines, "\n"), nil

View File

@@ -5,6 +5,7 @@ import (
"encoding/base64"
"fmt"
"path/filepath"
"github.com/gospider007/requests"
"google.golang.org/protobuf/proto"
@@ -24,6 +25,8 @@ import (
"net/http"
"os/exec"
"strings"
"sync"
//"time"
"github.com/grafov/m3u8"
"github.com/schollz/progressbar/v3"
@@ -154,7 +157,7 @@ func GetWebplayback(adamId string, authtoken string, mutoken string, mvmode bool
return obj.List[0].HlsPlaylistUrl, "", nil
}
// 遍历 Assets
for i, _ := range obj.List[0].Assets {
for i := range obj.List[0].Assets {
if obj.List[0].Assets[i].Flavor == "28:ctrp256" {
kidBase64, fileurl, err := extractKidBase64(obj.List[0].Assets[i].URL, false)
if err != nil {
@@ -165,7 +168,7 @@ func GetWebplayback(adamId string, authtoken string, mutoken string, mvmode bool
continue
}
}
return "", "", nil
return "", "", errors.New("Unavailable")
}
type Songlist struct {
@@ -298,10 +301,19 @@ func Run(adamId string, trackpath string, authtoken string, mutoken string, mvmo
AfterRequest: AfterRequest,
}
key.CdmInit()
keystr, keybt, err := key.GetKey(ctx, "https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/acquireWebPlaybackLicense", pssh, nil)
if err != nil {
fmt.Println(err)
return "", err
var keybt []byte
if strings.Contains(adamId, "ra.") {
keystr, keybt, err = key.GetKey(ctx, "https://play.itunes.apple.com/WebObjects/MZPlay.woa/web/radio/versions/1/license", pssh, nil)
if err != nil {
fmt.Println(err)
return "", err
}
} else {
keystr, keybt, err = key.GetKey(ctx, "https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/acquireWebPlaybackLicense", pssh, nil)
if err != nil {
fmt.Println(err)
return "", err
}
}
if mvmode {
keyAndUrls := "1:" + keystr + ";" + fileurl
@@ -335,6 +347,95 @@ func Run(adamId string, trackpath string, authtoken string, mutoken string, mvmo
return "", nil
}
// Segment 结构体用于在 Channel 中传递分段数据
type Segment struct {
Index int
Data []byte
}
func downloadSegment(url string, index int, wg *sync.WaitGroup, segmentsChan chan<- Segment, client *http.Client, limiter chan struct{}) {
// 函数退出时,从 limiter 中接收一个值,释放一个并发槽位
defer func() {
<-limiter
wg.Done()
}()
req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Printf("错误(分段 %d): 创建请求失败: %v\n", index, err)
return
}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("错误(分段 %d): 下载失败: %v\n", index, err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("错误(分段 %d): 服务器返回状态码 %d\n", index, resp.StatusCode)
return
}
data, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("错误(分段 %d): 读取数据失败: %v\n", index, err)
return
}
// 将下载好的分段(包含序号和数据)发送到 Channel
segmentsChan <- Segment{Index: index, Data: data}
}
// fileWriter 从 Channel 接收分段并按顺序写入文件
func fileWriter(wg *sync.WaitGroup, segmentsChan <-chan Segment, outputFile io.Writer, totalSegments int) {
defer wg.Done()
// 缓冲区,用于存放乱序到达的分段
// key 是分段序号value 是分段数据
segmentBuffer := make(map[int][]byte)
nextIndex := 0 // 期望写入的下一个分段的序号
for segment := range segmentsChan {
// 检查收到的分段是否是当前期望的
if segment.Index == nextIndex {
//fmt.Printf("写入分段 %d\n", segment.Index)
_, err := outputFile.Write(segment.Data)
if err != nil {
fmt.Printf("错误(分段 %d): 写入文件失败: %v\n", segment.Index, err)
}
nextIndex++
// 检查缓冲区中是否有下一个连续的分段
for {
data, ok := segmentBuffer[nextIndex]
if !ok {
break // 缓冲区里没有下一个,跳出循环,等待下一个分段到达
}
//fmt.Printf("从缓冲区写入分段 %d\n", nextIndex)
_, err := outputFile.Write(data)
if err != nil {
fmt.Printf("错误(分段 %d): 从缓冲区写入文件失败: %v\n", nextIndex, err)
}
// 从缓冲区删除已写入的分段,释放内存
delete(segmentBuffer, nextIndex)
nextIndex++
}
} else {
// 如果不是期望的分段,先存入缓冲区
//fmt.Printf("缓冲分段 %d (等待 %d)\n", segment.Index, nextIndex)
segmentBuffer[segment.Index] = segment.Data
}
}
// 确保所有分段都已写入
if nextIndex != totalSegments {
fmt.Printf("警告: 写入完成,但似乎有分段丢失。期望 %d 个, 实际写入 %d 个。\n", totalSegments, nextIndex)
}
}
func ExtMvData(keyAndUrls string, savePath string) error {
segments := strings.Split(keyAndUrls, ";")
key := segments[0]
@@ -345,36 +446,51 @@ func ExtMvData(keyAndUrls string, savePath string) error {
fmt.Printf("创建文件失败:%v\n", err)
return err
}
defer tempFile.Close()
defer os.Remove(tempFile.Name())
defer tempFile.Close()
// 依次下载每个链接并写入文件
bar := progressbar.DefaultBytes(
-1,
"Downloading...",
)
var downloadWg, writerWg sync.WaitGroup
segmentsChan := make(chan Segment, len(urls))
// --- 新增代码: 定义最大并发数 ---
const maxConcurrency = 10
// --- 新增代码: 创建带缓冲的 Channel 作为信号量 ---
limiter := make(chan struct{}, maxConcurrency)
client := &http.Client{}
// 初始化进度条
bar := progressbar.DefaultBytes(-1, "Downloading...")
barWriter := io.MultiWriter(tempFile, bar)
for _, url := range urls {
resp, err := http.Get(url)
if err != nil {
fmt.Printf("下载链接 %s 失败:%v\n", url, err)
return err
}
if resp.StatusCode != http.StatusOK {
fmt.Printf("链接 %s 响应失败:%v\n", url, resp.Status)
return errors.New(resp.Status)
}
// 将响应体写入输出文件
_, err = io.Copy(barWriter, resp.Body)
defer resp.Body.Close() // 注意及时关闭响应体,避免资源泄露
if err != nil {
fmt.Printf("写入文件失败:%v\n", err)
return err
}
//fmt.Printf("第 %d 个链接 %s 下载并写入完成\n", idx+1, url)
// 启动写入 Goroutine
writerWg.Add(1)
go fileWriter(&writerWg, segmentsChan, barWriter, len(urls))
// 启动下载 Goroutines
for i, url := range urls {
// 在启动 Goroutine 前,向 limiter 发送一个值来“获取”一个槽位
// 如果 limiter 已满 (达到10个),这里会阻塞,直到有其他任务完成并释放槽位
//fmt.Printf("请求启动任务 %d...\n", i)
limiter <- struct{}{}
//fmt.Printf("...任务 %d 已启动\n", i)
downloadWg.Add(1)
// 将 limiter 传递给下载函数
go downloadSegment(url, i, &downloadWg, segmentsChan, client, limiter)
}
// 等待所有下载任务完成
downloadWg.Wait()
// 下载完成后,关闭 Channel。写入 Goroutine 会在处理完 Channel 中所有数据后退出。
close(segmentsChan)
// 等待写入 Goroutine 完成所有写入和缓冲处理
writerWg.Wait()
// 显式关闭文件defer会再次调用但重复关闭是安全的
if err := tempFile.Close(); err != nil {
fmt.Printf("关闭临时文件失败: %v\n", err)
return err
}
tempFile.Close()
fmt.Println("\nDownloaded.")
cmd1 := exec.Command("mp4decrypt", "--key", key, tempFile.Name(), filepath.Base(savePath))

View File

@@ -1,6 +1,7 @@
package structs
type ConfigSet struct {
Storefront string `yaml:"storefront"`
MediaUserToken string `yaml:"media-user-token"`
AuthorizationToken string `yaml:"authorization-token"`
Language string `yaml:"language"`
@@ -16,6 +17,7 @@ type ConfigSet struct {
CoverFormat string `yaml:"cover-format"`
AlacSaveFolder string `yaml:"alac-save-folder"`
AtmosSaveFolder string `yaml:"atmos-save-folder"`
AacSaveFolder string `yaml:"aac-save-folder"`
AlbumFolderFormat string `yaml:"album-folder-format"`
PlaylistFolderFormat string `yaml:"playlist-folder-format"`
ArtistFolderFormat string `yaml:"artist-folder-format"`
@@ -46,380 +48,7 @@ type Counter struct {
Total int
}
type ApiResult struct {
Data []SongData `json:"data"`
}
type SongAttributes struct {
ArtistName string `json:"artistName"`
DiscNumber int `json:"discNumber"`
GenreNames []string `json:"genreNames"`
ExtendedAssetUrls struct {
EnhancedHls string `json:"enhancedHls"`
} `json:"extendedAssetUrls"`
IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"`
Name string `json:"name"`
Isrc string `json:"isrc"`
AlbumName string `json:"albumName"`
TrackNumber int `json:"trackNumber"`
ComposerName string `json:"composerName"`
}
type AlbumAttributes struct {
ArtistName string `json:"artistName"`
IsSingle bool `json:"isSingle"`
IsComplete bool `json:"isComplete"`
GenreNames []string `json:"genreNames"`
TrackCount int `json:"trackCount"`
IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"`
Name string `json:"name"`
RecordLabel string `json:"recordLabel"`
Upc string `json:"upc"`
Copyright string `json:"copyright"`
IsCompilation bool `json:"isCompilation"`
}
type SongData struct {
ID string `json:"id"`
Attributes SongAttributes `json:"attributes"`
Relationships struct {
Albums struct {
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes AlbumAttributes `json:"attributes"`
} `json:"data"`
} `json:"albums"`
Artists struct {
Href string `json:"href"`
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
} `json:"data"`
} `json:"artists"`
} `json:"relationships"`
}
type SongResult struct {
Artwork struct {
Width int `json:"width"`
URL string `json:"url"`
Height int `json:"height"`
TextColor3 string `json:"textColor3"`
TextColor2 string `json:"textColor2"`
TextColor4 string `json:"textColor4"`
HasAlpha bool `json:"hasAlpha"`
TextColor1 string `json:"textColor1"`
BgColor string `json:"bgColor"`
HasP3 bool `json:"hasP3"`
SupportsLayeredImage bool `json:"supportsLayeredImage"`
} `json:"artwork"`
ArtistName string `json:"artistName"`
CollectionID string `json:"collectionId"`
DiscNumber int `json:"discNumber"`
GenreNames []string `json:"genreNames"`
ID string `json:"id"`
DurationInMillis int `json:"durationInMillis"`
ReleaseDate string `json:"releaseDate"`
ContentRatingsBySystem struct {
} `json:"contentRatingsBySystem"`
Name string `json:"name"`
Composer struct {
Name string `json:"name"`
URL string `json:"url"`
} `json:"composer"`
EditorialArtwork struct {
} `json:"editorialArtwork"`
CollectionName string `json:"collectionName"`
AssetUrls struct {
Plus string `json:"plus"`
Lightweight string `json:"lightweight"`
SuperLightweight string `json:"superLightweight"`
LightweightPlus string `json:"lightweightPlus"`
EnhancedHls string `json:"enhancedHls"`
} `json:"assetUrls"`
AudioTraits []string `json:"audioTraits"`
Kind string `json:"kind"`
Copyright string `json:"copyright"`
ArtistID string `json:"artistId"`
Genres []struct {
GenreID string `json:"genreId"`
Name string `json:"name"`
URL string `json:"url"`
MediaType string `json:"mediaType"`
} `json:"genres"`
TrackNumber int `json:"trackNumber"`
AudioLocale string `json:"audioLocale"`
Offers []struct {
ActionText struct {
Short string `json:"short"`
Medium string `json:"medium"`
Long string `json:"long"`
Downloaded string `json:"downloaded"`
Downloading string `json:"downloading"`
} `json:"actionText"`
Type string `json:"type"`
PriceFormatted string `json:"priceFormatted"`
Price float64 `json:"price"`
BuyParams string `json:"buyParams"`
Variant string `json:"variant,omitempty"`
Assets []struct {
Flavor string `json:"flavor"`
Preview struct {
Duration int `json:"duration"`
URL string `json:"url"`
} `json:"preview"`
Size int `json:"size"`
Duration int `json:"duration"`
} `json:"assets"`
} `json:"offers"`
}
type TrackData struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
Previews []struct {
URL string `json:"url"`
} `json:"previews"`
Artwork struct {
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url"`
BgColor string `json:"bgColor"`
TextColor1 string `json:"textColor1"`
TextColor2 string `json:"textColor2"`
TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"`
} `json:"artwork"`
ArtistName string `json:"artistName"`
URL string `json:"url"`
DiscNumber int `json:"discNumber"`
GenreNames []string `json:"genreNames"`
HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"`
IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"`
DurationInMillis int `json:"durationInMillis"`
ReleaseDate string `json:"releaseDate"`
Name string `json:"name"`
Isrc string `json:"isrc"`
AudioTraits []string `json:"audioTraits"`
HasLyrics bool `json:"hasLyrics"`
AlbumName string `json:"albumName"`
PlayParams struct {
ID string `json:"id"`
Kind string `json:"kind"`
} `json:"playParams"`
TrackNumber int `json:"trackNumber"`
AudioLocale string `json:"audioLocale"`
ComposerName string `json:"composerName"`
} `json:"attributes"`
Relationships struct {
Artists struct {
Href string `json:"href"`
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
Name string `json:"name"`
} `json:"attributes"`
} `json:"data"`
} `json:"artists"`
Albums struct {
Href string `json:"href"`
Data []AlbumData `json:"data"`
}
} `json:"relationships"`
}
type AlbumData struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
ArtistName string `json:"artistName"`
Artwork struct {
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url"`
BgColor string `json:"bgColor"`
TextColor1 string `json:"textColor1"`
TextColor2 string `json:"textColor2"`
TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"`
} `json:"artwork"`
GenreNames []string `json:"genreNames"`
IsCompilation bool `json:"isCompilation"`
IsComplete bool `json:"isComplete"`
IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsPrerelease bool `json:"isPrerelease"`
IsSingle bool `json:"isSingle"`
Name string `json:"name"`
PlayParams struct {
ID string `json:"id"`
Kind string `json:"kind"`
} `json:"playParams"`
ReleaseDate string `json:"releaseDate"`
TrackCount int `json:"trackCount"`
Upc string `json:"upc"`
URL string `json:"url"`
}
}
type AutoGenerated struct {
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
Artwork struct {
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url"`
BgColor string `json:"bgColor"`
TextColor1 string `json:"textColor1"`
TextColor2 string `json:"textColor2"`
TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"`
} `json:"artwork"`
ArtistName string `json:"artistName"`
IsSingle bool `json:"isSingle"`
URL string `json:"url"`
IsComplete bool `json:"isComplete"`
GenreNames []string `json:"genreNames"`
TrackCount int `json:"trackCount"`
IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"`
Name string `json:"name"`
RecordLabel string `json:"recordLabel"`
Upc string `json:"upc"`
AudioTraits []string `json:"audioTraits"`
Copyright string `json:"copyright"`
PlayParams struct {
ID string `json:"id"`
Kind string `json:"kind"`
} `json:"playParams"`
IsCompilation bool `json:"isCompilation"`
EditorialVideo struct {
MotionTall struct {
Video string `json:"video"`
} `json:"motionTallVideo3x4"`
MotionSquare struct {
Video string `json:"video"`
} `json:"motionSquareVideo1x1"`
MotionDetailTall struct {
Video string `json:"video"`
} `json:"motionDetailTall"`
MotionDetailSquare struct {
Video string `json:"video"`
} `json:"motionDetailSquare"`
} `json:"editorialVideo"`
} `json:"attributes"`
Relationships struct {
RecordLabels struct {
Href string `json:"href"`
Data []interface{} `json:"data"`
} `json:"record-labels"`
Artists struct {
Href string `json:"href"`
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
Name string `json:"name"`
Artwork struct {
Url string `json:"url"`
} `json:"artwork"`
} `json:"attributes"`
} `json:"data"`
} `json:"artists"`
Tracks struct {
Href string `json:"href"`
Next string `json:"next"`
Data []TrackData `json:"data"`
} `json:"tracks"`
} `json:"relationships"`
} `json:"data"`
}
type AutoGeneratedTrack struct {
Href string `json:"href"`
Next string `json:"next"`
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
Previews []struct {
URL string `json:"url"`
} `json:"previews"`
Artwork struct {
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url"`
BgColor string `json:"bgColor"`
TextColor1 string `json:"textColor1"`
TextColor2 string `json:"textColor2"`
TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"`
} `json:"artwork"`
ArtistName string `json:"artistName"`
URL string `json:"url"`
DiscNumber int `json:"discNumber"`
GenreNames []string `json:"genreNames"`
HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"`
IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"`
DurationInMillis int `json:"durationInMillis"`
ReleaseDate string `json:"releaseDate"`
Name string `json:"name"`
Isrc string `json:"isrc"`
AudioTraits []string `json:"audioTraits"`
HasLyrics bool `json:"hasLyrics"`
AlbumName string `json:"albumName"`
PlayParams struct {
ID string `json:"id"`
Kind string `json:"kind"`
} `json:"playParams"`
TrackNumber int `json:"trackNumber"`
AudioLocale string `json:"audioLocale"`
ComposerName string `json:"composerName"`
} `json:"attributes"`
Relationships struct {
Artists struct {
Href string `json:"href"`
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
Name string `json:"name"`
} `json:"attributes"`
} `json:"data"`
} `json:"artists"`
Albums struct {
Href string `json:"href"`
Data []AlbumData `json:"data"`
}
} `json:"relationships"`
} `json:"data"`
}
// 艺术家页面
type AutoGeneratedArtist struct {
Next string `json:"next"`
Data []struct {
@@ -465,59 +94,3 @@ type AutoGeneratedArtist struct {
} `json:"attributes"`
} `json:"data"`
}
type AutoGeneratedMusicVideo struct {
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
Previews []struct {
URL string `json:"url"`
} `json:"previews"`
Artwork struct {
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url"`
BgColor string `json:"bgColor"`
TextColor1 string `json:"textColor1"`
TextColor2 string `json:"textColor2"`
TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"`
} `json:"artwork"`
AlbumName string `json:"albumName"`
ArtistName string `json:"artistName"`
URL string `json:"url"`
GenreNames []string `json:"genreNames"`
DurationInMillis int `json:"durationInMillis"`
Isrc string `json:"isrc"`
TrackNumber int `json:"trackNumber"`
DiscNumber int `json:"discNumber"`
ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"`
Name string `json:"name"`
Has4K bool `json:"has4K"`
HasHDR bool `json:"hasHDR"`
PlayParams struct {
ID string `json:"id"`
Kind string `json:"kind"`
} `json:"playParams"`
} `json:"attributes"`
} `json:"data"`
}
type SongLyrics struct {
Data []struct {
Id string `json:"id"`
Type string `json:"type"`
Attributes struct {
Ttml string `json:"ttml"`
PlayParams struct {
Id string `json:"id"`
Kind string `json:"kind"`
CatalogId string `json:"catalogId"`
DisplayType int `json:"displayType"`
} `json:"playParams"`
} `json:"attributes"`
} `json:"data"`
}

193
utils/task/album.go Normal file
View 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
View 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
View 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
View 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
}