mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-05-06 20:42:48 +08:00
chore: 将 video list model / video list 重命名为 video source (#260)
This commit is contained in:
@@ -9,10 +9,10 @@ use sea_orm::sea_query::{OnConflict, SimpleExpr};
|
|||||||
use sea_orm::ActiveValue::Set;
|
use sea_orm::ActiveValue::Set;
|
||||||
use sea_orm::{DatabaseConnection, Unchanged};
|
use sea_orm::{DatabaseConnection, Unchanged};
|
||||||
|
|
||||||
use crate::adapter::{VideoListModel, VideoListModelEnum, _ActiveModel};
|
use crate::adapter::{VideoSource, VideoSourceEnum, _ActiveModel};
|
||||||
use crate::bilibili::{BiliClient, Collection, CollectionItem, CollectionType, VideoInfo};
|
use crate::bilibili::{BiliClient, Collection, CollectionItem, CollectionType, VideoInfo};
|
||||||
|
|
||||||
impl VideoListModel for collection::Model {
|
impl VideoSource for collection::Model {
|
||||||
fn filter_expr(&self) -> SimpleExpr {
|
fn filter_expr(&self) -> SimpleExpr {
|
||||||
video::Column::CollectionId.eq(self.id)
|
video::Column::CollectionId.eq(self.id)
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ pub(super) async fn collection_from<'a>(
|
|||||||
bili_client: &'a BiliClient,
|
bili_client: &'a BiliClient,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<(
|
) -> Result<(
|
||||||
VideoListModelEnum,
|
VideoSourceEnum,
|
||||||
Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a + Send>>,
|
Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a + Send>>,
|
||||||
)> {
|
)> {
|
||||||
let collection = Collection::new(bili_client, collection_item);
|
let collection = Collection::new(bili_client, collection_item);
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ use sea_orm::sea_query::{OnConflict, SimpleExpr};
|
|||||||
use sea_orm::ActiveValue::Set;
|
use sea_orm::ActiveValue::Set;
|
||||||
use sea_orm::{DatabaseConnection, Unchanged};
|
use sea_orm::{DatabaseConnection, Unchanged};
|
||||||
|
|
||||||
use crate::adapter::{VideoListModel, VideoListModelEnum, _ActiveModel};
|
use crate::adapter::{VideoSource, VideoSourceEnum, _ActiveModel};
|
||||||
use crate::bilibili::{BiliClient, FavoriteList, VideoInfo};
|
use crate::bilibili::{BiliClient, FavoriteList, VideoInfo};
|
||||||
|
|
||||||
impl VideoListModel for favorite::Model {
|
impl VideoSource for favorite::Model {
|
||||||
fn filter_expr(&self) -> SimpleExpr {
|
fn filter_expr(&self) -> SimpleExpr {
|
||||||
video::Column::FavoriteId.eq(self.id)
|
video::Column::FavoriteId.eq(self.id)
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ pub(super) async fn favorite_from<'a>(
|
|||||||
bili_client: &'a BiliClient,
|
bili_client: &'a BiliClient,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<(
|
) -> Result<(
|
||||||
VideoListModelEnum,
|
VideoSourceEnum,
|
||||||
Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a + Send>>,
|
Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a + Send>>,
|
||||||
)> {
|
)> {
|
||||||
let favorite = FavoriteList::new(bili_client, fid.to_owned());
|
let favorite = FavoriteList::new(bili_client, fid.to_owned());
|
||||||
|
|||||||
@@ -26,15 +26,15 @@ use crate::adapter::watch_later::watch_later_from;
|
|||||||
use crate::bilibili::{BiliClient, CollectionItem, VideoInfo};
|
use crate::bilibili::{BiliClient, CollectionItem, VideoInfo};
|
||||||
|
|
||||||
#[enum_dispatch]
|
#[enum_dispatch]
|
||||||
pub enum VideoListModelEnum {
|
pub enum VideoSourceEnum {
|
||||||
Favorite,
|
Favorite,
|
||||||
Collection,
|
Collection,
|
||||||
Submission,
|
Submission,
|
||||||
WatchLater,
|
WatchLater,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[enum_dispatch(VideoListModelEnum)]
|
#[enum_dispatch(VideoSourceEnum)]
|
||||||
pub trait VideoListModel {
|
pub trait VideoSource {
|
||||||
/// 获取特定视频列表的筛选条件
|
/// 获取特定视频列表的筛选条件
|
||||||
fn filter_expr(&self) -> SimpleExpr;
|
fn filter_expr(&self) -> SimpleExpr;
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ pub trait VideoListModel {
|
|||||||
fn get_latest_row_at(&self) -> DateTime;
|
fn get_latest_row_at(&self) -> DateTime;
|
||||||
|
|
||||||
/// 更新视频 model 中记录的最新时间,此处返回需要更新的 ActiveModel,接着调用 save 方法执行保存
|
/// 更新视频 model 中记录的最新时间,此处返回需要更新的 ActiveModel,接着调用 save 方法执行保存
|
||||||
/// 不同 VideoListModel 返回的类型不同,为了 VideoListModel 的 object safety 不能使用 impl Trait
|
/// 不同 VideoSource 返回的类型不同,为了 VideoSource 的 object safety 不能使用 impl Trait
|
||||||
/// Box<dyn ActiveModelTrait> 又提示 ActiveModelTrait 没有 object safety,因此手写一个 Enum 静态分发
|
/// Box<dyn ActiveModelTrait> 又提示 ActiveModelTrait 没有 object safety,因此手写一个 Enum 静态分发
|
||||||
fn update_latest_row_at(&self, datetime: DateTime) -> _ActiveModel;
|
fn update_latest_row_at(&self, datetime: DateTime) -> _ActiveModel;
|
||||||
|
|
||||||
@@ -79,13 +79,13 @@ pub enum Args<'a> {
|
|||||||
Submission { upper_id: &'a str },
|
Submission { upper_id: &'a str },
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn video_list_from<'a>(
|
pub async fn video_source_from<'a>(
|
||||||
args: Args<'a>,
|
args: Args<'a>,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
bili_client: &'a BiliClient,
|
bili_client: &'a BiliClient,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<(
|
) -> Result<(
|
||||||
VideoListModelEnum,
|
VideoSourceEnum,
|
||||||
Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a + Send>>,
|
Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a + Send>>,
|
||||||
)> {
|
)> {
|
||||||
match args {
|
match args {
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ use sea_orm::sea_query::{OnConflict, SimpleExpr};
|
|||||||
use sea_orm::ActiveValue::Set;
|
use sea_orm::ActiveValue::Set;
|
||||||
use sea_orm::{DatabaseConnection, Unchanged};
|
use sea_orm::{DatabaseConnection, Unchanged};
|
||||||
|
|
||||||
use crate::adapter::{VideoListModel, VideoListModelEnum, _ActiveModel};
|
use crate::adapter::{VideoSource, VideoSourceEnum, _ActiveModel};
|
||||||
use crate::bilibili::{BiliClient, Submission, VideoInfo};
|
use crate::bilibili::{BiliClient, Submission, VideoInfo};
|
||||||
|
|
||||||
impl VideoListModel for submission::Model {
|
impl VideoSource for submission::Model {
|
||||||
fn filter_expr(&self) -> SimpleExpr {
|
fn filter_expr(&self) -> SimpleExpr {
|
||||||
video::Column::SubmissionId.eq(self.id)
|
video::Column::SubmissionId.eq(self.id)
|
||||||
}
|
}
|
||||||
@@ -68,7 +68,7 @@ pub(super) async fn submission_from<'a>(
|
|||||||
bili_client: &'a BiliClient,
|
bili_client: &'a BiliClient,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<(
|
) -> Result<(
|
||||||
VideoListModelEnum,
|
VideoSourceEnum,
|
||||||
Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a + Send>>,
|
Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a + Send>>,
|
||||||
)> {
|
)> {
|
||||||
let submission = Submission::new(bili_client, upper_id.to_owned());
|
let submission = Submission::new(bili_client, upper_id.to_owned());
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ use sea_orm::sea_query::{OnConflict, SimpleExpr};
|
|||||||
use sea_orm::ActiveValue::Set;
|
use sea_orm::ActiveValue::Set;
|
||||||
use sea_orm::{DatabaseConnection, Unchanged};
|
use sea_orm::{DatabaseConnection, Unchanged};
|
||||||
|
|
||||||
use crate::adapter::{VideoListModel, VideoListModelEnum, _ActiveModel};
|
use crate::adapter::{VideoSource, VideoSourceEnum, _ActiveModel};
|
||||||
use crate::bilibili::{BiliClient, VideoInfo, WatchLater};
|
use crate::bilibili::{BiliClient, VideoInfo, WatchLater};
|
||||||
|
|
||||||
impl VideoListModel for watch_later::Model {
|
impl VideoSource for watch_later::Model {
|
||||||
fn filter_expr(&self) -> SimpleExpr {
|
fn filter_expr(&self) -> SimpleExpr {
|
||||||
video::Column::WatchLaterId.eq(self.id)
|
video::Column::WatchLaterId.eq(self.id)
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ pub(super) async fn watch_later_from<'a>(
|
|||||||
bili_client: &'a BiliClient,
|
bili_client: &'a BiliClient,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<(
|
) -> Result<(
|
||||||
VideoListModelEnum,
|
VideoSourceEnum,
|
||||||
Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a + Send>>,
|
Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a + Send>>,
|
||||||
)> {
|
)> {
|
||||||
let watch_later = WatchLater::new(bili_client);
|
let watch_later = WatchLater::new(bili_client);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use tokio::time;
|
|||||||
use crate::adapter::Args;
|
use crate::adapter::Args;
|
||||||
use crate::bilibili::{self, BiliClient};
|
use crate::bilibili::{self, BiliClient};
|
||||||
use crate::config::CONFIG;
|
use crate::config::CONFIG;
|
||||||
use crate::workflow::process_video_list;
|
use crate::workflow::process_video_source;
|
||||||
|
|
||||||
/// 启动周期下载视频的任务
|
/// 启动周期下载视频的任务
|
||||||
pub async fn video_downloader(connection: Arc<DatabaseConnection>) {
|
pub async fn video_downloader(connection: Arc<DatabaseConnection>) {
|
||||||
@@ -35,7 +35,7 @@ pub async fn video_downloader(connection: Arc<DatabaseConnection>) {
|
|||||||
anchor = chrono::Local::now().date_naive();
|
anchor = chrono::Local::now().date_naive();
|
||||||
}
|
}
|
||||||
for (args, path) in ¶ms {
|
for (args, path) in ¶ms {
|
||||||
if let Err(e) = process_video_list(*args, &bili_client, path, &connection).await {
|
if let Err(e) = process_video_source(*args, &bili_client, path, &connection).await {
|
||||||
error!("处理过程遇到错误:{e}");
|
error!("处理过程遇到错误:{e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use sea_orm::entity::prelude::*;
|
|||||||
use sea_orm::sea_query::{OnConflict, SimpleExpr};
|
use sea_orm::sea_query::{OnConflict, SimpleExpr};
|
||||||
use sea_orm::DatabaseTransaction;
|
use sea_orm::DatabaseTransaction;
|
||||||
|
|
||||||
use crate::adapter::{VideoListModel, VideoListModelEnum};
|
use crate::adapter::{VideoSource, VideoSourceEnum};
|
||||||
use crate::bilibili::{PageInfo, VideoInfo};
|
use crate::bilibili::{PageInfo, VideoInfo};
|
||||||
use crate::utils::status::STATUS_COMPLETED;
|
use crate::utils::status::STATUS_COMPLETED;
|
||||||
|
|
||||||
@@ -50,14 +50,14 @@ pub async fn filter_unhandled_video_pages(
|
|||||||
/// 尝试创建 Video Model,如果发生冲突则忽略
|
/// 尝试创建 Video Model,如果发生冲突则忽略
|
||||||
pub async fn create_videos(
|
pub async fn create_videos(
|
||||||
videos_info: Vec<VideoInfo>,
|
videos_info: Vec<VideoInfo>,
|
||||||
video_list_model: &VideoListModelEnum,
|
video_source: &VideoSourceEnum,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let video_models = videos_info
|
let video_models = videos_info
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|v| {
|
.map(|v| {
|
||||||
let mut model = v.into_simple_model();
|
let mut model = v.into_simple_model();
|
||||||
video_list_model.set_relation_id(&mut model);
|
video_source.set_relation_id(&mut model);
|
||||||
model
|
model
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use sea_orm::TransactionTrait;
|
|||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tokio::sync::Semaphore;
|
use tokio::sync::Semaphore;
|
||||||
|
|
||||||
use crate::adapter::{video_list_from, Args, VideoListModel, VideoListModelEnum};
|
use crate::adapter::{video_source_from, Args, VideoSource, VideoSourceEnum};
|
||||||
use crate::bilibili::{BestStream, BiliClient, BiliError, Dimension, PageInfo, Video, VideoInfo};
|
use crate::bilibili::{BestStream, BiliClient, BiliError, Dimension, PageInfo, Video, VideoInfo};
|
||||||
use crate::config::{PathSafeTemplate, ARGS, CONFIG, TEMPLATE};
|
use crate::config::{PathSafeTemplate, ARGS, CONFIG, TEMPLATE};
|
||||||
use crate::downloader::Downloader;
|
use crate::downloader::Downloader;
|
||||||
@@ -25,36 +25,36 @@ use crate::utils::model::{
|
|||||||
use crate::utils::nfo::{ModelWrapper, NFOMode, NFOSerializer};
|
use crate::utils::nfo::{ModelWrapper, NFOMode, NFOSerializer};
|
||||||
use crate::utils::status::{PageStatus, VideoStatus};
|
use crate::utils::status::{PageStatus, VideoStatus};
|
||||||
|
|
||||||
/// 完整地处理某个视频列表
|
/// 完整地处理某个视频来源
|
||||||
pub async fn process_video_list(
|
pub async fn process_video_source(
|
||||||
args: Args<'_>,
|
args: Args<'_>,
|
||||||
bili_client: &BiliClient,
|
bili_client: &BiliClient,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// 从参数中获取视频列表的 Model 与视频流
|
// 从参数中获取视频列表的 Model 与视频流
|
||||||
let (video_list_model, video_streams) = video_list_from(args, path, bili_client, connection).await?;
|
let (video_source, video_streams) = video_source_from(args, path, bili_client, connection).await?;
|
||||||
// 从视频流中获取新视频的简要信息,写入数据库
|
// 从视频流中获取新视频的简要信息,写入数据库
|
||||||
refresh_video_list(&video_list_model, video_streams, connection).await?;
|
refresh_video_source(&video_source, video_streams, connection).await?;
|
||||||
// 单独请求视频详情接口,获取视频的详情信息与所有的分页,写入数据库
|
// 单独请求视频详情接口,获取视频的详情信息与所有的分页,写入数据库
|
||||||
fetch_video_details(bili_client, &video_list_model, connection).await?;
|
fetch_video_details(bili_client, &video_source, connection).await?;
|
||||||
if ARGS.scan_only {
|
if ARGS.scan_only {
|
||||||
warn!("已开启仅扫描模式,跳过视频下载..");
|
warn!("已开启仅扫描模式,跳过视频下载..");
|
||||||
} else {
|
} else {
|
||||||
// 从数据库中查找所有未下载的视频与分页,下载并处理
|
// 从数据库中查找所有未下载的视频与分页,下载并处理
|
||||||
download_unprocessed_videos(bili_client, &video_list_model, connection).await?;
|
download_unprocessed_videos(bili_client, &video_source, connection).await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 请求接口,获取视频列表中所有新添加的视频信息,将其写入数据库
|
/// 请求接口,获取视频列表中所有新添加的视频信息,将其写入数据库
|
||||||
pub async fn refresh_video_list<'a>(
|
pub async fn refresh_video_source<'a>(
|
||||||
video_list_model: &VideoListModelEnum,
|
video_source: &VideoSourceEnum,
|
||||||
video_streams: Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a + Send>>,
|
video_streams: Pin<Box<dyn Stream<Item = Result<VideoInfo>> + 'a + Send>>,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
video_list_model.log_refresh_video_start();
|
video_source.log_refresh_video_start();
|
||||||
let latest_row_at = video_list_model.get_latest_row_at().and_utc();
|
let latest_row_at = video_source.get_latest_row_at().and_utc();
|
||||||
let mut max_datetime = latest_row_at;
|
let mut max_datetime = latest_row_at;
|
||||||
let mut error = Ok(());
|
let mut error = Ok(());
|
||||||
let mut video_streams = video_streams
|
let mut video_streams = video_streams
|
||||||
@@ -81,28 +81,28 @@ pub async fn refresh_video_list<'a>(
|
|||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
while let Some(videos_info) = video_streams.next().await {
|
while let Some(videos_info) = video_streams.next().await {
|
||||||
count += videos_info.len();
|
count += videos_info.len();
|
||||||
create_videos(videos_info, video_list_model, connection).await?;
|
create_videos(videos_info, video_source, connection).await?;
|
||||||
}
|
}
|
||||||
// 如果获取视频分页过程中发生了错误,直接在此处返回,不更新 latest_row_at
|
// 如果获取视频分页过程中发生了错误,直接在此处返回,不更新 latest_row_at
|
||||||
error?;
|
error?;
|
||||||
if max_datetime != latest_row_at {
|
if max_datetime != latest_row_at {
|
||||||
video_list_model
|
video_source
|
||||||
.update_latest_row_at(max_datetime.naive_utc())
|
.update_latest_row_at(max_datetime.naive_utc())
|
||||||
.save(connection)
|
.save(connection)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
video_list_model.log_refresh_video_end(count);
|
video_source.log_refresh_video_end(count);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 筛选出所有未获取到全部信息的视频,尝试补充其详细信息
|
/// 筛选出所有未获取到全部信息的视频,尝试补充其详细信息
|
||||||
pub async fn fetch_video_details(
|
pub async fn fetch_video_details(
|
||||||
bili_client: &BiliClient,
|
bili_client: &BiliClient,
|
||||||
video_list_model: &VideoListModelEnum,
|
video_source: &VideoSourceEnum,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
video_list_model.log_fetch_video_start();
|
video_source.log_fetch_video_start();
|
||||||
let videos_model = filter_unfilled_videos(video_list_model.filter_expr(), connection).await?;
|
let videos_model = filter_unfilled_videos(video_source.filter_expr(), connection).await?;
|
||||||
for video_model in videos_model {
|
for video_model in videos_model {
|
||||||
let video = Video::new(bili_client, video_model.bvid.clone());
|
let video = Video::new(bili_client, video_model.bvid.clone());
|
||||||
let info: Result<_> = async { Ok((video.get_tags().await?, video.get_view_info().await?)) }.await;
|
let info: Result<_> = async { Ok((video.get_tags().await?, video.get_view_info().await?)) }.await;
|
||||||
@@ -128,7 +128,7 @@ pub async fn fetch_video_details(
|
|||||||
// 将分页信息写入数据库
|
// 将分页信息写入数据库
|
||||||
create_pages(pages, &video_model, &txn).await?;
|
create_pages(pages, &video_model, &txn).await?;
|
||||||
let mut video_active_model = view_info.into_detail_model(video_model);
|
let mut video_active_model = view_info.into_detail_model(video_model);
|
||||||
video_list_model.set_relation_id(&mut video_active_model);
|
video_source.set_relation_id(&mut video_active_model);
|
||||||
video_active_model.single_page = Set(Some(pages_len == 1));
|
video_active_model.single_page = Set(Some(pages_len == 1));
|
||||||
video_active_model.tags = Set(Some(serde_json::to_value(tags)?));
|
video_active_model.tags = Set(Some(serde_json::to_value(tags)?));
|
||||||
video_active_model.save(&txn).await?;
|
video_active_model.save(&txn).await?;
|
||||||
@@ -136,20 +136,20 @@ pub async fn fetch_video_details(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
video_list_model.log_fetch_video_end();
|
video_source.log_fetch_video_end();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 下载所有未处理成功的视频
|
/// 下载所有未处理成功的视频
|
||||||
pub async fn download_unprocessed_videos(
|
pub async fn download_unprocessed_videos(
|
||||||
bili_client: &BiliClient,
|
bili_client: &BiliClient,
|
||||||
video_list_model: &VideoListModelEnum,
|
video_source: &VideoSourceEnum,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
video_list_model.log_download_video_start();
|
video_source.log_download_video_start();
|
||||||
let semaphore = Semaphore::new(CONFIG.concurrent_limit.video);
|
let semaphore = Semaphore::new(CONFIG.concurrent_limit.video);
|
||||||
let downloader = Downloader::new(bili_client.client.clone());
|
let downloader = Downloader::new(bili_client.client.clone());
|
||||||
let unhandled_videos_pages = filter_unhandled_video_pages(video_list_model.filter_expr(), connection).await?;
|
let unhandled_videos_pages = filter_unhandled_video_pages(video_source.filter_expr(), connection).await?;
|
||||||
let mut assigned_upper = HashSet::new();
|
let mut assigned_upper = HashSet::new();
|
||||||
let tasks = unhandled_videos_pages
|
let tasks = unhandled_videos_pages
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -158,7 +158,7 @@ pub async fn download_unprocessed_videos(
|
|||||||
assigned_upper.insert(video_model.upper_id);
|
assigned_upper.insert(video_model.upper_id);
|
||||||
download_video_pages(
|
download_video_pages(
|
||||||
bili_client,
|
bili_client,
|
||||||
video_list_model,
|
video_source,
|
||||||
video_model,
|
video_model,
|
||||||
pages_model,
|
pages_model,
|
||||||
connection,
|
connection,
|
||||||
@@ -190,14 +190,14 @@ pub async fn download_unprocessed_videos(
|
|||||||
if download_aborted {
|
if download_aborted {
|
||||||
error!("下载触发风控,已终止所有任务,等待下一轮执行");
|
error!("下载触发风控,已终止所有任务,等待下一轮执行");
|
||||||
}
|
}
|
||||||
video_list_model.log_download_video_end();
|
video_source.log_download_video_end();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn download_video_pages(
|
pub async fn download_video_pages(
|
||||||
bili_client: &BiliClient,
|
bili_client: &BiliClient,
|
||||||
video_list_model: &VideoListModelEnum,
|
video_source: &VideoSourceEnum,
|
||||||
video_model: video::Model,
|
video_model: video::Model,
|
||||||
pages: Vec<page::Model>,
|
pages: Vec<page::Model>,
|
||||||
connection: &DatabaseConnection,
|
connection: &DatabaseConnection,
|
||||||
@@ -208,7 +208,7 @@ pub async fn download_video_pages(
|
|||||||
let _permit = semaphore.acquire().await.context("acquire semaphore failed")?;
|
let _permit = semaphore.acquire().await.context("acquire semaphore failed")?;
|
||||||
let mut status = VideoStatus::from(video_model.download_status);
|
let mut status = VideoStatus::from(video_model.download_status);
|
||||||
let separate_status = status.should_run();
|
let separate_status = status.should_run();
|
||||||
let base_path = video_list_model
|
let base_path = video_source
|
||||||
.path()
|
.path()
|
||||||
.join(TEMPLATE.path_safe_render("video", &video_format_args(&video_model))?);
|
.join(TEMPLATE.path_safe_render("video", &video_format_args(&video_model))?);
|
||||||
let upper_id = video_model.upper_id.to_string();
|
let upper_id = video_model.upper_id.to_string();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ pub struct Migration;
|
|||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl MigrationTrait for Migration {
|
impl MigrationTrait for Migration {
|
||||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
// 为四张 video list 表添加 latest_row_at 字段,表示该列表处理到的最新时间
|
// 为四张 video source 表添加 latest_row_at 字段,表示该列表处理到的最新时间
|
||||||
manager
|
manager
|
||||||
.alter_table(
|
.alter_table(
|
||||||
Table::alter()
|
Table::alter()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
- 每个视频都有唯一的 bvid,包含了封面、描述和标签信息,并包含了一个或多个分页;
|
- 每个视频都有唯一的 bvid,包含了封面、描述和标签信息,并包含了一个或多个分页;
|
||||||
- 每个分页都有一个唯一的 cid,包含了封面、视频、音频、弹幕。
|
- 每个分页都有一个唯一的 cid,包含了封面、视频、音频、弹幕。
|
||||||
|
|
||||||
为了描述方便,在后文会将收藏夹、稍后再看这类结构统称为 video list,将视频称为 video,将分页称为 page。不难看出这三者有着很明显的层级关系:**video list 包含若干 video,video 包含若干 page**。
|
为了描述方便,在后文会将收藏夹、稍后再看这类结构统称为 video source,将视频称为 video,将分页称为 page。不难看出这三者有着很明显的层级关系:**video source 包含若干 video,video 包含若干 page**。
|
||||||
|
|
||||||
一个非常容易混淆的点是视频合集/视频列表与多页视频的区别:
|
一个非常容易混淆的点是视频合集/视频列表与多页视频的区别:
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
这两张图中,上图是视频合集,下图是多页视频。这两者在展示上区别较小,但在结构上有相当大的不同。结合上面对 b 站视频结构的介绍,这个区别可以简单总结为:
|
这两张图中,上图是视频合集,下图是多页视频。这两者在展示上区别较小,但在结构上有相当大的不同。结合上面对 b 站视频结构的介绍,这个区别可以简单总结为:
|
||||||
|
|
||||||
+ **视频合集是由多个仅包含单个 page 的 video 组成的 video list**;
|
+ **视频合集是由多个仅包含单个 page 的 video 组成的 video source**;
|
||||||
|
|
||||||
+ **多页视频是由多个 page 组成的 video**。
|
+ **多页视频是由多个 page 组成的 video**。
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
EMBY 的一般结构是: `媒体库 - 文件夹 - 电影/电视剧 - 分季/分集`,方便起见,我采用了如下的对应关系:
|
EMBY 的一般结构是: `媒体库 - 文件夹 - 电影/电视剧 - 分季/分集`,方便起见,我采用了如下的对应关系:
|
||||||
|
|
||||||
1. **文件夹**:对应 b 站的 video list;
|
1. **文件夹**:对应 b 站的 video source;
|
||||||
2. **电视剧**: 对应 b 站的 video;
|
2. **电视剧**: 对应 b 站的 video;
|
||||||
3. **第一季的所有分集**:对应 b 站的 page。
|
3. **第一季的所有分集**:对应 b 站的 page。
|
||||||
|
|
||||||
@@ -54,11 +54,11 @@ EMBY 的一般结构是: `媒体库 - 文件夹 - 电影/电视剧 - 分季/
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> 可以[前往此处](https://github.com/amtoaer/bili-sync/tree/main/crates/bili_sync_entity/src/entities)实时查看当前版本的数据库表结构。
|
> 可以[前往此处](https://github.com/amtoaer/bili-sync/tree/main/crates/bili_sync_entity/src/entities)实时查看当前版本的数据库表结构。
|
||||||
|
|
||||||
既然拥有着明显的层级关系,那数据库表就很容易设计了。为了简化实现,程序没有额外考虑单个 video 被多个 video list 引用的情况(如一个视频同时在收藏夹和稍后再看中)。而是简单的将其设计为了不交叉的层级结构。
|
既然拥有着明显的层级关系,那数据库表就很容易设计了。为了简化实现,程序没有额外考虑单个 video 被多个 video source 引用的情况(如一个视频同时在收藏夹和稍后再看中)。而是简单的将其设计为了不交叉的层级结构。
|
||||||
|
|
||||||
### video list 表
|
### video source 表
|
||||||
|
|
||||||
从上面的介绍可以看出,video list 并不是一个具体的结构,而是拥有多种实现的抽象概念。我选择将其特化实现为多张表:
|
从上面的介绍可以看出,video source 并不是一个具体的结构,而是拥有多种实现的抽象概念。我选择将其特化实现为多张表:
|
||||||
|
|
||||||
1. favorite:收藏夹;
|
1. favorite:收藏夹;
|
||||||
2. watch_later:稍后再看;
|
2. watch_later:稍后再看;
|
||||||
@@ -67,9 +67,9 @@ EMBY 的一般结构是: `媒体库 - 文件夹 - 电影/电视剧 - 分季/
|
|||||||
|
|
||||||
### video 表
|
### video 表
|
||||||
|
|
||||||
video 表包含了 video 的基本信息,如 bvid、标题、封面、描述、标签等。此外,video 表还包含了与 video list 的关联。
|
video 表包含了 video 的基本信息,如 bvid、标题、封面、描述、标签等。此外,video 表还包含了与 video source 的关联。
|
||||||
|
|
||||||
具体来说,每一种 video list 都在 video 表中有一个对应的列,指向 video list 表中的 id,如 favorite_id、collection_id 等。接下来将这些键与 video 的 bvid 绑在一起建立唯一索引,就可以保证在同一个 video list 中不会有重复的 video。
|
具体来说,每一种 video source 都在 video 表中有一个对应的列,指向 video source 表中的 id,如 favorite_id、collection_id 等。接下来将这些键与 video 的 bvid 绑在一起建立唯一索引,就可以保证在同一个 video source 中不会有重复的 video。
|
||||||
|
|
||||||
### page 表
|
### page 表
|
||||||
|
|
||||||
@@ -81,20 +81,20 @@ page 表包含了 page 的基本信息,如 cid、标题、封面等。与 vide
|
|||||||
|
|
||||||
程序启动时会读取配置文件、迁移数据库、初始化日志等操作。如果发现需要的文件不存在,程序会自动创建。
|
程序启动时会读取配置文件、迁移数据库、初始化日志等操作。如果发现需要的文件不存在,程序会自动创建。
|
||||||
|
|
||||||
### 扫描 video list 获取新视频
|
### 扫描 video source 获取新视频
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> b 站实现接口时为了节省资源,通过 video list 获取到的 video 列表通常是分页且不包含详细信息的。
|
> b 站实现接口时为了节省资源,通过 video source 获取到的 video 列表通常是分页且不包含详细信息的。
|
||||||
|
|
||||||
程序会扫描所有配置文件中包含的 video list,获取其中包含的 video 的简单信息并填充到数据库。在实现时需要避免频繁的全量扫描。
|
程序会扫描所有配置文件中包含的 video source,获取其中包含的 video 的简单信息并填充到数据库。在实现时需要避免频繁的全量扫描。
|
||||||
|
|
||||||
具体到 bili-sync 的实现中,每个 video list 都有一个 `latest_row_at` 列,用于记录处理过的最新视频时间。程序在请求接口时会设置按时间排序,确保新视频位于前面。排序依据的时间根据 video list 的类型而定:收藏夹按收藏时间,投稿按照投稿时间...
|
具体到 bili-sync 的实现中,每个 video source 都有一个 `latest_row_at` 列,用于记录处理过的最新视频时间。程序在请求接口时会设置按时间排序,确保新视频位于前面。排序依据的时间根据 video source 的类型而定:收藏夹按收藏时间,投稿按照投稿时间...
|
||||||
|
|
||||||
拉取过程会逐页请求,程序会不断将获取到的视频保存到数据库中,直到发现第一个小于等于 `latest_row_at` 的视频时停止。接着将 `latest_row_at` 更新为最新的视频时间。
|
拉取过程会逐页请求,程序会不断将获取到的视频保存到数据库中,直到发现第一个小于等于 `latest_row_at` 的视频时停止。接着将 `latest_row_at` 更新为最新的视频时间。
|
||||||
|
|
||||||
### 填充 video 详情
|
### 填充 video 详情
|
||||||
|
|
||||||
将新增视频的简单信息写入数据库后,下一步会填充 video 详情。正如上文所述:**通过 video list 获取到的 video 列表通常是不包含详细信息的**,因此需要额外的请求来填充这些信息。
|
将新增视频的简单信息写入数据库后,下一步会填充 video 详情。正如上文所述:**通过 video source 获取到的 video 列表通常是不包含详细信息的**,因此需要额外的请求来填充这些信息。
|
||||||
|
|
||||||
这一步会筛选出所有未完全填充信息的 video,逐个获取 video 的详细信息(如标签、包含的 page 等)并填充到数据库中。
|
这一步会筛选出所有未完全填充信息的 video,逐个获取 video 的详细信息(如标签、包含的 page 等)并填充到数据库中。
|
||||||
|
|
||||||
@@ -108,4 +108,4 @@ page 表包含了 page 的基本信息,如 cid、标题、封面等。与 vide
|
|||||||
|
|
||||||
如果某些部分下载失败,status 字段会记录这些部分的失败次数,程序会在下次下载时重试。如果重试次数超过了设定的阈值,那么视频会被标记为下载失败,后续直接忽略。
|
如果某些部分下载失败,status 字段会记录这些部分的失败次数,程序会在下次下载时重试。如果重试次数超过了设定的阈值,那么视频会被标记为下载失败,后续直接忽略。
|
||||||
|
|
||||||
此处程序对风控做了额外的处理,一般风控发生时接下来的所有请求都会失败,因此程序检测到风控时不会认为是某个视频下载失败,而是直接终止 video list 的全部下载任务,等待下次扫描时重试。
|
此处程序对风控做了额外的处理,一般风控发生时接下来的所有请求都会失败,因此程序检测到风控时不会认为是某个视频下载失败,而是直接终止 video source 的全部下载任务,等待下次扫描时重试。
|
||||||
Reference in New Issue
Block a user