mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-08 07:28:37 +08:00
1
This commit is contained in:
3
.claude/commands/add-doc.md
Normal file
3
.claude/commands/add-doc.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
给代码加注释 $FILE_NAME
|
||||||
|
1.每一行代码上方加上注释
|
||||||
|
2.注释内容需要简洁,使用中文
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Tabs } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
|
import { View } from 'react-native';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
|
import { MiniPlayer } from '../components/MiniPlayer';
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
const restore = useAuthStore(s => s.restore);
|
const restore = useAuthStore(s => s.restore);
|
||||||
@@ -15,28 +16,10 @@ export default function RootLayout() {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<StatusBar style="dark" />
|
<StatusBar style="dark" />
|
||||||
<Tabs
|
<View style={{ flex: 1 }}>
|
||||||
screenOptions={{
|
<Stack screenOptions={{ headerShown: false }} />
|
||||||
tabBarActiveTintColor: '#00AEEC',
|
<MiniPlayer />
|
||||||
tabBarInactiveTintColor: '#999',
|
</View>
|
||||||
tabBarStyle: { borderTopColor: '#eee' },
|
|
||||||
headerShown: false,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tabs.Screen
|
|
||||||
name="index"
|
|
||||||
options={{
|
|
||||||
title: '首页',
|
|
||||||
tabBarIcon: ({ color, size }: { color: string; size: number }) => (
|
|
||||||
<Ionicons name="home" size={size} color={color} />
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tabs.Screen
|
|
||||||
name="video"
|
|
||||||
options={{ href: null }}
|
|
||||||
/>
|
|
||||||
</Tabs>
|
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
124
app/index.tsx
124
app/index.tsx
@@ -1,9 +1,9 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
View, FlatList, StyleSheet,
|
View, StyleSheet,
|
||||||
Text, TouchableOpacity, ActivityIndicator, Dimensions
|
Text, TouchableOpacity, ActivityIndicator, Animated, Image,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { VideoCard } from '../components/VideoCard';
|
import { VideoCard } from '../components/VideoCard';
|
||||||
@@ -12,11 +12,26 @@ import { useVideoList } from '../hooks/useVideoList';
|
|||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
import type { VideoItem } from '../services/types';
|
import type { VideoItem } from '../services/types';
|
||||||
|
|
||||||
|
const HEADER_H = 44;
|
||||||
|
const TAB_H = 38;
|
||||||
|
const NAV_H = HEADER_H + TAB_H;
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { videos, loading, refreshing, load, refresh } = useVideoList();
|
const { videos, loading, refreshing, load, refresh } = useVideoList();
|
||||||
const { isLoggedIn, logout } = useAuthStore();
|
const { isLoggedIn, face, logout } = useAuthStore();
|
||||||
const [showLogin, setShowLogin] = useState(false);
|
const [showLogin, setShowLogin] = useState(false);
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
const scrollY = useRef(new Animated.Value(0)).current;
|
||||||
|
const clampedScroll = useRef(
|
||||||
|
Animated.diffClamp(scrollY, 0, NAV_H)
|
||||||
|
).current;
|
||||||
|
const headerTranslate = clampedScroll.interpolate({
|
||||||
|
inputRange: [0, NAV_H],
|
||||||
|
outputRange: [0, -NAV_H],
|
||||||
|
extrapolate: 'clamp',
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => { load(); }, []);
|
useEffect(() => { load(); }, []);
|
||||||
|
|
||||||
@@ -30,7 +45,34 @@ export default function HomeScreen() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safe}>
|
<SafeAreaView style={styles.safe} edges={['left', 'right']}>
|
||||||
|
<Animated.FlatList
|
||||||
|
style={styles.listContainer}
|
||||||
|
data={videos}
|
||||||
|
keyExtractor={(item, index) => `${item.bvid}-${index}`}
|
||||||
|
numColumns={2}
|
||||||
|
columnWrapperStyle={styles.row}
|
||||||
|
contentContainerStyle={{ paddingTop: insets.top + NAV_H + 8, paddingBottom: insets.bottom + 16 }}
|
||||||
|
renderItem={renderItem}
|
||||||
|
onRefresh={refresh}
|
||||||
|
refreshing={refreshing}
|
||||||
|
onEndReached={() => load()}
|
||||||
|
onEndReachedThreshold={0.5}
|
||||||
|
ListFooterComponent={loading ? <ActivityIndicator style={styles.footer} color="#00AEEC" /> : null}
|
||||||
|
onScroll={Animated.event(
|
||||||
|
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
|
||||||
|
{ useNativeDriver: true }
|
||||||
|
)}
|
||||||
|
scrollEventThrottle={16}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 绝对定位导航栏:paddingTop 手动适配刘海/状态栏 */}
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.navBar,
|
||||||
|
{ paddingTop: insets.top, transform: [{ translateY: headerTranslate }] },
|
||||||
|
]}
|
||||||
|
>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={styles.logo}>哔哩哔哩</Text>
|
<Text style={styles.logo}>哔哩哔哩</Text>
|
||||||
<View style={styles.headerRight}>
|
<View style={styles.headerRight}>
|
||||||
@@ -41,7 +83,11 @@ export default function HomeScreen() {
|
|||||||
style={styles.headerBtn}
|
style={styles.headerBtn}
|
||||||
onPress={() => isLoggedIn ? logout() : setShowLogin(true)}
|
onPress={() => isLoggedIn ? logout() : setShowLogin(true)}
|
||||||
>
|
>
|
||||||
|
{isLoggedIn && face ? (
|
||||||
|
<Image source={{ uri: face }} style={styles.userAvatar} />
|
||||||
|
) : (
|
||||||
<Ionicons name={isLoggedIn ? 'person' : 'person-outline'} size={22} color="#00AEEC" />
|
<Ionicons name={isLoggedIn ? 'person' : 'person-outline'} size={22} color="#00AEEC" />
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -50,20 +96,7 @@ export default function HomeScreen() {
|
|||||||
<Text style={styles.tabActive}>热门</Text>
|
<Text style={styles.tabActive}>热门</Text>
|
||||||
<View style={styles.tabUnderline} />
|
<View style={styles.tabUnderline} />
|
||||||
</View>
|
</View>
|
||||||
|
</Animated.View>
|
||||||
<FlatList
|
|
||||||
data={videos}
|
|
||||||
keyExtractor={(item, index) => `${item.bvid}-${index}`}
|
|
||||||
numColumns={2}
|
|
||||||
columnWrapperStyle={styles.row}
|
|
||||||
contentContainerStyle={styles.list}
|
|
||||||
renderItem={renderItem}
|
|
||||||
onRefresh={refresh}
|
|
||||||
refreshing={refreshing}
|
|
||||||
onEndReached={() => load()}
|
|
||||||
onEndReachedThreshold={0.5}
|
|
||||||
ListFooterComponent={loading ? <ActivityIndicator style={styles.footer} color="#00AEEC" /> : null}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LoginModal visible={showLogin} onClose={() => setShowLogin(false)} />
|
<LoginModal visible={showLogin} onClose={() => setShowLogin(false)} />
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
@@ -72,15 +105,54 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
safe: { flex: 1, backgroundColor: '#f4f4f4' },
|
safe: { flex: 1, backgroundColor: '#f4f4f4' },
|
||||||
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 14, paddingVertical: 10, backgroundColor: '#fff', borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: '#eee' },
|
listContainer: { flex: 1 },
|
||||||
|
navBar: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
zIndex: 10,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
overflow: 'hidden',
|
||||||
|
// 安卓投影
|
||||||
|
elevation: 2,
|
||||||
|
// iOS 投影
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.06,
|
||||||
|
shadowRadius: 2,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
height: HEADER_H,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderBottomColor: '#eee',
|
||||||
|
},
|
||||||
logo: { fontSize: 20, fontWeight: '800', color: '#00AEEC', letterSpacing: -0.5 },
|
logo: { fontSize: 20, fontWeight: '800', color: '#00AEEC', letterSpacing: -0.5 },
|
||||||
headerRight: { flexDirection: 'row', gap: 8 },
|
headerRight: { flexDirection: 'row', gap: 8 },
|
||||||
headerBtn: { padding: 4 },
|
headerBtn: { padding: 6 },
|
||||||
tabRow: { backgroundColor: '#fff', paddingHorizontal: 16, paddingBottom: 0, flexDirection: 'row', alignItems: 'center', position: 'relative' },
|
userAvatar: { width: 28, height: 28, borderRadius: 14, backgroundColor: '#eee' },
|
||||||
tabActive: { fontSize: 15, fontWeight: '700', color: '#00AEEC', paddingVertical: 10 },
|
tabRow: {
|
||||||
tabUnderline: { position: 'absolute', bottom: 0, left: 16, width: 24, height: 2, backgroundColor: '#00AEEC', borderRadius: 1 },
|
height: TAB_H,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
tabActive: { fontSize: 15, fontWeight: '700', color: '#00AEEC' },
|
||||||
|
tabUnderline: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
left: 16,
|
||||||
|
width: 24,
|
||||||
|
height: 2,
|
||||||
|
backgroundColor: '#00AEEC',
|
||||||
|
borderRadius: 1,
|
||||||
|
},
|
||||||
row: { paddingHorizontal: 8 },
|
row: { paddingHorizontal: 8 },
|
||||||
list: { paddingTop: 8, paddingBottom: 80 },
|
|
||||||
leftCol: { marginLeft: 4, marginRight: 2 },
|
leftCol: { marginLeft: 4, marginRight: 2 },
|
||||||
rightCol: { marginLeft: 2, marginRight: 4 },
|
rightCol: { marginLeft: 2, marginRight: 4 },
|
||||||
footer: { marginVertical: 16 },
|
footer: { marginVertical: 16 },
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { VideoPlayer } from '../../components/VideoPlayer';
|
|||||||
import { CommentItem } from '../../components/CommentItem';
|
import { CommentItem } from '../../components/CommentItem';
|
||||||
import { useVideoDetail } from '../../hooks/useVideoDetail';
|
import { useVideoDetail } from '../../hooks/useVideoDetail';
|
||||||
import { useComments } from '../../hooks/useComments';
|
import { useComments } from '../../hooks/useComments';
|
||||||
|
import { useVideoStore } from '../../store/videoStore';
|
||||||
import { formatCount } from '../../utils/format';
|
import { formatCount } from '../../utils/format';
|
||||||
|
|
||||||
type Tab = 'intro' | 'comments';
|
type Tab = 'intro' | 'comments';
|
||||||
@@ -17,14 +18,26 @@ type Tab = 'intro' | 'comments';
|
|||||||
export default function VideoDetailScreen() {
|
export default function VideoDetailScreen() {
|
||||||
const { bvid } = useLocalSearchParams<{ bvid: string }>();
|
const { bvid } = useLocalSearchParams<{ bvid: string }>();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { video, streamUrl, loading: videoLoading } = useVideoDetail(bvid as string);
|
const { video, playData, loading: videoLoading, qualities, currentQn, changeQuality } = useVideoDetail(bvid as string);
|
||||||
const { comments, loading: cmtLoading, load: loadComments } = useComments(video?.aid ?? 0);
|
const { comments, loading: cmtLoading, load: loadComments } = useComments(video?.aid ?? 0);
|
||||||
const [tab, setTab] = useState<Tab>('comments');
|
const [tab, setTab] = useState<Tab>('comments');
|
||||||
|
const { setVideo, clearVideo } = useVideoStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
clearVideo();
|
||||||
|
}, [bvid]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (video?.aid) loadComments();
|
if (video?.aid) loadComments();
|
||||||
}, [video?.aid]);
|
}, [video?.aid]);
|
||||||
|
|
||||||
|
function handleMiniPlayer() {
|
||||||
|
if (video) {
|
||||||
|
setVideo(bvid as string, video.title, video.pic);
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safe}>
|
<SafeAreaView style={styles.safe}>
|
||||||
<View style={styles.topBar}>
|
<View style={styles.topBar}>
|
||||||
@@ -32,9 +45,18 @@ export default function VideoDetailScreen() {
|
|||||||
<Ionicons name="chevron-back" size={24} color="#212121" />
|
<Ionicons name="chevron-back" size={24} color="#212121" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.topTitle} numberOfLines={1}>{video?.title ?? '视频详情'}</Text>
|
<Text style={styles.topTitle} numberOfLines={1}>{video?.title ?? '视频详情'}</Text>
|
||||||
|
<TouchableOpacity style={styles.miniBtn} onPress={handleMiniPlayer}>
|
||||||
|
<Ionicons name="copy-outline" size={22} color="#212121" />
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<VideoPlayer uri={streamUrl} />
|
<VideoPlayer
|
||||||
|
playData={playData}
|
||||||
|
qualities={qualities}
|
||||||
|
currentQn={currentQn}
|
||||||
|
onQualityChange={changeQuality}
|
||||||
|
onMiniPlayer={handleMiniPlayer}
|
||||||
|
/>
|
||||||
|
|
||||||
<ScrollView style={styles.scroll} showsVerticalScrollIndicator={false}>
|
<ScrollView style={styles.scroll} showsVerticalScrollIndicator={false}>
|
||||||
{videoLoading ? (
|
{videoLoading ? (
|
||||||
@@ -111,6 +133,7 @@ const styles = StyleSheet.create({
|
|||||||
topBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8, paddingVertical: 8, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: '#eee' },
|
topBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8, paddingVertical: 8, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: '#eee' },
|
||||||
backBtn: { padding: 4 },
|
backBtn: { padding: 4 },
|
||||||
topTitle: { flex: 1, fontSize: 15, fontWeight: '600', marginLeft: 4, color: '#212121' },
|
topTitle: { flex: 1, fontSize: 15, fontWeight: '600', marginLeft: 4, color: '#212121' },
|
||||||
|
miniBtn: { padding: 4 },
|
||||||
scroll: { flex: 1 },
|
scroll: { flex: 1 },
|
||||||
loader: { marginVertical: 30 },
|
loader: { marginVertical: 30 },
|
||||||
titleSection: { padding: 14 },
|
titleSection: { padding: 14 },
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { Stack } from 'expo-router';
|
import { Slot } from 'expo-router';
|
||||||
|
|
||||||
export default function VideoLayout() {
|
export default function VideoLayout() {
|
||||||
return (
|
return <Slot />;
|
||||||
<Stack screenOptions={{ headerShown: false }}>
|
|
||||||
<Stack.Screen name="[bvid]" />
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import { Modal, View, Text, StyleSheet, TouchableOpacity, Image, ActivityIndicator } from 'react-native';
|
import { Modal, View, Text, StyleSheet, TouchableOpacity, Image, ActivityIndicator } from 'react-native';
|
||||||
import { generateQRCode, pollQRCode } from '../services/bilibili';
|
import { generateQRCode, pollQRCode, getUserInfo } from '../services/bilibili';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -14,6 +14,7 @@ export function LoginModal({ visible, onClose }: Props) {
|
|||||||
const [status, setStatus] = useState<'loading' | 'waiting' | 'scanned' | 'done' | 'error'>('loading');
|
const [status, setStatus] = useState<'loading' | 'waiting' | 'scanned' | 'done' | 'error'>('loading');
|
||||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
const login = useAuthStore(s => s.login);
|
const login = useAuthStore(s => s.login);
|
||||||
|
const setProfile = useAuthStore(s => s.setProfile);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!visible) return;
|
if (!visible) return;
|
||||||
@@ -39,6 +40,8 @@ export function LoginModal({ visible, onClose }: Props) {
|
|||||||
clearInterval(pollRef.current!);
|
clearInterval(pollRef.current!);
|
||||||
await login(result.cookie, '', '');
|
await login(result.cookie, '', '');
|
||||||
setStatus('done');
|
setStatus('done');
|
||||||
|
// 登录后异步拉取用户头像和昵称
|
||||||
|
getUserInfo().then(info => setProfile(info.face, info.uname, String(info.mid))).catch(() => {});
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|||||||
90
components/MiniPlayer.tsx
Normal file
90
components/MiniPlayer.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import React, { useRef } from 'react';
|
||||||
|
import {
|
||||||
|
View, Text, Image, StyleSheet, TouchableOpacity,
|
||||||
|
Animated, PanResponder,
|
||||||
|
} from 'react-native';
|
||||||
|
import { useRouter } from 'expo-router';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
|
import { useVideoStore } from '../store/videoStore';
|
||||||
|
|
||||||
|
export function MiniPlayer() {
|
||||||
|
const { isActive, bvid, title, cover, clearVideo } = useVideoStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
const pan = useRef(new Animated.ValueXY()).current;
|
||||||
|
|
||||||
|
const panResponder = useRef(
|
||||||
|
PanResponder.create({
|
||||||
|
onStartShouldSetPanResponder: () => true,
|
||||||
|
onPanResponderMove: Animated.event([null, { dx: pan.x, dy: pan.y }], { useNativeDriver: false }),
|
||||||
|
onPanResponderRelease: () => {
|
||||||
|
pan.flattenOffset();
|
||||||
|
},
|
||||||
|
onPanResponderGrant: () => {
|
||||||
|
pan.setOffset({ x: (pan.x as any)._value, y: (pan.y as any)._value });
|
||||||
|
pan.setValue({ x: 0, y: 0 });
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).current;
|
||||||
|
|
||||||
|
if (!isActive) return null;
|
||||||
|
|
||||||
|
const bottomOffset = insets.bottom + 16;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
style={[styles.container, { bottom: bottomOffset, transform: pan.getTranslateTransform() }]}
|
||||||
|
{...panResponder.panHandlers}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.main}
|
||||||
|
onPress={() => router.push(`/video/${bvid}` as any)}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
>
|
||||||
|
<Image source={{ uri: cover }} style={styles.cover} />
|
||||||
|
<Text style={styles.title} numberOfLines={1}>{title}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity style={styles.closeBtn} onPress={clearVideo}>
|
||||||
|
<Ionicons name="close" size={14} color="#fff" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
position: 'absolute',
|
||||||
|
right: 12,
|
||||||
|
width: 160,
|
||||||
|
height: 90,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: '#1a1a1a',
|
||||||
|
overflow: 'hidden',
|
||||||
|
elevation: 8,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 4,
|
||||||
|
},
|
||||||
|
main: { flex: 1 },
|
||||||
|
cover: { width: '100%', height: 64, backgroundColor: '#333' },
|
||||||
|
title: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 11,
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
paddingVertical: 4,
|
||||||
|
lineHeight: 14,
|
||||||
|
},
|
||||||
|
closeBtn: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 4,
|
||||||
|
right: 4,
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
borderRadius: 9,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,38 +1,87 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, StyleSheet, Dimensions } from 'react-native';
|
import {
|
||||||
|
View, StyleSheet, Dimensions, TouchableOpacity,
|
||||||
|
Text, Modal, FlatList,
|
||||||
|
} from 'react-native';
|
||||||
import { WebView } from 'react-native-webview';
|
import { WebView } from 'react-native-webview';
|
||||||
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
|
import { buildMpd } from '../utils/buildMpd';
|
||||||
|
import type { PlayUrlResponse } from '../services/types';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
const VIDEO_HEIGHT = width * 0.5625;
|
const VIDEO_HEIGHT = width * 0.5625;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
uri: string;
|
playData: PlayUrlResponse | null;
|
||||||
|
qualities: { qn: number; desc: string }[];
|
||||||
|
currentQn: number;
|
||||||
|
onQualityChange: (qn: number) => void;
|
||||||
|
onFullscreen: () => void;
|
||||||
|
onMiniPlayer?: () => void;
|
||||||
|
style?: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildHtml = (uri: string) => `
|
function buildMp4Html(url: string): string {
|
||||||
<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<style>
|
<style>* { margin:0; padding:0; box-sizing:border-box; background:#000; } video { width:100vw; height:100vh; object-fit:contain; display:block; }</style>
|
||||||
* { margin:0; padding:0; box-sizing:border-box; background:#000; }
|
|
||||||
video { width:100vw; height:100vh; object-fit:contain; display:block; }
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<video id="v" controls autoplay playsinline webkit-playsinline></video>
|
<video id="v" controls autoplay playsinline webkit-playsinline></video>
|
||||||
|
<script>document.getElementById('v').src = ${JSON.stringify(url)};</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDashHtml(mpdStr: string): string {
|
||||||
|
const mpdBase64 = `data:application/dash+xml;base64,${btoa(unescape(encodeURIComponent(mpdStr)))}`;
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<style>* { margin:0; padding:0; box-sizing:border-box; background:#000; } video { width:100vw; height:100vh; object-fit:contain; display:block; }</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<video id="v" autoplay controls playsinline webkit-playsinline></video>
|
||||||
|
<script src="https://cdn.dashjs.org/latest/dash.all.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.getElementById('v').src = ${JSON.stringify(uri)};
|
var player = dashjs.MediaPlayer().create();
|
||||||
|
player.initialize(document.getElementById('v'), ${JSON.stringify(mpdBase64)}, true);
|
||||||
|
player.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: false } } } });
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>`;
|
||||||
`;
|
}
|
||||||
|
|
||||||
|
function getHtml(playData: PlayUrlResponse | null): string {
|
||||||
|
if (!playData) return '<html><body style="background:#000"></body></html>';
|
||||||
|
if (playData.dash) {
|
||||||
|
const v = playData.dash.video[0];
|
||||||
|
const a = playData.dash.audio[0];
|
||||||
|
if (v && a) {
|
||||||
|
const mpd = buildMpd(v.baseUrl, v.codecs, v.bandwidth, a.baseUrl, a.codecs, a.bandwidth);
|
||||||
|
return buildDashHtml(mpd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const url = playData.durl?.[0]?.url;
|
||||||
|
if (url) return buildMp4Html(url);
|
||||||
|
return '<html><body style="background:#000"></body></html>';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NativeVideoPlayer({
|
||||||
|
playData, qualities, currentQn, onQualityChange, onFullscreen, onMiniPlayer, style,
|
||||||
|
}: Props) {
|
||||||
|
const [showQuality, setShowQuality] = useState(false);
|
||||||
|
const currentDesc = qualities.find(q => q.qn === currentQn)?.desc ?? (currentQn ? String(currentQn) : 'HD');
|
||||||
|
const html = getHtml(playData);
|
||||||
|
|
||||||
export function NativeVideoPlayer({ uri }: Props) {
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={[styles.container, style]}>
|
||||||
<WebView
|
<WebView
|
||||||
source={{ html: buildHtml(uri) }}
|
key={html}
|
||||||
|
source={{ html }}
|
||||||
style={styles.webview}
|
style={styles.webview}
|
||||||
allowsInlineMediaPlayback
|
allowsInlineMediaPlayback
|
||||||
mediaPlaybackRequiresUserAction={false}
|
mediaPlaybackRequiresUserAction={false}
|
||||||
@@ -40,11 +89,88 @@ export function NativeVideoPlayer({ uri }: Props) {
|
|||||||
originWhitelist={['*']}
|
originWhitelist={['*']}
|
||||||
scrollEnabled={false}
|
scrollEnabled={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<View style={styles.controls}>
|
||||||
|
<TouchableOpacity style={styles.ctrlBtn} onPress={() => setShowQuality(true)}>
|
||||||
|
<Text style={styles.qualityText}>{currentDesc}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity style={styles.ctrlBtn} onPress={onFullscreen}>
|
||||||
|
<Ionicons name="expand" size={18} color="#fff" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
{onMiniPlayer && (
|
||||||
|
<TouchableOpacity style={styles.ctrlBtn} onPress={onMiniPlayer}>
|
||||||
|
<Ionicons name="tablet-portrait-outline" size={18} color="#fff" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Modal visible={showQuality} transparent animationType="fade">
|
||||||
|
<TouchableOpacity style={styles.modalOverlay} onPress={() => setShowQuality(false)}>
|
||||||
|
<View style={styles.qualityList}>
|
||||||
|
<Text style={styles.qualityTitle}>选择清晰度</Text>
|
||||||
|
{qualities.map(q => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={q.qn}
|
||||||
|
style={styles.qualityItem}
|
||||||
|
onPress={() => {
|
||||||
|
setShowQuality(false);
|
||||||
|
onQualityChange(q.qn);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={[styles.qualityItemText, q.qn === currentQn && styles.qualityItemActive]}>
|
||||||
|
{q.desc}
|
||||||
|
</Text>
|
||||||
|
{q.qn === currentQn && <Ionicons name="checkmark" size={16} color="#00AEEC" />}
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Modal>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { width, height: VIDEO_HEIGHT, backgroundColor: '#000' },
|
container: { width, height: VIDEO_HEIGHT, backgroundColor: '#000' },
|
||||||
webview: { width, height: VIDEO_HEIGHT, backgroundColor: '#000' },
|
webview: { flex: 1, backgroundColor: '#000' },
|
||||||
|
controls: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 8,
|
||||||
|
right: 8,
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
ctrlBtn: {
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.55)',
|
||||||
|
borderRadius: 4,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 4,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
qualityText: { color: '#fff', fontSize: 12, fontWeight: '600' },
|
||||||
|
modalOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
qualityList: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: 12,
|
||||||
|
paddingVertical: 8,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
minWidth: 180,
|
||||||
|
},
|
||||||
|
qualityTitle: { fontSize: 15, fontWeight: '700', color: '#212121', paddingVertical: 10, textAlign: 'center' },
|
||||||
|
qualityItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderTopWidth: StyleSheet.hairlineWidth,
|
||||||
|
borderTopColor: '#eee',
|
||||||
|
},
|
||||||
|
qualityItemText: { fontSize: 14, color: '#333' },
|
||||||
|
qualityItemActive: { color: '#00AEEC', fontWeight: '700' },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, StyleSheet, Dimensions, Text, Platform } from 'react-native';
|
import { View, StyleSheet, Dimensions, Text, Platform, Modal, TouchableOpacity, StatusBar } from 'react-native';
|
||||||
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { NativeVideoPlayer } from './NativeVideoPlayer';
|
import { NativeVideoPlayer } from './NativeVideoPlayer';
|
||||||
|
import type { PlayUrlResponse } from '../services/types';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
const VIDEO_HEIGHT = width * 0.5625;
|
const VIDEO_HEIGHT = width * 0.5625;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
uri: string | null;
|
playData: PlayUrlResponse | null;
|
||||||
|
qualities: { qn: number; desc: string }[];
|
||||||
|
currentQn: number;
|
||||||
|
onQualityChange: (qn: number) => void;
|
||||||
|
onMiniPlayer?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VideoPlayer({ uri }: Props) {
|
export function VideoPlayer({ playData, qualities, currentQn, onQualityChange, onMiniPlayer }: Props) {
|
||||||
if (!uri) {
|
const [fullscreen, setFullscreen] = useState(false);
|
||||||
|
|
||||||
|
if (!playData) {
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, styles.placeholder]}>
|
<View style={[styles.container, styles.placeholder]}>
|
||||||
<Text style={styles.placeholderText}>视频加载中...</Text>
|
<Text style={styles.placeholderText}>视频加载中...</Text>
|
||||||
@@ -19,10 +27,11 @@ export function VideoPlayer({ uri }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Platform.OS === 'web') {
|
if (Platform.OS === 'web') {
|
||||||
|
const url = playData.durl?.[0]?.url ?? '';
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<video
|
<video
|
||||||
src={uri}
|
src={url}
|
||||||
style={{ width: '100%', height: '100%', backgroundColor: '#000' } as any}
|
style={{ width: '100%', height: '100%', backgroundColor: '#000' } as any}
|
||||||
controls
|
controls
|
||||||
playsInline
|
playsInline
|
||||||
@@ -31,11 +40,48 @@ export function VideoPlayer({ uri }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <NativeVideoPlayer uri={uri} />;
|
return (
|
||||||
|
<>
|
||||||
|
<NativeVideoPlayer
|
||||||
|
playData={playData}
|
||||||
|
qualities={qualities}
|
||||||
|
currentQn={currentQn}
|
||||||
|
onQualityChange={onQualityChange}
|
||||||
|
onFullscreen={() => setFullscreen(true)}
|
||||||
|
onMiniPlayer={onMiniPlayer}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Modal visible={fullscreen} animationType="fade" statusBarTranslucent>
|
||||||
|
<StatusBar hidden />
|
||||||
|
<View style={styles.fullscreenContainer}>
|
||||||
|
<NativeVideoPlayer
|
||||||
|
playData={playData}
|
||||||
|
qualities={qualities}
|
||||||
|
currentQn={currentQn}
|
||||||
|
onQualityChange={onQualityChange}
|
||||||
|
onFullscreen={() => setFullscreen(false)}
|
||||||
|
style={{ width: '100%', height: '100%' } as any}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity style={styles.closeBtn} onPress={() => setFullscreen(false)}>
|
||||||
|
<Ionicons name="close" size={28} color="#fff" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { width, height: VIDEO_HEIGHT, backgroundColor: '#000' },
|
container: { width, height: VIDEO_HEIGHT, backgroundColor: '#000' },
|
||||||
placeholder: { justifyContent: 'center', alignItems: 'center' },
|
placeholder: { justifyContent: 'center', alignItems: 'center' },
|
||||||
placeholderText: { color: '#fff', fontSize: 14 },
|
placeholderText: { color: '#fff', fontSize: 14 },
|
||||||
|
fullscreenContainer: { flex: 1, backgroundColor: '#000' },
|
||||||
|
closeBtn: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 40,
|
||||||
|
right: 16,
|
||||||
|
padding: 8,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||||
|
borderRadius: 20,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
60
dev-proxy.js
Normal file
60
dev-proxy.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
const https = require('https');
|
||||||
|
const express = require('express');
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// CORS: allow any local origin (Expo web dev server)
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Buvid3, X-Sessdata');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||||
|
if (req.method === 'OPTIONS') return res.sendStatus(200);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeProxy(targetHost) {
|
||||||
|
return (req, res) => {
|
||||||
|
const buvid3 = req.headers['x-buvid3'] || '';
|
||||||
|
const sessdata = req.headers['x-sessdata'] || '';
|
||||||
|
const cookies = [
|
||||||
|
buvid3 && `buvid3=${buvid3}`,
|
||||||
|
sessdata && `SESSDATA=${sessdata}`,
|
||||||
|
].filter(Boolean).join('; ');
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
hostname: targetHost,
|
||||||
|
path: req.url,
|
||||||
|
method: req.method,
|
||||||
|
headers: {
|
||||||
|
'Cookie': cookies,
|
||||||
|
'Referer': 'https://www.bilibili.com',
|
||||||
|
'Origin': 'https://www.bilibili.com',
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'application/json, text/plain, */*',
|
||||||
|
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const proxy = https.request(options, (proxyRes) => {
|
||||||
|
// On successful QR login, extract SESSDATA from set-cookie and relay via custom header
|
||||||
|
const setCookies = proxyRes.headers['set-cookie'] || [];
|
||||||
|
const match = setCookies.find(c => c.includes('SESSDATA='));
|
||||||
|
if (match) {
|
||||||
|
const val = match.split(';')[0].replace('SESSDATA=', '');
|
||||||
|
res.setHeader('X-Sessdata', val);
|
||||||
|
}
|
||||||
|
res.writeHead(proxyRes.statusCode, {
|
||||||
|
'Content-Type': proxyRes.headers['content-type'] || 'application/json',
|
||||||
|
});
|
||||||
|
proxyRes.pipe(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
proxy.on('error', (err) => res.status(502).json({ error: err.message }));
|
||||||
|
req.pipe(proxy);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use('/bilibili-api', makeProxy('api.bilibili.com'));
|
||||||
|
app.use('/bilibili-passport', makeProxy('passport.bilibili.com'));
|
||||||
|
|
||||||
|
const PORT = process.env.PROXY_PORT || 3001;
|
||||||
|
app.listen(PORT, () => console.log(`[Proxy] http://localhost:${PORT}`));
|
||||||
@@ -1,12 +1,35 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { getVideoDetail, getPlayUrl } from '../services/bilibili';
|
import { getVideoDetail, getPlayUrl } from '../services/bilibili';
|
||||||
import type { VideoItem } from '../services/types';
|
import { useAuthStore } from '../store/authStore';
|
||||||
|
import type { VideoItem, PlayUrlResponse } from '../services/types';
|
||||||
|
|
||||||
export function useVideoDetail(bvid: string) {
|
export function useVideoDetail(bvid: string) {
|
||||||
const [video, setVideo] = useState<VideoItem | null>(null);
|
const [video, setVideo] = useState<VideoItem | null>(null);
|
||||||
const [streamUrl, setStreamUrl] = useState<string | null>(null);
|
const [playData, setPlayData] = useState<PlayUrlResponse | null>(null);
|
||||||
|
const [qualities, setQualities] = useState<{ qn: number; desc: string }[]>([]);
|
||||||
|
const [currentQn, setCurrentQn] = useState<number>(0);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const cidRef = useRef<number>(0);
|
||||||
|
const isLoggedIn = useAuthStore(s => s.isLoggedIn);
|
||||||
|
|
||||||
|
async function fetchPlayData(cid: number, qn: number, updateList = false) {
|
||||||
|
const data = await getPlayUrl(bvid, cid, qn);
|
||||||
|
setPlayData(data);
|
||||||
|
setCurrentQn(data.quality);
|
||||||
|
if (updateList && data.accept_quality?.length) {
|
||||||
|
setQualities(
|
||||||
|
data.accept_quality.map((q, i) => ({
|
||||||
|
qn: q,
|
||||||
|
desc: data.accept_description?.[i] ?? String(q),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changeQuality(qn: number) {
|
||||||
|
await fetchPlayData(cidRef.current, qn);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
@@ -15,8 +38,8 @@ export function useVideoDetail(bvid: string) {
|
|||||||
const detail = await getVideoDetail(bvid);
|
const detail = await getVideoDetail(bvid);
|
||||||
setVideo(detail);
|
setVideo(detail);
|
||||||
const cid = detail.pages?.[0]?.cid ?? detail.cid;
|
const cid = detail.pages?.[0]?.cid ?? detail.cid;
|
||||||
const playData = await getPlayUrl(bvid, cid);
|
cidRef.current = cid;
|
||||||
setStreamUrl(playData.durl[0]?.url ?? null);
|
await fetchPlayData(cid, 120, true);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e.message ?? 'Load failed');
|
setError(e.message ?? 'Load failed');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -26,5 +49,12 @@ export function useVideoDetail(bvid: string) {
|
|||||||
if (bvid) fetchData();
|
if (bvid) fetchData();
|
||||||
}, [bvid]);
|
}, [bvid]);
|
||||||
|
|
||||||
return { video, streamUrl, loading, error };
|
// 登录状态变化时重新拉取清晰度列表(登录后可能获得更高画质)
|
||||||
|
useEffect(() => {
|
||||||
|
if (cidRef.current) {
|
||||||
|
fetchPlayData(cidRef.current, 120, true).catch(() => {});
|
||||||
|
}
|
||||||
|
}, [isLoggedIn]);
|
||||||
|
|
||||||
|
return { video, playData, loading, error, qualities, currentQn, changeQuality };
|
||||||
}
|
}
|
||||||
|
|||||||
462
package-lock.json
generated
462
package-lock.json
generated
@@ -29,6 +29,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.2.2",
|
"@types/react": "~19.2.2",
|
||||||
|
"express": "^4.22.1",
|
||||||
"typescript": "~5.9.2"
|
"typescript": "~5.9.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3138,6 +3139,13 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/array-flatten": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/asap": {
|
"node_modules/asap": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||||
@@ -3415,6 +3423,61 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/body-parser": {
|
||||||
|
"version": "1.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||||
|
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "~3.1.2",
|
||||||
|
"content-type": "~1.0.5",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "2.0.0",
|
||||||
|
"destroy": "~1.2.0",
|
||||||
|
"http-errors": "~2.0.1",
|
||||||
|
"iconv-lite": "~0.4.24",
|
||||||
|
"on-finished": "~2.4.1",
|
||||||
|
"qs": "~6.14.0",
|
||||||
|
"raw-body": "~2.5.3",
|
||||||
|
"type-is": "~1.6.18",
|
||||||
|
"unpipe": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8",
|
||||||
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/body-parser/node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/body-parser/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/body-parser/node_modules/on-finished": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ee-first": "1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bplist-creator": {
|
"node_modules/bplist-creator": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
|
||||||
@@ -3531,6 +3594,23 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-bound": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/camelcase": {
|
"node_modules/camelcase": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
||||||
@@ -3822,12 +3902,52 @@
|
|||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/content-disposition": {
|
||||||
|
"version": "0.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||||
|
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "5.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/content-type": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/convert-source-map": {
|
"node_modules/convert-source-map": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie-signature": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.48.0",
|
"version": "3.48.0",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz",
|
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz",
|
||||||
@@ -4912,6 +5032,122 @@
|
|||||||
"integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
|
"integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/express": {
|
||||||
|
"version": "4.22.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||||
|
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "~1.3.8",
|
||||||
|
"array-flatten": "1.1.1",
|
||||||
|
"body-parser": "~1.20.3",
|
||||||
|
"content-disposition": "~0.5.4",
|
||||||
|
"content-type": "~1.0.4",
|
||||||
|
"cookie": "~0.7.1",
|
||||||
|
"cookie-signature": "~1.0.6",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "2.0.0",
|
||||||
|
"encodeurl": "~2.0.0",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"etag": "~1.8.1",
|
||||||
|
"finalhandler": "~1.3.1",
|
||||||
|
"fresh": "~0.5.2",
|
||||||
|
"http-errors": "~2.0.0",
|
||||||
|
"merge-descriptors": "1.0.3",
|
||||||
|
"methods": "~1.1.2",
|
||||||
|
"on-finished": "~2.4.1",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"path-to-regexp": "~0.1.12",
|
||||||
|
"proxy-addr": "~2.0.7",
|
||||||
|
"qs": "~6.14.0",
|
||||||
|
"range-parser": "~1.2.1",
|
||||||
|
"safe-buffer": "5.2.1",
|
||||||
|
"send": "~0.19.0",
|
||||||
|
"serve-static": "~1.16.2",
|
||||||
|
"setprototypeof": "1.2.0",
|
||||||
|
"statuses": "~2.0.1",
|
||||||
|
"type-is": "~1.6.18",
|
||||||
|
"utils-merge": "1.0.1",
|
||||||
|
"vary": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express/node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express/node_modules/encodeurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express/node_modules/finalhandler": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"encodeurl": "~2.0.0",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"on-finished": "~2.4.1",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"statuses": "~2.0.2",
|
||||||
|
"unpipe": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/express/node_modules/on-finished": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ee-first": "1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express/node_modules/statuses": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -5096,6 +5332,16 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/forwarded": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fresh": {
|
"node_modules/fresh": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
@@ -5386,6 +5632,19 @@
|
|||||||
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
|
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.4.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
|
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -5454,6 +5713,16 @@
|
|||||||
"loose-envify": "^1.0.0"
|
"loose-envify": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ipaddr.js": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-arrayish": {
|
"node_modules/is-arrayish": {
|
||||||
"version": "0.3.4",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
|
||||||
@@ -6250,12 +6519,32 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/media-typer": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/memoize-one": {
|
"node_modules/memoize-one": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
|
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/merge-descriptors": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/merge-options": {
|
"node_modules/merge-options": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
|
||||||
@@ -6274,6 +6563,16 @@
|
|||||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/methods": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/metro": {
|
"node_modules/metro": {
|
||||||
"version": "0.83.3",
|
"version": "0.83.3",
|
||||||
"resolved": "https://registry.npmjs.org/metro/-/metro-0.83.3.tgz",
|
"resolved": "https://registry.npmjs.org/metro/-/metro-0.83.3.tgz",
|
||||||
@@ -6765,6 +7064,19 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-inspect": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/on-finished": {
|
"node_modules/on-finished": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||||
@@ -7047,6 +7359,13 @@
|
|||||||
"node": "20 || >=22"
|
"node": "20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-to-regexp": {
|
||||||
|
"version": "0.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||||
|
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -7197,12 +7516,42 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-addr": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"forwarded": "0.2.0",
|
||||||
|
"ipaddr.js": "1.9.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/qs": {
|
||||||
|
"version": "6.14.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
||||||
|
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/query-string": {
|
"node_modules/query-string": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
|
||||||
@@ -7239,6 +7588,22 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/raw-body": {
|
||||||
|
"version": "2.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||||
|
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "~3.1.2",
|
||||||
|
"http-errors": "~2.0.1",
|
||||||
|
"iconv-lite": "~0.4.24",
|
||||||
|
"unpipe": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.2.0",
|
"version": "19.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||||
@@ -7800,6 +8165,13 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/sax": {
|
"node_modules/sax": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz",
|
||||||
@@ -7995,6 +8367,82 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/side-channel": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-list": "^1.0.0",
|
||||||
|
"side-channel-map": "^1.0.1",
|
||||||
|
"side-channel-weakmap": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-list": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-map": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-weakmap": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-map": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/signal-exit": {
|
"node_modules/signal-exit": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||||
@@ -8410,6 +8858,20 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/type-is": {
|
||||||
|
"version": "1.6.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||||
|
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"media-typer": "0.3.0",
|
||||||
|
"mime-types": "~2.1.24"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
"android": "expo run:android",
|
"android": "expo run:android",
|
||||||
"ios": "expo run:ios",
|
"ios": "expo run:ios",
|
||||||
"web": "expo start --web"
|
"web": "expo start --web",
|
||||||
|
"proxy": "node dev-proxy.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^15.0.2",
|
"@expo/vector-icons": "^15.0.2",
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.2.2",
|
"@types/react": "~19.2.2",
|
||||||
|
"express": "^4.22.1",
|
||||||
"typescript": "~5.9.2"
|
"typescript": "~5.9.2"
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
import type { VideoItem, Comment, PlayUrlResponse, QRCodeInfo } from './types';
|
import type { VideoItem, Comment, PlayUrlResponse, QRCodeInfo } from './types';
|
||||||
|
|
||||||
const BASE = 'https://api.bilibili.com';
|
const isWeb = Platform.OS === 'web';
|
||||||
const PASSPORT = 'https://passport.bilibili.com';
|
const BASE = isWeb ? 'http://localhost:3001/bilibili-api' : 'https://api.bilibili.com';
|
||||||
|
const PASSPORT = isWeb ? 'http://localhost:3001/bilibili-passport' : 'https://passport.bilibili.com';
|
||||||
|
|
||||||
function generateBuvid3(): string {
|
function generateBuvid3(): string {
|
||||||
const h = () => Math.floor(Math.random() * 16).toString(16);
|
const h = () => Math.floor(Math.random() * 16).toString(16);
|
||||||
@@ -23,7 +25,10 @@ async function getBuvid3(): Promise<string> {
|
|||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: BASE,
|
baseURL: BASE,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
headers: {
|
headers: isWeb ? {
|
||||||
|
'Accept': 'application/json, text/plain, */*',
|
||||||
|
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||||
|
} : {
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
'Referer': 'https://www.bilibili.com',
|
'Referer': 'https://www.bilibili.com',
|
||||||
'Origin': 'https://www.bilibili.com',
|
'Origin': 'https://www.bilibili.com',
|
||||||
@@ -37,9 +42,15 @@ api.interceptors.request.use(async (config) => {
|
|||||||
AsyncStorage.getItem('SESSDATA'),
|
AsyncStorage.getItem('SESSDATA'),
|
||||||
getBuvid3(),
|
getBuvid3(),
|
||||||
]);
|
]);
|
||||||
|
if (isWeb) {
|
||||||
|
// Browsers block Cookie/Referer/Origin headers; relay via custom headers to proxy
|
||||||
|
if (buvid3) config.headers['X-Buvid3'] = buvid3;
|
||||||
|
if (sessdata) config.headers['X-Sessdata'] = sessdata;
|
||||||
|
} else {
|
||||||
const cookies: string[] = [`buvid3=${buvid3}`];
|
const cookies: string[] = [`buvid3=${buvid3}`];
|
||||||
if (sessdata) cookies.push(`SESSDATA=${sessdata}`);
|
if (sessdata) cookies.push(`SESSDATA=${sessdata}`);
|
||||||
config.headers['Cookie'] = cookies.join('; ');
|
config.headers['Cookie'] = cookies.join('; ');
|
||||||
|
}
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,13 +64,20 @@ export async function getVideoDetail(bvid: string): Promise<VideoItem> {
|
|||||||
return res.data.data as VideoItem;
|
return res.data.data as VideoItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPlayUrl(bvid: string, cid: number): Promise<PlayUrlResponse> {
|
export async function getPlayUrl(bvid: string, cid: number, qn = 64): Promise<PlayUrlResponse> {
|
||||||
|
const fnval = qn >= 80 ? 16 : 0;
|
||||||
const res = await api.get('/x/player/playurl', {
|
const res = await api.get('/x/player/playurl', {
|
||||||
params: { bvid, cid, qn: 64, fnval: 0, platform: 'html5' },
|
params: { bvid, cid, qn, fnval, platform: 'html5', fourk: 1 },
|
||||||
});
|
});
|
||||||
return res.data.data as PlayUrlResponse;
|
return res.data.data as PlayUrlResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUserInfo(): Promise<{ face: string; uname: string; mid: number }> {
|
||||||
|
const res = await api.get('/x/web-interface/nav');
|
||||||
|
const { face, uname, mid } = res.data.data;
|
||||||
|
return { face: face ?? '', uname: uname ?? '', mid: mid ?? 0 };
|
||||||
|
}
|
||||||
|
|
||||||
export async function getComments(aid: number, pn = 1): Promise<Comment[]> {
|
export async function getComments(aid: number, pn = 1): Promise<Comment[]> {
|
||||||
const res = await api.get('/x/v2/reply', {
|
const res = await api.get('/x/v2/reply', {
|
||||||
params: { oid: aid, type: 1, pn, ps: 20, sort: 2 },
|
params: { oid: aid, type: 1, pn, ps: 20, sort: 2 },
|
||||||
@@ -68,25 +86,34 @@ export async function getComments(aid: number, pn = 1): Promise<Comment[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function generateQRCode(): Promise<QRCodeInfo> {
|
export async function generateQRCode(): Promise<QRCodeInfo> {
|
||||||
const res = await axios.get(`${PASSPORT}/x/passport-login/web/qrcode/generate`, {
|
const headers = isWeb
|
||||||
headers: { 'Referer': 'https://www.bilibili.com' },
|
? {}
|
||||||
});
|
: { 'Referer': 'https://www.bilibili.com' };
|
||||||
|
const res = await axios.get(`${PASSPORT}/x/passport-login/web/qrcode/generate`, { headers });
|
||||||
return res.data.data as QRCodeInfo;
|
return res.data.data as QRCodeInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pollQRCode(qrcode_key: string): Promise<{ code: number; cookie?: string }> {
|
export async function pollQRCode(qrcode_key: string): Promise<{ code: number; cookie?: string }> {
|
||||||
|
const headers = isWeb
|
||||||
|
? {}
|
||||||
|
: { 'Referer': 'https://www.bilibili.com' };
|
||||||
const res = await axios.get(`${PASSPORT}/x/passport-login/web/qrcode/poll`, {
|
const res = await axios.get(`${PASSPORT}/x/passport-login/web/qrcode/poll`, {
|
||||||
params: { qrcode_key },
|
params: { qrcode_key },
|
||||||
headers: { 'Referer': 'https://www.bilibili.com' },
|
headers,
|
||||||
});
|
});
|
||||||
const { code } = res.data.data;
|
const { code } = res.data.data;
|
||||||
let cookie: string | undefined;
|
let cookie: string | undefined;
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
|
if (isWeb) {
|
||||||
|
// Proxy relays SESSDATA via custom response header
|
||||||
|
cookie = res.headers['x-sessdata'] as string | undefined;
|
||||||
|
} else {
|
||||||
const setCookie = res.headers['set-cookie'];
|
const setCookie = res.headers['set-cookie'];
|
||||||
const match = setCookie?.find((c: string) => c.includes('SESSDATA'));
|
const match = setCookie?.find((c: string) => c.includes('SESSDATA'));
|
||||||
if (match) {
|
if (match) {
|
||||||
cookie = match.split(';')[0].replace('SESSDATA=', '');
|
cookie = match.split(';')[0].replace('SESSDATA=', '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return { code, cookie };
|
return { code, cookie };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,12 +35,18 @@ export interface Comment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PlayUrlResponse {
|
export interface PlayUrlResponse {
|
||||||
durl: Array<{
|
durl?: Array<{
|
||||||
url: string;
|
url: string;
|
||||||
length: number;
|
length: number;
|
||||||
size: number;
|
size: number;
|
||||||
}>;
|
}>;
|
||||||
|
dash?: {
|
||||||
|
video: Array<{ id: number; baseUrl: string; codecs: string; bandwidth: number }>;
|
||||||
|
audio: Array<{ id: number; baseUrl: string; codecs: string; bandwidth: number }>;
|
||||||
|
};
|
||||||
quality: number;
|
quality: number;
|
||||||
|
accept_quality: number[];
|
||||||
|
accept_description: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QRCodeInfo {
|
export interface QRCodeInfo {
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { getUserInfo } from '../services/bilibili';
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
sessdata: string | null;
|
sessdata: string | null;
|
||||||
uid: string | null;
|
uid: string | null;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
|
face: string | null;
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
login: (sessdata: string, uid: string, username?: string) => Promise<void>;
|
login: (sessdata: string, uid: string, username?: string) => Promise<void>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
restore: () => Promise<void>;
|
restore: () => Promise<void>;
|
||||||
|
setProfile: (face: string, username: string, uid: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthStore = create<AuthState>((set) => ({
|
export const useAuthStore = create<AuthState>((set) => ({
|
||||||
sessdata: null,
|
sessdata: null,
|
||||||
uid: null,
|
uid: null,
|
||||||
username: null,
|
username: null,
|
||||||
|
face: null,
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
|
|
||||||
login: async (sessdata, uid, username) => {
|
login: async (sessdata, uid, username) => {
|
||||||
@@ -27,16 +31,21 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
logout: async () => {
|
logout: async () => {
|
||||||
await AsyncStorage.multiRemove(['SESSDATA', 'UID', 'USERNAME']);
|
await AsyncStorage.multiRemove(['SESSDATA', 'UID', 'USERNAME', 'FACE']);
|
||||||
set({ sessdata: null, uid: null, username: null, isLoggedIn: false });
|
set({ sessdata: null, uid: null, username: null, face: null, isLoggedIn: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
restore: async () => {
|
restore: async () => {
|
||||||
const sessdata = await AsyncStorage.getItem('SESSDATA');
|
const sessdata = await AsyncStorage.getItem('SESSDATA');
|
||||||
const uid = await AsyncStorage.getItem('UID');
|
|
||||||
const username = await AsyncStorage.getItem('USERNAME');
|
|
||||||
if (sessdata) {
|
if (sessdata) {
|
||||||
set({ sessdata, uid, username, isLoggedIn: true });
|
set({ sessdata, isLoggedIn: true });
|
||||||
|
try {
|
||||||
|
const info = await getUserInfo();
|
||||||
|
await AsyncStorage.setItem('FACE', info.face);
|
||||||
|
set({ face: info.face, username: info.uname, uid: String(info.mid) });
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setProfile: (face, username, uid) => set({ face, username, uid }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
19
store/videoStore.ts
Normal file
19
store/videoStore.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface VideoStore {
|
||||||
|
isActive: boolean;
|
||||||
|
bvid: string;
|
||||||
|
title: string;
|
||||||
|
cover: string;
|
||||||
|
setVideo: (bvid: string, title: string, cover: string) => void;
|
||||||
|
clearVideo: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useVideoStore = create<VideoStore>(set => ({
|
||||||
|
isActive: false,
|
||||||
|
bvid: '',
|
||||||
|
title: '',
|
||||||
|
cover: '',
|
||||||
|
setVideo: (bvid, title, cover) => set({ isActive: true, bvid, title, cover }),
|
||||||
|
clearVideo: () => set({ isActive: false }),
|
||||||
|
}));
|
||||||
26
utils/buildMpd.ts
Normal file
26
utils/buildMpd.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
export function buildMpd(
|
||||||
|
videoUrl: string,
|
||||||
|
videoCodecs: string,
|
||||||
|
videoBandwidth: number,
|
||||||
|
audioUrl: string,
|
||||||
|
audioCodecs: string,
|
||||||
|
audioBandwidth: number,
|
||||||
|
): string {
|
||||||
|
return `<?xml version="1.0"?>
|
||||||
|
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" type="static" mediaPresentationDuration="PT9999S">
|
||||||
|
<Period>
|
||||||
|
<AdaptationSet contentType="video" mimeType="video/mp4" segmentAlignment="true">
|
||||||
|
<Representation id="v1" codecs="${videoCodecs}" bandwidth="${videoBandwidth}">
|
||||||
|
<BaseURL>${videoUrl}</BaseURL>
|
||||||
|
<SegmentBase><Initialization range="0-999"/></SegmentBase>
|
||||||
|
</Representation>
|
||||||
|
</AdaptationSet>
|
||||||
|
<AdaptationSet contentType="audio" mimeType="audio/mp4">
|
||||||
|
<Representation id="a1" codecs="${audioCodecs}" bandwidth="${audioBandwidth}">
|
||||||
|
<BaseURL>${audioUrl}</BaseURL>
|
||||||
|
<SegmentBase><Initialization range="0-999"/></SegmentBase>
|
||||||
|
</Representation>
|
||||||
|
</AdaptationSet>
|
||||||
|
</Period>
|
||||||
|
</MPD>`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user