diff --git a/components/VideoCard.tsx b/components/VideoCard.tsx
index c58592d..bf506e7 100644
--- a/components/VideoCard.tsx
+++ b/components/VideoCard.tsx
@@ -29,9 +29,9 @@ export function VideoCard({ item, onPress }: Props) {
{item.title}
- {formatCount(item.stat.view)}
+ {formatCount(item.stat?.view ?? 0)}
- {item.owner.name}
+ {item.owner?.name ?? ''}
);
diff --git a/hooks/useVideoList.ts b/hooks/useVideoList.ts
index 8adc6b8..8eb3bd2 100644
--- a/hooks/useVideoList.ts
+++ b/hooks/useVideoList.ts
@@ -1,32 +1,31 @@
import { useState, useCallback } from 'react';
-import { getPopularVideos } from '../services/bilibili';
+import { getRecommendFeed } from '../services/bilibili';
import type { VideoItem } from '../services/types';
export function useVideoList() {
const [videos, setVideos] = useState([]);
- const [page, setPage] = useState(1);
+ const [freshIdx, setFreshIdx] = useState(0);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const load = useCallback(async (reset = false) => {
if (loading) return;
- const nextPage = reset ? 1 : page;
+ const idx = reset ? 0 : freshIdx;
setLoading(true);
try {
- const data = await getPopularVideos(nextPage);
+ const data = await getRecommendFeed(idx);
setVideos(prev => reset ? data : [...prev, ...data]);
- setPage(nextPage + 1);
+ setFreshIdx(idx + 1);
} catch (e) {
console.error('Failed to load videos', e);
} finally {
setLoading(false);
setRefreshing(false);
}
- }, [loading, page]);
+ }, [loading, freshIdx]);
const refresh = useCallback(() => {
setRefreshing(true);
- setPage(1);
load(true);
}, [load]);
diff --git a/services/bilibili.ts b/services/bilibili.ts
index 28d3aaf..4095188 100644
--- a/services/bilibili.ts
+++ b/services/bilibili.ts
@@ -2,6 +2,7 @@ import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Platform } from 'react-native';
import type { VideoItem, Comment, PlayUrlResponse, QRCodeInfo } from './types';
+import { signWbi } from '../utils/wbi';
const isWeb = Platform.OS === 'web';
const BASE = isWeb ? 'http://localhost:3001/bilibili-api' : 'https://api.bilibili.com';
@@ -54,6 +55,35 @@ api.interceptors.request.use(async (config) => {
return config;
});
+// WBI key cache (rotates ~daily, reuse within process lifetime)
+let wbiKeys: { imgKey: string; subKey: string } | null = null;
+
+async function getWbiKeys(): Promise<{ imgKey: string; subKey: string }> {
+ if (wbiKeys) return wbiKeys;
+ const res = await api.get('/x/web-interface/nav');
+ const { img_url, sub_url } = res.data.data.wbi_img;
+ const extract = (url: string) => url.split('/').pop()!.replace(/\.\w+$/, '');
+ wbiKeys = { imgKey: extract(img_url), subKey: extract(sub_url) };
+ return wbiKeys;
+}
+
+export async function getRecommendFeed(freshIdx = 0): Promise {
+ const { imgKey, subKey } = await getWbiKeys();
+ const signed = signWbi(
+ { fresh_type: 3, fresh_idx: freshIdx, fresh_idx_1h: freshIdx, ps: 12, feed_version: 'V8' },
+ imgKey,
+ subKey,
+ );
+ const res = await api.get('/x/web-interface/wbi/index/top/feed/rcmd', { params: signed });
+ const items: any[] = res.data.data?.item ?? [];
+ return items.map(item => ({
+ ...item,
+ aid: item.id ?? item.aid,
+ pic: item.pic ?? item.cover,
+ owner: item.owner ?? { mid: 0, name: item.owner_info?.name ?? '', face: item.owner_info?.face ?? '' },
+ })) as VideoItem[];
+}
+
export async function getPopularVideos(pn = 1): Promise {
const res = await api.get('/x/web-interface/popular', { params: { pn, ps: 20 } });
return res.data.data.list as VideoItem[];
diff --git a/services/types.ts b/services/types.ts
index 28a3146..a702f83 100644
--- a/services/types.ts
+++ b/services/types.ts
@@ -8,17 +8,17 @@ export interface VideoItem {
name: string;
face: string;
};
- stat: {
+ stat?: {
view: number;
danmaku: number;
reply: number;
like: number;
coin: number;
favorite: number;
- };
+ } | null;
duration: number;
desc: string;
- cid: number;
+ cid?: number;
pages?: Array<{ cid: number; part: string }>;
}
diff --git a/utils/wbi.ts b/utils/wbi.ts
new file mode 100644
index 0000000..42f07e5
--- /dev/null
+++ b/utils/wbi.ts
@@ -0,0 +1,79 @@
+// WBI signature algorithm for Bilibili API
+// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md
+
+const MIXIN_KEY_ENC_TAB = [
+ 46,47,18,2,53,8,23,32,15,50,10,31,58,3,45,35,
+ 27,43,5,49,33,9,42,19,29,28,14,39,12,38,41,13,
+ 37,48,7,16,24,55,40,61,26,17,0,1,60,51,30,4,
+ 22,25,54,21,56,59,6,63,57,62,11,36,20,34,44,52,
+];
+
+function getMixinKey(imgKey: string, subKey: string): string {
+ const raw = imgKey + subKey;
+ return MIXIN_KEY_ENC_TAB.slice(0, 32).map(i => raw[i]).join('');
+}
+
+// Minimal MD5 implementation (no dependencies)
+function md5(input: string): string {
+ function safeAdd(x: number, y: number) { const lsw=(x&0xffff)+(y&0xffff); const msw=(x>>16)+(y>>16)+(lsw>>16); return (msw<<16)|(lsw&0xffff); }
+ function bitRotateLeft(num: number, cnt: number) { return (num<>>(32-cnt)); }
+ function md5cmn(q:number,a:number,b:number,x:number,s:number,t:number){return safeAdd(bitRotateLeft(safeAdd(safeAdd(a,q),safeAdd(x,t)),s),b);}
+ function md5ff(a:number,b:number,c:number,d:number,x:number,s:number,t:number){return md5cmn((b&c)|(~b&d),a,b,x,s,t);}
+ function md5gg(a:number,b:number,c:number,d:number,x:number,s:number,t:number){return md5cmn((b&d)|(c&~d),a,b,x,s,t);}
+ function md5hh(a:number,b:number,c:number,d:number,x:number,s:number,t:number){return md5cmn(b^c^d,a,b,x,s,t);}
+ function md5ii(a:number,b:number,c:number,d:number,x:number,s:number,t:number){return md5cmn(c^(b|~d),a,b,x,s,t);}
+
+ function md5blks(s: string): number[] {
+ const nblk = ((s.length+8)>>6)+1;
+ const blks = new Array(nblk*16).fill(0);
+ for(let i=0;i>2]|=s.charCodeAt(i)<<((i%4)*8);
+ blks[s.length>>2]|=0x80<<((s.length%4)*8);
+ blks[nblk*16-2]=s.length*8;
+ return blks;
+ }
+
+ const x = md5blks(input);
+ let a=1732584193,b=-271733879,c=-1732584194,d=271733878;
+ for(let i=0;i>8)&0xff).toString(16)).slice(-2)+('0'+((n>>16)&0xff).toString(16)).slice(-2)+('0'+((n>>24)&0xff).toString(16)).slice(-2); }
+ return hex(a)+hex(b)+hex(c)+hex(d);
+}
+
+export function signWbi(
+ params: Record,
+ imgKey: string,
+ subKey: string,
+): Record {
+ const mixinKey = getMixinKey(imgKey, subKey);
+ const wts = Math.floor(Date.now() / 1000);
+ const all: Record = { ...params, wts };
+ const query = Object.keys(all)
+ .sort()
+ .map(k => {
+ const v = String(all[k]).replace(/[!'()*]/g, '');
+ return `${k}=${v}`;
+ })
+ .join('&');
+ const w_rid = md5(query + mixinKey);
+ return { ...all, w_rid };
+}