commit
bd40e9f0a7
@ -31,6 +31,5 @@ Original script by Sorrow. Modified by me to include some fixes and improvements
|
|||||||
1. Open [Apple Music](https://music.apple.com) and log in
|
1. Open [Apple Music](https://music.apple.com) and log in
|
||||||
2. Open the Developer tools, Click `Application -> Storage -> Cookies -> https://music.apple.com`
|
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
|
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
|
4. Paste the cookie value obtained in step 3 into the config.yaml and save it
|
||||||
5. Paste the cookie value obtained in step 3 into the file and save it
|
5. Start the script as usual
|
||||||
6. Start the script as usual
|
|
||||||
|
8
config.yaml
Normal file
8
config.yaml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
media-user-token: "your-media-user-token"
|
||||||
|
embed-lrc: true
|
||||||
|
save-lrc-file: false
|
||||||
|
embed-cover: true
|
||||||
|
cover-size: 5000x5000
|
||||||
|
cover-format: jpg
|
||||||
|
alac-save-folder: AM-DL downloads
|
||||||
|
atmos-save-folder: AM-DL-Atmos downloads
|
1
go.mod
1
go.mod
@ -10,4 +10,5 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/beevik/etree v1.3.0 // indirect
|
github.com/beevik/etree v1.3.0 // indirect
|
||||||
github.com/google/uuid v1.1.2 // indirect
|
github.com/google/uuid v1.1.2 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.2.8
|
||||||
)
|
)
|
||||||
|
66
main.go
66
main.go
@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -34,6 +35,18 @@ const (
|
|||||||
var (
|
var (
|
||||||
forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`)
|
forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`)
|
||||||
)
|
)
|
||||||
|
type Config struct {
|
||||||
|
MediaUserToken string `yaml:"media-user-token"`
|
||||||
|
SaveLrcFile bool `yaml:"save-lrc-file"`
|
||||||
|
EmbedLrc bool `yaml:"embed-lrc"`
|
||||||
|
EmbedCover bool `yaml:"embed-cover"`
|
||||||
|
CoverSize string `yaml:"cover-size"`
|
||||||
|
CoverFormat string `yaml:"cover-format"`
|
||||||
|
AlacSaveFolder string `yaml:"alac-save-folder"`
|
||||||
|
AtmosSaveFolder string `yaml:"atmos-save-folder"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
var oktrackNum int = 0
|
var oktrackNum int = 0
|
||||||
var trackTotalnum int = 0
|
var trackTotalnum int = 0
|
||||||
|
|
||||||
@ -48,6 +61,19 @@ type SongInfo struct {
|
|||||||
alacParam *Alac
|
alacParam *Alac
|
||||||
samples []SampleInfo
|
samples []SampleInfo
|
||||||
}
|
}
|
||||||
|
func loadConfig() error {
|
||||||
|
// 读取config.yaml文件内容
|
||||||
|
data, err := ioutil.ReadFile("config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 将yaml解析到config变量中
|
||||||
|
err = yaml.Unmarshal(data, &config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SongInfo) Duration() (ret uint64) {
|
func (s *SongInfo) Duration() (ret uint64) {
|
||||||
for i := range s.samples {
|
for i := range s.samples {
|
||||||
@ -1021,7 +1047,7 @@ func getSongLyrics(songId string, storefront string, token string, userToken str
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeCover(sanAlbumFolder, url string) error {
|
func writeCover(sanAlbumFolder, url string) error {
|
||||||
covPath := filepath.Join(sanAlbumFolder, "cover.jpg")
|
covPath := filepath.Join(sanAlbumFolder, "cover." + config.CoverFormat)
|
||||||
exists, err := fileExists(covPath)
|
exists, err := fileExists(covPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Failed to check if cover exists.")
|
fmt.Println("Failed to check if cover exists.")
|
||||||
@ -1030,7 +1056,12 @@ func writeCover(sanAlbumFolder, url string) error {
|
|||||||
if exists {
|
if exists {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
url = strings.Replace(url, "{w}x{h}", "5000x5000", 1)
|
if config.CoverFormat == "png" {
|
||||||
|
re := regexp.MustCompile(`\{w\}x\{h\}`)
|
||||||
|
parts := re.Split(url, 2)
|
||||||
|
url = parts[0] + "{w}x{h}" + strings.Replace(parts[1], ".jpg", ".png", 1)
|
||||||
|
}
|
||||||
|
url = strings.Replace(url, "{w}x{h}", config.CoverSize, 1)
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -1081,7 +1112,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro
|
|||||||
singerFoldername = strings.ReplaceAll(singerFoldername, ".", "")
|
singerFoldername = strings.ReplaceAll(singerFoldername, ".", "")
|
||||||
}
|
}
|
||||||
singerFoldername = strings.TrimSpace(singerFoldername)
|
singerFoldername = strings.TrimSpace(singerFoldername)
|
||||||
singerFolder := filepath.Join("AM-DL downloads", forbiddenNames.ReplaceAllString(singerFoldername, "_"))
|
singerFolder := filepath.Join(config.AlacSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_"))
|
||||||
albumFolder := fmt.Sprintf("%s", meta.Data[0].Attributes.Name)
|
albumFolder := fmt.Sprintf("%s", meta.Data[0].Attributes.Name)
|
||||||
if strings.HasSuffix(albumFolder, ".") {
|
if strings.HasSuffix(albumFolder, ".") {
|
||||||
albumFolder = strings.ReplaceAll(albumFolder, ".", "")
|
albumFolder = strings.ReplaceAll(albumFolder, ".", "")
|
||||||
@ -1110,9 +1141,10 @@ func rip(albumId string, token string, storefront string, userToken string) erro
|
|||||||
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)
|
||||||
var lrc string = ""
|
var lrc string = ""
|
||||||
if userToken != "" {
|
if userToken != "your-media-user-token" && (config.EmbedLrc || config.SaveLrcFile) {
|
||||||
ttml, err := getSongLyrics(track.ID, storefront, token, userToken)
|
ttml, err := getSongLyrics(track.ID, storefront, token, userToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Failed to get lyrics")
|
fmt.Println("Failed to get lyrics")
|
||||||
@ -1120,6 +1152,16 @@ func rip(albumId string, token string, storefront string, userToken string) erro
|
|||||||
lrc, err = conventTTMLToLRC(ttml)
|
lrc, err = conventTTMLToLRC(ttml)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to parse lyrics: %s \n", err)
|
fmt.Printf("Failed to parse lyrics: %s \n", err)
|
||||||
|
} else {
|
||||||
|
if config.SaveLrcFile {
|
||||||
|
err := writeLyrics(sanAlbumFolder, lrcFilename, lrc)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to write lyrics")
|
||||||
|
}
|
||||||
|
if !config.EmbedLrc {
|
||||||
|
lrc = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1162,7 +1204,9 @@ func rip(albumId string, token string, storefront string, userToken string) erro
|
|||||||
}
|
}
|
||||||
tags := []string{
|
tags := []string{
|
||||||
fmt.Sprintf("lyrics=%s", lrc),
|
fmt.Sprintf("lyrics=%s", lrc),
|
||||||
fmt.Sprintf("cover=%s/cover.jpg", sanAlbumFolder),
|
}
|
||||||
|
if config.EmbedCover {
|
||||||
|
tags = append(tags, fmt.Sprintf("cover=%s/cover.%s", sanAlbumFolder, config.CoverFormat))
|
||||||
}
|
}
|
||||||
tagsString := strings.Join(tags, ":")
|
tagsString := strings.Join(tags, ":")
|
||||||
cmd := exec.Command("MP4Box", "-itags", tagsString, trackPath)
|
cmd := exec.Command("MP4Box", "-itags", tagsString, trackPath)
|
||||||
@ -1176,12 +1220,10 @@ func rip(albumId string, token string, storefront string, userToken string) erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var mediaUserToken string
|
err := loadConfig()
|
||||||
if _, err := os.Stat("media-user-token.txt"); err == nil {
|
if err != nil {
|
||||||
file, err := os.ReadFile("media-user-token.txt")
|
fmt.Printf("load config failed: %v", err)
|
||||||
if err == nil && file != nil {
|
return
|
||||||
mediaUserToken = string(file)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
token, err := getToken()
|
token, err := getToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1202,7 +1244,7 @@ func main() {
|
|||||||
fmt.Printf("Invalid URL: %s\n", url)
|
fmt.Printf("Invalid URL: %s\n", url)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
err = rip(albumId, token, storefront, mediaUserToken)
|
err = rip(albumId, token, storefront, config.MediaUserToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Album failed.")
|
fmt.Println("Album failed.")
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/beevik/etree"
|
"github.com/beevik/etree"
|
||||||
@ -34,6 +35,18 @@ const (
|
|||||||
var (
|
var (
|
||||||
forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`)
|
forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`)
|
||||||
)
|
)
|
||||||
|
type Config struct {
|
||||||
|
MediaUserToken string `yaml:"media-user-token"`
|
||||||
|
SaveLrcFile bool `yaml:"save-lrc-file"`
|
||||||
|
EmbedLrc bool `yaml:"embed-lrc"`
|
||||||
|
EmbedCover bool `yaml:"embed-cover"`
|
||||||
|
CoverSize string `yaml:"cover-size"`
|
||||||
|
CoverFormat string `yaml:"cover-format"`
|
||||||
|
AlacSaveFolder string `yaml:"alac-save-folder"`
|
||||||
|
AtmosSaveFolder string `yaml:"atmos-save-folder"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
var oktrackNum int = 0
|
var oktrackNum int = 0
|
||||||
var trackTotalnum int = 0
|
var trackTotalnum int = 0
|
||||||
|
|
||||||
@ -49,6 +62,20 @@ type SongInfo struct {
|
|||||||
samples []SampleInfo
|
samples []SampleInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadConfig() error {
|
||||||
|
// 读取config.yaml文件内容
|
||||||
|
data, err := ioutil.ReadFile("config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 将yaml解析到config变量中
|
||||||
|
err = yaml.Unmarshal(data, &config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SongInfo) Duration() (ret uint64) {
|
func (s *SongInfo) Duration() (ret uint64) {
|
||||||
for i := range s.samples {
|
for i := range s.samples {
|
||||||
ret += uint64(s.samples[i].duration)
|
ret += uint64(s.samples[i].duration)
|
||||||
@ -1027,7 +1054,7 @@ func getSongLyrics(songId string, storefront string, token string, userToken str
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeCover(sanAlbumFolder, url string) error {
|
func writeCover(sanAlbumFolder, url string) error {
|
||||||
covPath := filepath.Join(sanAlbumFolder, "cover.jpg")
|
covPath := filepath.Join(sanAlbumFolder, "cover." + config.CoverFormat)
|
||||||
exists, err := fileExists(covPath)
|
exists, err := fileExists(covPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Failed to check if cover exists.")
|
fmt.Println("Failed to check if cover exists.")
|
||||||
@ -1036,7 +1063,12 @@ func writeCover(sanAlbumFolder, url string) error {
|
|||||||
if exists {
|
if exists {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
url = strings.Replace(url, "{w}x{h}", "5000x5000", 1)
|
if config.CoverFormat == "png" {
|
||||||
|
re := regexp.MustCompile(`\{w\}x\{h\}`)
|
||||||
|
parts := re.Split(url, 2)
|
||||||
|
url = parts[0] + "{w}x{h}" + strings.Replace(parts[1], ".jpg", ".png", 1)
|
||||||
|
}
|
||||||
|
url = strings.Replace(url, "{w}x{h}", config.CoverSize, 1)
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -1087,7 +1119,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro
|
|||||||
singerFoldername = strings.ReplaceAll(singerFoldername, ".", "")
|
singerFoldername = strings.ReplaceAll(singerFoldername, ".", "")
|
||||||
}
|
}
|
||||||
singerFoldername = strings.TrimSpace(singerFoldername)
|
singerFoldername = strings.TrimSpace(singerFoldername)
|
||||||
singerFolder := filepath.Join("AM-DL-Atmos downloads", forbiddenNames.ReplaceAllString(singerFoldername, "_"))
|
singerFolder := filepath.Join(config.AtmosSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_"))
|
||||||
albumFolder := fmt.Sprintf("%s [Atmos]", meta.Data[0].Attributes.Name)
|
albumFolder := fmt.Sprintf("%s [Atmos]", meta.Data[0].Attributes.Name)
|
||||||
if strings.HasSuffix(albumFolder, ".") {
|
if strings.HasSuffix(albumFolder, ".") {
|
||||||
albumFolder = strings.ReplaceAll(albumFolder, ".", "")
|
albumFolder = strings.ReplaceAll(albumFolder, ".", "")
|
||||||
@ -1117,7 +1149,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro
|
|||||||
}
|
}
|
||||||
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, "_"))
|
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)
|
||||||
@ -1134,7 +1166,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var lrc string = ""
|
var lrc string = ""
|
||||||
if userToken != "" {
|
if userToken != "your-media-user-token" && (config.EmbedLrc || config.SaveLrcFile) {
|
||||||
ttml, err := getSongLyrics(track.ID, storefront, token, userToken)
|
ttml, err := getSongLyrics(track.ID, storefront, token, userToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Failed to get lyrics")
|
fmt.Println("Failed to get lyrics")
|
||||||
@ -1142,6 +1174,16 @@ func rip(albumId string, token string, storefront string, userToken string) erro
|
|||||||
lrc, err = conventTTMLToLRC(ttml)
|
lrc, err = conventTTMLToLRC(ttml)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to parse lyrics: %s \n", err)
|
fmt.Printf("Failed to parse lyrics: %s \n", err)
|
||||||
|
} else {
|
||||||
|
if config.SaveLrcFile {
|
||||||
|
err := writeLyrics(sanAlbumFolder, lrcFilename, lrc)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to write lyrics")
|
||||||
|
}
|
||||||
|
if !config.EmbedLrc {
|
||||||
|
lrc = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1177,7 +1219,6 @@ func rip(albumId string, token string, storefront string, userToken string) erro
|
|||||||
tags := []string{
|
tags := []string{
|
||||||
"tool=",
|
"tool=",
|
||||||
fmt.Sprintf("lyrics=%s", lrc),
|
fmt.Sprintf("lyrics=%s", lrc),
|
||||||
fmt.Sprintf("cover=%s/cover.jpg", sanAlbumFolder),
|
|
||||||
fmt.Sprintf("album=%s", meta.Data[0].Attributes.Name),
|
fmt.Sprintf("album=%s", meta.Data[0].Attributes.Name),
|
||||||
fmt.Sprintf("title=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.Name),
|
fmt.Sprintf("title=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.Name),
|
||||||
fmt.Sprintf("artist=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName),
|
fmt.Sprintf("artist=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName),
|
||||||
@ -1190,7 +1231,9 @@ func rip(albumId string, token string, storefront string, userToken string) erro
|
|||||||
fmt.Sprintf("UPC=%s", meta.Data[0].Attributes.Upc),
|
fmt.Sprintf("UPC=%s", meta.Data[0].Attributes.Upc),
|
||||||
fmt.Sprintf("track=%d/%d", trackNum, trackTotal),
|
fmt.Sprintf("track=%d/%d", trackNum, trackTotal),
|
||||||
}
|
}
|
||||||
|
if config.EmbedCover {
|
||||||
|
tags = append(tags, fmt.Sprintf("cover=%s/cover.%s", sanAlbumFolder, config.CoverFormat))
|
||||||
|
}
|
||||||
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))
|
||||||
@ -1211,12 +1254,10 @@ func rip(albumId string, token string, storefront string, userToken string) erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var mediaUserToken string
|
err := loadConfig()
|
||||||
if _, err := os.Stat("media-user-token.txt"); err == nil {
|
if err != nil {
|
||||||
file, err := os.ReadFile("media-user-token.txt")
|
fmt.Printf("load config failed: %v", err)
|
||||||
if err == nil && file != nil {
|
return
|
||||||
mediaUserToken = string(file)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
token, err := getToken()
|
token, err := getToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1236,7 +1277,7 @@ func main() {
|
|||||||
fmt.Printf("Invalid URL: %s\n", url)
|
fmt.Printf("Invalid URL: %s\n", url)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
err := rip(albumId, token, storefront, mediaUserToken)
|
err := rip(albumId, token, storefront, config.MediaUserToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Album failed.")
|
fmt.Println("Album failed.")
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/beevik/etree"
|
"github.com/beevik/etree"
|
||||||
@ -35,6 +36,18 @@ const (
|
|||||||
var (
|
var (
|
||||||
forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`)
|
forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`)
|
||||||
)
|
)
|
||||||
|
type Config struct {
|
||||||
|
MediaUserToken string `yaml:"media-user-token"`
|
||||||
|
SaveLrcFile bool `yaml:"save-lrc-file"`
|
||||||
|
EmbedLrc bool `yaml:"embed-lrc"`
|
||||||
|
EmbedCover bool `yaml:"embed-cover"`
|
||||||
|
CoverSize string `yaml:"cover-size"`
|
||||||
|
CoverFormat string `yaml:"cover-format"`
|
||||||
|
AlacSaveFolder string `yaml:"alac-save-folder"`
|
||||||
|
AtmosSaveFolder string `yaml:"atmos-save-folder"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
|
||||||
type SampleInfo struct {
|
type SampleInfo struct {
|
||||||
data []byte
|
data []byte
|
||||||
@ -48,6 +61,20 @@ type SongInfo struct {
|
|||||||
samples []SampleInfo
|
samples []SampleInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadConfig() error {
|
||||||
|
// 读取config.yaml文件内容
|
||||||
|
data, err := ioutil.ReadFile("config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 将yaml解析到config变量中
|
||||||
|
err = yaml.Unmarshal(data, &config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SongInfo) Duration() (ret uint64) {
|
func (s *SongInfo) Duration() (ret uint64) {
|
||||||
for i := range s.samples {
|
for i := range s.samples {
|
||||||
ret += uint64(s.samples[i].duration)
|
ret += uint64(s.samples[i].duration)
|
||||||
@ -1020,7 +1047,7 @@ func getSongLyrics(songId string, storefront string, token string, userToken str
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeCover(sanAlbumFolder, url string) error {
|
func writeCover(sanAlbumFolder, url string) error {
|
||||||
covPath := filepath.Join(sanAlbumFolder, "cover.jpg")
|
covPath := filepath.Join(sanAlbumFolder, "cover." + config.CoverFormat)
|
||||||
exists, err := fileExists(covPath)
|
exists, err := fileExists(covPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Failed to check if cover exists.")
|
fmt.Println("Failed to check if cover exists.")
|
||||||
@ -1029,7 +1056,12 @@ func writeCover(sanAlbumFolder, url string) error {
|
|||||||
if exists {
|
if exists {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
url = strings.Replace(url, "{w}x{h}", "5000x5000", 1)
|
if config.CoverFormat == "png" {
|
||||||
|
re := regexp.MustCompile(`\{w\}x\{h\}`)
|
||||||
|
parts := re.Split(url, 2)
|
||||||
|
url = parts[0] + "{w}x{h}" + strings.Replace(parts[1], ".jpg", ".png", 1)
|
||||||
|
}
|
||||||
|
url = strings.Replace(url, "{w}x{h}", config.CoverSize, 1)
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -1146,9 +1178,10 @@ func rip(albumId string, token string, storefront string, userToken string) erro
|
|||||||
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)
|
||||||
var lrc string = ""
|
var lrc string = ""
|
||||||
if userToken != "" {
|
if userToken != "your-media-user-token" && (config.EmbedLrc || config.SaveLrcFile) {
|
||||||
ttml, err := getSongLyrics(track.ID, storefront, token, userToken)
|
ttml, err := getSongLyrics(track.ID, storefront, token, userToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Failed to get lyrics")
|
fmt.Println("Failed to get lyrics")
|
||||||
@ -1156,6 +1189,16 @@ func rip(albumId string, token string, storefront string, userToken string) erro
|
|||||||
lrc, err = conventTTMLToLRC(ttml)
|
lrc, err = conventTTMLToLRC(ttml)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to parse lyrics: %s \n", err)
|
fmt.Printf("Failed to parse lyrics: %s \n", err)
|
||||||
|
} else {
|
||||||
|
if config.SaveLrcFile {
|
||||||
|
err := writeLyrics(sanAlbumFolder, lrcFilename, lrc)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to write lyrics")
|
||||||
|
}
|
||||||
|
if !config.EmbedLrc {
|
||||||
|
lrc = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1197,7 +1240,9 @@ func rip(albumId string, token string, storefront string, userToken string) erro
|
|||||||
}
|
}
|
||||||
tags := []string{
|
tags := []string{
|
||||||
fmt.Sprintf("lyrics=%s", lrc),
|
fmt.Sprintf("lyrics=%s", lrc),
|
||||||
fmt.Sprintf("cover=%s/cover.jpg", sanAlbumFolder),
|
}
|
||||||
|
if config.EmbedCover {
|
||||||
|
tags = append(tags, fmt.Sprintf("cover=%s/cover.%s", sanAlbumFolder, config.CoverFormat))
|
||||||
}
|
}
|
||||||
tagsString := strings.Join(tags, ":")
|
tagsString := strings.Join(tags, ":")
|
||||||
cmd := exec.Command("MP4Box", "-itags", tagsString, trackPath)
|
cmd := exec.Command("MP4Box", "-itags", tagsString, trackPath)
|
||||||
@ -1211,12 +1256,10 @@ func rip(albumId string, token string, storefront string, userToken string) erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var mediaUserToken string
|
err := loadConfig()
|
||||||
if _, err := os.Stat("media-user-token.txt"); err == nil {
|
if err != nil {
|
||||||
file, err := os.ReadFile("media-user-token.txt")
|
fmt.Printf("load config failed: %v", err)
|
||||||
if err == nil && file != nil {
|
return
|
||||||
mediaUserToken = string(file)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
token, err := getToken()
|
token, err := getToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1237,7 +1280,7 @@ func main() {
|
|||||||
fmt.Printf("Invalid URL: %s\n", url)
|
fmt.Printf("Invalid URL: %s\n", url)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
err := rip(albumId, token, storefront, mediaUserToken)
|
err := rip(albumId, token, storefront, config.MediaUserToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Album failed.")
|
fmt.Println("Album failed.")
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user