feat: lrc lyrics download

This commit is contained in:
WorldObservationLog
2024-04-24 01:02:04 +08:00
parent 712d548a63
commit d331a9d10a
6 changed files with 417 additions and 52 deletions

View File

@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/beevik/etree"
"io"
"io/ioutil"
"math"
@@ -17,9 +18,9 @@ import (
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"
"strconv"
"github.com/abema/go-mp4"
"github.com/grafov/m3u8"
@@ -916,7 +917,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 {
@@ -932,7 +933,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
}
@@ -962,11 +963,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 {
@@ -991,7 +992,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
}
}
@@ -1000,6 +1001,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)
@@ -1036,7 +1058,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")
@@ -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, "_"))
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)
m4atrackPath := filepath.Join(sanAlbumFolder, m4afilename)
exists, err := fileExists(trackPath)
@@ -1090,6 +1127,22 @@ func rip(albumId string, token string, storefront string) error {
oktrackNum += 1
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)
if err != nil {
fmt.Println("Failed to extract info from manifest.\n", err)
@@ -1133,9 +1186,9 @@ func rip(albumId string, token string, storefront string) error {
fmt.Sprintf("UPC=%s", meta.Data[0].Attributes.Upc),
fmt.Sprintf("track=%d/%d", trackNum, trackTotal),
}
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))
if err := cmd.Run(); err != nil {
fmt.Printf("Error encapsulating file: %v\n", err)
@@ -1153,8 +1206,14 @@ func rip(albumId string, token string, storefront string) error {
return err
}
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.")
@@ -1173,7 +1232,7 @@ func main() {
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)
@@ -1182,6 +1241,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 {
@@ -1290,7 +1391,7 @@ func extractSong(url string) (*SongInfo, error) {
// }
extracted := &SongInfo{
r: f,
r: f,
// alacParam: aalac[0].Payload.(*Alac),
}
@@ -1814,4 +1915,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"`
}