~cytrogen/ytm

ref: f884dc989c8c945af42bb3bcbc3a442529d2e027 ytm/main.go -rw-r--r-- 6.7 KiB
f884dc98 — HallowDem Initial commit: YTM - YouTube Music Downloader a month ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
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)
		}
	}
}