|
|
@ -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"` |
|
|
|
|
|
|
|
} |
|
|
|