|
|
|
@ -41,8 +41,11 @@ var ( |
|
|
|
|
type Config struct { |
|
|
|
|
MediaUserToken string `yaml:"media-user-token"` |
|
|
|
|
SaveLrcFile bool `yaml:"save-lrc-file"` |
|
|
|
|
SaveAnimatedArtwork bool `yaml:"save-animated-artwork"` |
|
|
|
|
EmbyAnimatedArtwork bool `yaml:"emby-animated-artwork"` |
|
|
|
|
EmbedLrc bool `yaml:"embed-lrc"` |
|
|
|
|
EmbedCover bool `yaml:"embed-cover"` |
|
|
|
|
SaveArtistCover bool `yaml:"save-artist-cover"` |
|
|
|
|
CoverSize string `yaml:"cover-size"` |
|
|
|
|
CoverFormat string `yaml:"cover-format"` |
|
|
|
|
AlacSaveFolder string `yaml:"alac-save-folder"` |
|
|
|
@ -1098,9 +1101,10 @@ func getMeta(albumId string, token string, storefront string) (*AutoGenerated, e |
|
|
|
|
query.Set("omit[resource]", "autos") |
|
|
|
|
query.Set("include", "tracks,artists,record-labels") |
|
|
|
|
query.Set("include[songs]", "artists") |
|
|
|
|
query.Set("fields[artists]", "name") |
|
|
|
|
query.Set("fields[artists]", "name,artwork") |
|
|
|
|
query.Set("fields[albums:albums]", "artistName,artwork,name,releaseDate,url") |
|
|
|
|
query.Set("fields[record-labels]", "name") |
|
|
|
|
query.Set("extend", "editorialVideo") |
|
|
|
|
// query.Set("l", "en-gb")
|
|
|
|
|
req.URL.RawQuery = query.Encode() |
|
|
|
|
do, err := http.DefaultClient.Do(req) |
|
|
|
@ -1315,10 +1319,48 @@ func rip(albumId string, token string, storefront string, userToken string) erro |
|
|
|
|
sanAlbumFolder := filepath.Join(singerFolder, forbiddenNames.ReplaceAllString(albumFolder, "_")) |
|
|
|
|
os.MkdirAll(sanAlbumFolder, os.ModePerm) |
|
|
|
|
fmt.Println(albumFolder) |
|
|
|
|
//get artist cover
|
|
|
|
|
if config.SaveArtistCover && !(strings.Contains(albumId, "pl.")) { |
|
|
|
|
err = writeCover(singerFolder, "folder", meta.Data[0].Relationships.Artists.Data[0].Attributes.Artwork.Url) |
|
|
|
|
if err != nil { |
|
|
|
|
fmt.Println("Failed to write artist cover.") |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
//get album cover
|
|
|
|
|
err = writeCover(sanAlbumFolder, "cover", meta.Data[0].Attributes.Artwork.URL) |
|
|
|
|
if err != nil { |
|
|
|
|
fmt.Println("Failed to write cover.") |
|
|
|
|
} |
|
|
|
|
//get animated artwork
|
|
|
|
|
if config.SaveAnimatedArtwork && meta.Data[0].Attributes.EditorialVideo.MotionDetailSquare.Video != "" { |
|
|
|
|
fmt.Println("Found Animation Artwork.") |
|
|
|
|
motionvideoUrl, err := extractVideo(meta.Data[0].Attributes.EditorialVideo.MotionDetailSquare.Video) |
|
|
|
|
if err != nil { |
|
|
|
|
fmt.Println("no motion video.\n", err) |
|
|
|
|
} |
|
|
|
|
exists, err := fileExists(filepath.Join(sanAlbumFolder, "animated_artwork.mp4")) |
|
|
|
|
if err != nil { |
|
|
|
|
fmt.Println("Failed to check if animated artwork exists.") |
|
|
|
|
} |
|
|
|
|
if exists { |
|
|
|
|
fmt.Println("Animated artwork already exists locally.") |
|
|
|
|
} else { |
|
|
|
|
fmt.Println("Animation Artwork Downloading...") |
|
|
|
|
cmd := exec.Command("ffmpeg", "-loglevel", "quiet", "-y", "-i", motionvideoUrl, "-c", "copy", filepath.Join(sanAlbumFolder, "animated_artwork.mp4")) |
|
|
|
|
if err := cmd.Run(); err != nil { |
|
|
|
|
fmt.Printf("animated artwork dl err: %v\n", err) |
|
|
|
|
} else { |
|
|
|
|
fmt.Println("Animation Artwork Downloaded") |
|
|
|
|
} |
|
|
|
|
if config.EmbyAnimatedArtwork { |
|
|
|
|
cmd2 := exec.Command("ffmpeg", "-i", filepath.Join(sanAlbumFolder, "animated_artwork.mp4"), "-vf", "scale=440:-1", "-r", "24", "-f", "gif", filepath.Join(sanAlbumFolder, "folder.jpg")) |
|
|
|
|
if err := cmd2.Run(); err != nil { |
|
|
|
|
fmt.Printf("animated artwork to gif err: %v\n", err) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
trackTotal := len(meta.Data[0].Relationships.Tracks.Data) |
|
|
|
|
for trackNum, track := range meta.Data[0].Relationships.Tracks.Data { |
|
|
|
|
trackNum++ |
|
|
|
@ -1651,6 +1693,46 @@ func extractMedia(b string) (string, []string, error) { |
|
|
|
|
return streamUrl.String(), keys, nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func extractVideo(c string) (string, error) { |
|
|
|
|
MediaUrl, err := url.Parse(c) |
|
|
|
|
if err != nil { |
|
|
|
|
return "", err |
|
|
|
|
} |
|
|
|
|
resp, err := http.Get(c) |
|
|
|
|
if err != nil { |
|
|
|
|
return "", err |
|
|
|
|
} |
|
|
|
|
defer resp.Body.Close() |
|
|
|
|
if resp.StatusCode != http.StatusOK { |
|
|
|
|
return "", errors.New(resp.Status) |
|
|
|
|
} |
|
|
|
|
body, err := io.ReadAll(resp.Body) |
|
|
|
|
if err != nil { |
|
|
|
|
return "", err |
|
|
|
|
} |
|
|
|
|
videoString := string(body) |
|
|
|
|
from, listType, err := m3u8.DecodeFrom(strings.NewReader(videoString), true) |
|
|
|
|
if err != nil || listType != m3u8.MASTER { |
|
|
|
|
return "", errors.New("m3u8 not of media type") |
|
|
|
|
} |
|
|
|
|
video := from.(*m3u8.MasterPlaylist) |
|
|
|
|
var streamUrl *url.URL |
|
|
|
|
sort.Slice(video.Variants, func(i, j int) bool { |
|
|
|
|
return video.Variants[i].AverageBandwidth > video.Variants[j].AverageBandwidth |
|
|
|
|
}) |
|
|
|
|
if len(video.Variants) > 0 { |
|
|
|
|
highestBandwidthVariant := video.Variants[0] |
|
|
|
|
streamUrl, err = MediaUrl.Parse(highestBandwidthVariant.URI) |
|
|
|
|
if err != nil { |
|
|
|
|
return "", err |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
if streamUrl == nil { |
|
|
|
|
return "", errors.New("no video codec found") |
|
|
|
|
} |
|
|
|
|
return streamUrl.String(), nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func extractSong(url string) (*SongInfo, error) { |
|
|
|
|
fmt.Println("Downloading...") |
|
|
|
|
track, err := http.Get(url) |
|
|
|
@ -2101,6 +2183,14 @@ type AutoGenerated struct { |
|
|
|
|
Kind string `json:"kind"` |
|
|
|
|
} `json:"playParams"` |
|
|
|
|
IsCompilation bool `json:"isCompilation"` |
|
|
|
|
EditorialVideo struct { |
|
|
|
|
MotionDetailSquare struct { |
|
|
|
|
Video string `json:"video"` |
|
|
|
|
} `json:"motionDetailSquare"` |
|
|
|
|
MotionSquareVideo1x1 struct { |
|
|
|
|
Video string `json:"video"` |
|
|
|
|
} `json:"motionSquareVideo1x1"` |
|
|
|
|
} `json:"editorialVideo"` |
|
|
|
|
} `json:"attributes"` |
|
|
|
|
Relationships struct { |
|
|
|
|
RecordLabels struct { |
|
|
|
@ -2115,6 +2205,9 @@ type AutoGenerated struct { |
|
|
|
|
Href string `json:"href"` |
|
|
|
|
Attributes struct { |
|
|
|
|
Name string `json:"name"` |
|
|
|
|
Artwork struct { |
|
|
|
|
Url string `json:"url"` |
|
|
|
|
} `json:"artwork"` |
|
|
|
|
} `json:"attributes"` |
|
|
|
|
} `json:"data"` |
|
|
|
|
} `json:"artists"` |
|
|
|
|