|
|
@ -37,6 +37,8 @@ const ( |
|
|
|
var ( |
|
|
|
var ( |
|
|
|
forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`) |
|
|
|
forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`) |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
var dl_atmos = false |
|
|
|
|
|
|
|
var dl_select = false |
|
|
|
|
|
|
|
|
|
|
|
type Config struct { |
|
|
|
type Config struct { |
|
|
|
MediaUserToken string `yaml:"media-user-token"` |
|
|
|
MediaUserToken string `yaml:"media-user-token"` |
|
|
@ -63,6 +65,7 @@ type Config struct { |
|
|
|
GetM3u8Port string `yaml:"get-m3u8-port"` |
|
|
|
GetM3u8Port string `yaml:"get-m3u8-port"` |
|
|
|
GetM3u8FromDevice bool `yaml:"get-m3u8-from-device"` |
|
|
|
GetM3u8FromDevice bool `yaml:"get-m3u8-from-device"` |
|
|
|
AlacMax int `yaml:"alac-max"` |
|
|
|
AlacMax int `yaml:"alac-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"` |
|
|
|
} |
|
|
|
} |
|
|
@ -109,6 +112,15 @@ func (*Alac) GetType() mp4.BoxType { |
|
|
|
return BoxTypeAlac() |
|
|
|
return BoxTypeAlac() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func isInArray(arr []int, target int) bool { |
|
|
|
|
|
|
|
for _, num := range arr { |
|
|
|
|
|
|
|
if num == target { |
|
|
|
|
|
|
|
return true |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return false |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func fileExists(path string) (bool, error) { |
|
|
|
func fileExists(path string) (bool, error) { |
|
|
|
f, err := os.Stat(path) |
|
|
|
f, err := os.Stat(path) |
|
|
|
if err == nil { |
|
|
|
if err == nil { |
|
|
@ -1009,6 +1021,14 @@ func decryptSong(info *SongInfo, keys []string, manifest *AutoGenerated, filenam |
|
|
|
return err |
|
|
|
return err |
|
|
|
} |
|
|
|
} |
|
|
|
defer create.Close() |
|
|
|
defer create.Close() |
|
|
|
|
|
|
|
if dl_atmos { |
|
|
|
|
|
|
|
_, err = create.Write(decrypted) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
panic(err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return nil |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return writeM4a(mp4.NewWriter(create), info, manifest, decrypted, trackNum, trackTotal) |
|
|
|
return writeM4a(mp4.NewWriter(create), info, manifest, decrypted, trackNum, trackTotal) |
|
|
|
} |
|
|
|
} |
|
|
@ -1304,6 +1324,12 @@ func writeLyrics(sanAlbumFolder, filename string, lrc string) error { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func rip(albumId string, token string, storefront string, userToken string) error { |
|
|
|
func rip(albumId string, token string, storefront string, userToken string) error { |
|
|
|
|
|
|
|
var Codec string |
|
|
|
|
|
|
|
if dl_atmos { |
|
|
|
|
|
|
|
Codec = "Atmos" |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
Codec = "ALAC" |
|
|
|
|
|
|
|
} |
|
|
|
meta, err := getMeta(albumId, token, storefront) |
|
|
|
meta, err := getMeta(albumId, token, storefront) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
fmt.Println("Failed to get album metadata.\n") |
|
|
|
fmt.Println("Failed to get album metadata.\n") |
|
|
@ -1336,20 +1362,24 @@ 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, "_")) |
|
|
|
var Quality string |
|
|
|
var Quality string |
|
|
|
if strings.Contains(config.AlbumFolderFormat, "Quality") { |
|
|
|
if strings.Contains(config.AlbumFolderFormat, "Quality") { |
|
|
|
manifest1, err := getInfoFromAdam(meta.Data[0].Relationships.Tracks.Data[0].ID, token, storefront) |
|
|
|
if dl_atmos { |
|
|
|
if err != nil { |
|
|
|
Quality = fmt.Sprintf("%dkbps", config.AtmosMax-2000) |
|
|
|
fmt.Println("Failed to get manifest.\n", err) |
|
|
|
|
|
|
|
} else { |
|
|
|
} else { |
|
|
|
if manifest1.Attributes.ExtendedAssetUrls.EnhancedHls == "" { |
|
|
|
manifest1, err := getInfoFromAdam(meta.Data[0].Relationships.Tracks.Data[0].ID, token, storefront) |
|
|
|
fmt.Println("Unavailable in ALAC.\n") |
|
|
|
if err != nil { |
|
|
|
|
|
|
|
fmt.Println("Failed to get manifest.\n", err) |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
EnhancedHls_m3u8, err := checkM3u8(meta.Data[0].Relationships.Tracks.Data[0].ID, "album") |
|
|
|
if manifest1.Attributes.ExtendedAssetUrls.EnhancedHls == "" { |
|
|
|
if strings.HasPrefix(EnhancedHls_m3u8, "http") { |
|
|
|
fmt.Println("Unavailable.\n") |
|
|
|
manifest1.Attributes.ExtendedAssetUrls.EnhancedHls = EnhancedHls_m3u8 |
|
|
|
} else { |
|
|
|
} |
|
|
|
EnhancedHls_m3u8, err := checkM3u8(meta.Data[0].Relationships.Tracks.Data[0].ID, "album") |
|
|
|
Quality, err = extractMediaQuality(manifest1.Attributes.ExtendedAssetUrls.EnhancedHls) |
|
|
|
if strings.HasPrefix(EnhancedHls_m3u8, "http") { |
|
|
|
if err != nil { |
|
|
|
manifest1.Attributes.ExtendedAssetUrls.EnhancedHls = EnhancedHls_m3u8 |
|
|
|
fmt.Println("Failed to extract quality from manifest.\n", err) |
|
|
|
} |
|
|
|
|
|
|
|
Quality, err = extractMediaQuality(manifest1.Attributes.ExtendedAssetUrls.EnhancedHls) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
fmt.Println("Failed to extract quality from manifest.\n", err) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@ -1378,7 +1408,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro |
|
|
|
"{PlaylistName}", meta.Data[0].Attributes.Name, |
|
|
|
"{PlaylistName}", meta.Data[0].Attributes.Name, |
|
|
|
"{PlaylistId}", albumId, |
|
|
|
"{PlaylistId}", albumId, |
|
|
|
"{Quality}", Quality, |
|
|
|
"{Quality}", Quality, |
|
|
|
"{Codec}", "ALAC", |
|
|
|
"{Codec}", Codec, |
|
|
|
"{Tag}", Tag_string, |
|
|
|
"{Tag}", Tag_string, |
|
|
|
).Replace(config.PlaylistFolderFormat) |
|
|
|
).Replace(config.PlaylistFolderFormat) |
|
|
|
} else { |
|
|
|
} else { |
|
|
@ -1392,7 +1422,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro |
|
|
|
"{Copyright}", meta.Data[0].Attributes.Copyright, |
|
|
|
"{Copyright}", meta.Data[0].Attributes.Copyright, |
|
|
|
"{AlbumId}", albumId, |
|
|
|
"{AlbumId}", albumId, |
|
|
|
"{Quality}", Quality, |
|
|
|
"{Quality}", Quality, |
|
|
|
"{Codec}", "ALAC", |
|
|
|
"{Codec}", Codec, |
|
|
|
"{Tag}", Tag_string, |
|
|
|
"{Tag}", Tag_string, |
|
|
|
).Replace(config.AlbumFolderFormat) |
|
|
|
).Replace(config.AlbumFolderFormat) |
|
|
|
} |
|
|
|
} |
|
|
@ -1448,157 +1478,260 @@ func rip(albumId string, token string, storefront string, userToken string) erro |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
trackTotal := len(meta.Data[0].Relationships.Tracks.Data) |
|
|
|
trackTotal := len(meta.Data[0].Relationships.Tracks.Data) |
|
|
|
for trackNum, track := range meta.Data[0].Relationships.Tracks.Data { |
|
|
|
arr := make([]int, trackTotal) |
|
|
|
trackNum++ |
|
|
|
for i := 0; i < trackTotal; i++ { |
|
|
|
trackTotalnum += 1 |
|
|
|
arr[i] = i + 1 |
|
|
|
fmt.Printf("Track %d of %d:\n", trackNum, trackTotal) |
|
|
|
} |
|
|
|
manifest, err := getInfoFromAdam(track.ID, token, storefront) |
|
|
|
selected := []int{} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if !dl_select { |
|
|
|
|
|
|
|
selected = arr |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fmt.Print("select: ") |
|
|
|
|
|
|
|
reader := bufio.NewReader(os.Stdin) |
|
|
|
|
|
|
|
input, err := reader.ReadString('\n') |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
fmt.Println("Failed to get manifest.\n", err) |
|
|
|
fmt.Println(err) |
|
|
|
continue |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if manifest.Attributes.ExtendedAssetUrls.EnhancedHls == "" { |
|
|
|
|
|
|
|
fmt.Println("Unavailable in ALAC.") |
|
|
|
|
|
|
|
continue |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
EnhancedHls_m3u8, err := checkM3u8(track.ID, "song") |
|
|
|
|
|
|
|
if strings.HasPrefix(EnhancedHls_m3u8, "http") { |
|
|
|
|
|
|
|
manifest.Attributes.ExtendedAssetUrls.EnhancedHls = EnhancedHls_m3u8 |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
var Quality string |
|
|
|
input = strings.TrimSpace(input) |
|
|
|
if strings.Contains(config.SongFileFormat, "Quality") { |
|
|
|
inputs := strings.Fields(input) |
|
|
|
Quality, err = extractMediaQuality(manifest.Attributes.ExtendedAssetUrls.EnhancedHls) |
|
|
|
|
|
|
|
|
|
|
|
for _, str := range inputs { |
|
|
|
|
|
|
|
num, err := strconv.Atoi(str) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
fmt.Println("Failed to extract quality from manifest.\n", err) |
|
|
|
fmt.Printf("wrong '%s', skip...\n", str) |
|
|
|
continue |
|
|
|
continue |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
stringsToJoin := []string{} |
|
|
|
found := false |
|
|
|
if track.Attributes.IsAppleDigitalMaster { |
|
|
|
for i := 0; i < len(arr); i++ { |
|
|
|
if config.AppleMasterChoice != "" { |
|
|
|
if arr[i] == num { |
|
|
|
stringsToJoin = append(stringsToJoin, config.AppleMasterChoice) |
|
|
|
selected = append(selected, num) |
|
|
|
} |
|
|
|
found = true |
|
|
|
} |
|
|
|
break |
|
|
|
if track.Attributes.ContentRating == "explicit" { |
|
|
|
} |
|
|
|
if config.ExplicitChoice != "" { |
|
|
|
|
|
|
|
stringsToJoin = append(stringsToJoin, config.ExplicitChoice) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if track.Attributes.ContentRating == "clean" { |
|
|
|
if !found { |
|
|
|
if config.CleanChoice != "" { |
|
|
|
fmt.Printf("Option '%d' not found or already selected, skipping...\n", num) |
|
|
|
stringsToJoin = append(stringsToJoin, config.CleanChoice) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
Tag_string := strings.Join(stringsToJoin, " ") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
songName := strings.NewReplacer( |
|
|
|
fmt.Println("Selected options:", selected) |
|
|
|
"{SongId}", track.ID, |
|
|
|
} |
|
|
|
"{SongNumer}", fmt.Sprintf("%02d", trackNum), |
|
|
|
for trackNum, track := range meta.Data[0].Relationships.Tracks.Data { |
|
|
|
"{SongName}", track.Attributes.Name, |
|
|
|
trackNum++ |
|
|
|
"{DiscNumber}", fmt.Sprintf("%0d", track.Attributes.DiscNumber), |
|
|
|
if isInArray(selected, trackNum) { |
|
|
|
"{TrackNumber}", fmt.Sprintf("%0d", track.Attributes.TrackNumber), |
|
|
|
trackTotalnum += 1 |
|
|
|
"{Quality}", Quality, |
|
|
|
fmt.Printf("Track %d of %d:\n", trackNum, trackTotal) |
|
|
|
"{Tag}", Tag_string, |
|
|
|
manifest, err := getInfoFromAdam(track.ID, token, storefront) |
|
|
|
"{Codec}", "ALAC", |
|
|
|
|
|
|
|
).Replace(config.SongFileFormat) |
|
|
|
|
|
|
|
fmt.Println(songName) |
|
|
|
|
|
|
|
filename := fmt.Sprintf("%s.m4a", forbiddenNames.ReplaceAllString(songName, "_")) |
|
|
|
|
|
|
|
lrcFilename := fmt.Sprintf("%s.lrc", forbiddenNames.ReplaceAllString(songName, "_")) |
|
|
|
|
|
|
|
trackPath := filepath.Join(sanAlbumFolder, filename) |
|
|
|
|
|
|
|
var lrc string = "" |
|
|
|
|
|
|
|
if userToken != "your-media-user-token" && (config.EmbedLrc || config.SaveLrcFile) { |
|
|
|
|
|
|
|
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 manifest.\n", err) |
|
|
|
} else { |
|
|
|
continue |
|
|
|
lrc, err = conventTTMLToLRC(ttml) |
|
|
|
} |
|
|
|
|
|
|
|
if manifest.Attributes.ExtendedAssetUrls.EnhancedHls == "" { |
|
|
|
|
|
|
|
fmt.Println("Unavailable.") |
|
|
|
|
|
|
|
continue |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
EnhancedHls_m3u8, err := checkM3u8(track.ID, "song") |
|
|
|
|
|
|
|
if strings.HasPrefix(EnhancedHls_m3u8, "http") { |
|
|
|
|
|
|
|
manifest.Attributes.ExtendedAssetUrls.EnhancedHls = EnhancedHls_m3u8 |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
var Quality string |
|
|
|
|
|
|
|
if strings.Contains(config.SongFileFormat, "Quality") { |
|
|
|
|
|
|
|
if dl_atmos { |
|
|
|
|
|
|
|
Quality = fmt.Sprintf("%dkbps", config.AtmosMax-2000) |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
Quality, err = extractMediaQuality(manifest.Attributes.ExtendedAssetUrls.EnhancedHls) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
fmt.Println("Failed to extract quality from manifest.\n", err) |
|
|
|
|
|
|
|
continue |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
stringsToJoin := []string{} |
|
|
|
|
|
|
|
if track.Attributes.IsAppleDigitalMaster { |
|
|
|
|
|
|
|
if config.AppleMasterChoice != "" { |
|
|
|
|
|
|
|
stringsToJoin = append(stringsToJoin, config.AppleMasterChoice) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if track.Attributes.ContentRating == "explicit" { |
|
|
|
|
|
|
|
if config.ExplicitChoice != "" { |
|
|
|
|
|
|
|
stringsToJoin = append(stringsToJoin, config.ExplicitChoice) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if track.Attributes.ContentRating == "clean" { |
|
|
|
|
|
|
|
if config.CleanChoice != "" { |
|
|
|
|
|
|
|
stringsToJoin = append(stringsToJoin, config.CleanChoice) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
Tag_string := strings.Join(stringsToJoin, " ") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
songName := strings.NewReplacer( |
|
|
|
|
|
|
|
"{SongId}", track.ID, |
|
|
|
|
|
|
|
"{SongNumer}", fmt.Sprintf("%02d", trackNum), |
|
|
|
|
|
|
|
"{SongName}", track.Attributes.Name, |
|
|
|
|
|
|
|
"{DiscNumber}", fmt.Sprintf("%0d", track.Attributes.DiscNumber), |
|
|
|
|
|
|
|
"{TrackNumber}", fmt.Sprintf("%0d", track.Attributes.TrackNumber), |
|
|
|
|
|
|
|
"{Quality}", Quality, |
|
|
|
|
|
|
|
"{Tag}", Tag_string, |
|
|
|
|
|
|
|
"{Codec}", Codec, |
|
|
|
|
|
|
|
).Replace(config.SongFileFormat) |
|
|
|
|
|
|
|
fmt.Println(songName) |
|
|
|
|
|
|
|
filename := fmt.Sprintf("%s.m4a", forbiddenNames.ReplaceAllString(songName, "_")) |
|
|
|
|
|
|
|
if dl_atmos { |
|
|
|
|
|
|
|
filename = fmt.Sprintf("%s.ec3", forbiddenNames.ReplaceAllString(songName, "_")) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
m4afilename := fmt.Sprintf("%s.m4a", forbiddenNames.ReplaceAllString(songName, "_")) |
|
|
|
|
|
|
|
lrcFilename := fmt.Sprintf("%s.lrc", forbiddenNames.ReplaceAllString(songName, "_")) |
|
|
|
|
|
|
|
trackPath := filepath.Join(sanAlbumFolder, filename) |
|
|
|
|
|
|
|
m4atrackPath := filepath.Join(sanAlbumFolder, m4afilename) |
|
|
|
|
|
|
|
var lrc string = "" |
|
|
|
|
|
|
|
if userToken != "your-media-user-token" && (config.EmbedLrc || config.SaveLrcFile) { |
|
|
|
|
|
|
|
ttml, err := getSongLyrics(track.ID, storefront, token, userToken) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
fmt.Printf("Failed to parse lyrics: %s \n", err) |
|
|
|
fmt.Println("Failed to get lyrics") |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
if config.SaveLrcFile { |
|
|
|
lrc, err = conventTTMLToLRC(ttml) |
|
|
|
err := writeLyrics(sanAlbumFolder, lrcFilename, lrc) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
fmt.Printf("Failed to parse lyrics: %s \n", err) |
|
|
|
fmt.Printf("Failed to write lyrics") |
|
|
|
} else { |
|
|
|
} |
|
|
|
if config.SaveLrcFile { |
|
|
|
if !config.EmbedLrc { |
|
|
|
err := writeLyrics(sanAlbumFolder, lrcFilename, lrc) |
|
|
|
lrc = "" |
|
|
|
if err != nil { |
|
|
|
|
|
|
|
fmt.Printf("Failed to write lyrics") |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if !config.EmbedLrc { |
|
|
|
|
|
|
|
lrc = "" |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
exists, err := fileExists(trackPath) |
|
|
|
exists, err := fileExists(trackPath) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
fmt.Println("Failed to check if track exists.") |
|
|
|
fmt.Println("Failed to check if track exists.") |
|
|
|
} |
|
|
|
} |
|
|
|
if exists { |
|
|
|
if exists { |
|
|
|
fmt.Println("Track already exists locally.") |
|
|
|
fmt.Println("Track already exists locally.") |
|
|
|
oktrackNum += 1 |
|
|
|
oktrackNum += 1 |
|
|
|
continue |
|
|
|
continue |
|
|
|
} |
|
|
|
} |
|
|
|
m4aexists, err := fileExists(m4atrackPath) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
fmt.Println("Failed to check if track exists.") |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if m4aexists { |
|
|
|
|
|
|
|
fmt.Println("Track already exists locally.") |
|
|
|
|
|
|
|
oktrackNum += 1 |
|
|
|
|
|
|
|
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) |
|
|
|
continue |
|
|
|
continue |
|
|
|
} |
|
|
|
} |
|
|
|
info, err := extractSong(trackUrl) |
|
|
|
info, err := extractSong(trackUrl) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
fmt.Println("Failed to extract track.", err) |
|
|
|
fmt.Println("Failed to extract track.", err) |
|
|
|
continue |
|
|
|
continue |
|
|
|
} |
|
|
|
} |
|
|
|
samplesOk := true |
|
|
|
samplesOk := true |
|
|
|
for samplesOk { |
|
|
|
for samplesOk { |
|
|
|
for _, i := range info.samples { |
|
|
|
for _, i := range info.samples { |
|
|
|
if int(i.descIndex) >= len(keys) { |
|
|
|
if int(i.descIndex) >= len(keys) { |
|
|
|
fmt.Println("Decryption size mismatch.") |
|
|
|
fmt.Println("Decryption size mismatch.") |
|
|
|
samplesOk = false |
|
|
|
samplesOk = false |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
break |
|
|
|
} |
|
|
|
} |
|
|
|
break |
|
|
|
if !samplesOk { |
|
|
|
} |
|
|
|
continue |
|
|
|
if !samplesOk { |
|
|
|
} |
|
|
|
continue |
|
|
|
err = decryptSong(info, keys, meta, trackPath, trackNum, trackTotal) |
|
|
|
} |
|
|
|
if err != nil { |
|
|
|
err = decryptSong(info, keys, meta, trackPath, trackNum, trackTotal) |
|
|
|
fmt.Println("Failed to decrypt track.\n", err) |
|
|
|
if err != nil { |
|
|
|
continue |
|
|
|
fmt.Println("Failed to decrypt track.\n", err) |
|
|
|
} |
|
|
|
continue |
|
|
|
tags := []string{ |
|
|
|
} |
|
|
|
fmt.Sprintf("lyrics=%s", lrc), |
|
|
|
tags := []string{ |
|
|
|
} |
|
|
|
fmt.Sprintf("lyrics=%s", lrc), |
|
|
|
|
|
|
|
} |
|
|
|
index := trackNum - 1 |
|
|
|
if track.Attributes.ContentRating == "explicit" { |
|
|
|
if dl_atmos { |
|
|
|
tags = append(tags, "rating=1") |
|
|
|
tags = []string{ |
|
|
|
} else if track.Attributes.ContentRating == "clean" { |
|
|
|
"tool=", |
|
|
|
tags = append(tags, "rating=2") |
|
|
|
fmt.Sprintf("lyrics=%s", lrc), |
|
|
|
} else { |
|
|
|
fmt.Sprintf("title=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.Name), |
|
|
|
tags = append(tags, "rating=0") |
|
|
|
fmt.Sprintf("artist=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName), |
|
|
|
} |
|
|
|
fmt.Sprintf("genre=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.GenreNames[0]), |
|
|
|
if config.EmbedCover { |
|
|
|
fmt.Sprintf("created=%s", meta.Data[0].Attributes.ReleaseDate), |
|
|
|
if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist { |
|
|
|
fmt.Sprintf("album_artist=%s", meta.Data[0].Attributes.ArtistName), |
|
|
|
err = writeCover(sanAlbumFolder, track.ID, track.Attributes.Artwork.URL) |
|
|
|
fmt.Sprintf("composer=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.ComposerName), |
|
|
|
if err != nil { |
|
|
|
fmt.Sprintf("writer=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.ComposerName), |
|
|
|
fmt.Println("Failed to write cover.") |
|
|
|
fmt.Sprintf("performer=%s", meta.Data[0].Attributes.ArtistName), |
|
|
|
|
|
|
|
fmt.Sprintf("copyright=%s", meta.Data[0].Attributes.Copyright), |
|
|
|
|
|
|
|
fmt.Sprintf("ISRC=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.Isrc), |
|
|
|
|
|
|
|
fmt.Sprintf("UPC=%s", meta.Data[0].Attributes.Upc), |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if strings.Contains(albumId, "pl.") && !config.UseSongInfoForPlaylist { |
|
|
|
|
|
|
|
tags = append(tags, "disk=1/1") |
|
|
|
|
|
|
|
tags = append(tags, fmt.Sprintf("track=%s", trackNum)) |
|
|
|
|
|
|
|
tags = append(tags, fmt.Sprintf("tracknum=%d/%d", trackNum, trackTotal)) |
|
|
|
|
|
|
|
tags = append(tags, fmt.Sprintf("album=%s", meta.Data[0].Attributes.Name)) |
|
|
|
|
|
|
|
} 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("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("album=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.AlbumName)) |
|
|
|
} |
|
|
|
} |
|
|
|
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, track.ID, config.CoverFormat)) |
|
|
|
} |
|
|
|
|
|
|
|
if track.Attributes.ContentRating == "explicit" { |
|
|
|
|
|
|
|
tags = append(tags, "rating=1") |
|
|
|
|
|
|
|
} else if track.Attributes.ContentRating == "clean" { |
|
|
|
|
|
|
|
tags = append(tags, "rating=2") |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, "cover", config.CoverFormat)) |
|
|
|
tags = append(tags, "rating=0") |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if config.EmbedCover { |
|
|
|
tagsString := strings.Join(tags, ":") |
|
|
|
if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist { |
|
|
|
cmd := exec.Command("MP4Box", "-itags", tagsString, trackPath) |
|
|
|
err = writeCover(sanAlbumFolder, track.ID, track.Attributes.Artwork.URL) |
|
|
|
if err := cmd.Run(); err != nil { |
|
|
|
if err != nil { |
|
|
|
fmt.Printf("Embed failed: %v\n", err) |
|
|
|
fmt.Println("Failed to write cover.") |
|
|
|
continue |
|
|
|
} |
|
|
|
} |
|
|
|
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, track.ID, config.CoverFormat)) |
|
|
|
if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist { |
|
|
|
} else { |
|
|
|
if err := os.Remove(fmt.Sprintf("%s/%s.%s", sanAlbumFolder, track.ID, config.CoverFormat)); err != nil { |
|
|
|
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, "cover", config.CoverFormat)) |
|
|
|
fmt.Printf("Error deleting file: %s/%s.%s\n", sanAlbumFolder, track.ID, config.CoverFormat) |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
tagsString := strings.Join(tags, ":") |
|
|
|
|
|
|
|
cmd := exec.Command("MP4Box", "-itags", tagsString, trackPath) |
|
|
|
|
|
|
|
if dl_atmos { |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if err := cmd.Run(); err != nil { |
|
|
|
|
|
|
|
fmt.Printf("Embed failed: %v\n", err) |
|
|
|
continue |
|
|
|
continue |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist { |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
continue |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if dl_atmos { |
|
|
|
|
|
|
|
fmt.Printf("Deleting original EC3 file: %s\n", filepath.Base(trackPath)) |
|
|
|
|
|
|
|
if err := os.Remove(trackPath); err != nil { |
|
|
|
|
|
|
|
fmt.Printf("Error deleting file: %v\n", err) |
|
|
|
|
|
|
|
continue |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
fmt.Printf("Successfully processed and deleted %s\n", filepath.Base(trackPath)) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
oktrackNum += 1 |
|
|
|
} |
|
|
|
} |
|
|
|
oktrackNum += 1 |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
return err |
|
|
|
return err |
|
|
|
} |
|
|
|
} |
|
|
@ -1614,6 +1747,17 @@ func main() { |
|
|
|
fmt.Println("Failed to get token.") |
|
|
|
fmt.Println("Failed to get token.") |
|
|
|
return |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
var dlArgs []string |
|
|
|
|
|
|
|
for _, arg := range os.Args { |
|
|
|
|
|
|
|
if strings.Contains(arg, "--atmos") { |
|
|
|
|
|
|
|
dl_atmos = true |
|
|
|
|
|
|
|
} else if strings.Contains(arg, "--select") { |
|
|
|
|
|
|
|
dl_select = true |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
dlArgs = append(dlArgs, arg) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
os.Args = dlArgs |
|
|
|
if strings.Contains(os.Args[1], "/artist/") { |
|
|
|
if strings.Contains(os.Args[1], "/artist/") { |
|
|
|
newArgs, err := checkArtist(os.Args[1], token) |
|
|
|
newArgs, err := checkArtist(os.Args[1], token) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
@ -1882,26 +2026,46 @@ func extractMedia(b string) (string, []string, error) { |
|
|
|
return master.Variants[i].AverageBandwidth > master.Variants[j].AverageBandwidth |
|
|
|
return master.Variants[i].AverageBandwidth > master.Variants[j].AverageBandwidth |
|
|
|
}) |
|
|
|
}) |
|
|
|
for _, variant := range master.Variants { |
|
|
|
for _, variant := range master.Variants { |
|
|
|
if variant.Codecs == "alac" { |
|
|
|
if dl_atmos { |
|
|
|
split := strings.Split(variant.Audio, "-") |
|
|
|
if variant.Codecs == "ec-3" { |
|
|
|
length := len(split) |
|
|
|
split := strings.Split(variant.Audio, "-") |
|
|
|
length_int, err := strconv.Atoi(split[length-2]) |
|
|
|
length := len(split) |
|
|
|
if err != nil { |
|
|
|
length_int, err := strconv.Atoi(split[length-1]) |
|
|
|
return "", nil, err |
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return "", nil, err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if length_int <= config.AtmosMax { |
|
|
|
|
|
|
|
fmt.Printf("%s\n", variant.Audio) |
|
|
|
|
|
|
|
streamUrlTemp, err := masterUrl.Parse(variant.URI) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
panic(err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
streamUrl = streamUrlTemp |
|
|
|
|
|
|
|
break |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if length_int <= config.AlacMax { |
|
|
|
} else { |
|
|
|
fmt.Printf("%s-bit / %s Hz\n", split[length-1], split[length-2]) |
|
|
|
if variant.Codecs == "alac" { |
|
|
|
streamUrlTemp, err := masterUrl.Parse(variant.URI) |
|
|
|
split := strings.Split(variant.Audio, "-") |
|
|
|
|
|
|
|
length := len(split) |
|
|
|
|
|
|
|
length_int, err := strconv.Atoi(split[length-2]) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
panic(err) |
|
|
|
return "", nil, err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if length_int <= config.AlacMax { |
|
|
|
|
|
|
|
fmt.Printf("%s-bit / %s Hz\n", split[length-1], split[length-2]) |
|
|
|
|
|
|
|
streamUrlTemp, err := masterUrl.Parse(variant.URI) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
panic(err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
streamUrl = streamUrlTemp |
|
|
|
|
|
|
|
break |
|
|
|
} |
|
|
|
} |
|
|
|
streamUrl = streamUrlTemp |
|
|
|
|
|
|
|
break |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if streamUrl == nil { |
|
|
|
if streamUrl == nil { |
|
|
|
return "", nil, errors.New("no alac codec found") |
|
|
|
return "", nil, errors.New("no codec found") |
|
|
|
} |
|
|
|
} |
|
|
|
var keys []string |
|
|
|
var keys []string |
|
|
|
keys = append(keys, prefetchKey) |
|
|
|
keys = append(keys, prefetchKey) |
|
|
@ -1909,8 +2073,14 @@ func extractMedia(b string) (string, []string, error) { |
|
|
|
regex := regexp.MustCompile(`"(skd?://[^"]*)"`) |
|
|
|
regex := regexp.MustCompile(`"(skd?://[^"]*)"`) |
|
|
|
matches := regex.FindAllStringSubmatch(masterString, -1) |
|
|
|
matches := regex.FindAllStringSubmatch(masterString, -1) |
|
|
|
for _, match := range matches { |
|
|
|
for _, match := range matches { |
|
|
|
if strings.HasSuffix(match[1], "c23") || strings.HasSuffix(match[1], "c6") { |
|
|
|
if dl_atmos { |
|
|
|
keys = append(keys, match[1]) |
|
|
|
if strings.HasSuffix(match[1], "c24") || strings.HasSuffix(match[1], "c6") { |
|
|
|
|
|
|
|
keys = append(keys, match[1]) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
if strings.HasSuffix(match[1], "c23") || strings.HasSuffix(match[1], "c6") { |
|
|
|
|
|
|
|
keys = append(keys, match[1]) |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
return streamUrl.String(), keys, nil |
|
|
|
return streamUrl.String(), keys, nil |
|
|
@ -1992,23 +2162,30 @@ func extractSong(url string) (*SongInfo, error) { |
|
|
|
return nil, err |
|
|
|
return nil, err |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
enca, err := mp4.ExtractBoxWithPayload(f, stbl[0], []mp4.BoxType{ |
|
|
|
var extracted *SongInfo |
|
|
|
mp4.BoxTypeStsd(), |
|
|
|
if !dl_atmos { |
|
|
|
mp4.BoxTypeEnca(), |
|
|
|
enca, err := mp4.ExtractBoxWithPayload(f, stbl[0], []mp4.BoxType{ |
|
|
|
}) |
|
|
|
mp4.BoxTypeStsd(), |
|
|
|
if err != nil { |
|
|
|
mp4.BoxTypeEnca(), |
|
|
|
return nil, err |
|
|
|
}) |
|
|
|
} |
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return nil, err |
|
|
|
aalac, err := mp4.ExtractBoxWithPayload(f, &enca[0].Info, |
|
|
|
} |
|
|
|
[]mp4.BoxType{BoxTypeAlac()}) |
|
|
|
|
|
|
|
if err != nil || len(aalac) != 1 { |
|
|
|
|
|
|
|
return nil, err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
extracted := &SongInfo{ |
|
|
|
aalac, err := mp4.ExtractBoxWithPayload(f, &enca[0].Info, |
|
|
|
r: f, |
|
|
|
[]mp4.BoxType{BoxTypeAlac()}) |
|
|
|
alacParam: aalac[0].Payload.(*Alac), |
|
|
|
if err != nil || len(aalac) != 1 { |
|
|
|
|
|
|
|
return nil, err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
extracted = &SongInfo{ |
|
|
|
|
|
|
|
r: f, |
|
|
|
|
|
|
|
alacParam: aalac[0].Payload.(*Alac), |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
extracted = &SongInfo{ |
|
|
|
|
|
|
|
r: f, |
|
|
|
|
|
|
|
// alacParam: aalac[0].Payload.(*Alac),
|
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
moofs, err := mp4.ExtractBox(f, nil, []mp4.BoxType{ |
|
|
|
moofs, err := mp4.ExtractBox(f, nil, []mp4.BoxType{ |
|
|
|