|
|
|
@ -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
|
|
|
|
@ -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 |
|
|
|
@ -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.") |
|
|
|
@ -1135,7 +1190,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) |
|
|
|
@ -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 { |
|
|
|
@ -1778,3 +1875,19 @@ type AutoGeneratedTrack struct { |
|
|
|
|
} `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"` |
|
|
|
|
} |
|
|
|
|