mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-05-06 20:42:48 +08:00
chore: 移除手动触发及相关逻辑(和当前的状态编辑功能基本重合)
This commit is contained in:
@@ -28,7 +28,6 @@ use crate::api::wrapper::{ApiError, ApiResponse, ValidatedJson};
|
|||||||
use crate::bilibili::BiliClient;
|
use crate::bilibili::BiliClient;
|
||||||
use crate::config::VersionedConfig;
|
use crate::config::VersionedConfig;
|
||||||
use crate::utils::status::{PageStatus, VideoStatus};
|
use crate::utils::status::{PageStatus, VideoStatus};
|
||||||
use crate::workflow_danmaku::{refresh_danmaku_for_page, refresh_danmaku_for_video};
|
|
||||||
|
|
||||||
pub(super) fn router() -> Router {
|
pub(super) fn router() -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
@@ -42,37 +41,8 @@ pub(super) fn router() -> Router {
|
|||||||
.route("/videos/{id}/update-status", post(update_video_status))
|
.route("/videos/{id}/update-status", post(update_video_status))
|
||||||
.route("/videos/reset-status", post(reset_filtered_video_status))
|
.route("/videos/reset-status", post(reset_filtered_video_status))
|
||||||
.route("/videos/update-status", post(update_filtered_video_status))
|
.route("/videos/update-status", post(update_filtered_video_status))
|
||||||
.route("/videos/{id}/refresh-danmaku", post(refresh_video_danmaku))
|
|
||||||
.route("/pages/{id}/refresh-danmaku", post(refresh_page_danmaku))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct RefreshDanmakuResponse {
|
|
||||||
/// 本次实际刷新成功的 page 数量;page 级接口始终为 0 或 1。
|
|
||||||
pub refreshed: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 手动触发:刷新某个视频所有 page 的弹幕。忽略策略,强制执行。
|
|
||||||
pub async fn refresh_video_danmaku(
|
|
||||||
Path(id): Path<i32>,
|
|
||||||
Extension(db): Extension<DatabaseConnection>,
|
|
||||||
Extension(bili_client): Extension<Arc<BiliClient>>,
|
|
||||||
) -> Result<ApiResponse<RefreshDanmakuResponse>, ApiError> {
|
|
||||||
let config = VersionedConfig::get().snapshot();
|
|
||||||
let refreshed = refresh_danmaku_for_video(id, &bili_client, &db, &config).await?;
|
|
||||||
Ok(ApiResponse::ok(RefreshDanmakuResponse { refreshed }))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 手动触发:刷新单个 page 的弹幕。忽略策略,强制执行;走严格模式,任何错误都直接 4xx/5xx。
|
|
||||||
pub async fn refresh_page_danmaku(
|
|
||||||
Path(id): Path<i32>,
|
|
||||||
Extension(db): Extension<DatabaseConnection>,
|
|
||||||
Extension(bili_client): Extension<Arc<BiliClient>>,
|
|
||||||
) -> Result<ApiResponse<RefreshDanmakuResponse>, ApiError> {
|
|
||||||
let config = VersionedConfig::get().snapshot();
|
|
||||||
let refreshed = refresh_danmaku_for_page(id, &bili_client, &db, &config).await?;
|
|
||||||
Ok(ApiResponse::ok(RefreshDanmakuResponse { refreshed }))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 列出视频的基本信息,支持根据视频来源筛选、名称查找和分页
|
/// 列出视频的基本信息,支持根据视频来源筛选、名称查找和分页
|
||||||
pub async fn get_videos(
|
pub async fn get_videos(
|
||||||
|
|||||||
@@ -50,10 +50,7 @@ pub async fn refresh_danmaku_incremental(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|page| {
|
.filter_map(|page| {
|
||||||
let pubtime = video_model.pubtime.and_utc();
|
let pubtime = video_model.pubtime.and_utc();
|
||||||
let last_synced = page
|
let last_synced = page.danmaku_last_synced_at.as_deref().and_then(parse_stored_datetime);
|
||||||
.danmaku_last_synced_at
|
|
||||||
.as_deref()
|
|
||||||
.and_then(parse_stored_datetime);
|
|
||||||
match should_sync_danmaku(
|
match should_sync_danmaku(
|
||||||
&config.danmaku_update_policy,
|
&config.danmaku_update_policy,
|
||||||
pubtime,
|
pubtime,
|
||||||
@@ -82,101 +79,16 @@ pub async fn refresh_danmaku_incremental(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
info!(
|
info!("弹幕增量更新结束:处理视频 {} 个,刷新分页 {} 个", processed, refreshed);
|
||||||
"弹幕增量更新结束:处理视频 {} 个,刷新分页 {} 个",
|
|
||||||
processed, refreshed
|
|
||||||
);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 手动触发:刷新某个视频所有 page 的弹幕(忽略策略,强制执行)。
|
|
||||||
pub async fn refresh_danmaku_for_video(
|
|
||||||
video_id: i32,
|
|
||||||
bili_client: &BiliClient,
|
|
||||||
connection: &DatabaseConnection,
|
|
||||||
config: &Config,
|
|
||||||
) -> Result<usize> {
|
|
||||||
let video_model = video::Entity::find_by_id(video_id)
|
|
||||||
.one(connection)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("video {} 不存在", video_id))?;
|
|
||||||
let pages = page::Entity::find()
|
|
||||||
.filter(page::Column::VideoId.eq(video_id))
|
|
||||||
.all(connection)
|
|
||||||
.await?;
|
|
||||||
if pages.is_empty() {
|
|
||||||
return Ok(0);
|
|
||||||
}
|
|
||||||
let now = Utc::now();
|
|
||||||
// 手动触发:next_stage 传 None,让 refresh_one_page 内部按 age 计算(且不会冻结)。
|
|
||||||
// 这样不会把 Mature/Cold 视频回退成 Fresh,也不会把活跃视频意外冻结。
|
|
||||||
let selected = pages.into_iter().map(|p| (p, None)).collect();
|
|
||||||
refresh_video_pages(bili_client, connection, config, &video_model, selected, now).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 手动触发:刷新单个 page 的弹幕(忽略策略,强制执行)。
|
|
||||||
///
|
|
||||||
/// 与 [`refresh_danmaku_for_video`] 的 best-effort 模式不同,本接口走严格模式:
|
|
||||||
/// 只要存在任何错误(page 不存在、view_info 拉取失败、新 view_info 中 pid 不再出现、
|
|
||||||
/// 弹幕写入失败等)都直接 bail,确保 API 调用方不会收到"假成功"。
|
|
||||||
///
|
|
||||||
/// 成功时返回刷新成功的 page 数(恒为 1)。
|
|
||||||
pub async fn refresh_danmaku_for_page(
|
|
||||||
page_id: i32,
|
|
||||||
bili_client: &BiliClient,
|
|
||||||
connection: &DatabaseConnection,
|
|
||||||
config: &Config,
|
|
||||||
) -> Result<usize> {
|
|
||||||
let page_model = page::Entity::find_by_id(page_id)
|
|
||||||
.one(connection)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("page {} 不存在", page_id))?;
|
|
||||||
let video_model = video::Entity::find_by_id(page_model.video_id)
|
|
||||||
.one(connection)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("page {} 的宿主 video 不存在", page_id))?;
|
|
||||||
let now = Utc::now();
|
|
||||||
let bili_video = Video::new(bili_client, video_model.bvid.as_str(), &config.credential);
|
|
||||||
let view_info = bili_video
|
|
||||||
.get_view_info()
|
|
||||||
.await
|
|
||||||
.with_context(|| format!("获取视频 {} 的 view_info 失败", video_model.bvid))?;
|
|
||||||
let VideoInfo::Detail { pages: fresh_pages, .. } = view_info else {
|
|
||||||
bail!("view_info 返回了非 Detail 类型,无法刷新弹幕");
|
|
||||||
};
|
|
||||||
let fresh = fresh_pages
|
|
||||||
.iter()
|
|
||||||
.find(|p| p.page == page_model.pid)
|
|
||||||
.ok_or_else(|| {
|
|
||||||
anyhow!(
|
|
||||||
"视频「{}」({}) 的分页 pid={} 在最新的 view_info 中已不存在",
|
|
||||||
video_model.name,
|
|
||||||
video_model.bvid,
|
|
||||||
page_model.pid
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
refresh_one_page(
|
|
||||||
&bili_video,
|
|
||||||
connection,
|
|
||||||
config,
|
|
||||||
&video_model,
|
|
||||||
page_model,
|
|
||||||
fresh,
|
|
||||||
None,
|
|
||||||
now,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 候选视频:有效 + 有路径(至少下载过) + 至少存在一个 page 的 download_status 弹幕位已成功,
|
/// 候选视频:有效 + 有路径(至少下载过) + 至少存在一个 page 的 download_status 弹幕位已成功,
|
||||||
/// 且**所属源仍处于启用状态**。
|
/// 且**所属源仍处于启用状态**。
|
||||||
///
|
///
|
||||||
/// 与项目里其他流程保持一致:disabled 源被视为"用户主动暂停处理",弹幕增量也不再触碰它的内容,
|
/// 与项目里其他流程保持一致:disabled 源被视为"用户主动暂停处理",弹幕增量也不再触碰它的内容,
|
||||||
/// 避免后台默默地继续请求 B 站接口和改写本地 ASS 文件。
|
/// 避免后台默默地继续请求 B 站接口和改写本地 ASS 文件。
|
||||||
async fn load_candidate_videos(
|
async fn load_candidate_videos(connection: &DatabaseConnection) -> Result<Vec<(video::Model, Vec<page::Model>)>> {
|
||||||
connection: &DatabaseConnection,
|
|
||||||
) -> Result<Vec<(video::Model, Vec<page::Model>)>> {
|
|
||||||
use sea_orm::{Condition, QuerySelect};
|
use sea_orm::{Condition, QuerySelect};
|
||||||
|
|
||||||
// 一次性取齐四类启用源的 id 集合
|
// 一次性取齐四类启用源的 id 集合
|
||||||
@@ -214,11 +126,7 @@ async fn load_candidate_videos(
|
|||||||
.context("load enabled watch_later ids failed")?;
|
.context("load enabled watch_later ids failed")?;
|
||||||
|
|
||||||
// 至少一个外键命中启用集合,才纳入候选;全部为空时直接 early-return 避免无意义查询。
|
// 至少一个外键命中启用集合,才纳入候选;全部为空时直接 early-return 避免无意义查询。
|
||||||
if favorite_ids.is_empty()
|
if favorite_ids.is_empty() && collection_ids.is_empty() && submission_ids.is_empty() && watch_later_ids.is_empty() {
|
||||||
&& collection_ids.is_empty()
|
|
||||||
&& submission_ids.is_empty()
|
|
||||||
&& watch_later_ids.is_empty()
|
|
||||||
{
|
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
let mut source_filter = Condition::any();
|
let mut source_filter = Condition::any();
|
||||||
@@ -487,10 +395,7 @@ fn resolve_danmaku_path(video_model: &video::Model, page_model: &page::Model) ->
|
|||||||
.0;
|
.0;
|
||||||
Ok(base_path
|
Ok(base_path
|
||||||
.join("Season 1")
|
.join("Season 1")
|
||||||
.join(format!(
|
.join(format!("{} - S01E{:0>2}.zh-CN.default.ass", base_name, page_model.pid)))
|
||||||
"{} - S01E{:0>2}.zh-CN.default.ass",
|
|
||||||
base_name, page_model.pid
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -525,10 +430,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn reset_non_danmaku_subtasks_keeps_only_danmaku_ok() {
|
fn reset_non_danmaku_subtasks_keeps_only_danmaku_ok() {
|
||||||
// 五个子任务都 OK + 完成位
|
// 五个子任务都 OK + 完成位
|
||||||
let all_ok_completed: u32 = (1u32 << 31)
|
let all_ok_completed: u32 = (1u32 << 31) | (0..5).map(|i| STATUS_OK << (i * 3)).fold(0u32, |a, b| a | b);
|
||||||
| (0..5)
|
|
||||||
.map(|i| STATUS_OK << (i * 3))
|
|
||||||
.fold(0u32, |a, b| a | b);
|
|
||||||
let reset = reset_non_danmaku_subtasks(all_ok_completed);
|
let reset = reset_non_danmaku_subtasks(all_ok_completed);
|
||||||
// 弹幕位保留
|
// 弹幕位保留
|
||||||
assert_eq!((reset >> 9) & 0b111, STATUS_OK);
|
assert_eq!((reset >> 9) & 0b111, STATUS_OK);
|
||||||
@@ -543,10 +445,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn reset_video_for_page_redownload_clears_subtask_4_and_completed_bit() {
|
fn reset_video_for_page_redownload_clears_subtask_4_and_completed_bit() {
|
||||||
// 五个子任务都 OK + 完成位
|
// 五个子任务都 OK + 完成位
|
||||||
let video_done: u32 = (1u32 << 31)
|
let video_done: u32 = (1u32 << 31) | (0..5).map(|i| STATUS_OK << (i * 3)).fold(0u32, |a, b| a | b);
|
||||||
| (0..5)
|
|
||||||
.map(|i| STATUS_OK << (i * 3))
|
|
||||||
.fold(0u32, |a, b| a | b);
|
|
||||||
let reset = reset_video_for_page_redownload(video_done);
|
let reset = reset_video_for_page_redownload(video_done);
|
||||||
// offset 4(分页下载子任务)被清零
|
// offset 4(分页下载子任务)被清零
|
||||||
assert_eq!((reset >> 12) & 0b111, 0);
|
assert_eq!((reset >> 12) & 0b111, 0);
|
||||||
@@ -560,9 +459,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_stored_datetime_roundtrip() {
|
fn parse_stored_datetime_roundtrip() {
|
||||||
let now = chrono::Utc
|
let now = chrono::Utc.with_ymd_and_hms(2026, 4, 13, 10, 20, 30).unwrap();
|
||||||
.with_ymd_and_hms(2026, 4, 13, 10, 20, 30)
|
|
||||||
.unwrap();
|
|
||||||
let s = now.naive_utc().to_string();
|
let s = now.naive_utc().to_string();
|
||||||
let parsed = parse_stored_datetime(&s).expect("parse ok");
|
let parsed = parse_stored_datetime(&s).expect("parse ok");
|
||||||
assert_eq!(parsed, now);
|
assert_eq!(parsed, now);
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import type {
|
|||||||
InsertSubmissionRequest,
|
InsertSubmissionRequest,
|
||||||
Notifier,
|
Notifier,
|
||||||
QrcodePollResponse as PollQrcodeResponse,
|
QrcodePollResponse as PollQrcodeResponse,
|
||||||
RefreshDanmakuResponse,
|
|
||||||
ResetFilteredVideosResponse,
|
ResetFilteredVideosResponse,
|
||||||
ResetFilteredVideoStatusRequest,
|
ResetFilteredVideoStatusRequest,
|
||||||
ResetVideoResponse,
|
ResetVideoResponse,
|
||||||
@@ -288,13 +287,6 @@ class ApiClient {
|
|||||||
return this.post<boolean>('/task/download');
|
return this.post<boolean>('/task/download');
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshDanmakuForVideo(id: number): Promise<ApiResponse<RefreshDanmakuResponse>> {
|
|
||||||
return this.post<RefreshDanmakuResponse>(`/videos/${id}/refresh-danmaku`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshDanmakuForPage(id: number): Promise<ApiResponse<RefreshDanmakuResponse>> {
|
|
||||||
return this.post<RefreshDanmakuResponse>(`/pages/${id}/refresh-danmaku`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateQrcode(): Promise<ApiResponse<GenerateQrcodeResponse>> {
|
async generateQrcode(): Promise<ApiResponse<GenerateQrcodeResponse>> {
|
||||||
return this.post<GenerateQrcodeResponse>('/login/qrcode/generate');
|
return this.post<GenerateQrcodeResponse>('/login/qrcode/generate');
|
||||||
@@ -354,8 +346,6 @@ const api = {
|
|||||||
updateConfig: (config: Config) => apiClient.updateConfig(config),
|
updateConfig: (config: Config) => apiClient.updateConfig(config),
|
||||||
getDashboard: () => apiClient.getDashboard(),
|
getDashboard: () => apiClient.getDashboard(),
|
||||||
triggerDownloadTask: () => apiClient.triggerDownloadTask(),
|
triggerDownloadTask: () => apiClient.triggerDownloadTask(),
|
||||||
refreshDanmakuForVideo: (id: number) => apiClient.refreshDanmakuForVideo(id),
|
|
||||||
refreshDanmakuForPage: (id: number) => apiClient.refreshDanmakuForPage(id),
|
|
||||||
generateQrcode: () => apiClient.generateQrcode(),
|
generateQrcode: () => apiClient.generateQrcode(),
|
||||||
pollQrcode: (qrcodeKey: string) => apiClient.pollQrcode(qrcodeKey),
|
pollQrcode: (qrcodeKey: string) => apiClient.pollQrcode(qrcodeKey),
|
||||||
subscribeToSysInfo: (onMessage: (data: SysInfo) => void) =>
|
subscribeToSysInfo: (onMessage: (data: SysInfo) => void) =>
|
||||||
|
|||||||
@@ -6,38 +6,3 @@ export const VIDEO_SOURCES = {
|
|||||||
SUBMISSION: { type: 'submission', title: '用户投稿', icon: UserIcon },
|
SUBMISSION: { type: 'submission', title: '用户投稿', icon: UserIcon },
|
||||||
WATCH_LATER: { type: 'watch_later', title: '稍后再看', icon: ClockIcon }
|
WATCH_LATER: { type: 'watch_later', title: '稍后再看', icon: ClockIcon }
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 弹幕同步阶段标签映射,对应 Rust 端 `danmaku_sync_generation` 字段。
|
|
||||||
* 0=未开始;首次同步前不展示 badge。
|
|
||||||
*/
|
|
||||||
export const DANMAKU_GENERATION_LABELS: Record<
|
|
||||||
number,
|
|
||||||
{ text: string; variant: 'default' | 'secondary' | 'outline' | 'destructive' }
|
|
||||||
> = {
|
|
||||||
0: { text: '待更新', variant: 'outline' },
|
|
||||||
1: { text: '新鲜期', variant: 'default' },
|
|
||||||
2: { text: '成熟期', variant: 'secondary' },
|
|
||||||
3: { text: '老化期', variant: 'outline' },
|
|
||||||
4: { text: '已冻结', variant: 'outline' }
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 将任意可解析为日期的字符串格式化为相对时间("2 小时前")。 */
|
|
||||||
export function formatRelativeTime(input: string | Date | null | undefined): string {
|
|
||||||
if (!input) return '从未同步';
|
|
||||||
const then = typeof input === 'string' ? new Date(input.replace(' ', 'T') + 'Z') : input;
|
|
||||||
const diff = Date.now() - then.getTime();
|
|
||||||
if (Number.isNaN(diff)) return '从未同步';
|
|
||||||
if (diff < 0) return '刚刚';
|
|
||||||
const minutes = Math.floor(diff / 60_000);
|
|
||||||
if (minutes < 1) return '刚刚';
|
|
||||||
if (minutes < 60) return `${minutes} 分钟前`;
|
|
||||||
const hours = Math.floor(minutes / 60);
|
|
||||||
if (hours < 24) return `${hours} 小时前`;
|
|
||||||
const days = Math.floor(hours / 24);
|
|
||||||
if (days < 30) return `${days} 天前`;
|
|
||||||
const months = Math.floor(days / 30);
|
|
||||||
if (months < 12) return `${months} 个月前`;
|
|
||||||
const years = Math.floor(days / 365);
|
|
||||||
return `${years} 年前`;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -58,9 +58,6 @@ export interface PageInfo {
|
|||||||
danmaku_cid_snapshot: number | null;
|
danmaku_cid_snapshot: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RefreshDanmakuResponse {
|
|
||||||
refreshed: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VideoResponse {
|
export interface VideoResponse {
|
||||||
video: VideoInfo;
|
video: VideoInfo;
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
import VideoCard from '$lib/components/video-card.svelte';
|
import VideoCard from '$lib/components/video-card.svelte';
|
||||||
import StatusEditor from '$lib/components/status-editor.svelte';
|
import StatusEditor from '$lib/components/status-editor.svelte';
|
||||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||||
import { DANMAKU_GENERATION_LABELS, formatRelativeTime } from '$lib/consts';
|
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
let videoData: VideoResponse | null = null;
|
let videoData: VideoResponse | null = null;
|
||||||
@@ -29,8 +28,6 @@
|
|||||||
let clearAndResetting = false;
|
let clearAndResetting = false;
|
||||||
let statusEditorOpen = false;
|
let statusEditorOpen = false;
|
||||||
let statusEditorLoading = false;
|
let statusEditorLoading = false;
|
||||||
let refreshingDanmaku = false;
|
|
||||||
let refreshingPageDanmaku = new Set<number>();
|
|
||||||
|
|
||||||
async function loadVideoDetail() {
|
async function loadVideoDetail() {
|
||||||
const videoId = parseInt($page.params.id!);
|
const videoId = parseInt($page.params.id!);
|
||||||
@@ -122,44 +119,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRefreshDanmaku() {
|
|
||||||
if (!videoData || refreshingDanmaku) return;
|
|
||||||
refreshingDanmaku = true;
|
|
||||||
try {
|
|
||||||
const result = await api.refreshDanmakuForVideo(videoData.video.id);
|
|
||||||
toast.success('弹幕刷新完成', {
|
|
||||||
description: `已成功刷新 ${result.data.refreshed} 个分页`
|
|
||||||
});
|
|
||||||
await loadVideoDetail();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('弹幕刷新失败:', error);
|
|
||||||
toast.error('弹幕刷新失败', {
|
|
||||||
description: (error as ApiError).message
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
refreshingDanmaku = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRefreshPageDanmaku(pageId: number) {
|
|
||||||
if (refreshingPageDanmaku.has(pageId)) return;
|
|
||||||
refreshingPageDanmaku = new Set([...refreshingPageDanmaku, pageId]);
|
|
||||||
try {
|
|
||||||
await api.refreshDanmakuForPage(pageId);
|
|
||||||
toast.success('弹幕刷新完成');
|
|
||||||
await loadVideoDetail();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('弹幕刷新失败:', error);
|
|
||||||
toast.error('弹幕刷新失败', {
|
|
||||||
description: (error as ApiError).message
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
const next = new Set(refreshingPageDanmaku);
|
|
||||||
next.delete(pageId);
|
|
||||||
refreshingPageDanmaku = next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleClearAndReset() {
|
async function handleClearAndReset() {
|
||||||
if (!videoData) return;
|
if (!videoData) return;
|
||||||
try {
|
try {
|
||||||
@@ -243,17 +202,6 @@
|
|||||||
<BrushCleaningIcon class="mr-2 h-4 w-4 {clearAndResetting ? 'animate-spin' : ''}" />
|
<BrushCleaningIcon class="mr-2 h-4 w-4 {clearAndResetting ? 'animate-spin' : ''}" />
|
||||||
清空重置
|
清空重置
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
class="shrink-0 cursor-pointer "
|
|
||||||
onclick={handleRefreshDanmaku}
|
|
||||||
disabled={refreshingDanmaku || resetting || clearAndResetting}
|
|
||||||
title="立即重新拉取所有分页的弹幕(忽略更新策略)"
|
|
||||||
>
|
|
||||||
<RefreshCwIcon class="mr-2 h-4 w-4 {refreshingDanmaku ? 'animate-spin' : ''}" />
|
|
||||||
刷新弹幕
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -315,36 +263,6 @@
|
|||||||
customSubtitle=""
|
customSubtitle=""
|
||||||
taskNames={['视频封面', '视频内容', '视频信息', '视频弹幕', '视频字幕']}
|
taskNames={['视频封面', '视频内容', '视频信息', '视频弹幕', '视频字幕']}
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
class="text-muted-foreground flex items-center justify-between gap-2 px-2 text-xs"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
{#if pageInfo.danmaku_sync_generation > 0}
|
|
||||||
{@const label = DANMAKU_GENERATION_LABELS[pageInfo.danmaku_sync_generation]}
|
|
||||||
<Badge variant={label?.variant ?? 'outline'} class="text-[10px]">
|
|
||||||
弹幕 · {label?.text ?? '—'}
|
|
||||||
</Badge>
|
|
||||||
{/if}
|
|
||||||
<span title={pageInfo.danmaku_last_synced_at ?? ''}>
|
|
||||||
{formatRelativeTime(pageInfo.danmaku_last_synced_at)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
class="h-6 px-2"
|
|
||||||
disabled={refreshingPageDanmaku.has(pageInfo.id)}
|
|
||||||
onclick={() => handleRefreshPageDanmaku(pageInfo.id)}
|
|
||||||
title="仅刷新该分页的弹幕"
|
|
||||||
>
|
|
||||||
<RefreshCwIcon
|
|
||||||
class="mr-1 h-3 w-3 {refreshingPageDanmaku.has(pageInfo.id)
|
|
||||||
? 'animate-spin'
|
|
||||||
: ''}"
|
|
||||||
/>
|
|
||||||
刷新
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user