package main
import (
"bufio"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
)
func readLine(reader *bufio.Reader) string {
line, _ := reader.ReadString('\n')
return strings.TrimSpace(line)
}
func promptChoice(reader *bufio.Reader, max int) int {
for {
s := readLine(reader)
n, err := strconv.Atoi(s)
if err == nil && n >= 0 && n <= max {
return n
}
fmt.Printf(" 请输入 0-%d: ", max)
}
}
func promptReleaseSelection(reader *bufio.Reader, max int) []int {
for {
s := readLine(reader)
if strings.ToLower(s) == "all" {
result := make([]int, max)
for i := range result {
result[i] = i + 1
}
return result
}
parts := strings.Split(s, ",")
var result []int
valid := true
for _, p := range parts {
p = strings.TrimSpace(p)
n, err := strconv.Atoi(p)
if err != nil || n < 1 || n > max {
valid = false
break
}
result = append(result, n)
}
if valid && len(result) > 0 {
return result
}
fmt.Printf(" 请输入 1-%d (逗号分隔) 或 'all': ", max)
}
}
func promptCategory(reader *bufio.Reader) string {
fmt.Println("\n分类:")
for i, cat := range cfg.Categories {
fmt.Printf(" [%d] %s\n", i+1, cat)
}
fmt.Printf("\n> 选择分类 [1]: ")
s := readLine(reader)
if s == "" {
return cfg.Categories[0]
}
n, err := strconv.Atoi(s)
if err != nil || n < 1 || n > len(cfg.Categories) {
return cfg.Categories[0]
}
return cfg.Categories[n-1]
}
func promptArtistFolder(reader *bufio.Reader, defaultName string) string {
fmt.Printf("> 艺术家文件夹名 [%s]: ", defaultName)
s := readLine(reader)
if s == "" {
return defaultName
}
return s
}
func printHelp() {
fmt.Println(`YTM - YouTube Music Downloader
从 YouTube Music 搜索并下载音乐到 Navidrome 音乐库
使用方法:
1. 输入关键词搜索艺术家或专辑
2. 选择搜索模式(艺术家/专辑)
3. 从列表中选择要下载的项目
4. 选择音乐分类和文件夹名
5. 等待下载完成
命令:
help 显示此帮助信息
q 退出程序
输入格式:
数字 选择单项 (如: 1)
逗号分隔 选择多项 (如: 1,3,5)
all 选择全部
回车 接受默认值 [方括号内]`)
fmt.Printf("\n下载目录: %s{分类}/{艺术家}/{专辑}/\n", cfg.MusicRoot)
fmt.Printf("分类: %s\n", strings.Join(cfg.Categories, " | "))
fmt.Println("单曲保存到: {艺术家}/Singles/")
fmt.Println()
}
func fetchTrackCounts(releases []Release) {
fmt.Println("获取曲目信息...")
for i := range releases {
tracks, err := ListPlaylistTracks(releases[i].OLAKID)
if err == nil {
releases[i].TrackCount = len(tracks)
releases[i].Tracks = tracks
} else {
releases[i].TrackCount = 0
}
}
}
func displayReleases(label string, releases []Release, showArtist bool) {
fmt.Printf("\n%s:\n", label)
for i, r := range releases {
trackLabel := "track"
if r.TrackCount != 1 {
trackLabel = "tracks"
}
if showArtist && r.Artist != "" {
fmt.Printf(" [%d] %s - %s (%d %s)\n", i+1, r.Artist, r.Title, r.TrackCount, trackLabel)
} else {
fmt.Printf(" [%d] %s (%d %s)\n", i+1, r.Title, r.TrackCount, trackLabel)
}
}
}
func downloadReleases(reader *bufio.Reader, releases []Release, defaultArtist string) {
fmt.Printf("\n> 选择要下载的 (逗号分隔, 或 'all'): ")
selections := promptReleaseSelection(reader, len(releases))
category := promptCategory(reader)
artistFolder := promptArtistFolder(reader, defaultArtist)
artistFolder = sanitizePath(artistFolder)
basePath := cfg.MusicRoot + category + "/" + artistFolder
fmt.Printf("\n开始下载到 %s/ ...\n", basePath)
totalTracks := 0
for idx, sel := range selections {
r := releases[sel-1]
fmt.Printf(" [%d/%d] %s (%d tracks) ", idx+1, len(selections), r.Title, r.TrackCount)
var dlErr error
if r.TrackCount == 1 && len(r.Tracks) == 1 {
singlesDir := basePath + "/Singles"
os.MkdirAll(singlesDir, 0755)
dlErr = DownloadSingle(r.Tracks[0].VideoID, singlesDir)
} else {
albumDir := basePath + "/" + sanitizePath(r.Title)
os.MkdirAll(albumDir, 0755)
dlErr = DownloadPlaylist(r.OLAKID, albumDir)
}
if dlErr != nil {
fmt.Printf("x\n 错误: %v\n", dlErr)
} else {
fmt.Println("OK")
totalTracks += r.TrackCount
}
}
fmt.Printf("\n下载完成! 共 %d 首曲目\n\n", totalTracks)
}
func main() {
// Set Windows console to UTF-8
exec.Command("cmd", "/C", "chcp", "65001").Run()
cfg = loadConfig()
initHTTPClient()
printHelp()
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("> 输入搜索关键词 (q 退出): ")
query := readLine(reader)
if query == "" {
continue
}
if query == "q" || query == "Q" {
fmt.Println("再见!")
return
}
if strings.ToLower(query) == "help" {
printHelp()
continue
}
// Choose search mode
fmt.Println("\n搜索模式:")
fmt.Println(" [1] 按艺术家搜索")
fmt.Println(" [2] 按专辑搜索")
fmt.Printf("\n> 选择模式 [1]: ")
modeStr := readLine(reader)
mode := 1
if modeStr == "2" {
mode = 2
}
if mode == 2 {
// Album search
fmt.Printf("\n搜索专辑 \"%s\" ...\n", query)
releases, err := SearchAlbums(query)
if err != nil {
fmt.Printf("搜索失败: %v\n\n", err)
continue
}
if len(releases) == 0 {
fmt.Println("未找到专辑\n")
continue
}
fetchTrackCounts(releases)
displayReleases("搜索结果", releases, true)
// Use artist from first release as default
defaultArtist := ""
if len(releases) > 0 {
defaultArtist = releases[0].Artist
}
downloadReleases(reader, releases, defaultArtist)
} else {
// Artist search
fmt.Printf("\n搜索艺术家 \"%s\" ...\n", query)
artists, err := SearchArtists(query)
if err != nil {
fmt.Printf("搜索失败: %v\n\n", err)
continue
}
if len(artists) == 0 {
fmt.Println("未找到艺术家\n")
continue
}
fmt.Println("\n搜索结果:")
for i, a := range artists {
fmt.Printf(" [%d] %s (%s) - %d releases\n", i+1, a.Name, a.ID, a.ReleaseCount)
}
fmt.Println(" [0] 重新搜索")
fmt.Printf("\n> 选择艺术家 [1]: ")
choice := promptChoice(reader, len(artists))
if choice == 0 {
continue
}
artist := artists[choice-1]
// Browse channel to get MPRE IDs
fmt.Printf("\n获取 %s 的发行列表...\n", artist.Name)
_, mpreIDs, err := BrowseChannel(artist.ID)
if err != nil {
fmt.Printf("获取发行列表失败: %v\n\n", err)
continue
}
releases := ResolveAllReleases(mpreIDs)
if len(releases) == 0 {
fmt.Println("未找到可用发行\n")
continue
}
fetchTrackCounts(releases)
displayReleases(artist.Name+" 的发行列表", releases, false)
downloadReleases(reader, releases, artist.Name)
}
}
}