mirror of
https://github.com/Xinrea/bili-shadowreplay.git
synced 2025-11-24 20:15:34 +08:00
ci/cd: docker package build
This commit is contained in:
39
.dockerignore
Normal file
39
.dockerignore
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
.pnpm-store
|
||||||
|
.npm
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
target
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Version control
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# IDE and editor files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Debug files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Tauri specific
|
||||||
|
src-tauri/target
|
||||||
|
src-tauri/dist
|
||||||
50
.github/workflows/package.yml
vendored
Normal file
50
.github/workflows/package.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: Docker Build and Push
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=sha,format=long
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
70
Dockerfile
Normal file
70
Dockerfile
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Build frontend
|
||||||
|
FROM node:20-bullseye AS frontend-builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
python3 \
|
||||||
|
make \
|
||||||
|
g++ \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
|
||||||
|
# Install dependencies with specific flags
|
||||||
|
RUN yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
# Copy source files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build frontend
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
# Build Rust backend
|
||||||
|
FROM rust:1.85-slim AS rust-builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install required system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
cmake \
|
||||||
|
pkg-config \
|
||||||
|
libssl-dev \
|
||||||
|
libwebkit2gtk-4.1-dev \
|
||||||
|
libappindicator3-dev \
|
||||||
|
librsvg2-dev \
|
||||||
|
patchelf \
|
||||||
|
libclang-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy Rust project files
|
||||||
|
COPY src-tauri/Cargo.toml src-tauri/Cargo.lock ./src-tauri/
|
||||||
|
COPY src-tauri/src ./src-tauri/src
|
||||||
|
|
||||||
|
# Build Rust backend
|
||||||
|
WORKDIR /app/src-tauri
|
||||||
|
RUN cargo build --features headless --release
|
||||||
|
|
||||||
|
# Final stage
|
||||||
|
FROM debian:bullseye-slim AS final
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install runtime dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libssl1.1 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy built frontend
|
||||||
|
COPY --from=frontend-builder /app/dist ./dist
|
||||||
|
|
||||||
|
# Copy built Rust binary
|
||||||
|
COPY --from=rust-builder /app/src-tauri/target/release/bili-shadowreplay .
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["./bili-shadowreplay"]
|
||||||
@@ -6,9 +6,6 @@
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> v2.0.0 版本为重大更新,将不兼容 v1.x 版本的数据。
|
|
||||||
|
|
||||||
BiliBili ShadowReplay 是一个缓存直播并进行实时编辑投稿的工具。通过划定时间区间,并编辑简单的必需信息,即可完成直播切片以及投稿,将整个流程压缩到分钟级。同时,也支持对缓存的历史直播进行回放,以及相同的切片编辑投稿处理流程。
|
BiliBili ShadowReplay 是一个缓存直播并进行实时编辑投稿的工具。通过划定时间区间,并编辑简单的必需信息,即可完成直播切片以及投稿,将整个流程压缩到分钟级。同时,也支持对缓存的历史直播进行回放,以及相同的切片编辑投稿处理流程。
|
||||||
|
|
||||||
目前仅支持 B 站和抖音平台的直播。
|
目前仅支持 B 站和抖音平台的直播。
|
||||||
|
|||||||
3
src-tauri/.gitignore
vendored
3
src-tauri/.gitignore
vendored
@@ -3,4 +3,5 @@
|
|||||||
/target/
|
/target/
|
||||||
tmps
|
tmps
|
||||||
clips
|
clips
|
||||||
data
|
data
|
||||||
|
config.toml
|
||||||
791
src-tauri/Cargo.lock
generated
791
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -9,11 +9,7 @@ edition = "2021"
|
|||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
tauri-build = { version = "2", features = [] }
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = ["protocol-asset", "tray-icon"] }
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
||||||
serde_derive = "1.0.158"
|
serde_derive = "1.0.158"
|
||||||
@@ -37,27 +33,19 @@ urlencoding = "2.1.3"
|
|||||||
log = "0.4.22"
|
log = "0.4.22"
|
||||||
simplelog = "0.12.2"
|
simplelog = "0.12.2"
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
|
||||||
tauri-plugin-dialog = "2"
|
|
||||||
tauri-plugin-shell = "2"
|
|
||||||
tauri-plugin-fs = "2"
|
|
||||||
tauri-plugin-http = "2"
|
|
||||||
tauri-utils = "2"
|
|
||||||
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
|
|
||||||
tauri-plugin-os = "2"
|
|
||||||
tauri-plugin-notification = "2"
|
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
base64 = "0.21"
|
base64 = "0.21"
|
||||||
mime_guess = "2.0"
|
mime_guess = "2.0"
|
||||||
async-trait = "0.1.87"
|
async-trait = "0.1.87"
|
||||||
whisper-rs = "0.14.2"
|
whisper-rs = "0.14.2"
|
||||||
hound = "3.5.1"
|
hound = "3.5.1"
|
||||||
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" }
|
|
||||||
uuid = { version = "1.4", features = ["v4"] }
|
uuid = { version = "1.4", features = ["v4"] }
|
||||||
axum = { version = "0.7", features = ["macros"] }
|
axum = { version = "0.7", features = ["macros"] }
|
||||||
tower-http = { version = "0.5", features = ["cors", "fs", "limit"] }
|
tower-http = { version = "0.5", features = ["cors", "fs", "limit"] }
|
||||||
futures-core = "0.3"
|
futures-core = "0.3"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
tokio-util = { version = "0.7", features = ["io"] }
|
tokio-util = { version = "0.7", features = ["io"] }
|
||||||
|
clap = { version = "4.5.37", features = ["derive"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||||
@@ -65,7 +53,6 @@ tokio-util = { version = "0.7", features = ["io"] }
|
|||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
cuda = ["whisper-rs/cuda"]
|
cuda = ["whisper-rs/cuda"]
|
||||||
headless = []
|
headless = []
|
||||||
default = ["headless"]
|
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-single-instance = "2"
|
tauri-plugin-single-instance = "2"
|
||||||
@@ -76,3 +63,18 @@ whisper-rs = { version = "0.14.2", default-features = false }
|
|||||||
[target.'cfg(darwin)'.dependencies.whisper-rs]
|
[target.'cfg(darwin)'.dependencies.whisper-rs]
|
||||||
version = "0.14.2"
|
version = "0.14.2"
|
||||||
features = ["metal"]
|
features = ["metal"]
|
||||||
|
|
||||||
|
[target.'cfg(not(feature = "headless"))'.dependencies]
|
||||||
|
tauri = { version = "2", features = ["protocol-asset", "tray-icon"] }
|
||||||
|
tauri-plugin-dialog = "2"
|
||||||
|
tauri-plugin-shell = "2"
|
||||||
|
tauri-plugin-fs = "2"
|
||||||
|
tauri-plugin-http = "2"
|
||||||
|
tauri-utils = "2"
|
||||||
|
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
|
||||||
|
tauri-plugin-os = "2"
|
||||||
|
tauri-plugin-notification = "2"
|
||||||
|
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" }
|
||||||
|
|
||||||
|
[target.'cfg(not(feature = "headless"))'.build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
14
src-tauri/config.example.toml
Normal file
14
src-tauri/config.example.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
cache = "./cache"
|
||||||
|
output = "./output"
|
||||||
|
live_start_notify = true
|
||||||
|
live_end_notify = true
|
||||||
|
clip_notify = true
|
||||||
|
post_notify = true
|
||||||
|
auto_subtitle = false
|
||||||
|
whisper_model = ""
|
||||||
|
whisper_prompt = "这是一段中文 你们好"
|
||||||
|
clip_name_format = "[{room_id}][{live_id}][{title}][{created_at}].mp4"
|
||||||
|
|
||||||
|
[auto_generate]
|
||||||
|
enabled = false
|
||||||
|
encode_danmu = false
|
||||||
File diff suppressed because one or more lines are too long
@@ -2372,6 +2372,12 @@
|
|||||||
"const": "core:app:allow-set-app-theme",
|
"const": "core:app:allow-set-app-theme",
|
||||||
"markdownDescription": "Enables the set_app_theme command without any pre-configured scope."
|
"markdownDescription": "Enables the set_app_theme command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the set_dock_visibility command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:allow-set-dock-visibility",
|
||||||
|
"markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the tauri_version command without any pre-configured scope.",
|
"description": "Enables the tauri_version command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -2432,6 +2438,12 @@
|
|||||||
"const": "core:app:deny-set-app-theme",
|
"const": "core:app:deny-set-app-theme",
|
||||||
"markdownDescription": "Denies the set_app_theme command without any pre-configured scope."
|
"markdownDescription": "Denies the set_app_theme command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the set_dock_visibility command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:deny-set-dock-visibility",
|
||||||
|
"markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the tauri_version command without any pre-configured scope.",
|
"description": "Denies the tauri_version command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -2372,6 +2372,12 @@
|
|||||||
"const": "core:app:allow-set-app-theme",
|
"const": "core:app:allow-set-app-theme",
|
||||||
"markdownDescription": "Enables the set_app_theme command without any pre-configured scope."
|
"markdownDescription": "Enables the set_app_theme command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the set_dock_visibility command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:allow-set-dock-visibility",
|
||||||
|
"markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the tauri_version command without any pre-configured scope.",
|
"description": "Enables the tauri_version command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -2432,6 +2438,12 @@
|
|||||||
"const": "core:app:deny-set-app-theme",
|
"const": "core:app:deny-set-app-theme",
|
||||||
"markdownDescription": "Denies the set_app_theme command without any pre-configured scope."
|
"markdownDescription": "Denies the set_app_theme command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the set_dock_visibility command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:deny-set-dock-visibility",
|
||||||
|
"markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the tauri_version command without any pre-configured scope.",
|
"description": "Denies the tauri_version command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ use crate::{recorder::PlatformType, recorder_manager::ClipRangeParams};
|
|||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub cache: String,
|
pub cache: String,
|
||||||
pub output: String,
|
pub output: String,
|
||||||
|
#[serde(skip)]
|
||||||
pub webid: String,
|
pub webid: String,
|
||||||
|
#[serde(skip)]
|
||||||
pub webid_ts: i64,
|
pub webid_ts: i64,
|
||||||
pub live_start_notify: bool,
|
pub live_start_notify: bool,
|
||||||
pub live_end_notify: bool,
|
pub live_end_notify: bool,
|
||||||
@@ -26,6 +28,8 @@ pub struct Config {
|
|||||||
pub clip_name_format: String,
|
pub clip_name_format: String,
|
||||||
#[serde(default = "default_auto_generate_config")]
|
#[serde(default = "default_auto_generate_config")]
|
||||||
pub auto_generate: AutoGenerateConfig,
|
pub auto_generate: AutoGenerateConfig,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub config_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
@@ -58,9 +62,7 @@ fn default_auto_generate_config() -> AutoGenerateConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn load() -> Self {
|
pub fn load(config_path: &str) -> Self {
|
||||||
let app_dirs = AppDirs::new(Some("cn.vjoi.bili-shadowreplay"), false).unwrap();
|
|
||||||
let config_path = app_dirs.config_dir.join("Conf.toml");
|
|
||||||
if let Ok(content) = std::fs::read_to_string(config_path) {
|
if let Ok(content) = std::fs::read_to_string(config_path) {
|
||||||
if let Ok(config) = toml::from_str(&content) {
|
if let Ok(config) = toml::from_str(&content) {
|
||||||
return config;
|
return config;
|
||||||
@@ -69,18 +71,8 @@ impl Config {
|
|||||||
let config = Config {
|
let config = Config {
|
||||||
webid: "".to_string(),
|
webid: "".to_string(),
|
||||||
webid_ts: 0,
|
webid_ts: 0,
|
||||||
cache: app_dirs
|
cache: "./cache".to_string(),
|
||||||
.cache_dir
|
output: "./output".to_string(),
|
||||||
.join("cache")
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.to_string(),
|
|
||||||
output: app_dirs
|
|
||||||
.data_dir
|
|
||||||
.join("output")
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.to_string(),
|
|
||||||
live_start_notify: true,
|
live_start_notify: true,
|
||||||
live_end_notify: true,
|
live_end_notify: true,
|
||||||
clip_notify: true,
|
clip_notify: true,
|
||||||
@@ -90,6 +82,7 @@ impl Config {
|
|||||||
whisper_prompt: "这是一段中文 你们好".to_string(),
|
whisper_prompt: "这是一段中文 你们好".to_string(),
|
||||||
clip_name_format: "[{room_id}][{live_id}][{title}][{created_at}].mp4".to_string(),
|
clip_name_format: "[{room_id}][{live_id}][{title}][{created_at}].mp4".to_string(),
|
||||||
auto_generate: default_auto_generate_config(),
|
auto_generate: default_auto_generate_config(),
|
||||||
|
config_path: config_path.to_string(),
|
||||||
};
|
};
|
||||||
config.save();
|
config.save();
|
||||||
config
|
config
|
||||||
@@ -97,11 +90,7 @@ impl Config {
|
|||||||
|
|
||||||
pub fn save(&self) {
|
pub fn save(&self) {
|
||||||
let content = toml::to_string(&self).unwrap();
|
let content = toml::to_string(&self).unwrap();
|
||||||
let app_dirs = AppDirs::new(Some("cn.vjoi.bili-shadowreplay"), false).unwrap();
|
std::fs::write(self.config_path.clone(), content).unwrap();
|
||||||
// Create app dirs if not exists
|
|
||||||
std::fs::create_dir_all(&app_dirs.config_dir).unwrap();
|
|
||||||
let config_path = app_dirs.config_dir.join("Conf.toml");
|
|
||||||
std::fs::write(config_path, content).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_cache_path(&mut self, path: &str) {
|
pub fn set_cache_path(&mut self, path: &str) {
|
||||||
|
|||||||
@@ -1031,7 +1031,7 @@ pub async fn start_api_server(state: State) {
|
|||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
// Serve static files from dist directory
|
// Serve static files from dist directory
|
||||||
.nest_service("/", ServeDir::new("../dist"))
|
.nest_service("/", ServeDir::new("./dist"))
|
||||||
// Account commands
|
// Account commands
|
||||||
.route("/api/get_accounts", post(handler_get_accounts))
|
.route("/api/get_accounts", post(handler_get_accounts))
|
||||||
.route("/api/add_account", post(handler_add_account))
|
.route("/api/add_account", post(handler_add_account))
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ mod subtitle_generator;
|
|||||||
mod tray;
|
mod tray;
|
||||||
|
|
||||||
use archive_migration::try_rebuild_archives;
|
use archive_migration::try_rebuild_archives;
|
||||||
|
use clap::{arg, command, Parser};
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use database::Database;
|
use database::Database;
|
||||||
use futures_core::future::BoxFuture;
|
use futures_core::future::BoxFuture;
|
||||||
@@ -129,23 +130,27 @@ impl MigrationSource<'static> for MigrationList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "headless")]
|
#[cfg(feature = "headless")]
|
||||||
async fn setup_server_state() -> Result<State, Box<dyn std::error::Error>> {
|
async fn setup_server_state(args: Args) -> Result<State, Box<dyn std::error::Error>> {
|
||||||
use progress_manager::ProgressManager;
|
use progress_manager::ProgressManager;
|
||||||
use progress_reporter::EventEmitter;
|
use progress_reporter::EventEmitter;
|
||||||
|
|
||||||
setup_logging(Path::new("bsr.log")).await?;
|
setup_logging(Path::new("./")).await?;
|
||||||
println!("Setting up server state...");
|
println!("Setting up server state...");
|
||||||
let client = Arc::new(BiliClient::new()?);
|
let client = Arc::new(BiliClient::new()?);
|
||||||
let config = Arc::new(RwLock::new(Config::load()));
|
let config = Arc::new(RwLock::new(Config::load(&args.config)));
|
||||||
let db = Arc::new(Database::new());
|
let db = Arc::new(Database::new());
|
||||||
// connect to sqlite database
|
// connect to sqlite database
|
||||||
|
|
||||||
let conn_url = "sqlite:data/data_v2.db";
|
let conn_url = format!("sqlite:{}/data_v2.db", args.db);
|
||||||
|
// create db folder if not exists
|
||||||
if !Sqlite::database_exists(conn_url).await.unwrap_or(false) {
|
if !Path::new(&args.db).exists() {
|
||||||
Sqlite::create_database(conn_url).await?;
|
std::fs::create_dir_all(&args.db)?;
|
||||||
}
|
}
|
||||||
let db_pool: Pool<Sqlite> = Pool::connect(conn_url).await?;
|
|
||||||
|
if !Sqlite::database_exists(&conn_url).await.unwrap_or(false) {
|
||||||
|
Sqlite::create_database(&conn_url).await?;
|
||||||
|
}
|
||||||
|
let db_pool: Pool<Sqlite> = Pool::connect(&conn_url).await?;
|
||||||
let migrations = get_migrations();
|
let migrations = get_migrations();
|
||||||
|
|
||||||
let migrator = Migrator::new(MigrationList(migrations))
|
let migrator = Migrator::new(MigrationList(migrations))
|
||||||
@@ -174,11 +179,16 @@ async fn setup_server_state() -> Result<State, Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
#[cfg(not(feature = "headless"))]
|
#[cfg(not(feature = "headless"))]
|
||||||
async fn setup_app_state(app: &tauri::App) -> Result<State, Box<dyn std::error::Error>> {
|
async fn setup_app_state(app: &tauri::App) -> Result<State, Box<dyn std::error::Error>> {
|
||||||
|
use platform_dirs::AppDirs;
|
||||||
use progress_manager::ProgressManager;
|
use progress_manager::ProgressManager;
|
||||||
|
use progress_reporter::EventEmitter;
|
||||||
|
|
||||||
println!("Setting up app state...");
|
println!("Setting up app state...");
|
||||||
|
let app_dirs = AppDirs::new(Some("cn.vjoi.bili-shadowreplay"), false).unwrap();
|
||||||
|
let config_path = app_dirs.config_dir.join("Conf.toml");
|
||||||
|
|
||||||
let client = Arc::new(BiliClient::new()?);
|
let client = Arc::new(BiliClient::new()?);
|
||||||
let config = Arc::new(RwLock::new(Config::load()));
|
let config = Arc::new(RwLock::new(Config::load(config_path.to_str().unwrap())));
|
||||||
let config_clone = config.clone();
|
let config_clone = config.clone();
|
||||||
let dbs = app.state::<tauri_plugin_sql::DbInstances>().inner();
|
let dbs = app.state::<tauri_plugin_sql::DbInstances>().inner();
|
||||||
let db = Arc::new(Database::new());
|
let db = Arc::new(Database::new());
|
||||||
@@ -188,8 +198,11 @@ async fn setup_app_state(app: &tauri::App) -> Result<State, Box<dyn std::error::
|
|||||||
let log_dir = app.path().app_log_dir()?;
|
let log_dir = app.path().app_log_dir()?;
|
||||||
setup_logging(&log_dir).await?;
|
setup_logging(&log_dir).await?;
|
||||||
|
|
||||||
|
let emitter = EventEmitter::new(app.handle().clone());
|
||||||
|
|
||||||
let recorder_manager = Arc::new(RecorderManager::new(
|
let recorder_manager = Arc::new(RecorderManager::new(
|
||||||
app.handle().clone(),
|
app.app_handle().clone(),
|
||||||
|
emitter,
|
||||||
db.clone(),
|
db.clone(),
|
||||||
config.clone(),
|
config.clone(),
|
||||||
));
|
));
|
||||||
@@ -407,10 +420,25 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "headless")]
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
/// Path to the config file
|
||||||
|
#[arg(short, long, default_value_t = String::from("config.toml"))]
|
||||||
|
config: String,
|
||||||
|
|
||||||
|
/// Path to the database folder
|
||||||
|
#[arg(short, long, default_value_t = String::from("./data"))]
|
||||||
|
db: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "headless")]
|
#[cfg(feature = "headless")]
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let state = setup_server_state()
|
// get params from command line
|
||||||
|
let args = Args::parse();
|
||||||
|
let state = setup_server_state(args)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to setup server state");
|
.expect("Failed to setup server state");
|
||||||
http_server::start_api_server(state).await;
|
http_server::start_api_server(state).await;
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ use async_trait::async_trait;
|
|||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use tauri::AppHandle;
|
|
||||||
use tauri::Emitter;
|
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::progress_manager::Event;
|
use crate::progress_manager::Event;
|
||||||
use crate::recorder::danmu::DanmuEntry;
|
|
||||||
|
#[cfg(not(feature = "headless"))]
|
||||||
|
use {
|
||||||
|
crate::recorder::danmu::DanmuEntry,
|
||||||
|
tauri::{AppHandle, Emitter},
|
||||||
|
};
|
||||||
|
|
||||||
type CancelFlagMap = std::collections::HashMap<String, Arc<AtomicBool>>;
|
type CancelFlagMap = std::collections::HashMap<String, Arc<AtomicBool>>;
|
||||||
|
|
||||||
@@ -49,7 +52,7 @@ impl EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn emit(&self, event: Event) {
|
pub fn emit(&self, event: &Event) {
|
||||||
#[cfg(not(feature = "headless"))]
|
#[cfg(not(feature = "headless"))]
|
||||||
{
|
{
|
||||||
match event {
|
match event {
|
||||||
@@ -65,7 +68,13 @@ impl EventEmitter {
|
|||||||
}
|
}
|
||||||
Event::DanmuReceived { room, ts, content } => {
|
Event::DanmuReceived { room, ts, content } => {
|
||||||
self.app_handle
|
self.app_handle
|
||||||
.emit(&format!("danmu:{}", room), DanmuEntry { ts, content })
|
.emit(
|
||||||
|
&format!("danmu:{}", room),
|
||||||
|
DanmuEntry {
|
||||||
|
ts: *ts,
|
||||||
|
content: content.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -73,7 +82,7 @@ impl EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "headless")]
|
#[cfg(feature = "headless")]
|
||||||
let _ = self.sender.send(event);
|
let _ = self.sender.send(event.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl ProgressReporter {
|
impl ProgressReporter {
|
||||||
@@ -81,7 +90,7 @@ impl ProgressReporter {
|
|||||||
// if already exists, return
|
// if already exists, return
|
||||||
if CANCEL_FLAG_MAP.read().await.get(event_id).is_some() {
|
if CANCEL_FLAG_MAP.read().await.get(event_id).is_some() {
|
||||||
log::error!("Task already exists: {}", event_id);
|
log::error!("Task already exists: {}", event_id);
|
||||||
emitter.emit(Event::ProgressFinished {
|
emitter.emit(&Event::ProgressFinished {
|
||||||
id: event_id.to_string(),
|
id: event_id.to_string(),
|
||||||
success: false,
|
success: false,
|
||||||
message: "任务已经存在".to_string(),
|
message: "任务已经存在".to_string(),
|
||||||
@@ -106,14 +115,14 @@ impl ProgressReporter {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ProgressReporterTrait for ProgressReporter {
|
impl ProgressReporterTrait for ProgressReporter {
|
||||||
fn update(&self, content: &str) {
|
fn update(&self, content: &str) {
|
||||||
self.emitter.emit(Event::ProgressUpdate {
|
self.emitter.emit(&Event::ProgressUpdate {
|
||||||
id: self.event_id.clone(),
|
id: self.event_id.clone(),
|
||||||
content: content.to_string(),
|
content: content.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn finish(&self, success: bool, message: &str) {
|
async fn finish(&self, success: bool, message: &str) {
|
||||||
self.emitter.emit(Event::ProgressFinished {
|
self.emitter.emit(&Event::ProgressFinished {
|
||||||
id: self.event_id.clone(),
|
id: self.event_id.clone(),
|
||||||
success,
|
success,
|
||||||
message: message.to_string(),
|
message: message.to_string(),
|
||||||
|
|||||||
@@ -403,7 +403,7 @@ impl BiliRecorder {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if let WsStreamMessageType::DanmuMsg(msg) = msg {
|
if let WsStreamMessageType::DanmuMsg(msg) = msg {
|
||||||
self.emitter.emit(Event::DanmuReceived {
|
self.emitter.emit(&Event::DanmuReceived {
|
||||||
room: self.room_id,
|
room: self.room_id,
|
||||||
ts: msg.timestamp as i64,
|
ts: msg.timestamp as i64,
|
||||||
content: msg.msg.clone(),
|
content: msg.msg.clone(),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use std::collections::HashMap;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tauri::AppHandle;
|
||||||
use tokio::fs::{remove_file, write};
|
use tokio::fs::{remove_file, write};
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
@@ -58,6 +59,8 @@ pub enum RecorderEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct RecorderManager {
|
pub struct RecorderManager {
|
||||||
|
#[cfg(not(feature = "headless"))]
|
||||||
|
app_handle: AppHandle,
|
||||||
emitter: EventEmitter,
|
emitter: EventEmitter,
|
||||||
db: Arc<Database>,
|
db: Arc<Database>,
|
||||||
config: Arc<RwLock<Config>>,
|
config: Arc<RwLock<Config>>,
|
||||||
@@ -104,12 +107,15 @@ impl From<RecorderManagerError> for String {
|
|||||||
|
|
||||||
impl RecorderManager {
|
impl RecorderManager {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
#[cfg(not(feature = "headless"))] app_handle: AppHandle,
|
||||||
emitter: EventEmitter,
|
emitter: EventEmitter,
|
||||||
db: Arc<Database>,
|
db: Arc<Database>,
|
||||||
config: Arc<RwLock<Config>>,
|
config: Arc<RwLock<Config>>,
|
||||||
) -> RecorderManager {
|
) -> RecorderManager {
|
||||||
let (event_tx, _) = broadcast::channel(100);
|
let (event_tx, _) = broadcast::channel(100);
|
||||||
let manager = RecorderManager {
|
let manager = RecorderManager {
|
||||||
|
#[cfg(not(feature = "headless"))]
|
||||||
|
app_handle,
|
||||||
emitter,
|
emitter,
|
||||||
db,
|
db,
|
||||||
config,
|
config,
|
||||||
@@ -134,6 +140,8 @@ impl RecorderManager {
|
|||||||
|
|
||||||
pub fn clone(&self) -> Self {
|
pub fn clone(&self) -> Self {
|
||||||
RecorderManager {
|
RecorderManager {
|
||||||
|
#[cfg(not(feature = "headless"))]
|
||||||
|
app_handle: self.app_handle.clone(),
|
||||||
emitter: self.emitter.clone(),
|
emitter: self.emitter.clone(),
|
||||||
db: self.db.clone(),
|
db: self.db.clone(),
|
||||||
config: self.config.clone(),
|
config: self.config.clone(),
|
||||||
|
|||||||
Reference in New Issue
Block a user