Compare commits

..

No commits in common. '6c9015200c4cb212436acf44354c63ddefabcd76' and '5f528e76a9c94271795b3eda61123998067a602c' have entirely different histories.

  1. 27
      .github/workflows/go.yml
  2. 341
      main.go
  3. 281
      main_atmos.go
  4. 317
      main_select.go

@ -12,6 +12,7 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
@ -22,17 +23,21 @@ jobs:
go build -o main.exe -v ./main.go go build -o main.exe -v ./main.go
go build -o main_atmos.exe -v ./main_atmos.go go build -o main_atmos.exe -v ./main_atmos.go
go build -o main_select.exe -v ./main_select.go go build -o main_select.exe -v ./main_select.go
- name: Create a new directory and copy files
run: |
mkdir -p alac
cp agent.js alac/
cp config.yaml alac/
cp main.exe alac/
cp main_atmos.exe alac/
cp main_select.exe alac/
- name: Upload apple-music-alac-atmos-downloader - name: Upload main.exe
uses: actions/upload-artifact@v2
with:
name: apple-music-alac-atmos-downloader-main
path: main.exe
- name: Upload main_atmos.exe
uses: actions/upload-artifact@v2
with:
name: apple-music-alac-atmos-downloader-atmos
path: main_atmos.exe
- name: Upload main_select.exe
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: apple-music-alac-atmos-downloader name: apple-music-alac-atmos-downloader-select
path: alac/* path: main_select.exe

@ -5,6 +5,7 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"gopkg.in/yaml.v2"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -22,8 +23,6 @@ import (
"strings" "strings"
"time" "time"
"gopkg.in/yaml.v2"
"github.com/abema/go-mp4" "github.com/abema/go-mp4"
"github.com/beevik/etree" "github.com/beevik/etree"
"github.com/grafov/m3u8" "github.com/grafov/m3u8"
@ -37,29 +36,28 @@ const (
var ( var (
forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`) forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`)
) )
type Config struct { type Config struct {
MediaUserToken string `yaml:"media-user-token"` MediaUserToken string `yaml:"media-user-token"`
SaveLrcFile bool `yaml:"save-lrc-file"` SaveLrcFile bool `yaml:"save-lrc-file"`
EmbedLrc bool `yaml:"embed-lrc"` EmbedLrc bool `yaml:"embed-lrc"`
EmbedCover bool `yaml:"embed-cover"` EmbedCover bool `yaml:"embed-cover"`
CoverSize string `yaml:"cover-size"` CoverSize string `yaml:"cover-size"`
CoverFormat string `yaml:"cover-format"` CoverFormat string `yaml:"cover-format"`
AlacSaveFolder string `yaml:"alac-save-folder"` AlacSaveFolder string `yaml:"alac-save-folder"`
AtmosSaveFolder string `yaml:"atmos-save-folder"` AtmosSaveFolder string `yaml:"atmos-save-folder"`
AlbumFolderFormat string `yaml:"album-folder-format"` AlbumFolderFormat string `yaml:"album-folder-format"`
PlaylistFolderFormat string `yaml:"playlist-folder-format"` PlaylistFolderFormat string `yaml:"playlist-folder-format"`
ArtistFolderFormat string `yaml:"artist-folder-format"` ArtistFolderFormat string `yaml:"artist-folder-format"`
SongFileFormat string `yaml:"song-file-format"` SongFileFormat string `yaml:"song-file-format"`
ExplicitChoice string `yaml:"explicit-choice"` ExplicitChoice string `yaml:"explicit-choice"`
CleanChoice string `yaml:"clean-choice"` CleanChoice string `yaml:"clean-choice"`
AppleMasterChoice string `yaml:"apple-master-choice"` AppleMasterChoice string `yaml:"apple-master-choice"`
ForceApi bool `yaml:"force-api"` ForceApi bool `yaml:"force-api"`
Check string `yaml:"check"` Check string `yaml:"check"`
GetM3u8FromDevice bool `yaml:"get-m3u8-from-device"` GetM3u8FromDevice bool `yaml:"get-m3u8-from-device"`
AlacMax int `yaml:"alac-max"` AlacMax int `yaml:"alac-max"`
UseSongInfoForPlaylist bool `yaml:"use-songinfo-for-playlist"` UseSongInfoForPlaylist bool `yaml:"use-songinfo-for-playlist"`
DlAlbumcoverForPlaylist bool `yaml:"dl-albumcover-for-playlist"` DlAlbumcoverForPlaylist bool `yaml:"dl-albumcover-for-playlist"`
} }
var config Config var config Config
@ -78,19 +76,18 @@ type SongInfo struct {
alacParam *Alac alacParam *Alac
samples []SampleInfo samples []SampleInfo
} }
func loadConfig() error { func loadConfig() error {
// 读取config.yaml文件内容 // 读取config.yaml文件内容
data, err := ioutil.ReadFile("config.yaml") data, err := ioutil.ReadFile("config.yaml")
if err != nil { if err != nil {
return err return err
} }
// 将yaml解析到config变量中 // 将yaml解析到config变量中
err = yaml.Unmarshal(data, &config) err = yaml.Unmarshal(data, &config)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
func (s *SongInfo) Duration() (ret uint64) { func (s *SongInfo) Duration() (ret uint64) {
@ -693,10 +690,10 @@ func writeM4a(w *mp4.Writer, info *SongInfo, meta *AutoGenerated, data []byte, t
if err != nil { if err != nil {
return err return err
} }
AlbumName := meta.Data[0].Relationships.Tracks.Data[index].Attributes.AlbumName AlbumName:=meta.Data[0].Relationships.Tracks.Data[index].Attributes.AlbumName
if strings.Contains(meta.Data[0].ID, "pl.") { if strings.Contains(meta.Data[0].ID, "pl."){
if !config.UseSongInfoForPlaylist { if !config.UseSongInfoForPlaylist {
AlbumName = meta.Data[0].Attributes.Name AlbumName=meta.Data[0].Attributes.Name
} }
} }
err = addMeta(mp4.BoxType{'\251', 'a', 'l', 'b'}, AlbumName) err = addMeta(mp4.BoxType{'\251', 'a', 'l', 'b'}, AlbumName)
@ -807,13 +804,13 @@ func writeM4a(w *mp4.Writer, info *SongInfo, meta *AutoGenerated, data []byte, t
if err != nil { if err != nil {
return err return err
} }
if !strings.Contains(meta.Data[0].ID, "pl.") { if !strings.Contains(meta.Data[0].ID, "pl."){
plID, err := strconv.ParseUint(meta.Data[0].ID, 10, 32) plID, err := strconv.ParseUint(meta.Data[0].ID, 10, 32)
if err != nil { if err != nil {
return err return err
} }
err = addMeta(mp4.BoxType{'p', 'l', 'I', 'D'}, uint32(plID)) err = addMeta(mp4.BoxType{'p', 'l', 'I', 'D'}, uint32(plID))
if err != nil { if err != nil {
return err return err
@ -839,8 +836,8 @@ func writeM4a(w *mp4.Writer, info *SongInfo, meta *AutoGenerated, data []byte, t
binary.BigEndian.PutUint32(trkn, uint32(meta.Data[0].Relationships.Tracks.Data[index].Attributes.TrackNumber)) binary.BigEndian.PutUint32(trkn, uint32(meta.Data[0].Relationships.Tracks.Data[index].Attributes.TrackNumber))
binary.BigEndian.PutUint16(trkn[4:], uint16(trackTotal)) binary.BigEndian.PutUint16(trkn[4:], uint16(trackTotal))
binary.BigEndian.PutUint32(disk, uint32(meta.Data[0].Relationships.Tracks.Data[index].Attributes.DiscNumber)) binary.BigEndian.PutUint32(disk, uint32(meta.Data[0].Relationships.Tracks.Data[index].Attributes.DiscNumber))
binary.BigEndian.PutUint16(disk[4:], uint16(meta.Data[0].Relationships.Tracks.Data[trackTotal-1].Attributes.DiscNumber)) binary.BigEndian.PutUint16(disk[4:], uint16(meta.Data[0].Relationships.Tracks.Data[trackTotal - 1].Attributes.DiscNumber))
if strings.Contains(meta.Data[0].ID, "pl.") { if strings.Contains(meta.Data[0].ID, "pl."){
if !config.UseSongInfoForPlaylist { if !config.UseSongInfoForPlaylist {
binary.BigEndian.PutUint32(trkn, uint32(trackNum)) binary.BigEndian.PutUint32(trkn, uint32(trackNum))
binary.BigEndian.PutUint16(trkn[4:], uint16(trackTotal)) binary.BigEndian.PutUint16(trkn[4:], uint16(trackTotal))
@ -1120,8 +1117,8 @@ func getSongLyrics(songId string, storefront string, token string, userToken str
} }
} }
func writeCover(sanAlbumFolder, name string, url string) error { func writeCover(sanAlbumFolder,name string, url string) error {
covPath := filepath.Join(sanAlbumFolder, name+"."+config.CoverFormat) covPath := filepath.Join(sanAlbumFolder, name+"." + 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.")
@ -1182,13 +1179,13 @@ func rip(albumId string, token string, storefront string, userToken string) erro
return err return err
} }
var singerFoldername string var singerFoldername string
if config.ArtistFolderFormat != "" { if config.ArtistFolderFormat != ""{
if strings.Contains(albumId, "pl.") { if strings.Contains(albumId, "pl.") {
singerFoldername = strings.NewReplacer( singerFoldername = strings.NewReplacer(
"{ArtistName}", "Apple Music", "{ArtistName}", "Apple Music",
"{ArtistId}", "", "{ArtistId}", "",
).Replace(config.ArtistFolderFormat) ).Replace(config.ArtistFolderFormat)
} else { }else{
singerFoldername = strings.NewReplacer( singerFoldername = strings.NewReplacer(
"{ArtistName}", meta.Data[0].Attributes.ArtistName, "{ArtistName}", meta.Data[0].Attributes.ArtistName,
"{ArtistId}", meta.Data[0].Relationships.Artists.Data[0].ID, "{ArtistId}", meta.Data[0].Relationships.Artists.Data[0].ID,
@ -1209,29 +1206,29 @@ func rip(albumId string, token string, storefront string, userToken string) erro
fmt.Println("Unavailable in ALAC.") fmt.Println("Unavailable in ALAC.")
} }
var Quality string var Quality string
EnhancedHls_m3u8, err := checkM3u8(meta.Data[0].Relationships.Tracks.Data[0].ID, "album") EnhancedHls_m3u8,err:=checkM3u8(meta.Data[0].Relationships.Tracks.Data[0].ID,"album")
if strings.HasPrefix(EnhancedHls_m3u8, "http") { if strings.HasPrefix(EnhancedHls_m3u8, "http"){
manifest1.Attributes.ExtendedAssetUrls.EnhancedHls = EnhancedHls_m3u8 manifest1.Attributes.ExtendedAssetUrls.EnhancedHls=EnhancedHls_m3u8
} }
if strings.Contains(config.AlbumFolderFormat, "Quality") { if strings.Contains(config.AlbumFolderFormat, "Quality"){
Quality, err = extractMediaQuality(manifest1.Attributes.ExtendedAssetUrls.EnhancedHls) Quality,err = extractMediaQuality(manifest1.Attributes.ExtendedAssetUrls.EnhancedHls)
if err != nil { if err != nil {
fmt.Println("Failed to extract quality from manifest.\n", err) fmt.Println("Failed to extract quality from manifest.\n", err)
} }
} }
stringsToJoin := []string{} stringsToJoin := []string{}
if meta.Data[0].Attributes.IsAppleDigitalMaster || meta.Data[0].Attributes.IsMasteredForItunes { if meta.Data[0].Attributes.IsAppleDigitalMaster{
if config.AppleMasterChoice != "" { if config.AppleMasterChoice != ""{
stringsToJoin = append(stringsToJoin, config.AppleMasterChoice) stringsToJoin = append(stringsToJoin, config.AppleMasterChoice)
} }
} }
if meta.Data[0].Attributes.ContentRating == "explicit" { if meta.Data[0].Attributes.ContentRating=="explicit"{
if config.ExplicitChoice != "" { if config.ExplicitChoice != ""{
stringsToJoin = append(stringsToJoin, config.ExplicitChoice) stringsToJoin = append(stringsToJoin, config.ExplicitChoice)
} }
} }
if meta.Data[0].Attributes.ContentRating == "clean" { if meta.Data[0].Attributes.ContentRating=="clean"{
if config.CleanChoice != "" { if config.CleanChoice != ""{
stringsToJoin = append(stringsToJoin, config.CleanChoice) stringsToJoin = append(stringsToJoin, config.CleanChoice)
} }
} }
@ -1242,11 +1239,11 @@ func rip(albumId string, token string, storefront string, userToken string) erro
"{ArtistName}", "Apple Music", "{ArtistName}", "Apple Music",
"{PlaylistName}", meta.Data[0].Attributes.Name, "{PlaylistName}", meta.Data[0].Attributes.Name,
"{PlaylistId}", albumId, "{PlaylistId}", albumId,
"{Quality}", Quality, "{Quality}",Quality,
"{Codec}", "ALAC", "{Codec}", "ALAC",
"{Tag}", Tag_string, "{Tag}",Tag_string,
).Replace(config.PlaylistFolderFormat) ).Replace(config.PlaylistFolderFormat)
} else { }else{
albumFolder = strings.NewReplacer( albumFolder = strings.NewReplacer(
"{ReleaseDate}", meta.Data[0].Attributes.ReleaseDate, "{ReleaseDate}", meta.Data[0].Attributes.ReleaseDate,
"{ReleaseYear}", meta.Data[0].Attributes.ReleaseDate[:4], "{ReleaseYear}", meta.Data[0].Attributes.ReleaseDate[:4],
@ -1258,7 +1255,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro
"{AlbumId}", albumId, "{AlbumId}", albumId,
"{Quality}", Quality, "{Quality}", Quality,
"{Codec}", "ALAC", "{Codec}", "ALAC",
"{Tag}", Tag_string, "{Tag}",Tag_string,
).Replace(config.AlbumFolderFormat) ).Replace(config.AlbumFolderFormat)
} }
if strings.HasSuffix(albumFolder, ".") { if strings.HasSuffix(albumFolder, ".") {
@ -1268,7 +1265,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro
sanAlbumFolder := filepath.Join(singerFolder, forbiddenNames.ReplaceAllString(albumFolder, "_")) sanAlbumFolder := filepath.Join(singerFolder, forbiddenNames.ReplaceAllString(albumFolder, "_"))
os.MkdirAll(sanAlbumFolder, os.ModePerm) os.MkdirAll(sanAlbumFolder, os.ModePerm)
fmt.Println(albumFolder) fmt.Println(albumFolder)
err = writeCover(sanAlbumFolder, "cover", meta.Data[0].Attributes.Artwork.URL) err = writeCover(sanAlbumFolder,"cover", meta.Data[0].Attributes.Artwork.URL)
if err != nil { if err != nil {
fmt.Println("Failed to write cover.") fmt.Println("Failed to write cover.")
} }
@ -1286,31 +1283,31 @@ func rip(albumId string, token string, storefront string, userToken string) erro
fmt.Println("Unavailable in ALAC.") fmt.Println("Unavailable in ALAC.")
continue continue
} }
EnhancedHls_m3u8, err := checkM3u8(track.ID, "song") EnhancedHls_m3u8,err:=checkM3u8(track.ID,"song")
if strings.HasPrefix(EnhancedHls_m3u8, "http") { if strings.HasPrefix(EnhancedHls_m3u8, "http"){
manifest.Attributes.ExtendedAssetUrls.EnhancedHls = EnhancedHls_m3u8 manifest.Attributes.ExtendedAssetUrls.EnhancedHls=EnhancedHls_m3u8
} }
var Quality string var Quality string
if strings.Contains(config.SongFileFormat, "Quality") { if strings.Contains(config.SongFileFormat, "Quality"){
Quality, err = extractMediaQuality(manifest.Attributes.ExtendedAssetUrls.EnhancedHls) Quality,err = extractMediaQuality(manifest.Attributes.ExtendedAssetUrls.EnhancedHls)
if err != nil { if err != nil {
fmt.Println("Failed to extract quality from manifest.\n", err) fmt.Println("Failed to extract quality from manifest.\n", err)
continue continue
} }
} }
stringsToJoin := []string{} stringsToJoin := []string{}
if track.Attributes.IsAppleDigitalMaster { if track.Attributes.IsAppleDigitalMaster{
if config.AppleMasterChoice != "" { if config.AppleMasterChoice != ""{
stringsToJoin = append(stringsToJoin, config.AppleMasterChoice) stringsToJoin = append(stringsToJoin, config.AppleMasterChoice)
} }
} }
if track.Attributes.ContentRating == "explicit" { if track.Attributes.ContentRating=="explicit"{
if config.ExplicitChoice != "" { if config.ExplicitChoice != ""{
stringsToJoin = append(stringsToJoin, config.ExplicitChoice) stringsToJoin = append(stringsToJoin, config.ExplicitChoice)
} }
} }
if track.Attributes.ContentRating == "clean" { if track.Attributes.ContentRating=="clean"{
if config.CleanChoice != "" { if config.CleanChoice != ""{
stringsToJoin = append(stringsToJoin, config.CleanChoice) stringsToJoin = append(stringsToJoin, config.CleanChoice)
} }
} }
@ -1323,7 +1320,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro
"{DiscNumber}", fmt.Sprintf("%0d", track.Attributes.DiscNumber), "{DiscNumber}", fmt.Sprintf("%0d", track.Attributes.DiscNumber),
"{TrackNumber}", fmt.Sprintf("%0d", track.Attributes.TrackNumber), "{TrackNumber}", fmt.Sprintf("%0d", track.Attributes.TrackNumber),
"{Quality}", Quality, "{Quality}", Quality,
"{Tag}", Tag_string, "{Tag}",Tag_string,
"{Codec}", "ALAC", "{Codec}", "ALAC",
).Replace(config.SongFileFormat) ).Replace(config.SongFileFormat)
fmt.Println(songName) fmt.Println(songName)
@ -1361,7 +1358,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro
oktrackNum += 1 oktrackNum += 1
continue continue
} }
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)
@ -1393,22 +1390,22 @@ 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),
} }
if track.Attributes.ContentRating == "explicit" { if track.Attributes.ContentRating=="explicit"{
tags = append(tags, "rating=1") tags = append(tags, "rating=1")
} else if track.Attributes.ContentRating == "clean" { }else if track.Attributes.ContentRating=="clean"{
tags = append(tags, "rating=2") tags = append(tags, "rating=2")
} else { }else{
tags = append(tags, "rating=0") tags = append(tags, "rating=0")
} }
if config.EmbedCover { if config.EmbedCover {
if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist { if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist {
err = writeCover(sanAlbumFolder, track.ID, track.Attributes.Artwork.URL) err = writeCover(sanAlbumFolder,track.ID, track.Attributes.Artwork.URL)
if err != nil { if err != nil {
fmt.Println("Failed to write cover.") fmt.Println("Failed to write cover.")
} }
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, track.ID, config.CoverFormat)) tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder,track.ID, config.CoverFormat))
} else { }else{
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, "cover", config.CoverFormat)) tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder,"cover", config.CoverFormat))
} }
} }
tagsString := strings.Join(tags, ":") tagsString := strings.Join(tags, ":")
@ -1418,8 +1415,8 @@ func rip(albumId string, token string, storefront string, userToken string) erro
continue continue
} }
if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist { if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist {
if err := os.Remove(fmt.Sprintf("%s/%s.%s", sanAlbumFolder, track.ID, config.CoverFormat)); err != nil { if err := os.Remove(fmt.Sprintf("%s/%s.%s", sanAlbumFolder,track.ID, config.CoverFormat)); err != nil {
fmt.Printf("Error deleting file: %s/%s.%s\n", sanAlbumFolder, track.ID, config.CoverFormat) fmt.Printf("Error deleting file: %s/%s.%s\n", sanAlbumFolder,track.ID, config.CoverFormat)
continue continue
} }
} }
@ -1520,12 +1517,12 @@ func conventTTMLToLRC(ttml string) (string, error) {
return strings.Join(lrcLines, "\n"), nil return strings.Join(lrcLines, "\n"), nil
} }
func checkM3u8(b string, f string) (string, error) { func checkM3u8(b string,f string) (string, error) {
var EnhancedHls string var EnhancedHls string
if config.Check != "" { if config.Check != ""{
config.Check = strings.TrimSpace(config.Check) config.Check=strings.TrimSpace(config.Check)
if strings.HasSuffix(config.Check, "txt") { if strings.HasSuffix(config.Check, "txt") {
txtpath = config.Check txtpath=config.Check
} }
if strings.HasPrefix(config.Check, "http") { if strings.HasPrefix(config.Check, "http") {
req, err := http.NewRequest("GET", config.Check, nil) req, err := http.NewRequest("GET", config.Check, nil)
@ -1547,8 +1544,8 @@ func checkM3u8(b string, f string) (string, error) {
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
if string(Checkbody) != "no_found" { if string(Checkbody) != "no_found"{
EnhancedHls = string(Checkbody) EnhancedHls=string(Checkbody)
fmt.Println("Found m3u8 from API") fmt.Println("Found m3u8 from API")
} else { } else {
if config.ForceApi { if config.ForceApi {
@ -1558,14 +1555,14 @@ func checkM3u8(b string, f string) (string, error) {
} }
} }
} }
if config.GetM3u8FromDevice { if config.GetM3u8FromDevice{
adamID := b adamID := b
conn, err := net.Dial("tcp", "127.0.0.1:20020") conn, err := net.Dial("tcp", "127.0.0.1:20020")
if err != nil { if err != nil {
fmt.Println("Error connecting to device:", err) fmt.Println("Error connecting to device:", err)
} }
defer conn.Close() defer conn.Close()
if f == "song" { if f =="song"{
fmt.Println("Connected to device") fmt.Println("Connected to device")
} }
@ -1591,10 +1588,10 @@ func checkM3u8(b string, f string) (string, error) {
} }
// Trim any newline characters from the response // Trim any newline characters from the response
response = bytes.TrimSpace(response) response = bytes.TrimSpace(response)
if len(response) > 0 { if len(response) > 0 {
if f == "song" { if f =="song"{
fmt.Println("Received URL:", string(response)) fmt.Println("Received URL:", string(response))
} }
EnhancedHls = string(response) EnhancedHls = string(response)
@ -1614,7 +1611,7 @@ func checkM3u8(b string, f string) (string, error) {
if strings.HasPrefix(line, b) { if strings.HasPrefix(line, b) {
parts := strings.SplitN(line, ",", 2) parts := strings.SplitN(line, ",", 2)
if len(parts) == 2 { if len(parts) == 2 {
EnhancedHls = parts[1] EnhancedHls=parts[1]
fmt.Println("Found m3u8 from txt") fmt.Println("Found m3u8 from txt")
} }
} }
@ -1653,16 +1650,16 @@ func extractMediaQuality(b string) (string, error) {
if variant.Codecs == "alac" { if variant.Codecs == "alac" {
split := strings.Split(variant.Audio, "-") split := strings.Split(variant.Audio, "-")
length := len(split) length := len(split)
length_int, err := strconv.Atoi(split[length-2]) length_int,err := strconv.Atoi(split[length-2])
if err != nil { if err != nil {
return "", err return "", err
} }
if length_int <= config.AlacMax { if length_int <= config.AlacMax{
HZ, err := strconv.Atoi(split[length-2]) HZ,err:=strconv.Atoi(split[length-2])
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
KHZ := float64(HZ) / 1000.0 KHZ:=float64(HZ) / 1000.0
Quality = fmt.Sprintf("%sB-%.1fkHz", split[length-1], KHZ) Quality = fmt.Sprintf("%sB-%.1fkHz", split[length-1], KHZ)
break break
} }
@ -1702,11 +1699,11 @@ func extractMedia(b string) (string, []string, error) {
if variant.Codecs == "alac" { if variant.Codecs == "alac" {
split := strings.Split(variant.Audio, "-") split := strings.Split(variant.Audio, "-")
length := len(split) length := len(split)
length_int, err := strconv.Atoi(split[length-2]) length_int,err := strconv.Atoi(split[length-2])
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
if length_int <= config.AlacMax { if length_int <= config.AlacMax{
fmt.Printf("%s-bit / %s Hz\n", split[length-1], split[length-2]) fmt.Printf("%s-bit / %s Hz\n", split[length-1], split[length-2])
streamUrlTemp, err := masterUrl.Parse(variant.URI) streamUrlTemp, err := masterUrl.Parse(variant.URI)
if err != nil { if err != nil {
@ -1975,32 +1972,32 @@ type SongAttributes struct {
ExtendedAssetUrls struct { ExtendedAssetUrls struct {
EnhancedHls string `json:"enhancedHls"` EnhancedHls string `json:"enhancedHls"`
} `json:"extendedAssetUrls"` } `json:"extendedAssetUrls"`
IsMasteredForItunes bool `json:"isMasteredForItunes"` IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"` ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"` ReleaseDate string `json:"releaseDate"`
Name string `json:"name"` Name string `json:"name"`
Isrc string `json:"isrc"` Isrc string `json:"isrc"`
AlbumName string `json:"albumName"` AlbumName string `json:"albumName"`
TrackNumber int `json:"trackNumber"` TrackNumber int `json:"trackNumber"`
ComposerName string `json:"composerName"` ComposerName string `json:"composerName"`
} }
type AlbumAttributes struct { type AlbumAttributes struct {
ArtistName string `json:"artistName"` ArtistName string `json:"artistName"`
IsSingle bool `json:"isSingle"` IsSingle bool `json:"isSingle"`
IsComplete bool `json:"isComplete"` IsComplete bool `json:"isComplete"`
GenreNames []string `json:"genreNames"` GenreNames []string `json:"genreNames"`
TrackCount int `json:"trackCount"` TrackCount int `json:"trackCount"`
IsMasteredForItunes bool `json:"isMasteredForItunes"` IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"` ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"` ReleaseDate string `json:"releaseDate"`
Name string `json:"name"` Name string `json:"name"`
RecordLabel string `json:"recordLabel"` RecordLabel string `json:"recordLabel"`
Upc string `json:"upc"` Upc string `json:"upc"`
Copyright string `json:"copyright"` Copyright string `json:"copyright"`
IsCompilation bool `json:"isCompilation"` IsCompilation bool `json:"isCompilation"`
} }
type SongData struct { type SongData struct {
@ -2162,22 +2159,22 @@ type AutoGenerated struct {
TextColor3 string `json:"textColor3"` TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"` TextColor4 string `json:"textColor4"`
} `json:"artwork"` } `json:"artwork"`
ArtistName string `json:"artistName"` ArtistName string `json:"artistName"`
IsSingle bool `json:"isSingle"` IsSingle bool `json:"isSingle"`
URL string `json:"url"` URL string `json:"url"`
IsComplete bool `json:"isComplete"` IsComplete bool `json:"isComplete"`
GenreNames []string `json:"genreNames"` GenreNames []string `json:"genreNames"`
TrackCount int `json:"trackCount"` TrackCount int `json:"trackCount"`
IsMasteredForItunes bool `json:"isMasteredForItunes"` IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"` ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"` ReleaseDate string `json:"releaseDate"`
Name string `json:"name"` Name string `json:"name"`
RecordLabel string `json:"recordLabel"` RecordLabel string `json:"recordLabel"`
Upc string `json:"upc"` Upc string `json:"upc"`
AudioTraits []string `json:"audioTraits"` AudioTraits []string `json:"audioTraits"`
Copyright string `json:"copyright"` Copyright string `json:"copyright"`
PlayParams struct { PlayParams struct {
ID string `json:"id"` ID string `json:"id"`
Kind string `json:"kind"` Kind string `json:"kind"`
} `json:"playParams"` } `json:"playParams"`
@ -2220,22 +2217,22 @@ type AutoGenerated struct {
TextColor3 string `json:"textColor3"` TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"` TextColor4 string `json:"textColor4"`
} `json:"artwork"` } `json:"artwork"`
ArtistName string `json:"artistName"` ArtistName string `json:"artistName"`
URL string `json:"url"` URL string `json:"url"`
DiscNumber int `json:"discNumber"` DiscNumber int `json:"discNumber"`
GenreNames []string `json:"genreNames"` GenreNames []string `json:"genreNames"`
HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"`
IsMasteredForItunes bool `json:"isMasteredForItunes"` IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"` ContentRating string `json:"contentRating"`
DurationInMillis int `json:"durationInMillis"` DurationInMillis int `json:"durationInMillis"`
ReleaseDate string `json:"releaseDate"` ReleaseDate string `json:"releaseDate"`
Name string `json:"name"` Name string `json:"name"`
Isrc string `json:"isrc"` Isrc string `json:"isrc"`
AudioTraits []string `json:"audioTraits"` AudioTraits []string `json:"audioTraits"`
HasLyrics bool `json:"hasLyrics"` HasLyrics bool `json:"hasLyrics"`
AlbumName string `json:"albumName"` AlbumName string `json:"albumName"`
PlayParams struct { PlayParams struct {
ID string `json:"id"` ID string `json:"id"`
Kind string `json:"kind"` Kind string `json:"kind"`
} `json:"playParams"` } `json:"playParams"`
@ -2283,22 +2280,22 @@ type AutoGeneratedTrack struct {
TextColor3 string `json:"textColor3"` TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"` TextColor4 string `json:"textColor4"`
} `json:"artwork"` } `json:"artwork"`
ArtistName string `json:"artistName"` ArtistName string `json:"artistName"`
URL string `json:"url"` URL string `json:"url"`
DiscNumber int `json:"discNumber"` DiscNumber int `json:"discNumber"`
GenreNames []string `json:"genreNames"` GenreNames []string `json:"genreNames"`
HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"`
IsMasteredForItunes bool `json:"isMasteredForItunes"` IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"` ContentRating string `json:"contentRating"`
DurationInMillis int `json:"durationInMillis"` DurationInMillis int `json:"durationInMillis"`
ReleaseDate string `json:"releaseDate"` ReleaseDate string `json:"releaseDate"`
Name string `json:"name"` Name string `json:"name"`
Isrc string `json:"isrc"` Isrc string `json:"isrc"`
AudioTraits []string `json:"audioTraits"` AudioTraits []string `json:"audioTraits"`
HasLyrics bool `json:"hasLyrics"` HasLyrics bool `json:"hasLyrics"`
AlbumName string `json:"albumName"` AlbumName string `json:"albumName"`
PlayParams struct { PlayParams struct {
ID string `json:"id"` ID string `json:"id"`
Kind string `json:"kind"` Kind string `json:"kind"`
} `json:"playParams"` } `json:"playParams"`

@ -4,8 +4,10 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"gopkg.in/yaml.v2"
"errors" "errors"
"fmt" "fmt"
"github.com/beevik/etree"
"io" "io"
"io/ioutil" "io/ioutil"
"math" "math"
@ -21,9 +23,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/beevik/etree"
"gopkg.in/yaml.v2"
"github.com/abema/go-mp4" "github.com/abema/go-mp4"
"github.com/grafov/m3u8" "github.com/grafov/m3u8"
) )
@ -36,26 +35,25 @@ const (
var ( var (
forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`) forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`)
) )
type Config struct { type Config struct {
MediaUserToken string `yaml:"media-user-token"` MediaUserToken string `yaml:"media-user-token"`
SaveLrcFile bool `yaml:"save-lrc-file"` SaveLrcFile bool `yaml:"save-lrc-file"`
EmbedLrc bool `yaml:"embed-lrc"` EmbedLrc bool `yaml:"embed-lrc"`
EmbedCover bool `yaml:"embed-cover"` EmbedCover bool `yaml:"embed-cover"`
CoverSize string `yaml:"cover-size"` CoverSize string `yaml:"cover-size"`
CoverFormat string `yaml:"cover-format"` CoverFormat string `yaml:"cover-format"`
AlacSaveFolder string `yaml:"alac-save-folder"` AlacSaveFolder string `yaml:"alac-save-folder"`
AtmosSaveFolder string `yaml:"atmos-save-folder"` AtmosSaveFolder string `yaml:"atmos-save-folder"`
AlbumFolderFormat string `yaml:"album-folder-format"` AlbumFolderFormat string `yaml:"album-folder-format"`
PlaylistFolderFormat string `yaml:"playlist-folder-format"` PlaylistFolderFormat string `yaml:"playlist-folder-format"`
ArtistFolderFormat string `yaml:"artist-folder-format"` ArtistFolderFormat string `yaml:"artist-folder-format"`
SongFileFormat string `yaml:"song-file-format"` SongFileFormat string `yaml:"song-file-format"`
ExplicitChoice string `yaml:"explicit-choice"` ExplicitChoice string `yaml:"explicit-choice"`
CleanChoice string `yaml:"clean-choice"` CleanChoice string `yaml:"clean-choice"`
AppleMasterChoice string `yaml:"apple-master-choice"` AppleMasterChoice string `yaml:"apple-master-choice"`
AtmosMax int `yaml:"atmos-max"` AtmosMax int `yaml:"atmos-max"`
UseSongInfoForPlaylist bool `yaml:"use-songinfo-for-playlist"` UseSongInfoForPlaylist bool `yaml:"use-songinfo-for-playlist"`
DlAlbumcoverForPlaylist bool `yaml:"dl-albumcover-for-playlist"` DlAlbumcoverForPlaylist bool `yaml:"dl-albumcover-for-playlist"`
} }
var config Config var config Config
@ -75,17 +73,17 @@ type SongInfo struct {
} }
func loadConfig() error { func loadConfig() error {
// 读取config.yaml文件内容 // 读取config.yaml文件内容
data, err := ioutil.ReadFile("config.yaml") data, err := ioutil.ReadFile("config.yaml")
if err != nil { if err != nil {
return err return err
} }
// 将yaml解析到config变量中 // 将yaml解析到config变量中
err = yaml.Unmarshal(data, &config) err = yaml.Unmarshal(data, &config)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
func (s *SongInfo) Duration() (ret uint64) { func (s *SongInfo) Duration() (ret uint64) {
@ -1065,8 +1063,8 @@ func getSongLyrics(songId string, storefront string, token string, userToken str
} }
} }
func writeCover(sanAlbumFolder, name string, url string) error { func writeCover(sanAlbumFolder,name string, url string) error {
covPath := filepath.Join(sanAlbumFolder, name+"."+config.CoverFormat) covPath := filepath.Join(sanAlbumFolder, name+"." + 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.")
@ -1127,13 +1125,13 @@ func rip(albumId string, token string, storefront string, userToken string) erro
return err return err
} }
var singerFoldername string var singerFoldername string
if config.ArtistFolderFormat != "" { if config.ArtistFolderFormat != ""{
if strings.Contains(albumId, "pl.") { if strings.Contains(albumId, "pl.") {
singerFoldername = strings.NewReplacer( singerFoldername = strings.NewReplacer(
"{ArtistName}", "Apple Music", "{ArtistName}", "Apple Music",
"{ArtistId}", "", "{ArtistId}", "",
).Replace(config.ArtistFolderFormat) ).Replace(config.ArtistFolderFormat)
} else { }else{
singerFoldername = strings.NewReplacer( singerFoldername = strings.NewReplacer(
"{ArtistName}", meta.Data[0].Attributes.ArtistName, "{ArtistName}", meta.Data[0].Attributes.ArtistName,
"{ArtistId}", meta.Data[0].Relationships.Artists.Data[0].ID, "{ArtistId}", meta.Data[0].Relationships.Artists.Data[0].ID,
@ -1147,45 +1145,46 @@ func rip(albumId string, token string, storefront string, userToken string) erro
} }
singerFolder := filepath.Join(config.AtmosSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_")) singerFolder := filepath.Join(config.AtmosSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_"))
stringsToJoin := []string{} stringsToJoin := []string{}
if meta.Data[0].Attributes.IsAppleDigitalMaster || meta.Data[0].Attributes.IsMasteredForItunes { if meta.Data[0].Attributes.IsAppleDigitalMaster{
if config.AppleMasterChoice != "" { if config.AppleMasterChoice != ""{
stringsToJoin = append(stringsToJoin, config.AppleMasterChoice) stringsToJoin = append(stringsToJoin, config.AppleMasterChoice)
} }
} }
if meta.Data[0].Attributes.ContentRating == "explicit" { if meta.Data[0].Attributes.ContentRating=="explicit"{
if config.ExplicitChoice != "" { if config.ExplicitChoice != ""{
stringsToJoin = append(stringsToJoin, config.ExplicitChoice) stringsToJoin = append(stringsToJoin, config.ExplicitChoice)
} }
} }
if meta.Data[0].Attributes.ContentRating == "clean" { if meta.Data[0].Attributes.ContentRating=="clean"{
if config.CleanChoice != "" { if config.CleanChoice != ""{
stringsToJoin = append(stringsToJoin, config.CleanChoice) stringsToJoin = append(stringsToJoin, config.CleanChoice)
} }
} }
Tag_string := strings.Join(stringsToJoin, " ") Tag_string := strings.Join(stringsToJoin, " ")
var albumFolder string var albumFolder string
Quality := fmt.Sprintf("%dkbps", config.AtmosMax-2000) Quality:=fmt.Sprintf("%dkbps", config.AtmosMax-2000)
if strings.Contains(albumId, "pl.") { if strings.Contains(albumId, "pl.") {
albumFolder = strings.NewReplacer( albumFolder = strings.NewReplacer(
"{ArtistName}", "Apple Music", "{ArtistName}", "Apple Music",
"{PlaylistName}", meta.Data[0].Attributes.Name, "{PlaylistName}", meta.Data[0].Attributes.Name,
"{PlaylistId}", albumId, "{PlaylistId}", albumId,
"{Quality}", Quality, "{Quality}",Quality,
"{Codec}", "Atmos", "{Codec}", "Atmos",
"{Tag}", Tag_string, "{Tag}",Tag_string,
).Replace(config.PlaylistFolderFormat) ).Replace(config.PlaylistFolderFormat)
} else { }else{
albumFolder = strings.NewReplacer( albumFolder = strings.NewReplacer(
"{ReleaseDate}", meta.Data[0].Attributes.ReleaseDate, "{ReleaseDate}", meta.Data[0].Attributes.ReleaseDate,
"{ReleaseYear}", meta.Data[0].Attributes.ReleaseDate[:4], "{ReleaseYear}", meta.Data[0].Attributes.ReleaseDate[:4],
"{ArtistName}", meta.Data[0].Attributes.ArtistName, "{ArtistName}", meta.Data[0].Attributes.ArtistName,
"{AlbumName}", meta.Data[0].Attributes.Name, "{AlbumName}", meta.Data[0].Attributes.Name,
"{UPC}", meta.Data[0].Attributes.Upc, "{UPC}", meta.Data[0].Attributes.Upc,
"{RecordLabel}", meta.Data[0].Attributes.RecordLabel,
"{Copyright}", meta.Data[0].Attributes.Copyright, "{Copyright}", meta.Data[0].Attributes.Copyright,
"{AlbumId}", albumId, "{AlbumId}", albumId,
"{Quality}", Quality, "{Quality}",Quality,
"{Codec}", "Atmos", "{Codec}", "Atmos",
"{Tag}", Tag_string, "{Tag}",Tag_string,
).Replace(config.AlbumFolderFormat) ).Replace(config.AlbumFolderFormat)
} }
if strings.HasSuffix(albumFolder, ".") { if strings.HasSuffix(albumFolder, ".") {
@ -1195,7 +1194,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro
sanAlbumFolder := filepath.Join(singerFolder, forbiddenNames.ReplaceAllString(albumFolder, "_")) sanAlbumFolder := filepath.Join(singerFolder, forbiddenNames.ReplaceAllString(albumFolder, "_"))
os.MkdirAll(sanAlbumFolder, os.ModePerm) os.MkdirAll(sanAlbumFolder, os.ModePerm)
fmt.Println(albumFolder) fmt.Println(albumFolder)
err = writeCover(sanAlbumFolder, "cover", meta.Data[0].Attributes.Artwork.URL) err = writeCover(sanAlbumFolder,"cover", meta.Data[0].Attributes.Artwork.URL)
if err != nil { if err != nil {
fmt.Println("Failed to write cover.") fmt.Println("Failed to write cover.")
} }
@ -1213,20 +1212,20 @@ func rip(albumId string, token string, storefront string, userToken string) erro
fmt.Println("Unavailable in ALAC.") fmt.Println("Unavailable in ALAC.")
continue continue
} }
stringsToJoin := []string{} stringsToJoin := []string{}
if track.Attributes.IsAppleDigitalMaster { if track.Attributes.IsAppleDigitalMaster{
if config.AppleMasterChoice != "" { if config.AppleMasterChoice != ""{
stringsToJoin = append(stringsToJoin, config.AppleMasterChoice) stringsToJoin = append(stringsToJoin, config.AppleMasterChoice)
} }
} }
if track.Attributes.ContentRating == "explicit" { if track.Attributes.ContentRating=="explicit"{
if config.ExplicitChoice != "" { if config.ExplicitChoice != ""{
stringsToJoin = append(stringsToJoin, config.ExplicitChoice) stringsToJoin = append(stringsToJoin, config.ExplicitChoice)
} }
} }
if track.Attributes.ContentRating == "clean" { if track.Attributes.ContentRating=="clean"{
if config.CleanChoice != "" { if config.CleanChoice != ""{
stringsToJoin = append(stringsToJoin, config.CleanChoice) stringsToJoin = append(stringsToJoin, config.CleanChoice)
} }
} }
@ -1237,9 +1236,9 @@ func rip(albumId string, token string, storefront string, userToken string) erro
"{SongName}", track.Attributes.Name, "{SongName}", track.Attributes.Name,
"{DiscNumber}", string(track.Attributes.DiscNumber), "{DiscNumber}", string(track.Attributes.DiscNumber),
"{TrackNumber}", fmt.Sprintf("%02d", track.Attributes.TrackNumber), "{TrackNumber}", fmt.Sprintf("%02d", track.Attributes.TrackNumber),
"{Quality}", Quality, "{Quality}",Quality,
"{Codec}", "Atmos", "{Codec}", "Atmos",
"{Tag}", Tag_string, "{Tag}",Tag_string,
).Replace(config.SongFileFormat) ).Replace(config.SongFileFormat)
fmt.Println(songName) fmt.Println(songName)
filename := fmt.Sprintf("%s.ec3", forbiddenNames.ReplaceAllString(songName, "_")) filename := fmt.Sprintf("%s.ec3", forbiddenNames.ReplaceAllString(songName, "_"))
@ -1328,13 +1327,13 @@ func rip(albumId string, token string, storefront string, userToken string) erro
} }
if config.EmbedCover { if config.EmbedCover {
if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist { if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist {
err = writeCover(sanAlbumFolder, track.ID, track.Attributes.Artwork.URL) err = writeCover(sanAlbumFolder,track.ID, track.Attributes.Artwork.URL)
if err != nil { if err != nil {
fmt.Println("Failed to write cover.") fmt.Println("Failed to write cover.")
} }
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, track.ID, config.CoverFormat)) tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder,track.ID, config.CoverFormat))
} else { }else{
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, "cover", config.CoverFormat)) tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder,"cover", config.CoverFormat))
} }
} }
if strings.Contains(albumId, "pl.") && !config.UseSongInfoForPlaylist { if strings.Contains(albumId, "pl.") && !config.UseSongInfoForPlaylist {
@ -1342,17 +1341,17 @@ func rip(albumId string, token string, storefront string, userToken string) erro
tags = append(tags, fmt.Sprintf("track=%s", trackNum)) tags = append(tags, fmt.Sprintf("track=%s", trackNum))
tags = append(tags, fmt.Sprintf("tracknum=%d/%d", trackNum, trackTotal)) tags = append(tags, fmt.Sprintf("tracknum=%d/%d", trackNum, trackTotal))
tags = append(tags, fmt.Sprintf("album=%s", meta.Data[0].Attributes.Name)) tags = append(tags, fmt.Sprintf("album=%s", meta.Data[0].Attributes.Name))
} else { }else{
tags = append(tags, fmt.Sprintf("disk=%d/%d", meta.Data[0].Relationships.Tracks.Data[index].Attributes.DiscNumber, meta.Data[0].Relationships.Tracks.Data[trackTotal-1].Attributes.DiscNumber)) tags = append(tags, fmt.Sprintf("disk=%d/%d", meta.Data[0].Relationships.Tracks.Data[index].Attributes.DiscNumber, meta.Data[0].Relationships.Tracks.Data[trackTotal - 1].Attributes.DiscNumber))
tags = append(tags, fmt.Sprintf("track=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.TrackNumber)) tags = append(tags, fmt.Sprintf("track=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.TrackNumber))
tags = append(tags, fmt.Sprintf("tracknum=%d/%d", meta.Data[0].Relationships.Tracks.Data[index].Attributes.TrackNumber, trackTotal)) tags = append(tags, fmt.Sprintf("tracknum=%d/%d", meta.Data[0].Relationships.Tracks.Data[index].Attributes.TrackNumber, trackTotal))
tags = append(tags, fmt.Sprintf("album=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.AlbumName)) tags = append(tags, fmt.Sprintf("album=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.AlbumName))
} }
if track.Attributes.ContentRating == "explicit" { if track.Attributes.ContentRating=="explicit"{
tags = append(tags, "rating=1") tags = append(tags, "rating=1")
} else if track.Attributes.ContentRating == "clean" { }else if track.Attributes.ContentRating=="clean"{
tags = append(tags, "rating=2") tags = append(tags, "rating=2")
} else { }else{
tags = append(tags, "rating=0") tags = append(tags, "rating=0")
} }
tagsString := strings.Join(tags, ":") tagsString := strings.Join(tags, ":")
@ -1368,8 +1367,8 @@ func rip(albumId string, token string, storefront string, userToken string) erro
continue continue
} }
if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist { if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist {
if err := os.Remove(fmt.Sprintf("%s/%s.%s", sanAlbumFolder, track.ID, config.CoverFormat)); err != nil { if err := os.Remove(fmt.Sprintf("%s/%s.%s", sanAlbumFolder,track.ID, config.CoverFormat)); err != nil {
fmt.Printf("Error deleting file: %s/%s.%s\n", sanAlbumFolder, track.ID, config.CoverFormat) fmt.Printf("Error deleting file: %s/%s.%s\n", sanAlbumFolder,track.ID, config.CoverFormat)
continue continue
} }
} }
@ -1492,11 +1491,11 @@ func extractMedia(b string) (string, []string, error) {
if variant.Codecs == "ec-3" { if variant.Codecs == "ec-3" {
split := strings.Split(variant.Audio, "-") split := strings.Split(variant.Audio, "-")
length := len(split) length := len(split)
length_int, err := strconv.Atoi(split[length-1]) length_int,err := strconv.Atoi(split[length-1])
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
if length_int <= config.AtmosMax { if length_int <= config.AtmosMax{
fmt.Printf("%s\n", variant.Audio) fmt.Printf("%s\n", variant.Audio)
streamUrlTemp, err := masterUrl.Parse(variant.URI) streamUrlTemp, err := masterUrl.Parse(variant.URI)
if err != nil { if err != nil {
@ -1766,32 +1765,32 @@ type SongAttributes struct {
ExtendedAssetUrls struct { ExtendedAssetUrls struct {
EnhancedHls string `json:"enhancedHls"` EnhancedHls string `json:"enhancedHls"`
} `json:"extendedAssetUrls"` } `json:"extendedAssetUrls"`
IsMasteredForItunes bool `json:"isMasteredForItunes"` IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"` ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"` ReleaseDate string `json:"releaseDate"`
Name string `json:"name"` Name string `json:"name"`
Isrc string `json:"isrc"` Isrc string `json:"isrc"`
AlbumName string `json:"albumName"` AlbumName string `json:"albumName"`
TrackNumber int `json:"trackNumber"` TrackNumber int `json:"trackNumber"`
ComposerName string `json:"composerName"` ComposerName string `json:"composerName"`
} }
type AlbumAttributes struct { type AlbumAttributes struct {
ArtistName string `json:"artistName"` ArtistName string `json:"artistName"`
IsSingle bool `json:"isSingle"` IsSingle bool `json:"isSingle"`
IsComplete bool `json:"isComplete"` IsComplete bool `json:"isComplete"`
GenreNames []string `json:"genreNames"` GenreNames []string `json:"genreNames"`
TrackCount int `json:"trackCount"` TrackCount int `json:"trackCount"`
IsMasteredForItunes bool `json:"isMasteredForItunes"` IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"` ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"` ReleaseDate string `json:"releaseDate"`
Name string `json:"name"` Name string `json:"name"`
RecordLabel string `json:"recordLabel"` RecordLabel string `json:"recordLabel"`
Upc string `json:"upc"` Upc string `json:"upc"`
Copyright string `json:"copyright"` Copyright string `json:"copyright"`
IsCompilation bool `json:"isCompilation"` IsCompilation bool `json:"isCompilation"`
} }
type SongData struct { type SongData struct {
@ -1953,22 +1952,22 @@ type AutoGenerated struct {
TextColor3 string `json:"textColor3"` TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"` TextColor4 string `json:"textColor4"`
} `json:"artwork"` } `json:"artwork"`
ArtistName string `json:"artistName"` ArtistName string `json:"artistName"`
IsSingle bool `json:"isSingle"` IsSingle bool `json:"isSingle"`
URL string `json:"url"` URL string `json:"url"`
IsComplete bool `json:"isComplete"` IsComplete bool `json:"isComplete"`
GenreNames []string `json:"genreNames"` GenreNames []string `json:"genreNames"`
TrackCount int `json:"trackCount"` TrackCount int `json:"trackCount"`
IsMasteredForItunes bool `json:"isMasteredForItunes"` IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"` ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"` ReleaseDate string `json:"releaseDate"`
Name string `json:"name"` Name string `json:"name"`
RecordLabel string `json:"recordLabel"` RecordLabel string `json:"recordLabel"`
Upc string `json:"upc"` Upc string `json:"upc"`
AudioTraits []string `json:"audioTraits"` AudioTraits []string `json:"audioTraits"`
Copyright string `json:"copyright"` Copyright string `json:"copyright"`
PlayParams struct { PlayParams struct {
ID string `json:"id"` ID string `json:"id"`
Kind string `json:"kind"` Kind string `json:"kind"`
} `json:"playParams"` } `json:"playParams"`
@ -2011,22 +2010,22 @@ type AutoGenerated struct {
TextColor3 string `json:"textColor3"` TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"` TextColor4 string `json:"textColor4"`
} `json:"artwork"` } `json:"artwork"`
ArtistName string `json:"artistName"` ArtistName string `json:"artistName"`
URL string `json:"url"` URL string `json:"url"`
DiscNumber int `json:"discNumber"` DiscNumber int `json:"discNumber"`
GenreNames []string `json:"genreNames"` GenreNames []string `json:"genreNames"`
HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"`
IsMasteredForItunes bool `json:"isMasteredForItunes"` IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"` ContentRating string `json:"contentRating"`
DurationInMillis int `json:"durationInMillis"` DurationInMillis int `json:"durationInMillis"`
ReleaseDate string `json:"releaseDate"` ReleaseDate string `json:"releaseDate"`
Name string `json:"name"` Name string `json:"name"`
Isrc string `json:"isrc"` Isrc string `json:"isrc"`
AudioTraits []string `json:"audioTraits"` AudioTraits []string `json:"audioTraits"`
HasLyrics bool `json:"hasLyrics"` HasLyrics bool `json:"hasLyrics"`
AlbumName string `json:"albumName"` AlbumName string `json:"albumName"`
PlayParams struct { PlayParams struct {
ID string `json:"id"` ID string `json:"id"`
Kind string `json:"kind"` Kind string `json:"kind"`
} `json:"playParams"` } `json:"playParams"`
@ -2074,22 +2073,22 @@ type AutoGeneratedTrack struct {
TextColor3 string `json:"textColor3"` TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"` TextColor4 string `json:"textColor4"`
} `json:"artwork"` } `json:"artwork"`
ArtistName string `json:"artistName"` ArtistName string `json:"artistName"`
URL string `json:"url"` URL string `json:"url"`
DiscNumber int `json:"discNumber"` DiscNumber int `json:"discNumber"`
GenreNames []string `json:"genreNames"` GenreNames []string `json:"genreNames"`
HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"`
IsMasteredForItunes bool `json:"isMasteredForItunes"` IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"` ContentRating string `json:"contentRating"`
DurationInMillis int `json:"durationInMillis"` DurationInMillis int `json:"durationInMillis"`
ReleaseDate string `json:"releaseDate"` ReleaseDate string `json:"releaseDate"`
Name string `json:"name"` Name string `json:"name"`
Isrc string `json:"isrc"` Isrc string `json:"isrc"`
AudioTraits []string `json:"audioTraits"` AudioTraits []string `json:"audioTraits"`
HasLyrics bool `json:"hasLyrics"` HasLyrics bool `json:"hasLyrics"`
AlbumName string `json:"albumName"` AlbumName string `json:"albumName"`
PlayParams struct { PlayParams struct {
ID string `json:"id"` ID string `json:"id"`
Kind string `json:"kind"` Kind string `json:"kind"`
} `json:"playParams"` } `json:"playParams"`

@ -5,8 +5,10 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"gopkg.in/yaml.v2"
"errors" "errors"
"fmt" "fmt"
"github.com/beevik/etree"
"io" "io"
"io/ioutil" "io/ioutil"
"math" "math"
@ -22,9 +24,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/beevik/etree"
"gopkg.in/yaml.v2"
"github.com/abema/go-mp4" "github.com/abema/go-mp4"
"github.com/grafov/m3u8" "github.com/grafov/m3u8"
) )
@ -37,26 +36,25 @@ const (
var ( var (
forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`) forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`)
) )
type Config struct { type Config struct {
MediaUserToken string `yaml:"media-user-token"` MediaUserToken string `yaml:"media-user-token"`
SaveLrcFile bool `yaml:"save-lrc-file"` SaveLrcFile bool `yaml:"save-lrc-file"`
EmbedLrc bool `yaml:"embed-lrc"` EmbedLrc bool `yaml:"embed-lrc"`
EmbedCover bool `yaml:"embed-cover"` EmbedCover bool `yaml:"embed-cover"`
CoverSize string `yaml:"cover-size"` CoverSize string `yaml:"cover-size"`
CoverFormat string `yaml:"cover-format"` CoverFormat string `yaml:"cover-format"`
AlacSaveFolder string `yaml:"alac-save-folder"` AlacSaveFolder string `yaml:"alac-save-folder"`
AtmosSaveFolder string `yaml:"atmos-save-folder"` AtmosSaveFolder string `yaml:"atmos-save-folder"`
AlbumFolderFormat string `yaml:"album-folder-format"` AlbumFolderFormat string `yaml:"album-folder-format"`
PlaylistFolderFormat string `yaml:"playlist-folder-format"` PlaylistFolderFormat string `yaml:"playlist-folder-format"`
ArtistFolderFormat string `yaml:"artist-folder-format"` ArtistFolderFormat string `yaml:"artist-folder-format"`
SongFileFormat string `yaml:"song-file-format"` SongFileFormat string `yaml:"song-file-format"`
ExplicitChoice string `yaml:"explicit-choice"` ExplicitChoice string `yaml:"explicit-choice"`
CleanChoice string `yaml:"clean-choice"` CleanChoice string `yaml:"clean-choice"`
AppleMasterChoice string `yaml:"apple-master-choice"` AppleMasterChoice string `yaml:"apple-master-choice"`
AlacMax int `yaml:"alac-max"` AlacMax int `yaml:"alac-max"`
UseSongInfoForPlaylist bool `yaml:"use-songinfo-for-playlist"` UseSongInfoForPlaylist bool `yaml:"use-songinfo-for-playlist"`
DlAlbumcoverForPlaylist bool `yaml:"dl-albumcover-for-playlist"` DlAlbumcoverForPlaylist bool `yaml:"dl-albumcover-for-playlist"`
} }
var config Config var config Config
@ -74,17 +72,17 @@ type SongInfo struct {
} }
func loadConfig() error { func loadConfig() error {
// 读取config.yaml文件内容 // 读取config.yaml文件内容
data, err := ioutil.ReadFile("config.yaml") data, err := ioutil.ReadFile("config.yaml")
if err != nil { if err != nil {
return err return err
} }
// 将yaml解析到config变量中 // 将yaml解析到config变量中
err = yaml.Unmarshal(data, &config) err = yaml.Unmarshal(data, &config)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
func (s *SongInfo) Duration() (ret uint64) { func (s *SongInfo) Duration() (ret uint64) {
@ -687,10 +685,10 @@ func writeM4a(w *mp4.Writer, info *SongInfo, meta *AutoGenerated, data []byte, t
if err != nil { if err != nil {
return err return err
} }
AlbumName := meta.Data[0].Relationships.Tracks.Data[index].Attributes.AlbumName AlbumName:=meta.Data[0].Relationships.Tracks.Data[index].Attributes.AlbumName
if strings.Contains(meta.Data[0].ID, "pl.") { if strings.Contains(meta.Data[0].ID, "pl."){
if !config.UseSongInfoForPlaylist { if !config.UseSongInfoForPlaylist {
AlbumName = meta.Data[0].Attributes.Name AlbumName=meta.Data[0].Attributes.Name
} }
} }
err = addMeta(mp4.BoxType{'\251', 'a', 'l', 'b'}, AlbumName) err = addMeta(mp4.BoxType{'\251', 'a', 'l', 'b'}, AlbumName)
@ -801,13 +799,13 @@ func writeM4a(w *mp4.Writer, info *SongInfo, meta *AutoGenerated, data []byte, t
if err != nil { if err != nil {
return err return err
} }
if !strings.Contains(meta.Data[0].ID, "pl.") { if !strings.Contains(meta.Data[0].ID, "pl."){
plID, err := strconv.ParseUint(meta.Data[0].ID, 10, 32) plID, err := strconv.ParseUint(meta.Data[0].ID, 10, 32)
if err != nil { if err != nil {
return err return err
} }
err = addMeta(mp4.BoxType{'p', 'l', 'I', 'D'}, uint32(plID)) err = addMeta(mp4.BoxType{'p', 'l', 'I', 'D'}, uint32(plID))
if err != nil { if err != nil {
return err return err
@ -832,8 +830,8 @@ func writeM4a(w *mp4.Writer, info *SongInfo, meta *AutoGenerated, data []byte, t
binary.BigEndian.PutUint32(trkn, uint32(meta.Data[0].Relationships.Tracks.Data[index].Attributes.TrackNumber)) binary.BigEndian.PutUint32(trkn, uint32(meta.Data[0].Relationships.Tracks.Data[index].Attributes.TrackNumber))
binary.BigEndian.PutUint16(trkn[4:], uint16(trackTotal)) binary.BigEndian.PutUint16(trkn[4:], uint16(trackTotal))
binary.BigEndian.PutUint32(disk, uint32(meta.Data[0].Relationships.Tracks.Data[index].Attributes.DiscNumber)) binary.BigEndian.PutUint32(disk, uint32(meta.Data[0].Relationships.Tracks.Data[index].Attributes.DiscNumber))
binary.BigEndian.PutUint16(disk[4:], uint16(meta.Data[0].Relationships.Tracks.Data[trackTotal-1].Attributes.DiscNumber)) binary.BigEndian.PutUint16(disk[4:], uint16(meta.Data[0].Relationships.Tracks.Data[trackTotal - 1].Attributes.DiscNumber))
if strings.Contains(meta.Data[0].ID, "pl.") { if strings.Contains(meta.Data[0].ID, "pl."){
if !config.UseSongInfoForPlaylist { if !config.UseSongInfoForPlaylist {
binary.BigEndian.PutUint32(trkn, uint32(trackNum)) binary.BigEndian.PutUint32(trkn, uint32(trackNum))
binary.BigEndian.PutUint16(trkn[4:], uint16(trackTotal)) binary.BigEndian.PutUint16(trkn[4:], uint16(trackTotal))
@ -1113,8 +1111,8 @@ func getSongLyrics(songId string, storefront string, token string, userToken str
} }
} }
func writeCover(sanAlbumFolder, name string, url string) error { func writeCover(sanAlbumFolder,name string, url string) error {
covPath := filepath.Join(sanAlbumFolder, name+"."+config.CoverFormat) covPath := filepath.Join(sanAlbumFolder, name+"." + 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.")
@ -1185,13 +1183,13 @@ func rip(albumId string, token string, storefront string, userToken string) erro
return err return err
} }
var singerFoldername string var singerFoldername string
if config.ArtistFolderFormat != "" { if config.ArtistFolderFormat != ""{
if strings.Contains(albumId, "pl.") { if strings.Contains(albumId, "pl.") {
singerFoldername = strings.NewReplacer( singerFoldername = strings.NewReplacer(
"{ArtistName}", "Apple Music", "{ArtistName}", "Apple Music",
"{ArtistId}", "", "{ArtistId}", "",
).Replace(config.ArtistFolderFormat) ).Replace(config.ArtistFolderFormat)
} else { }else{
singerFoldername = strings.NewReplacer( singerFoldername = strings.NewReplacer(
"{ArtistName}", meta.Data[0].Attributes.ArtistName, "{ArtistName}", meta.Data[0].Attributes.ArtistName,
"{ArtistId}", meta.Data[0].Relationships.Artists.Data[0].ID, "{ArtistId}", meta.Data[0].Relationships.Artists.Data[0].ID,
@ -1205,18 +1203,18 @@ func rip(albumId string, token string, storefront string, userToken string) erro
} }
singerFolder := filepath.Join(config.AlacSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_")) singerFolder := filepath.Join(config.AlacSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_"))
stringsToJoin := []string{} stringsToJoin := []string{}
if meta.Data[0].Attributes.IsAppleDigitalMaster || meta.Data[0].Attributes.IsMasteredForItunes { if meta.Data[0].Attributes.IsAppleDigitalMaster{
if config.AppleMasterChoice != "" { if config.AppleMasterChoice != ""{
stringsToJoin = append(stringsToJoin, config.AppleMasterChoice) stringsToJoin = append(stringsToJoin, config.AppleMasterChoice)
} }
} }
if meta.Data[0].Attributes.ContentRating == "explicit" { if meta.Data[0].Attributes.ContentRating=="explicit"{
if config.ExplicitChoice != "" { if config.ExplicitChoice != ""{
stringsToJoin = append(stringsToJoin, config.ExplicitChoice) stringsToJoin = append(stringsToJoin, config.ExplicitChoice)
} }
} }
if meta.Data[0].Attributes.ContentRating == "clean" { if meta.Data[0].Attributes.ContentRating=="clean"{
if config.CleanChoice != "" { if config.CleanChoice != ""{
stringsToJoin = append(stringsToJoin, config.CleanChoice) stringsToJoin = append(stringsToJoin, config.CleanChoice)
} }
} }
@ -1229,8 +1227,8 @@ func rip(albumId string, token string, storefront string, userToken string) erro
fmt.Println("Unavailable in ALAC.") fmt.Println("Unavailable in ALAC.")
} }
var Quality string var Quality string
if strings.Contains(config.AlbumFolderFormat, "Quality") { if strings.Contains(config.AlbumFolderFormat, "Quality"){
Quality, err = extractMediaQuality(manifest1.Attributes.ExtendedAssetUrls.EnhancedHls) Quality,err = extractMediaQuality(manifest1.Attributes.ExtendedAssetUrls.EnhancedHls)
if err != nil { if err != nil {
fmt.Println("Failed to extract quality from manifest.\n", err) fmt.Println("Failed to extract quality from manifest.\n", err)
} }
@ -1241,22 +1239,23 @@ func rip(albumId string, token string, storefront string, userToken string) erro
"{ArtistName}", "Apple Music", "{ArtistName}", "Apple Music",
"{PlaylistName}", meta.Data[0].Attributes.Name, "{PlaylistName}", meta.Data[0].Attributes.Name,
"{PlaylistId}", albumId, "{PlaylistId}", albumId,
"{Quality}", Quality, "{Quality}",Quality,
"{Codec}", "ALAC", "{Codec}", "ALAC",
"{Tag}", Tag_string, "{Tag}",Tag_string,
).Replace(config.PlaylistFolderFormat) ).Replace(config.PlaylistFolderFormat)
} else { }else{
albumFolder = strings.NewReplacer( albumFolder = strings.NewReplacer(
"{ReleaseDate}", meta.Data[0].Attributes.ReleaseDate, "{ReleaseDate}", meta.Data[0].Attributes.ReleaseDate,
"{ReleaseYear}", meta.Data[0].Attributes.ReleaseDate[:4], "{ReleaseYear}", meta.Data[0].Attributes.ReleaseDate[:4],
"{ArtistName}", meta.Data[0].Attributes.ArtistName, "{ArtistName}", meta.Data[0].Attributes.ArtistName,
"{AlbumName}", meta.Data[0].Attributes.Name, "{AlbumName}", meta.Data[0].Attributes.Name,
"{UPC}", meta.Data[0].Attributes.Upc, "{UPC}", meta.Data[0].Attributes.Upc,
"{RecordLabel}", meta.Data[0].Attributes.RecordLabel,
"{Copyright}", meta.Data[0].Attributes.Copyright, "{Copyright}", meta.Data[0].Attributes.Copyright,
"{AlbumId}", albumId, "{AlbumId}", albumId,
"{Quality}", Quality, "{Quality}", Quality,
"{Codec}", "ALAC", "{Codec}", "ALAC",
"{Tag}", Tag_string, "{Tag}",Tag_string,
).Replace(config.AlbumFolderFormat) ).Replace(config.AlbumFolderFormat)
} }
if strings.HasSuffix(albumFolder, ".") { if strings.HasSuffix(albumFolder, ".") {
@ -1266,7 +1265,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro
sanAlbumFolder := filepath.Join(singerFolder, forbiddenNames.ReplaceAllString(albumFolder, "_")) sanAlbumFolder := filepath.Join(singerFolder, forbiddenNames.ReplaceAllString(albumFolder, "_"))
os.MkdirAll(sanAlbumFolder, os.ModePerm) os.MkdirAll(sanAlbumFolder, os.ModePerm)
fmt.Println(albumFolder) fmt.Println(albumFolder)
err = writeCover(sanAlbumFolder, "cover", meta.Data[0].Attributes.Artwork.URL) err = writeCover(sanAlbumFolder,"cover", meta.Data[0].Attributes.Artwork.URL)
if err != nil { if err != nil {
fmt.Println("Failed to write cover.") fmt.Println("Failed to write cover.")
} }
@ -1285,7 +1284,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro
manually_txt := false manually_txt := false
m3u8_txt := "" m3u8_txt := ""
if strings.Contains(input, "txt") { if strings.Contains(input, "txt") {
m3u8_txt = strings.TrimSpace(input) m3u8_txt= strings.TrimSpace(input)
fmt.Print(m3u8_txt) fmt.Print(m3u8_txt)
strArr := make([]string, len(arr)) strArr := make([]string, len(arr))
for i, num := range arr { for i, num := range arr {
@ -1293,11 +1292,11 @@ func rip(albumId string, token string, storefront string, userToken string) erro
} }
input = strings.Join(strArr, " ") input = strings.Join(strArr, " ")
manually_txt = true manually_txt = true
} }
if strings.Contains(input, "#") { if strings.Contains(input, "#") {
input = strings.ReplaceAll(input, "#", "") input = strings.ReplaceAll(input, "#", "")
manually = true manually = true
} }
input = strings.TrimSpace(input) input = strings.TrimSpace(input)
inputs := strings.Fields(input) inputs := strings.Fields(input)
for _, str := range inputs { for _, str := range inputs {
@ -1329,26 +1328,26 @@ func rip(albumId string, token string, storefront string, userToken string) erro
continue continue
} }
var Quality string var Quality string
if strings.Contains(config.SongFileFormat, "Quality") { if strings.Contains(config.SongFileFormat, "Quality"){
Quality, err = extractMediaQuality(manifest.Attributes.ExtendedAssetUrls.EnhancedHls) Quality,err = extractMediaQuality(manifest.Attributes.ExtendedAssetUrls.EnhancedHls)
if err != nil { if err != nil {
fmt.Println("Failed to extract quality from manifest.\n", err) fmt.Println("Failed to extract quality from manifest.\n", err)
continue continue
} }
} }
stringsToJoin := []string{} stringsToJoin := []string{}
if track.Attributes.IsAppleDigitalMaster { if track.Attributes.IsAppleDigitalMaster{
if config.AppleMasterChoice != "" { if config.AppleMasterChoice != ""{
stringsToJoin = append(stringsToJoin, config.AppleMasterChoice) stringsToJoin = append(stringsToJoin, config.AppleMasterChoice)
} }
} }
if track.Attributes.ContentRating == "explicit" { if track.Attributes.ContentRating=="explicit"{
if config.ExplicitChoice != "" { if config.ExplicitChoice != ""{
stringsToJoin = append(stringsToJoin, config.ExplicitChoice) stringsToJoin = append(stringsToJoin, config.ExplicitChoice)
} }
} }
if track.Attributes.ContentRating == "clean" { if track.Attributes.ContentRating=="clean"{
if config.CleanChoice != "" { if config.CleanChoice != ""{
stringsToJoin = append(stringsToJoin, config.CleanChoice) stringsToJoin = append(stringsToJoin, config.CleanChoice)
} }
} }
@ -1359,9 +1358,9 @@ func rip(albumId string, token string, storefront string, userToken string) erro
"{SongName}", track.Attributes.Name, "{SongName}", track.Attributes.Name,
"{DiscNumber}", string(track.Attributes.DiscNumber), "{DiscNumber}", string(track.Attributes.DiscNumber),
"{TrackNumber}", fmt.Sprintf("%02d", track.Attributes.TrackNumber), "{TrackNumber}", fmt.Sprintf("%02d", track.Attributes.TrackNumber),
"{Quality}", Quality, "{Quality}",Quality,
"{Codec}", "ALAC", "{Codec}", "ALAC",
"{Tag}", Tag_string, "{Tag}",Tag_string,
).Replace(config.SongFileFormat) ).Replace(config.SongFileFormat)
fmt.Println(songName) fmt.Println(songName)
filename := fmt.Sprintf("%s.m4a", forbiddenNames.ReplaceAllString(songName, "_")) filename := fmt.Sprintf("%s.m4a", forbiddenNames.ReplaceAllString(songName, "_"))
@ -1409,7 +1408,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro
if strings.HasPrefix(line, track.ID) { if strings.HasPrefix(line, track.ID) {
parts := strings.SplitN(line, ",", 2) parts := strings.SplitN(line, ",", 2)
if len(parts) == 2 { if len(parts) == 2 {
manifest.Attributes.ExtendedAssetUrls.EnhancedHls = parts[1] manifest.Attributes.ExtendedAssetUrls.EnhancedHls=parts[1]
} }
} }
} }
@ -1425,7 +1424,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro
fmt.Println(err) fmt.Println(err)
} }
m3u8_url = strings.TrimSpace(m3u8_url) m3u8_url = strings.TrimSpace(m3u8_url)
manifest.Attributes.ExtendedAssetUrls.EnhancedHls = m3u8_url manifest.Attributes.ExtendedAssetUrls.EnhancedHls=m3u8_url
} }
trackUrl, keys, err := extractMedia(manifest.Attributes.ExtendedAssetUrls.EnhancedHls) trackUrl, keys, err := extractMedia(manifest.Attributes.ExtendedAssetUrls.EnhancedHls)
if err != nil { if err != nil {
@ -1460,21 +1459,21 @@ func rip(albumId string, token string, storefront string, userToken string) erro
} }
if config.EmbedCover { if config.EmbedCover {
if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist { if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist {
err = writeCover(sanAlbumFolder, track.ID, track.Attributes.Artwork.URL) err = writeCover(sanAlbumFolder,track.ID, track.Attributes.Artwork.URL)
if err != nil { if err != nil {
fmt.Println("Failed to write cover.") fmt.Println("Failed to write cover.")
} }
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, track.ID, config.CoverFormat)) tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder,track.ID, config.CoverFormat))
} else { }else{
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, "cover", config.CoverFormat)) tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder,"cover", config.CoverFormat))
} }
} }
if track.Attributes.ContentRating == "explicit" { if track.Attributes.ContentRating=="explicit"{
tags = append(tags, "rating=1") tags = append(tags, "rating=1")
} else if track.Attributes.ContentRating == "clean" { }else if track.Attributes.ContentRating=="clean"{
tags = append(tags, "rating=2") tags = append(tags, "rating=2")
} else { }else{
tags = append(tags, "rating=0") tags = append(tags, "rating=0")
} }
tagsString := strings.Join(tags, ":") tagsString := strings.Join(tags, ":")
@ -1484,8 +1483,8 @@ func rip(albumId string, token string, storefront string, userToken string) erro
continue continue
} }
if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist { if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist {
if err := os.Remove(fmt.Sprintf("%s/%s.%s", sanAlbumFolder, track.ID, config.CoverFormat)); err != nil { if err := os.Remove(fmt.Sprintf("%s/%s.%s", sanAlbumFolder,track.ID, config.CoverFormat)); err != nil {
fmt.Printf("Error deleting file: %s/%s.%s\n", sanAlbumFolder, track.ID, config.CoverFormat) fmt.Printf("Error deleting file: %s/%s.%s\n", sanAlbumFolder,track.ID, config.CoverFormat)
continue continue
} }
} }
@ -1601,16 +1600,16 @@ func extractMediaQuality(b string) (string, error) {
if variant.Codecs == "alac" { if variant.Codecs == "alac" {
split := strings.Split(variant.Audio, "-") split := strings.Split(variant.Audio, "-")
length := len(split) length := len(split)
length_int, err := strconv.Atoi(split[length-2]) length_int,err := strconv.Atoi(split[length-2])
if err != nil { if err != nil {
return "", err return "", err
} }
if length_int <= config.AlacMax { if length_int <= config.AlacMax{
HZ, err := strconv.Atoi(split[length-2]) HZ,err:=strconv.Atoi(split[length-2])
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
KHZ := float64(HZ) / 1000.0 KHZ:=float64(HZ) / 1000.0
Quality = fmt.Sprintf("%sB-%.1fkHz", split[length-1], KHZ) Quality = fmt.Sprintf("%sB-%.1fkHz", split[length-1], KHZ)
break break
} }
@ -1650,11 +1649,11 @@ func extractMedia(b string) (string, []string, error) {
if variant.Codecs == "alac" { if variant.Codecs == "alac" {
split := strings.Split(variant.Audio, "-") split := strings.Split(variant.Audio, "-")
length := len(split) length := len(split)
length_int, err := strconv.Atoi(split[length-2]) length_int,err := strconv.Atoi(split[length-2])
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
if length_int <= config.AlacMax { if length_int <= config.AlacMax{
fmt.Printf("%s-bit / %s Hz\n", split[length-1], split[length-2]) fmt.Printf("%s-bit / %s Hz\n", split[length-1], split[length-2])
streamUrlTemp, err := masterUrl.Parse(variant.URI) streamUrlTemp, err := masterUrl.Parse(variant.URI)
if err != nil { if err != nil {
@ -1923,32 +1922,32 @@ type SongAttributes struct {
ExtendedAssetUrls struct { ExtendedAssetUrls struct {
EnhancedHls string `json:"enhancedHls"` EnhancedHls string `json:"enhancedHls"`
} `json:"extendedAssetUrls"` } `json:"extendedAssetUrls"`
IsMasteredForItunes bool `json:"isMasteredForItunes"` IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"` ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"` ReleaseDate string `json:"releaseDate"`
Name string `json:"name"` Name string `json:"name"`
Isrc string `json:"isrc"` Isrc string `json:"isrc"`
AlbumName string `json:"albumName"` AlbumName string `json:"albumName"`
TrackNumber int `json:"trackNumber"` TrackNumber int `json:"trackNumber"`
ComposerName string `json:"composerName"` ComposerName string `json:"composerName"`
} }
type AlbumAttributes struct { type AlbumAttributes struct {
ArtistName string `json:"artistName"` ArtistName string `json:"artistName"`
IsSingle bool `json:"isSingle"` IsSingle bool `json:"isSingle"`
IsComplete bool `json:"isComplete"` IsComplete bool `json:"isComplete"`
GenreNames []string `json:"genreNames"` GenreNames []string `json:"genreNames"`
TrackCount int `json:"trackCount"` TrackCount int `json:"trackCount"`
IsMasteredForItunes bool `json:"isMasteredForItunes"` IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"` ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"` ReleaseDate string `json:"releaseDate"`
Name string `json:"name"` Name string `json:"name"`
RecordLabel string `json:"recordLabel"` RecordLabel string `json:"recordLabel"`
Upc string `json:"upc"` Upc string `json:"upc"`
Copyright string `json:"copyright"` Copyright string `json:"copyright"`
IsCompilation bool `json:"isCompilation"` IsCompilation bool `json:"isCompilation"`
} }
type SongData struct { type SongData struct {
@ -2110,22 +2109,22 @@ type AutoGenerated struct {
TextColor3 string `json:"textColor3"` TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"` TextColor4 string `json:"textColor4"`
} `json:"artwork"` } `json:"artwork"`
ArtistName string `json:"artistName"` ArtistName string `json:"artistName"`
IsSingle bool `json:"isSingle"` IsSingle bool `json:"isSingle"`
URL string `json:"url"` URL string `json:"url"`
IsComplete bool `json:"isComplete"` IsComplete bool `json:"isComplete"`
GenreNames []string `json:"genreNames"` GenreNames []string `json:"genreNames"`
TrackCount int `json:"trackCount"` TrackCount int `json:"trackCount"`
IsMasteredForItunes bool `json:"isMasteredForItunes"` IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"` ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"` ReleaseDate string `json:"releaseDate"`
Name string `json:"name"` Name string `json:"name"`
RecordLabel string `json:"recordLabel"` RecordLabel string `json:"recordLabel"`
Upc string `json:"upc"` Upc string `json:"upc"`
AudioTraits []string `json:"audioTraits"` AudioTraits []string `json:"audioTraits"`
Copyright string `json:"copyright"` Copyright string `json:"copyright"`
PlayParams struct { PlayParams struct {
ID string `json:"id"` ID string `json:"id"`
Kind string `json:"kind"` Kind string `json:"kind"`
} `json:"playParams"` } `json:"playParams"`
@ -2168,22 +2167,22 @@ type AutoGenerated struct {
TextColor3 string `json:"textColor3"` TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"` TextColor4 string `json:"textColor4"`
} `json:"artwork"` } `json:"artwork"`
ArtistName string `json:"artistName"` ArtistName string `json:"artistName"`
URL string `json:"url"` URL string `json:"url"`
DiscNumber int `json:"discNumber"` DiscNumber int `json:"discNumber"`
GenreNames []string `json:"genreNames"` GenreNames []string `json:"genreNames"`
HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"`
IsMasteredForItunes bool `json:"isMasteredForItunes"` IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"` ContentRating string `json:"contentRating"`
DurationInMillis int `json:"durationInMillis"` DurationInMillis int `json:"durationInMillis"`
ReleaseDate string `json:"releaseDate"` ReleaseDate string `json:"releaseDate"`
Name string `json:"name"` Name string `json:"name"`
Isrc string `json:"isrc"` Isrc string `json:"isrc"`
AudioTraits []string `json:"audioTraits"` AudioTraits []string `json:"audioTraits"`
HasLyrics bool `json:"hasLyrics"` HasLyrics bool `json:"hasLyrics"`
AlbumName string `json:"albumName"` AlbumName string `json:"albumName"`
PlayParams struct { PlayParams struct {
ID string `json:"id"` ID string `json:"id"`
Kind string `json:"kind"` Kind string `json:"kind"`
} `json:"playParams"` } `json:"playParams"`
@ -2231,22 +2230,22 @@ type AutoGeneratedTrack struct {
TextColor3 string `json:"textColor3"` TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"` TextColor4 string `json:"textColor4"`
} `json:"artwork"` } `json:"artwork"`
ArtistName string `json:"artistName"` ArtistName string `json:"artistName"`
URL string `json:"url"` URL string `json:"url"`
DiscNumber int `json:"discNumber"` DiscNumber int `json:"discNumber"`
GenreNames []string `json:"genreNames"` GenreNames []string `json:"genreNames"`
HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"`
IsMasteredForItunes bool `json:"isMasteredForItunes"` IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"` ContentRating string `json:"contentRating"`
DurationInMillis int `json:"durationInMillis"` DurationInMillis int `json:"durationInMillis"`
ReleaseDate string `json:"releaseDate"` ReleaseDate string `json:"releaseDate"`
Name string `json:"name"` Name string `json:"name"`
Isrc string `json:"isrc"` Isrc string `json:"isrc"`
AudioTraits []string `json:"audioTraits"` AudioTraits []string `json:"audioTraits"`
HasLyrics bool `json:"hasLyrics"` HasLyrics bool `json:"hasLyrics"`
AlbumName string `json:"albumName"` AlbumName string `json:"albumName"`
PlayParams struct { PlayParams struct {
ID string `json:"id"` ID string `json:"id"`
Kind string `json:"kind"` Kind string `json:"kind"`
} `json:"playParams"` } `json:"playParams"`

Loading…
Cancel
Save