├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── LICENSE
├── README.md
├── actions.go
├── app.go
├── bilibili
├── Audio.go
├── Video.go
├── action.go
├── bilibili.go
├── collect.go
├── compilation.go
├── login.go
├── profile.go
└── wbi.go
├── build
├── README.md
├── appicon.png
├── darwin
│ ├── Info.dev.plist
│ └── Info.plist
└── windows
│ ├── icon.ico
│ ├── info.json
│ ├── installer
│ ├── project.nsi
│ └── wails_tools.nsh
│ └── wails.exe.manifest
├── config
├── config.go
└── migrate.go
├── constants
└── constants.go
├── download.go
├── format-convert.go
├── frontend
├── index.html
├── package.json
├── package.json.md5
├── src
│ ├── App.vue
│ ├── assets
│ │ └── fonts
│ │ │ ├── OFL.txt
│ │ │ └── nunito-v16-latin-regular.woff2
│ ├── components
│ │ ├── collect_download.vue
│ │ ├── collect_download
│ │ │ ├── add_videos.vue
│ │ │ ├── creat_videolist.vue
│ │ │ ├── download_process.vue
│ │ │ └── videolist_editor.vue
│ │ ├── main_page.vue
│ │ ├── modules
│ │ │ ├── addition_card.vue
│ │ │ ├── fav_information.vue
│ │ │ ├── frame_page.vue
│ │ │ └── head_bar.vue
│ │ ├── setting_page.vue
│ │ └── user_space.vue
│ └── main.js
├── vite.config.js
└── wailsjs
│ ├── go
│ ├── main
│ │ ├── App.d.ts
│ │ └── App.js
│ └── models.ts
│ └── runtime
│ ├── package.json
│ ├── runtime.d.ts
│ └── runtime.js
├── go.mod
├── go.sum
├── logic.go
├── main.go
├── music_tag.go
├── package-lock.json
├── package.json
├── services
├── logger.go
└── updateChecker.go
├── setHidewindow-win.go
├── setHidewindow.go
├── video_list.go
└── wails.json
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a golang project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
3 |
4 | name: Build
5 |
6 | on:
7 | workflow_dispatch:
8 |
9 | jobs:
10 |
11 | build-macos-version:
12 | runs-on: macos-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 |
16 | - name: Install ExPackages
17 | # run: "npm install -D unplugin-vue-components unplugin-auto-import"
18 | run: "npm i @varlet/ui -S"
19 |
20 | - name: Wails Build Action (macos)
21 | # You may pin to the exact commit or the version.
22 | # uses: cryptodeal/wails-bun-build-action@92cb2b7027a9cf33c3c626e0d002e957cd0cc655
23 | uses: dAppServer/wails-build-action@v2.2
24 | with:
25 | # The name of the binary file
26 | build-name: BADownloader
27 | # Platform to build for
28 | build-platform: darwin
29 |
30 | build-windows-version:
31 | runs-on: windows-latest
32 | steps:
33 | - uses: actions/checkout@v3
34 |
35 | - name: Install ExPackages
36 | # run: "npm install -D unplugin-vue-components unplugin-auto-import"
37 | run: "npm i @varlet/ui -S"
38 |
39 | - name: Wails Build Action (windows)
40 | # You may pin to the exact commit or the version.
41 | # uses: cryptodeal/wails-bun-build-action@92cb2b7027a9cf33c3c626e0d002e957cd0cc655
42 | uses: dAppServer/wails-build-action@v2.2
43 | with:
44 | # The name of the binary file
45 | build-name: BADownloader.exe
46 | # Platform to build for
47 | build-platform: windows
48 |
49 |
50 | # - name: Upload to Release Action
51 | # # You may pin to the exact commit or the version.
52 | # # uses: Shopify/upload-to-release@c77c9b3e5d288adaef98a7007bf92340ec6ce03b
53 | # uses: Shopify/upload-to-release@v2.0.0
54 | # with:
55 | # # Artifact name
56 | # name: BiliAudioDownloader-win-amd64.exe
57 | # # Path to the file to upload
58 | # path: ./build/bin/BiliAudioDownloader.exe
59 | # # secrets.GITHUB_TOKEN
60 | # repo-token: ${{ secrets.ACCESS_TOKEN }}
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build/bin
2 | node_modules
3 | frontend/dist
4 | frontend/package-lock.json
5 | config.json
6 | Download/*
7 | Cache/*
8 | package-lock.json
9 | *.log
10 | .DS_Store
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 HIM~
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | # Bili Audio Downloader UI - 使用 wails CLI 重构的全新版本
6 |
7 | 考虑到以往 Bili Audio Downloader 使用命令行操作的不便,于是有了这个完全重构的带 UI 版本!
8 | 这是我第一次使用类似 wails 以及 Vue 来进行开发,经验不足,请多包涵。如有好的修改建议欢迎向我提出!
9 | (受个人安排影响, PR 和信息处理回复的周期大约为 7 天)
10 |
11 | ## 下载
12 | 软件的预编译版本请移步至 [Releases](https://github.com/HIM049/BADownloaderUI/releases) 页面下载。
13 | 如果你是 scoop 包管理器的用户,也可以在由 [Weidows](https://github.com/Weidows) 整理的软件仓库中下载使用该软件。
14 |
15 | ```
16 | scoop bucket add apps https://github.com/kkzzhizhou/scoop-apps
17 | scoop install BADownloaderUI
18 | ```
19 |
20 | ## 使用说明
21 | - 下载 Bili Audio Downloader 的可执行文件,并放入到一个文件夹中
22 | - 运行程序,程序会在目录下生成其配置文件以及缓存目录等
23 | - 输入你希望下载的收藏夹编号或 URL(网址),在确定收藏夹信息正确后点击“下一步” **目前仅支持获取公开收藏夹下载** *(补充说明 #1)*
24 | - 编辑下载偏好。下载数量为 0 时会下载收藏夹中的全部内容,其他数量则是按照收藏夹从前到后排序下载。元数据是音乐的标签,音乐 APP 和播放器通常会需要这些数据。打开对应的开关后程序会将对应的视频数据写入歌曲的元数据中。
25 | - 点击 “生成视频列表” 按钮, 软件会将接下来要下载的歌曲制作成 json 格式的信息表保存在本地。
26 | - 编辑列表内容。列表内的是接下来会下载的内容以及对应内容的元数据。你可以根据需要进行修改。
27 | - 随后点击 “开始下载” 按钮。软件会自动完成剩余的步骤。最终歌曲会被默认输出到 `./Downloads` 文件夹中。
28 |
29 | ## 补充说明
30 | 1. 在 B 站查看收藏夹时,浏览器 URL 中靠后部分的 `fid=` 后跟随的数字部分就是收藏夹编号。如 URL 是以 `/favlist` 结尾,请点击一下希望下载的收藏夹
31 | 3. **程序目前未对大部分输入框进行输入审核,请注意输入内容符合要求!**
32 |
33 | ## 发布说明
34 | - 目前“发布”页面内提供 Windows 平台的预编译内容。格式为 `BAdownloader-{ 版本号 }-{ 平台 }-{ 架构 }` 不了解的用户请下载后缀为 `amd64` 的软件包。
35 |
36 |
--------------------------------------------------------------------------------
/actions.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bili-audio-downloader/bilibili"
5 | "bili-audio-downloader/config"
6 | "errors"
7 | "strconv"
8 |
9 | "github.com/spf13/viper"
10 | "github.com/tidwall/gjson"
11 | wails "github.com/wailsapp/wails/v2/pkg/runtime"
12 | )
13 |
14 | // 获取版本号
15 | func (a *App) GetAppVersion() string {
16 | return APP_VERSION
17 | }
18 |
19 | // 获取主题字符串
20 | func (a *App) GetTheme() string {
21 | return config.Cfg.Theme
22 | }
23 |
24 | // 获取列表中视频数量
25 | func (a *App) GetListCount(path string) int {
26 | videoList := new(VideoList)
27 | err := videoList.Get(path)
28 | if err != nil {
29 | return 0
30 | }
31 | return videoList.Count
32 | }
33 |
34 | // 查询视频信息
35 | func (a *App) QueryVideo(bvid string) (bilibili.Video, error) {
36 | sessdata := ""
37 | if config.Cfg.Account.UseAccount && config.Cfg.Account.IsLogin {
38 | sessdata = config.Cfg.Account.SESSDATA
39 | }
40 |
41 | video := new(bilibili.Video)
42 | err := video.Query(sessdata, bvid)
43 | if err != nil {
44 | wails.EventsEmit(a.ctx, "error", "错误:"+err.Error())
45 | return bilibili.Video{}, err
46 | }
47 | return *video, err
48 | }
49 |
50 | // 查询并返回收藏夹信息
51 | func (a *App) QueryCollection(favListID string) bilibili.FavList {
52 | sessdata := ""
53 | if config.Cfg.Account.UseAccount && config.Cfg.Account.IsLogin {
54 | sessdata = config.Cfg.Account.SESSDATA
55 | }
56 | listInf, err := bilibili.GetFavListObj(favListID, sessdata, 1, 1)
57 | if err != nil {
58 | wails.LogErrorf(a.ctx, "获取收藏夹内容时出现错误:%s", err)
59 | wails.EventsEmit(a.ctx, "error", "错误:"+err.Error())
60 | return bilibili.FavList{}
61 | }
62 | return *listInf
63 | }
64 |
65 | // 查询并返回合集信息
66 | func (a *App) QueryCompilation(mid, sid int) bilibili.CompliationInformation {
67 | listInf, err := bilibili.GetCompliationObj(mid, sid, 1, 1)
68 | if err != nil {
69 | wails.LogErrorf(a.ctx, "获取合集内容时出现错误:%s", err)
70 | wails.EventsEmit(a.ctx, "error", "错误:"+err.Error())
71 | return bilibili.CompliationInformation{}
72 | }
73 | return *listInf
74 | }
75 |
76 | // 查询音频信息
77 | func (a *App) QueryAudio(auid string) (bilibili.Audio, error) {
78 | audio := new(bilibili.Audio)
79 | err := audio.Query(auid)
80 | if err != nil {
81 | wails.EventsEmit(a.ctx, "error", "错误:"+err.Error())
82 | return bilibili.Audio{}, err
83 | }
84 | return *audio, err
85 | }
86 |
87 | // 查询音频信息
88 | func (a *App) QueryProfileVideo(mid string) (int, error) {
89 | sessdata := ""
90 | if config.Cfg.Account.UseAccount && config.Cfg.Account.IsLogin {
91 | sessdata = config.Cfg.Account.SESSDATA
92 | }
93 |
94 | respJson, err := bilibili.GetProfileVideo(mid, "1", "1", sessdata)
95 | if err != nil {
96 | wails.EventsEmit(a.ctx, "error", "错误:"+err.Error())
97 | return 0, err
98 | }
99 | return int(gjson.Get(respJson, "data.page.count").Int()), err
100 | }
101 |
102 | // 创建视频列表
103 | func (a *App) CreatVideoList() error {
104 | videoList := new(VideoList)
105 | err := videoList.Save()
106 | if err != nil {
107 | wails.EventsEmit(a.ctx, "error", "错误:"+err.Error())
108 | return err
109 | }
110 | return nil
111 | }
112 |
113 | // 添加单个视频
114 | func (a *App) AddVideoToList(listPath, bvid string, downloadCompilation bool) error {
115 | videolist := new(VideoList)
116 | err := videolist.Get(listPath)
117 | if err != nil {
118 | return err
119 | }
120 |
121 | sessdata := ""
122 | if config.Cfg.Account.IsLogin && config.Cfg.Account.UseAccount {
123 | sessdata = config.Cfg.Account.SESSDATA
124 | }
125 |
126 | err = videolist.AddVideo(sessdata, bvid, downloadCompilation)
127 | if err != nil {
128 | return err
129 | }
130 |
131 | videolist.Save(listPath)
132 |
133 | return nil
134 | }
135 |
136 | // 添加收藏夹内容
137 | func (a *App) AddCollectionToList(listPath, fid string, count int, downloadCompilation bool) error {
138 | videoList := new(VideoList)
139 | err := videoList.Get(listPath)
140 | if err != nil {
141 | return err
142 | }
143 |
144 | sessdata := ""
145 | if config.Cfg.Account.IsLogin && config.Cfg.Account.UseAccount {
146 | sessdata = config.Cfg.Account.SESSDATA
147 | }
148 |
149 | err = videoList.AddCollection(sessdata, fid, count, downloadCompilation)
150 | if err != nil {
151 | return err
152 | }
153 |
154 | err = videoList.Save(listPath)
155 | if err != nil {
156 | return err
157 | }
158 |
159 | return nil
160 | }
161 |
162 | // 添加视频合集
163 | func (a *App) AddCompilationToList(listPath string, mid, sid, count int, downloadCompilation bool) error {
164 | videoList := new(VideoList)
165 | err := videoList.Get(listPath)
166 | if err != nil {
167 | return nil
168 | }
169 |
170 | sessdata := ""
171 | if config.Cfg.Account.IsLogin && config.Cfg.Account.UseAccount {
172 | sessdata = config.Cfg.Account.SESSDATA
173 | }
174 |
175 | err = videoList.AddCompilation(sessdata, mid, sid, count, downloadCompilation)
176 | if err != nil {
177 | return err
178 | }
179 |
180 | err = videoList.Save(listPath)
181 | if err != nil {
182 | return err
183 | }
184 |
185 | return nil
186 | }
187 |
188 | // 添加单个音频
189 | func (a *App) AddAudioToList(listPath, auid string) error {
190 | videolist := new(VideoList)
191 | err := videolist.Get(listPath)
192 | if err != nil {
193 | return err
194 | }
195 |
196 | sessdata := ""
197 | if config.Cfg.Account.IsLogin && config.Cfg.Account.UseAccount {
198 | sessdata = config.Cfg.Account.SESSDATA
199 | }
200 |
201 | err = videolist.AddAudio(sessdata, auid)
202 | if err != nil {
203 | return err
204 | }
205 |
206 | videolist.Save(listPath)
207 |
208 | return nil
209 | }
210 |
211 | // 添加个人主页视频
212 | func (a *App) AddProfileVideoToList(listPath string, mid, count int, downloadCompilation bool) error {
213 | videoList := new(VideoList)
214 | err := videoList.Get(listPath)
215 | if err != nil {
216 | return nil
217 | }
218 |
219 | sessdata := ""
220 | if config.Cfg.Account.IsLogin && config.Cfg.Account.UseAccount {
221 | sessdata = config.Cfg.Account.SESSDATA
222 | }
223 |
224 | err = videoList.AddProfileVideo(sessdata, mid, count, downloadCompilation)
225 | if err != nil {
226 | return err
227 | }
228 |
229 | err = videoList.Save(listPath)
230 | if err != nil {
231 | return err
232 | }
233 |
234 | return nil
235 | }
236 |
237 | // 加载视频列表
238 | func (a *App) LoadVideoList(listPath string) (VideoList, error) {
239 | videoList := new(VideoList)
240 | err := videoList.Get(listPath)
241 | if err != nil {
242 | return VideoList{}, err
243 | }
244 | return *videoList, nil
245 | }
246 |
247 | // 保存视频列表
248 | func (a *App) SaveVideoList(newList VideoList, path string) error {
249 | err := newList.Save(path)
250 | if err != nil {
251 | return err
252 | }
253 | return nil
254 | }
255 |
256 | // 删除列表中的废弃项
257 | func (a *App) TidyVideoList(listPath string) error {
258 | videoList := new(VideoList)
259 | err := videoList.Get(listPath)
260 | if err != nil {
261 | return err
262 | }
263 |
264 | videoList.Tidy()
265 |
266 | err = videoList.Save(listPath)
267 | if err != nil {
268 | return err
269 | }
270 | return nil
271 | }
272 |
273 | // 获取用户创建的收藏夹
274 | func (a *App) GetUsersCollect() bilibili.Collects {
275 | // 获取收藏夹列表
276 | collects := new(bilibili.Collects)
277 | mid, _ := strconv.Atoi(config.Cfg.Account.DedeUserID)
278 | collects.UserMid = mid
279 | err := collects.GetUsersCollect(config.Cfg.Account.SESSDATA)
280 | if err != nil {
281 | wails.LogErrorf(a.ctx, "获取收藏夹列表失败:%s", err)
282 | return bilibili.Collects{}
283 | }
284 |
285 | return *collects
286 | }
287 |
288 | // 获取收藏的收藏夹
289 | func (a *App) GetFavCollect(pn int) bilibili.Collects {
290 | // 获取收藏夹列表
291 | collects := new(bilibili.Collects)
292 | mid, _ := strconv.Atoi(config.Cfg.Account.DedeUserID)
293 | collects.UserMid = mid
294 | err := collects.GetFavCollect(config.Cfg.Account.SESSDATA, 20, pn)
295 | if err != nil {
296 | wails.LogErrorf(a.ctx, "获取收藏夹列表失败:%s", err)
297 | return bilibili.Collects{}
298 | }
299 |
300 | return *collects
301 | }
302 |
303 | // 查询并返回歌曲信息
304 | func (a *App) QuerySongInformation(auid string) (bilibili.Audio, error) {
305 | audioInf := new(bilibili.Audio)
306 | err := audioInf.Query(auid)
307 | if err != nil {
308 | return bilibili.Audio{}, err
309 | }
310 | audioInf.GetStream("")
311 | return *audioInf, nil
312 | }
313 |
314 | // 调用打开文件窗口
315 | func (a *App) OpenFileDialog() (string, error) {
316 | var FileFilter []wails.FileFilter
317 |
318 | fileFilter := wails.FileFilter{
319 | DisplayName: "视频下载列表 (*.json)",
320 | Pattern: "*.json",
321 | }
322 | FileFilter = append(FileFilter, fileFilter)
323 |
324 | option := wails.OpenDialogOptions{
325 | DefaultDirectory: "./",
326 | DefaultFilename: "",
327 | Title: "打开本地列表文件",
328 | Filters: FileFilter,
329 | }
330 | // 弹出对话框
331 | path, err := wails.OpenFileDialog(a.ctx, option)
332 | if err != nil {
333 | wails.LogErrorf(a.ctx, err.Error())
334 | return "", err
335 | }
336 |
337 | return path, nil
338 | }
339 |
340 | func (a *App) SetDownloadPathDialog() {
341 |
342 | option := wails.OpenDialogOptions{
343 | DefaultDirectory: "./",
344 | DefaultFilename: "",
345 | Title: "选择下载路径",
346 | }
347 |
348 | path, err := wails.OpenDirectoryDialog(a.ctx, option)
349 | if err != nil {
350 | wails.EventsEmit(a.ctx, "error", "错误:"+err.Error())
351 | }
352 |
353 | config.Cfg.FileConfig.DownloadPath = path
354 | err = config.Cfg.UpdateAndSave()
355 | if err != nil {
356 | wails.EventsEmit(a.ctx, "error", "错误:"+err.Error())
357 | }
358 |
359 | }
360 |
361 | // 调用保存窗口
362 | func (a *App) SaveVideoListTo(videolist VideoList) error {
363 | var FileFilter []wails.FileFilter
364 |
365 | fileFilter := wails.FileFilter{
366 | DisplayName: "视频下载列表 (*.json)",
367 | Pattern: "*.json",
368 | }
369 | FileFilter = append(FileFilter, fileFilter)
370 |
371 | option := wails.SaveDialogOptions{
372 | DefaultDirectory: "./",
373 | DefaultFilename: "BAD_VideoList",
374 | Title: "另存视频列表",
375 | Filters: FileFilter,
376 | }
377 |
378 | // 弹出对话框
379 | path, err := wails.SaveFileDialog(a.ctx, option)
380 | if err != nil {
381 | return err
382 | }
383 |
384 | // 用户取消操作
385 | if path == "" {
386 | wails.EventsEmit(a.ctx, "error", "未选择保存路径")
387 | return nil
388 | }
389 |
390 | // 保存列表
391 | err = videolist.Save(path)
392 | if err != nil {
393 | return err
394 | }
395 | return nil
396 | }
397 |
398 | // 获取已登录用户的信息
399 | func (a *App) GetUserInf() (bilibili.AccountInformation, error) {
400 | if !config.Cfg.Account.IsLogin {
401 | return bilibili.AccountInformation{}, errors.New("用户未登录")
402 | }
403 | sessdata := config.Cfg.Account.SESSDATA
404 |
405 | accountInf := new(bilibili.AccountInformation)
406 | accountInf.GetUserInf(sessdata)
407 |
408 | return *accountInf, nil
409 | }
410 |
411 | // 重置设置文件
412 | func (a *App) ResetConfig() {
413 | cfg := config.DefaultConfig()
414 | err := cfg.UpdateAndSave()
415 | if err != nil {
416 | wails.LogErrorf(a.ctx, "写入设置文件失败:%s", err)
417 | wails.EventsEmit(a.ctx, "error", "写入设置时出错:"+err.Error())
418 |
419 | }
420 | }
421 |
422 | // 读取设置
423 | func (a *App) LoadConfig() config.Config {
424 | return config.Cfg
425 | }
426 |
427 | // 写入设置
428 | func (a *App) SaveConfig(cfg config.Config) {
429 | err := cfg.UpdateAndSave()
430 | if err != nil {
431 | wails.LogErrorf(a.ctx, "写入设置文件失败:%s", err)
432 | wails.EventsEmit(a.ctx, "error", "写入设置时出错:"+err.Error())
433 | }
434 | }
435 |
436 | func (a *App) RefreshConfig() error {
437 | err := viper.ReadInConfig()
438 | if err != nil {
439 | return err
440 | }
441 | return nil
442 | }
443 |
444 | // 打开下载文件夹
445 | func (a *App) OpenDownloadFolader() error {
446 |
447 | err := OpenFolder(config.Cfg.GetDownloadPath())
448 | if err != nil {
449 | return err
450 | }
451 |
452 | return nil
453 | }
454 |
--------------------------------------------------------------------------------
/app.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bili-audio-downloader/config"
5 | "bili-audio-downloader/services"
6 | "context"
7 | "os"
8 |
9 | wails "github.com/wailsapp/wails/v2/pkg/runtime"
10 | )
11 |
12 | // App struct
13 | type App struct {
14 | ctx context.Context
15 | }
16 |
17 | // startup is called when the app starts. The context is saved
18 | // so we can call the wails methods
19 | func (a *App) startup(ctx context.Context) {
20 | a.ctx = ctx
21 |
22 | // 程序初始化
23 | config.InitConfig()
24 |
25 | downloadPath := config.Cfg.GetDownloadPath()
26 | cachePath := config.Cfg.GetCachePath()
27 | err2 := os.MkdirAll(downloadPath, 0755)
28 | err3 := os.MkdirAll(cachePath, 0755)
29 | err4 := os.MkdirAll(cachePath+"/music", 0755)
30 | err5 := os.MkdirAll(cachePath+"/cover", 0755)
31 | err6 := os.MkdirAll(cachePath+"/single/cover", 0755)
32 | err7 := os.MkdirAll(cachePath+"/single/music", 0755)
33 | if err2 != nil ||
34 | err3 != nil ||
35 | err4 != nil ||
36 | err5 != nil ||
37 | err6 != nil ||
38 | err7 != nil {
39 | wails.LogFatal(a.ctx, "Initialize Folder Faild")
40 | } else {
41 | wails.LogInfo(a.ctx, "Initialize Folder Successful")
42 | }
43 |
44 | // 检查版本更新
45 | version, err := services.CheckUpdate(APP_VERSION)
46 | if err != nil {
47 | wails.LogErrorf(a.ctx, "Check for update Faild: %s", err)
48 | } else if version == "0" {
49 | wails.LogInfo(a.ctx, "No software update")
50 | } else {
51 | wails.LogInfof(a.ctx, "Found new version: %s", version)
52 |
53 | result, err := wails.MessageDialog(a.ctx, wails.MessageDialogOptions{
54 | Type: wails.QuestionDialog,
55 | Title: "找到新版本:" + version,
56 | Message: "软件有新版本发布了,是否前往下载?",
57 | DefaultButton: "Yes",
58 | })
59 |
60 | if err != nil {
61 | wails.LogError(a.ctx, "弹出更新提示失败")
62 | }
63 |
64 | wails.LogDebugf(a.ctx, "选择结果:%s", result)
65 |
66 | if result == "Yes" {
67 | wails.BrowserOpenURL(a.ctx, "https://github.com/HIM049/BADownloaderUI/releases/tag/"+version)
68 | }
69 |
70 | }
71 | }
72 |
73 | // 程序关闭时
74 | func (a *App) shutdown(ctx context.Context) {
75 | // 清理缓存
76 | if config.Cfg.DeleteCache {
77 | os.RemoveAll(config.Cfg.GetCachePath())
78 | }
79 | }
80 |
81 | type DownloadOption struct {
82 | SongName bool `json:"song_name"`
83 | SongCover bool `json:"song_cover"`
84 | SongAuthor bool `json:"song_author"`
85 | }
86 |
--------------------------------------------------------------------------------
/bilibili/Audio.go:
--------------------------------------------------------------------------------
1 | package bilibili
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "net/http"
7 | "net/url"
8 | "strconv"
9 |
10 | "github.com/tidwall/gjson"
11 | )
12 |
13 | // // 用于获取 AUID 音频流信息
14 | // type audio struct {
15 | // Code int `json:"code"`
16 | // Msg string `json:"msg"`
17 | // Data struct {
18 | // ID int `json:"id"` // 音频 AUID
19 | // Title string `json:"title"` // 音频标题
20 | // Cover string `json:"cover"` // 音频封面
21 | // Intro string `json:"intro"` // 音频简介
22 | // Lyric string `json:"lyric"` // lrc歌词url
23 | // Author string `json:"author"` // 作者名
24 | // Bvid string `json:"bvid"` // 关联稿件 BVID
25 | // Cid int `json:"cid"` // 关联稿件 CID
26 | // }
27 | // }
28 |
29 | // // 用于获取 AUID 音频流
30 | // type AudioStream struct {
31 | // Code int `json:"code"`
32 | // Msg string `json:"msg"`
33 | // Data struct {
34 | // Type int `json:"type"` //-1:试听片段(192K) 0:128K 1:192K 2:320K 3:FLAC
35 | // Title string `json:"title"` // 音频标题
36 | // Cover string `json:"cover"` // 音频封面
37 | // Cdns []string `json:"cdns"` // 音频流列表
38 | // }
39 | // }
40 |
41 | type Audio struct {
42 | Auid string `json:"auid"`
43 | Meta struct {
44 | Title string `json:"title"` // 音频标题
45 | Cover string `json:"cover"` // 音频封面
46 | Lyric string `json:"lyric"` // lrc歌词url
47 | }
48 | Up struct {
49 | Author string `json:"author"` // 作者名
50 | }
51 | Stream struct {
52 | Type int `json:"type"` //-1:试听片段(192K) 0:128K 1:192K 2:320K 3:FLAC
53 | StreamLink string `json:"stream_link"` // 音频流列表
54 | }
55 | }
56 |
57 | func (audio *Audio) Query(auid string) error {
58 |
59 | // 设置 URL 并发送 GET 请求
60 | params := url.Values{}
61 | Url, _ := url.Parse("https://www.bilibili.com/audio/music-service-c/web/song/info")
62 |
63 | // 设置 URL 参数
64 | params.Set("sid", auid)
65 |
66 | Url.RawQuery = params.Encode()
67 | urlPath := Url.String()
68 | resp, err := http.Get(urlPath)
69 | if err != nil {
70 | return err
71 | }
72 | // 将 body 转为字符串并返回
73 | body, _ := io.ReadAll(resp.Body)
74 | bodyJson := string(body)
75 | defer resp.Body.Close()
76 |
77 | audio.Auid = auid
78 | audio.Meta.Title = gjson.Get(bodyJson, "data.title").String()
79 | audio.Meta.Cover = gjson.Get(bodyJson, "data.cover").String()
80 | audio.Meta.Lyric = gjson.Get(bodyJson, "data.lyric").String()
81 | audio.Up.Author = gjson.Get(bodyJson, "data.author").String()
82 |
83 | return nil
84 | }
85 |
86 | func (audio *Audio) GetStream(sessdata string) error {
87 | // 创建请求
88 | req, err := http.NewRequest("GET", "https://api.bilibili.com/audio/music-service-c/url", nil)
89 | if err != nil {
90 | return err
91 | }
92 |
93 | // 添加 Cookie 到请求头
94 | if sessdata != "" {
95 | req.Header.Add("Cookie", "SESSDATA="+sessdata)
96 | }
97 |
98 | // 设置 URL 参数
99 | q := req.URL.Query()
100 | q.Add("songid", audio.Auid)
101 | q.Add("quality", "2")
102 | q.Add("privilege", "2")
103 | q.Add("mid", "2")
104 | q.Add("platform", "web")
105 | req.URL.RawQuery = q.Encode()
106 |
107 | // 创建 HTTP 客户端并发送请求
108 | client := &http.Client{}
109 | resp, err := client.Do(req)
110 | if err != nil {
111 | return err
112 | }
113 | defer resp.Body.Close()
114 |
115 | // 检查响应状态
116 | if resp.StatusCode != http.StatusOK {
117 | return errors.New("Error: " + strconv.Itoa(resp.StatusCode))
118 | }
119 |
120 | // 将 body 转为字符串并返回
121 | body, _ := io.ReadAll(resp.Body)
122 | bodyJson := string(body)
123 |
124 | // 错误检查
125 | if CheckObj(int(gjson.Get(bodyJson, "code").Int())) {
126 | return errors.New(gjson.Get(bodyJson, "message").String())
127 | }
128 |
129 | audio.Stream.Type = int(gjson.Get(bodyJson, "data.type").Int())
130 | audio.Stream.StreamLink = gjson.Get(bodyJson, "data.cdns.0").String()
131 |
132 | return nil
133 | }
134 |
--------------------------------------------------------------------------------
/bilibili/Video.go:
--------------------------------------------------------------------------------
1 | package bilibili
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "net/http"
7 | "strconv"
8 |
9 | "github.com/tidwall/gjson"
10 | )
11 |
12 | type Video struct {
13 | Bvid string `json:"bvid"`
14 | Meta struct {
15 | Title string `json:"title"` // 视频标题
16 | Cover string `json:"cover"` // 封面
17 | Author string `json:"author"` // 作者
18 | LyricsPath string `json:"lyrics_path"` // 歌词
19 | }
20 | Up struct {
21 | Mid int `json:"mid"` // UP MID
22 | Name string `json:"name"` // UP 昵称
23 | Avatar string `json:"avatar"` // UP 头像
24 | }
25 | Videos []Videos
26 | }
27 | type Videos struct {
28 | Cid int `json:"cid"`
29 | Part string `json:"part"` // 分集名称
30 | Meta struct {
31 | SongName string `json:"song_name"` // 歌名
32 | }
33 | }
34 |
35 | // 以 BVID 为单位请求视频详细信息
36 | func (v *Video) Query(sessdata, bvid string) error {
37 | // 创建请求
38 | req, err := http.NewRequest("GET", "https://api.bilibili.com/x/web-interface/view", nil)
39 | if err != nil {
40 | return err
41 | }
42 |
43 | // 添加 Cookie 到请求头
44 | if sessdata != "" {
45 | req.Header.Add("Cookie", "SESSDATA="+sessdata)
46 | }
47 |
48 | // 设置 URL 参数
49 | q := req.URL.Query()
50 | q.Add("bvid", bvid)
51 | req.URL.RawQuery = q.Encode()
52 |
53 | // 创建 HTTP 客户端并发送请求
54 | client := &http.Client{}
55 | resp, err := client.Do(req)
56 | if err != nil {
57 | return err
58 | }
59 | defer resp.Body.Close()
60 |
61 | // 检查响应状态
62 | if resp.StatusCode != http.StatusOK {
63 | return errors.New("Error: " + strconv.Itoa(resp.StatusCode))
64 | }
65 |
66 | // 将 body 转为字符串并返回
67 | body, _ := io.ReadAll(resp.Body)
68 | json := string(body)
69 |
70 | // 将信息写入结构体
71 | v.Bvid = bvid
72 | v.Meta.Title = gjson.Get(json, "data.title").String() // 视频标题
73 | v.Meta.Cover = gjson.Get(json, "data.pic").String() // 视频封面
74 | v.Meta.LyricsPath = gjson.Get(json, "data.subtitle.0.subtitle_url").String() // 字幕获取(临时)
75 | v.Up.Mid = int(gjson.Get(json, "data.owner.mid").Int()) // UP MID
76 | v.Up.Name = gjson.Get(json, "data.owner.name").String() // UP 昵称
77 | v.Up.Avatar = gjson.Get(json, "data.owner.face").String() // UP 头像
78 |
79 | // 根据分 P 数量写入对应信息
80 | for i := 0; i < int(gjson.Get(json, "data.videos").Int()); i++ {
81 |
82 | // 单个分集视频信息
83 | videos := Videos{
84 | Cid: int(gjson.Get(json, "data.pages."+strconv.Itoa(i)+".cid").Int()),
85 | Part: gjson.Get(json, "data.pages."+strconv.Itoa(i)+".part").String(),
86 | }
87 | v.Videos = append(v.Videos, videos)
88 | }
89 |
90 | return nil
91 | }
92 |
93 | // 请求视频详细信息
94 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/info.md
95 | // TODO:重新添加字幕信息
96 | func GetVideoPageInformation(bvid, sessdata string) (string, error) {
97 | // 创建请求
98 | req, err := http.NewRequest("GET", "https://api.bilibili.com/x/web-interface/view", nil)
99 | if err != nil {
100 | return "", err
101 | }
102 |
103 | // 添加 Cookie 到请求头
104 | if sessdata != "" {
105 | req.Header.Add("Cookie", "SESSDATA="+sessdata)
106 | }
107 |
108 | // 设置 URL 参数
109 | q := req.URL.Query()
110 | q.Add("bvid", bvid)
111 | req.URL.RawQuery = q.Encode()
112 |
113 | // 创建 HTTP 客户端并发送请求
114 | client := &http.Client{}
115 | resp, err := client.Do(req)
116 | if err != nil {
117 | return "", err
118 | }
119 | defer resp.Body.Close()
120 |
121 | // 检查响应状态
122 | if resp.StatusCode != http.StatusOK {
123 | return "", errors.New("Error: " + strconv.Itoa(resp.StatusCode))
124 | }
125 |
126 | // 将 body 转为字符串并返回
127 | body, _ := io.ReadAll(resp.Body)
128 | bodyString := string(body)
129 | return bodyString, nil
130 | }
131 |
132 | // 获取视频流
133 | // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/videostream_url.md#%E8%8E%B7%E5%8F%96%E8%A7%86%E9%A2%91%E6%B5%81%E5%9C%B0%E5%9D%80_web%E7%AB%AF
134 | func GetVideoStream(bvid, cid, sessdata string) (string, error) {
135 | // 创建请求
136 | request, err := http.NewRequest("GET", "https://api.bilibili.com/x/player/wbi/playurl", nil)
137 | if err != nil {
138 | return "", err
139 | }
140 |
141 | // 设置 URL 参数
142 | q := request.URL.Query()
143 | q.Add("bvid", bvid)
144 | q.Add("cid", cid)
145 | q.Add("fnval", "16")
146 | request.URL.RawQuery = q.Encode()
147 |
148 | signedUrl, err := WbiSignURLParams(request.URL.String())
149 | if err != nil {
150 | return "", errors.New("Wbi Sign Error: " + err.Error())
151 | }
152 |
153 | signedRequest, err := http.NewRequest("GET", signedUrl, nil)
154 | if err != nil {
155 | return "", errors.New("New Signed Request Error: " + err.Error())
156 | }
157 |
158 | signedRequest.Header.Set("referer", "https://www.bilibili.com")
159 | signedRequest.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0")
160 |
161 | // 添加 Cookie 到请求头
162 | if sessdata != "" {
163 | signedRequest.Header.Add("Cookie", "SESSDATA="+sessdata)
164 | }
165 |
166 | // 创建 HTTP 客户端并发送请求
167 | client := &http.Client{}
168 | resp, err := client.Do(signedRequest)
169 | if err != nil {
170 | return "", err
171 | }
172 | defer resp.Body.Close()
173 |
174 | // 检查响应状态
175 | if resp.StatusCode != http.StatusOK {
176 | return "", errors.New("Error: " + strconv.Itoa(resp.StatusCode))
177 | }
178 |
179 | // 将 body 转为字符串并返回
180 | body, _ := io.ReadAll(resp.Body)
181 | bodyString := string(body)
182 | return bodyString, nil
183 | }
184 |
--------------------------------------------------------------------------------
/bilibili/action.go:
--------------------------------------------------------------------------------
1 | package bilibili
2 |
3 | import (
4 | "encoding/json"
5 | "io"
6 | "net/http"
7 | "os"
8 | )
9 |
10 | // 用于下载音频流的函数
11 | // 传入流 URL 和文件名
12 | func StreamingDownloader(audioURL, filePathAndName string) error {
13 | // 先判断文件是否存在,如果存在则跳过下载,否则创建文件
14 | out, err := os.Create(filePathAndName)
15 | if err != nil {
16 | return err
17 | }
18 | defer out.Close()
19 |
20 | // 音频流下载函数。接收音频url和文件名。
21 | client := &http.Client{}
22 | request, err := http.NewRequest("GET", audioURL, nil)
23 | if err != nil {
24 | return err
25 | }
26 | request.Header.Set("referer", "https://www.bilibili.com")
27 | response, err := client.Do(request)
28 | if err != nil {
29 | return err
30 | }
31 | defer response.Body.Close()
32 |
33 | _, err = io.Copy(out, response.Body)
34 | if err != nil {
35 | return err
36 | }
37 | return nil
38 | }
39 |
40 | // 从 URL 下载图片
41 | func SaveFromURL(url string, filePath string) error {
42 | file, err := os.Create(filePath)
43 | if err != nil {
44 | return err
45 | }
46 | defer file.Close()
47 |
48 | // 发起 HTTP 请求获取图片内容
49 | response, err := http.Get(url)
50 | if err != nil {
51 | return err
52 | }
53 | defer response.Body.Close()
54 |
55 | // 将图片内容写入文件
56 | _, err = io.Copy(file, response.Body)
57 | if err != nil {
58 | return err
59 | }
60 |
61 | return nil
62 | }
63 |
64 | // 工具函数
65 | // json解析函数
66 | func DecodeJson(jsonFile string, object any) error {
67 | err := json.Unmarshal([]byte([]byte(jsonFile)), object)
68 | if err != nil {
69 | return err
70 | }
71 | return nil
72 | }
73 |
--------------------------------------------------------------------------------
/bilibili/bilibili.go:
--------------------------------------------------------------------------------
1 | package bilibili
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/json"
6 | "io"
7 | "os"
8 | )
9 |
10 | // 读取图片的函数
11 | func GetImage(ImgPath string) (string, error) {
12 | // 打开图片
13 | img, err := os.Open(ImgPath)
14 | if err != nil {
15 | return "", err
16 | }
17 | defer img.Close()
18 |
19 | // 读取图片
20 | data, err := io.ReadAll(img)
21 | if err != nil {
22 | return "", err
23 | }
24 |
25 | // 编码为 base64
26 | base64Data := base64.StdEncoding.EncodeToString(data)
27 |
28 | return base64Data, nil
29 | }
30 |
31 | // 工具函数
32 | // json解析函数
33 | func decodeJson(jsonFile string, object any) error {
34 | err := json.Unmarshal([]byte([]byte(jsonFile)), object)
35 | if err != nil {
36 | return err
37 | }
38 | return nil
39 | }
40 |
41 | // 工具函数
42 | // 检查结构体中的状态码
43 | func CheckObj(code int) bool {
44 | if code == 0 {
45 | return false
46 | } else {
47 | return true
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/bilibili/collect.go:
--------------------------------------------------------------------------------
1 | package bilibili
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "net/http"
7 | "strconv"
8 |
9 | "github.com/tidwall/gjson"
10 | )
11 |
12 | // 用于获取收藏夹基本信息的函数
13 | // 传入收藏夹 ID ,ps 单页大小, pn 页码
14 | // 获得如下结构体
15 | type FavList struct {
16 | Code int `json:"code"` // 状态码
17 | Message string `json:"message"` // 错误消息
18 | Data struct {
19 | Info struct { // 收藏夹信息
20 | Title string `json:"title"` // 收藏夹标题
21 | Cover string `json:"cover"` // 收藏夹封面
22 | Media_count int `json:"media_count"` // 收藏夹数量
23 | Upper struct {
24 | Name string `json:"name"` // 创建者昵称
25 | Face string `json:"face"` // 创建者头像 url
26 | }
27 | }
28 | Medias []struct { // 收藏夹中的视频
29 | Id int `json:"id"` // 稿件 avid
30 | Type int `json:"type"` // 内容类型 (视频稿件2 音频12 合集21)
31 | Title string `json:"title"` // 标题
32 | Cover string `json:"cover"` // 封面 url
33 | Page int `json:"page"` // 视频分P数
34 | Bvid string `json:"bvid"` // BV 号
35 | }
36 | }
37 | }
38 |
39 | func getFavList(id, ps, pn, sessdata string) (string, error) {
40 |
41 | // 创建请求
42 | req, err := http.NewRequest("GET", "https://api.bilibili.com/x/v3/fav/resource/list", nil)
43 | if err != nil {
44 | return "", err
45 | }
46 |
47 | // 添加 Cookie 到请求头
48 | if sessdata != "" {
49 | req.Header.Add("Cookie", "SESSDATA="+sessdata)
50 | }
51 |
52 | // 设置 URL 参数
53 | q := req.URL.Query()
54 | q.Add("media_id", id) // 每页项数
55 | q.Add("ps", ps) // 页码
56 | q.Add("pn", pn) // 页码+
57 | q.Add("platform", "web") // 平台
58 | req.URL.RawQuery = q.Encode()
59 |
60 | // 创建 HTTP 客户端并发送请求
61 | client := &http.Client{}
62 | resp, err := client.Do(req)
63 | if err != nil {
64 | return "", err
65 | }
66 | defer resp.Body.Close()
67 |
68 | // 检查响应状态
69 | if resp.StatusCode != http.StatusOK {
70 | return "", errors.New("Error: " + strconv.Itoa(resp.StatusCode))
71 | }
72 |
73 | // 将 body 转为字符串并返回
74 | body, _ := io.ReadAll(resp.Body)
75 | bodyString := string(body)
76 | return bodyString, nil
77 | }
78 |
79 | func GetFavListObj(id, sessdata string, ps, pn int) (*FavList, error) {
80 | var obj FavList
81 | body, err := getFavList(id, strconv.Itoa(ps), strconv.Itoa(pn), sessdata)
82 | if err != nil {
83 | return nil, err
84 | }
85 | err = decodeJson(body, &obj)
86 | if err != nil {
87 | return nil, err
88 | }
89 | // 错误检查
90 | if CheckObj(obj.Code) {
91 | return nil, errors.New(obj.Message)
92 | }
93 | return &obj, nil
94 | }
95 |
96 | // 获取用户创建的收藏夹
97 | type Collects struct {
98 | UserMid int `json:"user_mid"`
99 | Count int `json:"count"`
100 | List []meta
101 | }
102 | type meta struct {
103 | Id int `json:"id"` // 收藏夹 ID
104 | Mid int `json:"mid"` // 创建者 MID
105 | Attr int `json:"attr"` // 属性
106 | Title string `json:"title"`
107 | Cover string `json:"cover"`
108 | MediaCount int `json:"media_count"`
109 | }
110 |
111 | // 获取用户收藏的收藏夹
112 | func (collects *Collects) GetFavCollect(sessdata string, ps, pn int) error {
113 | json, err := getUserfavoritesCollect(sessdata, strconv.Itoa(collects.UserMid), strconv.Itoa(ps), strconv.Itoa(pn))
114 | if err != nil {
115 | return err
116 | }
117 |
118 | // 错误检查
119 | if CheckObj(int(gjson.Get(json, "code").Int())) {
120 | return errors.New(gjson.Get(json, "message").String())
121 | }
122 |
123 | collects.Count = int(gjson.Get(json, "data.count").Int())
124 | pageCount := collects.Count
125 |
126 | if collects.Count/20 >= pn {
127 | pageCount = 20
128 | } else {
129 | pageCount = collects.Count % 20
130 | }
131 |
132 | for i := 0; i < pageCount; i++ {
133 | meta := new(meta)
134 | meta.Id = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".id").Int())
135 | meta.Mid = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".mid").Int())
136 | meta.Attr = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".attr").Int())
137 | meta.Title = gjson.Get(json, "data.list."+strconv.Itoa(i)+".title").String()
138 | meta.Cover = gjson.Get(json, "data.list."+strconv.Itoa(i)+".cover").String()
139 | meta.MediaCount = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".media_count").Int())
140 | collects.List = append(collects.List, *meta)
141 | }
142 |
143 | return nil
144 | }
145 |
146 | // 获取用户收藏的收藏夹
147 | func getUserfavoritesCollect(sessdata, mid, pageSize, pageNumber string) (string, error) {
148 | // 创建请求
149 | req, err := http.NewRequest("GET", "https://api.bilibili.com/x/v3/fav/folder/collected/list", nil)
150 | if err != nil {
151 | return "", err
152 | }
153 |
154 | // 添加 Cookie 到请求头
155 | if sessdata != "" {
156 | req.Header.Add("Cookie", "SESSDATA="+sessdata)
157 | }
158 |
159 | // 设置 URL 参数
160 | q := req.URL.Query()
161 | q.Add("ps", pageSize) // 每页项数
162 | q.Add("pn", pageNumber) // 页码
163 | q.Add("up_mid", mid) // 用户 mid
164 | q.Add("platform", "web") // 平台
165 | req.URL.RawQuery = q.Encode()
166 |
167 | // 创建 HTTP 客户端并发送请求
168 | client := &http.Client{}
169 | resp, err := client.Do(req)
170 | if err != nil {
171 | return "", err
172 | }
173 | defer resp.Body.Close()
174 |
175 | // 检查响应状态
176 | if resp.StatusCode != http.StatusOK {
177 | return "", errors.New("Error: " + strconv.Itoa(resp.StatusCode))
178 | }
179 |
180 | // 将 body 转为字符串并返回
181 | body, _ := io.ReadAll(resp.Body)
182 | bodyString := string(body)
183 | return bodyString, nil
184 | }
185 |
186 | // 获取用户创建的收藏夹
187 | func (collects *Collects) GetUsersCollect(sessdata string) error {
188 | json, err := getUsersCollect(sessdata, strconv.Itoa(collects.UserMid))
189 | if err != nil {
190 | return err
191 | }
192 |
193 | // 错误检查
194 | if CheckObj(int(gjson.Get(json, "code").Int())) {
195 | return errors.New(gjson.Get(json, "message").String())
196 | }
197 |
198 | collects.Count = int(gjson.Get(json, "data.count").Int())
199 | for i := 0; i < collects.Count; i++ {
200 | meta := new(meta)
201 | meta.Id = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".id").Int())
202 | meta.Mid = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".mid").Int())
203 | meta.Attr = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".attr").Int())
204 | meta.Title = gjson.Get(json, "data.list."+strconv.Itoa(i)+".title").String()
205 | meta.MediaCount = int(gjson.Get(json, "data.list."+strconv.Itoa(i)+".media_count").Int())
206 | collects.List = append(collects.List, *meta)
207 | }
208 |
209 | return nil
210 | }
211 |
212 | // 获取用户创建的收藏夹
213 | func getUsersCollect(sessdata, mid string) (string, error) {
214 | // 创建请求
215 | req, err := http.NewRequest("GET", "https://api.bilibili.com/x/v3/fav/folder/created/list-all", nil)
216 | if err != nil {
217 | return "", err
218 | }
219 |
220 | // 添加 Cookie 到请求头
221 | if sessdata != "" {
222 | req.Header.Add("Cookie", "SESSDATA="+sessdata)
223 | }
224 |
225 | // 设置 URL 参数
226 | q := req.URL.Query()
227 | q.Add("up_mid", mid) // 用户 mid
228 | q.Add("platform", "web") // 平台
229 | req.URL.RawQuery = q.Encode()
230 |
231 | // 创建 HTTP 客户端并发送请求
232 | client := &http.Client{}
233 | resp, err := client.Do(req)
234 | if err != nil {
235 | return "", err
236 | }
237 | defer resp.Body.Close()
238 |
239 | // 检查响应状态
240 | if resp.StatusCode != http.StatusOK {
241 | return "", errors.New("Error: " + strconv.Itoa(resp.StatusCode))
242 | }
243 |
244 | // 将 body 转为字符串并返回
245 | body, _ := io.ReadAll(resp.Body)
246 | bodyString := string(body)
247 | return bodyString, nil
248 | }
249 |
--------------------------------------------------------------------------------
/bilibili/compilation.go:
--------------------------------------------------------------------------------
1 | package bilibili
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "net/http"
7 | "strconv"
8 | )
9 |
10 | // 用于获取收藏夹基本信息的函数
11 | // 传入收藏夹 ID ,ps 单页大小, pn 页码
12 | type CompliationInformation struct {
13 | Code int `json:"code"`
14 | Message string `json:"message"`
15 | Data struct {
16 | Archives []struct {
17 | Bvid string `json:"bvid"`
18 | Pic string `json:"pic"`
19 | Title string `json:"title"`
20 | }
21 | Meta struct {
22 | Cover string `json:"cover"`
23 | Description string `json:"description"`
24 | Name string `json:"name"`
25 | Total int `json:"total"`
26 | }
27 | }
28 | }
29 |
30 | func getCompliation(mid, sid, ps, pn string) (string, error) {
31 | // 创建请求
32 | req, err := http.NewRequest("GET", "https://api.bilibili.com/x/polymer/web-space/seasons_archives_list", nil)
33 | if err != nil {
34 | return "", err
35 | }
36 |
37 | // // 添加 Cookie 到请求头
38 | // if sessdata != "" {
39 | // req.Header.Add("Cookie", "SESSDATA="+sessdata)
40 | // }
41 | req.Header.Set("referer", "https://www.bilibili.com")
42 | req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0")
43 |
44 | // 设置 URL 参数
45 | q := req.URL.Query()
46 | q.Add("mid", mid)
47 | q.Add("season_id", sid)
48 | q.Add("page_size", ps)
49 | q.Add("page_num", pn)
50 | req.URL.RawQuery = q.Encode()
51 |
52 | // 创建 HTTP 客户端并发送请求
53 | client := &http.Client{}
54 | resp, err := client.Do(req)
55 | if err != nil {
56 | return "", err
57 | }
58 | defer resp.Body.Close()
59 |
60 | // 检查响应状态
61 | if resp.StatusCode != http.StatusOK {
62 | return "", errors.New("Error: " + strconv.Itoa(resp.StatusCode))
63 | }
64 |
65 | // 将 body 转为字符串并返回
66 | body, _ := io.ReadAll(resp.Body)
67 | bodyString := string(body)
68 | return bodyString, nil
69 | }
70 |
71 | func GetCompliationObj(mid, sid, ps, pn int) (*CompliationInformation, error) {
72 | var obj CompliationInformation
73 | body, err := getCompliation(strconv.Itoa(mid), strconv.Itoa(sid), strconv.Itoa(ps), strconv.Itoa(pn))
74 | if err != nil {
75 | return nil, err
76 | }
77 | err = decodeJson(body, &obj)
78 | if err != nil {
79 | return nil, err
80 | }
81 |
82 | // 错误检查
83 | if CheckObj(obj.Code) {
84 | return nil, errors.New(obj.Message)
85 | }
86 | return &obj, nil
87 | }
88 |
--------------------------------------------------------------------------------
/bilibili/login.go:
--------------------------------------------------------------------------------
1 | package bilibili
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "net/http"
7 | "strconv"
8 |
9 | "github.com/tidwall/gjson"
10 | )
11 |
12 | // 登录密钥请求返回内容
13 | type GetLoginKeyReturn struct {
14 | Code int `json:"code"`
15 | Message string `json:"message"`
16 | Data struct {
17 | Url string `json:"url"`
18 | Qrcode_key string `json:"qrcode_key"`
19 | }
20 | }
21 |
22 | // 获取登录密钥
23 | func GetLoginKey() (string, string, error) {
24 | var obj GetLoginKeyReturn
25 | body, err := getLoginKey()
26 | if err != nil {
27 | return "", "", err
28 | }
29 | err = decodeJson(body, &obj)
30 | if err != nil {
31 | return "", "", err
32 | }
33 | // 错误检查
34 | if CheckObj(obj.Code) {
35 | return "", "", errors.New(obj.Message)
36 | }
37 | return obj.Data.Url, obj.Data.Qrcode_key, nil
38 | }
39 |
40 | // 请求登录密钥
41 | func getLoginKey() (string, error) {
42 | resp, err := http.Get("https://passport.bilibili.com/x/passport-login/web/qrcode/generate")
43 | if err != nil {
44 | return "", err
45 | }
46 | // 将 body 转为字符串并返回
47 | body, _ := io.ReadAll(resp.Body)
48 | bodyString := string(body)
49 | defer resp.Body.Close()
50 | return bodyString, nil
51 | }
52 |
53 | // 用于检查扫码状态和获取 cookie 的函数
54 | type checkLoginReturn struct {
55 | Code int `json:"code"`
56 | Message string `json:"message"`
57 | Data struct {
58 | Url string `json:"url"` // 游戏分站跨域登录 url
59 | Refresh_token string `json:"refresh_token"` // 刷新 refresh_token
60 | Timestamp int `json:"timestamp"` // 登录时间
61 | Code int `json:"code"` // 0:扫码登录成功 86038:二维码已失效 86090:二维码已扫码未确认 86101:未扫码
62 | Message string `json:"message"` // 扫码状态信息
63 | }
64 | }
65 |
66 | // 检查扫码状态
67 | func checkLoginStatus(qrcode_key string) (string, *[]*http.Cookie, error) {
68 | // 创建一个 HTTP 客户端
69 | client := &http.Client{}
70 |
71 | // 创建一个 GET 请求
72 | req, err := http.NewRequest("GET", "https://passport.bilibili.com/x/passport-login/web/qrcode/poll", nil)
73 | if err != nil {
74 | return "", nil, err
75 | }
76 |
77 | // 添加参数到请求的查询字符串
78 | q := req.URL.Query()
79 | q.Add("qrcode_key", qrcode_key)
80 | req.URL.RawQuery = q.Encode()
81 |
82 | // 发送请求并获取响应
83 | resp, err := client.Do(req)
84 | if err != nil {
85 | return "", nil, err
86 | }
87 | defer resp.Body.Close()
88 |
89 | // 检查响应状态码
90 | if resp.StatusCode != http.StatusOK {
91 | return "", nil, errors.New("Error:" + strconv.Itoa(resp.StatusCode))
92 | }
93 |
94 | // 读取 Set-Cookie 头部信息
95 | cookies := resp.Cookies()
96 |
97 | // 将 body 转为字符串并返回
98 | body, _ := io.ReadAll(resp.Body)
99 | bodyString := string(body)
100 | defer resp.Body.Close()
101 | return bodyString, &cookies, nil
102 | }
103 |
104 | func CheckLoginStatus(qrcode_key string) (*checkLoginReturn, *[]*http.Cookie, error) {
105 | var obj checkLoginReturn
106 | body, cookies, err := checkLoginStatus(qrcode_key)
107 | if err != nil {
108 | return nil, nil, err
109 | }
110 | err = decodeJson(body, &obj)
111 | if err != nil {
112 | return nil, nil, err
113 | }
114 | // 错误检查
115 | if CheckObj(obj.Code) {
116 | return nil, nil, errors.New(obj.Message)
117 | }
118 |
119 | return &obj, cookies, nil
120 | }
121 |
122 | // TODO: 与登录部分整合结构体
123 | type AccountInformation struct {
124 | Avatar string `json:"avatar"`
125 | Name string `json:"name"`
126 | }
127 |
128 | // 获取用户信息
129 | // https://socialsisteryi.github.io/bilibili-API-collect/docs/login/login_info.html
130 | func (accountInf *AccountInformation) GetUserInf(sessdata string) error {
131 |
132 | // 创建请求
133 | req, err := http.NewRequest("GET", "https://api.bilibili.com/x/web-interface/nav", nil)
134 | if err != nil {
135 | return err
136 | }
137 |
138 | // 添加 Cookie 到请求头
139 | if sessdata != "" {
140 | req.Header.Add("Cookie", "SESSDATA="+sessdata)
141 | }
142 |
143 | // 创建 HTTP 客户端并发送请求
144 | client := &http.Client{}
145 | resp, err := client.Do(req)
146 | if err != nil {
147 | return err
148 | }
149 | defer resp.Body.Close()
150 |
151 | // 检查响应状态
152 | if resp.StatusCode != http.StatusOK {
153 | return errors.New("Error: " + strconv.Itoa(resp.StatusCode))
154 | }
155 |
156 | // 将 body 转为字符串并返回
157 | body, _ := io.ReadAll(resp.Body)
158 | bodyJson := string(body)
159 |
160 | // 错误检查
161 | if CheckObj(int(gjson.Get(bodyJson, "code").Int())) {
162 | return errors.New(gjson.Get(bodyJson, "message").String())
163 | }
164 |
165 | accountInf.Avatar = gjson.Get(bodyJson, "data.face").String()
166 | accountInf.Name = gjson.Get(bodyJson, "data.uname").String()
167 |
168 | return nil
169 | }
170 |
--------------------------------------------------------------------------------
/bilibili/profile.go:
--------------------------------------------------------------------------------
1 | package bilibili
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "net/http"
7 | "strconv"
8 | )
9 |
10 | // 获取用户投稿列表
11 | // https://socialsisteryi.github.io/bilibili-API-collect/docs/user/space.html#%E6%9F%A5%E8%AF%A2%E7%94%A8%E6%88%B7%E6%8A%95%E7%A8%BF%E8%A7%86%E9%A2%91%E6%98%8E%E7%BB%86
12 | func GetProfileVideo(mid, pn, ps, sessdata string) (string, error) {
13 | // 创建请求
14 | request, err := http.NewRequest("GET", "https://api.bilibili.com/x/space/wbi/arc/search", nil)
15 | if err != nil {
16 | return "", err
17 | }
18 |
19 | // 设置 URL 参数
20 | q := request.URL.Query()
21 | q.Add("mid", mid)
22 | q.Add("order", "pubdate")
23 | q.Add("pn", pn)
24 | q.Add("ps", ps)
25 | request.URL.RawQuery = q.Encode()
26 |
27 | signedUrl, err := WbiSignURLParams(request.URL.String())
28 | if err != nil {
29 | return "", errors.New("Wbi Sign Error: " + err.Error())
30 | }
31 |
32 | signedRequest, err := http.NewRequest("GET", signedUrl, nil)
33 | if err != nil {
34 | return "", errors.New("New Signed Request Error: " + err.Error())
35 | }
36 |
37 | signedRequest.Header.Set("referer", "https://www.bilibili.com")
38 | signedRequest.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0")
39 |
40 | // 添加 Cookie 到请求头
41 | if sessdata != "" {
42 | signedRequest.Header.Add("Cookie", "SESSDATA="+sessdata)
43 | }
44 |
45 | // 创建 HTTP 客户端并发送请求
46 | client := &http.Client{}
47 | resp, err := client.Do(signedRequest)
48 | if err != nil {
49 | return "", err
50 | }
51 | defer resp.Body.Close()
52 |
53 | // 检查响应状态
54 | if resp.StatusCode != http.StatusOK {
55 | return "", errors.New("Error: " + strconv.Itoa(resp.StatusCode))
56 | }
57 |
58 | // 将 body 转为字符串并返回
59 | body, _ := io.ReadAll(resp.Body)
60 | bodyString := string(body)
61 | return bodyString, nil
62 | }
63 |
--------------------------------------------------------------------------------
/bilibili/wbi.go:
--------------------------------------------------------------------------------
1 | package bilibili
2 |
3 | import (
4 | "crypto/md5"
5 | "encoding/hex"
6 | "fmt"
7 | "io"
8 | "net/http"
9 | "net/url"
10 | "sort"
11 | "strconv"
12 | "strings"
13 | "sync"
14 | "time"
15 |
16 | "github.com/tidwall/gjson"
17 | )
18 |
19 | var (
20 | mixinKeyEncTab = []int{
21 | 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
22 | 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
23 | 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
24 | 36, 20, 34, 44, 52,
25 | }
26 | cache sync.Map
27 | lastUpdateTime time.Time
28 | )
29 |
30 | // Wbi 签名函数
31 | func WbiSignURLParams(urlStr string) (string, error) {
32 | urlObj, err := url.Parse(urlStr)
33 | if err != nil {
34 | return "", err
35 | }
36 | imgKey, subKey := getWbiKeysCached()
37 | query := urlObj.Query()
38 | params := map[string]string{}
39 | for k, v := range query {
40 | params[k] = v[0]
41 | }
42 | newParams := encWbi(params, imgKey, subKey)
43 | for k, v := range newParams {
44 | query.Set(k, v)
45 | }
46 | urlObj.RawQuery = query.Encode()
47 | newUrlStr := urlObj.String()
48 | return newUrlStr, nil
49 | }
50 |
51 | func encWbi(params map[string]string, imgKey, subKey string) map[string]string {
52 | mixinKey := getMixinKey(imgKey + subKey)
53 | currTime := strconv.FormatInt(time.Now().Unix(), 10)
54 | params["wts"] = currTime
55 |
56 | // Sort keys
57 | keys := make([]string, 0, len(params))
58 | for k := range params {
59 | keys = append(keys, k)
60 | }
61 | sort.Strings(keys)
62 |
63 | // Remove unwanted characters
64 | for k, v := range params {
65 | v = sanitizeString(v)
66 | params[k] = v
67 | }
68 |
69 | // Build URL parameters
70 | query := url.Values{}
71 | for _, k := range keys {
72 | query.Set(k, params[k])
73 | }
74 | queryStr := query.Encode()
75 |
76 | // Calculate w_rid
77 | hash := md5.Sum([]byte(queryStr + mixinKey))
78 | params["w_rid"] = hex.EncodeToString(hash[:])
79 | return params
80 | }
81 |
82 | func getMixinKey(orig string) string {
83 | var str strings.Builder
84 | for _, v := range mixinKeyEncTab {
85 | if v < len(orig) {
86 | str.WriteByte(orig[v])
87 | }
88 | }
89 | return str.String()[:32]
90 | }
91 |
92 | func sanitizeString(s string) string {
93 | unwantedChars := []string{"!", "'", "(", ")", "*"}
94 | for _, char := range unwantedChars {
95 | s = strings.ReplaceAll(s, char, "")
96 | }
97 | return s
98 | }
99 |
100 | func updateCache() {
101 | if time.Since(lastUpdateTime).Minutes() < 10 {
102 | return
103 | }
104 | imgKey, subKey := getWbiKeys()
105 | cache.Store("imgKey", imgKey)
106 | cache.Store("subKey", subKey)
107 | lastUpdateTime = time.Now()
108 | }
109 |
110 | func getWbiKeysCached() (string, string) {
111 | updateCache()
112 | imgKeyI, _ := cache.Load("imgKey")
113 | subKeyI, _ := cache.Load("subKey")
114 | return imgKeyI.(string), subKeyI.(string)
115 | }
116 |
117 | func getWbiKeys() (string, string) {
118 | client := &http.Client{}
119 | req, err := http.NewRequest("GET", "https://api.bilibili.com/x/web-interface/nav", nil)
120 | if err != nil {
121 | fmt.Printf("Error creating request: %s", err)
122 | return "", ""
123 | }
124 | 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")
125 | req.Header.Set("Referer", "https://www.bilibili.com/")
126 | resp, err := client.Do(req)
127 | if err != nil {
128 | fmt.Printf("Error sending request: %s", err)
129 | return "", ""
130 | }
131 | defer resp.Body.Close()
132 | body, err := io.ReadAll(resp.Body)
133 | if err != nil {
134 | fmt.Printf("Error reading response: %s", err)
135 | return "", ""
136 | }
137 | json := string(body)
138 | imgURL := gjson.Get(json, "data.wbi_img.img_url").String()
139 | subURL := gjson.Get(json, "data.wbi_img.sub_url").String()
140 | imgKey := strings.Split(strings.Split(imgURL, "/")[len(strings.Split(imgURL, "/"))-1], ".")[0]
141 | subKey := strings.Split(strings.Split(subURL, "/")[len(strings.Split(subURL, "/"))-1], ".")[0]
142 | return imgKey, subKey
143 | }
144 |
--------------------------------------------------------------------------------
/build/README.md:
--------------------------------------------------------------------------------
1 | # Build Directory
2 |
3 | The build directory is used to house all the build files and assets for your application.
4 |
5 | The structure is:
6 |
7 | * bin - Output directory
8 | * darwin - macOS specific files
9 | * windows - Windows specific files
10 |
11 | ## Mac
12 |
13 | The `darwin` directory holds files specific to Mac builds.
14 | These may be customised and used as part of the build. To return these files to the default state, simply delete them
15 | and
16 | build with `wails build`.
17 |
18 | The directory contains the following files:
19 |
20 | - `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
21 | - `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
22 |
23 | ## Windows
24 |
25 | The `windows` directory contains the manifest and rc files used when building with `wails build`.
26 | These may be customised for your application. To return these files to the default state, simply delete them and
27 | build with `wails build`.
28 |
29 | - `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
30 | use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
31 | will be created using the `appicon.png` file in the build directory.
32 | - `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
33 | - `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
34 | as well as the application itself (right click the exe -> properties -> details)
35 | - `wails.exe.manifest` - The main application manifest file.
--------------------------------------------------------------------------------
/build/appicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HIM049/BADownloaderUI/9b77f43e106dd06f032aaa3ca129303c19732d46/build/appicon.png
--------------------------------------------------------------------------------
/build/darwin/Info.dev.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundlePackageType
5 | APPL
6 | CFBundleName
7 | {{.Info.ProductName}}
8 | CFBundleExecutable
9 | {{.Name}}
10 | CFBundleIdentifier
11 | com.wails.{{.Name}}
12 | CFBundleVersion
13 | {{.Info.ProductVersion}}
14 | CFBundleGetInfoString
15 | {{.Info.Comments}}
16 | CFBundleShortVersionString
17 | {{.Info.ProductVersion}}
18 | CFBundleIconFile
19 | iconfile
20 | LSMinimumSystemVersion
21 | 10.13.0
22 | NSHighResolutionCapable
23 | true
24 | NSHumanReadableCopyright
25 | {{.Info.Copyright}}
26 | NSAppTransportSecurity
27 |
28 | NSAllowsLocalNetworking
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/build/darwin/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | CFBundlePackageType
5 | APPL
6 | CFBundleName
7 | {{.Info.ProductName}}
8 | CFBundleExecutable
9 | {{.Name}}
10 | CFBundleIdentifier
11 | com.wails.{{.Name}}
12 | CFBundleVersion
13 | {{.Info.ProductVersion}}
14 | CFBundleGetInfoString
15 | {{.Info.Comments}}
16 | CFBundleShortVersionString
17 | {{.Info.ProductVersion}}
18 | CFBundleIconFile
19 | iconfile
20 | LSMinimumSystemVersion
21 | 10.13.0
22 | NSHighResolutionCapable
23 | true
24 | NSHumanReadableCopyright
25 | {{.Info.Copyright}}
26 |
27 |
--------------------------------------------------------------------------------
/build/windows/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HIM049/BADownloaderUI/9b77f43e106dd06f032aaa3ca129303c19732d46/build/windows/icon.ico
--------------------------------------------------------------------------------
/build/windows/info.json:
--------------------------------------------------------------------------------
1 | {
2 | "fixed": {
3 | "file_version": "{{.Info.ProductVersion}}"
4 | },
5 | "info": {
6 | "0000": {
7 | "ProductVersion": "{{.Info.ProductVersion}}",
8 | "CompanyName": "{{.Info.CompanyName}}",
9 | "FileDescription": "{{.Info.ProductName}}",
10 | "LegalCopyright": "{{.Info.Copyright}}",
11 | "ProductName": "{{.Info.ProductName}}",
12 | "Comments": "{{.Info.Comments}}"
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/build/windows/installer/project.nsi:
--------------------------------------------------------------------------------
1 | Unicode true
2 |
3 | ####
4 | ## Please note: Template replacements don't work in this file. They are provided with default defines like
5 | ## mentioned underneath.
6 | ## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
7 | ## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
8 | ## from outside of Wails for debugging and development of the installer.
9 | ##
10 | ## For development first make a wails nsis build to populate the "wails_tools.nsh":
11 | ## > wails build --target windows/amd64 --nsis
12 | ## Then you can call makensis on this file with specifying the path to your binary:
13 | ## For a AMD64 only installer:
14 | ## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
15 | ## For a ARM64 only installer:
16 | ## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
17 | ## For a installer with both architectures:
18 | ## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
19 | ####
20 | ## The following information is taken from the ProjectInfo file, but they can be overwritten here.
21 | ####
22 | ## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
23 | ## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
24 | ## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
25 | ## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
26 | ## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
27 | ###
28 | ## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
29 | ## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
30 | ####
31 | ## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
32 | ####
33 | ## Include the wails tools
34 | ####
35 | !include "wails_tools.nsh"
36 |
37 | # The version information for this two must consist of 4 parts
38 | VIProductVersion "${INFO_PRODUCTVERSION}.0"
39 | VIFileVersion "${INFO_PRODUCTVERSION}.0"
40 |
41 | VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
42 | VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
43 | VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
44 | VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
45 | VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
46 | VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
47 |
48 | # Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
49 | ManifestDPIAware true
50 |
51 | !include "MUI.nsh"
52 |
53 | !define MUI_ICON "..\icon.ico"
54 | !define MUI_UNICON "..\icon.ico"
55 | # !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
56 | !define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
57 | !define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
58 |
59 | !insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
60 | # !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
61 | !insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
62 | !insertmacro MUI_PAGE_INSTFILES # Installing page.
63 | !insertmacro MUI_PAGE_FINISH # Finished installation page.
64 |
65 | !insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
66 |
67 | !insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
68 |
69 | ## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
70 | #!uninstfinalize 'signtool --file "%1"'
71 | #!finalize 'signtool --file "%1"'
72 |
73 | Name "${INFO_PRODUCTNAME}"
74 | OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
75 | InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
76 | ShowInstDetails show # This will always show the installation details.
77 |
78 | Function .onInit
79 | !insertmacro wails.checkArchitecture
80 | FunctionEnd
81 |
82 | Section
83 | !insertmacro wails.setShellContext
84 |
85 | !insertmacro wails.webview2runtime
86 |
87 | SetOutPath $INSTDIR
88 |
89 | !insertmacro wails.files
90 |
91 | CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
92 | CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
93 |
94 | !insertmacro wails.writeUninstaller
95 | SectionEnd
96 |
97 | Section "uninstall"
98 | !insertmacro wails.setShellContext
99 |
100 | RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
101 |
102 | RMDir /r $INSTDIR
103 |
104 | Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
105 | Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
106 |
107 | !insertmacro wails.deleteUninstaller
108 | SectionEnd
109 |
--------------------------------------------------------------------------------
/build/windows/installer/wails_tools.nsh:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT - Generated automatically by `wails build`
2 |
3 | !include "x64.nsh"
4 | !include "WinVer.nsh"
5 | !include "FileFunc.nsh"
6 |
7 | !ifndef INFO_PROJECTNAME
8 | !define INFO_PROJECTNAME "{{.Name}}"
9 | !endif
10 | !ifndef INFO_COMPANYNAME
11 | !define INFO_COMPANYNAME "{{.Info.CompanyName}}"
12 | !endif
13 | !ifndef INFO_PRODUCTNAME
14 | !define INFO_PRODUCTNAME "{{.Info.ProductName}}"
15 | !endif
16 | !ifndef INFO_PRODUCTVERSION
17 | !define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
18 | !endif
19 | !ifndef INFO_COPYRIGHT
20 | !define INFO_COPYRIGHT "{{.Info.Copyright}}"
21 | !endif
22 | !ifndef PRODUCT_EXECUTABLE
23 | !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
24 | !endif
25 | !ifndef UNINST_KEY_NAME
26 | !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
27 | !endif
28 | !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
29 |
30 | !ifndef REQUEST_EXECUTION_LEVEL
31 | !define REQUEST_EXECUTION_LEVEL "admin"
32 | !endif
33 |
34 | RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
35 |
36 | !ifdef ARG_WAILS_AMD64_BINARY
37 | !define SUPPORTS_AMD64
38 | !endif
39 |
40 | !ifdef ARG_WAILS_ARM64_BINARY
41 | !define SUPPORTS_ARM64
42 | !endif
43 |
44 | !ifdef SUPPORTS_AMD64
45 | !ifdef SUPPORTS_ARM64
46 | !define ARCH "amd64_arm64"
47 | !else
48 | !define ARCH "amd64"
49 | !endif
50 | !else
51 | !ifdef SUPPORTS_ARM64
52 | !define ARCH "arm64"
53 | !else
54 | !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
55 | !endif
56 | !endif
57 |
58 | !macro wails.checkArchitecture
59 | !ifndef WAILS_WIN10_REQUIRED
60 | !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
61 | !endif
62 |
63 | !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
64 | !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
65 | !endif
66 |
67 | ${If} ${AtLeastWin10}
68 | !ifdef SUPPORTS_AMD64
69 | ${if} ${IsNativeAMD64}
70 | Goto ok
71 | ${EndIf}
72 | !endif
73 |
74 | !ifdef SUPPORTS_ARM64
75 | ${if} ${IsNativeARM64}
76 | Goto ok
77 | ${EndIf}
78 | !endif
79 |
80 | IfSilent silentArch notSilentArch
81 | silentArch:
82 | SetErrorLevel 65
83 | Abort
84 | notSilentArch:
85 | MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
86 | Quit
87 | ${else}
88 | IfSilent silentWin notSilentWin
89 | silentWin:
90 | SetErrorLevel 64
91 | Abort
92 | notSilentWin:
93 | MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
94 | Quit
95 | ${EndIf}
96 |
97 | ok:
98 | !macroend
99 |
100 | !macro wails.files
101 | !ifdef SUPPORTS_AMD64
102 | ${if} ${IsNativeAMD64}
103 | File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
104 | ${EndIf}
105 | !endif
106 |
107 | !ifdef SUPPORTS_ARM64
108 | ${if} ${IsNativeARM64}
109 | File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
110 | ${EndIf}
111 | !endif
112 | !macroend
113 |
114 | !macro wails.writeUninstaller
115 | WriteUninstaller "$INSTDIR\uninstall.exe"
116 |
117 | SetRegView 64
118 | WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
119 | WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
120 | WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
121 | WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
122 | WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
123 | WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
124 |
125 | ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
126 | IntFmt $0 "0x%08X" $0
127 | WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
128 | !macroend
129 |
130 | !macro wails.deleteUninstaller
131 | Delete "$INSTDIR\uninstall.exe"
132 |
133 | SetRegView 64
134 | DeleteRegKey HKLM "${UNINST_KEY}"
135 | !macroend
136 |
137 | !macro wails.setShellContext
138 | ${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
139 | SetShellVarContext all
140 | ${else}
141 | SetShellVarContext current
142 | ${EndIf}
143 | !macroend
144 |
145 | # Install webview2 by launching the bootstrapper
146 | # See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
147 | !macro wails.webview2runtime
148 | !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
149 | !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
150 | !endif
151 |
152 | SetRegView 64
153 | # If the admin key exists and is not empty then webview2 is already installed
154 | ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
155 | ${If} $0 != ""
156 | Goto ok
157 | ${EndIf}
158 |
159 | ${If} ${REQUEST_EXECUTION_LEVEL} == "user"
160 | # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
161 | ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
162 | ${If} $0 != ""
163 | Goto ok
164 | ${EndIf}
165 | ${EndIf}
166 |
167 | SetDetailsPrint both
168 | DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
169 | SetDetailsPrint listonly
170 |
171 | InitPluginsDir
172 | CreateDirectory "$pluginsdir\webview2bootstrapper"
173 | SetOutPath "$pluginsdir\webview2bootstrapper"
174 | File "tmp\MicrosoftEdgeWebview2Setup.exe"
175 | ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
176 |
177 | SetDetailsPrint both
178 | ok:
179 | !macroend
--------------------------------------------------------------------------------
/build/windows/wails.exe.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | true/pm
12 | permonitorv2,permonitor
13 |
14 |
15 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "bili-audio-downloader/constants"
5 | "fmt"
6 | "log"
7 | "path/filepath"
8 |
9 | "github.com/spf13/viper"
10 | )
11 |
12 | type Config struct {
13 | ConfigVersion int `json:"config_version"`
14 | DeleteCache bool `json:"delete_cache"`
15 | Theme string `json:"theme"`
16 | DownloadConfig DownloadConfig `json:"download_config"`
17 | FileConfig FileConfig `json:"file_config"`
18 | Account Account
19 | }
20 |
21 | type DownloadConfig struct {
22 | DownloadThreads int `json:"download_threads"`
23 | RetryCount int `json:"retry_count"`
24 | }
25 |
26 | type FileConfig struct {
27 | ConvertFormat bool `json:"convert_format"`
28 | FileNameTemplate string `json:"file_name_template"`
29 | DownloadPath string `json:"download_path"`
30 | CachePath string `json:"cache_path"`
31 | VideoListPath string `json:"videolist_path"`
32 | }
33 |
34 | type Account struct {
35 | IsLogin bool `json:"is_login"`
36 | UseAccount bool `json:"use_account"`
37 | SESSDATA string `json:"sessdata"`
38 | Bili_jct string `json:"bili_jct"`
39 | DedeUserID string `json:"dede_user_id"`
40 | DedeUserID__ckMd5 string `json:"dede_user_id__ck_md5"`
41 | Sid string `json:"sid"`
42 | }
43 |
44 | var Cfg Config
45 |
46 | func InitConfig() {
47 | viper.SetConfigName("config")
48 | viper.SetConfigType("json")
49 | viper.AddConfigPath("./")
50 |
51 | err := viper.ReadInConfig()
52 | if err != nil {
53 | if _, ok := err.(viper.ConfigFileNotFoundError); ok {
54 | fmt.Println("Config file not found: ", err)
55 | newConfig := DefaultConfig()
56 | newConfig.UpdateAndSave()
57 | fmt.Println("Created a new config")
58 | } else {
59 | log.Fatalf("Failed to read config file: %v", err)
60 | }
61 | }
62 |
63 | // 初始化嵌套结构体
64 | downloadCfg := DownloadConfig{
65 | DownloadThreads: viper.GetInt("download_config.download_threads"),
66 | RetryCount: viper.GetInt("download_config.retry_count"),
67 | }
68 |
69 | fileCfg := FileConfig{
70 | ConvertFormat: viper.GetBool("file_config.convert_format"),
71 | FileNameTemplate: viper.GetString("file_config.file_name_template"),
72 | DownloadPath: viper.GetString("file_config.download_path"),
73 | CachePath: viper.GetString("file_config.cache_path"),
74 | VideoListPath: viper.GetString("file_config.videolist_path"),
75 | }
76 |
77 | account := Account{
78 | IsLogin: viper.GetBool("Account.is_login"),
79 | UseAccount: viper.GetBool("Account.use_account"),
80 | SESSDATA: viper.GetString("Account.sessdata"),
81 | Bili_jct: viper.GetString("Account.bili_jct"),
82 | DedeUserID: viper.GetString("Account.dede_user_id"),
83 | DedeUserID__ckMd5: viper.GetString("Account.dede_user_id__ck_md5"),
84 | Sid: viper.GetString("Account.sid"),
85 | }
86 |
87 | // 初始化主配置结构体
88 | Cfg = Config{
89 | ConfigVersion: viper.GetInt("config_version"),
90 | DeleteCache: viper.GetBool("delete_cache"),
91 | Theme: viper.GetString("theme"),
92 | // Debug: viper.GetBool("debug"),
93 | DownloadConfig: downloadCfg,
94 | FileConfig: fileCfg,
95 | Account: account,
96 | }
97 |
98 | // 检查配置文件版本
99 | if Cfg.ConfigVersion != constants.CONFIG_VERSION {
100 | if Cfg.ConfigVersion < constants.CONFIG_VERSION {
101 | err := migrateConfig("./config.json")
102 | if err != nil {
103 | log.Fatalf("Failed to migrate config: %v\n", err)
104 | }
105 | } else {
106 | fmt.Println("Config version is higher than current version")
107 | }
108 | }
109 | }
110 |
111 | func (cfg *Config) UpdateAndSave() error {
112 | viper.Set("config_version", cfg.ConfigVersion)
113 | viper.Set("delete_cache", cfg.DeleteCache)
114 | viper.Set("theme", cfg.Theme)
115 |
116 | viper.Set("download_config.download_threads", cfg.DownloadConfig.DownloadThreads)
117 | viper.Set("download_config.retry_count", cfg.DownloadConfig.RetryCount)
118 |
119 | viper.Set("file_config.convert_format", cfg.FileConfig.ConvertFormat)
120 | viper.Set("file_config.file_name_template", cfg.FileConfig.FileNameTemplate)
121 | viper.Set("file_config.download_path", cfg.FileConfig.DownloadPath)
122 | viper.Set("file_config.cache_path", cfg.FileConfig.CachePath)
123 | viper.Set("file_config.videolist_path", cfg.FileConfig.VideoListPath)
124 |
125 | viper.Set("Account.is_login", cfg.Account.IsLogin)
126 | viper.Set("Account.use_account", cfg.Account.UseAccount)
127 | viper.Set("Account.sessdata", cfg.Account.SESSDATA)
128 | viper.Set("Account.bili_jct", cfg.Account.Bili_jct)
129 | viper.Set("Account.dede_user_id", cfg.Account.DedeUserID)
130 | viper.Set("Account.dede_user_id__ck_md5", cfg.Account.DedeUserID__ckMd5)
131 | viper.Set("Account.sid", cfg.Account.Sid)
132 |
133 | if err := viper.WriteConfig(); err != nil {
134 | return err
135 | }
136 | return nil
137 | }
138 |
139 | // 初始化设置
140 | func DefaultConfig() *Config {
141 | cfg := Config{
142 | ConfigVersion: constants.CONFIG_VERSION,
143 | DeleteCache: true,
144 | Theme: "lightPink",
145 | DownloadConfig: DownloadConfig{
146 | DownloadThreads: 5,
147 | RetryCount: 10,
148 | },
149 | FileConfig: FileConfig{
150 | ConvertFormat: false, // TODO
151 | FileNameTemplate: "{{.ID}}_{{.Title}}({{.Subtitle}})_{{.Quality}}.{{.Format}}",
152 | DownloadPath: "./Download",
153 | CachePath: "./Cache",
154 | VideoListPath: "./Cache/video_list.json",
155 | },
156 | Account: Account{
157 | IsLogin: false,
158 | UseAccount: false,
159 | SESSDATA: "",
160 | Bili_jct: "",
161 | DedeUserID: "",
162 | DedeUserID__ckMd5: "",
163 | Sid: "",
164 | },
165 | }
166 | return &cfg
167 | }
168 |
169 | func (cfg *Config) GetDownloadPath() string {
170 | path, err := filepath.Abs(cfg.FileConfig.DownloadPath)
171 | if err != nil {
172 | log.Fatalln("Failed to get abs path: ", err)
173 | }
174 | return path
175 | }
176 |
177 | func (cfg *Config) GetCachePath() string {
178 | path, err := filepath.Abs(cfg.FileConfig.CachePath)
179 | if err != nil {
180 | log.Fatalln("Failed to get abs path: ", err)
181 | }
182 | return path
183 | }
184 |
185 | func (cfg *Config) GetVideolistPath() string {
186 | path, err := filepath.Abs(cfg.FileConfig.VideoListPath)
187 | if err != nil {
188 | log.Fatalln("Failed to get abs path: ", err)
189 | }
190 | return path
191 | }
192 |
--------------------------------------------------------------------------------
/config/migrate.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "bili-audio-downloader/constants"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "os"
9 | )
10 |
11 | func migrateConfig(filePath string) error {
12 | // 打开 JSON 文件
13 | file, err := os.Open(filePath)
14 | if err != nil {
15 | return fmt.Errorf("failed to open config file: %w", err)
16 | }
17 | defer file.Close()
18 |
19 | data, err := io.ReadAll(file)
20 | if err != nil {
21 | return fmt.Errorf("failed to read config file: %w", err)
22 | }
23 |
24 | // 将 JSON 文件反序列化为 map
25 | var configMap map[string]interface{}
26 | err = json.Unmarshal(data, &configMap)
27 | if err != nil {
28 | return fmt.Errorf("failed to parse config file: %w", err)
29 | }
30 | config := DefaultConfig()
31 |
32 | // 匹配结构体字段
33 | config.ConfigVersion = constants.CONFIG_VERSION
34 |
35 | if v, ok := configMap["delete_cache"].(bool); ok {
36 | config.DeleteCache = v
37 | }
38 |
39 | if v, ok := configMap["theme"].(string); ok {
40 | config.Theme = v
41 | }
42 |
43 | if downloadConfig, ok := configMap["download_config"].(map[string]interface{}); ok {
44 | if v, ok := downloadConfig["download_threads"].(float64); ok {
45 | config.DownloadConfig.DownloadThreads = int(v)
46 | }
47 | if v, ok := downloadConfig["retry_count"].(float64); ok {
48 | config.DownloadConfig.RetryCount = int(v)
49 | }
50 | }
51 |
52 | if fileConfig, ok := configMap["file_config"].(map[string]interface{}); ok {
53 | if v, ok := fileConfig["convert_format"].(bool); ok {
54 | config.FileConfig.ConvertFormat = v
55 | }
56 | if v, ok := fileConfig["file_name_template"].(string); ok {
57 | config.FileConfig.FileNameTemplate = v
58 | }
59 | if v, ok := fileConfig["download_path"].(string); ok {
60 | config.FileConfig.DownloadPath = v
61 | }
62 | if v, ok := fileConfig["cache_path"].(string); ok {
63 | config.FileConfig.CachePath = v
64 | }
65 | if v, ok := fileConfig["videolist_path"].(string); ok {
66 | config.FileConfig.VideoListPath = v
67 | }
68 | }
69 |
70 | if account, ok := configMap["account"].(map[string]interface{}); ok {
71 | if v, ok := account["is_login"].(bool); ok {
72 | config.Account.IsLogin = v
73 | }
74 | if v, ok := account["use_account"].(bool); ok {
75 | config.Account.UseAccount = v
76 | }
77 | if v, ok := account["sessdata"].(string); ok {
78 | config.Account.SESSDATA = v
79 | }
80 | if v, ok := account["bili_jct"].(string); ok {
81 | config.Account.Bili_jct = v
82 | }
83 | if v, ok := account["dede_user_id"].(string); ok {
84 | config.Account.DedeUserID = v
85 | }
86 | if v, ok := account["dede_user_id__ck_md5"].(string); ok {
87 | config.Account.DedeUserID__ckMd5 = v
88 | }
89 | if v, ok := account["sid"].(string); ok {
90 | config.Account.Sid = v
91 | }
92 | }
93 |
94 | config.UpdateAndSave()
95 | return nil
96 | }
97 |
--------------------------------------------------------------------------------
/constants/constants.go:
--------------------------------------------------------------------------------
1 | package constants
2 |
3 | const CONFIG_VERSION int = 2
4 |
--------------------------------------------------------------------------------
/download.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bili-audio-downloader/bilibili"
5 | "bili-audio-downloader/config"
6 | "path"
7 | "strconv"
8 | "sync"
9 | "time"
10 |
11 | wails "github.com/wailsapp/wails/v2/pkg/runtime"
12 | )
13 |
14 | var AudioType = struct {
15 | m4a string
16 | mp3 string
17 | flac string
18 | }{m4a: ".m4a", mp3: ".mp3", flac: ".flac"}
19 |
20 | func (a *App) ListDownload(listPath string, opt DownloadOption) error {
21 | // 初始化参数
22 | sessdata := ""
23 | if config.Cfg.Account.UseAccount && config.Cfg.Account.IsLogin {
24 | sessdata = config.Cfg.Account.SESSDATA
25 | }
26 |
27 | sem := make(chan struct{}, config.Cfg.DownloadConfig.DownloadThreads+1)
28 | var wg sync.WaitGroup
29 |
30 | videoList := new(VideoList)
31 | err := videoList.Get(listPath)
32 | if err != nil {
33 | wails.LogErrorf(a.ctx, "读取视频列表时发生错误:%s", err)
34 | return err
35 | }
36 |
37 | // 格式判断
38 | audioType := AudioType.m4a
39 | if config.Cfg.FileConfig.ConvertFormat {
40 | audioType = AudioType.mp3
41 | }
42 |
43 | // 遍历下载队列
44 | for i, video := range videoList.List {
45 | // 并发函数
46 | go func(v VideoInformation, num int) {
47 | sem <- struct{}{} // 给通道中填入数据
48 | wg.Add(1) // 任务 +1
49 | // 下载完成后
50 | defer func() {
51 | <-sem // 释放一个并发槽
52 | wg.Done() // 发出任务完成通知
53 |
54 | wails.EventsEmit(a.ctx, "downloadFinish", v.Meta.SongName)
55 | }()
56 |
57 | // 处理文件名结构体
58 | fileName := new(FileName)
59 | fileName.Title = v.Title
60 | fileName.Subtitle = v.PageTitle
61 | fileName.ID = num
62 | fileName.Quality = "hires"
63 |
64 | //判断是否已下载
65 | finalFile := path.Join(config.Cfg.GetDownloadPath(), v.Title+audioType)
66 | if IsFileExists(finalFile) {
67 | wails.LogInfof(a.ctx, "跳过已存在的视频: %s", finalFile)
68 | return
69 | }
70 |
71 | musicPathAndName := config.Cfg.GetCachePath() + "/music/" + strconv.Itoa(v.Cid)
72 |
73 | // 下载视频
74 | for i := 0; i < config.Cfg.DownloadConfig.RetryCount; i++ {
75 |
76 | // 音频下载逻辑
77 | if v.IsAudio {
78 | audio := new(bilibili.Audio)
79 | audio.Auid = v.Bvid
80 | err := audio.GetStream(sessdata)
81 | if err != nil {
82 | wails.LogErrorf(a.ctx, "(队列%d) 获取媒体流时出现错误:%s (重试 %d )", num, err, i+1)
83 | continue
84 | }
85 |
86 | // 下载媒体流
87 | err = bilibili.StreamingDownloader(audio.Stream.StreamLink, musicPathAndName+AudioType.m4a)
88 | if err != nil {
89 | // 下载失败
90 | wails.LogErrorf(a.ctx, "(队列%d) 下载时出现错误:%s (重试 %d )", num, err, i+1)
91 | continue
92 | } else {
93 | wails.LogInfof(a.ctx, "(队列%d) 下载视频成功", num)
94 | }
95 | break
96 | }
97 |
98 | err := v.GetStream(sessdata)
99 | if err != nil {
100 | // 获取流失败
101 | wails.LogErrorf(a.ctx, "(队列%d) 获取媒体流时出现错误:%s (重试 %d )", num, err, i+1)
102 | continue
103 | }
104 | // 下载媒体流
105 | err = bilibili.StreamingDownloader(v.Audio.Stream, musicPathAndName+v.Format)
106 | if err != nil {
107 | // 下载失败
108 | wails.LogErrorf(a.ctx, "(队列%d) 下载时出现错误:%s (重试 %d )", num, err, i+1)
109 | continue
110 | } else {
111 | wails.LogInfof(a.ctx, "(队列%d) 下载视频成功", num)
112 | }
113 |
114 | break
115 | }
116 |
117 | // 判断文件类型并转码
118 | if v.Format == AudioType.m4a && config.Cfg.FileConfig.ConvertFormat {
119 | wails.LogInfof(a.ctx, "(队列%d) 转码为 MP3", num)
120 | v.Format = AudioType.mp3
121 | fileName.Format = AudioType.mp3
122 |
123 | // 转码文件
124 | err = ConventFile(musicPathAndName+AudioType.m4a, musicPathAndName+AudioType.mp3)
125 | if err != nil {
126 | wails.LogErrorf(a.ctx, "转码文件时发生错误:%s", err)
127 | } else {
128 | wails.LogInfof(a.ctx, "(队列%d) 转码文件成功", num)
129 | }
130 | } else {
131 | wails.LogInfof(a.ctx, "(队列%d) 无需转码", num)
132 | fileName.Format = v.Format
133 | }
134 |
135 | // 写入元数据
136 | if v.Format != AudioType.flac {
137 | fileName.Quality = "normal"
138 | err = ChangeTag(&config.Cfg, &opt, &v)
139 | if err != nil {
140 | wails.LogErrorf(a.ctx, "(队列%d) 写入元数据时发生错误:%s", num, err)
141 | } else {
142 | wails.LogInfof(a.ctx, "(队列%d) 写入元数据成功", num)
143 | }
144 | }
145 |
146 | // 输出文件
147 | err = OutputFile(&config.Cfg, &v, *fileName)
148 | if err != nil {
149 | wails.LogErrorf(a.ctx, "输出文件时发生错误:%s", err)
150 | } else {
151 | wails.LogInfof(a.ctx, "(队列%d) 输出文件成功", num)
152 | }
153 |
154 | }(video, i)
155 |
156 | go func(v VideoInformation, num int) {
157 | // 下载封面图片
158 | err = bilibili.SaveFromURL(v.Meta.Cover, config.Cfg.GetCachePath()+"/cover/"+strconv.Itoa(v.Cid)+".jpg")
159 | if err != nil {
160 | wails.LogErrorf(a.ctx, "保存封面时发生错误:%s", err)
161 | } else {
162 | wails.LogInfof(a.ctx, "(队列%d) 下载封面成功", num)
163 | }
164 | }(video, i)
165 | time.Sleep(10 * time.Millisecond)
166 | }
167 | // 等待任务执行完成
168 | wg.Wait()
169 |
170 | // 下载完成后保存列表
171 | err = videoList.Save(listPath)
172 | if err != nil {
173 | return err
174 | }
175 |
176 | return nil
177 | }
178 |
--------------------------------------------------------------------------------
/format-convert.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | ffmpeg "github.com/u2takey/ffmpeg-go"
5 | "os/exec"
6 | "runtime"
7 | )
8 |
9 | func (a *App) Checkffmpeg() bool {
10 | return Checkffmpeg()
11 | }
12 |
13 | // 检查系统中是否安装 ffmpeg
14 | // (临时方案)
15 | func Checkffmpeg() bool {
16 | switch runtime.GOOS {
17 | case "windows":
18 | return checkffmpegOnWindows()
19 | case "darwin":
20 | return checkffmpegOnMacOS()
21 | default:
22 | return false
23 | }
24 | }
25 |
26 | // windows
27 | func checkffmpegOnWindows() bool {
28 | cmd := exec.Command("where", "ffmpeg")
29 | setHideWindow(cmd)
30 | _, err := cmd.Output()
31 | return err == nil
32 | }
33 |
34 | // MacOS
35 | func checkffmpegOnMacOS() bool {
36 | cmd := exec.Command("which", "ffmpeg")
37 | _, err := cmd.Output()
38 | return err == nil
39 | }
40 |
41 | // 调用 ffmpeg 转码
42 | func ConventFile(inputFile, outputFile string) error {
43 | stream := ffmpeg.Input(inputFile).Output(outputFile, ffmpeg.KwArgs{"qscale": "0"})
44 | cmd := stream.Compile()
45 | setHideWindow(cmd)
46 | err := cmd.Run()
47 |
48 | if err != nil {
49 | return err
50 | }
51 | return nil
52 | }
53 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | BiliAudioDownloaderUI
8 |
9 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "vue": "^3.2.37",
13 | "@varlet/ui": "^3.6.1"
14 | },
15 | "devDependencies": {
16 | "@vitejs/plugin-vue": "^3.0.3",
17 | "vite": "^3.0.7"
18 | }
19 | }
--------------------------------------------------------------------------------
/frontend/package.json.md5:
--------------------------------------------------------------------------------
1 | 4a755aa8d82c1f44ce8a545fab0d3126
--------------------------------------------------------------------------------
/frontend/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
137 |
138 |
--------------------------------------------------------------------------------
/frontend/src/assets/fonts/OFL.txt:
--------------------------------------------------------------------------------
1 | Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HIM049/BADownloaderUI/9b77f43e106dd06f032aaa3ca129303c19732d46/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
--------------------------------------------------------------------------------
/frontend/src/components/collect_download.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
28 |
29 |
30 |
153 |
154 |
--------------------------------------------------------------------------------
/frontend/src/components/collect_download/add_videos.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 收藏夹
5 | 视频合集
6 | 视频链接
7 | AUID
8 | 用户空间
9 |
10 |
11 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
![]()
25 |
26 | {{ resp.title }}
27 |
28 | 视频数量:{{ resp.count }}
29 |
30 | 创建人:{{ resp.up_name }}
31 |
32 |
33 |
34 |
35 |
36 |
37 | 添加至列表
38 |
39 |
40 |
41 |
42 |
43 |
86 |
87 |
88 |
89 |
413 |
414 |
--------------------------------------------------------------------------------
/frontend/src/components/collect_download/creat_videolist.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 创建新的下载列表
5 | 打开本地下载列表
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/frontend/src/components/collect_download/download_process.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 开始下载
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 正在下载:{{ progress.downloadingTitle }} ( {{ progress.downFinished }} / {{ parms.listCount }} )
16 |
17 |
18 |
19 |
20 |
25 |
26 | 打开下载文件夹
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/frontend/src/components/collect_download/videolist_editor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 整理并刷新列表
5 | 导出列表文件
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | 歌曲名称:{{ video.Meta.song_name }}
16 | 歌曲作者:{{ video.Meta.author }}
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 |
54 |
55 | 保存
56 |
57 |
58 |
59 |
60 |
61 |
185 |
186 |
--------------------------------------------------------------------------------
/frontend/src/components/main_page.vue:
--------------------------------------------------------------------------------
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 |
51 |
52 |
--------------------------------------------------------------------------------
/frontend/src/components/modules/addition_card.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ title }}
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/src/components/modules/fav_information.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
21 |
22 |
--------------------------------------------------------------------------------
/frontend/src/components/modules/frame_page.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ title }}
5 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
--------------------------------------------------------------------------------
/frontend/src/components/modules/head_bar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
42 |
43 |
--------------------------------------------------------------------------------
/frontend/src/components/setting_page.vue:
--------------------------------------------------------------------------------
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 | 使用 ffmpeg 转码音频
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | 双大括号中的为文件名变量,可以通过自行修改或删除自定义文件名
83 | 列表编号{{.ID}} 视频标题{{.Title}} 单集标题{{.Subtitle}} 音频质量{{.Quality}} 格式后缀名{{.Format}}
84 |
85 |
87 |
88 |
89 |
90 |
91 |
92 | 更改
93 |
94 |
95 |
96 |
97 | 更改
98 |
99 |
100 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | 重置设置
109 | 保存更改
110 |
111 |
112 |
113 |
114 |
115 |
116 |
236 |
237 |
--------------------------------------------------------------------------------
/frontend/src/components/user_space.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 你还没有登录
5 | 登录
6 |
7 |
8 |
9 | 扫码登录
10 |
11 | {{ loginText }}
12 |
13 |
14 |
15 |
16 | {{ user_Information.name }}
17 |
18 |
19 |
20 |
21 |
22 |
23 | {{ collect.title }}
24 | 添加至列表
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {{ collect.title }}
36 |
37 | 添加至列表
40 |
41 |
42 |
43 |
44 |
45 |
46 | 上一页
47 | 下一页
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/frontend/src/main.js:
--------------------------------------------------------------------------------
1 | import App from './App.vue'
2 | import Varlet from '@varlet/ui'
3 | import { createApp } from 'vue'
4 | import '@varlet/ui/es/style'
5 |
6 | createApp(App).use(Varlet).mount('#app')
7 |
--------------------------------------------------------------------------------
/frontend/vite.config.js:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [vue()],
7 |
8 | resolve: {
9 | dedupe: [
10 | 'vue'
11 | ]
12 | }
13 | })
14 |
--------------------------------------------------------------------------------
/frontend/wailsjs/go/main/App.d.ts:
--------------------------------------------------------------------------------
1 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
2 | // This file is automatically generated. DO NOT EDIT
3 | import {bilibili} from '../models';
4 | import {main} from '../models';
5 | import {config} from '../models';
6 |
7 | export function AddAudioToList(arg1:string,arg2:string):Promise;
8 |
9 | export function AddCollectionToList(arg1:string,arg2:string,arg3:number,arg4:boolean):Promise;
10 |
11 | export function AddCompilationToList(arg1:string,arg2:number,arg3:number,arg4:number,arg5:boolean):Promise;
12 |
13 | export function AddProfileVideoToList(arg1:string,arg2:number,arg3:number,arg4:boolean):Promise;
14 |
15 | export function AddVideoToList(arg1:string,arg2:string,arg3:boolean):Promise;
16 |
17 | export function Checkffmpeg():Promise;
18 |
19 | export function CreatVideoList():Promise;
20 |
21 | export function GetAppVersion():Promise;
22 |
23 | export function GetFavCollect(arg1:number):Promise;
24 |
25 | export function GetListCount(arg1:string):Promise;
26 |
27 | export function GetTheme():Promise;
28 |
29 | export function GetUserInf():Promise;
30 |
31 | export function GetUsersCollect():Promise;
32 |
33 | export function ListDownload(arg1:string,arg2:main.DownloadOption):Promise;
34 |
35 | export function LoadConfig():Promise;
36 |
37 | export function LoadVideoList(arg1:string):Promise;
38 |
39 | export function LoginBilibili():Promise;
40 |
41 | export function OpenDownloadFolader():Promise;
42 |
43 | export function OpenFileDialog():Promise;
44 |
45 | export function QueryAudio(arg1:string):Promise;
46 |
47 | export function QueryCollection(arg1:string):Promise;
48 |
49 | export function QueryCompilation(arg1:number,arg2:number):Promise;
50 |
51 | export function QueryProfileVideo(arg1:string):Promise;
52 |
53 | export function QuerySongInformation(arg1:string):Promise;
54 |
55 | export function QueryVideo(arg1:string):Promise;
56 |
57 | export function RefreshConfig():Promise;
58 |
59 | export function ResetConfig():Promise;
60 |
61 | export function SaveConfig(arg1:config.Config):Promise;
62 |
63 | export function SaveVideoList(arg1:main.VideoList,arg2:string):Promise;
64 |
65 | export function SaveVideoListTo(arg1:main.VideoList):Promise;
66 |
67 | export function SetDownloadPathDialog():Promise;
68 |
69 | export function TidyVideoList(arg1:string):Promise;
70 |
--------------------------------------------------------------------------------
/frontend/wailsjs/go/main/App.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
3 | // This file is automatically generated. DO NOT EDIT
4 |
5 | export function AddAudioToList(arg1, arg2) {
6 | return window['go']['main']['App']['AddAudioToList'](arg1, arg2);
7 | }
8 |
9 | export function AddCollectionToList(arg1, arg2, arg3, arg4) {
10 | return window['go']['main']['App']['AddCollectionToList'](arg1, arg2, arg3, arg4);
11 | }
12 |
13 | export function AddCompilationToList(arg1, arg2, arg3, arg4, arg5) {
14 | return window['go']['main']['App']['AddCompilationToList'](arg1, arg2, arg3, arg4, arg5);
15 | }
16 |
17 | export function AddProfileVideoToList(arg1, arg2, arg3, arg4) {
18 | return window['go']['main']['App']['AddProfileVideoToList'](arg1, arg2, arg3, arg4);
19 | }
20 |
21 | export function AddVideoToList(arg1, arg2, arg3) {
22 | return window['go']['main']['App']['AddVideoToList'](arg1, arg2, arg3);
23 | }
24 |
25 | export function Checkffmpeg() {
26 | return window['go']['main']['App']['Checkffmpeg']();
27 | }
28 |
29 | export function CreatVideoList() {
30 | return window['go']['main']['App']['CreatVideoList']();
31 | }
32 |
33 | export function GetAppVersion() {
34 | return window['go']['main']['App']['GetAppVersion']();
35 | }
36 |
37 | export function GetFavCollect(arg1) {
38 | return window['go']['main']['App']['GetFavCollect'](arg1);
39 | }
40 |
41 | export function GetListCount(arg1) {
42 | return window['go']['main']['App']['GetListCount'](arg1);
43 | }
44 |
45 | export function GetTheme() {
46 | return window['go']['main']['App']['GetTheme']();
47 | }
48 |
49 | export function GetUserInf() {
50 | return window['go']['main']['App']['GetUserInf']();
51 | }
52 |
53 | export function GetUsersCollect() {
54 | return window['go']['main']['App']['GetUsersCollect']();
55 | }
56 |
57 | export function ListDownload(arg1, arg2) {
58 | return window['go']['main']['App']['ListDownload'](arg1, arg2);
59 | }
60 |
61 | export function LoadConfig() {
62 | return window['go']['main']['App']['LoadConfig']();
63 | }
64 |
65 | export function LoadVideoList(arg1) {
66 | return window['go']['main']['App']['LoadVideoList'](arg1);
67 | }
68 |
69 | export function LoginBilibili() {
70 | return window['go']['main']['App']['LoginBilibili']();
71 | }
72 |
73 | export function OpenDownloadFolader() {
74 | return window['go']['main']['App']['OpenDownloadFolader']();
75 | }
76 |
77 | export function OpenFileDialog() {
78 | return window['go']['main']['App']['OpenFileDialog']();
79 | }
80 |
81 | export function QueryAudio(arg1) {
82 | return window['go']['main']['App']['QueryAudio'](arg1);
83 | }
84 |
85 | export function QueryCollection(arg1) {
86 | return window['go']['main']['App']['QueryCollection'](arg1);
87 | }
88 |
89 | export function QueryCompilation(arg1, arg2) {
90 | return window['go']['main']['App']['QueryCompilation'](arg1, arg2);
91 | }
92 |
93 | export function QueryProfileVideo(arg1) {
94 | return window['go']['main']['App']['QueryProfileVideo'](arg1);
95 | }
96 |
97 | export function QuerySongInformation(arg1) {
98 | return window['go']['main']['App']['QuerySongInformation'](arg1);
99 | }
100 |
101 | export function QueryVideo(arg1) {
102 | return window['go']['main']['App']['QueryVideo'](arg1);
103 | }
104 |
105 | export function RefreshConfig() {
106 | return window['go']['main']['App']['RefreshConfig']();
107 | }
108 |
109 | export function ResetConfig() {
110 | return window['go']['main']['App']['ResetConfig']();
111 | }
112 |
113 | export function SaveConfig(arg1) {
114 | return window['go']['main']['App']['SaveConfig'](arg1);
115 | }
116 |
117 | export function SaveVideoList(arg1, arg2) {
118 | return window['go']['main']['App']['SaveVideoList'](arg1, arg2);
119 | }
120 |
121 | export function SaveVideoListTo(arg1) {
122 | return window['go']['main']['App']['SaveVideoListTo'](arg1);
123 | }
124 |
125 | export function SetDownloadPathDialog() {
126 | return window['go']['main']['App']['SetDownloadPathDialog']();
127 | }
128 |
129 | export function TidyVideoList(arg1) {
130 | return window['go']['main']['App']['TidyVideoList'](arg1);
131 | }
132 |
--------------------------------------------------------------------------------
/frontend/wailsjs/runtime/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@wailsapp/runtime",
3 | "version": "2.0.0",
4 | "description": "Wails Javascript runtime library",
5 | "main": "runtime.js",
6 | "types": "runtime.d.ts",
7 | "scripts": {
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/wailsapp/wails.git"
12 | },
13 | "keywords": [
14 | "Wails",
15 | "Javascript",
16 | "Go"
17 | ],
18 | "author": "Lea Anthony ",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/wailsapp/wails/issues"
22 | },
23 | "homepage": "https://github.com/wailsapp/wails#readme"
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/wailsjs/runtime/runtime.d.ts:
--------------------------------------------------------------------------------
1 | /*
2 | _ __ _ __
3 | | | / /___ _(_) /____
4 | | | /| / / __ `/ / / ___/
5 | | |/ |/ / /_/ / / (__ )
6 | |__/|__/\__,_/_/_/____/
7 | The electron alternative for Go
8 | (c) Lea Anthony 2019-present
9 | */
10 |
11 | export interface Position {
12 | x: number;
13 | y: number;
14 | }
15 |
16 | export interface Size {
17 | w: number;
18 | h: number;
19 | }
20 |
21 | export interface Screen {
22 | isCurrent: boolean;
23 | isPrimary: boolean;
24 | width : number
25 | height : number
26 | }
27 |
28 | // Environment information such as platform, buildtype, ...
29 | export interface EnvironmentInfo {
30 | buildType: string;
31 | platform: string;
32 | arch: string;
33 | }
34 |
35 | // [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
36 | // emits the given event. Optional data may be passed with the event.
37 | // This will trigger any event listeners.
38 | export function EventsEmit(eventName: string, ...data: any): void;
39 |
40 | // [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
41 | export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
42 |
43 | // [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
44 | // sets up a listener for the given event name, but will only trigger a given number times.
45 | export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
46 |
47 | // [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
48 | // sets up a listener for the given event name, but will only trigger once.
49 | export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
50 |
51 | // [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
52 | // unregisters the listener for the given event name.
53 | export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
54 |
55 | // [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
56 | // unregisters all listeners.
57 | export function EventsOffAll(): void;
58 |
59 | // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
60 | // logs the given message as a raw message
61 | export function LogPrint(message: string): void;
62 |
63 | // [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
64 | // logs the given message at the `trace` log level.
65 | export function LogTrace(message: string): void;
66 |
67 | // [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
68 | // logs the given message at the `debug` log level.
69 | export function LogDebug(message: string): void;
70 |
71 | // [LogError](https://wails.io/docs/reference/runtime/log#logerror)
72 | // logs the given message at the `error` log level.
73 | export function LogError(message: string): void;
74 |
75 | // [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
76 | // logs the given message at the `fatal` log level.
77 | // The application will quit after calling this method.
78 | export function LogFatal(message: string): void;
79 |
80 | // [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
81 | // logs the given message at the `info` log level.
82 | export function LogInfo(message: string): void;
83 |
84 | // [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
85 | // logs the given message at the `warning` log level.
86 | export function LogWarning(message: string): void;
87 |
88 | // [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
89 | // Forces a reload by the main application as well as connected browsers.
90 | export function WindowReload(): void;
91 |
92 | // [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
93 | // Reloads the application frontend.
94 | export function WindowReloadApp(): void;
95 |
96 | // [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
97 | // Sets the window AlwaysOnTop or not on top.
98 | export function WindowSetAlwaysOnTop(b: boolean): void;
99 |
100 | // [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
101 | // *Windows only*
102 | // Sets window theme to system default (dark/light).
103 | export function WindowSetSystemDefaultTheme(): void;
104 |
105 | // [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
106 | // *Windows only*
107 | // Sets window to light theme.
108 | export function WindowSetLightTheme(): void;
109 |
110 | // [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
111 | // *Windows only*
112 | // Sets window to dark theme.
113 | export function WindowSetDarkTheme(): void;
114 |
115 | // [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
116 | // Centers the window on the monitor the window is currently on.
117 | export function WindowCenter(): void;
118 |
119 | // [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
120 | // Sets the text in the window title bar.
121 | export function WindowSetTitle(title: string): void;
122 |
123 | // [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
124 | // Makes the window full screen.
125 | export function WindowFullscreen(): void;
126 |
127 | // [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
128 | // Restores the previous window dimensions and position prior to full screen.
129 | export function WindowUnfullscreen(): void;
130 |
131 | // [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
132 | // Returns the state of the window, i.e. whether the window is in full screen mode or not.
133 | export function WindowIsFullscreen(): Promise;
134 |
135 | // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
136 | // Sets the width and height of the window.
137 | export function WindowSetSize(width: number, height: number): void;
138 |
139 | // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
140 | // Gets the width and height of the window.
141 | export function WindowGetSize(): Promise;
142 |
143 | // [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
144 | // Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
145 | // Setting a size of 0,0 will disable this constraint.
146 | export function WindowSetMaxSize(width: number, height: number): void;
147 |
148 | // [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
149 | // Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
150 | // Setting a size of 0,0 will disable this constraint.
151 | export function WindowSetMinSize(width: number, height: number): void;
152 |
153 | // [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
154 | // Sets the window position relative to the monitor the window is currently on.
155 | export function WindowSetPosition(x: number, y: number): void;
156 |
157 | // [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
158 | // Gets the window position relative to the monitor the window is currently on.
159 | export function WindowGetPosition(): Promise;
160 |
161 | // [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
162 | // Hides the window.
163 | export function WindowHide(): void;
164 |
165 | // [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
166 | // Shows the window, if it is currently hidden.
167 | export function WindowShow(): void;
168 |
169 | // [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
170 | // Maximises the window to fill the screen.
171 | export function WindowMaximise(): void;
172 |
173 | // [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
174 | // Toggles between Maximised and UnMaximised.
175 | export function WindowToggleMaximise(): void;
176 |
177 | // [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
178 | // Restores the window to the dimensions and position prior to maximising.
179 | export function WindowUnmaximise(): void;
180 |
181 | // [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
182 | // Returns the state of the window, i.e. whether the window is maximised or not.
183 | export function WindowIsMaximised(): Promise;
184 |
185 | // [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
186 | // Minimises the window.
187 | export function WindowMinimise(): void;
188 |
189 | // [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
190 | // Restores the window to the dimensions and position prior to minimising.
191 | export function WindowUnminimise(): void;
192 |
193 | // [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
194 | // Returns the state of the window, i.e. whether the window is minimised or not.
195 | export function WindowIsMinimised(): Promise;
196 |
197 | // [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
198 | // Returns the state of the window, i.e. whether the window is normal or not.
199 | export function WindowIsNormal(): Promise;
200 |
201 | // [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
202 | // Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
203 | export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
204 |
205 | // [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
206 | // Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
207 | export function ScreenGetAll(): Promise;
208 |
209 | // [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
210 | // Opens the given URL in the system browser.
211 | export function BrowserOpenURL(url: string): void;
212 |
213 | // [Environment](https://wails.io/docs/reference/runtime/intro#environment)
214 | // Returns information about the environment
215 | export function Environment(): Promise;
216 |
217 | // [Quit](https://wails.io/docs/reference/runtime/intro#quit)
218 | // Quits the application.
219 | export function Quit(): void;
220 |
221 | // [Hide](https://wails.io/docs/reference/runtime/intro#hide)
222 | // Hides the application.
223 | export function Hide(): void;
224 |
225 | // [Show](https://wails.io/docs/reference/runtime/intro#show)
226 | // Shows the application.
227 | export function Show(): void;
228 |
229 | // [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
230 | // Returns the current text stored on clipboard
231 | export function ClipboardGetText(): Promise;
232 |
233 | // [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
234 | // Sets a text on the clipboard
235 | export function ClipboardSetText(text: string): Promise;
236 |
237 | // [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
238 | // OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
239 | export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
240 |
241 | // [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
242 | // OnFileDropOff removes the drag and drop listeners and handlers.
243 | export function OnFileDropOff() :void
244 |
245 | // Check if the file path resolver is available
246 | export function CanResolveFilePaths(): boolean;
247 |
248 | // Resolves file paths for an array of files
249 | export function ResolveFilePaths(files: File[]): void
--------------------------------------------------------------------------------
/frontend/wailsjs/runtime/runtime.js:
--------------------------------------------------------------------------------
1 | /*
2 | _ __ _ __
3 | | | / /___ _(_) /____
4 | | | /| / / __ `/ / / ___/
5 | | |/ |/ / /_/ / / (__ )
6 | |__/|__/\__,_/_/_/____/
7 | The electron alternative for Go
8 | (c) Lea Anthony 2019-present
9 | */
10 |
11 | export function LogPrint(message) {
12 | window.runtime.LogPrint(message);
13 | }
14 |
15 | export function LogTrace(message) {
16 | window.runtime.LogTrace(message);
17 | }
18 |
19 | export function LogDebug(message) {
20 | window.runtime.LogDebug(message);
21 | }
22 |
23 | export function LogInfo(message) {
24 | window.runtime.LogInfo(message);
25 | }
26 |
27 | export function LogWarning(message) {
28 | window.runtime.LogWarning(message);
29 | }
30 |
31 | export function LogError(message) {
32 | window.runtime.LogError(message);
33 | }
34 |
35 | export function LogFatal(message) {
36 | window.runtime.LogFatal(message);
37 | }
38 |
39 | export function EventsOnMultiple(eventName, callback, maxCallbacks) {
40 | return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
41 | }
42 |
43 | export function EventsOn(eventName, callback) {
44 | return EventsOnMultiple(eventName, callback, -1);
45 | }
46 |
47 | export function EventsOff(eventName, ...additionalEventNames) {
48 | return window.runtime.EventsOff(eventName, ...additionalEventNames);
49 | }
50 |
51 | export function EventsOnce(eventName, callback) {
52 | return EventsOnMultiple(eventName, callback, 1);
53 | }
54 |
55 | export function EventsEmit(eventName) {
56 | let args = [eventName].slice.call(arguments);
57 | return window.runtime.EventsEmit.apply(null, args);
58 | }
59 |
60 | export function WindowReload() {
61 | window.runtime.WindowReload();
62 | }
63 |
64 | export function WindowReloadApp() {
65 | window.runtime.WindowReloadApp();
66 | }
67 |
68 | export function WindowSetAlwaysOnTop(b) {
69 | window.runtime.WindowSetAlwaysOnTop(b);
70 | }
71 |
72 | export function WindowSetSystemDefaultTheme() {
73 | window.runtime.WindowSetSystemDefaultTheme();
74 | }
75 |
76 | export function WindowSetLightTheme() {
77 | window.runtime.WindowSetLightTheme();
78 | }
79 |
80 | export function WindowSetDarkTheme() {
81 | window.runtime.WindowSetDarkTheme();
82 | }
83 |
84 | export function WindowCenter() {
85 | window.runtime.WindowCenter();
86 | }
87 |
88 | export function WindowSetTitle(title) {
89 | window.runtime.WindowSetTitle(title);
90 | }
91 |
92 | export function WindowFullscreen() {
93 | window.runtime.WindowFullscreen();
94 | }
95 |
96 | export function WindowUnfullscreen() {
97 | window.runtime.WindowUnfullscreen();
98 | }
99 |
100 | export function WindowIsFullscreen() {
101 | return window.runtime.WindowIsFullscreen();
102 | }
103 |
104 | export function WindowGetSize() {
105 | return window.runtime.WindowGetSize();
106 | }
107 |
108 | export function WindowSetSize(width, height) {
109 | window.runtime.WindowSetSize(width, height);
110 | }
111 |
112 | export function WindowSetMaxSize(width, height) {
113 | window.runtime.WindowSetMaxSize(width, height);
114 | }
115 |
116 | export function WindowSetMinSize(width, height) {
117 | window.runtime.WindowSetMinSize(width, height);
118 | }
119 |
120 | export function WindowSetPosition(x, y) {
121 | window.runtime.WindowSetPosition(x, y);
122 | }
123 |
124 | export function WindowGetPosition() {
125 | return window.runtime.WindowGetPosition();
126 | }
127 |
128 | export function WindowHide() {
129 | window.runtime.WindowHide();
130 | }
131 |
132 | export function WindowShow() {
133 | window.runtime.WindowShow();
134 | }
135 |
136 | export function WindowMaximise() {
137 | window.runtime.WindowMaximise();
138 | }
139 |
140 | export function WindowToggleMaximise() {
141 | window.runtime.WindowToggleMaximise();
142 | }
143 |
144 | export function WindowUnmaximise() {
145 | window.runtime.WindowUnmaximise();
146 | }
147 |
148 | export function WindowIsMaximised() {
149 | return window.runtime.WindowIsMaximised();
150 | }
151 |
152 | export function WindowMinimise() {
153 | window.runtime.WindowMinimise();
154 | }
155 |
156 | export function WindowUnminimise() {
157 | window.runtime.WindowUnminimise();
158 | }
159 |
160 | export function WindowSetBackgroundColour(R, G, B, A) {
161 | window.runtime.WindowSetBackgroundColour(R, G, B, A);
162 | }
163 |
164 | export function ScreenGetAll() {
165 | return window.runtime.ScreenGetAll();
166 | }
167 |
168 | export function WindowIsMinimised() {
169 | return window.runtime.WindowIsMinimised();
170 | }
171 |
172 | export function WindowIsNormal() {
173 | return window.runtime.WindowIsNormal();
174 | }
175 |
176 | export function BrowserOpenURL(url) {
177 | window.runtime.BrowserOpenURL(url);
178 | }
179 |
180 | export function Environment() {
181 | return window.runtime.Environment();
182 | }
183 |
184 | export function Quit() {
185 | window.runtime.Quit();
186 | }
187 |
188 | export function Hide() {
189 | window.runtime.Hide();
190 | }
191 |
192 | export function Show() {
193 | window.runtime.Show();
194 | }
195 |
196 | export function ClipboardGetText() {
197 | return window.runtime.ClipboardGetText();
198 | }
199 |
200 | export function ClipboardSetText(text) {
201 | return window.runtime.ClipboardSetText(text);
202 | }
203 |
204 | /**
205 | * Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
206 | *
207 | * @export
208 | * @callback OnFileDropCallback
209 | * @param {number} x - x coordinate of the drop
210 | * @param {number} y - y coordinate of the drop
211 | * @param {string[]} paths - A list of file paths.
212 | */
213 |
214 | /**
215 | * OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
216 | *
217 | * @export
218 | * @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
219 | * @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
220 | */
221 | export function OnFileDrop(callback, useDropTarget) {
222 | return window.runtime.OnFileDrop(callback, useDropTarget);
223 | }
224 |
225 | /**
226 | * OnFileDropOff removes the drag and drop listeners and handlers.
227 | */
228 | export function OnFileDropOff() {
229 | return window.runtime.OnFileDropOff();
230 | }
231 |
232 | export function CanResolveFilePaths() {
233 | return window.runtime.CanResolveFilePaths();
234 | }
235 |
236 | export function ResolveFilePaths(files) {
237 | return window.runtime.ResolveFilePaths(files);
238 | }
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module bili-audio-downloader
2 |
3 | go 1.23.0
4 |
5 | toolchain go1.23.6
6 |
7 | require (
8 | github.com/gcottom/audiometa v1.3.1
9 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
10 | github.com/spf13/viper v1.20.1
11 | github.com/tidwall/gjson v1.18.0
12 | github.com/u2takey/ffmpeg-go v0.5.0
13 | github.com/wailsapp/wails/v2 v2.10.1
14 | )
15 |
16 | require (
17 | github.com/abema/go-mp4 v1.4.1 // indirect
18 | github.com/aler9/writerseeker v1.1.0 // indirect
19 | github.com/aws/aws-sdk-go v1.55.6 // indirect
20 | github.com/bep/debounce v1.2.1 // indirect
21 | github.com/bogem/id3v2/v2 v2.1.4 // indirect
22 | github.com/fsnotify/fsnotify v1.8.0 // indirect
23 | github.com/go-flac/flacpicture v0.3.0 // indirect
24 | github.com/go-flac/flacvorbis v0.2.0 // indirect
25 | github.com/go-flac/go-flac v1.0.0 // indirect
26 | github.com/go-ole/go-ole v1.3.0 // indirect
27 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
28 | github.com/godbus/dbus/v5 v5.1.0 // indirect
29 | github.com/google/uuid v1.6.0 // indirect
30 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
31 | github.com/jmespath/go-jmespath v0.4.0 // indirect
32 | github.com/labstack/echo/v4 v4.13.3 // indirect
33 | github.com/labstack/gommon v0.4.2 // indirect
34 | github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
35 | github.com/leaanthony/gosod v1.0.4 // indirect
36 | github.com/leaanthony/slicer v1.6.0 // indirect
37 | github.com/leaanthony/u v1.1.1 // indirect
38 | github.com/mattn/go-colorable v0.1.14 // indirect
39 | github.com/mattn/go-isatty v0.0.20 // indirect
40 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect
41 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
42 | github.com/pkg/errors v0.9.1 // indirect
43 | github.com/rivo/uniseg v0.4.7 // indirect
44 | github.com/sagikazarmark/locafero v0.7.0 // indirect
45 | github.com/samber/lo v1.49.1 // indirect
46 | github.com/sourcegraph/conc v0.3.0 // indirect
47 | github.com/spf13/afero v1.12.0 // indirect
48 | github.com/spf13/cast v1.7.1 // indirect
49 | github.com/spf13/pflag v1.0.6 // indirect
50 | github.com/subosito/gotenv v1.6.0 // indirect
51 | github.com/sunfish-shogi/bufseekio v0.1.0 // indirect
52 | github.com/tidwall/match v1.1.1 // indirect
53 | github.com/tidwall/pretty v1.2.1 // indirect
54 | github.com/tkrajina/go-reflector v0.5.8 // indirect
55 | github.com/u2takey/go-utils v0.3.1 // indirect
56 | github.com/valyala/bytebufferpool v1.0.0 // indirect
57 | github.com/valyala/fasttemplate v1.2.2 // indirect
58 | github.com/wailsapp/go-webview2 v1.0.21 // indirect
59 | github.com/wailsapp/mimetype v1.4.1 // indirect
60 | go.uber.org/atomic v1.9.0 // indirect
61 | go.uber.org/multierr v1.9.0 // indirect
62 | golang.org/x/crypto v0.36.0 // indirect
63 | golang.org/x/net v0.37.0 // indirect
64 | golang.org/x/sys v0.31.0 // indirect
65 | golang.org/x/text v0.23.0 // indirect
66 | gopkg.in/yaml.v3 v3.0.1 // indirect
67 | )
68 |
--------------------------------------------------------------------------------
/logic.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bili-audio-downloader/bilibili"
5 | "bili-audio-downloader/config"
6 | "encoding/json"
7 | "errors"
8 | "net/http"
9 | "os"
10 | "os/exec"
11 | "regexp"
12 | "time"
13 |
14 | qrcode "github.com/skip2/go-qrcode"
15 | "github.com/wailsapp/wails/v2/pkg/runtime"
16 | )
17 |
18 | // 登录 bilibili
19 | func (a *App) LoginBilibili() error {
20 | // 获取二维码和请求密钥
21 | url, key, err := bilibili.GetLoginKey()
22 | if err != nil {
23 | return err
24 | }
25 |
26 | // 生成二维码
27 | qrcodePath := config.Cfg.GetCachePath() + "/qr.png"
28 | err = qrcode.WriteFile(url, qrcode.Medium, 256, qrcodePath)
29 | if err != nil {
30 | return err
31 | }
32 |
33 | base64Data, err := bilibili.GetImage(qrcodePath)
34 | if err != nil {
35 | return err
36 | }
37 | runtime.EventsEmit(a.ctx, "qrcodeStr", base64Data)
38 |
39 | // 请求登录
40 | cookies, err := func() (*[]*http.Cookie, error) {
41 | for {
42 | time.Sleep(2 * time.Second)
43 |
44 | returnObj, cookies, err := bilibili.CheckLoginStatus(key)
45 | if err != nil {
46 | return nil, err
47 | }
48 | switch returnObj.Data.Code {
49 | case 0:
50 | // 登录成功
51 | runtime.LogDebug(a.ctx, "登录成功")
52 | runtime.EventsEmit(a.ctx, "loginStatus", "登录成功")
53 | return cookies, nil
54 | case 86038:
55 | // 二维码失效
56 | runtime.LogDebug(a.ctx, "二维码已失效")
57 | runtime.EventsEmit(a.ctx, "loginStatus", "二维码已失效")
58 | return nil, errors.New("二维码已失效")
59 | case 86090:
60 | // 扫描成功,待确认
61 | runtime.LogDebug(a.ctx, "扫描成功,待确认")
62 | runtime.EventsEmit(a.ctx, "loginStatus", "扫描成功,待确认")
63 | case 86101:
64 | // 未扫描
65 | runtime.LogDebug(a.ctx, "未扫描")
66 | runtime.EventsEmit(a.ctx, "loginStatus", "请扫描二维码登录")
67 | }
68 | }
69 | }()
70 | if err != nil {
71 | return err
72 | }
73 |
74 | config.Cfg.Account.SESSDATA = (*cookies)[0].Value
75 | config.Cfg.Account.Bili_jct = (*cookies)[1].Value
76 | config.Cfg.Account.DedeUserID = (*cookies)[2].Value
77 | config.Cfg.Account.DedeUserID__ckMd5 = (*cookies)[3].Value
78 | config.Cfg.Account.Sid = (*cookies)[4].Value
79 | config.Cfg.Account.IsLogin = true
80 | config.Cfg.Account.UseAccount = true
81 |
82 | err = config.Cfg.UpdateAndSave()
83 | if err != nil {
84 | return err
85 | }
86 |
87 | return nil
88 | }
89 |
90 | // 保存 JSON
91 | func SaveJsonFile(filePath string, theData any) error {
92 | data, err := json.MarshalIndent(theData, "", " ")
93 | if err != nil {
94 | return err
95 | }
96 |
97 | err = os.WriteFile(filePath, data, 0644)
98 | if err != nil {
99 | return err
100 | }
101 | return nil
102 | }
103 |
104 | // 读取 JSON
105 | func LoadJsonFile(filePath string, obj interface{}) error {
106 | file, err := os.ReadFile(filePath)
107 | if err != nil {
108 | return err
109 | }
110 | err = json.Unmarshal(file, obj)
111 | if err != nil {
112 | return err
113 | }
114 | return nil
115 | }
116 |
117 | // 检查文件是否存在
118 | func IsFileExists(path string) bool {
119 | _, err := os.Stat(path)
120 | if err == nil {
121 | return true // 文件存在
122 | }
123 | if os.IsNotExist(err) {
124 | return false // 文件不存在
125 | }
126 | return false // 其他错误
127 | }
128 |
129 | // 剔除文件名中的奇怪字符
130 | func CheckFileName(SFileN string) string {
131 | re := regexp.MustCompile(`[/$<>?:*|]`)
132 | newName := re.ReplaceAllString(SFileN, "")
133 | return newName
134 | }
135 |
136 | // 书名号匹配
137 | func ExtractTitle(input string) (string, error) {
138 | // 定义书名号正则表达式
139 | re := regexp.MustCompile(`《(.*?)》`)
140 |
141 | // 查找匹配的字符串
142 | matches := re.FindStringSubmatch(input)
143 | if len(matches) < 2 {
144 | return "", errors.New("无法找到合适的书名号")
145 | }
146 |
147 | // 返回匹配的书名号内容
148 | return matches[1], nil
149 | }
150 |
151 | // 工具函数
152 | // 检查结构体中的状态码
153 | func CheckObj(code int) bool {
154 | if code == 0 {
155 | return false
156 | } else {
157 | return true
158 | }
159 | }
160 |
161 | // 打开文件夹
162 | func OpenFolder(path string) error {
163 | cmd := exec.Command("cmd", "/c", "start", "", path)
164 | setHideWindow(cmd)
165 | return cmd.Start()
166 | }
167 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bili-audio-downloader/services"
5 | "embed"
6 |
7 | "github.com/wailsapp/wails/v2"
8 | "github.com/wailsapp/wails/v2/pkg/logger"
9 | "github.com/wailsapp/wails/v2/pkg/options"
10 | "github.com/wailsapp/wails/v2/pkg/options/assetserver"
11 | "github.com/wailsapp/wails/v2/pkg/options/windows"
12 | )
13 |
14 | //go:embed all:frontend/dist
15 | var assets embed.FS
16 |
17 | // 全局版本号
18 | const APP_VERSION string = "4.9.1"
19 |
20 | func main() {
21 | // Create an instance of the app structure
22 | app := &App{}
23 |
24 | // Init logger
25 | customLogger, err := services.NewCustomLogger()
26 | if err != nil {
27 | println("Error:", err.Error())
28 | return
29 | }
30 |
31 | // Create application with options
32 | err = wails.Run(&options.App{
33 | Title: "BiliAudioDownloader " + APP_VERSION,
34 | Width: 1024,
35 | Height: 720,
36 | AssetServer: &assetserver.Options{
37 | Assets: assets,
38 | },
39 | Frameless: true, // 无边框窗口
40 | DisableResize: true, // 窗口尺寸
41 | Windows: &windows.Options{
42 | IsZoomControlEnabled: false, // 页面缩放比例
43 | },
44 | BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 1},
45 | OnStartup: app.startup,
46 | OnShutdown: app.shutdown,
47 | LogLevelProduction: logger.INFO,
48 | Logger: customLogger,
49 | Bind: []interface{}{
50 | app,
51 | },
52 | })
53 |
54 | if err != nil {
55 | println("Error:", err.Error())
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/music_tag.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bili-audio-downloader/config"
5 | "bytes"
6 | "html/template"
7 | "os"
8 | "path"
9 | "strconv"
10 |
11 | tag "github.com/gcottom/audiometa"
12 | )
13 |
14 | // 修改 TAG
15 | func ChangeTag(cfg *config.Config, opt *DownloadOption, v *VideoInformation) error {
16 |
17 | // 准备参数
18 | file := cfg.FileConfig.CachePath + "/music/" + strconv.Itoa(v.Cid) + v.Format
19 | songCover := cfg.FileConfig.CachePath + "/cover/" + strconv.Itoa(v.Cid) + ".jpg"
20 | songName := v.Meta.SongName
21 | songAuthor := v.Meta.Author
22 |
23 | // 打开歌曲元数据
24 | tags, err := tag.OpenTag(file)
25 | if err != nil {
26 | return err
27 | }
28 |
29 | // 封面
30 | if opt.SongCover {
31 | err := tags.SetAlbumArtFromFilePath(songCover)
32 | if err != nil {
33 | return err
34 | }
35 | }
36 | // 歌曲名
37 | if opt.SongName {
38 | tags.SetTitle(songName)
39 | }
40 | // 艺术家
41 | if opt.SongAuthor {
42 | tags.SetArtist(songAuthor)
43 | }
44 |
45 | // TODO: 将歌曲 tag 数据整理为结构体
46 | // TODO: 修改作词人,作曲人等,以及自动适配
47 |
48 | // 保存更改
49 | err = tags.Save()
50 | if err != nil {
51 | return err
52 | }
53 |
54 | return nil
55 | }
56 |
57 | type FileName struct {
58 | Title string
59 | Subtitle string
60 | Quality string
61 | ID int
62 | Format string
63 | }
64 |
65 | // 输出文件
66 | func OutputFile(cfg *config.Config, v *VideoInformation, fileName FileName) error {
67 | // 处理模板和生成文件名
68 | tmpl, err := template.New("filename").Parse(cfg.FileConfig.FileNameTemplate)
69 | if err != nil {
70 | return err
71 | }
72 |
73 | var output bytes.Buffer
74 | err = tmpl.Execute(&output, fileName)
75 | if err != nil {
76 | return err
77 | }
78 |
79 | // 添加路径
80 | sourcePath := path.Join(cfg.FileConfig.CachePath, "music", strconv.Itoa(v.Cid)+v.Format)
81 | destPath := path.Join(cfg.FileConfig.DownloadPath, output.String())
82 |
83 | // 重命名歌曲文件并移动位置
84 | err = os.Rename(sourcePath, destPath)
85 | if err != nil {
86 | return err
87 | }
88 | return nil
89 | }
90 |
91 | // 修改 TAG
92 | func SingleChangeTag(cfg *config.Config, opt *DownloadOption, auid, songName, songAuthor string) error {
93 |
94 | // 准备参数
95 | file := cfg.FileConfig.CachePath + "/single/music/" + auid + AudioType.m4a
96 | songCover := cfg.FileConfig.CachePath + "/single/cover/" + auid + ".jpg"
97 |
98 | // 打开歌曲元数据
99 | tags, err := tag.OpenTag(file)
100 | if err != nil {
101 | return err
102 | }
103 |
104 | // 封面
105 | if opt.SongCover {
106 | tags.SetAlbumArtFromFilePath(songCover)
107 | }
108 | // 歌曲名
109 | if opt.SongName {
110 | tags.SetTitle(songName)
111 | }
112 | // 艺术家
113 | if opt.SongAuthor {
114 | tags.SetArtist(songAuthor)
115 | }
116 |
117 | // TODO: 将歌曲 tag 数据整理为结构体
118 | // TODO: 修改作词人,作曲人等,以及自动适配
119 |
120 | // 保存更改
121 | err = tags.Save()
122 | if err != nil {
123 | return err
124 | }
125 |
126 | return nil
127 | }
128 |
129 | // 输出文件
130 | func SingleOutputFile(cfg *config.Config, uuid, Title string) error {
131 |
132 | sourcePath := path.Join(cfg.FileConfig.CachePath, "single/music", uuid+AudioType.m4a)
133 | destPath := path.Join(cfg.FileConfig.DownloadPath, Title+AudioType.mp3)
134 |
135 | // 重命名歌曲文件并移动位置
136 | err := os.Rename(sourcePath, destPath)
137 | if err != nil {
138 | return err
139 | }
140 | return nil
141 | }
142 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "BADownloaderUI",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {
6 | "": {
7 | "dependencies": {
8 | "@varlet/ui": "^3.6.1"
9 | }
10 | },
11 | "node_modules/@babel/helper-string-parser": {
12 | "version": "7.25.7",
13 | "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz",
14 | "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==",
15 | "peer": true,
16 | "engines": {
17 | "node": ">=6.9.0"
18 | }
19 | },
20 | "node_modules/@babel/helper-validator-identifier": {
21 | "version": "7.25.7",
22 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz",
23 | "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==",
24 | "peer": true,
25 | "engines": {
26 | "node": ">=6.9.0"
27 | }
28 | },
29 | "node_modules/@babel/parser": {
30 | "version": "7.25.8",
31 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz",
32 | "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==",
33 | "peer": true,
34 | "dependencies": {
35 | "@babel/types": "^7.25.8"
36 | },
37 | "bin": {
38 | "parser": "bin/babel-parser.js"
39 | },
40 | "engines": {
41 | "node": ">=6.0.0"
42 | }
43 | },
44 | "node_modules/@babel/types": {
45 | "version": "7.25.8",
46 | "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz",
47 | "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==",
48 | "peer": true,
49 | "dependencies": {
50 | "@babel/helper-string-parser": "^7.25.7",
51 | "@babel/helper-validator-identifier": "^7.25.7",
52 | "to-fast-properties": "^2.0.0"
53 | },
54 | "engines": {
55 | "node": ">=6.9.0"
56 | }
57 | },
58 | "node_modules/@jridgewell/sourcemap-codec": {
59 | "version": "1.5.0",
60 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
61 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
62 | "peer": true
63 | },
64 | "node_modules/@popperjs/core": {
65 | "version": "2.11.8",
66 | "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
67 | "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
68 | "funding": {
69 | "type": "opencollective",
70 | "url": "https://opencollective.com/popperjs"
71 | }
72 | },
73 | "node_modules/@varlet/icons": {
74 | "version": "3.6.1",
75 | "resolved": "https://registry.npmjs.org/@varlet/icons/-/icons-3.6.1.tgz",
76 | "integrity": "sha512-8ibwr8lhGPoeCZOi7911FTHam1xTJMBW+z6eNlww7jSYkWJ5YatQ0xvEACYDr5HlG9zY7RtDqOS8ThuR0ZMWwg=="
77 | },
78 | "node_modules/@varlet/shared": {
79 | "version": "3.6.1",
80 | "resolved": "https://registry.npmjs.org/@varlet/shared/-/shared-3.6.1.tgz",
81 | "integrity": "sha512-1UYk/rrYcZmxv9DPXY6UfPNMaJZkkC3jr0rrMSZAN+FZAtPsudP7cODxePykMmSNh9Osfwzz33S2BamNKwoNYA=="
82 | },
83 | "node_modules/@varlet/ui": {
84 | "version": "3.6.1",
85 | "resolved": "https://registry.npmjs.org/@varlet/ui/-/ui-3.6.1.tgz",
86 | "integrity": "sha512-hpAVcjwJtXlk5rYMkJRxFfOkTLFYeHw9AgG+vmNCY+KH+VzNF/gAESBV+ZWQqShHZstJBY2jXCLG7TJ1foxSvA==",
87 | "dependencies": {
88 | "@popperjs/core": "^2.11.6",
89 | "@varlet/icons": "3.6.1",
90 | "@varlet/shared": "3.6.1",
91 | "@varlet/use": "3.6.1",
92 | "dayjs": "^1.10.4",
93 | "decimal.js": "^10.2.1"
94 | },
95 | "peerDependencies": {
96 | "vue": "^3.2.0"
97 | }
98 | },
99 | "node_modules/@varlet/use": {
100 | "version": "3.6.1",
101 | "resolved": "https://registry.npmjs.org/@varlet/use/-/use-3.6.1.tgz",
102 | "integrity": "sha512-PFYuS5PxWiTPWqB6rM8XWsQEstUOG1YY3KgDNzgEsMF0kPUeCibfKPW80evTC6EFzKqOSYwodRO9vSNYEFhHTQ==",
103 | "dependencies": {
104 | "@varlet/shared": "3.6.1"
105 | },
106 | "peerDependencies": {
107 | "vue": "^3.2.0"
108 | }
109 | },
110 | "node_modules/@vue/compiler-core": {
111 | "version": "3.5.12",
112 | "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz",
113 | "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==",
114 | "peer": true,
115 | "dependencies": {
116 | "@babel/parser": "^7.25.3",
117 | "@vue/shared": "3.5.12",
118 | "entities": "^4.5.0",
119 | "estree-walker": "^2.0.2",
120 | "source-map-js": "^1.2.0"
121 | }
122 | },
123 | "node_modules/@vue/compiler-dom": {
124 | "version": "3.5.12",
125 | "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz",
126 | "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==",
127 | "peer": true,
128 | "dependencies": {
129 | "@vue/compiler-core": "3.5.12",
130 | "@vue/shared": "3.5.12"
131 | }
132 | },
133 | "node_modules/@vue/compiler-sfc": {
134 | "version": "3.5.12",
135 | "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz",
136 | "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==",
137 | "peer": true,
138 | "dependencies": {
139 | "@babel/parser": "^7.25.3",
140 | "@vue/compiler-core": "3.5.12",
141 | "@vue/compiler-dom": "3.5.12",
142 | "@vue/compiler-ssr": "3.5.12",
143 | "@vue/shared": "3.5.12",
144 | "estree-walker": "^2.0.2",
145 | "magic-string": "^0.30.11",
146 | "postcss": "^8.4.47",
147 | "source-map-js": "^1.2.0"
148 | }
149 | },
150 | "node_modules/@vue/compiler-ssr": {
151 | "version": "3.5.12",
152 | "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz",
153 | "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==",
154 | "peer": true,
155 | "dependencies": {
156 | "@vue/compiler-dom": "3.5.12",
157 | "@vue/shared": "3.5.12"
158 | }
159 | },
160 | "node_modules/@vue/reactivity": {
161 | "version": "3.5.12",
162 | "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.12.tgz",
163 | "integrity": "sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==",
164 | "peer": true,
165 | "dependencies": {
166 | "@vue/shared": "3.5.12"
167 | }
168 | },
169 | "node_modules/@vue/runtime-core": {
170 | "version": "3.5.12",
171 | "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.12.tgz",
172 | "integrity": "sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==",
173 | "peer": true,
174 | "dependencies": {
175 | "@vue/reactivity": "3.5.12",
176 | "@vue/shared": "3.5.12"
177 | }
178 | },
179 | "node_modules/@vue/runtime-dom": {
180 | "version": "3.5.12",
181 | "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz",
182 | "integrity": "sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==",
183 | "peer": true,
184 | "dependencies": {
185 | "@vue/reactivity": "3.5.12",
186 | "@vue/runtime-core": "3.5.12",
187 | "@vue/shared": "3.5.12",
188 | "csstype": "^3.1.3"
189 | }
190 | },
191 | "node_modules/@vue/server-renderer": {
192 | "version": "3.5.12",
193 | "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.12.tgz",
194 | "integrity": "sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==",
195 | "peer": true,
196 | "dependencies": {
197 | "@vue/compiler-ssr": "3.5.12",
198 | "@vue/shared": "3.5.12"
199 | },
200 | "peerDependencies": {
201 | "vue": "3.5.12"
202 | }
203 | },
204 | "node_modules/@vue/shared": {
205 | "version": "3.5.12",
206 | "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz",
207 | "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==",
208 | "peer": true
209 | },
210 | "node_modules/csstype": {
211 | "version": "3.1.3",
212 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
213 | "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
214 | "peer": true
215 | },
216 | "node_modules/dayjs": {
217 | "version": "1.11.11",
218 | "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz",
219 | "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg=="
220 | },
221 | "node_modules/decimal.js": {
222 | "version": "10.4.3",
223 | "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
224 | "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA=="
225 | },
226 | "node_modules/entities": {
227 | "version": "4.5.0",
228 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
229 | "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
230 | "peer": true,
231 | "engines": {
232 | "node": ">=0.12"
233 | },
234 | "funding": {
235 | "url": "https://github.com/fb55/entities?sponsor=1"
236 | }
237 | },
238 | "node_modules/estree-walker": {
239 | "version": "2.0.2",
240 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
241 | "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
242 | "peer": true
243 | },
244 | "node_modules/magic-string": {
245 | "version": "0.30.12",
246 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz",
247 | "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==",
248 | "peer": true,
249 | "dependencies": {
250 | "@jridgewell/sourcemap-codec": "^1.5.0"
251 | }
252 | },
253 | "node_modules/nanoid": {
254 | "version": "3.3.7",
255 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
256 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
257 | "funding": [
258 | {
259 | "type": "github",
260 | "url": "https://github.com/sponsors/ai"
261 | }
262 | ],
263 | "peer": true,
264 | "bin": {
265 | "nanoid": "bin/nanoid.cjs"
266 | },
267 | "engines": {
268 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
269 | }
270 | },
271 | "node_modules/picocolors": {
272 | "version": "1.1.1",
273 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
274 | "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
275 | "peer": true
276 | },
277 | "node_modules/postcss": {
278 | "version": "8.4.47",
279 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
280 | "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
281 | "funding": [
282 | {
283 | "type": "opencollective",
284 | "url": "https://opencollective.com/postcss/"
285 | },
286 | {
287 | "type": "tidelift",
288 | "url": "https://tidelift.com/funding/github/npm/postcss"
289 | },
290 | {
291 | "type": "github",
292 | "url": "https://github.com/sponsors/ai"
293 | }
294 | ],
295 | "peer": true,
296 | "dependencies": {
297 | "nanoid": "^3.3.7",
298 | "picocolors": "^1.1.0",
299 | "source-map-js": "^1.2.1"
300 | },
301 | "engines": {
302 | "node": "^10 || ^12 || >=14"
303 | }
304 | },
305 | "node_modules/source-map-js": {
306 | "version": "1.2.1",
307 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
308 | "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
309 | "peer": true,
310 | "engines": {
311 | "node": ">=0.10.0"
312 | }
313 | },
314 | "node_modules/to-fast-properties": {
315 | "version": "2.0.0",
316 | "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
317 | "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
318 | "peer": true,
319 | "engines": {
320 | "node": ">=4"
321 | }
322 | },
323 | "node_modules/vue": {
324 | "version": "3.5.12",
325 | "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.12.tgz",
326 | "integrity": "sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==",
327 | "peer": true,
328 | "dependencies": {
329 | "@vue/compiler-dom": "3.5.12",
330 | "@vue/compiler-sfc": "3.5.12",
331 | "@vue/runtime-dom": "3.5.12",
332 | "@vue/server-renderer": "3.5.12",
333 | "@vue/shared": "3.5.12"
334 | },
335 | "peerDependencies": {
336 | "typescript": "*"
337 | },
338 | "peerDependenciesMeta": {
339 | "typescript": {
340 | "optional": true
341 | }
342 | }
343 | }
344 | }
345 | }
346 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@varlet/ui": "^3.6.1"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/services/logger.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | )
7 |
8 | type CustomLogger struct {
9 | logFile *os.File
10 | }
11 |
12 | // 创建并返回日志记录器
13 | func NewCustomLogger() (*CustomLogger, error) {
14 |
15 | // 创建日志文件
16 | logFile, err := os.OpenFile("./app.log", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
17 | if err != nil {
18 | return nil, fmt.Errorf("创建日志文件失败: %v", err)
19 | }
20 |
21 | return &CustomLogger{logFile: logFile}, nil
22 | }
23 |
24 | func (cl *CustomLogger) Print(message string) {
25 | cl.logToFile(message)
26 | }
27 |
28 | func (cl *CustomLogger) Trace(message string) {
29 | cl.logToFile("TRACE: " + message)
30 | }
31 |
32 | func (cl *CustomLogger) Debug(message string) {
33 | cl.logToFile("DEBUG: " + message)
34 | }
35 |
36 | func (cl *CustomLogger) Info(message string) {
37 | cl.logToFile("INFO: " + message)
38 | }
39 |
40 | func (cl *CustomLogger) Warning(message string) {
41 | cl.logToFile("WARNING: " + message)
42 | }
43 |
44 | func (cl *CustomLogger) Error(message string) {
45 | cl.logToFile("ERROR: " + message)
46 | }
47 |
48 | func (cl *CustomLogger) Fatal(message string) {
49 | cl.logToFile("FATAL: " + message)
50 | os.Exit(1)
51 | }
52 |
53 | // logToFile 记录日志到文件
54 | func (cl *CustomLogger) logToFile(message string) {
55 | fmt.Fprintln(cl.logFile, message)
56 | }
57 |
--------------------------------------------------------------------------------
/services/updateChecker.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "net/http"
8 |
9 | "github.com/tidwall/gjson"
10 | )
11 |
12 | // 通过 GitHub 检查程序更新
13 | // string 为 "0" 代表没有更新,有更新时该位为最新版本号
14 | func CheckUpdate(currentVersion string) (string, error) {
15 | url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", "HIM049/BADownloaderUI")
16 | resp, err := http.Get(url)
17 | if err != nil {
18 | return "0", errors.New(fmt.Sprintln("无法获取最新版本:", err))
19 | }
20 | defer resp.Body.Close()
21 |
22 | if resp.StatusCode != http.StatusOK {
23 | fmt.Println("请求失败,状态码:", resp.StatusCode)
24 | return "0", errors.New(fmt.Sprintln("请求失败,状态码:", resp.StatusCode))
25 | }
26 |
27 | bodyString, err := io.ReadAll(resp.Body)
28 | if err != nil {
29 | return "0", errors.New(fmt.Sprintln("读取请求体失败:", err))
30 | }
31 |
32 | latestVersion := gjson.Get(string(bodyString), "tag_name").String()
33 |
34 | // 比较版本号
35 | if latestVersion > currentVersion {
36 | return latestVersion, nil
37 | }
38 | return "0", nil
39 | }
40 |
--------------------------------------------------------------------------------
/setHidewindow-win.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 |
3 | package main
4 |
5 | import (
6 | "os/exec"
7 | "syscall"
8 | )
9 |
10 | func setHideWindow(cmd *exec.Cmd) {
11 | cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
12 | }
13 |
--------------------------------------------------------------------------------
/setHidewindow.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 |
3 | package main
4 |
5 | import (
6 | "os/exec"
7 | "syscall"
8 | )
9 |
10 | func setHideWindow(cmd *exec.Cmd) {
11 | cmd.SysProcAttr = &syscall.SysProcAttr{}
12 | }
13 |
--------------------------------------------------------------------------------
/video_list.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bili-audio-downloader/bilibili"
5 | "bili-audio-downloader/config"
6 | "errors"
7 | "strconv"
8 |
9 | "github.com/tidwall/gjson"
10 | )
11 |
12 | // 视频列表
13 | type VideoList struct {
14 | Count int `json:"count"`
15 | List []VideoInformation
16 | }
17 |
18 | // 视频数据结构
19 | type VideoInformation struct {
20 | Bvid string `json:"bvid"`
21 | Cid int `json:"cid"`
22 | Title string `json:"title"`
23 | PageTitle string `json:"page_title"`
24 | Format string `json:"format"`
25 | PartId int `json:"part_id"`
26 | IsAudio bool `json:"is_audio"`
27 | Delete bool `json:"delete"`
28 | Audio AudioInformation
29 | Meta MetaInformation
30 | }
31 | type AudioInformation struct {
32 | Quality int `json:"quality"`
33 | Stream string `json:"stream"`
34 | }
35 | type MetaInformation struct {
36 | SongName string `json:"song_name"`
37 | Cover string `json:"cover"`
38 | Author string `json:"author"`
39 | Lyrics_path string `json:"lyrics_path"`
40 | }
41 |
42 | // 向列表中添加一个项目
43 | func (list *VideoList) Add(video *VideoInformation) {
44 | list.List = append(list.List, *video)
45 | list.Count++
46 | }
47 |
48 | // 向列表中添加一个视频
49 | func (VideoList *VideoList) AddVideo(sessdata, bvid string, downloadCompilation bool) error {
50 | // 查询视频信息
51 | video := new(bilibili.Video)
52 | err := video.Query(sessdata, bvid)
53 | if err != nil {
54 | return err
55 | }
56 |
57 | // 处理分集数量
58 | var total int = 1
59 | if downloadCompilation {
60 | total = len(video.Videos)
61 | }
62 |
63 | // 保存信息
64 | for i := 0; i < total; i++ {
65 | var list VideoInformation
66 | list.Bvid = video.Bvid
67 | list.Cid = video.Videos[i].Cid
68 | list.Title = CheckFileName(video.Meta.Title)
69 | list.PageTitle = CheckFileName(video.Videos[i].Part)
70 | list.Format = AudioType.m4a
71 | // 元数据
72 | list.Meta.Cover = video.Meta.Cover
73 | list.Meta.Author = video.Up.Name
74 | list.Delete = false
75 | // list.Meta.Lyrics_path =
76 |
77 | list.IsAudio = false
78 |
79 | // 处理音频标题(单 P 视频)
80 | var SongName string
81 | if total <= 1 {
82 | // 单集使用视频标题
83 | SongName, err = ExtractTitle(list.Title)
84 | if err != nil {
85 | SongName = list.Title
86 | }
87 | } else {
88 | // 多集视频使用分集标题
89 | SongName, err = ExtractTitle(list.PageTitle)
90 | if err != nil {
91 | SongName = list.PageTitle
92 | }
93 | }
94 | list.Meta.SongName = SongName
95 | VideoList.Add(&list)
96 | }
97 | return nil
98 | }
99 |
100 | // 向列表中添加一个音频项目
101 | func (VideoList *VideoList) AddAudio(sessdata, auid string) error {
102 | // 查询视频信息
103 | audio := new(bilibili.Audio)
104 | err := audio.Query(auid)
105 | if err != nil {
106 | return err
107 | }
108 |
109 | aucid, err := strconv.Atoi(auid)
110 | if err != nil {
111 | return err
112 | }
113 |
114 | // 保存信息
115 | var list VideoInformation
116 | list.Bvid = auid
117 | list.Cid = aucid
118 | list.Title = CheckFileName(audio.Meta.Title)
119 | list.PageTitle = CheckFileName(audio.Meta.Title)
120 | list.Format = AudioType.m4a
121 | // 元数据
122 | list.Meta.Cover = audio.Meta.Cover
123 | list.Meta.Author = audio.Up.Author
124 | list.Meta.Lyrics_path = audio.Meta.Lyric
125 | list.Meta.SongName = audio.Meta.Title
126 |
127 | list.IsAudio = true
128 | list.Delete = false
129 |
130 | VideoList.Add(&list)
131 | return nil
132 | }
133 |
134 | // 向列表中添加一个收藏夹
135 | func (VideoList *VideoList) AddCollection(sessdata, favlistId string, count int, downloadCompilation bool) error {
136 | // 请求收藏夹基础数据,初始化循环
137 | favlist, err := bilibili.GetFavListObj(favlistId, sessdata, 1, 1)
138 | if err != nil {
139 | return err
140 | }
141 | // 计算下载页数
142 | var pageCount int
143 | if count == 0 {
144 | // 如果下载数量为 0 (全部下载)
145 | count = favlist.Data.Info.Media_count
146 | pageCount = count / 20
147 | } else {
148 | // 计算下载页数
149 | pageCount = count / 20
150 | }
151 | // 非完整页面
152 | if count%20 != 0 {
153 | pageCount++
154 | }
155 |
156 | // 主循环
157 | for i := 0; i < pageCount; i++ {
158 | // 获取当前分页信息
159 | favlist, err := bilibili.GetFavListObj(favlistId, sessdata, 20, i+1)
160 | if err != nil {
161 | return err
162 | }
163 |
164 | // 遍历分页
165 | for j := 0; j < len(favlist.Data.Medias); j++ {
166 |
167 | if favlist.Data.Medias[j].Type == 2 {
168 | // 添加视频到列表
169 | err := VideoList.AddVideo(sessdata, favlist.Data.Medias[j].Bvid, downloadCompilation)
170 | if err != nil {
171 | continue
172 | }
173 | } else {
174 | // 添加收藏夹中的音频
175 | err := VideoList.AddAudio(sessdata, strconv.Itoa(favlist.Data.Medias[j].Id))
176 | if err != nil {
177 | continue
178 | }
179 | }
180 | }
181 | }
182 |
183 | return nil
184 | }
185 |
186 | // 向列表中添加一个视频合集
187 | func (VideoList *VideoList) AddCompilation(sessdata string, mid, sid, count int, downloadCompilation bool) error {
188 | // 请求收藏夹基础数据,初始化循环
189 | favlist, err := bilibili.GetCompliationObj(mid, sid, 1, 1)
190 | if err != nil {
191 | return err
192 | }
193 | // 计算下载页数
194 | var pageCount int
195 | if count == 0 {
196 | // 如果下载数量为 0 (全部下载)
197 | count = favlist.Data.Meta.Total
198 | pageCount = count / 20
199 | } else {
200 | // 计算下载页数
201 | pageCount = count / 20
202 | }
203 | // 非完整页面
204 | if count%20 != 0 {
205 | pageCount++
206 | }
207 |
208 | // 主循环
209 | for i := 0; i < pageCount; i++ {
210 | // 获取当前分页信息
211 | favlist, err := bilibili.GetCompliationObj(mid, sid, 20, i+1)
212 | if err != nil {
213 | return err
214 | }
215 | // 遍历分页
216 | for j := 0; j < len(favlist.Data.Archives); j++ {
217 | // 添加视频到列表
218 | err := VideoList.AddVideo(sessdata, favlist.Data.Archives[j].Bvid, downloadCompilation)
219 | if err != nil {
220 | continue
221 | }
222 | }
223 | }
224 |
225 | return nil
226 | }
227 |
228 | // 向列表中添加个人主页视频
229 | func (VideoList *VideoList) AddProfileVideo(sessdata string, mid, count int, downloadCompilation bool) error {
230 | respJson, err := bilibili.GetProfileVideo(strconv.Itoa(mid), "1", "1", sessdata)
231 | if err != nil {
232 | return err
233 | }
234 |
235 | // 计算下载页数
236 | var pageCount int
237 | if count == 0 {
238 | // 如果下载数量为 0 (全部下载)
239 | count = int(gjson.Get(respJson, "data.page.count").Int())
240 | pageCount = count / 20
241 | } else {
242 | // 计算下载页数
243 | pageCount = count / 20
244 | }
245 | // 非完整页面
246 | if count%20 != 0 {
247 | pageCount++
248 | }
249 |
250 | // 主循环
251 | for i := 0; i < pageCount; i++ {
252 | pageSize := 20
253 |
254 | // 处理非完整尾页
255 | if i+1 == pageCount && count%20 != 0 {
256 | pageSize = count % 20
257 | }
258 |
259 | // 获取当前分页信息
260 | respJson, err := bilibili.GetProfileVideo(strconv.Itoa(mid), strconv.Itoa(i+1), "20", sessdata)
261 | if err != nil {
262 | return err
263 | }
264 | // 遍历分页
265 | for j := 0; j < pageSize; j++ {
266 | // 添加视频到列表
267 | err := VideoList.AddVideo(sessdata, gjson.Get(respJson, "data.list.vlist."+strconv.Itoa(j)+".bvid").String(), downloadCompilation)
268 | if err != nil {
269 | continue
270 | }
271 | }
272 | }
273 |
274 | return nil
275 | }
276 |
277 | // 读取视频列表
278 | func (VideoList *VideoList) Get(path ...string) error {
279 | // 指定路径
280 | filePath := config.Cfg.GetVideolistPath()
281 | if len(path) > 0 {
282 | filePath = path[0]
283 | }
284 |
285 | err := LoadJsonFile(filePath, VideoList)
286 | if err != nil {
287 | return err
288 | }
289 | return err
290 | }
291 |
292 | // 保存视频列表
293 | func (VideoList *VideoList) Save(path ...string) error {
294 | // 指定路径
295 | filePath := config.Cfg.GetVideolistPath()
296 | if len(path) > 0 {
297 | filePath = path[0]
298 | }
299 |
300 | err := SaveJsonFile(filePath, VideoList)
301 | if err != nil {
302 | return err
303 | }
304 | return nil
305 | }
306 |
307 | // 获取视频流
308 | // TODO:请求前检查数据
309 | func (v *VideoInformation) GetStream(sessdata string) error {
310 | // 请求信息
311 | json, err := bilibili.GetVideoStream(v.Bvid, strconv.Itoa(v.Cid), sessdata)
312 | if err != nil {
313 | return err
314 | }
315 | // 错误检查
316 | if CheckObj(int(gjson.Get(json, "code").Int())) {
317 | return errors.New(gjson.Get(json, "message").String())
318 | }
319 |
320 | // 选择音频流
321 | if gjson.Get(json, "data.dash.flac.audio").String() != "" {
322 | v.Audio.Quality = int(gjson.Get(json, "data.dash.audio.id").Int())
323 | v.Audio.Stream = gjson.Get(json, "data.dash.flac.audio.base_url").String()
324 | v.Format = AudioType.flac
325 |
326 | return nil
327 | }
328 | v.Audio.Quality = int(gjson.Get(json, "data.dash.audio.0.id").Int())
329 | v.Audio.Stream = gjson.Get(json, "data.dash.audio.0.base_url").String()
330 |
331 | return nil
332 | }
333 |
334 | func (videoList *VideoList) Tidy() {
335 | if len(videoList.List) == 0 {
336 | return
337 | }
338 |
339 | result := videoList.List[:0]
340 | for _, video := range videoList.List {
341 | if !video.Delete {
342 | result = append(result, video)
343 | }
344 | }
345 | videoList.List = result
346 | videoList.Count = len(result)
347 | }
348 |
--------------------------------------------------------------------------------
/wails.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://wails.io/schemas/config.v2.json",
3 | "name": "BiliAudioDownloaderUI",
4 | "outputfilename": "BiliAudioDownloaderUI",
5 | "frontend:install": "npm install",
6 | "frontend:build": "npm run build",
7 | "frontend:dev:watcher": "npm run dev",
8 | "frontend:dev:serverUrl": "auto",
9 | "author": {
10 | "name": "HIM049"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------