mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-08 07:28:37 +08:00
fix: proxy B站图片 CDN 解决 web 端防盗链图片不显示问题
- dev-proxy.js 新增 /bilibili-img 路由,代理 *.hdslb.com 并注入正确 Referer - utils/imageUrl.ts 新增 proxyImageUrl(),web 端将图片 URL 转为本地代理地址 - VideoCard / CommentItem / MiniPlayer / [bvid] 对所有 B站图片应用 proxyImageUrl
This commit is contained in:
@@ -13,6 +13,7 @@ import { useVideoDetail } from '../../hooks/useVideoDetail';
|
|||||||
import { useComments } from '../../hooks/useComments';
|
import { useComments } from '../../hooks/useComments';
|
||||||
import { useVideoStore } from '../../store/videoStore';
|
import { useVideoStore } from '../../store/videoStore';
|
||||||
import { formatCount } from '../../utils/format';
|
import { formatCount } from '../../utils/format';
|
||||||
|
import { proxyImageUrl } from '../../utils/imageUrl';
|
||||||
|
|
||||||
type Tab = 'intro' | 'comments';
|
type Tab = 'intro' | 'comments';
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ export default function VideoDetailScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.upRow}>
|
<View style={styles.upRow}>
|
||||||
<Image source={{ uri: video.owner.face }} style={styles.avatar} />
|
<Image source={{ uri: proxyImageUrl(video.owner.face) }} style={styles.avatar} />
|
||||||
<Text style={styles.upName}>{video.owner.name}</Text>
|
<Text style={styles.upName}>{video.owner.name}</Text>
|
||||||
<TouchableOpacity style={styles.followBtn}>
|
<TouchableOpacity style={styles.followBtn}>
|
||||||
<Text style={styles.followTxt}>+ 关注</Text>
|
<Text style={styles.followTxt}>+ 关注</Text>
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import { View, Text, Image, StyleSheet } from 'react-native';
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import type { Comment } from '../services/types';
|
import type { Comment } from '../services/types';
|
||||||
import { formatTime } from '../utils/format';
|
import { formatTime } from '../utils/format';
|
||||||
|
import { proxyImageUrl } from '../utils/imageUrl';
|
||||||
|
|
||||||
interface Props { item: Comment; }
|
interface Props { item: Comment; }
|
||||||
|
|
||||||
export function CommentItem({ item }: Props) {
|
export function CommentItem({ item }: Props) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
<Image source={{ uri: item.member.avatar }} style={styles.avatar} />
|
<Image source={{ uri: proxyImageUrl(item.member.avatar) }} style={styles.avatar} />
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<Text style={styles.username}>{item.member.uname}</Text>
|
<Text style={styles.username}>{item.member.uname}</Text>
|
||||||
<Text style={styles.message}>{item.content.message}</Text>
|
<Text style={styles.message}>{item.content.message}</Text>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useRouter } from 'expo-router';
|
|||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useVideoStore } from '../store/videoStore';
|
import { useVideoStore } from '../store/videoStore';
|
||||||
|
import { proxyImageUrl } from '../utils/imageUrl';
|
||||||
|
|
||||||
export function MiniPlayer() {
|
export function MiniPlayer() {
|
||||||
const { isActive, bvid, title, cover, clearVideo } = useVideoStore();
|
const { isActive, bvid, title, cover, clearVideo } = useVideoStore();
|
||||||
@@ -42,7 +43,7 @@ export function MiniPlayer() {
|
|||||||
onPress={() => router.push(`/video/${bvid}` as any)}
|
onPress={() => router.push(`/video/${bvid}` as any)}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
>
|
>
|
||||||
<Image source={{ uri: cover }} style={styles.cover} />
|
<Image source={{ uri: proxyImageUrl(cover) }} style={styles.cover} />
|
||||||
<Text style={styles.title} numberOfLines={1}>{title}</Text>
|
<Text style={styles.title} numberOfLines={1}>{title}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={styles.closeBtn} onPress={clearVideo}>
|
<TouchableOpacity style={styles.closeBtn} onPress={clearVideo}>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { View, Text, Image, TouchableOpacity, StyleSheet, Dimensions } from 'rea
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import type { VideoItem } from '../services/types';
|
import type { VideoItem } from '../services/types';
|
||||||
import { formatCount, formatDuration } from '../utils/format';
|
import { formatCount, formatDuration } from '../utils/format';
|
||||||
|
import { proxyImageUrl } from '../utils/imageUrl';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
const CARD_WIDTH = (width - 24) / 2;
|
const CARD_WIDTH = (width - 24) / 2;
|
||||||
@@ -17,7 +18,7 @@ export function VideoCard({ item, onPress }: Props) {
|
|||||||
<TouchableOpacity style={styles.card} onPress={onPress} activeOpacity={0.85}>
|
<TouchableOpacity style={styles.card} onPress={onPress} activeOpacity={0.85}>
|
||||||
<View style={styles.thumbContainer}>
|
<View style={styles.thumbContainer}>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: item.pic }}
|
source={{ uri: proxyImageUrl(item.pic) }}
|
||||||
style={styles.thumb}
|
style={styles.thumb}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -56,5 +56,14 @@ function makeProxy(targetHost) {
|
|||||||
app.use('/bilibili-api', makeProxy('api.bilibili.com'));
|
app.use('/bilibili-api', makeProxy('api.bilibili.com'));
|
||||||
app.use('/bilibili-passport', makeProxy('passport.bilibili.com'));
|
app.use('/bilibili-passport', makeProxy('passport.bilibili.com'));
|
||||||
|
|
||||||
|
// Image CDN proxy — strips the host segment and forwards to the real CDN with Referer
|
||||||
|
app.use('/bilibili-img', (req, res) => {
|
||||||
|
const parts = req.url.split('/').filter(Boolean);
|
||||||
|
const host = parts[0];
|
||||||
|
if (!host || !host.endsWith('.hdslb.com')) return res.status(403).end();
|
||||||
|
req.url = '/' + parts.slice(1).join('/');
|
||||||
|
makeProxy(host)(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
const PORT = process.env.PROXY_PORT || 3001;
|
const PORT = process.env.PROXY_PORT || 3001;
|
||||||
app.listen(PORT, () => console.log(`[Proxy] http://localhost:${PORT}`));
|
app.listen(PORT, () => console.log(`[Proxy] http://localhost:${PORT}`));
|
||||||
|
|||||||
13
utils/imageUrl.ts
Normal file
13
utils/imageUrl.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Platform } from 'react-native';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web 端将 B站图片 CDN URL 转为本地代理地址,注入正确 Referer 绕过防盗链。
|
||||||
|
* Native 端直接返回原 URL(App 请求头由 axios 拦截器统一设置)。
|
||||||
|
*/
|
||||||
|
export function proxyImageUrl(url: string): string {
|
||||||
|
if (Platform.OS !== 'web' || !url) return url;
|
||||||
|
return url.replace(
|
||||||
|
/^https?:\/\/([a-z0-9]+\.hdslb\.com)/,
|
||||||
|
'http://localhost:3001/bilibili-img/$1',
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user