Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 83 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,88 @@ biliresourcedownloader_版本号_x64-setup.exe`

## 文档

WIP
### Web UI 本机部署

Web UI 适合在浏览器中临时使用本项目功能,不需要安装桌面端。由于浏览器不能直接跨域访问 B 站接口,也不能直接设置 `Cookie` 请求头,Web UI 必须通过本项目内置的 Vite 本地代理运行,不建议将 `dist` 目录直接部署到普通静态站点。

1. 安装依赖

```bash
npm install
```

如果本机使用 Yarn,也可以执行:

```bash
yarn install
```

2. 开发模式运行

```bash
npm run dev -- --host 127.0.0.1
```

默认端口为 `1420`。浏览器打开终端输出的本地地址即可使用。

3. 构建并以预览模式部署

```bash
npm run build
npm run preview -- --host 127.0.0.1 --port 4173
```

访问 `http://127.0.0.1:4173/`。

4. 登录与下载说明

- Web UI 的扫码登录依赖本地代理转发登录 Cookie,请保持页面从 `127.0.0.1` 或 `localhost` 的 Vite 服务访问。
- 批量下载目录选择依赖浏览器的 File System Access API。支持该 API 的 Chromium 系浏览器可以选择本地目录并按资源结构写入文件;不支持时会回退到浏览器默认下载目录。
- 桌面端 Tauri 版本仍然支持原生文件选择、下载和本地存储能力,长期使用建议优先使用桌面版。

### 桌面 App 打包

桌面端使用 Tauri v2 打包。打包前请先安装对应系统的 Tauri 前置依赖,包括 Rust、平台 WebView 依赖和系统构建工具;本项目的 Tauri 配置会在打包前执行 `bun run build`,因此还需要安装 Bun。

1. 安装前置工具

- 安装 [Rust](https://www.rust-lang.org/tools/install)
- 按照 [Tauri v2 前置依赖文档](https://v2.tauri.app/start/prerequisites/) 安装当前系统所需依赖
- 安装 [Bun](https://bun.sh/)

2. 安装前端依赖

```bash
bun install
```

如果使用 npm 管理依赖,也可以执行:

```bash
npm install
```

3. 打包 App

```bash
npm run buildAll
```

该脚本等价于:

```bash
npm run tauri build -b
```

4. 查看产物

打包完成后,安装包和可执行文件会输出到:

```text
src-tauri/target/release/bundle/
```

当前配置的 `bundle.targets` 为 `all`,Tauri 会为当前操作系统生成可用的安装包格式。跨平台安装包通常需要在对应系统上分别打包。

## 主要技术栈

Expand All @@ -51,4 +132,4 @@ WIP

## 开源协议

本项目基于 [MIT协议](./LICENSE) 进行开源
本项目基于 [MIT协议](./LICENSE) 进行开源
5 changes: 2 additions & 3 deletions auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ declare global {
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const fetch: typeof import('@tauri-apps/plugin-http')['fetch']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
Expand Down Expand Up @@ -298,6 +297,7 @@ declare global {
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

// for vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
Expand Down Expand Up @@ -333,7 +333,6 @@ declare module 'vue' {
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly fetch: UnwrapRef<typeof import('@tauri-apps/plugin-http')['fetch']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly h: UnwrapRef<typeof import('vue')['h']>
Expand Down Expand Up @@ -589,4 +588,4 @@ declare module 'vue' {
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
}
}
}
12 changes: 6 additions & 6 deletions src/APIFetch.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { LazyStore } from '@tauri-apps/plugin-store';
import { clearLoginCookie, getLoginCookie, userLoggedIn } from "./utils/loginManager.ts";
import { GeneralAPIResponse } from "./types.ts";
import { encWbiWithFetch } from "./utils/wbi.ts";
import { useWbiStore } from "./store/useWbiStore";
import { setDebugInfo } from "./utils/debug.ts";
import md5 from "md5";
import { ClientOptions, fetch } from "@tauri-apps/plugin-http";
import { globalConfig } from "./utils/globalConfig.ts";
import { createRuntimeStore } from "./runtime/store.ts";
import { runtimeFetch } from "./runtime/http.ts";

const store = new LazyStore('APIResponseCache', {
const store = createRuntimeStore('APIResponseCache', {
// 不要持久化请求缓存
autoSave: false
})
Expand Down Expand Up @@ -94,7 +94,7 @@ async function APIFetch<T>(url: URL | string, init?: RequestInit, extraOptions?:
cookie = await getLoginCookie()
}

const options: RequestInit & ClientOptions = {
const options: RequestInit = {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0',
// 无cookie时,填写这几个参数貌似也能过一些验证
Expand All @@ -110,7 +110,7 @@ async function APIFetch<T>(url: URL | string, init?: RequestInit, extraOptions?:
let json: GeneralAPIResponse<T>

do {
json = await fetch(parsedURL, finalOptions).then(r => r.json()) as GeneralAPIResponse<T>
json = await runtimeFetch(parsedURL, finalOptions).then(r => r.json()) as GeneralAPIResponse<T>

if (json.code !== 0) {
if ((json.code === -352 || json.code === -403) && retryCount < 1) {
Expand Down Expand Up @@ -160,4 +160,4 @@ async function clearAPICache() {
await store.clear()
}

export { APIFetch, clearAPICache }
export { APIFetch, clearAPICache }
6 changes: 3 additions & 3 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<script setup lang="ts">
import { emitter } from "./main.ts"
import { getCurrentWindow } from "@tauri-apps/api/window"
import Icon from '../src-tauri/icons/icon.png'
import { globalConfig } from "./utils/globalConfig.ts";
import AppBackground from "./components/AppBackground.vue";
import { isMobileDevice } from "./utils/deviceUtils.ts";
import { getCurrentAppWindow } from "./runtime/window.ts";


const window = getCurrentWindow()
const window = getCurrentAppWindow()
const showDownloadDrawer = ref(false)
const showLoginDrawer = ref(false)

Expand Down Expand Up @@ -371,4 +371,4 @@ watch(() => route.fullPath, () => {
:deep(.el-input__inner) {
line-height: 0.5px;
}
</style>
</style>
18 changes: 11 additions & 7 deletions src/components/AppBackground.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { globalConfig } from "../utils/globalConfig";
import { readFile } from '@tauri-apps/plugin-fs'
import { readFile } from '../runtime/files.ts'

const backgroundImgPath = ref('')

Expand All @@ -14,14 +14,18 @@ watch(() => globalConfig.value.background.url, async () => {
return
}

const img = await readFile(path)
const extension = path.split('.').pop()
try {
const img = await readFile(path)
const extension = path.split('.').pop()

const blob = new Blob([img], { type: `image/${extension}` })
const blob = new Blob([img], { type: `image/${extension}` })

const url = URL.createObjectURL(blob)
const url = URL.createObjectURL(blob)

backgroundImgPath.value = `url(${url})`
backgroundImgPath.value = `url(${url})`
} catch {
backgroundImgPath.value = `url(${path})`
}
}
} else {
backgroundImgPath.value = ''
Expand All @@ -35,4 +39,4 @@ const enable = computed(() => globalConfig.value.background.enable)
class="color-bg w-full h-full absolute top-0 left-0 -z-50"
:style="{ backgroundImage: enable ? backgroundImgPath : '', opacity: enable ? globalConfig.background.opacity : 1 }"
/>
</template>
</template>
21 changes: 17 additions & 4 deletions src/components/BatchDownloadButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import { ElMessage, FormInstance } from "element-plus";
import type { BatchDownloadTask } from "../types.ts";
import { pushNewTask, startDownload } from "../utils/downloadManager.ts";
import { open } from "@tauri-apps/plugin-dialog";
import { TreeInstance } from "element-plus/lib/components";
import { sep } from "@tauri-apps/api/path";
import { globalConfig } from "../utils/globalConfig.ts";
import { download, isWebRuntime, open } from "../runtime/files.ts";
import { sep } from "../runtime/path.ts";

const props = defineProps<{
task: (() => BatchDownloadTask) | (() => Promise<BatchDownloadTask>)
Expand Down Expand Up @@ -130,6 +130,19 @@ const submit = async () => {
path: downloadConfig.path,
files: selectedFiles
}
if (isWebRuntime()) {
await Promise.all(finalTask.files.map(file => download(
file.url,
[finalTask.path, file.path].filter(Boolean).join(sep()),
)))
ElMessage({
message: `${finalTask.name}已完成浏览器下载`,
type: 'success',
})
showDialog.value = false
return
}

await pushNewTask(finalTask)

if (globalConfig.value.autoStartDownload) {
Expand Down Expand Up @@ -161,7 +174,7 @@ const selectSaveFolder = async () => {
directory: true,
})

if (path === null) return
if (typeof path !== 'string') return
downloadConfig.path = path
}

Expand Down Expand Up @@ -245,4 +258,4 @@ const openDialog = async () => {
.el-select-dropdown__wrap:has(.download-treeselect) {
max-height: calc(85vh - 200px);
}
</style>
</style>
9 changes: 2 additions & 7 deletions src/components/ImageVideoCard.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
<script setup lang="ts">
import { download } from '@tauri-apps/plugin-upload'
import { save } from '@tauri-apps/plugin-dialog'
import { invoke } from "@tauri-apps/api/core";
import { download, save, saveDataURL } from "../runtime/files.ts";

const props = defineProps<{
image?: string
Expand All @@ -18,9 +16,6 @@ const props = defineProps<{
suffix?: string
}>()

const saveDataURL = async (path: string, data: string) => {
return await invoke('save_data_url', { path, data }) === 'ok'
}
const downloadFile = async (url: string) => {
const suffix = props.suffix ?? url.split('?')[0].split('.').pop() ?? ''
const name = (props.downloadName ?? props.title) + '.' + suffix
Expand Down Expand Up @@ -222,4 +217,4 @@ function closeVideo() {
</div>
</template>
</ElCard>
</template>
</template>
55 changes: 39 additions & 16 deletions src/components/LoginManager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))

const qrCodeData = ref('')

interface LoginState {
isLogin: boolean
uname?: string
}

const getLoginState = () => APIFetch<LoginState>(
'https://api.bilibili.com/x/web-interface/nav',
undefined,
{ useCache: false, useCookie: true },
)

// 是否正在进行登录
const loggingIn = ref(false)
const login = async () => {
Expand Down Expand Up @@ -54,8 +65,6 @@ const login = async () => {

pollResp = await APIFetch(pollURL, undefined, { useCache: false })

console.log(pollResp)

if (pollResp.code !== 0) {
console.error(pollResp)
ElMessage({
Expand All @@ -77,16 +86,36 @@ const login = async () => {
}

if (pollResp.data.code === 0) {
ElMessage({
message: '登录成功',
type: 'success',
})

const loginURL = new URL(pollResp.data.url)
const params = loginURL.search.substring(1).split('&').join('; ')

await saveLoginCookie(params)
userLoggedIn.value = true

try {
const resp = await getLoginState()
userLoggedIn.value = resp.data?.isLogin

if (userLoggedIn.value) {
ElMessage({
message: '登录成功,欢迎 ' + resp.data.uname,
type: 'success',
})
} else {
await clearLoginCookie()
ElMessage({
message: '扫码成功,但登录状态校验未通过,请重新登录',
type: 'error',
})
}
} catch (e) {
console.error(e)
await clearLoginCookie()
ElMessage({
message: '扫码成功,但登录状态校验失败:' + e,
type: 'error',
})
}

loggingIn.value = false
}
}
Expand All @@ -105,15 +134,9 @@ emitter.on('loginDrawerOpen', async () => {
})

const testLoginState = async () => {
let resp: GeneralAPIResponse<{
isLogin: boolean
uname?: string
}>
let resp: GeneralAPIResponse<LoginState>
try {
resp = await APIFetch<{
isLogin: boolean
uname?: string
}>(`https://api.bilibili.com/x/web-interface/nav`, undefined, { useCache: false, useCookie: true })
resp = await getLoginState()
} catch (e) {
if ((e as GeneralAPIResponse<unknown>).code === -101) {
ElMessage({
Expand Down
Loading