diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml
new file mode 100644
index 0000000..a26354e
--- /dev/null
+++ b/.github/workflows/preview.yml
@@ -0,0 +1,129 @@
+name: Preview Deployment
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened]
+ pull_request_target:
+ types: [closed]
+
+jobs:
+ deploy-preview:
+ if: github.event.action != 'closed'
+ runs-on: ubuntu-latest
+ name: Deploy Preview
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup Rust
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+
+ - name: Cache Rust dependencies
+ uses: actions/cache@v3
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-cargo-
+
+ - name: Build
+ run: cargo install -q worker-build && worker-build --release
+
+ - name: Deploy to Cloudflare Workers
+ uses: cloudflare/wrangler-action@v3
+ with:
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ command: deploy --name firefly-api-pr-${{ github.event.number }} --compatibility-date 2025-06-22
+ workingDirectory: '.'
+ env:
+ FIREFLY_API_AUTHN_TOKEN: ${{ secrets.FIREFLY_API_AUTHN_TOKEN }}
+ BILIBILI_SESSDATA: ${{ secrets.BILIBILI_SESSDATA }}
+ SPOTIFY_WEB_API_CLIENT_ID: ${{ secrets.SPOTIFY_WEB_API_CLIENT_ID }}
+ SPOTIFY_WEB_API_CLIENT_SECRET: ${{ secrets.SPOTIFY_WEB_API_CLIENT_SECRET }}
+ SPOTIFY_WEB_API_REDIRECT_URI: ${{ secrets.SPOTIFY_WEB_API_REDIRECT_URI }}
+ - name: Comment PR
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const prNumber = context.issue.number;
+ const previewUrl = `https://firefly-api-pr-${prNumber}.eikasia30.workers.dev`;
+
+ // Find existing preview comment
+ const comments = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ });
+
+ const botComment = comments.data.find(comment =>
+ comment.user.type === 'Bot' &&
+ comment.body.includes('🚀 Preview Deployment')
+ );
+
+ const body = `🚀 **Preview Deployment**
+
+ Your preview deployment is ready!
+
+ **🔗 Preview URL:** ${previewUrl}
+
+ This preview will be updated automatically when you push new commits to this PR.
+
+ Deployed from commit: ${context.sha.substring(0, 7)}`;
+
+ if (botComment) {
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: botComment.id,
+ body: body
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ body: body
+ });
+ }
+
+ cleanup-preview:
+ if: github.event.action == 'closed'
+ runs-on: ubuntu-latest
+ name: Cleanup Preview
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Delete preview deployment
+ uses: cloudflare/wrangler-action@v3
+ with:
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ command: delete firefly-api-pr-${{ github.event.number }}
+ continue-on-error: true
+
+ - name: Comment PR cleanup
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const prNumber = context.issue.number;
+
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ body: `🧹 **Preview Cleaned Up**
+
+ The preview deployment for this PR has been removed.`
+ });
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 1501d29..2ddbe86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,7 @@
target
node_modules
.wrangler
-.dev.vars
\ No newline at end of file
+.dev.vars
+
+# test request json file
+request.json
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 6d2cc79..e51f413 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -132,6 +132,7 @@ dependencies = [
"pin-project-lite",
"rustversion",
"serde",
+ "serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper",
@@ -233,11 +234,34 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+[[package]]
+name = "bitflag"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3beac6a55fd5b7b4f4e857cc74d09f8b3e7c063fe147a83e38a76773750fdb1b"
+dependencies = [
+ "bitflag-attr-macros",
+]
+
+[[package]]
+name = "bitflag-attr-macros"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b618aab4883d58a2d2ed1f59d8d9cb2c8c735588f873f4a4b7966626f3fdad50"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+dependencies = [
+ "serde",
+]
[[package]]
name = "block-buffer"
@@ -335,6 +359,15 @@ dependencies = [
"typenum",
]
+[[package]]
+name = "deranged"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
+dependencies = [
+ "powerfmt",
+]
+
[[package]]
name = "digest"
version = "0.10.7"
@@ -356,6 +389,12 @@ dependencies = [
"syn",
]
+[[package]]
+name = "dotenv"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
+
[[package]]
name = "encoding_rs"
version = "0.8.35"
@@ -407,17 +446,29 @@ dependencies = [
"axum 0.8.4",
"axum-cloudflare-adapter",
"base64",
+ "bitflag",
+ "bitflags",
"console_error_panic_hook",
+ "dotenv",
"futures",
"http",
+ "instant",
+ "jiff",
+ "md5",
"oneshot",
"reqwest",
"rspotify",
"serde",
"serde_json",
+ "serde_repr",
+ "time",
"tower-service",
+ "tracing",
+ "tracing-subscriber",
+ "tracing-web",
"wasm-bindgen-futures",
"wasm-bindgen-test",
+ "web-sys",
"worker",
]
@@ -871,6 +922,18 @@ dependencies = [
"hashbrown",
]
+[[package]]
+name = "instant"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
[[package]]
name = "ipnet"
version = "2.11.0"
@@ -893,6 +956,47 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+[[package]]
+name = "jiff"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
+dependencies = [
+ "jiff-static",
+ "jiff-tzdb-platform",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "jiff-tzdb"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524"
+
+[[package]]
+name = "jiff-tzdb-platform"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
+dependencies = [
+ "jiff-tzdb",
+]
+
[[package]]
name = "js-sys"
version = "0.3.77"
@@ -903,6 +1007,12 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
[[package]]
name = "libc"
version = "0.2.174"
@@ -950,6 +1060,12 @@ dependencies = [
"syn",
]
+[[package]]
+name = "md5"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0"
+
[[package]]
name = "memchr"
version = "2.7.5"
@@ -1009,6 +1125,22 @@ dependencies = [
"tempfile",
]
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -1083,6 +1215,12 @@ dependencies = [
"vcpkg",
]
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
[[package]]
name = "percent-encoding"
version = "2.3.1"
@@ -1127,6 +1265,21 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+[[package]]
+name = "portable-atomic"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
+dependencies = [
+ "portable-atomic",
+]
+
[[package]]
name = "potential_utf"
version = "0.1.2"
@@ -1136,6 +1289,12 @@ dependencies = [
"zerovec",
]
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
[[package]]
name = "proc-macro2"
version = "1.0.95"
@@ -1170,6 +1329,7 @@ dependencies = [
"bytes",
"encoding_rs",
"futures-core",
+ "futures-util",
"h2",
"http",
"http-body",
@@ -1191,12 +1351,14 @@ dependencies = [
"sync_wrapper",
"tokio",
"tokio-native-tls",
+ "tokio-util",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
+ "wasm-streams",
"web-sys",
]
@@ -1441,6 +1603,17 @@ dependencies = [
"serde",
]
+[[package]]
+name = "serde_repr"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -1464,6 +1637,15 @@ dependencies = [
"digest",
]
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
[[package]]
name = "shlex"
version = "1.3.0"
@@ -1631,6 +1813,47 @@ dependencies = [
"syn",
]
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "time"
+version = "0.3.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
+dependencies = [
+ "deranged",
+ "itoa",
+ "js-sys",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
+
+[[package]]
+name = "time-macros"
+version = "0.2.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
[[package]]
name = "tinystr"
version = "0.8.1"
@@ -1741,9 +1964,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
+ "tracing-attributes",
"tracing-core",
]
+[[package]]
+name = "tracing-attributes"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "tracing-core"
version = "0.1.34"
@@ -1751,6 +1986,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-serde"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
+dependencies = [
+ "serde",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
+dependencies = [
+ "nu-ansi-term",
+ "serde",
+ "serde_json",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "time",
+ "tracing-core",
+ "tracing-log",
+ "tracing-serde",
+]
+
+[[package]]
+name = "tracing-web"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e6a141feebd51f8d91ebfd785af50fca223c570b86852166caa3b141defe7c"
+dependencies = [
+ "js-sys",
+ "tracing-core",
+ "tracing-subscriber",
+ "wasm-bindgen",
+ "web-sys",
]
[[package]]
@@ -1794,6 +2082,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
[[package]]
name = "vcpkg"
version = "0.2.15"
@@ -1958,6 +2252,22 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
[[package]]
name = "winapi-util"
version = "0.1.9"
@@ -1967,6 +2277,12 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
[[package]]
name = "windows-core"
version = "0.61.2"
diff --git a/Cargo.toml b/Cargo.toml
index 4cac1b0..3c76e65 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,7 +20,7 @@ crate-type = ["cdylib"]
[dependencies]
worker = { version = "0.5.0", features = ['http', 'axum'] }
-axum = { version = "0.8.4", default-features = false, features = ["macros", "query"] }
+axum = { version = "0.8.4", default-features = false, features = ["json", "macros", "query"] }
tower-service = "0.3.3"
console_error_panic_hook = { version = "0.1.7" }
rspotify = "0.14.0"
@@ -28,12 +28,24 @@ serde = { version = "1.0.219", features = ["derive"] }
axum-cloudflare-adapter = "0.14.0"
wasm-bindgen-futures = "0.4.50"
oneshot = "0.1.11"
-reqwest = { version = "0.12.22", features = ["json"] }
+reqwest = { version = "0.12.22", features = ["json", "stream"] }
http = "1.3.1"
serde_json = "1.0.140"
anyhow = "1.0.98"
base64 = "0.22.1"
futures = "0.3.31"
+md5 = "0.8.0"
+dotenv = "0.15.0"
+bitflag = "0.10.1"
+bitflags = { version = "2.9.1", features = ["serde"] }
+instant = { version = "0.1.13", features = ["wasm-bindgen"] }
+serde_repr = "0.1.20"
+web-sys = "0.3.77"
+jiff = "0.2.15"
+tracing = "0.1.41"
+tracing-web = "0.1.3"
+tracing-subscriber = { version = "0.3.19", features = ["time", "json"] }
+time = { version = "0.3.41", features = ["wasm-bindgen"] }
[dev-dependencies]
wasm-bindgen-test = "0.3.50"
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..dd80dce
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,7 @@
+// build.rs
+fn main() {
+ println!("cargo::rustc-check-cfg=cfg(ci_build)");
+ if std::env::var("CI").is_ok() {
+ println!("cargo:rustc-cfg=ci_build");
+ }
+}
\ No newline at end of file
diff --git a/src/api/v1/bilibili/mod.rs b/src/api/v1/bilibili/mod.rs
new file mode 100644
index 0000000..b04b953
--- /dev/null
+++ b/src/api/v1/bilibili/mod.rs
@@ -0,0 +1 @@
+pub mod video;
diff --git a/src/api/v1/bilibili/video/download.rs b/src/api/v1/bilibili/video/download.rs
new file mode 100644
index 0000000..a69dee9
--- /dev/null
+++ b/src/api/v1/bilibili/video/download.rs
@@ -0,0 +1,301 @@
+use axum::extract::{Json, State};
+use axum_cloudflare_adapter::wasm_compat;
+use jiff::{tz::TimeZone, Timestamp};
+use serde::Deserialize;
+
+use crate::{api::v1::ApiV1Response, utils::bilibili::Credential, AppState};
+
+#[derive(Debug, Deserialize)]
+pub struct DownloadRequest {
+ bvid: String,
+ /// Authorization token. This API is admin-only.
+ token: String,
+}
+
+#[derive(Debug, serde::Serialize)]
+pub struct DownloadResponse {
+ /// Name of the cloudflare r2 bucket where the video is stored
+ bucket: String,
+ /// A list of downloaded audio files from the given video
+ audios: Vec