mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-09-07 00:17:24 +08:00
feat: 修改各处定义,初步实现读取收藏夹功能
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_stream::stream;
|
use async_stream::stream;
|
||||||
|
use chrono::serde::ts_seconds;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use futures_core::stream::Stream;
|
use futures_core::stream::Stream;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
@@ -11,10 +13,9 @@ pub struct FavoriteList {
|
|||||||
fid: String,
|
fid: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
pub struct FavoriteListInfo {
|
pub struct FavoriteListInfo {
|
||||||
pub id: u64,
|
pub id: i32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,19 +23,23 @@ pub struct FavoriteListInfo {
|
|||||||
pub struct VideoInfo {
|
pub struct VideoInfo {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
pub vtype: u64,
|
pub vtype: i32,
|
||||||
pub bvid: String,
|
pub bvid: String,
|
||||||
pub intro: String,
|
pub intro: String,
|
||||||
pub cover: String,
|
pub cover: String,
|
||||||
pub upper: Upper,
|
pub upper: Upper,
|
||||||
pub ctime: u64,
|
#[serde(with = "ts_seconds")]
|
||||||
pub fav_time: u64,
|
pub ctime: DateTime<Utc>,
|
||||||
pub pubtime: u64,
|
#[serde(with = "ts_seconds")]
|
||||||
|
pub fav_time: DateTime<Utc>,
|
||||||
|
#[serde(with = "ts_seconds")]
|
||||||
|
pub pubtime: DateTime<Utc>,
|
||||||
|
pub attr: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
pub struct Upper {
|
pub struct Upper {
|
||||||
pub mid: u64,
|
pub mid: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
impl FavoriteList {
|
impl FavoriteList {
|
||||||
|
|||||||
+1
-1
@@ -10,4 +10,4 @@ pub use analyzer::{
|
|||||||
pub use client::{BiliClient, Client};
|
pub use client::{BiliClient, Client};
|
||||||
pub use credential::Credential;
|
pub use credential::Credential;
|
||||||
pub use favorite_list::{FavoriteList, FavoriteListInfo, VideoInfo};
|
pub use favorite_list::{FavoriteList, FavoriteListInfo, VideoInfo};
|
||||||
pub use video::Video;
|
pub use video::{PageInfo, Video};
|
||||||
|
|||||||
@@ -27,15 +27,14 @@ pub struct Tag {
|
|||||||
pub tag_name: String,
|
pub tag_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
pub struct Page {
|
pub struct PageInfo {
|
||||||
cid: u64,
|
pub cid: i32,
|
||||||
page: u32,
|
pub page: i32,
|
||||||
#[serde(rename = "part")]
|
#[serde(rename = "part")]
|
||||||
name: String,
|
pub name: String,
|
||||||
#[serde(default = "String::new")]
|
#[serde(default = "String::new")]
|
||||||
first_frame: String, // 可能不存在,默认填充为空
|
pub first_frame: String, // 可能不存在,默认填充为空
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Video {
|
impl Video {
|
||||||
@@ -44,7 +43,7 @@ impl Video {
|
|||||||
Self { client, aid, bvid }
|
Self { client, aid, bvid }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_pages(&self) -> Result<Vec<Page>> {
|
pub async fn get_pages(&self) -> Result<Vec<PageInfo>> {
|
||||||
let mut res = self
|
let mut res = self
|
||||||
.client
|
.client
|
||||||
.request(Method::GET, "https://api.bilibili.com/x/player/pagelist")
|
.request(Method::GET, "https://api.bilibili.com/x/player/pagelist")
|
||||||
@@ -71,7 +70,7 @@ impl Video {
|
|||||||
Ok(serde_json::from_value(res["data"].take())?)
|
Ok(serde_json::from_value(res["data"].take())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_page_analyzer(&self, page: &Page) -> Result<PageAnalyzer> {
|
pub async fn get_page_analyzer(&self, page: &PageInfo) -> Result<PageAnalyzer> {
|
||||||
let mut res = self
|
let mut res = self
|
||||||
.client
|
.client
|
||||||
.request(Method::GET, "https://api.bilibili.com/x/player/wbi/playurl")
|
.request(Method::GET, "https://api.bilibili.com/x/player/wbi/playurl")
|
||||||
|
|||||||
+17
-97
@@ -1,15 +1,12 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use entity::*;
|
|
||||||
use futures_util::{pin_mut, StreamExt};
|
use futures_util::{pin_mut, StreamExt};
|
||||||
use migration::OnConflict;
|
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use sea_orm::ActiveValue::Set;
|
|
||||||
use sea_orm::QuerySelect;
|
|
||||||
|
|
||||||
use crate::bilibili::{BiliClient, FavoriteList, FavoriteListInfo, VideoInfo};
|
use crate::bilibili::{BiliClient, FavoriteList, Video};
|
||||||
|
use crate::core::utils::{
|
||||||
|
create_video_pages, create_videos, exists_bvids_favtime, filter_videos, handle_favorite_info,
|
||||||
|
};
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
|
|
||||||
pub async fn process_favorite(
|
pub async fn process_favorite(
|
||||||
@@ -29,100 +26,23 @@ pub async fn process_favorite(
|
|||||||
while let Some(videos_info) = video_stream.next().await {
|
while let Some(videos_info) = video_stream.next().await {
|
||||||
let exist_bvids_pubtimes =
|
let exist_bvids_pubtimes =
|
||||||
exists_bvids_favtime(&videos_info, fid, connection.as_ref()).await?;
|
exists_bvids_favtime(&videos_info, fid, connection.as_ref()).await?;
|
||||||
let video_info_to_create = videos_info
|
let should_break = videos_info
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|v| !exist_bvids_pubtimes.contains(&(v.bvid.clone(), v.fav_time.to_string())))
|
// 出现 bvid 和 fav_time 都相同的记录,说明已经到达了上次处理到的位置
|
||||||
.collect::<Vec<&VideoInfo>>();
|
.any(|v| exist_bvids_pubtimes.contains(&(v.bvid.clone(), v.fav_time.naive_utc())));
|
||||||
let len = video_info_to_create.len();
|
create_videos(&videos_info, &favorite_obj, connection.as_ref()).await?;
|
||||||
if !video_info_to_create.is_empty() {
|
let all_unprocessed_videos =
|
||||||
create_videos(video_info_to_create, &favorite_obj, connection.as_ref()).await?;
|
filter_videos(&videos_info, &favorite_obj, true, true, connection.as_ref()).await?;
|
||||||
|
if !all_unprocessed_videos.is_empty() {
|
||||||
|
for video in all_unprocessed_videos {
|
||||||
|
let bili_video = Video::new(bili_client.clone(), video.bvid.clone());
|
||||||
|
let pages = bili_video.get_pages().await?;
|
||||||
|
create_video_pages(&pages, &video, connection.as_ref()).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if videos_info.len() != len {
|
if should_break {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据获得的收藏夹信息,插入或更新数据库中的收藏夹,并返回收藏夹对象
|
|
||||||
async fn handle_favorite_info(
|
|
||||||
info: &FavoriteListInfo,
|
|
||||||
connection: &DatabaseConnection,
|
|
||||||
) -> Result<favorite::Model> {
|
|
||||||
Ok(favorite::Entity::insert(favorite::ActiveModel {
|
|
||||||
f_id: Set(info.id as i32),
|
|
||||||
name: Set(info.title.to_string()),
|
|
||||||
path: Set("/home/amtoaer/Documents/code/rust/bili-sync/video".to_string()),
|
|
||||||
enabled: Set(true),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.on_conflict(
|
|
||||||
OnConflict::column(favorite::Column::FId)
|
|
||||||
.update_column(favorite::Column::Name)
|
|
||||||
.update_column(favorite::Column::Path)
|
|
||||||
.update_column(favorite::Column::Enabled)
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.exec_with_returning(connection)
|
|
||||||
.await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取数据库中存在的与该视频 favorite_id 和 bvid 重合的视频
|
|
||||||
async fn exists_bvids_favtime(
|
|
||||||
videos_info: &[VideoInfo],
|
|
||||||
fid: i32,
|
|
||||||
connection: &DatabaseConnection,
|
|
||||||
) -> Result<HashSet<(String, String)>> {
|
|
||||||
let bvids = videos_info
|
|
||||||
.iter()
|
|
||||||
.map(|v| v.bvid.clone())
|
|
||||||
.collect::<Vec<String>>();
|
|
||||||
let exist_bvid_favtime = video::Entity::find()
|
|
||||||
.filter(
|
|
||||||
video::Column::FavoriteId
|
|
||||||
.eq(fid)
|
|
||||||
.and(video::Column::Bvid.is_in(bvids)),
|
|
||||||
)
|
|
||||||
.select_only()
|
|
||||||
.columns([video::Column::Bvid, video::Column::Favtime])
|
|
||||||
.all(connection)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|v| (v.bvid, v.favtime))
|
|
||||||
.collect::<HashSet<(String, String)>>();
|
|
||||||
Ok(exist_bvid_favtime)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_videos(
|
|
||||||
videos_info: Vec<&VideoInfo>,
|
|
||||||
favorite_obj: &favorite::Model,
|
|
||||||
connection: &DatabaseConnection,
|
|
||||||
) -> Result<u64> {
|
|
||||||
let video_models = videos_info
|
|
||||||
.iter()
|
|
||||||
.map(move |v| video::ActiveModel {
|
|
||||||
favorite_id: Set(favorite_obj.id),
|
|
||||||
bvid: Set(v.bvid.clone()),
|
|
||||||
path: Set(Path::new(favorite_obj.path.as_str())
|
|
||||||
.join(&v.title)
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
.to_string()),
|
|
||||||
name: Set(v.title.clone()),
|
|
||||||
category: Set(v.vtype.to_string()),
|
|
||||||
intro: Set(v.intro.clone()),
|
|
||||||
cover: Set(v.cover.clone()),
|
|
||||||
ctime: Set(v.ctime.to_string()),
|
|
||||||
pubtime: Set(v.pubtime.to_string()),
|
|
||||||
favtime: Set(v.fav_time.to_string()),
|
|
||||||
downloaded: Set(false),
|
|
||||||
valid: Set(true),
|
|
||||||
tags: Set("[]".to_string()),
|
|
||||||
single_page: Set(false),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.collect::<Vec<video::ActiveModel>>();
|
|
||||||
Ok(video::Entity::insert_many(video_models)
|
|
||||||
.exec_without_returning(connection)
|
|
||||||
.await?)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
pub mod command;
|
pub mod command;
|
||||||
|
pub mod utils;
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use entity::*;
|
||||||
|
use migration::OnConflict;
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
|
use sea_orm::QuerySelect;
|
||||||
|
|
||||||
|
use crate::bilibili::{FavoriteListInfo, PageInfo, VideoInfo};
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
// 根据获得的收藏夹信息,插入或更新数据库中的收藏夹,并返回收藏夹对象
|
||||||
|
pub async fn handle_favorite_info(
|
||||||
|
info: &FavoriteListInfo,
|
||||||
|
connection: &DatabaseConnection,
|
||||||
|
) -> Result<favorite::Model> {
|
||||||
|
favorite::Entity::insert(favorite::ActiveModel {
|
||||||
|
f_id: Set(info.id),
|
||||||
|
name: Set(info.title.to_string()),
|
||||||
|
path: Set("/home/amtoaer/Documents/code/rust/bili-sync/video".to_string()),
|
||||||
|
enabled: Set(true),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.on_conflict(
|
||||||
|
OnConflict::column(favorite::Column::FId)
|
||||||
|
.update_columns([
|
||||||
|
favorite::Column::Name,
|
||||||
|
favorite::Column::Path,
|
||||||
|
favorite::Column::Enabled,
|
||||||
|
])
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.exec(connection)
|
||||||
|
.await?;
|
||||||
|
Ok(favorite::Entity::find()
|
||||||
|
.filter(favorite::Column::FId.eq(info.id))
|
||||||
|
.one(connection)
|
||||||
|
.await?
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据库中存在的与该视频 favorite_id 和 bvid 重合的视频
|
||||||
|
pub async fn exists_bvids_favtime(
|
||||||
|
videos_info: &[VideoInfo],
|
||||||
|
fid: i32,
|
||||||
|
connection: &DatabaseConnection,
|
||||||
|
) -> Result<HashSet<(String, DateTime)>> {
|
||||||
|
let bvids = videos_info
|
||||||
|
.iter()
|
||||||
|
.map(|v| v.bvid.clone())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
let exist_bvid_favtime = video::Entity::find()
|
||||||
|
.filter(
|
||||||
|
video::Column::FavoriteId
|
||||||
|
.eq(fid)
|
||||||
|
.and(video::Column::Bvid.is_in(bvids)),
|
||||||
|
)
|
||||||
|
.select_only()
|
||||||
|
.columns([video::Column::Bvid, video::Column::Favtime])
|
||||||
|
.all(connection)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| (v.bvid, v.favtime))
|
||||||
|
.collect::<HashSet<(String, DateTime)>>();
|
||||||
|
Ok(exist_bvid_favtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试创建 Video Model,如果发生冲突则忽略
|
||||||
|
pub async fn create_videos(
|
||||||
|
videos_info: &[VideoInfo],
|
||||||
|
favorite_obj: &favorite::Model,
|
||||||
|
connection: &DatabaseConnection,
|
||||||
|
) -> Result<()> {
|
||||||
|
let video_models = videos_info
|
||||||
|
.iter()
|
||||||
|
.map(move |v| video::ActiveModel {
|
||||||
|
favorite_id: Set(favorite_obj.id),
|
||||||
|
bvid: Set(v.bvid.clone()),
|
||||||
|
path: Set(Path::new(favorite_obj.path.as_str())
|
||||||
|
.join(&v.title)
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_string()),
|
||||||
|
name: Set(v.title.clone()),
|
||||||
|
category: Set(v.vtype),
|
||||||
|
intro: Set(v.intro.clone()),
|
||||||
|
cover: Set(v.cover.clone()),
|
||||||
|
ctime: Set(v.ctime.naive_utc()),
|
||||||
|
pubtime: Set(v.pubtime.naive_utc()),
|
||||||
|
favtime: Set(v.fav_time.naive_utc()),
|
||||||
|
handled: Set(false),
|
||||||
|
valid: Set(v.attr == 0),
|
||||||
|
tags: Set(None),
|
||||||
|
single_page: Set(None),
|
||||||
|
upper_id: Set(v.upper.mid),
|
||||||
|
upper_name: Set(v.upper.name.clone()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect::<Vec<video::ActiveModel>>();
|
||||||
|
video::Entity::insert_many(video_models)
|
||||||
|
.on_conflict(
|
||||||
|
OnConflict::columns([video::Column::FavoriteId, video::Column::Bvid])
|
||||||
|
.do_nothing()
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.exec(connection)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选所有符合条件的视频
|
||||||
|
pub async fn filter_videos(
|
||||||
|
videos_info: &[VideoInfo],
|
||||||
|
favorite_obj: &favorite::Model,
|
||||||
|
only_unhandled: bool,
|
||||||
|
only_no_page: bool,
|
||||||
|
connection: &DatabaseConnection,
|
||||||
|
) -> Result<Vec<video::Model>> {
|
||||||
|
let bvids = videos_info
|
||||||
|
.iter()
|
||||||
|
.map(|v| v.bvid.clone())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
let mut condition = video::Column::FavoriteId
|
||||||
|
.eq(favorite_obj.id)
|
||||||
|
.and(video::Column::Bvid.is_in(bvids))
|
||||||
|
.and(video::Column::Valid.eq(true));
|
||||||
|
if only_unhandled {
|
||||||
|
condition = condition.and(video::Column::Handled.eq(false));
|
||||||
|
}
|
||||||
|
if only_no_page {
|
||||||
|
condition = condition.and(video::Column::SinglePage.is_null());
|
||||||
|
}
|
||||||
|
Ok(video::Entity::find()
|
||||||
|
.filter(condition)
|
||||||
|
.all(connection)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_video_pages(
|
||||||
|
pages: &[PageInfo],
|
||||||
|
video_obj: &video::Model,
|
||||||
|
connection: &DatabaseConnection,
|
||||||
|
) -> Result<()> {
|
||||||
|
let page_models = pages
|
||||||
|
.iter()
|
||||||
|
.map(move |p| page::ActiveModel {
|
||||||
|
video_id: Set(video_obj.id),
|
||||||
|
cid: Set(p.cid),
|
||||||
|
pid: Set(p.page),
|
||||||
|
name: Set(p.name.clone()),
|
||||||
|
path: Set(Path::new(video_obj.path.as_str())
|
||||||
|
.join(&p.name)
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_string()),
|
||||||
|
image: Set(p.first_frame.clone()),
|
||||||
|
valid: Set(video_obj.valid),
|
||||||
|
download_status: Set(0),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect::<Vec<page::ActiveModel>>();
|
||||||
|
page::Entity::insert_many(page_models)
|
||||||
|
.on_conflict(
|
||||||
|
OnConflict::columns([page::Column::VideoId, page::Column::Pid])
|
||||||
|
.do_nothing()
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.exec(connection)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
+5
-2
@@ -10,8 +10,11 @@ async fn main() -> ! {
|
|||||||
let bili_client = Arc::new(BiliClient::new(None));
|
let bili_client = Arc::new(BiliClient::new(None));
|
||||||
loop {
|
loop {
|
||||||
for fid in [52642258] {
|
for fid in [52642258] {
|
||||||
let _ = process_favorite(bili_client.clone(), fid, connection.clone()).await;
|
let res = process_favorite(bili_client.clone(), fid, connection.clone()).await;
|
||||||
|
if let Err(e) = res {
|
||||||
|
eprintln!("Error: {:?}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
|
tokio::time::sleep(std::time::Duration::from_secs(600)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user