diff --git a/main.go b/main.go index 3508b18..e90584a 100644 --- a/main.go +++ b/main.go @@ -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) diff --git a/utils/structs/structs.go b/utils/structs/structs.go index 885c4df..0d31af7 100644 --- a/utils/structs/structs.go +++ b/utils/structs/structs.go @@ -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"`