├── .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 | 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 | 29 | 30 | 153 | 154 | -------------------------------------------------------------------------------- /frontend/src/components/collect_download/add_videos.vue: -------------------------------------------------------------------------------- 1 | 88 | 89 | 413 | 414 | -------------------------------------------------------------------------------- /frontend/src/components/collect_download/creat_videolist.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /frontend/src/components/collect_download/download_process.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | -------------------------------------------------------------------------------- /frontend/src/components/collect_download/videolist_editor.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 185 | 186 | -------------------------------------------------------------------------------- /frontend/src/components/main_page.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 51 | 52 | -------------------------------------------------------------------------------- /frontend/src/components/modules/addition_card.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/src/components/modules/fav_information.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /frontend/src/components/modules/frame_page.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /frontend/src/components/modules/head_bar.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 42 | 43 | -------------------------------------------------------------------------------- /frontend/src/components/setting_page.vue: -------------------------------------------------------------------------------- 1 | 115 | 116 | 236 | 237 | -------------------------------------------------------------------------------- /frontend/src/components/user_space.vue: -------------------------------------------------------------------------------- 1 | 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 | --------------------------------------------------------------------------------