mirror of
https://github.com/zhaarey/apple-music-downloader.git
synced 2025-10-23 15:11:05 +00:00
feat: Add interactive search with arrow-key navigation
This commit is contained in:
@@ -45,3 +45,5 @@ use-songinfo-for-playlist: false
|
|||||||
dl-albumcover-for-playlist: false
|
dl-albumcover-for-playlist: false
|
||||||
mv-audio-type: atmos #atmos ac3 aac
|
mv-audio-type: atmos #atmos ac3 aac
|
||||||
mv-max: 2160
|
mv-max: 2160
|
||||||
|
Storefront: "enter your storefront" #country-code that is shown in urls i.e (us, ca, jp etc..)
|
||||||
|
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -14,6 +14,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
|
||||||
github.com/PuerkitoBio/goquery v1.10.1 // indirect
|
github.com/PuerkitoBio/goquery v1.10.1 // indirect
|
||||||
github.com/STARRY-S/zip v0.2.1 // indirect
|
github.com/STARRY-S/zip v0.2.1 // indirect
|
||||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
@@ -46,6 +47,7 @@ require (
|
|||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/klauspost/compress v1.17.11 // indirect
|
github.com/klauspost/compress v1.17.11 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||||
@@ -54,6 +56,7 @@ require (
|
|||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||||
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
github.com/mholt/acmez/v3 v3.0.0 // indirect
|
github.com/mholt/acmez/v3 v3.0.0 // indirect
|
||||||
github.com/mholt/archives v0.1.0 // indirect
|
github.com/mholt/archives v0.1.0 // indirect
|
||||||
github.com/miekg/dns v1.1.62 // indirect
|
github.com/miekg/dns v1.1.62 // indirect
|
||||||
|
|||||||
373
main.go
373
main.go
@@ -27,6 +27,7 @@ import (
|
|||||||
"main/utils/structs"
|
"main/utils/structs"
|
||||||
"main/utils/task"
|
"main/utils/task"
|
||||||
|
|
||||||
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/grafov/m3u8"
|
"github.com/grafov/m3u8"
|
||||||
"github.com/olekukonko/tablewriter"
|
"github.com/olekukonko/tablewriter"
|
||||||
@@ -54,16 +55,17 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func loadConfig() error {
|
func loadConfig() error {
|
||||||
// 读取config.yaml文件内容
|
|
||||||
data, err := os.ReadFile("config.yaml")
|
data, err := os.ReadFile("config.yaml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// 将yaml解析到config变量中
|
|
||||||
err = yaml.Unmarshal(data, &Config)
|
err = yaml.Unmarshal(data, &Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if Config.Storefront == "" {
|
||||||
|
Config.Storefront = "us"
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,11 +245,7 @@ func checkArtist(artistUrl string, token string, relationship string) ([]string,
|
|||||||
} else if relationship == "music-videos" {
|
} else if relationship == "music-videos" {
|
||||||
table.SetHeader([]string{"", "MV Name", "Date", "MV ID"})
|
table.SetHeader([]string{"", "MV Name", "Date", "MV ID"})
|
||||||
}
|
}
|
||||||
//table.SetFooter([]string{"", "", "Total", "$146.93"})
|
|
||||||
//table.SetAutoMergeCells(true)
|
|
||||||
//table.SetAutoMergeCellsByColumnIndex([]int{1,2,3})
|
|
||||||
table.SetRowLine(false)
|
table.SetRowLine(false)
|
||||||
//table.AppendBulk(options)
|
|
||||||
table.SetHeaderColor(tablewriter.Colors{},
|
table.SetHeaderColor(tablewriter.Colors{},
|
||||||
tablewriter.Colors{tablewriter.FgRedColor, tablewriter.Bold},
|
tablewriter.Colors{tablewriter.FgRedColor, tablewriter.Bold},
|
||||||
tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor},
|
tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor},
|
||||||
@@ -273,29 +271,26 @@ func checkArtist(artistUrl string, token string, relationship string) ([]string,
|
|||||||
cyanColor.Print("Enter your choice: ")
|
cyanColor.Print("Enter your choice: ")
|
||||||
input, _ := reader.ReadString('\n')
|
input, _ := reader.ReadString('\n')
|
||||||
|
|
||||||
// Remove newline and whitespace
|
|
||||||
input = strings.TrimSpace(input)
|
input = strings.TrimSpace(input)
|
||||||
if input == "all" {
|
if input == "all" {
|
||||||
fmt.Println("You have selected all options:")
|
fmt.Println("You have selected all options:")
|
||||||
return urls, nil
|
return urls, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split input into string slices
|
|
||||||
selectedOptions := [][]string{}
|
selectedOptions := [][]string{}
|
||||||
parts := strings.Split(input, ",")
|
parts := strings.Split(input, ",")
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
if strings.Contains(part, "-") { // Range setting
|
if strings.Contains(part, "-") {
|
||||||
rangeParts := strings.Split(part, "-")
|
rangeParts := strings.Split(part, "-")
|
||||||
selectedOptions = append(selectedOptions, rangeParts)
|
selectedOptions = append(selectedOptions, rangeParts)
|
||||||
} else { // Single option
|
} else {
|
||||||
selectedOptions = append(selectedOptions, []string{part})
|
selectedOptions = append(selectedOptions, []string{part})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print selected options
|
|
||||||
fmt.Println("You have selected the following options:")
|
fmt.Println("You have selected the following options:")
|
||||||
for _, opt := range selectedOptions {
|
for _, opt := range selectedOptions {
|
||||||
if len(opt) == 1 { // Single option
|
if len(opt) == 1 {
|
||||||
num, err := strconv.Atoi(opt[0])
|
num, err := strconv.Atoi(opt[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Invalid option:", opt[0])
|
fmt.Println("Invalid option:", opt[0])
|
||||||
@@ -307,7 +302,7 @@ func checkArtist(artistUrl string, token string, relationship string) ([]string,
|
|||||||
} else {
|
} else {
|
||||||
fmt.Println("Option out of range:", opt[0])
|
fmt.Println("Option out of range:", opt[0])
|
||||||
}
|
}
|
||||||
} else if len(opt) == 2 { // Range
|
} else if len(opt) == 2 {
|
||||||
start, err1 := strconv.Atoi(opt[0])
|
start, err1 := strconv.Atoi(opt[0])
|
||||||
end, err2 := strconv.Atoi(opt[1])
|
end, err2 := strconv.Atoi(opt[1])
|
||||||
if err1 != nil || err2 != nil {
|
if err1 != nil || err2 != nil {
|
||||||
@@ -402,6 +397,215 @@ func contains(slice []string, item string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// START: New functions for search functionality
|
||||||
|
|
||||||
|
// SearchResultItem is a unified struct to hold search results for display.
|
||||||
|
type SearchResultItem struct {
|
||||||
|
Type string
|
||||||
|
Name string
|
||||||
|
Detail string
|
||||||
|
URL string
|
||||||
|
ID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// QualityOption holds information about a downloadable quality.
|
||||||
|
type QualityOption struct {
|
||||||
|
ID string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
// setDlFlags configures the global download flags based on the user's quality selection.
|
||||||
|
func setDlFlags(quality string) {
|
||||||
|
dl_atmos = false
|
||||||
|
dl_aac = false
|
||||||
|
|
||||||
|
switch quality {
|
||||||
|
case "atmos":
|
||||||
|
dl_atmos = true
|
||||||
|
fmt.Println("Quality set to: Dolby Atmos")
|
||||||
|
case "aac":
|
||||||
|
dl_aac = true
|
||||||
|
*aac_type = "aac"
|
||||||
|
fmt.Println("Quality set to: High-Quality (AAC)")
|
||||||
|
case "alac":
|
||||||
|
fmt.Println("Quality set to: Lossless (ALAC)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// promptForQuality asks the user to select a download quality for the chosen media.
|
||||||
|
func promptForQuality(item SearchResultItem, token string) (string, error) {
|
||||||
|
if item.Type == "Artist" {
|
||||||
|
fmt.Println("Artist selected. Proceeding to list all albums/videos.")
|
||||||
|
return "default", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nFetching available qualities for: %s\n", item.Name)
|
||||||
|
|
||||||
|
qualities := []QualityOption{
|
||||||
|
{ID: "alac", Description: "Lossless (ALAC)"},
|
||||||
|
{ID: "aac", Description: "High-Quality (AAC)"},
|
||||||
|
{ID: "atmos", Description: "Dolby Atmos"},
|
||||||
|
}
|
||||||
|
qualityOptions := []string{}
|
||||||
|
for _, q := range qualities {
|
||||||
|
qualityOptions = append(qualityOptions, q.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt := &survey.Select{
|
||||||
|
Message: "Select a quality to download:",
|
||||||
|
Options: qualityOptions,
|
||||||
|
PageSize: 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedIndex := 0
|
||||||
|
err := survey.AskOne(prompt, &selectedIndex)
|
||||||
|
if err != nil {
|
||||||
|
// This can happen if the user presses Ctrl+C
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return qualities[selectedIndex].ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSearch manages the entire interactive search process.
|
||||||
|
func handleSearch(searchType string, queryParts []string, token string) (string, error) {
|
||||||
|
query := strings.Join(queryParts, " ")
|
||||||
|
validTypes := map[string]bool{"album": true, "song": true, "artist": true}
|
||||||
|
if !validTypes[searchType] {
|
||||||
|
return "", fmt.Errorf("invalid search type: %s. Use 'album', 'song', or 'artist'", searchType)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Searching for %ss: \"%s\" in storefront \"%s\"\n", searchType, query, Config.Storefront)
|
||||||
|
|
||||||
|
offset := 0
|
||||||
|
limit := 15 // Increased limit for better navigation
|
||||||
|
|
||||||
|
apiSearchType := searchType + "s"
|
||||||
|
|
||||||
|
for {
|
||||||
|
searchResp, err := ampapi.Search(Config.Storefront, query, apiSearchType, Config.Language, token, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error fetching search results: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var items []SearchResultItem
|
||||||
|
var displayOptions []string
|
||||||
|
hasNext := false
|
||||||
|
|
||||||
|
// Special options for navigation
|
||||||
|
const prevPageOpt = "⬅️ Previous Page"
|
||||||
|
const nextPageOpt = "➡️ Next Page"
|
||||||
|
|
||||||
|
// Add previous page option if applicable
|
||||||
|
if offset > 0 {
|
||||||
|
displayOptions = append(displayOptions, prevPageOpt)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch searchType {
|
||||||
|
case "album":
|
||||||
|
if searchResp.Results.Albums != nil {
|
||||||
|
for _, item := range searchResp.Results.Albums.Data {
|
||||||
|
year := ""
|
||||||
|
if len(item.Attributes.ReleaseDate) >= 4 {
|
||||||
|
year = item.Attributes.ReleaseDate[:4]
|
||||||
|
}
|
||||||
|
trackInfo := fmt.Sprintf("%d tracks", item.Attributes.TrackCount)
|
||||||
|
detail := fmt.Sprintf("%s (%s, %s)", item.Attributes.ArtistName, year, trackInfo)
|
||||||
|
displayOptions = append(displayOptions, fmt.Sprintf("%s - %s", item.Attributes.Name, detail))
|
||||||
|
items = append(items, SearchResultItem{Type: "Album", URL: item.Attributes.URL, ID: item.ID})
|
||||||
|
}
|
||||||
|
hasNext = searchResp.Results.Albums.Next != ""
|
||||||
|
}
|
||||||
|
case "song":
|
||||||
|
if searchResp.Results.Songs != nil {
|
||||||
|
for _, item := range searchResp.Results.Songs.Data {
|
||||||
|
detail := fmt.Sprintf("%s (%s)", item.Attributes.ArtistName, item.Attributes.AlbumName)
|
||||||
|
displayOptions = append(displayOptions, fmt.Sprintf("%s - %s", item.Attributes.Name, detail))
|
||||||
|
items = append(items, SearchResultItem{Type: "Song", URL: item.Attributes.URL, ID: item.ID})
|
||||||
|
}
|
||||||
|
hasNext = searchResp.Results.Songs.Next != ""
|
||||||
|
}
|
||||||
|
case "artist":
|
||||||
|
if searchResp.Results.Artists != nil {
|
||||||
|
for _, item := range searchResp.Results.Artists.Data {
|
||||||
|
detail := ""
|
||||||
|
if len(item.Attributes.GenreNames) > 0 {
|
||||||
|
detail = strings.Join(item.Attributes.GenreNames, ", ")
|
||||||
|
}
|
||||||
|
displayOptions = append(displayOptions, fmt.Sprintf("%s (%s)", item.Attributes.Name, detail))
|
||||||
|
items = append(items, SearchResultItem{Type: "Artist", URL: item.Attributes.URL, ID: item.ID})
|
||||||
|
}
|
||||||
|
hasNext = searchResp.Results.Artists.Next != ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(items) == 0 && offset == 0 {
|
||||||
|
fmt.Println("No results found.")
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add next page option if applicable
|
||||||
|
if hasNext {
|
||||||
|
displayOptions = append(displayOptions, nextPageOpt)
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt := &survey.Select{
|
||||||
|
Message: "Use arrow keys to navigate, Enter to select:",
|
||||||
|
Options: displayOptions,
|
||||||
|
PageSize: limit, // Show a full page of results
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedIndex := 0
|
||||||
|
err = survey.AskOne(prompt, &selectedIndex)
|
||||||
|
if err != nil {
|
||||||
|
// User pressed Ctrl+C
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedOption := displayOptions[selectedIndex]
|
||||||
|
|
||||||
|
// Handle pagination
|
||||||
|
if selectedOption == nextPageOpt {
|
||||||
|
offset += limit
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if selectedOption == prevPageOpt {
|
||||||
|
offset -= limit
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust index to match the `items` slice if "Previous Page" was an option
|
||||||
|
itemIndex := selectedIndex
|
||||||
|
if offset > 0 {
|
||||||
|
itemIndex--
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedItem := items[itemIndex]
|
||||||
|
|
||||||
|
// Automatically set single song download flag
|
||||||
|
if selectedItem.Type == "Song" {
|
||||||
|
dl_song = true
|
||||||
|
}
|
||||||
|
|
||||||
|
quality, err := promptForQuality(selectedItem, token)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("could not process quality selection: %w", err)
|
||||||
|
}
|
||||||
|
if quality == "" { // User cancelled quality selection
|
||||||
|
fmt.Println("Selection cancelled.")
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if quality != "default" {
|
||||||
|
setDlFlags(quality)
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedItem.URL, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// END: New functions for search functionality
|
||||||
|
|
||||||
func ripTrack(track *task.Track, token string, mediaUserToken string) {
|
func ripTrack(track *task.Track, token string, mediaUserToken string) {
|
||||||
var err error
|
var err error
|
||||||
counter.Total++
|
counter.Total++
|
||||||
@@ -573,7 +777,6 @@ func ripTrack(track *task.Track, token string, mediaUserToken string) {
|
|||||||
tags := []string{
|
tags := []string{
|
||||||
"tool=",
|
"tool=",
|
||||||
fmt.Sprintf("artist=%s", track.Resp.Attributes.ArtistName),
|
fmt.Sprintf("artist=%s", track.Resp.Attributes.ArtistName),
|
||||||
//fmt.Sprintf("lyrics=%s", lrc),
|
|
||||||
}
|
}
|
||||||
if Config.EmbedCover {
|
if Config.EmbedCover {
|
||||||
if (strings.Contains(track.PreID, "pl.") || strings.Contains(track.PreID, "ra.")) && Config.DlAlbumcoverForPlaylist {
|
if (strings.Contains(track.PreID, "pl.") || strings.Contains(track.PreID, "ra.")) && Config.DlAlbumcoverForPlaylist {
|
||||||
@@ -627,7 +830,6 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken
|
|||||||
Codec = "ALAC"
|
Codec = "ALAC"
|
||||||
}
|
}
|
||||||
station.Codec = Codec
|
station.Codec = Codec
|
||||||
// Get Artist Folder
|
|
||||||
var singerFoldername string
|
var singerFoldername string
|
||||||
if Config.ArtistFolderFormat != "" {
|
if Config.ArtistFolderFormat != "" {
|
||||||
singerFoldername = strings.NewReplacer(
|
singerFoldername = strings.NewReplacer(
|
||||||
@@ -648,10 +850,9 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken
|
|||||||
if dl_aac {
|
if dl_aac {
|
||||||
singerFolder = filepath.Join(Config.AacSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_"))
|
singerFolder = filepath.Join(Config.AacSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_"))
|
||||||
}
|
}
|
||||||
os.MkdirAll(singerFolder, os.ModePerm) // Create artist folder
|
os.MkdirAll(singerFolder, os.ModePerm)
|
||||||
station.SaveDir = singerFolder
|
station.SaveDir = singerFolder
|
||||||
|
|
||||||
//Get Playlist Folder Name
|
|
||||||
playlistFolder := strings.NewReplacer(
|
playlistFolder := strings.NewReplacer(
|
||||||
"{ArtistName}", "Apple Music Station",
|
"{ArtistName}", "Apple Music Station",
|
||||||
"{PlaylistName}", LimitString(station.Name),
|
"{PlaylistName}", LimitString(station.Name),
|
||||||
@@ -668,19 +869,16 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken
|
|||||||
os.MkdirAll(playlistFolderPath, os.ModePerm)
|
os.MkdirAll(playlistFolderPath, os.ModePerm)
|
||||||
station.SaveName = playlistFolder
|
station.SaveName = playlistFolder
|
||||||
fmt.Println(playlistFolder)
|
fmt.Println(playlistFolder)
|
||||||
//先省略封面相关的获取
|
|
||||||
//get playlist cover
|
|
||||||
covPath, err := writeCover(playlistFolderPath, "cover", meta.Data[0].Attributes.Artwork.URL)
|
covPath, err := writeCover(playlistFolderPath, "cover", meta.Data[0].Attributes.Artwork.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Failed to write cover.")
|
fmt.Println("Failed to write cover.")
|
||||||
}
|
}
|
||||||
station.CoverPath = covPath
|
station.CoverPath = covPath
|
||||||
|
|
||||||
//get animated artwork
|
|
||||||
if Config.SaveAnimatedArtwork && meta.Data[0].Attributes.EditorialVideo.MotionSquare.Video != "" {
|
if Config.SaveAnimatedArtwork && meta.Data[0].Attributes.EditorialVideo.MotionSquare.Video != "" {
|
||||||
fmt.Println("Found Animation Artwork.")
|
fmt.Println("Found Animation Artwork.")
|
||||||
|
|
||||||
// Download square version
|
|
||||||
motionvideoUrlSquare, err := extractVideo(meta.Data[0].Attributes.EditorialVideo.MotionSquare.Video)
|
motionvideoUrlSquare, err := extractVideo(meta.Data[0].Attributes.EditorialVideo.MotionSquare.Video)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("no motion video square.\n", err)
|
fmt.Println("no motion video square.\n", err)
|
||||||
@@ -703,14 +901,12 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken
|
|||||||
}
|
}
|
||||||
|
|
||||||
if Config.EmbyAnimatedArtwork {
|
if Config.EmbyAnimatedArtwork {
|
||||||
// Convert square version to gif
|
|
||||||
cmd3 := exec.Command("ffmpeg", "-i", filepath.Join(playlistFolderPath, "square_animated_artwork.mp4"), "-vf", "scale=440:-1", "-r", "24", "-f", "gif", filepath.Join(playlistFolderPath, "folder.jpg"))
|
cmd3 := exec.Command("ffmpeg", "-i", filepath.Join(playlistFolderPath, "square_animated_artwork.mp4"), "-vf", "scale=440:-1", "-r", "24", "-f", "gif", filepath.Join(playlistFolderPath, "folder.jpg"))
|
||||||
if err := cmd3.Run(); err != nil {
|
if err := cmd3.Run(); err != nil {
|
||||||
fmt.Printf("animated artwork square to gif err: %v\n", err)
|
fmt.Printf("animated artwork square to gif err: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 处理stream类型的station
|
|
||||||
if station.Type == "stream" {
|
if station.Type == "stream" {
|
||||||
counter.Total++
|
counter.Total++
|
||||||
if isInArray(okDict[station.ID], 1) {
|
if isInArray(okDict[station.ID], 1) {
|
||||||
@@ -744,7 +940,6 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
trackM3U8 := strings.ReplaceAll(assetsUrl, "index.m3u8", "256/prog_index.m3u8")
|
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)
|
keyAndUrls, _ := runv3.Run(station.ID, trackM3U8, token, mediaUserToken, true)
|
||||||
err = runv3.ExtMvData(keyAndUrls, trackPath)
|
err = runv3.ExtMvData(keyAndUrls, trackPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -752,7 +947,6 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken
|
|||||||
counter.Error++
|
counter.Error++
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
//tags
|
|
||||||
tags := []string{
|
tags := []string{
|
||||||
"tool=",
|
"tool=",
|
||||||
"disk=1/1",
|
"disk=1/1",
|
||||||
@@ -761,7 +955,6 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken
|
|||||||
fmt.Sprintf("artist=%s", "Apple Music Station"),
|
fmt.Sprintf("artist=%s", "Apple Music Station"),
|
||||||
fmt.Sprintf("performer=%s", "Apple Music Station"),
|
fmt.Sprintf("performer=%s", "Apple Music Station"),
|
||||||
fmt.Sprintf("album_artist=%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("album=%s", station.Name),
|
||||||
fmt.Sprintf("title=%s", station.Name),
|
fmt.Sprintf("title=%s", station.Name),
|
||||||
}
|
}
|
||||||
@@ -794,18 +987,10 @@ func ripStation(albumId string, token string, storefront string, mediaUserToken
|
|||||||
if true {
|
if true {
|
||||||
selected = arr
|
selected = arr
|
||||||
}
|
}
|
||||||
//Download tracks
|
|
||||||
for i := range station.Tracks {
|
for i := range station.Tracks {
|
||||||
i++
|
i++
|
||||||
// if isInArray(okDict[playlistId], i) {
|
|
||||||
// //fmt.Println("已完成直接跳过.\n")
|
|
||||||
// counter.Total++
|
|
||||||
// counter.Success++
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
if isInArray(selected, i) {
|
if isInArray(selected, i) {
|
||||||
ripTrack(&station.Tracks[i-1], token, mediaUserToken)
|
ripTrack(&station.Tracks[i-1], token, mediaUserToken)
|
||||||
//downloadTrack(trackNum, trackTotal, meta, track, albumId, token, storefront, mediaUserToken, sanAlbumFolder, Codec, covPath)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -819,9 +1004,7 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
meta := album.Resp
|
meta := album.Resp
|
||||||
//debug mode
|
|
||||||
if debug_mode {
|
if debug_mode {
|
||||||
// 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)
|
||||||
|
|
||||||
@@ -837,11 +1020,9 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st
|
|||||||
}
|
}
|
||||||
|
|
||||||
var m3u8Url string
|
var m3u8Url string
|
||||||
//Web端m3u8
|
|
||||||
if manifest.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls != "" {
|
if manifest.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls != "" {
|
||||||
m3u8Url = manifest.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls
|
m3u8Url = manifest.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls
|
||||||
}
|
}
|
||||||
//设备端满血m3u8
|
|
||||||
needCheck := false
|
needCheck := false
|
||||||
if Config.GetM3u8Mode == "all" {
|
if Config.GetM3u8Mode == "all" {
|
||||||
needCheck = true
|
needCheck = true
|
||||||
@@ -863,9 +1044,8 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil // Return directly without showing statistics
|
return nil
|
||||||
}
|
}
|
||||||
// Get Codec
|
|
||||||
var Codec string
|
var Codec string
|
||||||
if dl_atmos {
|
if dl_atmos {
|
||||||
Codec = "ATMOS"
|
Codec = "ATMOS"
|
||||||
@@ -875,7 +1055,6 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st
|
|||||||
Codec = "ALAC"
|
Codec = "ALAC"
|
||||||
}
|
}
|
||||||
album.Codec = Codec
|
album.Codec = Codec
|
||||||
// Get Artist Folder
|
|
||||||
var singerFoldername string
|
var singerFoldername string
|
||||||
if Config.ArtistFolderFormat != "" {
|
if Config.ArtistFolderFormat != "" {
|
||||||
if len(meta.Data[0].Relationships.Artists.Data) > 0 {
|
if len(meta.Data[0].Relationships.Artists.Data) > 0 {
|
||||||
@@ -904,9 +1083,8 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st
|
|||||||
if dl_aac {
|
if dl_aac {
|
||||||
singerFolder = filepath.Join(Config.AacSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_"))
|
singerFolder = filepath.Join(Config.AacSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_"))
|
||||||
}
|
}
|
||||||
os.MkdirAll(singerFolder, os.ModePerm) // Create artist folder
|
os.MkdirAll(singerFolder, os.ModePerm)
|
||||||
album.SaveDir = singerFolder
|
album.SaveDir = singerFolder
|
||||||
//Get Quality
|
|
||||||
var Quality string
|
var Quality string
|
||||||
if strings.Contains(Config.AlbumFolderFormat, "Quality") {
|
if strings.Contains(Config.AlbumFolderFormat, "Quality") {
|
||||||
if dl_atmos {
|
if dl_atmos {
|
||||||
@@ -921,7 +1099,6 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st
|
|||||||
if manifest1.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls == "" {
|
if manifest1.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls == "" {
|
||||||
Codec = "AAC"
|
Codec = "AAC"
|
||||||
Quality = "256Kbps"
|
Quality = "256Kbps"
|
||||||
//fmt.Println("Unavailable.\n")
|
|
||||||
} else {
|
} else {
|
||||||
needCheck := false
|
needCheck := false
|
||||||
|
|
||||||
@@ -945,7 +1122,6 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Set Album Folder Tags
|
|
||||||
stringsToJoin := []string{}
|
stringsToJoin := []string{}
|
||||||
if meta.Data[0].Attributes.IsAppleDigitalMaster || meta.Data[0].Attributes.IsMasteredForItunes {
|
if meta.Data[0].Attributes.IsAppleDigitalMaster || meta.Data[0].Attributes.IsMasteredForItunes {
|
||||||
if Config.AppleMasterChoice != "" {
|
if Config.AppleMasterChoice != "" {
|
||||||
@@ -963,7 +1139,6 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Tag_string := strings.Join(stringsToJoin, " ")
|
Tag_string := strings.Join(stringsToJoin, " ")
|
||||||
//Get Album Folder Name
|
|
||||||
var albumFolderName string
|
var albumFolderName string
|
||||||
albumFolderName = strings.NewReplacer(
|
albumFolderName = strings.NewReplacer(
|
||||||
"{ReleaseDate}", meta.Data[0].Attributes.ReleaseDate,
|
"{ReleaseDate}", meta.Data[0].Attributes.ReleaseDate,
|
||||||
@@ -984,10 +1159,9 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st
|
|||||||
}
|
}
|
||||||
albumFolderName = strings.TrimSpace(albumFolderName)
|
albumFolderName = strings.TrimSpace(albumFolderName)
|
||||||
albumFolderPath := filepath.Join(singerFolder, forbiddenNames.ReplaceAllString(albumFolderName, "_"))
|
albumFolderPath := filepath.Join(singerFolder, forbiddenNames.ReplaceAllString(albumFolderName, "_"))
|
||||||
os.MkdirAll(albumFolderPath, os.ModePerm) // Create album folder
|
os.MkdirAll(albumFolderPath, os.ModePerm)
|
||||||
album.SaveName = albumFolderName
|
album.SaveName = albumFolderName
|
||||||
fmt.Println(albumFolderName)
|
fmt.Println(albumFolderName)
|
||||||
//get artist cover
|
|
||||||
if Config.SaveArtistCover {
|
if Config.SaveArtistCover {
|
||||||
if len(meta.Data[0].Relationships.Artists.Data) > 0 {
|
if len(meta.Data[0].Relationships.Artists.Data) > 0 {
|
||||||
_, err = writeCover(singerFolder, "folder", meta.Data[0].Relationships.Artists.Data[0].Attributes.Artwork.Url)
|
_, err = writeCover(singerFolder, "folder", meta.Data[0].Relationships.Artists.Data[0].Attributes.Artwork.Url)
|
||||||
@@ -996,16 +1170,13 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//get playlist cover
|
|
||||||
covPath, err := writeCover(albumFolderPath, "cover", meta.Data[0].Attributes.Artwork.URL)
|
covPath, err := writeCover(albumFolderPath, "cover", meta.Data[0].Attributes.Artwork.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Failed to write cover.")
|
fmt.Println("Failed to write cover.")
|
||||||
}
|
}
|
||||||
//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 square version
|
|
||||||
motionvideoUrlSquare, err := extractVideo(meta.Data[0].Attributes.EditorialVideo.MotionDetailSquare.Video)
|
motionvideoUrlSquare, err := extractVideo(meta.Data[0].Attributes.EditorialVideo.MotionDetailSquare.Video)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("no motion video square.\n", err)
|
fmt.Println("no motion video square.\n", err)
|
||||||
@@ -1028,14 +1199,12 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st
|
|||||||
}
|
}
|
||||||
|
|
||||||
if Config.EmbyAnimatedArtwork {
|
if Config.EmbyAnimatedArtwork {
|
||||||
// Convert square version to gif
|
|
||||||
cmd3 := exec.Command("ffmpeg", "-i", filepath.Join(albumFolderPath, "square_animated_artwork.mp4"), "-vf", "scale=440:-1", "-r", "24", "-f", "gif", filepath.Join(albumFolderPath, "folder.jpg"))
|
cmd3 := exec.Command("ffmpeg", "-i", filepath.Join(albumFolderPath, "square_animated_artwork.mp4"), "-vf", "scale=440:-1", "-r", "24", "-f", "gif", filepath.Join(albumFolderPath, "folder.jpg"))
|
||||||
if err := cmd3.Run(); err != nil {
|
if err := cmd3.Run(); err != nil {
|
||||||
fmt.Printf("animated artwork square to gif err: %v\n", err)
|
fmt.Printf("animated artwork square to gif err: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
fmt.Println("no motion video tall.\n", err)
|
fmt.Println("no motion video tall.\n", err)
|
||||||
@@ -1057,13 +1226,11 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//填充子track信息
|
|
||||||
for i := range album.Tracks {
|
for i := range album.Tracks {
|
||||||
album.Tracks[i].CoverPath = covPath
|
album.Tracks[i].CoverPath = covPath
|
||||||
album.Tracks[i].SaveDir = albumFolderPath
|
album.Tracks[i].SaveDir = albumFolderPath
|
||||||
album.Tracks[i].Codec = Codec
|
album.Tracks[i].Codec = Codec
|
||||||
}
|
}
|
||||||
//Get selected tracks
|
|
||||||
trackTotal := len(meta.Data[0].Relationships.Tracks.Data)
|
trackTotal := len(meta.Data[0].Relationships.Tracks.Data)
|
||||||
arr := make([]int, trackTotal)
|
arr := make([]int, trackTotal)
|
||||||
for i := 0; i < trackTotal; i++ {
|
for i := 0; i < trackTotal; i++ {
|
||||||
@@ -1072,13 +1239,10 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st
|
|||||||
|
|
||||||
if dl_song {
|
if dl_song {
|
||||||
if urlArg_i == "" {
|
if urlArg_i == "" {
|
||||||
//fmt.Println("URL does not contain parameter 'i'. Please ensure the URL includes 'i' or use another mode.")
|
|
||||||
//return nil
|
|
||||||
} else {
|
} else {
|
||||||
for i := range album.Tracks {
|
for i := range album.Tracks {
|
||||||
if urlArg_i == album.Tracks[i].ID {
|
if urlArg_i == album.Tracks[i].ID {
|
||||||
ripTrack(&album.Tracks[i], token, mediaUserToken)
|
ripTrack(&album.Tracks[i], token, mediaUserToken)
|
||||||
//downloadTrack(trackNum, trackTotal, meta, track, albumId, token, storefront, mediaUserToken, albumFolderPath, Codec, covPath)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1091,18 +1255,15 @@ func ripAlbum(albumId string, token string, storefront string, mediaUserToken st
|
|||||||
} else {
|
} else {
|
||||||
selected = album.ShowSelect()
|
selected = album.ShowSelect()
|
||||||
}
|
}
|
||||||
//Download tracks
|
|
||||||
for i := range album.Tracks {
|
for i := range album.Tracks {
|
||||||
i++
|
i++
|
||||||
if isInArray(okDict[albumId], i) {
|
if isInArray(okDict[albumId], i) {
|
||||||
//fmt.Println("已完成直接跳过.\n")
|
|
||||||
counter.Total++
|
counter.Total++
|
||||||
counter.Success++
|
counter.Success++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isInArray(selected, i) {
|
if isInArray(selected, i) {
|
||||||
ripTrack(&album.Tracks[i-1], token, mediaUserToken)
|
ripTrack(&album.Tracks[i-1], token, mediaUserToken)
|
||||||
//downloadTrack(trackNum, trackTotal, meta, track, albumId, token, storefront, mediaUserToken, sanAlbumFolder, Codec, covPath)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -1117,7 +1278,6 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo
|
|||||||
}
|
}
|
||||||
meta := playlist.Resp
|
meta := playlist.Resp
|
||||||
if debug_mode {
|
if debug_mode {
|
||||||
// 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)
|
||||||
|
|
||||||
@@ -1133,11 +1293,9 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo
|
|||||||
}
|
}
|
||||||
|
|
||||||
var m3u8Url string
|
var m3u8Url string
|
||||||
//Web端m3u8
|
|
||||||
if manifest.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls != "" {
|
if manifest.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls != "" {
|
||||||
m3u8Url = manifest.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls
|
m3u8Url = manifest.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls
|
||||||
}
|
}
|
||||||
//设备端满血m3u8
|
|
||||||
needCheck := false
|
needCheck := false
|
||||||
if Config.GetM3u8Mode == "all" {
|
if Config.GetM3u8Mode == "all" {
|
||||||
needCheck = true
|
needCheck = true
|
||||||
@@ -1159,7 +1317,7 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil // Return directly without showing statistics
|
return nil
|
||||||
}
|
}
|
||||||
var Codec string
|
var Codec string
|
||||||
if dl_atmos {
|
if dl_atmos {
|
||||||
@@ -1170,7 +1328,6 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo
|
|||||||
Codec = "ALAC"
|
Codec = "ALAC"
|
||||||
}
|
}
|
||||||
playlist.Codec = Codec
|
playlist.Codec = Codec
|
||||||
// Get Artist Folder
|
|
||||||
var singerFoldername string
|
var singerFoldername string
|
||||||
if Config.ArtistFolderFormat != "" {
|
if Config.ArtistFolderFormat != "" {
|
||||||
singerFoldername = strings.NewReplacer(
|
singerFoldername = strings.NewReplacer(
|
||||||
@@ -1191,9 +1348,8 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo
|
|||||||
if dl_aac {
|
if dl_aac {
|
||||||
singerFolder = filepath.Join(Config.AacSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_"))
|
singerFolder = filepath.Join(Config.AacSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_"))
|
||||||
}
|
}
|
||||||
os.MkdirAll(singerFolder, os.ModePerm) // Create artist folder
|
os.MkdirAll(singerFolder, os.ModePerm)
|
||||||
playlist.SaveDir = singerFolder
|
playlist.SaveDir = singerFolder
|
||||||
//Get Quality
|
|
||||||
|
|
||||||
var Quality string
|
var Quality string
|
||||||
if strings.Contains(Config.AlbumFolderFormat, "Quality") {
|
if strings.Contains(Config.AlbumFolderFormat, "Quality") {
|
||||||
@@ -1209,7 +1365,6 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo
|
|||||||
if manifest1.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls == "" {
|
if manifest1.Data[0].Attributes.ExtendedAssetUrls.EnhancedHls == "" {
|
||||||
Codec = "AAC"
|
Codec = "AAC"
|
||||||
Quality = "256Kbps"
|
Quality = "256Kbps"
|
||||||
//fmt.Println("Unavailable.\n")
|
|
||||||
} else {
|
} else {
|
||||||
needCheck := false
|
needCheck := false
|
||||||
|
|
||||||
@@ -1233,7 +1388,6 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Set Playlist Folder Tags
|
|
||||||
stringsToJoin := []string{}
|
stringsToJoin := []string{}
|
||||||
if meta.Data[0].Attributes.IsAppleDigitalMaster || meta.Data[0].Attributes.IsMasteredForItunes {
|
if meta.Data[0].Attributes.IsAppleDigitalMaster || meta.Data[0].Attributes.IsMasteredForItunes {
|
||||||
if Config.AppleMasterChoice != "" {
|
if Config.AppleMasterChoice != "" {
|
||||||
@@ -1251,7 +1405,6 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Tag_string := strings.Join(stringsToJoin, " ")
|
Tag_string := strings.Join(stringsToJoin, " ")
|
||||||
//Get Playlist Folder Name
|
|
||||||
playlistFolder := strings.NewReplacer(
|
playlistFolder := strings.NewReplacer(
|
||||||
"{ArtistName}", "Apple Music",
|
"{ArtistName}", "Apple Music",
|
||||||
"{PlaylistName}", LimitString(meta.Data[0].Attributes.Name),
|
"{PlaylistName}", LimitString(meta.Data[0].Attributes.Name),
|
||||||
@@ -1268,8 +1421,6 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo
|
|||||||
os.MkdirAll(playlistFolderPath, os.ModePerm)
|
os.MkdirAll(playlistFolderPath, os.ModePerm)
|
||||||
playlist.SaveName = playlistFolder
|
playlist.SaveName = playlistFolder
|
||||||
fmt.Println(playlistFolder)
|
fmt.Println(playlistFolder)
|
||||||
//先省略封面相关的获取
|
|
||||||
//get playlist cover
|
|
||||||
covPath, err := writeCover(playlistFolderPath, "cover", meta.Data[0].Attributes.Artwork.URL)
|
covPath, err := writeCover(playlistFolderPath, "cover", meta.Data[0].Attributes.Artwork.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Failed to write cover.")
|
fmt.Println("Failed to write cover.")
|
||||||
@@ -1281,11 +1432,9 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo
|
|||||||
playlist.Tracks[i].Codec = Codec
|
playlist.Tracks[i].Codec = Codec
|
||||||
}
|
}
|
||||||
|
|
||||||
//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 square version
|
|
||||||
motionvideoUrlSquare, err := extractVideo(meta.Data[0].Attributes.EditorialVideo.MotionDetailSquare.Video)
|
motionvideoUrlSquare, err := extractVideo(meta.Data[0].Attributes.EditorialVideo.MotionDetailSquare.Video)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("no motion video square.\n", err)
|
fmt.Println("no motion video square.\n", err)
|
||||||
@@ -1308,14 +1457,12 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if Config.EmbyAnimatedArtwork {
|
if Config.EmbyAnimatedArtwork {
|
||||||
// Convert square version to gif
|
|
||||||
cmd3 := exec.Command("ffmpeg", "-i", filepath.Join(playlistFolderPath, "square_animated_artwork.mp4"), "-vf", "scale=440:-1", "-r", "24", "-f", "gif", filepath.Join(playlistFolderPath, "folder.jpg"))
|
cmd3 := exec.Command("ffmpeg", "-i", filepath.Join(playlistFolderPath, "square_animated_artwork.mp4"), "-vf", "scale=440:-1", "-r", "24", "-f", "gif", filepath.Join(playlistFolderPath, "folder.jpg"))
|
||||||
if err := cmd3.Run(); err != nil {
|
if err := cmd3.Run(); err != nil {
|
||||||
fmt.Printf("animated artwork square to gif err: %v\n", err)
|
fmt.Printf("animated artwork square to gif err: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
fmt.Println("no motion video tall.\n", err)
|
fmt.Println("no motion video tall.\n", err)
|
||||||
@@ -1349,18 +1496,15 @@ func ripPlaylist(playlistId string, token string, storefront string, mediaUserTo
|
|||||||
} else {
|
} else {
|
||||||
selected = playlist.ShowSelect()
|
selected = playlist.ShowSelect()
|
||||||
}
|
}
|
||||||
//Download tracks
|
|
||||||
for i := range playlist.Tracks {
|
for i := range playlist.Tracks {
|
||||||
i++
|
i++
|
||||||
if isInArray(okDict[playlistId], i) {
|
if isInArray(okDict[playlistId], i) {
|
||||||
//fmt.Println("已完成直接跳过.\n")
|
|
||||||
counter.Total++
|
counter.Total++
|
||||||
counter.Success++
|
counter.Success++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isInArray(selected, i) {
|
if isInArray(selected, i) {
|
||||||
ripTrack(&playlist.Tracks[i-1], token, mediaUserToken)
|
ripTrack(&playlist.Tracks[i-1], token, mediaUserToken)
|
||||||
//downloadTrack(trackNum, trackTotal, meta, track, albumId, token, storefront, mediaUserToken, sanAlbumFolder, Codec, covPath)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -1381,10 +1525,7 @@ func writeMP4Tags(track *task.Track, lrc string) error {
|
|||||||
},
|
},
|
||||||
Composer: track.Resp.Attributes.ComposerName,
|
Composer: track.Resp.Attributes.ComposerName,
|
||||||
ComposerSort: track.Resp.Attributes.ComposerName,
|
ComposerSort: track.Resp.Attributes.ComposerName,
|
||||||
//Date: meta.Data[0].Attributes.ReleaseDate,
|
|
||||||
CustomGenre: track.Resp.Attributes.GenreNames[0],
|
CustomGenre: track.Resp.Attributes.GenreNames[0],
|
||||||
//Copyright: meta.Data[0].Attributes.Copyright,
|
|
||||||
//Publisher: meta.Data[0].Attributes.RecordLabel,
|
|
||||||
Lyrics: lrc,
|
Lyrics: lrc,
|
||||||
TrackNumber: int16(track.Resp.Attributes.TrackNumber),
|
TrackNumber: int16(track.Resp.Attributes.TrackNumber),
|
||||||
DiscNumber: int16(track.Resp.Attributes.DiscNumber),
|
DiscNumber: int16(track.Resp.Attributes.DiscNumber),
|
||||||
@@ -1473,7 +1614,8 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Define command-line flags
|
var search_type string
|
||||||
|
pflag.StringVar(&search_type, "search", "", "Search for 'album', 'song', or 'artist'. Provide query after flags.")
|
||||||
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")
|
||||||
@@ -1486,14 +1628,13 @@ func main() {
|
|||||||
mv_audio_type = pflag.String("mv-audio-type", Config.MVAudioType, "Select MV audio type, atmos ac3 aac")
|
mv_audio_type = pflag.String("mv-audio-type", Config.MVAudioType, "Select MV audio type, atmos ac3 aac")
|
||||||
mv_max = pflag.Int("mv-max", Config.MVMax, "Specify the max quality for download MV")
|
mv_max = pflag.Int("mv-max", Config.MVMax, "Specify the max quality for download MV")
|
||||||
|
|
||||||
// Custom usage message for help
|
|
||||||
pflag.Usage = func() {
|
pflag.Usage = func() {
|
||||||
fmt.Fprintf(os.Stderr, "Usage: %s [options] url1 url2 ...\n", "[main | main.exe | go run main.go]")
|
fmt.Fprintf(os.Stderr, "Usage: %s [options] [url1 url2 ...]\n", "[main | main.exe | go run main.go]")
|
||||||
fmt.Println("Options:")
|
fmt.Fprintf(os.Stderr, "Search Usage: %s --search [album|song|artist] [query]\n", "[main | main.exe | go run main.go]")
|
||||||
|
fmt.Println("\nOptions:")
|
||||||
pflag.PrintDefaults()
|
pflag.PrintDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the flag arguments
|
|
||||||
pflag.Parse()
|
pflag.Parse()
|
||||||
Config.AlacMax = *alac_max
|
Config.AlacMax = *alac_max
|
||||||
Config.AtmosMax = *atmos_max
|
Config.AtmosMax = *atmos_max
|
||||||
@@ -1502,19 +1643,38 @@ func main() {
|
|||||||
Config.MVMax = *mv_max
|
Config.MVMax = *mv_max
|
||||||
|
|
||||||
args := pflag.Args()
|
args := pflag.Args()
|
||||||
|
|
||||||
|
if search_type != "" {
|
||||||
|
if len(args) == 0 {
|
||||||
|
fmt.Println("Error: --search flag requires a query.")
|
||||||
|
pflag.Usage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectedUrl, err := handleSearch(search_type, args, token)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("\nSearch process failed: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if selectedUrl == "" {
|
||||||
|
fmt.Println("\nExiting.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os.Args = []string{selectedUrl}
|
||||||
|
} else {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
fmt.Println("No URLs provided. Please provide at least one URL.")
|
fmt.Println("No URLs provided. Please provide at least one URL.")
|
||||||
pflag.Usage()
|
pflag.Usage()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
os.Args = args
|
os.Args = args
|
||||||
|
}
|
||||||
|
|
||||||
if strings.Contains(os.Args[0], "/artist/") {
|
if strings.Contains(os.Args[0], "/artist/") {
|
||||||
urlArtistName, urlArtistID, err := getUrlArtistName(os.Args[0], token)
|
urlArtistName, urlArtistID, err := getUrlArtistName(os.Args[0], token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Failed to get artistname.")
|
fmt.Println("Failed to get artistname.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//fmt.Println("get artistname:", urlArtistName)
|
|
||||||
Config.ArtistFolderFormat = strings.NewReplacer(
|
Config.ArtistFolderFormat = strings.NewReplacer(
|
||||||
"{UrlArtistName}", LimitString(urlArtistName),
|
"{UrlArtistName}", LimitString(urlArtistName),
|
||||||
"{ArtistId}", urlArtistID,
|
"{ArtistId}", urlArtistID,
|
||||||
@@ -1527,7 +1687,6 @@ func main() {
|
|||||||
mvArgs, err := checkArtist(os.Args[0], token, "music-videos")
|
mvArgs, err := checkArtist(os.Args[0], token, "music-videos")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Failed to get artist music-videos.")
|
fmt.Println("Failed to get artist music-videos.")
|
||||||
//return
|
|
||||||
}
|
}
|
||||||
os.Args = append(albumArgs, mvArgs...)
|
os.Args = append(albumArgs, mvArgs...)
|
||||||
}
|
}
|
||||||
@@ -1537,7 +1696,6 @@ func main() {
|
|||||||
fmt.Printf("Queue %d of %d: ", albumNum+1, albumTotal)
|
fmt.Printf("Queue %d of %d: ", albumNum+1, albumTotal)
|
||||||
var storefront, albumId string
|
var storefront, albumId string
|
||||||
|
|
||||||
//mv dl dev
|
|
||||||
if strings.Contains(urlRaw, "/music-video/") {
|
if strings.Contains(urlRaw, "/music-video/") {
|
||||||
fmt.Println("Music Video")
|
fmt.Println("Music Video")
|
||||||
if debug_mode {
|
if debug_mode {
|
||||||
@@ -1576,12 +1734,15 @@ func main() {
|
|||||||
}
|
}
|
||||||
if strings.Contains(urlRaw, "/song/") {
|
if strings.Contains(urlRaw, "/song/") {
|
||||||
fmt.Printf("Song->")
|
fmt.Printf("Song->")
|
||||||
|
// When dl_song is true from search, we don't need to re-fetch the album URL
|
||||||
|
if !dl_song {
|
||||||
urlRaw, err = getUrlSong(urlRaw, token)
|
urlRaw, err = getUrlSong(urlRaw, token)
|
||||||
dl_song = true
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Failed to get Song info.")
|
fmt.Println("Failed to get Song info.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dl_song = true
|
||||||
|
}
|
||||||
parse, err := url.Parse(urlRaw)
|
parse, err := url.Parse(urlRaw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Invalid URL: %v", err)
|
log.Fatalf("Invalid URL: %v", err)
|
||||||
@@ -1663,16 +1824,13 @@ func mvDownloader(adamID string, saveDir string, token string, storefront string
|
|||||||
}
|
}
|
||||||
|
|
||||||
os.MkdirAll(saveDir, os.ModePerm)
|
os.MkdirAll(saveDir, os.ModePerm)
|
||||||
//video
|
|
||||||
videom3u8url, _ := extractVideo(mvm3u8url)
|
videom3u8url, _ := extractVideo(mvm3u8url)
|
||||||
videokeyAndUrls, _ := runv3.Run(adamID, videom3u8url, token, mediaUserToken, true)
|
videokeyAndUrls, _ := runv3.Run(adamID, videom3u8url, token, mediaUserToken, true)
|
||||||
_ = runv3.ExtMvData(videokeyAndUrls, vidPath)
|
_ = runv3.ExtMvData(videokeyAndUrls, vidPath)
|
||||||
//audio
|
|
||||||
audiom3u8url, _ := extractMvAudio(mvm3u8url)
|
audiom3u8url, _ := extractMvAudio(mvm3u8url)
|
||||||
audiokeyAndUrls, _ := runv3.Run(adamID, audiom3u8url, token, mediaUserToken, true)
|
audiokeyAndUrls, _ := runv3.Run(adamID, audiom3u8url, token, mediaUserToken, true)
|
||||||
_ = runv3.ExtMvData(audiokeyAndUrls, audPath)
|
_ = runv3.ExtMvData(audiokeyAndUrls, audPath)
|
||||||
|
|
||||||
//tags
|
|
||||||
tags := []string{
|
tags := []string{
|
||||||
"tool=",
|
"tool=",
|
||||||
fmt.Sprintf("artist=%s", MVInfo.Data[0].Attributes.ArtistName),
|
fmt.Sprintf("artist=%s", MVInfo.Data[0].Attributes.ArtistName),
|
||||||
@@ -1682,7 +1840,6 @@ func mvDownloader(adamID string, saveDir string, token string, storefront string
|
|||||||
fmt.Sprintf("ISRC=%s", MVInfo.Data[0].Attributes.Isrc),
|
fmt.Sprintf("ISRC=%s", MVInfo.Data[0].Attributes.Isrc),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContentRating tag
|
|
||||||
if MVInfo.Data[0].Attributes.ContentRating == "explicit" {
|
if MVInfo.Data[0].Attributes.ContentRating == "explicit" {
|
||||||
tags = append(tags, "rating=1")
|
tags = append(tags, "rating=1")
|
||||||
} else if MVInfo.Data[0].Attributes.ContentRating == "clean" {
|
} else if MVInfo.Data[0].Attributes.ContentRating == "clean" {
|
||||||
@@ -1691,7 +1848,6 @@ func mvDownloader(adamID string, saveDir string, token string, storefront string
|
|||||||
tags = append(tags, "rating=0")
|
tags = append(tags, "rating=0")
|
||||||
}
|
}
|
||||||
|
|
||||||
//根据情况额外添加可使用的tags
|
|
||||||
if track != nil {
|
if track != nil {
|
||||||
if track.PreType == "playlists" && !Config.UseSongInfoForPlaylist {
|
if track.PreType == "playlists" && !Config.UseSongInfoForPlaylist {
|
||||||
tags = append(tags, "disk=1/1")
|
tags = append(tags, "disk=1/1")
|
||||||
@@ -1700,8 +1856,6 @@ func mvDownloader(adamID string, saveDir string, token string, storefront string
|
|||||||
tags = append(tags, fmt.Sprintf("tracknum=%d/%d", track.TaskNum, track.TaskTotal))
|
tags = append(tags, fmt.Sprintf("tracknum=%d/%d", track.TaskNum, track.TaskTotal))
|
||||||
tags = append(tags, fmt.Sprintf("album_artist=%s", track.PlaylistData.Attributes.ArtistName))
|
tags = append(tags, fmt.Sprintf("album_artist=%s", track.PlaylistData.Attributes.ArtistName))
|
||||||
tags = append(tags, fmt.Sprintf("performer=%s", track.Resp.Attributes.ArtistName))
|
tags = append(tags, fmt.Sprintf("performer=%s", track.Resp.Attributes.ArtistName))
|
||||||
//tags = append(tags, fmt.Sprintf("copyright=%s", track.PlaylistData.Attributes.Copyright))
|
|
||||||
//tags = append(tags, fmt.Sprintf("UPC=%s", track.PlaylistData.Attributes.Upc))
|
|
||||||
} else if track.PreType == "playlists" && Config.UseSongInfoForPlaylist {
|
} else if track.PreType == "playlists" && Config.UseSongInfoForPlaylist {
|
||||||
tags = append(tags, fmt.Sprintf("album=%s", track.AlbumData.Attributes.Name))
|
tags = append(tags, fmt.Sprintf("album=%s", track.AlbumData.Attributes.Name))
|
||||||
tags = append(tags, fmt.Sprintf("disk=%d/%d", track.Resp.Attributes.DiscNumber, track.DiscTotal))
|
tags = append(tags, fmt.Sprintf("disk=%d/%d", track.Resp.Attributes.DiscNumber, track.DiscTotal))
|
||||||
@@ -1726,31 +1880,21 @@ func mvDownloader(adamID string, saveDir string, token string, storefront string
|
|||||||
tags = append(tags, fmt.Sprintf("disk=%d", MVInfo.Data[0].Attributes.DiscNumber))
|
tags = append(tags, fmt.Sprintf("disk=%d", MVInfo.Data[0].Attributes.DiscNumber))
|
||||||
tags = append(tags, fmt.Sprintf("track=%d", MVInfo.Data[0].Attributes.TrackNumber))
|
tags = append(tags, fmt.Sprintf("track=%d", MVInfo.Data[0].Attributes.TrackNumber))
|
||||||
tags = append(tags, fmt.Sprintf("tracknum=%d", MVInfo.Data[0].Attributes.TrackNumber))
|
tags = append(tags, fmt.Sprintf("tracknum=%d", MVInfo.Data[0].Attributes.TrackNumber))
|
||||||
//tags = append(tags, fmt.Sprintf("album_artist=%s", MVInfo.Data[0].Attributes.ArtistName))
|
|
||||||
tags = append(tags, fmt.Sprintf("performer=%s", MVInfo.Data[0].Attributes.ArtistName))
|
tags = append(tags, fmt.Sprintf("performer=%s", MVInfo.Data[0].Attributes.ArtistName))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract and save thumbnail if enabled
|
|
||||||
var covPath string
|
var covPath string
|
||||||
//强制嵌入封面
|
|
||||||
if true {
|
if true {
|
||||||
// Get the highest quality thumbnail URL from the MV info
|
|
||||||
thumbURL := MVInfo.Data[0].Attributes.Artwork.URL
|
thumbURL := MVInfo.Data[0].Attributes.Artwork.URL
|
||||||
|
|
||||||
// Generate base name without extension
|
|
||||||
baseThumbName := forbiddenNames.ReplaceAllString(mvSaveName, "_") + "_thumbnail"
|
baseThumbName := forbiddenNames.ReplaceAllString(mvSaveName, "_") + "_thumbnail"
|
||||||
|
|
||||||
// Download and save thumbnail
|
|
||||||
covPath, err = writeCover(saveDir, baseThumbName, thumbURL)
|
covPath, err = writeCover(saveDir, baseThumbName, thumbURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Failed to save MV thumbnail:", err)
|
fmt.Println("Failed to save MV thumbnail:", err)
|
||||||
} else {
|
} else {
|
||||||
//fmt.Println("MV thumbnail saved successfully")
|
|
||||||
tags = append(tags, fmt.Sprintf("cover=%s", covPath))
|
tags = append(tags, fmt.Sprintf("cover=%s", covPath))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//mux and add tag
|
|
||||||
tagsString := strings.Join(tags, ":")
|
tagsString := strings.Join(tags, ":")
|
||||||
muxCmd := exec.Command("MP4Box", "-itags", tagsString, "-quiet", "-add", vidPath, "-add", audPath, "-keep-utc", "-new", mvOutPath)
|
muxCmd := exec.Command("MP4Box", "-itags", tagsString, "-quiet", "-add", vidPath, "-add", audPath, "-keep-utc", "-new", mvOutPath)
|
||||||
fmt.Printf("MV Remuxing...")
|
fmt.Printf("MV Remuxing...")
|
||||||
@@ -1858,11 +2002,9 @@ func checkM3u8(b string, f string) (string, error) {
|
|||||||
fmt.Println("Connected to device")
|
fmt.Println("Connected to device")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the length of adamID and the adamID itself
|
|
||||||
adamIDBuffer := []byte(adamID)
|
adamIDBuffer := []byte(adamID)
|
||||||
lengthBuffer := []byte{byte(len(adamIDBuffer))}
|
lengthBuffer := []byte{byte(len(adamIDBuffer))}
|
||||||
|
|
||||||
// Write length and adamID to the connection
|
|
||||||
_, err = conn.Write(lengthBuffer)
|
_, err = conn.Write(lengthBuffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error writing length to device:", err)
|
fmt.Println("Error writing length to device:", err)
|
||||||
@@ -1875,15 +2017,12 @@ func checkM3u8(b string, f string) (string, error) {
|
|||||||
return "none", err
|
return "none", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the response (URL) from the device
|
|
||||||
response, err := bufio.NewReader(conn).ReadBytes('\n')
|
response, err := bufio.NewReader(conn).ReadBytes('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error reading response from device:", err)
|
fmt.Println("Error reading response from device:", err)
|
||||||
return "none", err
|
return "none", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trim any newline characters from the response
|
|
||||||
|
|
||||||
response = bytes.TrimSpace(response)
|
response = bytes.TrimSpace(response)
|
||||||
if len(response) > 0 {
|
if len(response) > 0 {
|
||||||
if f == "song" {
|
if f == "song" {
|
||||||
@@ -1936,14 +2075,10 @@ func extractMedia(b string, more_mode bool) (string, string, error) {
|
|||||||
var data [][]string
|
var data [][]string
|
||||||
for _, variant := range master.Variants {
|
for _, variant := range master.Variants {
|
||||||
data = append(data, []string{variant.Codecs, variant.Audio, fmt.Sprint(variant.Bandwidth)})
|
data = append(data, []string{variant.Codecs, variant.Audio, fmt.Sprint(variant.Bandwidth)})
|
||||||
//fmt.Printf("Codec: %s, Audio: %s, Bandwidth: %d\n",
|
|
||||||
//variant.Codecs, variant.Audio, variant.Bandwidth)
|
|
||||||
}
|
}
|
||||||
table := tablewriter.NewWriter(os.Stdout)
|
table := tablewriter.NewWriter(os.Stdout)
|
||||||
table.SetHeader([]string{"Codec", "Audio", "Bandwidth"})
|
table.SetHeader([]string{"Codec", "Audio", "Bandwidth"})
|
||||||
//table.SetFooter([]string{"", "", "Total", "$146.93"})
|
|
||||||
table.SetAutoMergeCells(true)
|
table.SetAutoMergeCells(true)
|
||||||
//table.SetAutoMergeCellsByColumnIndex([]int{1,2,3})
|
|
||||||
table.SetRowLine(true)
|
table.SetRowLine(true)
|
||||||
table.AppendBulk(data)
|
table.AppendBulk(data)
|
||||||
table.Render()
|
table.Render()
|
||||||
@@ -1951,7 +2086,6 @@ func extractMedia(b string, more_mode bool) (string, string, error) {
|
|||||||
var hasAAC, hasLossless, hasHiRes, hasAtmos, hasDolbyAudio bool
|
var hasAAC, hasLossless, hasHiRes, hasAtmos, hasDolbyAudio bool
|
||||||
var aacQuality, losslessQuality, hiResQuality, atmosQuality, dolbyAudioQuality string
|
var aacQuality, losslessQuality, hiResQuality, atmosQuality, dolbyAudioQuality string
|
||||||
|
|
||||||
// Check for all formats
|
|
||||||
for _, variant := range master.Variants {
|
for _, variant := range master.Variants {
|
||||||
if variant.Codecs == "mp4a.40.2" { // AAC
|
if variant.Codecs == "mp4a.40.2" { // AAC
|
||||||
hasAAC = true
|
hasAAC = true
|
||||||
@@ -1973,7 +2107,6 @@ func extractMedia(b string, more_mode bool) (string, string, error) {
|
|||||||
split := strings.Split(variant.Audio, "-")
|
split := strings.Split(variant.Audio, "-")
|
||||||
if len(split) > 0 {
|
if len(split) > 0 {
|
||||||
bitrateStr := split[len(split)-1]
|
bitrateStr := split[len(split)-1]
|
||||||
// Remove leading "2" if present in "2768"
|
|
||||||
if len(bitrateStr) == 4 && bitrateStr[0] == '2' {
|
if len(bitrateStr) == 4 && bitrateStr[0] == '2' {
|
||||||
bitrateStr = bitrateStr[1:]
|
bitrateStr = bitrateStr[1:]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package structs
|
package structs
|
||||||
|
|
||||||
type ConfigSet struct {
|
type ConfigSet struct {
|
||||||
|
Storefront string `yaml:"storefront"`
|
||||||
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"`
|
||||||
|
|||||||
Reference in New Issue
Block a user