支持本地数据

main
mula.liu 2026-01-26 20:02:00 +08:00
parent 45242a1fa1
commit 479c7fca43
6 changed files with 47 additions and 14 deletions

3
.gitignore vendored
View File

@ -22,3 +22,6 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Data directories
/data

17
App.tsx
View File

@ -78,18 +78,23 @@ const App: React.FC = () => {
if (track.lyrics.length === 0 && track.lyricsSource) { if (track.lyrics.length === 0 && track.lyricsSource) {
try { try {
let lrcContent = ""; let lrcContent = "";
// Check if it's a URL // Check if it's a URL (http or relative/absolute local path)
if (track.lyricsSource.startsWith('http')) { // We treat anything starting with http, /, or ./ as a URL to fetch
if (track.lyricsSource.startsWith('http') || track.lyricsSource.startsWith('/') || track.lyricsSource.startsWith('./')) {
// Try to fetch lyrics, might need proxy if CORS fails // Try to fetch lyrics, might need proxy if CORS fails
try { try {
const res = await fetch(track.lyricsSource); const res = await fetch(track.lyricsSource);
if (!res.ok) throw new Error("Direct lyrics fetch failed"); if (!res.ok) throw new Error("Direct lyrics fetch failed");
lrcContent = await res.text(); lrcContent = await res.text();
} catch (e) { } catch (e) {
// Simple proxy fallback for lyrics text // Only try proxy if it's a remote HTTP URL
const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(track.lyricsSource)}`; if (track.lyricsSource.startsWith('http')) {
const res = await fetch(proxyUrl); const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(track.lyricsSource)}`;
lrcContent = await res.text(); const res = await fetch(proxyUrl);
lrcContent = await res.text();
} else {
throw e;
}
} }
} else { } else {
// Assume it's raw text // Assume it's raw text

View File

@ -5,7 +5,9 @@ services:
build: . build: .
container_name: nex-music-web container_name: nex-music-web
ports: ports:
- "8081:80" - "80:80"
volumes:
- ./data:/usr/share/nginx/html/data:ro
restart: always restart: always
# If you want to mount the .env file dynamically or valid it locally # If you want to mount the .env file dynamically or valid it locally
# env_file: # env_file:

View File

@ -13,6 +13,13 @@ server {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
# Serve static music data
location /data/ {
alias /usr/share/nginx/html/data/;
autoindex on;
add_header Access-Control-Allow-Origin *;
}
# Cache static assets # Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y; expires 1y;

View File

@ -47,8 +47,18 @@ const getValue = (item: any, keys: string[]) => {
const resolveUrl = (url: string, baseUrl: string) => { const resolveUrl = (url: string, baseUrl: string) => {
if (!url) return ""; if (!url) return "";
try { try {
// If url is absolute, this ignores baseUrl // Handle case where baseUrl is relative (e.g. "/data/playlist.json")
return new URL(url, baseUrl).href; // We construct a full URL for the base if it's not already absolute
let fullBaseUrl = baseUrl;
if (baseUrl && !baseUrl.match(/^https?:\/\//)) {
// If running in browser context
if (typeof window !== 'undefined') {
fullBaseUrl = new URL(baseUrl, window.location.origin).href;
}
}
// If url is absolute, this ignores fullBaseUrl
return new URL(url, fullBaseUrl).href;
} catch (e) { } catch (e) {
return url; return url;
} }
@ -122,13 +132,22 @@ export const fetchPlaylist = async (url: string): Promise<TrackData[]> => {
audioUrl = resolveUrl(audioUrl, url); audioUrl = resolveUrl(audioUrl, url);
lyricsSource = resolveUrl(lyricsSource, url); lyricsSource = resolveUrl(lyricsSource, url);
// Check if URL is local/same-origin
const isLocal = (u: string) => {
if (typeof window === 'undefined') return false;
try {
return new URL(u).origin === window.location.origin;
} catch(e) { return false; }
};
// Fix Mixed Content for Cover: Route HTTP images through Weserv proxy // Fix Mixed Content for Cover: Route HTTP images through Weserv proxy
if (coverUrl && coverUrl.startsWith('http:')) { // ONLY if it's external. Local/same-origin HTTP is fine.
if (coverUrl && coverUrl.startsWith('http:') && !isLocal(coverUrl)) {
coverUrl = `https://images.weserv.nl/?url=${encodeURIComponent(coverUrl)}`; coverUrl = `https://images.weserv.nl/?url=${encodeURIComponent(coverUrl)}`;
} }
// Fix Mixed Content for Audio // Fix Mixed Content for Audio
if (audioUrl && audioUrl.startsWith('http:')) { if (audioUrl && audioUrl.startsWith('http:') && !isLocal(audioUrl)) {
// Strategy: Prefer native HTTPS upgrade if possible, otherwise proxy. // Strategy: Prefer native HTTPS upgrade if possible, otherwise proxy.
// Qiniu (clouddn.com) and other major CDNs usually support HTTPS. // Qiniu (clouddn.com) and other major CDNs usually support HTTPS.
if (audioUrl.includes('.clouddn.com') || audioUrl.includes('aliyuncs.com')) { if (audioUrl.includes('.clouddn.com') || audioUrl.includes('aliyuncs.com')) {

View File

@ -11,9 +11,6 @@ export default defineConfig(({ mode }) => {
hmr: true, hmr: true,
}, },
plugins: [react()], plugins: [react()],
build: {
target: 'es2015',
},
define: { define: {
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)