mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-05-11 18:11:05 +08:00
352 lines
12 KiB
Rust
352 lines
12 KiB
Rust
use std::collections::HashSet;
|
|
use std::sync::Arc;
|
|
|
|
use anyhow::Result;
|
|
use axum::extract::{Extension, Path, Query};
|
|
use bili_sync_entity::*;
|
|
use bili_sync_migration::Expr;
|
|
use sea_orm::{
|
|
ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect,
|
|
TransactionTrait,
|
|
};
|
|
use utoipa::OpenApi;
|
|
|
|
use crate::api::auth::OpenAPIAuth;
|
|
use crate::api::error::InnerApiError;
|
|
use crate::api::helper::{update_page_download_status, update_video_download_status};
|
|
use crate::api::request::{ResetVideoStatusRequest, VideosRequest};
|
|
use crate::api::response::{
|
|
PageInfo, ResetAllVideosResponse, ResetVideoResponse, ResetVideoStatusResponse, VideoInfo, VideoResponse,
|
|
VideoSource, VideoSourcesResponse, VideosResponse,
|
|
};
|
|
use crate::api::wrapper::{ApiError, ApiResponse, ValidatedJson};
|
|
use crate::utils::status::{PageStatus, VideoStatus};
|
|
|
|
#[derive(OpenApi)]
|
|
#[openapi(
|
|
paths(get_video_sources, get_videos, get_video, reset_video, reset_all_videos, reset_video_status),
|
|
modifiers(&OpenAPIAuth),
|
|
security(
|
|
("Token" = []),
|
|
)
|
|
)]
|
|
pub struct ApiDoc;
|
|
|
|
/// 列出所有视频来源
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/api/video-sources",
|
|
responses(
|
|
(status = 200, body = ApiResponse<VideoSourcesResponse>),
|
|
)
|
|
)]
|
|
pub async fn get_video_sources(
|
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
|
) -> Result<ApiResponse<VideoSourcesResponse>, ApiError> {
|
|
let (collection, favorite, submission, watch_later) = tokio::try_join!(
|
|
collection::Entity::find()
|
|
.select_only()
|
|
.columns([collection::Column::Id, collection::Column::Name])
|
|
.into_model::<VideoSource>()
|
|
.all(db.as_ref()),
|
|
favorite::Entity::find()
|
|
.select_only()
|
|
.columns([favorite::Column::Id, favorite::Column::Name])
|
|
.into_model::<VideoSource>()
|
|
.all(db.as_ref()),
|
|
submission::Entity::find()
|
|
.select_only()
|
|
.column(submission::Column::Id)
|
|
.column_as(submission::Column::UpperName, "name")
|
|
.into_model::<VideoSource>()
|
|
.all(db.as_ref()),
|
|
watch_later::Entity::find()
|
|
.select_only()
|
|
.column(watch_later::Column::Id)
|
|
.column_as(Expr::value("稍后再看"), "name")
|
|
.into_model::<VideoSource>()
|
|
.all(db.as_ref())
|
|
)?;
|
|
Ok(ApiResponse::ok(VideoSourcesResponse {
|
|
collection,
|
|
favorite,
|
|
submission,
|
|
watch_later,
|
|
}))
|
|
}
|
|
|
|
/// 列出视频的基本信息,支持根据视频来源筛选、名称查找和分页
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/api/videos",
|
|
params(
|
|
VideosRequest,
|
|
),
|
|
responses(
|
|
(status = 200, body = ApiResponse<VideosResponse>),
|
|
)
|
|
)]
|
|
pub async fn get_videos(
|
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
|
Query(params): Query<VideosRequest>,
|
|
) -> Result<ApiResponse<VideosResponse>, ApiError> {
|
|
let mut query = video::Entity::find();
|
|
for (field, column) in [
|
|
(params.collection, video::Column::CollectionId),
|
|
(params.favorite, video::Column::FavoriteId),
|
|
(params.submission, video::Column::SubmissionId),
|
|
(params.watch_later, video::Column::WatchLaterId),
|
|
] {
|
|
if let Some(id) = field {
|
|
query = query.filter(column.eq(id));
|
|
}
|
|
}
|
|
if let Some(query_word) = params.query {
|
|
query = query.filter(video::Column::Name.contains(query_word));
|
|
}
|
|
let total_count = query.clone().count(db.as_ref()).await?;
|
|
let (page, page_size) = if let (Some(page), Some(page_size)) = (params.page, params.page_size) {
|
|
(page, page_size)
|
|
} else {
|
|
(0, 10)
|
|
};
|
|
Ok(ApiResponse::ok(VideosResponse {
|
|
videos: query
|
|
.order_by_desc(video::Column::Id)
|
|
.into_partial_model::<VideoInfo>()
|
|
.paginate(db.as_ref(), page_size)
|
|
.fetch_page(page)
|
|
.await?,
|
|
total_count,
|
|
}))
|
|
}
|
|
|
|
/// 获取视频详细信息,包括关联的所有 page
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/api/videos/{id}",
|
|
responses(
|
|
(status = 200, body = ApiResponse<VideoResponse>),
|
|
)
|
|
)]
|
|
pub async fn get_video(
|
|
Path(id): Path<i32>,
|
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
|
) -> Result<ApiResponse<VideoResponse>, ApiError> {
|
|
let (video_info, pages_info) = tokio::try_join!(
|
|
video::Entity::find_by_id(id)
|
|
.into_partial_model::<VideoInfo>()
|
|
.one(db.as_ref()),
|
|
page::Entity::find()
|
|
.filter(page::Column::VideoId.eq(id))
|
|
.order_by_asc(page::Column::Cid)
|
|
.into_partial_model::<PageInfo>()
|
|
.all(db.as_ref())
|
|
)?;
|
|
let Some(video_info) = video_info else {
|
|
return Err(InnerApiError::NotFound(id).into());
|
|
};
|
|
Ok(ApiResponse::ok(VideoResponse {
|
|
video: video_info,
|
|
pages: pages_info,
|
|
}))
|
|
}
|
|
|
|
/// 将某个视频与其所有分页的失败状态清空为未下载状态,这样在下次下载任务中会触发重试
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/api/videos/{id}/reset",
|
|
responses(
|
|
(status = 200, body = ApiResponse<ResetVideoResponse>),
|
|
)
|
|
)]
|
|
pub async fn reset_video(
|
|
Path(id): Path<i32>,
|
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
|
) -> Result<ApiResponse<ResetVideoResponse>, ApiError> {
|
|
let (video_info, pages_info) = tokio::try_join!(
|
|
video::Entity::find_by_id(id)
|
|
.into_partial_model::<VideoInfo>()
|
|
.one(db.as_ref()),
|
|
page::Entity::find()
|
|
.filter(page::Column::VideoId.eq(id))
|
|
.order_by_asc(page::Column::Cid)
|
|
.into_partial_model::<PageInfo>()
|
|
.all(db.as_ref())
|
|
)?;
|
|
let Some(mut video_info) = video_info else {
|
|
return Err(InnerApiError::NotFound(id).into());
|
|
};
|
|
let resetted_pages_info = pages_info
|
|
.into_iter()
|
|
.filter_map(|mut page_info| {
|
|
let mut page_status = PageStatus::from(page_info.download_status);
|
|
if page_status.reset_failed() {
|
|
page_info.download_status = page_status.into();
|
|
Some(page_info)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let mut video_status = VideoStatus::from(video_info.download_status);
|
|
let mut video_resetted = video_status.reset_failed();
|
|
if !resetted_pages_info.is_empty() {
|
|
video_status.set(4, 0); // 将“分P下载”重置为 0
|
|
video_resetted = true;
|
|
}
|
|
let resetted_videos_info = if video_resetted {
|
|
video_info.download_status = video_status.into();
|
|
vec![&video_info]
|
|
} else {
|
|
vec![]
|
|
};
|
|
let resetted = !resetted_videos_info.is_empty() || !resetted_pages_info.is_empty();
|
|
if resetted {
|
|
let txn = db.begin().await?;
|
|
if !resetted_videos_info.is_empty() {
|
|
// 只可能有 1 个元素,所以不用 batch
|
|
update_video_download_status(&txn, &resetted_videos_info, None).await?;
|
|
}
|
|
if !resetted_pages_info.is_empty() {
|
|
update_page_download_status(&txn, &resetted_pages_info, Some(500)).await?;
|
|
}
|
|
txn.commit().await?;
|
|
}
|
|
Ok(ApiResponse::ok(ResetVideoResponse {
|
|
resetted,
|
|
video: video_info,
|
|
pages: resetted_pages_info,
|
|
}))
|
|
}
|
|
|
|
/// 重置所有视频和页面的失败状态为未下载状态,这样在下次下载任务中会触发重试
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/api/videos/reset-all",
|
|
responses(
|
|
(status = 200, body = ApiResponse<ResetAllVideosResponse>),
|
|
)
|
|
)]
|
|
pub async fn reset_all_videos(
|
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
|
) -> Result<ApiResponse<ResetAllVideosResponse>, ApiError> {
|
|
// 先查询所有视频和页面数据
|
|
let (all_videos, all_pages) = tokio::try_join!(
|
|
video::Entity::find().into_partial_model::<VideoInfo>().all(db.as_ref()),
|
|
page::Entity::find().into_partial_model::<PageInfo>().all(db.as_ref())
|
|
)?;
|
|
let resetted_pages_info = all_pages
|
|
.into_iter()
|
|
.filter_map(|mut page_info| {
|
|
let mut page_status = PageStatus::from(page_info.download_status);
|
|
if page_status.reset_failed() {
|
|
page_info.download_status = page_status.into();
|
|
Some(page_info)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let video_ids_with_resetted_pages: HashSet<i32> = resetted_pages_info.iter().map(|page| page.video_id).collect();
|
|
let resetted_videos_info = all_videos
|
|
.into_iter()
|
|
.filter_map(|mut video_info| {
|
|
let mut video_status = VideoStatus::from(video_info.download_status);
|
|
let mut video_resetted = video_status.reset_failed();
|
|
if video_ids_with_resetted_pages.contains(&video_info.id) {
|
|
video_status.set(4, 0); // 将"分P下载"重置为 0
|
|
video_resetted = true;
|
|
}
|
|
if video_resetted {
|
|
video_info.download_status = video_status.into();
|
|
Some(video_info)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let resetted = !(resetted_videos_info.is_empty() && resetted_pages_info.is_empty());
|
|
if resetted {
|
|
let txn = db.begin().await?;
|
|
if !resetted_videos_info.is_empty() {
|
|
update_video_download_status(&txn, &resetted_videos_info, Some(500)).await?;
|
|
}
|
|
if !resetted_pages_info.is_empty() {
|
|
update_page_download_status(&txn, &resetted_pages_info, Some(500)).await?;
|
|
}
|
|
txn.commit().await?;
|
|
}
|
|
Ok(ApiResponse::ok(ResetAllVideosResponse {
|
|
resetted,
|
|
resetted_videos_count: resetted_videos_info.len(),
|
|
resetted_pages_count: resetted_pages_info.len(),
|
|
}))
|
|
}
|
|
|
|
/// 重置指定视频及其分页的指定状态位
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/api/videos/{id}/reset-status",
|
|
request_body = ResetVideoStatusRequest,
|
|
responses(
|
|
(status = 200, body = ApiResponse<ResetVideoStatusResponse>),
|
|
)
|
|
)]
|
|
pub async fn reset_video_status(
|
|
Path(id): Path<i32>,
|
|
Extension(db): Extension<Arc<DatabaseConnection>>,
|
|
ValidatedJson(request): ValidatedJson<ResetVideoStatusRequest>,
|
|
) -> Result<ApiResponse<ResetVideoStatusResponse>, ApiError> {
|
|
let (video_info, mut pages_info) = tokio::try_join!(
|
|
video::Entity::find_by_id(id)
|
|
.into_partial_model::<VideoInfo>()
|
|
.one(db.as_ref()),
|
|
page::Entity::find()
|
|
.filter(page::Column::VideoId.eq(id))
|
|
.order_by_asc(page::Column::Cid)
|
|
.into_partial_model::<PageInfo>()
|
|
.all(db.as_ref())
|
|
)?;
|
|
let Some(mut video_info) = video_info else {
|
|
return Err(InnerApiError::NotFound(id).into());
|
|
};
|
|
let mut video_status = VideoStatus::from(video_info.download_status);
|
|
for update in &request.video_updates {
|
|
video_status.set(update.status_index, update.status_value);
|
|
}
|
|
video_info.download_status = video_status.into();
|
|
let mut updated_pages_info = Vec::new();
|
|
let mut page_id_map = pages_info
|
|
.iter_mut()
|
|
.map(|page| (page.id, page))
|
|
.collect::<std::collections::HashMap<_, _>>();
|
|
for page_update in &request.page_updates {
|
|
if let Some(page_info) = page_id_map.remove(&page_update.page_id) {
|
|
let mut page_status = PageStatus::from(page_info.download_status);
|
|
for update in &page_update.updates {
|
|
page_status.set(update.status_index, update.status_value);
|
|
}
|
|
page_info.download_status = page_status.into();
|
|
updated_pages_info.push(page_info);
|
|
}
|
|
}
|
|
let has_video_updates = !request.video_updates.is_empty();
|
|
let has_page_updates = !updated_pages_info.is_empty();
|
|
if has_video_updates || has_page_updates {
|
|
let txn = db.begin().await?;
|
|
if has_video_updates {
|
|
update_video_download_status(&txn, &[&video_info], None).await?;
|
|
}
|
|
if has_page_updates {
|
|
update_page_download_status(&txn, &updated_pages_info, None).await?;
|
|
}
|
|
txn.commit().await?;
|
|
}
|
|
Ok(ApiResponse::ok(ResetVideoStatusResponse {
|
|
success: has_video_updates || has_page_updates,
|
|
video: video_info,
|
|
pages: pages_info,
|
|
}))
|
|
}
|