Merge branch 'main' into fix/panic_runtime_error

This commit is contained in:
ZHAAREY 2025-02-08 14:39:56 +08:00 committed by GitHub
commit b336cdc87f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 292 additions and 260 deletions

422
main.go
View File

@ -8,6 +8,7 @@ import (
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
@ -38,6 +39,7 @@ var (
dl_atmos bool
dl_aac bool
dl_select bool
dl_song bool
artist_select bool
alac_max *int
atmos_max *int
@ -425,7 +427,201 @@ func contains(slice []string, item string) bool {
return false
}
func rip(albumId string, token string, storefront string, userToken string) error {
// 下载单曲逻辑
func downloadTrack(trackNum int, trackTotal int, meta *structs.AutoGenerated, track structs.TrackData, albumId, token, storefront, userToken, 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 userToken != "your-media-user-token" && (Config.EmbedLrc || Config.SaveLrcFile) {
ttml, err := getSongLyrics(track.ID, storefront, token, userToken)
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 userToken == "your-media-user-token" {
fmt.Println("media-user-token Unset!")
counter.Error++
return
}
err := runv3.Run(track.ID, trackPath, token, userToken)
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, userToken string, urlArg_i string) error {
var Codec string
if dl_atmos {
Codec = "ATMOS"
@ -608,6 +804,22 @@ func rip(albumId string, token string, storefront string, userToken string) erro
}
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, userToken, sanAlbumFolder, Codec, &counter)
return nil
}
}
}
return nil
}
if !dl_select {
selected = arr
} else {
@ -654,195 +866,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro
}
if isInArray(selected, trackNum) {
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++
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 userToken != "your-media-user-token" && (Config.EmbedLrc || Config.SaveLrcFile) {
ttml, err := getSongLyrics(track.ID, storefront, token, userToken)
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 userToken == "your-media-user-token" {
fmt.Println("media-user-token Unset!")
counter.Error++
continue
}
err := runv3.Run(track.ID, trackPath, token, userToken)
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)
downloadTrack(trackNum, trackTotal, meta, track, albumId, token, storefront, userToken, sanAlbumFolder, Codec, &counter)
}
}
return nil
@ -947,6 +971,7 @@ func main() {
pflag.BoolVar(&dl_atmos, "atmos", false, "Enable atmos 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_song, "song", false, "Enable single song download mode")
pflag.BoolVar(&artist_select, "all-album", false, "Download all artist albums")
alac_max = pflag.Int("alac-max", Config.AlacMax, "Specify the max quality for download alac")
atmos_max = pflag.Int("atmos-max", Config.AtmosMax, "Specify the max quality for download atmos")
@ -991,19 +1016,24 @@ func main() {
}
albumTotal := len(os.Args)
for {
for albumNum, url := range os.Args {
for albumNum, urlRaw := range os.Args {
fmt.Printf("Album %d of %d:\n", albumNum+1, albumTotal)
var storefront, albumId string
if strings.Contains(url, "/playlist/") {
storefront, albumId = checkUrlPlaylist(url)
if strings.Contains(urlRaw, "/playlist/") {
storefront, albumId = checkUrlPlaylist(urlRaw)
} else {
storefront, albumId = checkUrl(url)
storefront, albumId = checkUrl(urlRaw)
}
if albumId == "" {
fmt.Printf("Invalid URL: %s\n", url)
fmt.Printf("Invalid URL: %s\n", urlRaw)
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 {
fmt.Println("Album failed.")
fmt.Println(err)

View File

@ -3,9 +3,9 @@ package structs
type ConfigSet struct {
MediaUserToken string `yaml:"media-user-token"`
AuthorizationToken string `yaml:"authorization-token"`
Language string `yaml:"language"`
Language string `yaml:"language"`
SaveLrcFile bool `yaml:"save-lrc-file"`
LrcType string `yaml:"lrc-type"`
LrcType string `yaml:"lrc-type"`
LrcFormat string `yaml:"lrc-format"`
SaveAnimatedArtwork bool `yaml:"save-animated-artwork"`
EmbyAnimatedArtwork bool `yaml:"emby-animated-artwork"`
@ -23,7 +23,7 @@ type ConfigSet struct {
ExplicitChoice string `yaml:"explicit-choice"`
CleanChoice string `yaml:"clean-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"`
GetM3u8Port string `yaml:"get-m3u8-port"`
GetM3u8Mode string `yaml:"get-m3u8-mode"`
@ -38,10 +38,10 @@ type ConfigSet struct {
type Counter struct {
Unavailable int
NotSong int
Error int
Success int
Total int
NotSong int
Error int
Success int
Total int
}
type ApiResult struct {
@ -181,6 +181,62 @@ type SongResult struct {
} `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 {
Data []struct {
ID string `json:"id"`
@ -246,63 +302,9 @@ type AutoGenerated struct {
} `json:"data"`
} `json:"artists"`
Tracks struct {
Href string `json:"href"`
Next string `json:"next"`
Data []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"`
} `json:"data"`
Href string `json:"href"`
Next string `json:"next"`
Data []TrackData `json:"data"`
} `json:"tracks"`
} `json:"relationships"`
} `json:"data"`