diff --git a/main.go b/main.go index 7acbc12..d702f40 100644 --- a/main.go +++ b/main.go @@ -465,7 +465,7 @@ func ripTrack(track *task.Track, token string, mediaUserToken string) { if dl_atmos { Quality = fmt.Sprintf("%dkbps", Config.AtmosMax-2000) } else if needDlAacLc { - Quality = "256kbps" + Quality = "256Kbps" } else { _, Quality, err = extractMedia(track.M3u8, true) if err != nil { @@ -651,7 +651,7 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken //Get Playlist Folder Name playlistFolder := strings.NewReplacer( "{ArtistName}", "Apple Music Station", - "{PlaylistName}", LimitString(meta.Data[0].Attributes.Name), + "{PlaylistName}", LimitString(station.Name), "{PlaylistId}", station.ID, "{Quality}", "", "{Codec}", Codec, @@ -671,6 +671,8 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken if err != nil { fmt.Println("Failed to write cover.") } + station.CoverPath = covPath + //get animated artwork if Config.SaveAnimatedArtwork && meta.Data[0].Attributes.EditorialVideo.MotionSquare.Video != "" { fmt.Println("Found Animation Artwork.") @@ -705,6 +707,73 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken } } } + // 处理stream类型的station + if station.Type == "stream" { + counter.Total++ + if isInArray(okDict[station.ID], 1) { + counter.Success++ + return nil + } + songName := strings.NewReplacer( + "{SongId}", station.ID, + "{SongNumer}", "01", + "{SongName}", LimitString(station.Name), + "{DiscNumber}", "1", + "{TrackNumber}", "01", + "{Quality}", "256Kbps", + "{Tag}", "", + "{Codec}", "AAC", + ).Replace(Config.SongFileFormat) + fmt.Println(songName) + trackPath := filepath.Join(playlistFolderPath, fmt.Sprintf("%s.m4a", forbiddenNames.ReplaceAllString(songName, "_"))) + exists, _ := fileExists(trackPath) + if exists { + counter.Success++ + okDict[station.ID] = append(okDict[station.ID], 1) + + fmt.Println("Radio already exists locally.") + return nil + } + assetsUrl, err := ampapi.GetStationAssetsUrl(station.ID, mediaUserToken, token) + if err != nil { + fmt.Println("Failed to get station assets url.", err) + counter.Error++ + return err + } + trackM3U8 := strings.ReplaceAll(assetsUrl, "index.m3u8", "256/prog_index.m3u8") + //testM3U8 := "https://itsliveradio.apple.com/bb/aod/exp_aod/jpopnowradio/AkinaNakamori/cmaf/256/prog_index.m3u8" + keyAndUrls, _ := runv3.Run(station.ID, trackM3U8, token, mediaUserToken, true) + err = runv3.ExtMvData(keyAndUrls, trackPath) + if err != nil { + fmt.Println("Failed to download station stream.", err) + counter.Error++ + return err + } + //tags + tags := []string{ + "tool=", + "disk=1/1", + "track=1", + "tracknum=1/1", + fmt.Sprintf("artist=%s", "Apple Music Station"), + fmt.Sprintf("performer=%s", "Apple Music Station"), + fmt.Sprintf("album_artist=%s", "Apple Music Station"), + fmt.Sprintf("artist=%s", "Apple Music Station"), + fmt.Sprintf("album=%s", station.Name), + fmt.Sprintf("title=%s", station.Name), + } + if Config.EmbedCover { + tags = append(tags, fmt.Sprintf("cover=%s", station.CoverPath)) + } + 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.Success++ + okDict[station.ID] = append(okDict[station.ID], 1) + return nil + } for i := range station.Tracks { station.Tracks[i].CoverPath = covPath @@ -1113,7 +1182,7 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo if dl_atmos { Quality = fmt.Sprintf("%dkbps", Config.AtmosMax-2000) } else if dl_aac && Config.AacType == "aac-lc" { - Quality = "256kbps" + Quality = "256Kbps" } else { manifest1, err := ampapi.GetSongResp(storefront, meta.Data[0].Relationships.Tracks.Data[0].ID, playlist.Language, token) if err != nil { @@ -1121,7 +1190,7 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo } else { if manifest1.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls == "" { Codec = "AAC" - Quality = "256kbps" + Quality = "256Kbps" //fmt.Println("Unavailable.\n") } else { needCheck := false diff --git a/utils/ampapi/station.go b/utils/ampapi/station.go index d45aeab..acb91b2 100644 --- a/utils/ampapi/station.go +++ b/utils/ampapi/station.go @@ -45,6 +45,46 @@ func GetStationResp(storefront string, id string, language string, token string) return obj, nil } +func GetStationAssetsUrl(id string, mutoken string, token string) (string, error) { + var err error + if token == "" { + token, err = GetToken() + if err != nil { + return "", err + } + } + + req, err := http.NewRequest("GET", "https://amp-api.music.apple.com/v1/play/assets", nil) + if err != nil { + return "", err + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") + req.Header.Set("Origin", "https://music.apple.com") + req.Header.Set("Media-User-Token", mutoken) + query := url.Values{} + //query.Set("omit[resource]", "autos") + //query.Set("extend", "editorialVideo") + query.Set("id", id) + query.Set("kind", "radioStation") + query.Set("keyFormat", "web") + req.URL.RawQuery = query.Encode() + do, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer do.Body.Close() + if do.StatusCode != http.StatusOK { + return "", errors.New(do.Status) + } + obj := new(StationAssets) + err = json.NewDecoder(do.Body).Decode(&obj) + if err != nil { + return "", err + } + return obj.Results.Assets[0].Url, nil +} + func GetStationNextTracks(id, mutoken, language, token string) (*TrackResp, error) { var err error if token == "" { @@ -92,6 +132,17 @@ type StationResp struct { Data []StationRespData `json:"data"` } +type StationAssets struct { + Results struct { + Assets []struct { + KeyServerUrl string `json:"keyServerUrl"` + Url string `json:"url"` + WidevineKeyCertificateUrl string `json:"widevineKeyCertificateUrl"` + FairPlayKeyCertificateUrl string `json:"fairPlayKeyCertificateUrl"` + } `json:"assets"` + } `json:"results"` +} + type StationRespData struct { ID string `json:"id"` Type string `json:"type"` diff --git a/utils/runv3/runv3.go b/utils/runv3/runv3.go index e2fbd7d..699fe52 100644 --- a/utils/runv3/runv3.go +++ b/utils/runv3/runv3.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "fmt" "path/filepath" + "github.com/gospider007/requests" "google.golang.org/protobuf/proto" @@ -154,7 +155,7 @@ func GetWebplayback(adamId string, authtoken string, mutoken string, mvmode bool return obj.List[0].HlsPlaylistUrl, "", nil } // 遍历 Assets - for i, _ := range obj.List[0].Assets { + for i := range obj.List[0].Assets { if obj.List[0].Assets[i].Flavor == "28:ctrp256" { kidBase64, fileurl, err := extractKidBase64(obj.List[0].Assets[i].URL, false) if err != nil { @@ -298,10 +299,19 @@ func Run(adamId string, trackpath string, authtoken string, mutoken string, mvmo AfterRequest: AfterRequest, } key.CdmInit() - keystr, keybt, err := key.GetKey(ctx, "https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/acquireWebPlaybackLicense", pssh, nil) - if err != nil { - fmt.Println(err) - return "", err + var keybt []byte + if strings.Contains(adamId, "ra.") { + keystr, keybt, err = key.GetKey(ctx, "https://play.itunes.apple.com/WebObjects/MZPlay.woa/web/radio/versions/1/license", pssh, nil) + if err != nil { + fmt.Println(err) + return "", err + } + } else { + keystr, keybt, err = key.GetKey(ctx, "https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/acquireWebPlaybackLicense", pssh, nil) + if err != nil { + fmt.Println(err) + return "", err + } } if mvmode { keyAndUrls := "1:" + keystr + ";" + fileurl diff --git a/utils/task/station.go b/utils/task/station.go index a31fca0..478167d 100644 --- a/utils/task/station.go +++ b/utils/task/station.go @@ -50,10 +50,10 @@ func (a *Station) GetResp(mutoken, token, l string) error { a.Resp = *resp //简化高频调用名称 a.Type = a.Resp.Data[0].Attributes.PlayParams.Format - if a.Type != "tracks" { - return errors.New("stream类型暂未开发") - } a.Name = a.Resp.Data[0].Attributes.Name + if a.Type != "tracks" { + return nil + } tracksResp, err := ampapi.GetStationNextTracks(a.ID, mutoken, a.Language, token) if err != nil { return errors.New("error getting station tracks response")