Merge branch 'main' into main
This commit is contained in:
commit
7db82d54a1
456
main.go
456
main.go
@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -38,6 +39,7 @@ var (
|
|||||||
dl_atmos bool
|
dl_atmos bool
|
||||||
dl_aac bool
|
dl_aac bool
|
||||||
dl_select bool
|
dl_select bool
|
||||||
|
dl_song bool
|
||||||
artist_select bool
|
artist_select bool
|
||||||
debug_mode bool
|
debug_mode bool
|
||||||
alac_max *int
|
alac_max *int
|
||||||
@ -426,7 +428,201 @@ func contains(slice []string, item string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func rip(albumId string, token string, storefront string, mediaUserToken string) error {
|
// 下载单曲逻辑
|
||||||
|
func downloadTrack(trackNum int, trackTotal int, meta *structs.AutoGenerated, track structs.TrackData, albumId, token, storefront, mediaUserToken, sanAlbumFolder, Codec string, counter *structs.Counter) {
|
||||||
|
counter.Total++
|
||||||
|
fmt.Printf("Track %d of %d:\n", trackNum, trackTotal)
|
||||||
|
manifest, err := getInfoFromAdam(track.ID, token, storefront)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("\u26A0 Failed to get manifest:", err)
|
||||||
|
counter.NotSong++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
needDlAacLc := false
|
||||||
|
if dl_aac && Config.AacType == "aac-lc" {
|
||||||
|
needDlAacLc = true
|
||||||
|
}
|
||||||
|
if manifest.Attributes.ExtendedAssetUrls.EnhancedHls == "" {
|
||||||
|
if dl_atmos {
|
||||||
|
fmt.Println("Unavailable")
|
||||||
|
counter.Unavailable++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Unavailable, Try DL AAC-LC")
|
||||||
|
needDlAacLc = true
|
||||||
|
}
|
||||||
|
needCheck := false
|
||||||
|
|
||||||
|
if Config.GetM3u8Mode == "all" {
|
||||||
|
needCheck = true
|
||||||
|
} else if Config.GetM3u8Mode == "hires" && contains(track.Attributes.AudioTraits, "hi-res-lossless") {
|
||||||
|
needCheck = true
|
||||||
|
}
|
||||||
|
var EnhancedHls_m3u8 string
|
||||||
|
if needCheck && !needDlAacLc {
|
||||||
|
EnhancedHls_m3u8, err = checkM3u8(track.ID, "song")
|
||||||
|
if strings.HasSuffix(EnhancedHls_m3u8, ".m3u8") {
|
||||||
|
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 if needDlAacLc {
|
||||||
|
Quality = fmt.Sprintf("256kbps")
|
||||||
|
} else {
|
||||||
|
Quality, err = extractMediaQuality(manifest.Attributes.ExtendedAssetUrls.EnhancedHls)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to extract quality from manifest.\n", err)
|
||||||
|
counter.Error++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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}", LimitString(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, "_"))
|
||||||
|
lrcFilename := fmt.Sprintf("%s.%s", forbiddenNames.ReplaceAllString(songName, "_"), Config.LrcFormat)
|
||||||
|
trackPath := filepath.Join(sanAlbumFolder, filename)
|
||||||
|
|
||||||
|
//get lrc
|
||||||
|
var lrc string = ""
|
||||||
|
if mediaUserToken != "" && len(mediaUserToken) > 10 {
|
||||||
|
ttml, err := getSongLyrics(track.ID, storefront, token, mediaUserToken)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to get lyrics")
|
||||||
|
} else if Config.LrcFormat == "ttml" {
|
||||||
|
if Config.SaveLrcFile {
|
||||||
|
lrc = ttml
|
||||||
|
err := writeLyrics(sanAlbumFolder, lrcFilename, lrc)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to write lyrics")
|
||||||
|
}
|
||||||
|
lrc = ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lrc, err = conventTTMLToLRC(ttml)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to parse lyrics: %s \n", err)
|
||||||
|
} else {
|
||||||
|
if Config.SaveLrcFile {
|
||||||
|
err := writeLyrics(sanAlbumFolder, lrcFilename, lrc)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to write lyrics")
|
||||||
|
}
|
||||||
|
if !Config.EmbedLrc {
|
||||||
|
lrc = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exists, err := fileExists(trackPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to check if track exists.")
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
fmt.Println("Track already exists locally.")
|
||||||
|
counter.Success++
|
||||||
|
okDict[albumId] = append(okDict[albumId], trackNum)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if needDlAacLc {
|
||||||
|
if mediaUserToken == "" || len(mediaUserToken) <= 10 {
|
||||||
|
fmt.Println("Invalid media-user-token")
|
||||||
|
counter.Error++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := runv3.Run(track.ID, trackPath, token, mediaUserToken)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to dl aac-lc:", err)
|
||||||
|
counter.Error++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
trackM3u8Url, err := extractMedia(manifest.Attributes.ExtendedAssetUrls.EnhancedHls)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("\u26A0 Failed to extract info from manifest:", err)
|
||||||
|
counter.Unavailable++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//边下载边解密
|
||||||
|
err = runv2.Run(track.ID, trackM3u8Url, trackPath, Config)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to run v2:", err)
|
||||||
|
counter.Error++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tags := []string{
|
||||||
|
"tool=",
|
||||||
|
fmt.Sprintf("artist=%s", meta.Data[0].Attributes.ArtistName),
|
||||||
|
fmt.Sprintf("lyrics=%s", lrc),
|
||||||
|
}
|
||||||
|
if Config.EmbedCover {
|
||||||
|
if strings.Contains(albumId, "pl.") && Config.DlAlbumcoverForPlaylist {
|
||||||
|
err = writeCover(sanAlbumFolder, track.ID, track.Attributes.Artwork.URL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to write cover.")
|
||||||
|
}
|
||||||
|
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, track.ID, Config.CoverFormat))
|
||||||
|
} else {
|
||||||
|
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, "cover", Config.CoverFormat))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tagsString := strings.Join(tags, ":")
|
||||||
|
cmd := exec.Command("MP4Box", "-itags", tagsString, trackPath)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Printf("Embed failed: %v\n", err)
|
||||||
|
counter.Error++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
counter.Error++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = writeMP4Tags(trackPath, meta, trackNum, trackTotal)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("\u26A0 Failed to write tags in media:", err)
|
||||||
|
counter.Unavailable++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
counter.Success++
|
||||||
|
okDict[albumId] = append(okDict[albumId], trackNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rip(albumId string, token string, storefront string, mediaUserToken string, urlArg_i string) error {
|
||||||
meta, err := getMeta(albumId, token, storefront)
|
meta, err := getMeta(albumId, token, storefront)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -436,12 +632,12 @@ func rip(albumId string, token string, storefront string, mediaUserToken string)
|
|||||||
// Print album info
|
// Print album info
|
||||||
fmt.Println(meta.Data[0].Attributes.ArtistName)
|
fmt.Println(meta.Data[0].Attributes.ArtistName)
|
||||||
fmt.Println(meta.Data[0].Attributes.Name)
|
fmt.Println(meta.Data[0].Attributes.Name)
|
||||||
|
|
||||||
for trackNum, track := range meta.Data[0].Relationships.Tracks.Data {
|
for trackNum, track := range meta.Data[0].Relationships.Tracks.Data {
|
||||||
trackNum++
|
trackNum++
|
||||||
fmt.Printf("\nTrack %d of %d:\n", trackNum, len(meta.Data[0].Relationships.Tracks.Data))
|
fmt.Printf("\nTrack %d of %d:\n", trackNum, len(meta.Data[0].Relationships.Tracks.Data))
|
||||||
fmt.Printf("%02d. %s\n", trackNum, track.Attributes.Name)
|
fmt.Printf("%02d. %s\n", trackNum, track.Attributes.Name)
|
||||||
|
|
||||||
manifest, err := getInfoFromAdam(track.ID, token, storefront)
|
manifest, err := getInfoFromAdam(track.ID, token, storefront)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to get manifest for track %d: %v\n", trackNum, err)
|
fmt.Printf("Failed to get manifest for track %d: %v\n", trackNum, err)
|
||||||
@ -459,7 +655,7 @@ func rip(albumId string, token string, storefront string, mediaUserToken string)
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m3u8Url != "" {
|
if m3u8Url != "" {
|
||||||
_, err = extractMediaQuality(m3u8Url)
|
_, err = extractMediaQuality(m3u8Url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -472,7 +668,6 @@ func rip(albumId string, token string, storefront string, mediaUserToken string)
|
|||||||
}
|
}
|
||||||
return nil // Return directly without showing statistics
|
return nil // Return directly without showing statistics
|
||||||
}
|
}
|
||||||
|
|
||||||
var Codec string
|
var Codec string
|
||||||
if dl_atmos {
|
if dl_atmos {
|
||||||
Codec = "ATMOS"
|
Codec = "ATMOS"
|
||||||
@ -616,7 +811,7 @@ func rip(albumId string, token string, storefront string, mediaUserToken string)
|
|||||||
//get animated artwork
|
//get animated artwork
|
||||||
if Config.SaveAnimatedArtwork && meta.Data[0].Attributes.EditorialVideo.MotionDetailSquare.Video != "" {
|
if Config.SaveAnimatedArtwork && meta.Data[0].Attributes.EditorialVideo.MotionDetailSquare.Video != "" {
|
||||||
fmt.Println("Found Animation Artwork.")
|
fmt.Println("Found Animation Artwork.")
|
||||||
|
|
||||||
// Download tall version
|
// Download tall version
|
||||||
motionvideoUrlTall, err := extractVideo(meta.Data[0].Attributes.EditorialVideo.MotionDetailTall.Video)
|
motionvideoUrlTall, err := extractVideo(meta.Data[0].Attributes.EditorialVideo.MotionDetailTall.Video)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -682,6 +877,22 @@ func rip(albumId string, token string, storefront string, mediaUserToken string)
|
|||||||
}
|
}
|
||||||
selected := []int{}
|
selected := []int{}
|
||||||
|
|
||||||
|
if dl_song {
|
||||||
|
if urlArg_i == "" {
|
||||||
|
//fmt.Println("URL does not contain parameter 'i'. Please ensure the URL includes 'i' or use another mode.")
|
||||||
|
//return nil
|
||||||
|
} else {
|
||||||
|
for trackNum, track := range meta.Data[0].Relationships.Tracks.Data {
|
||||||
|
trackNum++
|
||||||
|
if urlArg_i == track.ID {
|
||||||
|
downloadTrack(trackNum, trackTotal, meta, track, albumId, token, storefront, mediaUserToken, sanAlbumFolder, Codec, &counter)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if !dl_select {
|
if !dl_select {
|
||||||
selected = arr
|
selected = arr
|
||||||
} else {
|
} else {
|
||||||
@ -728,195 +939,7 @@ func rip(albumId string, token string, storefront string, mediaUserToken string)
|
|||||||
}
|
}
|
||||||
if isInArray(selected, trackNum) {
|
if isInArray(selected, trackNum) {
|
||||||
counter.Total++
|
counter.Total++
|
||||||
fmt.Printf("Track %d of %d:\n", trackNum, trackTotal)
|
downloadTrack(trackNum, trackTotal, meta, track, albumId, token, storefront, mediaUserToken, sanAlbumFolder, Codec, &counter)
|
||||||
manifest, err := getInfoFromAdam(track.ID, token, storefront)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("\u26A0 Failed to get manifest:", err)
|
|
||||||
counter.NotSong++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
needDlAacLc := false
|
|
||||||
if dl_aac && Config.AacType == "aac-lc" {
|
|
||||||
needDlAacLc = true
|
|
||||||
}
|
|
||||||
if manifest.Attributes.ExtendedAssetUrls.EnhancedHls == "" {
|
|
||||||
if dl_atmos {
|
|
||||||
fmt.Println("Unavailable")
|
|
||||||
counter.Unavailable++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fmt.Println("Unavailable, Try DL AAC-LC")
|
|
||||||
needDlAacLc = true
|
|
||||||
}
|
|
||||||
needCheck := false
|
|
||||||
|
|
||||||
if Config.GetM3u8Mode == "all" {
|
|
||||||
needCheck = true
|
|
||||||
} else if Config.GetM3u8Mode == "hires" && contains(track.Attributes.AudioTraits, "hi-res-lossless") {
|
|
||||||
needCheck = true
|
|
||||||
}
|
|
||||||
var EnhancedHls_m3u8 string
|
|
||||||
if needCheck && !needDlAacLc {
|
|
||||||
EnhancedHls_m3u8, err = checkM3u8(track.ID, "song")
|
|
||||||
if strings.HasSuffix(EnhancedHls_m3u8, ".m3u8") {
|
|
||||||
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 if needDlAacLc {
|
|
||||||
Quality = fmt.Sprintf("256kbps")
|
|
||||||
} else {
|
|
||||||
Quality, err = extractMediaQuality(manifest.Attributes.ExtendedAssetUrls.EnhancedHls)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Failed to extract quality from manifest.\n", err)
|
|
||||||
counter.Error++
|
|
||||||
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}", LimitString(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, "_"))
|
|
||||||
lrcFilename := fmt.Sprintf("%s.%s", forbiddenNames.ReplaceAllString(songName, "_"), Config.LrcFormat)
|
|
||||||
trackPath := filepath.Join(sanAlbumFolder, filename)
|
|
||||||
|
|
||||||
//get lrc
|
|
||||||
var lrc string = ""
|
|
||||||
if mediaUserToken != "" && len(mediaUserToken) > 10 {
|
|
||||||
ttml, err := getSongLyrics(track.ID, storefront, token, mediaUserToken)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Failed to get lyrics")
|
|
||||||
} else if Config.LrcFormat == "ttml" {
|
|
||||||
if Config.SaveLrcFile {
|
|
||||||
lrc = ttml
|
|
||||||
err := writeLyrics(sanAlbumFolder, lrcFilename, lrc)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Failed to write lyrics")
|
|
||||||
}
|
|
||||||
lrc = ""
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lrc, err = conventTTMLToLRC(ttml)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Failed to parse lyrics: %s \n", err)
|
|
||||||
} else {
|
|
||||||
if Config.SaveLrcFile {
|
|
||||||
err := writeLyrics(sanAlbumFolder, lrcFilename, lrc)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Failed to write lyrics")
|
|
||||||
}
|
|
||||||
if !Config.EmbedLrc {
|
|
||||||
lrc = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exists, err := fileExists(trackPath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Failed to check if track exists.")
|
|
||||||
}
|
|
||||||
if exists {
|
|
||||||
fmt.Println("Track already exists locally.")
|
|
||||||
counter.Success++
|
|
||||||
okDict[albumId] = append(okDict[albumId], trackNum)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if needDlAacLc {
|
|
||||||
if mediaUserToken == "" || len(mediaUserToken) <= 10 {
|
|
||||||
fmt.Println("Invalid media-user-token")
|
|
||||||
counter.Error++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err := runv3.Run(track.ID, trackPath, token, mediaUserToken)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Failed to dl aac-lc:", err)
|
|
||||||
counter.Error++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
trackM3u8Url, err := extractMedia(manifest.Attributes.ExtendedAssetUrls.EnhancedHls)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("\u26A0 Failed to extract info from manifest:", err)
|
|
||||||
counter.Unavailable++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
//边下载边解密
|
|
||||||
err = runv2.Run(track.ID, trackM3u8Url, trackPath, Config)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Failed to run v2:", err)
|
|
||||||
counter.Error++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tags := []string{
|
|
||||||
"tool=",
|
|
||||||
fmt.Sprintf("artist=%s", meta.Data[0].Attributes.ArtistName),
|
|
||||||
fmt.Sprintf("lyrics=%s", lrc),
|
|
||||||
}
|
|
||||||
if Config.EmbedCover {
|
|
||||||
if strings.Contains(albumId, "pl.") && Config.DlAlbumcoverForPlaylist {
|
|
||||||
err = writeCover(sanAlbumFolder, track.ID, track.Attributes.Artwork.URL)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Failed to write cover.")
|
|
||||||
}
|
|
||||||
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, track.ID, Config.CoverFormat))
|
|
||||||
} else {
|
|
||||||
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, "cover", Config.CoverFormat))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tagsString := strings.Join(tags, ":")
|
|
||||||
cmd := exec.Command("MP4Box", "-itags", tagsString, trackPath)
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
fmt.Printf("Embed failed: %v\n", err)
|
|
||||||
counter.Error++
|
|
||||||
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)
|
|
||||||
counter.Error++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = writeMP4Tags(trackPath, meta, trackNum, trackTotal)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("\u26A0 Failed to write tags in media:", err)
|
|
||||||
counter.Unavailable++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
counter.Success++
|
|
||||||
okDict[albumId] = append(okDict[albumId], trackNum)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -957,11 +980,13 @@ func writeMP4Tags(trackPath string, meta *structs.AutoGenerated, trackNum, track
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(meta.Data[0].Relationships.Artists.Data) > 0 {
|
if len(meta.Data[0].Relationships.Artists.Data) > 0 {
|
||||||
artistID, err := strconv.ParseUint(meta.Data[0].Relationships.Tracks.Data[index].Relationships.Artists.Data[0].ID, 10, 32)
|
if len(meta.Data[0].Relationships.Tracks.Data[index].Relationships.Artists.Data) > 0 {
|
||||||
if err != nil {
|
artistID, err := strconv.ParseUint(meta.Data[0].Relationships.Tracks.Data[index].Relationships.Artists.Data[0].ID, 10, 32)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.ItunesArtistID = int32(artistID)
|
||||||
}
|
}
|
||||||
t.ItunesArtistID = int32(artistID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(meta.Data[0].ID, "pl.") && !Config.UseSongInfoForPlaylist {
|
if strings.Contains(meta.Data[0].ID, "pl.") && !Config.UseSongInfoForPlaylist {
|
||||||
@ -1019,6 +1044,7 @@ func main() {
|
|||||||
pflag.BoolVar(&dl_atmos, "atmos", false, "Enable atmos download mode")
|
pflag.BoolVar(&dl_atmos, "atmos", false, "Enable atmos download mode")
|
||||||
pflag.BoolVar(&dl_aac, "aac", false, "Enable adm-aac download mode")
|
pflag.BoolVar(&dl_aac, "aac", false, "Enable adm-aac download mode")
|
||||||
pflag.BoolVar(&dl_select, "select", false, "Enable selective download")
|
pflag.BoolVar(&dl_select, "select", false, "Enable selective download")
|
||||||
|
pflag.BoolVar(&dl_song, "song", false, "Enable single song download mode")
|
||||||
pflag.BoolVar(&artist_select, "all-album", false, "Download all artist albums")
|
pflag.BoolVar(&artist_select, "all-album", false, "Download all artist albums")
|
||||||
pflag.BoolVar(&debug_mode, "debug", false, "Enable debug mode to show audio quality information")
|
pflag.BoolVar(&debug_mode, "debug", false, "Enable debug mode to show audio quality information")
|
||||||
alac_max = pflag.Int("alac-max", Config.AlacMax, "Specify the max quality for download alac")
|
alac_max = pflag.Int("alac-max", Config.AlacMax, "Specify the max quality for download alac")
|
||||||
@ -1064,19 +1090,24 @@ func main() {
|
|||||||
}
|
}
|
||||||
albumTotal := len(os.Args)
|
albumTotal := len(os.Args)
|
||||||
for {
|
for {
|
||||||
for albumNum, url := range os.Args {
|
for albumNum, urlRaw := range os.Args {
|
||||||
fmt.Printf("Album %d of %d:\n", albumNum+1, albumTotal)
|
fmt.Printf("Album %d of %d:\n", albumNum+1, albumTotal)
|
||||||
var storefront, albumId string
|
var storefront, albumId string
|
||||||
if strings.Contains(url, "/playlist/") {
|
if strings.Contains(urlRaw, "/playlist/") {
|
||||||
storefront, albumId = checkUrlPlaylist(url)
|
storefront, albumId = checkUrlPlaylist(urlRaw)
|
||||||
} else {
|
} else {
|
||||||
storefront, albumId = checkUrl(url)
|
storefront, albumId = checkUrl(urlRaw)
|
||||||
}
|
}
|
||||||
if albumId == "" {
|
if albumId == "" {
|
||||||
fmt.Printf("Invalid URL: %s\n", url)
|
fmt.Printf("Invalid URL: %s\n", urlRaw)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
err = rip(albumId, token, storefront, Config.MediaUserToken)
|
parse, err := url.Parse(urlRaw)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Invalid URL: %v", err)
|
||||||
|
}
|
||||||
|
var urlArg_i = parse.Query().Get("i")
|
||||||
|
err = rip(albumId, token, storefront, Config.MediaUserToken, urlArg_i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Album failed.")
|
fmt.Println("Album failed.")
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
@ -1308,12 +1339,11 @@ func extractMediaQuality(b string) (string, error) {
|
|||||||
return "", errors.New("m3u8 not of master type")
|
return "", errors.New("m3u8 not of master type")
|
||||||
}
|
}
|
||||||
master := from.(*m3u8.MasterPlaylist)
|
master := from.(*m3u8.MasterPlaylist)
|
||||||
|
|
||||||
if debug_mode {
|
if debug_mode {
|
||||||
fmt.Println("\nDebug: All Available Variants:")
|
fmt.Println("\nDebug: All Available Variants:")
|
||||||
fmt.Println("-----------------------------")
|
fmt.Println("-----------------------------")
|
||||||
for _, variant := range master.Variants {
|
for _, variant := range master.Variants {
|
||||||
fmt.Printf("Codec: %s, Audio: %s, Bandwidth: %d\n",
|
fmt.Printf("Codec: %s, Audio: %s, Bandwidth: %d\n",
|
||||||
variant.Codecs, variant.Audio, variant.Bandwidth)
|
variant.Codecs, variant.Audio, variant.Bandwidth)
|
||||||
}
|
}
|
||||||
fmt.Println("-----------------------------")
|
fmt.Println("-----------------------------")
|
||||||
@ -1389,11 +1419,9 @@ func extractMediaQuality(b string) (string, error) {
|
|||||||
fmt.Printf("Dolby Atmos : %s\n", formatAvailability(hasAtmos, atmosQuality))
|
fmt.Printf("Dolby Atmos : %s\n", formatAvailability(hasAtmos, atmosQuality))
|
||||||
fmt.Printf("Dolby Audio : %s\n", formatAvailability(hasDolbyAudio, dolbyAudioQuality))
|
fmt.Printf("Dolby Audio : %s\n", formatAvailability(hasDolbyAudio, dolbyAudioQuality))
|
||||||
fmt.Println("------------------------")
|
fmt.Println("------------------------")
|
||||||
|
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// For non-debug mode, return the quality string based on the selected format
|
|
||||||
var Quality string
|
var Quality string
|
||||||
for _, variant := range master.Variants {
|
for _, variant := range master.Variants {
|
||||||
if dl_atmos {
|
if dl_atmos {
|
||||||
@ -1471,7 +1499,7 @@ func extractMedia(b string) (string, error) {
|
|||||||
if dl_atmos {
|
if dl_atmos {
|
||||||
if variant.Codecs == "ec-3" && strings.Contains(variant.Audio, "atmos") {
|
if variant.Codecs == "ec-3" && strings.Contains(variant.Audio, "atmos") {
|
||||||
if debug_mode {
|
if debug_mode {
|
||||||
fmt.Printf("Debug: Found Dolby Atmos variant - %s (Bitrate: %d kbps)\n",
|
fmt.Printf("Debug: Found Dolby Atmos variant - %s (Bitrate: %d kbps)\n",
|
||||||
variant.Audio, variant.Bandwidth/1000)
|
variant.Audio, variant.Bandwidth/1000)
|
||||||
}
|
}
|
||||||
streamUrlTemp, err := masterUrl.Parse(variant.URI)
|
streamUrlTemp, err := masterUrl.Parse(variant.URI)
|
||||||
@ -1482,7 +1510,7 @@ func extractMedia(b string) (string, error) {
|
|||||||
break
|
break
|
||||||
} else if variant.Codecs == "ac-3" { // Add Dolby Audio support
|
} else if variant.Codecs == "ac-3" { // Add Dolby Audio support
|
||||||
if debug_mode {
|
if debug_mode {
|
||||||
fmt.Printf("Debug: Found Dolby Audio variant - %s (Bitrate: %d kbps)\n",
|
fmt.Printf("Debug: Found Dolby Audio variant - %s (Bitrate: %d kbps)\n",
|
||||||
variant.Audio, variant.Bandwidth/1000)
|
variant.Audio, variant.Bandwidth/1000)
|
||||||
}
|
}
|
||||||
streamUrlTemp, err := masterUrl.Parse(variant.URI)
|
streamUrlTemp, err := masterUrl.Parse(variant.URI)
|
||||||
@ -1517,10 +1545,6 @@ func extractMedia(b string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if length_int <= Config.AlacMax {
|
if length_int <= Config.AlacMax {
|
||||||
if debug_mode {
|
|
||||||
fmt.Printf("Debug: Found ALAC variant - %s-bit / %s Hz (Bitrate: %d)\n",
|
|
||||||
split[length-1], split[length-2], variant.Bandwidth)
|
|
||||||
}
|
|
||||||
streamUrlTemp, err := masterUrl.Parse(variant.URI)
|
streamUrlTemp, err := masterUrl.Parse(variant.URI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -3,9 +3,9 @@ package structs
|
|||||||
type ConfigSet struct {
|
type ConfigSet struct {
|
||||||
MediaUserToken string `yaml:"media-user-token"`
|
MediaUserToken string `yaml:"media-user-token"`
|
||||||
AuthorizationToken string `yaml:"authorization-token"`
|
AuthorizationToken string `yaml:"authorization-token"`
|
||||||
Language string `yaml:"language"`
|
Language string `yaml:"language"`
|
||||||
SaveLrcFile bool `yaml:"save-lrc-file"`
|
SaveLrcFile bool `yaml:"save-lrc-file"`
|
||||||
LrcType string `yaml:"lrc-type"`
|
LrcType string `yaml:"lrc-type"`
|
||||||
LrcFormat string `yaml:"lrc-format"`
|
LrcFormat string `yaml:"lrc-format"`
|
||||||
SaveAnimatedArtwork bool `yaml:"save-animated-artwork"`
|
SaveAnimatedArtwork bool `yaml:"save-animated-artwork"`
|
||||||
EmbyAnimatedArtwork bool `yaml:"emby-animated-artwork"`
|
EmbyAnimatedArtwork bool `yaml:"emby-animated-artwork"`
|
||||||
@ -23,7 +23,7 @@ type ConfigSet struct {
|
|||||||
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"`
|
||||||
MaxMemoryLimit int `yaml:"max-memory-limit"`
|
MaxMemoryLimit int `yaml:"max-memory-limit"`
|
||||||
DecryptM3u8Port string `yaml:"decrypt-m3u8-port"`
|
DecryptM3u8Port string `yaml:"decrypt-m3u8-port"`
|
||||||
GetM3u8Port string `yaml:"get-m3u8-port"`
|
GetM3u8Port string `yaml:"get-m3u8-port"`
|
||||||
GetM3u8Mode string `yaml:"get-m3u8-mode"`
|
GetM3u8Mode string `yaml:"get-m3u8-mode"`
|
||||||
@ -38,10 +38,10 @@ type ConfigSet struct {
|
|||||||
|
|
||||||
type Counter struct {
|
type Counter struct {
|
||||||
Unavailable int
|
Unavailable int
|
||||||
NotSong int
|
NotSong int
|
||||||
Error int
|
Error int
|
||||||
Success int
|
Success int
|
||||||
Total int
|
Total int
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApiResult struct {
|
type ApiResult struct {
|
||||||
@ -181,6 +181,62 @@ type SongResult struct {
|
|||||||
} `json:"offers"`
|
} `json:"offers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TrackData struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
Attributes struct {
|
||||||
|
Previews []struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
} `json:"previews"`
|
||||||
|
Artwork struct {
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
BgColor string `json:"bgColor"`
|
||||||
|
TextColor1 string `json:"textColor1"`
|
||||||
|
TextColor2 string `json:"textColor2"`
|
||||||
|
TextColor3 string `json:"textColor3"`
|
||||||
|
TextColor4 string `json:"textColor4"`
|
||||||
|
} `json:"artwork"`
|
||||||
|
ArtistName string `json:"artistName"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
DiscNumber int `json:"discNumber"`
|
||||||
|
GenreNames []string `json:"genreNames"`
|
||||||
|
HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"`
|
||||||
|
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
||||||
|
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
|
||||||
|
ContentRating string `json:"contentRating"`
|
||||||
|
DurationInMillis int `json:"durationInMillis"`
|
||||||
|
ReleaseDate string `json:"releaseDate"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Isrc string `json:"isrc"`
|
||||||
|
AudioTraits []string `json:"audioTraits"`
|
||||||
|
HasLyrics bool `json:"hasLyrics"`
|
||||||
|
AlbumName string `json:"albumName"`
|
||||||
|
PlayParams struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
} `json:"playParams"`
|
||||||
|
TrackNumber int `json:"trackNumber"`
|
||||||
|
AudioLocale string `json:"audioLocale"`
|
||||||
|
ComposerName string `json:"composerName"`
|
||||||
|
} `json:"attributes"`
|
||||||
|
Relationships struct {
|
||||||
|
Artists struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Href string `json:"href"`
|
||||||
|
Attributes struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"attributes"`
|
||||||
|
} `json:"data"`
|
||||||
|
} `json:"artists"`
|
||||||
|
} `json:"relationships"`
|
||||||
|
}
|
||||||
|
|
||||||
type AutoGenerated struct {
|
type AutoGenerated struct {
|
||||||
Data []struct {
|
Data []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
@ -246,63 +302,9 @@ type AutoGenerated struct {
|
|||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
} `json:"artists"`
|
} `json:"artists"`
|
||||||
Tracks struct {
|
Tracks struct {
|
||||||
Href string `json:"href"`
|
Href string `json:"href"`
|
||||||
Next string `json:"next"`
|
Next string `json:"next"`
|
||||||
Data []struct {
|
Data []TrackData `json:"data"`
|
||||||
ID string `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Href string `json:"href"`
|
|
||||||
Attributes struct {
|
|
||||||
Previews []struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
} `json:"previews"`
|
|
||||||
Artwork struct {
|
|
||||||
Width int `json:"width"`
|
|
||||||
Height int `json:"height"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
BgColor string `json:"bgColor"`
|
|
||||||
TextColor1 string `json:"textColor1"`
|
|
||||||
TextColor2 string `json:"textColor2"`
|
|
||||||
TextColor3 string `json:"textColor3"`
|
|
||||||
TextColor4 string `json:"textColor4"`
|
|
||||||
} `json:"artwork"`
|
|
||||||
ArtistName string `json:"artistName"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
DiscNumber int `json:"discNumber"`
|
|
||||||
GenreNames []string `json:"genreNames"`
|
|
||||||
HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"`
|
|
||||||
IsMasteredForItunes bool `json:"isMasteredForItunes"`
|
|
||||||
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
|
|
||||||
ContentRating string `json:"contentRating"`
|
|
||||||
DurationInMillis int `json:"durationInMillis"`
|
|
||||||
ReleaseDate string `json:"releaseDate"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Isrc string `json:"isrc"`
|
|
||||||
AudioTraits []string `json:"audioTraits"`
|
|
||||||
HasLyrics bool `json:"hasLyrics"`
|
|
||||||
AlbumName string `json:"albumName"`
|
|
||||||
PlayParams struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Kind string `json:"kind"`
|
|
||||||
} `json:"playParams"`
|
|
||||||
TrackNumber int `json:"trackNumber"`
|
|
||||||
AudioLocale string `json:"audioLocale"`
|
|
||||||
ComposerName string `json:"composerName"`
|
|
||||||
} `json:"attributes"`
|
|
||||||
Relationships struct {
|
|
||||||
Artists struct {
|
|
||||||
Href string `json:"href"`
|
|
||||||
Data []struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Href string `json:"href"`
|
|
||||||
Attributes struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
} `json:"attributes"`
|
|
||||||
} `json:"data"`
|
|
||||||
} `json:"artists"`
|
|
||||||
} `json:"relationships"`
|
|
||||||
} `json:"data"`
|
|
||||||
} `json:"tracks"`
|
} `json:"tracks"`
|
||||||
} `json:"relationships"`
|
} `json:"relationships"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
|
Loading…
x
Reference in New Issue
Block a user