From b5ef76b0ed5f731dbe856e177e9756249bc8734d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=80=E1=B4=8D=E1=B4=9B=E1=B4=8F=E1=B4=80=E1=B4=87?= =?UTF-8?q?=CA=80?= Date: Fri, 5 Dec 2025 16:38:10 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=AD=A3=E7=A1=AE=E5=A4=84=E7=90=86?= =?UTF-8?q?=E2=80=9C=E6=88=91=E8=BF=BD=E7=9A=84=E5=90=88=E9=9B=86=20/=20?= =?UTF-8?q?=E6=94=B6=E8=97=8F=E5=A4=B9=E2=80=9D=E4=B8=AD=E7=9A=84=E6=94=B6?= =?UTF-8?q?=E8=97=8F=E5=A4=B9=E6=9D=A1=E7=9B=AE=EF=BC=8C=E4=BB=A5=E5=8F=8A?= =?UTF-8?q?=E4=B8=80=E4=BA=9B=E6=A0=B7=E5=BC=8F=E3=80=81=E6=96=87=E6=9C=AC?= =?UTF-8?q?=E8=B0=83=E6=95=B4=20(#553)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 12 +- Cargo.toml | 1 + crates/bili_sync/Cargo.toml | 1 + crates/bili_sync/src/api/response.rs | 57 ++++----- crates/bili_sync/src/api/routes/me/mod.rs | 95 +++++++++----- crates/bili_sync/src/bilibili/me.rs | 2 + web/src/lib/components/app-sidebar.svelte | 8 +- .../lib/components/subscription-card.svelte | 118 ++++++++---------- .../lib/components/subscription-dialog.svelte | 90 ++++++------- web/src/lib/types.ts | 61 ++++----- web/src/lib/utils.ts | 11 ++ web/src/routes/me/collections/+page.svelte | 36 +++--- web/src/routes/me/favorites/+page.svelte | 15 +-- web/src/routes/me/uppers/+page.svelte | 12 +- 14 files changed, 277 insertions(+), 242 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f29cc88..9ce3f6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -367,6 +367,7 @@ dependencies = [ "git2", "handlebars", "hex", + "itertools 0.14.0", "leaky-bucket", "md5", "memchr", @@ -1785,6 +1786,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -2468,7 +2478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" dependencies = [ "anyhow", - "itertools", + "itertools 0.12.1", "proc-macro2", "quote", "syn 2.0.96", diff --git a/Cargo.toml b/Cargo.toml index 6f8c17d..14aaf5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ futures = "0.3.31" git2 = { version = "0.20.2", features = [], default-features = false } handlebars = "6.3.2" hex = "0.4.3" +itertools = "0.14.0" leaky-bucket = "1.1.2" md5 = "0.8.0" memchr = "2.7.6" diff --git a/crates/bili_sync/Cargo.toml b/crates/bili_sync/Cargo.toml index a764cbd..f1fba40 100644 --- a/crates/bili_sync/Cargo.toml +++ b/crates/bili_sync/Cargo.toml @@ -29,6 +29,7 @@ float-ord = { workspace = true } futures = { workspace = true } handlebars = { workspace = true } hex = { workspace = true } +itertools = { workspace = true } leaky-bucket = { workspace = true } md5 = { workspace = true } memchr = { workspace = true } diff --git a/crates/bili_sync/src/api/response.rs b/crates/bili_sync/src/api/response.rs index cd97791..e7c3313 100644 --- a/crates/bili_sync/src/api/response.rs +++ b/crates/bili_sync/src/api/response.rs @@ -92,47 +92,48 @@ where } #[derive(Serialize)] -pub struct FavoriteWithSubscriptionStatus { - pub title: String, - pub media_count: i64, - pub fid: i64, - pub mid: i64, - pub subscribed: bool, -} - -#[derive(Serialize)] -pub struct CollectionWithSubscriptionStatus { - pub title: String, - pub sid: i64, - pub mid: i64, - pub invalid: bool, - pub subscribed: bool, -} - -#[derive(Serialize)] -pub struct UpperWithSubscriptionStatus { - pub mid: i64, - pub uname: String, - pub face: String, - pub sign: String, - pub invalid: bool, - pub subscribed: bool, +#[serde(tag = "type", rename_all = "snake_case")] +pub enum Followed { + Favorite { + title: String, + media_count: i64, + fid: i64, + mid: i64, + invalid: bool, + subscribed: bool, + }, + Collection { + title: String, + sid: i64, + mid: i64, + media_count: i64, + invalid: bool, + subscribed: bool, + }, + Upper { + mid: i64, + uname: String, + face: String, + sign: String, + invalid: bool, + subscribed: bool, + }, } #[derive(Serialize)] pub struct FavoritesResponse { - pub favorites: Vec, + pub favorites: Vec, } #[derive(Serialize)] pub struct CollectionsResponse { - pub collections: Vec, + pub collections: Vec, pub total: i64, } #[derive(Serialize)] pub struct UppersResponse { - pub uppers: Vec, + pub uppers: Vec, pub total: i64, } diff --git a/crates/bili_sync/src/api/routes/me/mod.rs b/crates/bili_sync/src/api/routes/me/mod.rs index fc18df5..36162d1 100644 --- a/crates/bili_sync/src/api/routes/me/mod.rs +++ b/crates/bili_sync/src/api/routes/me/mod.rs @@ -6,13 +6,11 @@ use axum::Router; use axum::extract::{Extension, Query}; use axum::routing::get; use bili_sync_entity::*; +use itertools::{Either, Itertools}; use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect}; use crate::api::request::{FollowedCollectionsRequest, FollowedUppersRequest}; -use crate::api::response::{ - CollectionWithSubscriptionStatus, CollectionsResponse, FavoriteWithSubscriptionStatus, FavoritesResponse, - UpperWithSubscriptionStatus, UppersResponse, -}; +use crate::api::response::{CollectionsResponse, FavoritesResponse, Followed, UppersResponse}; use crate::api::wrapper::{ApiError, ApiResponse}; use crate::bilibili::{BiliClient, Me}; use crate::config::VersionedConfig; @@ -36,25 +34,26 @@ pub async fn get_created_favorites( let favorites = if let Some(bili_favorites) = bili_favorites { // b 站收藏夹相关接口使用的所谓“fid”其实是该处的 id,即 fid + mid 后两位 let bili_fids: Vec<_> = bili_favorites.iter().map(|fav| fav.id).collect(); - - let subscribed_fids: Vec = favorite::Entity::find() + let subscribed_fids: HashSet = favorite::Entity::find() .select_only() .column(favorite::Column::FId) .filter(favorite::Column::FId.is_in(bili_fids)) .into_tuple() .all(&db) - .await?; - let subscribed_set: HashSet = subscribed_fids.into_iter().collect(); + .await? + .into_iter() + .collect(); bili_favorites .into_iter() - .map(|fav| FavoriteWithSubscriptionStatus { + .map(|fav| Followed::Favorite { title: fav.title, media_count: fav.media_count, // api 返回的 id 才是真实的 fid fid: fav.id, mid: fav.mid, - subscribed: subscribed_set.contains(&fav.id), + invalid: false, + subscribed: subscribed_fids.contains(&fav.id), }) .collect() } else { @@ -64,7 +63,7 @@ pub async fn get_created_favorites( Ok(ApiResponse::ok(FavoritesResponse { favorites })) } -/// 获取当前用户收藏的合集 +/// 获取当前用户收藏的合集/收藏夹 pub async fn get_followed_collections( Extension(db): Extension, Extension(bili_client): Extension>, @@ -76,25 +75,63 @@ pub async fn get_followed_collections( let bili_collections = me.get_followed_collections(page_num, page_size).await?; let collections = if let Some(collection_list) = bili_collections.list { - let bili_sids: Vec<_> = collection_list.iter().map(|col| col.id).collect(); - - let subscribed_ids: Vec = collection::Entity::find() - .select_only() - .column(collection::Column::SId) - .filter(collection::Column::SId.is_in(bili_sids)) - .into_tuple() - .all(&db) - .await?; - let subscribed_set: HashSet = subscribed_ids.into_iter().collect(); - + // collection_list 中的条目可能是合集或者收藏夹,需要分类处理 + // 目前看下来,最显著的区别是合集的 fid 是 0 + let (bili_fids, bili_sids): (Vec<_>, Vec<_>) = collection_list.iter().partition_map(|col| { + if col.fid != 0 { + Either::Left(col.id) + } else { + Either::Right(col.id) + } + }); + let (subscribed_fids, subscribed_sids): (HashSet, HashSet) = tokio::try_join!( + async { + Result::<_, anyhow::Error>::Ok( + favorite::Entity::find() + .select_only() + .column(favorite::Column::FId) + .filter(favorite::Column::FId.is_in(bili_fids)) + .into_tuple() + .all(&db) + .await? + .into_iter() + .collect(), + ) + }, + async { + Ok(collection::Entity::find() + .select_only() + .column(collection::Column::SId) + .filter(collection::Column::SId.is_in(bili_sids)) + .into_tuple() + .all(&db) + .await? + .into_iter() + .collect()) + } + )?; collection_list .into_iter() - .map(|col| CollectionWithSubscriptionStatus { - title: col.title, - sid: col.id, - mid: col.mid, - invalid: col.state == 1, - subscribed: subscribed_set.contains(&col.id), + .map(|col| { + if col.fid != 0 { + Followed::Favorite { + title: col.title, + media_count: col.media_count, + fid: col.id, + mid: col.mid, + invalid: col.state == 1, + subscribed: subscribed_fids.contains(&col.id), + } + } else { + Followed::Collection { + title: col.title, + sid: col.id, + mid: col.mid, + media_count: col.media_count, + invalid: col.state == 1, + subscribed: subscribed_sids.contains(&col.id), + } + } }) .collect() } else { @@ -132,7 +169,7 @@ pub async fn get_followed_uppers( let uppers = bili_uppers .list .into_iter() - .map(|upper| UpperWithSubscriptionStatus { + .map(|upper| Followed::Upper { mid: upper.mid, // 官方没有提供字段,但是可以使用这种方式简单判断下 invalid: upper.uname == "账号已注销" && upper.face == "https://i0.hdslb.com/bfs/face/member/noface.jpg", diff --git a/crates/bili_sync/src/bilibili/me.rs b/crates/bili_sync/src/bilibili/me.rs index 18e579a..819723b 100644 --- a/crates/bili_sync/src/bilibili/me.rs +++ b/crates/bili_sync/src/bilibili/me.rs @@ -100,9 +100,11 @@ pub struct FavoriteItem { #[derive(Debug, serde::Deserialize)] pub struct CollectionItem { pub id: i64, + pub fid: i64, pub mid: i64, pub state: i32, pub title: String, + pub media_count: i64, } #[derive(Debug, serde::Deserialize)] diff --git a/web/src/lib/components/app-sidebar.svelte b/web/src/lib/components/app-sidebar.svelte index 6cfaefe..063a85c 100644 --- a/web/src/lib/components/app-sidebar.svelte +++ b/web/src/lib/components/app-sidebar.svelte @@ -4,7 +4,7 @@ import BotIcon from '@lucide/svelte/icons/bot'; import ChartPieIcon from '@lucide/svelte/icons/chart-pie'; import HeartIcon from '@lucide/svelte/icons/heart'; - import FolderIcon from '@lucide/svelte/icons/folder'; + import FoldersIcon from '@lucide/svelte/icons/folders'; import UserIcon from '@lucide/svelte/icons/user'; import Settings2Icon from '@lucide/svelte/icons/settings-2'; import SquareTerminalIcon from '@lucide/svelte/icons/square-terminal'; @@ -62,12 +62,12 @@ href: '/me/favorites' }, { - title: '我关注的合集', - icon: FolderIcon, + title: '我追的合集 / 收藏夹', + icon: FoldersIcon, href: '/me/collections' }, { - title: '我关注的 up 主', + title: '我关注的 UP 主', icon: UserIcon, href: '/me/uppers' } diff --git a/web/src/lib/components/subscription-card.svelte b/web/src/lib/components/subscription-card.svelte index ffdf2ec..7eb201c 100644 --- a/web/src/lib/components/subscription-card.svelte +++ b/web/src/lib/components/subscription-card.svelte @@ -10,28 +10,20 @@ import CheckIcon from '@lucide/svelte/icons/check'; import PlusIcon from '@lucide/svelte/icons/plus'; import XIcon from '@lucide/svelte/icons/x'; - import type { - FavoriteWithSubscriptionStatus, - CollectionWithSubscriptionStatus, - UpperWithSubscriptionStatus - } from '$lib/types'; + import type { Followed } from '$lib/types'; - export let item: - | FavoriteWithSubscriptionStatus - | CollectionWithSubscriptionStatus - | UpperWithSubscriptionStatus; - export let type: 'favorites' | 'collections' | 'submissions' = 'favorites'; + export let item: Followed; export let onSubscriptionSuccess: (() => void) | null = null; let dialogOpen = false; function getIcon() { - switch (type) { - case 'favorites': + switch (item.type) { + case 'favorite': return HeartIcon; - case 'collections': + case 'collection': return FolderIcon; - case 'submissions': + case 'upper': return UserIcon; default: return VideoIcon; @@ -39,12 +31,12 @@ } function getTypeLabel() { - switch (type) { - case 'favorites': + switch (item.type) { + case 'favorite': return '收藏夹'; - case 'collections': + case 'collection': return '合集'; - case 'submissions': + case 'upper': return 'UP 主'; default: return ''; @@ -52,55 +44,52 @@ } function getTitle(): string { - switch (type) { - case 'favorites': - return (item as FavoriteWithSubscriptionStatus).title; - case 'collections': - return (item as CollectionWithSubscriptionStatus).title; - case 'submissions': - return (item as UpperWithSubscriptionStatus).uname; + switch (item.type) { + case 'favorite': + case 'collection': + return item.title; + case 'upper': + return item.uname; default: return ''; } } function getSubtitle(): string { - switch (type) { - case 'favorites': - return `uid: ${(item as FavoriteWithSubscriptionStatus).mid}`; - case 'collections': - return `uid: ${(item as CollectionWithSubscriptionStatus).mid}`; + switch (item.type) { + case 'favorite': + case 'collection': + return `UID:${item.mid}`; default: return ''; } } function getDescription(): string { - switch (type) { - case 'submissions': - return (item as UpperWithSubscriptionStatus).sign || ''; + switch (item.type) { + case 'upper': + return item.sign || ''; default: return ''; } } function isDisabled(): boolean { - switch (type) { - case 'collections': - return (item as CollectionWithSubscriptionStatus).invalid; - case 'submissions': { - return (item as UpperWithSubscriptionStatus).invalid; - } + switch (item.type) { + case 'collection': + case 'upper': + case 'favorite': + return item.invalid; default: return false; } } function getDisabledReason(): string { - switch (type) { - case 'collections': + switch (item.type) { + case 'collection': return '已失效'; - case 'submissions': + case 'upper': return '账号已注销'; default: return ''; @@ -108,22 +97,19 @@ } function getCount(): number | null { - switch (type) { - case 'favorites': - return (item as FavoriteWithSubscriptionStatus).media_count; + switch (item.type) { + case 'favorite': + case 'collection': + return item.media_count; default: return null; } } - function getCountLabel(): string { - return '个视频'; - } - function getAvatarUrl(): string { - switch (type) { - case 'submissions': - return (item as UpperWithSubscriptionStatus).face; + switch (item.type) { + case 'upper': + return item.face; default: return ''; } @@ -149,7 +135,6 @@ const subtitle = getSubtitle(); const description = getDescription(); const count = getCount(); - const countLabel = getCountLabel(); const avatarUrl = getAvatarUrl(); const subscribed = item.subscribed; const disabled = isDisabled(); @@ -161,7 +146,7 @@ ? 'opacity-60' : ''}" > - +
- {#if avatarUrl && type === 'submissions'} + {#if avatarUrl && item.type === 'upper'} {title}不可用 {:else} - + {subscribed ? '已订阅' : typeLabel} {/if} @@ -211,25 +196,26 @@
{/if} + + {#if count !== null && !disabled} +
+ + 视频数:{count} +
+ {/if} + + {#if description && !disabled}

{description}

{/if} - - - {#if count !== null && !disabled} -
- {count} - {countLabel} -
- {/if}
- +
{#if disabled}
- {#if type === 'favorites'} - {@const favorite = item as FavoriteWithSubscriptionStatus} + {#if item!.type !== 'upper'}
视频数量: - {favorite.media_count} 个 + {item!.media_count} 条 +
+ {:else if item!.sign} +
+ 个人简介: + {item!.sign}
- {/if} - {#if type === 'submissions'} - {@const upper = item as UpperWithSubscriptionStatus} - {#if upper.sign} -
- 个人简介: - {upper.sign} -
- {/if} {/if} diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 3e2f124..0ee05cd 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -110,45 +110,46 @@ export interface ResetRequest { force: boolean; } -// 收藏夹相关类型 -export interface FavoriteWithSubscriptionStatus { - title: string; - media_count: number; - fid: number; - mid: number; - subscribed: boolean; -} +export type Followed = + | { + type: 'favorite'; + title: string; + media_count: number; + fid: number; + mid: number; + invalid: boolean; + subscribed: boolean; + } + | { + type: 'collection'; + title: string; + sid: number; + mid: number; + media_count: number; + invalid: boolean; + subscribed: boolean; + } + | { + type: 'upper'; + mid: number; + uname: string; + face: string; + sign: string; + invalid: boolean; + subscribed: boolean; + }; export interface FavoritesResponse { - favorites: FavoriteWithSubscriptionStatus[]; -} - -// 合集相关类型 -export interface CollectionWithSubscriptionStatus { - title: string; - sid: number; - mid: number; - invalid: boolean; - subscribed: boolean; + favorites: Followed[]; } export interface CollectionsResponse { - collections: CollectionWithSubscriptionStatus[]; + collections: Followed[]; total: number; } -// UP 主相关类型 -export interface UpperWithSubscriptionStatus { - mid: number; - uname: string; - face: string; - sign: string; - invalid: boolean; - subscribed: boolean; -} - export interface UppersResponse { - uppers: UpperWithSubscriptionStatus[]; + uppers: Followed[]; total: number; } diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index f92bfcb..d2af642 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -1,5 +1,16 @@ import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; +import type { Followed } from './types'; + +export function getFollowedKey(followed: Followed): number { + if (followed.type == 'favorite') { + return followed.fid; + } else if (followed.type == 'collection') { + return followed.sid; + } else { + return followed.mid; + } +} export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); diff --git a/web/src/routes/me/collections/+page.svelte b/web/src/routes/me/collections/+page.svelte index ea91e1f..c1ab62d 100644 --- a/web/src/routes/me/collections/+page.svelte +++ b/web/src/routes/me/collections/+page.svelte @@ -5,9 +5,10 @@ import Pagination from '$lib/components/pagination.svelte'; import { setBreadcrumb } from '$lib/stores/breadcrumb'; import api from '$lib/api'; - import type { CollectionWithSubscriptionStatus, ApiError } from '$lib/types'; + import type { Followed, ApiError } from '$lib/types'; + import { getFollowedKey } from '$lib/utils'; - let collections: CollectionWithSubscriptionStatus[] = []; + let collections: Followed[] = []; let totalCount = 0; let currentPage = 0; let loading = false; @@ -21,8 +22,8 @@ collections = response.data.collections; totalCount = response.data.total; } catch (error) { - console.error('加载合集失败:', error); - toast.error('加载合集失败', { + console.error('加载合集 / 收藏夹失败:', error); + toast.error('加载合集 / 收藏夹失败', { description: (error as ApiError).message }); } finally { @@ -43,7 +44,7 @@ onMount(async () => { setBreadcrumb([ { - label: '我关注的合集' + label: '我追的合集 / 收藏夹' } ]); await loadCollections(); @@ -53,14 +54,19 @@ - 关注的合集 - Bili Sync + 我追的合集 / 收藏夹 - Bili Sync
-
+
{#if !loading} - 共 {totalCount} 个合集 +
+ 共 {totalCount} 个合集 / 收藏夹 +
+
+ 当前第 {currentPage + 1} / {totalPages} 页 +
{/if}
@@ -73,13 +79,9 @@
- {#each collections as collection (collection.sid)} + {#each collections as collection (getFollowedKey(collection))}
- +
{/each}
@@ -91,8 +93,10 @@ {:else}
-

暂无合集数据

-

请先在 B 站关注一些合集,或检查账号配置

+

暂无合集 / 收藏夹数据

+

+ 请先在 B 站关注一些合集 / 收藏夹,或检查账号配置 +

{/if} diff --git a/web/src/routes/me/favorites/+page.svelte b/web/src/routes/me/favorites/+page.svelte index 588ae51..c721205 100644 --- a/web/src/routes/me/favorites/+page.svelte +++ b/web/src/routes/me/favorites/+page.svelte @@ -6,9 +6,10 @@ import { setBreadcrumb } from '$lib/stores/breadcrumb'; import api from '$lib/api'; - import type { FavoriteWithSubscriptionStatus, ApiError } from '$lib/types'; + import type { Followed, ApiError } from '$lib/types'; + import { getFollowedKey } from '$lib/utils'; - let favorites: FavoriteWithSubscriptionStatus[] = []; + let favorites: Followed[] = []; let loading = false; async function loadFavorites() { @@ -39,7 +40,7 @@ - 我的收藏夹 - Bili Sync + 我创建的收藏夹 - Bili Sync
@@ -59,13 +60,9 @@
- {#each favorites as favorite (favorite.fid)} + {#each favorites as favorite (getFollowedKey(favorite))}
- +
{/each}
diff --git a/web/src/routes/me/uppers/+page.svelte b/web/src/routes/me/uppers/+page.svelte index 5495dc9..88030f6 100644 --- a/web/src/routes/me/uppers/+page.svelte +++ b/web/src/routes/me/uppers/+page.svelte @@ -5,9 +5,9 @@ import Pagination from '$lib/components/pagination.svelte'; import { setBreadcrumb } from '$lib/stores/breadcrumb'; import api from '$lib/api'; - import type { UpperWithSubscriptionStatus, ApiError } from '$lib/types'; + import type { Followed, ApiError } from '$lib/types'; - let uppers: UpperWithSubscriptionStatus[] = []; + let uppers: Followed[] = []; let totalCount = 0; let currentPage = 0; let loading = false; @@ -49,7 +49,7 @@ - 关注的UP主 - Bili Sync + 我关注的 UP 主 - Bili Sync
@@ -76,11 +76,7 @@ > {#each uppers as upper (upper.mid)}
- +
{/each}