mirror of
https://github.com/zhaarey/apple-music-downloader.git
synced 2025-10-23 15:11:05 +00:00
feat: lrc lyrics download
This commit is contained in:
149
main.go
149
main.go
@@ -16,11 +16,12 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"strconv"
|
||||
|
||||
"github.com/abema/go-mp4"
|
||||
"github.com/beevik/etree"
|
||||
"github.com/grafov/m3u8"
|
||||
)
|
||||
|
||||
@@ -717,7 +718,6 @@ func writeM4a(w *mp4.Writer, info *SongInfo, meta *AutoGenerated, data []byte, t
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
// plID, err := strconv.ParseUint(album.ID, 10, 32)
|
||||
// if err != nil {
|
||||
// return err
|
||||
@@ -900,7 +900,7 @@ func decryptSong(info *SongInfo, keys []string, manifest *AutoGenerated, filenam
|
||||
func checkUrl(url string) (string, string) {
|
||||
pat := regexp.MustCompile(`^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w{2})(?:\/album|\/album\/.+))\/(?:id)?(\d[^\D]+)(?:$|\?)`)
|
||||
matches := pat.FindAllStringSubmatch(url, -1)
|
||||
|
||||
|
||||
if matches == nil {
|
||||
return "", ""
|
||||
} else {
|
||||
@@ -910,7 +910,7 @@ func checkUrl(url string) (string, string) {
|
||||
func checkUrlPlaylist(url string) (string, string) {
|
||||
pat := regexp.MustCompile(`^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w{2})(?:\/playlist|\/playlist\/.+))\/(?:id)?(pl\.[\w-]+)(?:$|\?)`)
|
||||
matches := pat.FindAllStringSubmatch(url, -1)
|
||||
|
||||
|
||||
if matches == nil {
|
||||
return "", ""
|
||||
} else {
|
||||
@@ -918,8 +918,6 @@ func checkUrlPlaylist(url string) (string, string) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func getMeta(albumId string, token string, storefront string) (*AutoGenerated, error) {
|
||||
var mtype string
|
||||
var page int
|
||||
@@ -928,7 +926,7 @@ func getMeta(albumId string, token string, storefront string) (*AutoGenerated, e
|
||||
} else {
|
||||
mtype = "albums"
|
||||
}
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s", storefront,mtype, albumId), nil)
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -958,11 +956,11 @@ func getMeta(albumId string, token string, storefront string) (*AutoGenerated, e
|
||||
return nil, err
|
||||
}
|
||||
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 {
|
||||
page=0
|
||||
for{
|
||||
page=page+100
|
||||
page = 0
|
||||
for {
|
||||
page = page + 100
|
||||
pageStr := strconv.Itoa(page)
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s/tracks?offset=%s", storefront, mtype, albumId, pageStr), nil)
|
||||
if err != nil {
|
||||
@@ -987,7 +985,7 @@ func getMeta(albumId string, token string, storefront string) (*AutoGenerated, e
|
||||
for _, value := range obj2.Data {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -996,6 +994,27 @@ func getMeta(albumId string, token string, storefront string) (*AutoGenerated, e
|
||||
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 {
|
||||
covPath := filepath.Join(sanAlbumFolder, "cover.jpg")
|
||||
exists, err := fileExists(covPath)
|
||||
@@ -1032,9 +1051,21 @@ func writeCover(sanAlbumFolder, url string) error {
|
||||
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)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to get album metadata.\n")
|
||||
@@ -1072,7 +1103,24 @@ func rip(albumId string, token string, storefront string) error {
|
||||
continue
|
||||
}
|
||||
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)
|
||||
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)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to check if track exists.")
|
||||
@@ -1105,7 +1153,7 @@ func rip(albumId string, token string, storefront string) error {
|
||||
if !samplesOk {
|
||||
continue
|
||||
}
|
||||
err = decryptSong(info, keys, meta, trackPath, trackNum, trackTotal)
|
||||
// err = decryptSong(info, keys, meta, trackPath, trackNum, trackTotal)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to decrypt track.\n", err)
|
||||
continue
|
||||
@@ -1116,6 +1164,13 @@ func rip(albumId string, token string, storefront string) error {
|
||||
}
|
||||
|
||||
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()
|
||||
if err != nil {
|
||||
fmt.Println("Failed to get token.")
|
||||
@@ -1130,12 +1185,12 @@ func main() {
|
||||
} else {
|
||||
storefront, albumId = checkUrl(url)
|
||||
}
|
||||
|
||||
|
||||
if albumId == "" {
|
||||
fmt.Printf("Invalid URL: %s\n", url)
|
||||
continue
|
||||
}
|
||||
err := rip(albumId, token, storefront)
|
||||
err = rip(albumId, token, storefront, mediaUserToken)
|
||||
if err != nil {
|
||||
fmt.Println("Album failed.")
|
||||
fmt.Println(err)
|
||||
@@ -1144,6 +1199,48 @@ func main() {
|
||||
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) {
|
||||
masterUrl, err := url.Parse(b)
|
||||
if err != nil {
|
||||
@@ -1777,4 +1874,20 @@ type AutoGeneratedTrack struct {
|
||||
} `json:"artists"`
|
||||
} `json:"relationships"`
|
||||
} `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"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user