feat: lrc lyrics download

pull/2/head
WorldObservationLog 5 months ago
parent 712d548a63
commit d331a9d10a
  1. 9
      README.md
  2. 5
      go.mod
  3. 8
      go.sum
  4. 141
      main.go
  5. 139
      main_atmos.go
  6. 143
      main_select.go

@ -4,6 +4,7 @@
1. 调用外部MP4Box自动封装ec3为m4a 1. 调用外部MP4Box自动封装ec3为m4a
2. 更改目录结构为 歌手名\专辑名 ;Atmos下载文件则另外移动到AM-DL-Atmos downloads,并更改目录结构为 歌手名\专辑名 [Atmos] 2. 更改目录结构为 歌手名\专辑名 ;Atmos下载文件则另外移动到AM-DL-Atmos downloads,并更改目录结构为 歌手名\专辑名 [Atmos]
3. 运行结束后显示总体完成情况 3. 运行结束后显示总体完成情况
4. 下载LRC歌词
@ -24,3 +25,11 @@ Original script by Sorrow. Modified by me to include some fixes and improvements
10. For dolby atmos: `go run main_atmos.go https://music.apple.com/us/album/1989-taylors-version-deluxe/1713845538`. 10. For dolby atmos: `go run main_atmos.go https://music.apple.com/us/album/1989-taylors-version-deluxe/1713845538`.
[中文教程-详见方法三](https://telegra.ph/Apple-Music-Alac高解析度无损音乐下载教程-04-02-2) [中文教程-详见方法三](https://telegra.ph/Apple-Music-Alac高解析度无损音乐下载教程-04-02-2)
## Downloading lyrics
1. Open [Apple Music](https://music.apple.com) and log in
2. Open the Developer tools, Click `Application -> Storage -> Cookies -> https://music.apple.com`
3. Find the cookie named `media-user-token` and copy its value
4. Create a file named `media-user-token.txt` in the project root directory
5. Paste the cookie value obtained in step 3 into the file and save it
6. Start the script as usual

@ -7,4 +7,7 @@ require (
github.com/grafov/m3u8 v0.11.1 github.com/grafov/m3u8 v0.11.1
) )
require github.com/google/uuid v1.1.2 // indirect require (
github.com/beevik/etree v1.3.0 // indirect
github.com/google/uuid v1.1.2 // indirect
)

@ -1,7 +1,10 @@
github.com/abema/go-mp4 v0.7.2 h1:ugTC8gfEmjyaDKpXs3vi2QzgJbDu9B8m6UMMIpbYbGg= github.com/abema/go-mp4 v0.7.2 h1:ugTC8gfEmjyaDKpXs3vi2QzgJbDu9B8m6UMMIpbYbGg=
github.com/abema/go-mp4 v0.7.2/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws= github.com/abema/go-mp4 v0.7.2/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
github.com/beevik/etree v1.3.0 h1:hQTc+pylzIKDb23yYprodCWWTt+ojFfUZyzU09a/hmU=
github.com/beevik/etree v1.3.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -11,9 +14,12 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw=
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0= github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I= github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -21,6 +27,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

@ -16,11 +16,12 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort" "sort"
"strconv"
"strings" "strings"
"time" "time"
"strconv"
"github.com/abema/go-mp4" "github.com/abema/go-mp4"
"github.com/beevik/etree"
"github.com/grafov/m3u8" "github.com/grafov/m3u8"
) )
@ -717,7 +718,6 @@ func writeM4a(w *mp4.Writer, info *SongInfo, meta *AutoGenerated, data []byte, t
return err return err
} }
// plID, err := strconv.ParseUint(album.ID, 10, 32) // plID, err := strconv.ParseUint(album.ID, 10, 32)
// if err != nil { // if err != nil {
// return err // return err
@ -918,8 +918,6 @@ func checkUrlPlaylist(url string) (string, string) {
} }
} }
func getMeta(albumId string, token string, storefront string) (*AutoGenerated, error) { func getMeta(albumId string, token string, storefront string) (*AutoGenerated, error) {
var mtype string var mtype string
var page int var page int
@ -928,7 +926,7 @@ func getMeta(albumId string, token string, storefront string) (*AutoGenerated, e
} else { } else {
mtype = "albums" mtype = "albums"
} }
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s", storefront,mtype, albumId), nil) req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s", storefront, mtype, albumId), nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -958,11 +956,11 @@ func getMeta(albumId string, token string, storefront string) (*AutoGenerated, e
return nil, err return nil, err
} }
if strings.Contains(albumId, "pl.") { if strings.Contains(albumId, "pl.") {
obj.Data[0].Attributes.ArtistName="Apple Music" obj.Data[0].Attributes.ArtistName = "Apple Music"
if len(obj.Data[0].Relationships.Tracks.Next) > 0 { if len(obj.Data[0].Relationships.Tracks.Next) > 0 {
page=0 page = 0
for{ for {
page=page+100 page = page + 100
pageStr := strconv.Itoa(page) pageStr := strconv.Itoa(page)
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s/tracks?offset=%s", storefront, mtype, albumId, pageStr), nil) req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s/tracks?offset=%s", storefront, mtype, albumId, pageStr), nil)
if err != nil { if err != nil {
@ -987,7 +985,7 @@ func getMeta(albumId string, token string, storefront string) (*AutoGenerated, e
for _, value := range obj2.Data { for _, value := range obj2.Data {
obj.Data[0].Relationships.Tracks.Data = append(obj.Data[0].Relationships.Tracks.Data, value) obj.Data[0].Relationships.Tracks.Data = append(obj.Data[0].Relationships.Tracks.Data, value)
} }
if len(obj2.Next)==0{ if len(obj2.Next) == 0 {
break break
} }
} }
@ -996,6 +994,27 @@ func getMeta(albumId string, token string, storefront string) (*AutoGenerated, e
return obj, nil return obj, nil
} }
func getSongLyrics(songId string, storefront string, token string, userToken string) (string, error) {
req, err := http.NewRequest("GET",
fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s/lyrics", storefront, songId), nil)
if err != nil {
return "", err
}
req.Header.Set("Origin", "https://music.apple.com")
req.Header.Set("Referer", "https://music.apple.com/")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
cookie := http.Cookie{Name: "media-user-token", Value: userToken}
req.AddCookie(&cookie)
do, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer do.Body.Close()
obj := new(SongLyrics)
err = json.NewDecoder(do.Body).Decode(&obj)
return obj.Data[0].Attributes.Ttml, nil
}
func writeCover(sanAlbumFolder, url string) error { func writeCover(sanAlbumFolder, url string) error {
covPath := filepath.Join(sanAlbumFolder, "cover.jpg") covPath := filepath.Join(sanAlbumFolder, "cover.jpg")
exists, err := fileExists(covPath) exists, err := fileExists(covPath)
@ -1032,9 +1051,21 @@ func writeCover(sanAlbumFolder, url string) error {
return nil return nil
} }
func rip(albumId string, token string, storefront string) error { func writeLyrics(sanAlbumFolder, filename string, lrc string) error {
lyricspath := filepath.Join(sanAlbumFolder, filename)
f, err := os.Create(lyricspath)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(lrc)
if err != nil {
return err
}
return nil
}
func rip(albumId string, token string, storefront string, userToken string) error {
meta, err := getMeta(albumId, token, storefront) meta, err := getMeta(albumId, token, storefront)
if err != nil { if err != nil {
fmt.Println("Failed to get album metadata.\n") fmt.Println("Failed to get album metadata.\n")
@ -1072,7 +1103,24 @@ func rip(albumId string, token string, storefront string) error {
continue continue
} }
filename := fmt.Sprintf("%02d. %s.m4a", trackNum, forbiddenNames.ReplaceAllString(track.Attributes.Name, "_")) filename := fmt.Sprintf("%02d. %s.m4a", trackNum, forbiddenNames.ReplaceAllString(track.Attributes.Name, "_"))
lrcFilename := fmt.Sprintf("%02d. %s.lrc", trackNum, forbiddenNames.ReplaceAllString(track.Attributes.Name, "_"))
trackPath := filepath.Join(sanAlbumFolder, filename) trackPath := filepath.Join(sanAlbumFolder, filename)
if userToken != "" {
ttml, err := getSongLyrics(track.ID, storefront, token, userToken)
if err != nil {
fmt.Println("Failed to get lyrics")
} else {
lrc, err := conventTTMLToLRC(ttml)
if err != nil {
fmt.Printf("Failed to parse lyrics: %s \n", err)
} else {
err := writeLyrics(sanAlbumFolder, lrcFilename, lrc)
if err != nil {
fmt.Printf("Failed to write lyrics")
}
}
}
}
exists, err := fileExists(trackPath) exists, err := fileExists(trackPath)
if err != nil { if err != nil {
fmt.Println("Failed to check if track exists.") fmt.Println("Failed to check if track exists.")
@ -1105,7 +1153,7 @@ func rip(albumId string, token string, storefront string) error {
if !samplesOk { if !samplesOk {
continue continue
} }
err = decryptSong(info, keys, meta, trackPath, trackNum, trackTotal) // err = decryptSong(info, keys, meta, trackPath, trackNum, trackTotal)
if err != nil { if err != nil {
fmt.Println("Failed to decrypt track.\n", err) fmt.Println("Failed to decrypt track.\n", err)
continue continue
@ -1116,6 +1164,13 @@ func rip(albumId string, token string, storefront string) error {
} }
func main() { func main() {
var mediaUserToken string
if _, err := os.Stat("media-user-token.txt"); err == nil {
file, err := os.ReadFile("media-user-token.txt")
if err == nil && file != nil {
mediaUserToken = string(file)
}
}
token, err := getToken() token, err := getToken()
if err != nil { if err != nil {
fmt.Println("Failed to get token.") fmt.Println("Failed to get token.")
@ -1135,7 +1190,7 @@ func main() {
fmt.Printf("Invalid URL: %s\n", url) fmt.Printf("Invalid URL: %s\n", url)
continue continue
} }
err := rip(albumId, token, storefront) err = rip(albumId, token, storefront, mediaUserToken)
if err != nil { if err != nil {
fmt.Println("Album failed.") fmt.Println("Album failed.")
fmt.Println(err) fmt.Println(err)
@ -1144,6 +1199,48 @@ func main() {
fmt.Printf("======= Completed %d/%d ###### %d errors!! =======\n", oktrackNum, trackTotalnum, trackTotalnum-oktrackNum) fmt.Printf("======= Completed %d/%d ###### %d errors!! =======\n", oktrackNum, trackTotalnum, trackTotalnum-oktrackNum)
} }
func conventTTMLToLRC(ttml string) (string, error) {
parsedTTML := etree.NewDocument()
err := parsedTTML.ReadFromString(ttml)
if err != nil {
return "", err
}
var lrcLines []string
for _, item := range parsedTTML.FindElement("tt").FindElement("body").ChildElements() {
for _, lyric := range item.ChildElements() {
var m, s, ms int
if lyric.SelectAttr("begin") == nil {
return "", errors.New("no synchronised lyrics")
}
if strings.Contains(lyric.SelectAttr("begin").Value, ":") {
_, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d.%d", &m, &s, &ms)
} else {
_, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d.%d", &s, &ms)
m = 0
}
if err != nil {
return "", err
}
var text string
if lyric.SelectAttr("text") == nil {
var textTmp []string
for _, span := range lyric.Child {
if _, ok := span.(*etree.CharData); ok {
textTmp = append(textTmp, span.(*etree.CharData).Data)
} else {
textTmp = append(textTmp, span.(*etree.Element).Text())
}
}
text = strings.Join(textTmp, "")
} else {
text = lyric.SelectAttr("text").Value
}
lrcLines = append(lrcLines, fmt.Sprintf("[%02d:%02d.%03d]%s", m, s, ms, text))
}
}
return strings.Join(lrcLines, "\n"), nil
}
func extractMedia(b string) (string, []string, error) { func extractMedia(b string) (string, []string, error) {
masterUrl, err := url.Parse(b) masterUrl, err := url.Parse(b)
if err != nil { if err != nil {
@ -1778,3 +1875,19 @@ type AutoGeneratedTrack struct {
} `json:"relationships"` } `json:"relationships"`
} `json:"data"` } `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"`
}

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/beevik/etree"
"io" "io"
"io/ioutil" "io/ioutil"
"math" "math"
@ -17,9 +18,9 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort" "sort"
"strconv"
"strings" "strings"
"time" "time"
"strconv"
"github.com/abema/go-mp4" "github.com/abema/go-mp4"
"github.com/grafov/m3u8" "github.com/grafov/m3u8"
@ -932,7 +933,7 @@ func getMeta(albumId string, token string, storefront string) (*AutoGenerated, e
} else { } else {
mtype = "albums" mtype = "albums"
} }
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s", storefront,mtype, albumId), nil) req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s", storefront, mtype, albumId), nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -962,11 +963,11 @@ func getMeta(albumId string, token string, storefront string) (*AutoGenerated, e
return nil, err return nil, err
} }
if strings.Contains(albumId, "pl.") { if strings.Contains(albumId, "pl.") {
obj.Data[0].Attributes.ArtistName="Apple Music" obj.Data[0].Attributes.ArtistName = "Apple Music"
if len(obj.Data[0].Relationships.Tracks.Next) > 0 { if len(obj.Data[0].Relationships.Tracks.Next) > 0 {
page=0 page = 0
for{ for {
page=page+100 page = page + 100
pageStr := strconv.Itoa(page) pageStr := strconv.Itoa(page)
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s/tracks?offset=%s", storefront, mtype, albumId, pageStr), nil) req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s/tracks?offset=%s", storefront, mtype, albumId, pageStr), nil)
if err != nil { if err != nil {
@ -991,7 +992,7 @@ func getMeta(albumId string, token string, storefront string) (*AutoGenerated, e
for _, value := range obj2.Data { for _, value := range obj2.Data {
obj.Data[0].Relationships.Tracks.Data = append(obj.Data[0].Relationships.Tracks.Data, value) obj.Data[0].Relationships.Tracks.Data = append(obj.Data[0].Relationships.Tracks.Data, value)
} }
if len(obj2.Next)==0{ if len(obj2.Next) == 0 {
break break
} }
} }
@ -1000,6 +1001,27 @@ func getMeta(albumId string, token string, storefront string) (*AutoGenerated, e
return obj, nil return obj, nil
} }
func getSongLyrics(songId string, storefront string, token string, userToken string) (string, error) {
req, err := http.NewRequest("GET",
fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s/lyrics", storefront, songId), nil)
if err != nil {
return "", err
}
req.Header.Set("Origin", "https://music.apple.com")
req.Header.Set("Referer", "https://music.apple.com/")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
cookie := http.Cookie{Name: "media-user-token", Value: userToken}
req.AddCookie(&cookie)
do, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer do.Body.Close()
obj := new(SongLyrics)
err = json.NewDecoder(do.Body).Decode(&obj)
return obj.Data[0].Attributes.Ttml, nil
}
func writeCover(sanAlbumFolder, url string) error { func writeCover(sanAlbumFolder, url string) error {
covPath := filepath.Join(sanAlbumFolder, "cover.jpg") covPath := filepath.Join(sanAlbumFolder, "cover.jpg")
exists, err := fileExists(covPath) exists, err := fileExists(covPath)
@ -1036,7 +1058,21 @@ func writeCover(sanAlbumFolder, url string) error {
return nil return nil
} }
func rip(albumId string, token string, storefront string) error { func writeLyrics(sanAlbumFolder, filename string, lrc string) error {
lyricspath := filepath.Join(sanAlbumFolder, filename)
f, err := os.Create(lyricspath)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(lrc)
if err != nil {
return err
}
return nil
}
func rip(albumId string, token string, storefront string, userToken string) error {
meta, err := getMeta(albumId, token, storefront) meta, err := getMeta(albumId, token, storefront)
if err != nil { if err != nil {
fmt.Println("Failed to get album metadata.\n") fmt.Println("Failed to get album metadata.\n")
@ -1075,6 +1111,7 @@ func rip(albumId string, token string, storefront string) error {
} }
filename := fmt.Sprintf("%02d. %s.ec3", trackNum, forbiddenNames.ReplaceAllString(track.Attributes.Name, "_")) filename := fmt.Sprintf("%02d. %s.ec3", trackNum, forbiddenNames.ReplaceAllString(track.Attributes.Name, "_"))
m4afilename := fmt.Sprintf("%02d. %s.m4a", trackNum, forbiddenNames.ReplaceAllString(track.Attributes.Name, "_")) m4afilename := fmt.Sprintf("%02d. %s.m4a", trackNum, forbiddenNames.ReplaceAllString(track.Attributes.Name, "_"))
lrcFilename := fmt.Sprintf("%02d. %s.lrc", trackNum, forbiddenNames.ReplaceAllString(track.Attributes.Name, "_"))
trackPath := filepath.Join(sanAlbumFolder, filename) trackPath := filepath.Join(sanAlbumFolder, filename)
m4atrackPath := filepath.Join(sanAlbumFolder, m4afilename) m4atrackPath := filepath.Join(sanAlbumFolder, m4afilename)
exists, err := fileExists(trackPath) exists, err := fileExists(trackPath)
@ -1090,6 +1127,22 @@ func rip(albumId string, token string, storefront string) error {
oktrackNum += 1 oktrackNum += 1
continue continue
} }
if userToken != "" {
ttml, err := getSongLyrics(track.ID, storefront, token, userToken)
if err != nil {
fmt.Println("Failed to get lyrics")
} else {
lrc, err := conventTTMLToLRC(ttml)
if err != nil {
fmt.Printf("Failed to parse lyrics: %s \n", err)
} else {
err := writeLyrics(sanAlbumFolder, lrcFilename, lrc)
if err != nil {
fmt.Printf("Failed to write lyrics")
}
}
}
}
trackUrl, keys, err := extractMedia(manifest.Attributes.ExtendedAssetUrls.EnhancedHls) trackUrl, keys, err := extractMedia(manifest.Attributes.ExtendedAssetUrls.EnhancedHls)
if err != nil { if err != nil {
fmt.Println("Failed to extract info from manifest.\n", err) fmt.Println("Failed to extract info from manifest.\n", err)
@ -1135,7 +1188,7 @@ func rip(albumId string, token string, storefront string) error {
} }
tagsString := strings.Join(tags, ":") tagsString := strings.Join(tags, ":")
cmd := exec.Command("MP4Box", "-add", trackPath,"-name",fmt.Sprintf("1=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.Name),"-itags",tagsString, "-brand", "mp42", "-ab", "dby1", m4atrackPath) cmd := exec.Command("MP4Box", "-add", trackPath, "-name", fmt.Sprintf("1=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.Name), "-itags", tagsString, "-brand", "mp42", "-ab", "dby1", m4atrackPath)
fmt.Printf("Encapsulating %s into %s\n", filepath.Base(trackPath), filepath.Base(m4atrackPath)) fmt.Printf("Encapsulating %s into %s\n", filepath.Base(trackPath), filepath.Base(m4atrackPath))
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
fmt.Printf("Error encapsulating file: %v\n", err) fmt.Printf("Error encapsulating file: %v\n", err)
@ -1153,8 +1206,14 @@ func rip(albumId string, token string, storefront string) error {
return err return err
} }
func main() { func main() {
var mediaUserToken string
if _, err := os.Stat("media-user-token.txt"); err == nil {
file, err := os.ReadFile("media-user-token.txt")
if err == nil && file != nil {
mediaUserToken = string(file)
}
}
token, err := getToken() token, err := getToken()
if err != nil { if err != nil {
fmt.Println("Failed to get token.") fmt.Println("Failed to get token.")
@ -1173,7 +1232,7 @@ func main() {
fmt.Printf("Invalid URL: %s\n", url) fmt.Printf("Invalid URL: %s\n", url)
continue continue
} }
err := rip(albumId, token, storefront) err := rip(albumId, token, storefront, mediaUserToken)
if err != nil { if err != nil {
fmt.Println("Album failed.") fmt.Println("Album failed.")
fmt.Println(err) fmt.Println(err)
@ -1182,6 +1241,48 @@ func main() {
fmt.Printf("======= Completed %d/%d ###### %d errors!! =======\n", oktrackNum, trackTotalnum, trackTotalnum-oktrackNum) fmt.Printf("======= Completed %d/%d ###### %d errors!! =======\n", oktrackNum, trackTotalnum, trackTotalnum-oktrackNum)
} }
func conventTTMLToLRC(ttml string) (string, error) {
parsedTTML := etree.NewDocument()
err := parsedTTML.ReadFromString(ttml)
if err != nil {
return "", err
}
var lrcLines []string
for _, item := range parsedTTML.FindElement("tt").FindElement("body").ChildElements() {
for _, lyric := range item.ChildElements() {
var m, s, ms int
if lyric.SelectAttr("begin") == nil {
return "", errors.New("no synchronised lyrics")
}
if strings.Contains(lyric.SelectAttr("begin").Value, ":") {
_, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d.%d", &m, &s, &ms)
} else {
_, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d.%d", &s, &ms)
m = 0
}
if err != nil {
return "", err
}
var text string
if lyric.SelectAttr("text") == nil {
var textTmp []string
for _, span := range lyric.Child {
if _, ok := span.(*etree.CharData); ok {
textTmp = append(textTmp, span.(*etree.CharData).Data)
} else {
textTmp = append(textTmp, span.(*etree.Element).Text())
}
}
text = strings.Join(textTmp, "")
} else {
text = lyric.SelectAttr("text").Value
}
lrcLines = append(lrcLines, fmt.Sprintf("[%02d:%02d.%03d]%s", m, s, ms, text))
}
}
return strings.Join(lrcLines, "\n"), nil
}
func extractMedia(b string) (string, []string, error) { func extractMedia(b string) (string, []string, error) {
masterUrl, err := url.Parse(b) masterUrl, err := url.Parse(b)
if err != nil { if err != nil {
@ -1815,3 +1916,19 @@ type AutoGeneratedTrack struct {
} `json:"relationships"` } `json:"relationships"`
} `json:"data"` } `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"`
}

@ -1,11 +1,13 @@
package main package main
import ( import (
"bufio"
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/beevik/etree"
"io" "io"
"io/ioutil" "io/ioutil"
"math" "math"
@ -16,10 +18,9 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort" "sort"
"strconv"
"strings" "strings"
"time" "time"
"bufio"
"strconv"
"github.com/abema/go-mp4" "github.com/abema/go-mp4"
"github.com/grafov/m3u8" "github.com/grafov/m3u8"
@ -716,7 +717,6 @@ func writeM4a(w *mp4.Writer, info *SongInfo, meta *AutoGenerated, data []byte, t
return err return err
} }
// plID, err := strconv.ParseUint(album.ID, 10, 32) // plID, err := strconv.ParseUint(album.ID, 10, 32)
// if err != nil { // if err != nil {
// return err // return err
@ -917,8 +917,6 @@ func checkUrlPlaylist(url string) (string, string) {
} }
} }
func getMeta(albumId string, token string, storefront string) (*AutoGenerated, error) { func getMeta(albumId string, token string, storefront string) (*AutoGenerated, error) {
var mtype string var mtype string
var page int var page int
@ -927,7 +925,7 @@ func getMeta(albumId string, token string, storefront string) (*AutoGenerated, e
} else { } else {
mtype = "albums" mtype = "albums"
} }
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s", storefront,mtype, albumId), nil) req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s", storefront, mtype, albumId), nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -957,11 +955,11 @@ func getMeta(albumId string, token string, storefront string) (*AutoGenerated, e
return nil, err return nil, err
} }
if strings.Contains(albumId, "pl.") { if strings.Contains(albumId, "pl.") {
obj.Data[0].Attributes.ArtistName="Apple Music" obj.Data[0].Attributes.ArtistName = "Apple Music"
if len(obj.Data[0].Relationships.Tracks.Next) > 0 { if len(obj.Data[0].Relationships.Tracks.Next) > 0 {
page=0 page = 0
for{ for {
page=page+100 page = page + 100
pageStr := strconv.Itoa(page) pageStr := strconv.Itoa(page)
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s/tracks?offset=%s", storefront, mtype, albumId, pageStr), nil) req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s/tracks?offset=%s", storefront, mtype, albumId, pageStr), nil)
if err != nil { if err != nil {
@ -986,7 +984,7 @@ func getMeta(albumId string, token string, storefront string) (*AutoGenerated, e
for _, value := range obj2.Data { for _, value := range obj2.Data {
obj.Data[0].Relationships.Tracks.Data = append(obj.Data[0].Relationships.Tracks.Data, value) obj.Data[0].Relationships.Tracks.Data = append(obj.Data[0].Relationships.Tracks.Data, value)
} }
if len(obj2.Next)==0{ if len(obj2.Next) == 0 {
break break
} }
} }
@ -995,6 +993,27 @@ func getMeta(albumId string, token string, storefront string) (*AutoGenerated, e
return obj, nil return obj, nil
} }
func getSongLyrics(songId string, storefront string, token string, userToken string) (string, error) {
req, err := http.NewRequest("GET",
fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s/lyrics", storefront, songId), nil)
if err != nil {
return "", err
}
req.Header.Set("Origin", "https://music.apple.com")
req.Header.Set("Referer", "https://music.apple.com/")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
cookie := http.Cookie{Name: "media-user-token", Value: userToken}
req.AddCookie(&cookie)
do, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer do.Body.Close()
obj := new(SongLyrics)
err = json.NewDecoder(do.Body).Decode(&obj)
return obj.Data[0].Attributes.Ttml, nil
}
func writeCover(sanAlbumFolder, url string) error { func writeCover(sanAlbumFolder, url string) error {
covPath := filepath.Join(sanAlbumFolder, "cover.jpg") covPath := filepath.Join(sanAlbumFolder, "cover.jpg")
exists, err := fileExists(covPath) exists, err := fileExists(covPath)
@ -1030,6 +1049,21 @@ func writeCover(sanAlbumFolder, url string) error {
} }
return nil return nil
} }
func writeLyrics(sanAlbumFolder, filename string, lrc string) error {
lyricspath := filepath.Join(sanAlbumFolder, filename)
f, err := os.Create(lyricspath)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(lrc)
if err != nil {
return err
}
return nil
}
func isInArray(arr []int, target int) bool { func isInArray(arr []int, target int) bool {
for _, num := range arr { for _, num := range arr {
if num == target { if num == target {
@ -1039,8 +1073,7 @@ func isInArray(arr []int, target int) bool {
return false return false
} }
func rip(albumId string, token string, storefront string) error { func rip(albumId string, token string, storefront string, userToken string) error {
meta, err := getMeta(albumId, token, storefront) meta, err := getMeta(albumId, token, storefront)
if err != nil { if err != nil {
@ -1106,7 +1139,24 @@ func rip(albumId string, token string, storefront string) error {
continue continue
} }
filename := fmt.Sprintf("%02d. %s.m4a", trackNum, forbiddenNames.ReplaceAllString(track.Attributes.Name, "_")) filename := fmt.Sprintf("%02d. %s.m4a", trackNum, forbiddenNames.ReplaceAllString(track.Attributes.Name, "_"))
lrcFilename := fmt.Sprintf("%02d. %s.lrc", trackNum, forbiddenNames.ReplaceAllString(track.Attributes.Name, "_"))
trackPath := filepath.Join(sanAlbumFolder, filename) trackPath := filepath.Join(sanAlbumFolder, filename)
if userToken != "" {
ttml, err := getSongLyrics(track.ID, storefront, token, userToken)
if err != nil {
fmt.Println("Failed to get lyrics")
} else {
lrc, err := conventTTMLToLRC(ttml)
if err != nil {
fmt.Printf("Failed to parse lyrics: %s \n", err)
} else {
err := writeLyrics(sanAlbumFolder, lrcFilename, lrc)
if err != nil {
fmt.Printf("Failed to write lyrics")
}
}
}
}
exists, err := fileExists(trackPath) exists, err := fileExists(trackPath)
if err != nil { if err != nil {
fmt.Println("Failed to check if track exists.") fmt.Println("Failed to check if track exists.")
@ -1149,6 +1199,13 @@ func rip(albumId string, token string, storefront string) error {
} }
func main() { func main() {
var mediaUserToken string
if _, err := os.Stat("media-user-token.txt"); err == nil {
file, err := os.ReadFile("media-user-token.txt")
if err == nil && file != nil {
mediaUserToken = string(file)
}
}
token, err := getToken() token, err := getToken()
if err != nil { if err != nil {
fmt.Println("Failed to get token.") fmt.Println("Failed to get token.")
@ -1168,7 +1225,7 @@ func main() {
fmt.Printf("Invalid URL: %s\n", url) fmt.Printf("Invalid URL: %s\n", url)
continue continue
} }
err := rip(albumId, token, storefront) err := rip(albumId, token, storefront, mediaUserToken)
if err != nil { if err != nil {
fmt.Println("Album failed.") fmt.Println("Album failed.")
fmt.Println(err) fmt.Println(err)
@ -1176,6 +1233,48 @@ func main() {
} }
} }
func conventTTMLToLRC(ttml string) (string, error) {
parsedTTML := etree.NewDocument()
err := parsedTTML.ReadFromString(ttml)
if err != nil {
return "", err
}
var lrcLines []string
for _, item := range parsedTTML.FindElement("tt").FindElement("body").ChildElements() {
for _, lyric := range item.ChildElements() {
var m, s, ms int
if lyric.SelectAttr("begin") == nil {
return "", errors.New("no synchronised lyrics")
}
if strings.Contains(lyric.SelectAttr("begin").Value, ":") {
_, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d.%d", &m, &s, &ms)
} else {
_, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d.%d", &s, &ms)
m = 0
}
if err != nil {
return "", err
}
var text string
if lyric.SelectAttr("text") == nil {
var textTmp []string
for _, span := range lyric.Child {
if _, ok := span.(*etree.CharData); ok {
textTmp = append(textTmp, span.(*etree.CharData).Data)
} else {
textTmp = append(textTmp, span.(*etree.Element).Text())
}
}
text = strings.Join(textTmp, "")
} else {
text = lyric.SelectAttr("text").Value
}
lrcLines = append(lrcLines, fmt.Sprintf("[%02d:%02d.%03d]%s", m, s, ms, text))
}
}
return strings.Join(lrcLines, "\n"), nil
}
func extractMedia(b string) (string, []string, error) { func extractMedia(b string) (string, []string, error) {
masterUrl, err := url.Parse(b) masterUrl, err := url.Parse(b)
if err != nil { if err != nil {
@ -1810,3 +1909,19 @@ type AutoGeneratedTrack struct {
} `json:"relationships"` } `json:"relationships"`
} `json:"data"` } `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"`
}

Loading…
Cancel
Save