音乐增加代理开关
This commit is contained in:
@@ -11537,12 +11537,14 @@ const MusicConfigComponent = ({
|
|||||||
const [enabled, setEnabled] = useState(false);
|
const [enabled, setEnabled] = useState(false);
|
||||||
const [baseUrl, setBaseUrl] = useState('');
|
const [baseUrl, setBaseUrl] = useState('');
|
||||||
const [token, setToken] = useState('');
|
const [token, setToken] = useState('');
|
||||||
|
const [proxyEnabled, setProxyEnabled] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config?.MusicConfig) {
|
if (config?.MusicConfig) {
|
||||||
setEnabled(config.MusicConfig.Enabled || false);
|
setEnabled(config.MusicConfig.Enabled || false);
|
||||||
setBaseUrl(config.MusicConfig.BaseUrl || '');
|
setBaseUrl(config.MusicConfig.BaseUrl || '');
|
||||||
setToken(config.MusicConfig.Token || '');
|
setToken(config.MusicConfig.Token || '');
|
||||||
|
setProxyEnabled(config.MusicConfig.ProxyEnabled ?? true);
|
||||||
}
|
}
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
@@ -11562,6 +11564,7 @@ const MusicConfigComponent = ({
|
|||||||
Enabled: enabled,
|
Enabled: enabled,
|
||||||
BaseUrl: normalizedBaseUrl,
|
BaseUrl: normalizedBaseUrl,
|
||||||
Token: token.trim(),
|
Token: token.trim(),
|
||||||
|
ProxyEnabled: proxyEnabled,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -11628,6 +11631,26 @@ const MusicConfigComponent = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
|
<div className='flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700'>
|
||||||
|
<div>
|
||||||
|
<h3 className='text-sm font-medium text-gray-900 dark:text-gray-100'>
|
||||||
|
启用播放代理
|
||||||
|
</h3>
|
||||||
|
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||||
|
开启后走服务器代理并设置浏览器永久缓存,关闭后将每次都解析播放链接
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<label className='relative inline-flex items-center cursor-pointer'>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
checked={proxyEnabled}
|
||||||
|
onChange={(e) => setProxyEnabled(e.target.checked)}
|
||||||
|
className='sr-only peer'
|
||||||
|
/>
|
||||||
|
<div className="w-14 h-7 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-green-300 dark:peer-focus:ring-green-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:start-[4px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-6 after:w-6 after:transition-all dark:border-gray-600 peer-checked:bg-green-600"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||||
lxserver Base URL
|
lxserver Base URL
|
||||||
|
|||||||
@@ -32,17 +32,20 @@ export async function POST(request: NextRequest) {
|
|||||||
Enabled,
|
Enabled,
|
||||||
BaseUrl,
|
BaseUrl,
|
||||||
Token,
|
Token,
|
||||||
|
ProxyEnabled,
|
||||||
} = body as {
|
} = body as {
|
||||||
Enabled?: boolean;
|
Enabled?: boolean;
|
||||||
BaseUrl?: string;
|
BaseUrl?: string;
|
||||||
Token?: string;
|
Token?: string;
|
||||||
|
ProxyEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 参数校验
|
// 参数校验
|
||||||
if (
|
if (
|
||||||
(Enabled !== undefined && typeof Enabled !== 'boolean') ||
|
(Enabled !== undefined && typeof Enabled !== 'boolean') ||
|
||||||
(BaseUrl !== undefined && typeof BaseUrl !== 'string') ||
|
(BaseUrl !== undefined && typeof BaseUrl !== 'string') ||
|
||||||
(Token !== undefined && typeof Token !== 'string')
|
(Token !== undefined && typeof Token !== 'string') ||
|
||||||
|
(ProxyEnabled !== undefined && typeof ProxyEnabled !== 'boolean')
|
||||||
) {
|
) {
|
||||||
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
|
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
|
||||||
}
|
}
|
||||||
@@ -62,6 +65,7 @@ export async function POST(request: NextRequest) {
|
|||||||
Enabled,
|
Enabled,
|
||||||
BaseUrl,
|
BaseUrl,
|
||||||
Token,
|
Token,
|
||||||
|
ProxyEnabled: ProxyEnabled ?? true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 写入数据库
|
// 写入数据库
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export default async function RootLayout({
|
|||||||
let webLiveEnabled = false;
|
let webLiveEnabled = false;
|
||||||
let customAdFilterVersion = 0;
|
let customAdFilterVersion = 0;
|
||||||
let tuneHubEnabled = false;
|
let tuneHubEnabled = false;
|
||||||
|
let musicProxyEnabled = true;
|
||||||
let advancedRecommendationEnabled = false;
|
let advancedRecommendationEnabled = false;
|
||||||
let customCategories = [] as {
|
let customCategories = [] as {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -149,6 +150,7 @@ export default async function RootLayout({
|
|||||||
customAdFilterVersion = config.SiteConfig?.CustomAdFilterVersion || 0;
|
customAdFilterVersion = config.SiteConfig?.CustomAdFilterVersion || 0;
|
||||||
// 音乐功能配置
|
// 音乐功能配置
|
||||||
tuneHubEnabled = config.MusicConfig?.Enabled || false;
|
tuneHubEnabled = config.MusicConfig?.Enabled || false;
|
||||||
|
musicProxyEnabled = config.MusicConfig?.ProxyEnabled ?? true;
|
||||||
// 高级推荐功能配置:存在已启用视频源脚本时显示
|
// 高级推荐功能配置:存在已启用视频源脚本时显示
|
||||||
advancedRecommendationEnabled =
|
advancedRecommendationEnabled =
|
||||||
(await listEnabledSourceScripts()).length > 0;
|
(await listEnabledSourceScripts()).length > 0;
|
||||||
@@ -223,6 +225,7 @@ export default async function RootLayout({
|
|||||||
ADVANCED_RECOMMENDATION_ENABLED: advancedRecommendationEnabled,
|
ADVANCED_RECOMMENDATION_ENABLED: advancedRecommendationEnabled,
|
||||||
CUSTOM_AD_FILTER_VERSION: customAdFilterVersion,
|
CUSTOM_AD_FILTER_VERSION: customAdFilterVersion,
|
||||||
MUSIC_ENABLED: tuneHubEnabled,
|
MUSIC_ENABLED: tuneHubEnabled,
|
||||||
|
MUSIC_PROXY_ENABLED: musicProxyEnabled,
|
||||||
FESTIVE_EFFECT_ENABLED:
|
FESTIVE_EFFECT_ENABLED:
|
||||||
process.env.FESTIVE_EFFECT_ENABLED === 'true',
|
process.env.FESTIVE_EFFECT_ENABLED === 'true',
|
||||||
};
|
};
|
||||||
|
|||||||
+143
-89
@@ -88,6 +88,10 @@ export default function MusicPage() {
|
|||||||
const [showPlayer, setShowPlayer] = useState(false);
|
const [showPlayer, setShowPlayer] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [showLyrics, setShowLyrics] = useState(false);
|
const [showLyrics, setShowLyrics] = useState(false);
|
||||||
|
const [musicProxyEnabled, setMusicProxyEnabled] = useState(() => {
|
||||||
|
if (typeof window === 'undefined') return true;
|
||||||
|
return (window as any).RUNTIME_CONFIG?.MUSIC_PROXY_ENABLED !== false;
|
||||||
|
});
|
||||||
const [lyrics, setLyrics] = useState<LyricLine[]>([]);
|
const [lyrics, setLyrics] = useState<LyricLine[]>([]);
|
||||||
const [currentLyricIndex, setCurrentLyricIndex] = useState(-1);
|
const [currentLyricIndex, setCurrentLyricIndex] = useState(-1);
|
||||||
const [currentSongUrl, setCurrentSongUrl] = useState('');
|
const [currentSongUrl, setCurrentSongUrl] = useState('');
|
||||||
@@ -190,6 +194,40 @@ export default function MusicPage() {
|
|||||||
return `/api/music/v2/stream?${params.toString()}`;
|
return `/api/music/v2/stream?${params.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getMusicProxyEnabled = () => {
|
||||||
|
if (typeof window === 'undefined') return true;
|
||||||
|
return (window as any).RUNTIME_CONFIG?.MUSIC_PROXY_ENABLED !== false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchPlayData = async (
|
||||||
|
song: Song,
|
||||||
|
source: MusicSource,
|
||||||
|
songQuality: '128k' | '320k' | 'flac' | 'flac24bit',
|
||||||
|
includeUrl = false
|
||||||
|
) => {
|
||||||
|
const response = await fetch('/api/music/v2/play', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
includeUrl,
|
||||||
|
song: {
|
||||||
|
songId: song.id,
|
||||||
|
source,
|
||||||
|
songmid: song.songmid,
|
||||||
|
name: song.name,
|
||||||
|
artist: song.artist,
|
||||||
|
album: song.album,
|
||||||
|
cover: song.pic,
|
||||||
|
durationSec: song.duration,
|
||||||
|
durationText: song.durationText,
|
||||||
|
},
|
||||||
|
quality: songQuality,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
|
||||||
const beginResolving = () => {
|
const beginResolving = () => {
|
||||||
setResolvingCount((prev) => prev + 1);
|
setResolvingCount((prev) => prev + 1);
|
||||||
};
|
};
|
||||||
@@ -251,6 +289,10 @@ export default function MusicPage() {
|
|||||||
// 此函数已不再使用,所有状态恢复都在 initializePlayState 中完成
|
// 此函数已不再使用,所有状态恢复都在 initializePlayState 中完成
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMusicProxyEnabled(getMusicProxyEnabled());
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 页面加载时恢复播放状态和数据库记录
|
// 页面加载时恢复播放状态和数据库记录
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializePlayState = async () => {
|
const initializePlayState = async () => {
|
||||||
@@ -300,6 +342,8 @@ export default function MusicPage() {
|
|||||||
|
|
||||||
// 4. 使用数据库的最新记录(歌曲和进度都从数据库获取)
|
// 4. 使用数据库的最新记录(歌曲和进度都从数据库获取)
|
||||||
if (sortedRecords.length > 0) {
|
if (sortedRecords.length > 0) {
|
||||||
|
const proxyEnabled = getMusicProxyEnabled();
|
||||||
|
setMusicProxyEnabled(proxyEnabled);
|
||||||
const latestDbRecord = sortedRecords[0];
|
const latestDbRecord = sortedRecords[0];
|
||||||
const latestDbSong = sortedSongs[0];
|
const latestDbSong = sortedSongs[0];
|
||||||
|
|
||||||
@@ -312,53 +356,49 @@ export default function MusicPage() {
|
|||||||
const dbPlayTime = latestDbRecord.playTime || 0;
|
const dbPlayTime = latestDbRecord.playTime || 0;
|
||||||
songStartTimeRef.current = Date.now();
|
songStartTimeRef.current = Date.now();
|
||||||
|
|
||||||
// 5. 先直接使用稳定 stream 地址恢复播放
|
|
||||||
const platform = latestDbSong.platform || 'kw';
|
const platform = latestDbSong.platform || 'kw';
|
||||||
const streamUrl = buildStreamUrl(latestDbSong, platform, playState.quality || '320k');
|
const selectedQuality = playState.quality || '320k';
|
||||||
setCurrentSongUrl(streamUrl);
|
|
||||||
|
|
||||||
if (audioRef.current) {
|
const restoreTime = () => {
|
||||||
audioRef.current.src = streamUrl;
|
if (audioRef.current && dbPlayTime > 0) {
|
||||||
|
audioRef.current.currentTime = dbPlayTime;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const restoreTime = () => {
|
if (proxyEnabled) {
|
||||||
if (audioRef.current && dbPlayTime > 0) {
|
const streamUrl = buildStreamUrl(latestDbSong, platform, selectedQuality);
|
||||||
audioRef.current.currentTime = dbPlayTime;
|
setCurrentSongUrl(streamUrl);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
audioRef.current.addEventListener('loadedmetadata', restoreTime, { once: true });
|
if (audioRef.current) {
|
||||||
audioRef.current.load();
|
audioRef.current.src = streamUrl;
|
||||||
}
|
audioRef.current.addEventListener('loadedmetadata', restoreTime, { once: true });
|
||||||
|
audioRef.current.load();
|
||||||
|
}
|
||||||
|
|
||||||
fetch('/api/music/v2/play', {
|
fetchPlayData(latestDbSong, platform, selectedQuality, false)
|
||||||
method: 'POST',
|
.then((data) => {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
if (data.success && data.data?.lyric?.lyric) {
|
||||||
body: JSON.stringify({
|
const parsedLyrics = parseLyric(data.data.lyric.lyric, data.data.lyric.tlyric);
|
||||||
includeUrl: false,
|
setLyrics(parsedLyrics);
|
||||||
song: {
|
}
|
||||||
songId: latestDbSong.id,
|
})
|
||||||
source: platform,
|
.catch((error) => {
|
||||||
songmid: latestDbSong.songmid,
|
console.error('加载歌词失败:', error);
|
||||||
name: latestDbSong.name,
|
});
|
||||||
artist: latestDbSong.artist,
|
} else {
|
||||||
album: latestDbSong.album,
|
const data = await fetchPlayData(latestDbSong, platform, selectedQuality, true);
|
||||||
cover: latestDbSong.pic,
|
if (data.success && data.data?.play?.directUrl && audioRef.current) {
|
||||||
durationSec: latestDbSong.duration,
|
setCurrentSongUrl(data.data.play.directUrl);
|
||||||
durationText: latestDbSong.durationText,
|
audioRef.current.src = data.data.play.directUrl;
|
||||||
},
|
audioRef.current.addEventListener('loadedmetadata', restoreTime, { once: true });
|
||||||
quality: playState.quality || '320k',
|
audioRef.current.load();
|
||||||
}),
|
|
||||||
})
|
if (data.data.lyric?.lyric) {
|
||||||
.then(res => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.success && data.data?.lyric?.lyric) {
|
|
||||||
const parsedLyrics = parseLyric(data.data.lyric.lyric, data.data.lyric.tlyric);
|
const parsedLyrics = parseLyric(data.data.lyric.lyric, data.data.lyric.tlyric);
|
||||||
setLyrics(parsedLyrics);
|
setLyrics(parsedLyrics);
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.catch((error) => {
|
}
|
||||||
console.error('加载歌词失败:', error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载播放记录失败:', error);
|
console.error('加载播放记录失败:', error);
|
||||||
@@ -767,7 +807,9 @@ export default function MusicPage() {
|
|||||||
beginResolving();
|
beginResolving();
|
||||||
try {
|
try {
|
||||||
// 使用歌曲自己的平台信息,如果没有则使用当前选择的平台
|
// 使用歌曲自己的平台信息,如果没有则使用当前选择的平台
|
||||||
const platform = song.platform || currentSource;
|
const platform = song.platform || currentSource;
|
||||||
|
const proxyEnabled = getMusicProxyEnabled();
|
||||||
|
setMusicProxyEnabled(proxyEnabled);
|
||||||
|
|
||||||
// 记录歌曲开始播放的时间
|
// 记录歌曲开始播放的时间
|
||||||
songStartTimeRef.current = Date.now();
|
songStartTimeRef.current = Date.now();
|
||||||
@@ -815,59 +857,71 @@ export default function MusicPage() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const streamUrl = buildStreamUrl(song, platform, quality);
|
if (proxyEnabled) {
|
||||||
setCurrentSongUrl(streamUrl);
|
const streamUrl = buildStreamUrl(song, platform, quality);
|
||||||
|
setCurrentSongUrl(streamUrl);
|
||||||
|
|
||||||
if (audioRef.current) {
|
if (audioRef.current) {
|
||||||
audioRef.current.src = streamUrl;
|
audioRef.current.src = streamUrl;
|
||||||
audioRef.current.load();
|
audioRef.current.load();
|
||||||
audioRef.current.play().catch(err => {
|
audioRef.current.play().catch(err => {
|
||||||
console.error('播放失败:', err);
|
console.error('播放失败:', err);
|
||||||
});
|
});
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch('/api/music/v2/play', {
|
fetchPlayData(song, platform, quality, false)
|
||||||
method: 'POST',
|
.then((data) => {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
if (data.success) {
|
||||||
body: JSON.stringify({
|
if (data.data.song?.cover) {
|
||||||
includeUrl: false,
|
setCurrentSong({
|
||||||
song: {
|
...song,
|
||||||
songId: song.id,
|
pic: data.data.song.cover,
|
||||||
source: platform,
|
platform,
|
||||||
songmid: song.songmid,
|
});
|
||||||
name: song.name,
|
}
|
||||||
artist: song.artist,
|
|
||||||
album: song.album,
|
if (data.data.lyric?.lyric) {
|
||||||
cover: song.pic,
|
const parsedLyrics = parseLyric(data.data.lyric.lyric, data.data.lyric.tlyric);
|
||||||
durationSec: song.duration,
|
setLyrics(parsedLyrics);
|
||||||
durationText: song.durationText,
|
}
|
||||||
},
|
} else {
|
||||||
quality,
|
console.error('播放信息获取失败:', data);
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then(res => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.success) {
|
|
||||||
if (data.data.song?.cover) {
|
|
||||||
setCurrentSong({
|
|
||||||
...song,
|
|
||||||
pic: data.data.song.cover,
|
|
||||||
platform,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
if (data.data.lyric?.lyric) {
|
.catch((error) => {
|
||||||
const parsedLyrics = parseLyric(data.data.lyric.lyric, data.data.lyric.tlyric);
|
console.error('加载歌词失败:', error);
|
||||||
setLyrics(parsedLyrics);
|
});
|
||||||
}
|
} else {
|
||||||
} else {
|
const data = await fetchPlayData(song, platform, quality, true);
|
||||||
console.error('播放信息获取失败:', data);
|
if (data.success && data.data?.play?.directUrl) {
|
||||||
|
if (data.data.song?.cover) {
|
||||||
|
setCurrentSong({
|
||||||
|
...song,
|
||||||
|
pic: data.data.song.cover,
|
||||||
|
platform,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch((error) => {
|
if (data.data.lyric?.lyric) {
|
||||||
console.error('加载歌词失败:', error);
|
const parsedLyrics = parseLyric(data.data.lyric.lyric, data.data.lyric.tlyric);
|
||||||
});
|
setLyrics(parsedLyrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentSongUrl(data.data.play.directUrl);
|
||||||
|
|
||||||
|
if (audioRef.current) {
|
||||||
|
audioRef.current.src = data.data.play.directUrl;
|
||||||
|
audioRef.current.load();
|
||||||
|
audioRef.current.play().catch(err => {
|
||||||
|
console.error('播放失败:', err);
|
||||||
|
});
|
||||||
|
setIsPlaying(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('播放信息获取失败:', data);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('播放失败:', error);
|
console.error('播放失败:', error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ export interface AdminConfig {
|
|||||||
Enabled?: boolean; // 启用音乐功能
|
Enabled?: boolean; // 启用音乐功能
|
||||||
BaseUrl?: string; // lxserver 地址
|
BaseUrl?: string; // lxserver 地址
|
||||||
Token?: string; // lxserver x-user-token
|
Token?: string; // lxserver x-user-token
|
||||||
|
ProxyEnabled?: boolean; // 是否走 stream 代理
|
||||||
// 兼容旧代码的遗留字段(待删除)
|
// 兼容旧代码的遗留字段(待删除)
|
||||||
TuneHubEnabled?: boolean;
|
TuneHubEnabled?: boolean;
|
||||||
TuneHubBaseUrl?: string;
|
TuneHubBaseUrl?: string;
|
||||||
|
|||||||
@@ -646,7 +646,10 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
|||||||
Enabled: false,
|
Enabled: false,
|
||||||
BaseUrl: '',
|
BaseUrl: '',
|
||||||
Token: '',
|
Token: '',
|
||||||
|
ProxyEnabled: true,
|
||||||
};
|
};
|
||||||
|
} else if (adminConfig.MusicConfig.ProxyEnabled === undefined) {
|
||||||
|
adminConfig.MusicConfig.ProxyEnabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return adminConfig;
|
return adminConfig;
|
||||||
|
|||||||
Reference in New Issue
Block a user