mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-09-06 16:07:26 +08:00
feat: 优化对全局配置的处理,调整下载路径填充逻辑 (#523)
This commit is contained in:
@@ -2,7 +2,6 @@ use anyhow::{Context, Result, bail};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::bilibili::error::BiliError;
|
||||
use crate::config::VersionedConfig;
|
||||
|
||||
pub struct PageAnalyzer {
|
||||
info: serde_json::Value,
|
||||
@@ -131,14 +130,14 @@ pub enum Stream {
|
||||
|
||||
// 通用的获取流链接的方法,交由 Downloader 使用
|
||||
impl Stream {
|
||||
pub fn urls(&self) -> Vec<&str> {
|
||||
pub fn urls(&self, enable_cdn_sorting: bool) -> Vec<&str> {
|
||||
match self {
|
||||
Self::Flv(url) | Self::Html5Mp4(url) | Self::EpisodeTryMp4(url) => vec![url],
|
||||
Self::DashVideo { url, backup_url, .. } | Self::DashAudio { url, backup_url, .. } => {
|
||||
let mut urls = std::iter::once(url.as_str())
|
||||
.chain(backup_url.iter().map(|s| s.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
if VersionedConfig::get().load().cdn_sorting {
|
||||
if enable_cdn_sorting {
|
||||
urls.sort_by_key(|u| {
|
||||
if u.contains("upos-") {
|
||||
0 // 服务商 cdn
|
||||
@@ -424,16 +423,17 @@ mod tests {
|
||||
Some(AudioQuality::Quality192k),
|
||||
),
|
||||
];
|
||||
let config = VersionedConfig::get().read();
|
||||
for (bvid, video_quality, video_codec, audio_quality) in testcases.into_iter() {
|
||||
let client = BiliClient::new();
|
||||
let video = Video::new(&client, bvid.to_owned());
|
||||
let video = Video::new(&client, bvid.to_owned(), &config.credential);
|
||||
let pages = video.get_pages().await.expect("failed to get pages");
|
||||
let first_page = pages.into_iter().next().expect("no page found");
|
||||
let best_stream = video
|
||||
.get_page_analyzer(&first_page)
|
||||
.await
|
||||
.expect("failed to get page analyzer")
|
||||
.best_stream(&VersionedConfig::get().load().filter_option)
|
||||
.best_stream(&config.filter_option)
|
||||
.expect("failed to get best stream");
|
||||
dbg!(bvid, &best_stream);
|
||||
match best_stream {
|
||||
@@ -469,7 +469,7 @@ mod tests {
|
||||
codecs: VideoCodecs::AVC,
|
||||
};
|
||||
assert_eq!(
|
||||
stream.urls(),
|
||||
stream.urls(true),
|
||||
vec![
|
||||
"https://upos-sz-mirrorcos.bilivideo.com",
|
||||
"https://cn-tj-cu-01-11.bilivideo.com",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, bail};
|
||||
use leaky_bucket::RateLimiter;
|
||||
use reqwest::{Method, header};
|
||||
use sea_orm::DatabaseConnection;
|
||||
use ua_generator::ua;
|
||||
|
||||
use crate::bilibili::Credential;
|
||||
use crate::bilibili::credential::WbiImg;
|
||||
use crate::config::{RateLimit, VersionedCache, VersionedConfig};
|
||||
use crate::config::{RateLimit, VersionedCache};
|
||||
|
||||
// 一个对 reqwest::Client 的简单封装,用于 Bilibili 请求
|
||||
#[derive(Clone)]
|
||||
@@ -60,56 +60,78 @@ impl Default for Client {
|
||||
}
|
||||
}
|
||||
|
||||
enum Limiter {
|
||||
Latest(VersionedCache<Option<RateLimiter>>),
|
||||
Snapshot(Arc<Option<RateLimiter>>),
|
||||
}
|
||||
|
||||
pub struct BiliClient {
|
||||
pub client: Client,
|
||||
limiter: VersionedCache<Option<RateLimiter>>,
|
||||
limiter: Limiter,
|
||||
}
|
||||
|
||||
impl BiliClient {
|
||||
pub fn new() -> Self {
|
||||
let client = Client::new();
|
||||
let limiter = VersionedCache::new(|config| {
|
||||
Ok(config
|
||||
.concurrent_limit
|
||||
.rate_limit
|
||||
.as_ref()
|
||||
.map(|RateLimit { limit, duration }| {
|
||||
RateLimiter::builder()
|
||||
.initial(*limit)
|
||||
.refill(*limit)
|
||||
.max(*limit)
|
||||
.interval(Duration::from_millis(*duration))
|
||||
.build()
|
||||
}))
|
||||
})
|
||||
.expect("failed to create rate limiter");
|
||||
let limiter = Limiter::Latest(
|
||||
VersionedCache::new(|config| {
|
||||
Ok(config
|
||||
.concurrent_limit
|
||||
.rate_limit
|
||||
.as_ref()
|
||||
.map(|RateLimit { limit, duration }| {
|
||||
RateLimiter::builder()
|
||||
.initial(*limit)
|
||||
.refill(*limit)
|
||||
.max(*limit)
|
||||
.interval(Duration::from_millis(*duration))
|
||||
.build()
|
||||
}))
|
||||
})
|
||||
.expect("failed to create rate limiter"),
|
||||
);
|
||||
Self { client, limiter }
|
||||
}
|
||||
|
||||
/// 获取当前 BiliClient 的快照,快照中的限流器固定不变
|
||||
pub fn snapshot(&self) -> Result<Self> {
|
||||
let Limiter::Latest(inner) = &self.limiter else {
|
||||
// 语法上没问题,但语义上不允许对快照进行快照
|
||||
bail!("cannot snapshot a snapshot BiliClient");
|
||||
};
|
||||
Ok(Self {
|
||||
client: self.client.clone(),
|
||||
limiter: Limiter::Snapshot(inner.snapshot()),
|
||||
})
|
||||
}
|
||||
|
||||
/// 获取一个预构建的请求,通过该方法获取请求时会检查并等待速率限制
|
||||
pub async fn request(&self, method: Method, url: &str) -> reqwest::RequestBuilder {
|
||||
if let Some(limiter) = self.limiter.load().as_ref() {
|
||||
limiter.acquire_one().await;
|
||||
pub async fn request(&self, method: Method, url: &str, credential: &Credential) -> reqwest::RequestBuilder {
|
||||
match &self.limiter {
|
||||
Limiter::Latest(inner) => {
|
||||
if let Some(limiter) = inner.read().as_ref() {
|
||||
limiter.acquire_one().await;
|
||||
}
|
||||
}
|
||||
Limiter::Snapshot(inner) => {
|
||||
if let Some(limiter) = inner.as_ref() {
|
||||
limiter.acquire_one().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
let credential = &VersionedConfig::get().load().credential;
|
||||
self.client.request(method, url, Some(credential))
|
||||
}
|
||||
|
||||
pub async fn check_refresh(&self, connection: &DatabaseConnection) -> Result<()> {
|
||||
let credential = &VersionedConfig::get().load().credential;
|
||||
/// 检查并刷新 Credential,不需要刷新返回 Ok(None),需要刷新返回 Ok(Some(new_credential))
|
||||
pub async fn check_refresh(&self, credential: &Credential) -> Result<Option<Credential>> {
|
||||
if !credential.need_refresh(&self.client).await? {
|
||||
return Ok(());
|
||||
return Ok(None);
|
||||
}
|
||||
let new_credential = credential.refresh(&self.client).await?;
|
||||
VersionedConfig::get()
|
||||
.update_credential(new_credential, connection)
|
||||
.await?;
|
||||
Ok(())
|
||||
Ok(Some(credential.refresh(&self.client).await?))
|
||||
}
|
||||
|
||||
/// 获取 wbi img,用于生成请求签名
|
||||
pub async fn wbi_img(&self) -> Result<WbiImg> {
|
||||
let credential = &VersionedConfig::get().load().credential;
|
||||
pub async fn wbi_img(&self, credential: &Credential) -> Result<WbiImg> {
|
||||
credential.wbi_img(&self.client).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use reqwest::Method;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::bilibili::{BiliClient, Validate, VideoInfo};
|
||||
use crate::bilibili::{BiliClient, Credential, Validate, VideoInfo};
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Debug, Default, Copy)]
|
||||
pub enum CollectionType {
|
||||
@@ -73,6 +73,7 @@ pub struct CollectionItem {
|
||||
pub struct Collection<'a> {
|
||||
client: &'a BiliClient,
|
||||
pub collection: CollectionItem,
|
||||
credential: &'a Credential,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@@ -111,8 +112,12 @@ impl<'de> Deserialize<'de> for CollectionInfo {
|
||||
}
|
||||
|
||||
impl<'a> Collection<'a> {
|
||||
pub fn new(client: &'a BiliClient, collection: CollectionItem) -> Self {
|
||||
Self { client, collection }
|
||||
pub fn new(client: &'a BiliClient, collection: CollectionItem, credential: &'a Credential) -> Self {
|
||||
Self {
|
||||
client,
|
||||
collection,
|
||||
credential,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_info(&self) -> Result<CollectionInfo> {
|
||||
@@ -126,7 +131,7 @@ impl<'a> Collection<'a> {
|
||||
|
||||
async fn get_series_info(&self) -> Result<Value> {
|
||||
self.client
|
||||
.request(Method::GET, "https://api.bilibili.com/x/series/series")
|
||||
.request(Method::GET, "https://api.bilibili.com/x/series/series", self.credential)
|
||||
.await
|
||||
.query(&[("series_id", self.collection.sid.as_str())])
|
||||
.send()
|
||||
@@ -141,7 +146,11 @@ impl<'a> Collection<'a> {
|
||||
let req = match self.collection.collection_type {
|
||||
CollectionType::Series => self
|
||||
.client
|
||||
.request(Method::GET, "https://api.bilibili.com/x/series/archives")
|
||||
.request(
|
||||
Method::GET,
|
||||
"https://api.bilibili.com/x/series/archives",
|
||||
self.credential,
|
||||
)
|
||||
.await
|
||||
.query(&[("pn", page)])
|
||||
.query(&[
|
||||
@@ -156,6 +165,7 @@ impl<'a> Collection<'a> {
|
||||
.request(
|
||||
Method::GET,
|
||||
"https://api.bilibili.com/x/polymer/web-space/seasons_archives_list",
|
||||
self.credential,
|
||||
)
|
||||
.await
|
||||
.query(&[("page_num", page)])
|
||||
|
||||
@@ -3,10 +3,9 @@ use std::path::PathBuf;
|
||||
use anyhow::Result;
|
||||
use tokio::fs::{self, File};
|
||||
|
||||
use crate::bilibili::PageInfo;
|
||||
use crate::bilibili::danmaku::canvas::CanvasConfig;
|
||||
use crate::bilibili::danmaku::{AssWriter, Danmu};
|
||||
use crate::config::VersionedConfig;
|
||||
use crate::bilibili::{DanmakuOption, PageInfo};
|
||||
|
||||
pub struct DanmakuWriter<'a> {
|
||||
page: &'a PageInfo,
|
||||
@@ -18,12 +17,11 @@ impl<'a> DanmakuWriter<'a> {
|
||||
DanmakuWriter { page, danmaku }
|
||||
}
|
||||
|
||||
pub async fn write(self, path: PathBuf) -> Result<()> {
|
||||
pub async fn write(self, path: PathBuf, danmaku_option: &DanmakuOption) -> Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).await?;
|
||||
}
|
||||
let config = VersionedConfig::get().load_full();
|
||||
let canvas_config = CanvasConfig::new(&config.danmaku_option, self.page);
|
||||
let canvas_config = CanvasConfig::new(danmaku_option, self.page);
|
||||
let mut writer =
|
||||
AssWriter::construct(File::create(path).await?, self.page.name.clone(), canvas_config.clone()).await?;
|
||||
let mut canvas = canvas_config.canvas();
|
||||
|
||||
@@ -6,11 +6,12 @@ use futures::Stream;
|
||||
use reqwest::Method;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::bilibili::{BiliClient, MIXIN_KEY, Validate, VideoInfo, WbiSign};
|
||||
use crate::bilibili::{BiliClient, Credential, MIXIN_KEY, Validate, VideoInfo, WbiSign};
|
||||
|
||||
pub struct Dynamic<'a> {
|
||||
client: &'a BiliClient,
|
||||
pub upper_id: String,
|
||||
credential: &'a Credential,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
@@ -20,8 +21,12 @@ pub struct DynamicItemPublished {
|
||||
}
|
||||
|
||||
impl<'a> Dynamic<'a> {
|
||||
pub fn new(client: &'a BiliClient, upper_id: String) -> Self {
|
||||
Self { client, upper_id }
|
||||
pub fn new(client: &'a BiliClient, upper_id: String, credential: &'a Credential) -> Self {
|
||||
Self {
|
||||
client,
|
||||
upper_id,
|
||||
credential,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_dynamics(&self, offset: Option<String>) -> Result<Value> {
|
||||
@@ -29,6 +34,7 @@ impl<'a> Dynamic<'a> {
|
||||
.request(
|
||||
Method::GET,
|
||||
"https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space",
|
||||
self.credential,
|
||||
)
|
||||
.await
|
||||
.query(&[
|
||||
|
||||
@@ -3,10 +3,11 @@ use async_stream::try_stream;
|
||||
use futures::Stream;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::bilibili::{BiliClient, Validate, VideoInfo};
|
||||
use crate::bilibili::{BiliClient, Credential, Validate, VideoInfo};
|
||||
pub struct FavoriteList<'a> {
|
||||
client: &'a BiliClient,
|
||||
fid: String,
|
||||
credential: &'a Credential,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
@@ -22,14 +23,22 @@ pub struct Upper<T> {
|
||||
pub face: String,
|
||||
}
|
||||
impl<'a> FavoriteList<'a> {
|
||||
pub fn new(client: &'a BiliClient, fid: String) -> Self {
|
||||
Self { client, fid }
|
||||
pub fn new(client: &'a BiliClient, fid: String, credential: &'a Credential) -> Self {
|
||||
Self {
|
||||
client,
|
||||
fid,
|
||||
credential,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_info(&self) -> Result<FavoriteListInfo> {
|
||||
let mut res = self
|
||||
.client
|
||||
.request(reqwest::Method::GET, "https://api.bilibili.com/x/v3/fav/folder/info")
|
||||
.request(
|
||||
reqwest::Method::GET,
|
||||
"https://api.bilibili.com/x/v3/fav/folder/info",
|
||||
self.credential,
|
||||
)
|
||||
.await
|
||||
.query(&[("media_id", &self.fid)])
|
||||
.send()
|
||||
@@ -43,7 +52,11 @@ impl<'a> FavoriteList<'a> {
|
||||
|
||||
async fn get_videos(&self, page: u32) -> Result<Value> {
|
||||
self.client
|
||||
.request(reqwest::Method::GET, "https://api.bilibili.com/x/v3/fav/resource/list")
|
||||
.request(
|
||||
reqwest::Method::GET,
|
||||
"https://api.bilibili.com/x/v3/fav/resource/list",
|
||||
self.credential,
|
||||
)
|
||||
.await
|
||||
.query(&[
|
||||
("media_id", self.fid.as_str()),
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
use anyhow::{Result, ensure};
|
||||
use reqwest::Method;
|
||||
|
||||
use crate::bilibili::{BiliClient, Validate};
|
||||
use crate::config::VersionedConfig;
|
||||
use crate::bilibili::{BiliClient, Credential, Validate};
|
||||
|
||||
pub struct Me<'a> {
|
||||
client: &'a BiliClient,
|
||||
mid: String,
|
||||
credential: &'a Credential,
|
||||
}
|
||||
|
||||
impl<'a> Me<'a> {
|
||||
pub fn new(client: &'a BiliClient) -> Self {
|
||||
Self {
|
||||
client,
|
||||
mid: Self::my_id(),
|
||||
}
|
||||
pub fn new(client: &'a BiliClient, credential: &'a Credential) -> Self {
|
||||
Self { client, credential }
|
||||
}
|
||||
|
||||
pub async fn get_created_favorites(&self) -> Result<Option<Vec<FavoriteItem>>> {
|
||||
ensure!(!self.mid.is_empty(), "未获取到用户 ID,请确保填写设置中的 B 站认证信息");
|
||||
ensure!(
|
||||
!self.mid().is_empty(),
|
||||
"未获取到用户 ID,请确保填写设置中的 B 站认证信息"
|
||||
);
|
||||
let mut resp = self
|
||||
.client
|
||||
.request(Method::GET, "https://api.bilibili.com/x/v3/fav/folder/created/list-all")
|
||||
.request(
|
||||
Method::GET,
|
||||
"https://api.bilibili.com/x/v3/fav/folder/created/list-all",
|
||||
self.credential,
|
||||
)
|
||||
.await
|
||||
.query(&[("up_mid", &self.mid)])
|
||||
.query(&[("up_mid", &self.mid())])
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
@@ -33,12 +37,19 @@ impl<'a> Me<'a> {
|
||||
}
|
||||
|
||||
pub async fn get_followed_collections(&self, page_num: i32, page_size: i32) -> Result<Collections> {
|
||||
ensure!(!self.mid.is_empty(), "未获取到用户 ID,请确保填写设置中的 B 站认证信息");
|
||||
ensure!(
|
||||
!self.mid().is_empty(),
|
||||
"未获取到用户 ID,请确保填写设置中的 B 站认证信息"
|
||||
);
|
||||
let mut resp = self
|
||||
.client
|
||||
.request(Method::GET, "https://api.bilibili.com/x/v3/fav/folder/collected/list")
|
||||
.request(
|
||||
Method::GET,
|
||||
"https://api.bilibili.com/x/v3/fav/folder/collected/list",
|
||||
self.credential,
|
||||
)
|
||||
.await
|
||||
.query(&[("up_mid", self.mid.as_str()), ("platform", "web")])
|
||||
.query(&[("up_mid", self.mid()), ("platform", "web")])
|
||||
.query(&[("pn", page_num), ("ps", page_size)])
|
||||
.send()
|
||||
.await?
|
||||
@@ -50,12 +61,19 @@ impl<'a> Me<'a> {
|
||||
}
|
||||
|
||||
pub async fn get_followed_uppers(&self, page_num: i32, page_size: i32) -> Result<FollowedUppers> {
|
||||
ensure!(!self.mid.is_empty(), "未获取到用户 ID,请确保填写设置中的 B 站认证信息");
|
||||
ensure!(
|
||||
!self.mid().is_empty(),
|
||||
"未获取到用户 ID,请确保填写设置中的 B 站认证信息"
|
||||
);
|
||||
let mut resp = self
|
||||
.client
|
||||
.request(Method::GET, "https://api.bilibili.com/x/relation/followings")
|
||||
.request(
|
||||
Method::GET,
|
||||
"https://api.bilibili.com/x/relation/followings",
|
||||
self.credential,
|
||||
)
|
||||
.await
|
||||
.query(&[("vmid", self.mid.as_str())])
|
||||
.query(&[("vmid", self.mid())])
|
||||
.query(&[("pn", page_num), ("ps", page_size)])
|
||||
.send()
|
||||
.await?
|
||||
@@ -66,8 +84,8 @@ impl<'a> Me<'a> {
|
||||
Ok(serde_json::from_value(resp["data"].take())?)
|
||||
}
|
||||
|
||||
fn my_id() -> String {
|
||||
VersionedConfig::get().load().credential.dedeuserid.clone()
|
||||
fn mid(&self) -> &str {
|
||||
&self.credential.dedeuserid
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use chrono::serde::ts_seconds;
|
||||
use chrono::{DateTime, Utc};
|
||||
pub use client::{BiliClient, Client};
|
||||
pub use collection::{Collection, CollectionItem, CollectionType};
|
||||
pub use credential::{Credential, WbiImg};
|
||||
pub use credential::Credential;
|
||||
pub use danmaku::DanmakuOption;
|
||||
pub use dynamic::Dynamic;
|
||||
pub use error::BiliError;
|
||||
@@ -208,10 +208,15 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_video_info_type() -> Result<()> {
|
||||
VersionedConfig::init_for_test(&setup_database(Path::new("./test.sqlite")).await?).await?;
|
||||
let credential = &VersionedConfig::get().read().credential;
|
||||
init_logger("None,bili_sync=debug", None);
|
||||
let bili_client = BiliClient::new();
|
||||
// 请求 UP 主视频必须要获取 mixin key,使用 key 计算请求参数的签名,否则直接提示权限不足返回空
|
||||
let mixin_key = bili_client.wbi_img().await?.into_mixin_key().context("no mixin key")?;
|
||||
let mixin_key = bili_client
|
||||
.wbi_img(credential)
|
||||
.await?
|
||||
.into_mixin_key()
|
||||
.context("no mixin key")?;
|
||||
set_global_mixin_key(mixin_key);
|
||||
let collection = Collection::new(
|
||||
&bili_client,
|
||||
@@ -220,6 +225,7 @@ mod tests {
|
||||
sid: "4523".to_string(),
|
||||
collection_type: CollectionType::Season,
|
||||
},
|
||||
&credential,
|
||||
);
|
||||
let videos = collection
|
||||
.into_video_stream()
|
||||
@@ -230,7 +236,7 @@ mod tests {
|
||||
assert!(videos.iter().all(|v| matches!(v, VideoInfo::Collection { .. })));
|
||||
assert!(videos.iter().rev().is_sorted_by_key(|v| v.release_datetime()));
|
||||
// 测试收藏夹
|
||||
let favorite = FavoriteList::new(&bili_client, "3144336058".to_string());
|
||||
let favorite = FavoriteList::new(&bili_client, "3144336058".to_string(), &credential);
|
||||
let videos = favorite
|
||||
.into_video_stream()
|
||||
.take(20)
|
||||
@@ -240,7 +246,7 @@ mod tests {
|
||||
assert!(videos.iter().all(|v| matches!(v, VideoInfo::Favorite { .. })));
|
||||
assert!(videos.iter().rev().is_sorted_by_key(|v| v.release_datetime()));
|
||||
// 测试稍后再看
|
||||
let watch_later = WatchLater::new(&bili_client);
|
||||
let watch_later = WatchLater::new(&bili_client, &credential);
|
||||
let videos = watch_later
|
||||
.into_video_stream()
|
||||
.take(20)
|
||||
@@ -250,7 +256,7 @@ mod tests {
|
||||
assert!(videos.iter().all(|v| matches!(v, VideoInfo::WatchLater { .. })));
|
||||
assert!(videos.iter().rev().is_sorted_by_key(|v| v.release_datetime()));
|
||||
// 测试投稿
|
||||
let submission = Submission::new(&bili_client, "956761".to_string());
|
||||
let submission = Submission::new(&bili_client, "956761".to_string(), &credential);
|
||||
let videos = submission
|
||||
.into_video_stream()
|
||||
.take(20)
|
||||
@@ -260,7 +266,7 @@ mod tests {
|
||||
assert!(videos.iter().all(|v| matches!(v, VideoInfo::Submission { .. })));
|
||||
assert!(videos.iter().rev().is_sorted_by_key(|v| v.release_datetime()));
|
||||
// 测试动态
|
||||
let dynamic = Dynamic::new(&bili_client, "659898".to_string());
|
||||
let dynamic = Dynamic::new(&bili_client, "659898".to_string(), &credential);
|
||||
let videos = dynamic
|
||||
.into_video_stream()
|
||||
.take(20)
|
||||
@@ -275,10 +281,16 @@ mod tests {
|
||||
#[ignore = "only for manual test"]
|
||||
#[tokio::test]
|
||||
async fn test_subtitle_parse() -> Result<()> {
|
||||
VersionedConfig::init_for_test(&setup_database(Path::new("./test.sqlite")).await?).await?;
|
||||
let credential = &VersionedConfig::get().read().credential;
|
||||
let bili_client = BiliClient::new();
|
||||
let mixin_key = bili_client.wbi_img().await?.into_mixin_key().context("no mixin key")?;
|
||||
let mixin_key = bili_client
|
||||
.wbi_img(credential)
|
||||
.await?
|
||||
.into_mixin_key()
|
||||
.context("no mixin key")?;
|
||||
set_global_mixin_key(mixin_key);
|
||||
let video = Video::new(&bili_client, "BV1gLfnY8E6D".to_string());
|
||||
let video = Video::new(&bili_client, "BV1gLfnY8E6D".to_string(), &credential);
|
||||
let pages = video.get_pages().await?;
|
||||
println!("pages: {:?}", pages);
|
||||
let subtitles = video.get_subtitles(&pages[0]).await?;
|
||||
@@ -296,15 +308,20 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_upower_parse() -> Result<()> {
|
||||
VersionedConfig::init_for_test(&setup_database(Path::new("./test.sqlite")).await?).await?;
|
||||
let credential = &VersionedConfig::get().read().credential;
|
||||
let bili_client = BiliClient::new();
|
||||
let mixin_key = bili_client.wbi_img().await?.into_mixin_key().context("no mixin key")?;
|
||||
let mixin_key = bili_client
|
||||
.wbi_img(credential)
|
||||
.await?
|
||||
.into_mixin_key()
|
||||
.context("no mixin key")?;
|
||||
set_global_mixin_key(mixin_key);
|
||||
for (bvid, (upower_exclusive, upower_play)) in [
|
||||
("BV1HxXwYEEqt", (true, false)), // 充电专享且无权观看
|
||||
("BV16w41187fx", (true, true)), // 充电专享但有权观看
|
||||
("BV1n34jzPEYq", (false, false)), // 普通视频
|
||||
] {
|
||||
let video = Video::new(&bili_client, bvid.to_string());
|
||||
let video = Video::new(&bili_client, bvid.to_string(), credential);
|
||||
let info = video.get_view_info().await?;
|
||||
let VideoInfo::Detail {
|
||||
is_upower_exclusive,
|
||||
@@ -324,15 +341,20 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn test_ep_parse() -> Result<()> {
|
||||
VersionedConfig::init_for_test(&setup_database(Path::new("./test.sqlite")).await?).await?;
|
||||
let credential = &VersionedConfig::get().read().credential;
|
||||
let bili_client = BiliClient::new();
|
||||
let mixin_key = bili_client.wbi_img().await?.into_mixin_key().context("no mixin key")?;
|
||||
let mixin_key = bili_client
|
||||
.wbi_img(credential)
|
||||
.await?
|
||||
.into_mixin_key()
|
||||
.context("no mixin key")?;
|
||||
set_global_mixin_key(mixin_key);
|
||||
for (bvid, redirect_is_none) in [
|
||||
("BV1SF411g796", false), // EP
|
||||
("BV13xtnzPEye", false), // 番剧
|
||||
("BV1kT4NzTEZj", true), // 普通视频
|
||||
] {
|
||||
let video = Video::new(&bili_client, bvid.to_string());
|
||||
let video = Video::new(&bili_client, bvid.to_string(), credential);
|
||||
let info = video.get_view_info().await?;
|
||||
let VideoInfo::Detail { redirect_url, .. } = info else {
|
||||
unreachable!();
|
||||
|
||||
@@ -5,27 +5,36 @@ use reqwest::Method;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::bilibili::favorite_list::Upper;
|
||||
use crate::bilibili::{BiliClient, Dynamic, MIXIN_KEY, Validate, VideoInfo, WbiSign};
|
||||
use crate::bilibili::{BiliClient, Credential, Dynamic, MIXIN_KEY, Validate, VideoInfo, WbiSign};
|
||||
pub struct Submission<'a> {
|
||||
client: &'a BiliClient,
|
||||
pub upper_id: String,
|
||||
credential: &'a Credential,
|
||||
}
|
||||
|
||||
impl<'a> From<Submission<'a>> for Dynamic<'a> {
|
||||
fn from(submission: Submission<'a>) -> Self {
|
||||
Dynamic::new(submission.client, submission.upper_id)
|
||||
Dynamic::new(submission.client, submission.upper_id, submission.credential)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Submission<'a> {
|
||||
pub fn new(client: &'a BiliClient, upper_id: String) -> Self {
|
||||
Self { client, upper_id }
|
||||
pub fn new(client: &'a BiliClient, upper_id: String, credential: &'a Credential) -> Self {
|
||||
Self {
|
||||
client,
|
||||
upper_id,
|
||||
credential,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_info(&self) -> Result<Upper<String>> {
|
||||
let mut res = self
|
||||
.client
|
||||
.request(Method::GET, "https://api.bilibili.com/x/web-interface/card")
|
||||
.request(
|
||||
Method::GET,
|
||||
"https://api.bilibili.com/x/web-interface/card",
|
||||
self.credential,
|
||||
)
|
||||
.await
|
||||
.query(&[("mid", self.upper_id.as_str())])
|
||||
.send()
|
||||
@@ -39,7 +48,11 @@ impl<'a> Submission<'a> {
|
||||
|
||||
async fn get_videos(&self, page: i32) -> Result<Value> {
|
||||
self.client
|
||||
.request(Method::GET, "https://api.bilibili.com/x/space/wbi/arc/search")
|
||||
.request(
|
||||
Method::GET,
|
||||
"https://api.bilibili.com/x/space/wbi/arc/search",
|
||||
self.credential,
|
||||
)
|
||||
.await
|
||||
.query(&[
|
||||
("mid", self.upper_id.as_str()),
|
||||
|
||||
@@ -8,11 +8,12 @@ use crate::bilibili::analyzer::PageAnalyzer;
|
||||
use crate::bilibili::client::BiliClient;
|
||||
use crate::bilibili::danmaku::{DanmakuElem, DanmakuWriter, DmSegMobileReply};
|
||||
use crate::bilibili::subtitle::{SubTitle, SubTitleBody, SubTitleInfo, SubTitlesInfo};
|
||||
use crate::bilibili::{MIXIN_KEY, Validate, VideoInfo, WbiSign};
|
||||
use crate::bilibili::{Credential, MIXIN_KEY, Validate, VideoInfo, WbiSign};
|
||||
|
||||
pub struct Video<'a> {
|
||||
client: &'a BiliClient,
|
||||
pub bvid: String,
|
||||
credential: &'a Credential,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, Default)]
|
||||
@@ -34,15 +35,23 @@ pub struct Dimension {
|
||||
}
|
||||
|
||||
impl<'a> Video<'a> {
|
||||
pub fn new(client: &'a BiliClient, bvid: String) -> Self {
|
||||
Self { client, bvid }
|
||||
pub fn new(client: &'a BiliClient, bvid: String, credential: &'a Credential) -> Self {
|
||||
Self {
|
||||
client,
|
||||
bvid,
|
||||
credential,
|
||||
}
|
||||
}
|
||||
|
||||
/// 直接调用视频信息接口获取详细的视频信息,视频信息中包含了视频的分页信息
|
||||
pub async fn get_view_info(&self) -> Result<VideoInfo> {
|
||||
let mut res = self
|
||||
.client
|
||||
.request(Method::GET, "https://api.bilibili.com/x/web-interface/wbi/view")
|
||||
.request(
|
||||
Method::GET,
|
||||
"https://api.bilibili.com/x/web-interface/wbi/view",
|
||||
self.credential,
|
||||
)
|
||||
.await
|
||||
.query(&[("bvid", &self.bvid)])
|
||||
.wbi_sign(MIXIN_KEY.load().as_deref())?
|
||||
@@ -59,7 +68,11 @@ impl<'a> Video<'a> {
|
||||
pub async fn get_pages(&self) -> Result<Vec<PageInfo>> {
|
||||
let mut res = self
|
||||
.client
|
||||
.request(Method::GET, "https://api.bilibili.com/x/player/pagelist")
|
||||
.request(
|
||||
Method::GET,
|
||||
"https://api.bilibili.com/x/player/pagelist",
|
||||
self.credential,
|
||||
)
|
||||
.await
|
||||
.query(&[("bvid", &self.bvid)])
|
||||
.send()
|
||||
@@ -74,7 +87,11 @@ impl<'a> Video<'a> {
|
||||
pub async fn get_tags(&self) -> Result<Vec<String>> {
|
||||
let res = self
|
||||
.client
|
||||
.request(Method::GET, "https://api.bilibili.com/x/web-interface/view/detail/tag")
|
||||
.request(
|
||||
Method::GET,
|
||||
"https://api.bilibili.com/x/web-interface/view/detail/tag",
|
||||
self.credential,
|
||||
)
|
||||
.await
|
||||
.query(&[("bvid", &self.bvid)])
|
||||
.send()
|
||||
@@ -105,7 +122,11 @@ impl<'a> Video<'a> {
|
||||
async fn get_danmaku_segment(&self, page: &PageInfo, segment_idx: i64) -> Result<Vec<DanmakuElem>> {
|
||||
let mut res = self
|
||||
.client
|
||||
.request(Method::GET, "https://api.bilibili.com/x/v2/dm/wbi/web/seg.so")
|
||||
.request(
|
||||
Method::GET,
|
||||
"https://api.bilibili.com/x/v2/dm/wbi/web/seg.so",
|
||||
self.credential,
|
||||
)
|
||||
.await
|
||||
.query(&[("type", 1), ("oid", page.cid), ("segment_index", segment_idx)])
|
||||
.wbi_sign(MIXIN_KEY.load().as_deref())?
|
||||
@@ -126,7 +147,11 @@ impl<'a> Video<'a> {
|
||||
pub async fn get_page_analyzer(&self, page: &PageInfo) -> Result<PageAnalyzer> {
|
||||
let mut res = self
|
||||
.client
|
||||
.request(Method::GET, "https://api.bilibili.com/x/player/wbi/playurl")
|
||||
.request(
|
||||
Method::GET,
|
||||
"https://api.bilibili.com/x/player/wbi/playurl",
|
||||
self.credential,
|
||||
)
|
||||
.await
|
||||
.query(&[
|
||||
("bvid", self.bvid.as_str()),
|
||||
@@ -149,7 +174,7 @@ impl<'a> Video<'a> {
|
||||
pub async fn get_subtitles(&self, page: &PageInfo) -> Result<Vec<SubTitle>> {
|
||||
let mut res = self
|
||||
.client
|
||||
.request(Method::GET, "https://api.bilibili.com/x/player/wbi/v2")
|
||||
.request(Method::GET, "https://api.bilibili.com/x/player/wbi/v2", self.credential)
|
||||
.await
|
||||
.query(&[("bvid", self.bvid.as_str())])
|
||||
.query(&[("cid", page.cid)])
|
||||
|
||||
@@ -3,19 +3,24 @@ use async_stream::try_stream;
|
||||
use futures::Stream;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::bilibili::{BiliClient, Validate, VideoInfo};
|
||||
use crate::bilibili::{BiliClient, Credential, Validate, VideoInfo};
|
||||
pub struct WatchLater<'a> {
|
||||
client: &'a BiliClient,
|
||||
credential: &'a Credential,
|
||||
}
|
||||
|
||||
impl<'a> WatchLater<'a> {
|
||||
pub fn new(client: &'a BiliClient) -> Self {
|
||||
Self { client }
|
||||
pub fn new(client: &'a BiliClient, credential: &'a Credential) -> Self {
|
||||
Self { client, credential }
|
||||
}
|
||||
|
||||
async fn get_videos(&self) -> Result<Value> {
|
||||
self.client
|
||||
.request(reqwest::Method::GET, "https://api.bilibili.com/x/v2/history/toview")
|
||||
.request(
|
||||
reqwest::Method::GET,
|
||||
"https://api.bilibili.com/x/v2/history/toview",
|
||||
self.credential,
|
||||
)
|
||||
.await
|
||||
.send()
|
||||
.await?
|
||||
|
||||
Reference in New Issue
Block a user