Progress bar and song reference/cover metadata
This commit is contained in:
@@ -293,6 +293,14 @@ export default function App() {
|
||||
viewCount: s.view_count || 0,
|
||||
userId: s.user_id,
|
||||
creator: s.creator,
|
||||
generationParams: (() => {
|
||||
try {
|
||||
if (!s.generation_params) return undefined;
|
||||
return typeof s.generation_params === 'string' ? JSON.parse(s.generation_params) : s.generation_params;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
})(),
|
||||
});
|
||||
|
||||
const mySongs = mySongsRes.songs.map(mapSong);
|
||||
@@ -671,6 +679,8 @@ export default function App() {
|
||||
return {
|
||||
...s,
|
||||
queuePosition: status.status === 'queued' ? status.queuePosition : undefined,
|
||||
progress: status.status === 'running' ? status.progress : undefined,
|
||||
stage: status.status === 'running' ? status.stage : undefined,
|
||||
};
|
||||
}
|
||||
return s;
|
||||
|
||||
+14
-15
@@ -445,11 +445,7 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
||||
|
||||
// Also set as current reference/source
|
||||
const selectedTarget = target ?? audioModalTarget;
|
||||
if (selectedTarget === 'reference') {
|
||||
setReferenceAudioUrl(data.track.audio_url);
|
||||
} else {
|
||||
setSourceAudioUrl(data.track.audio_url);
|
||||
}
|
||||
applyAudioTargetUrl(selectedTarget, data.track.audio_url);
|
||||
setShowAudioModal(false);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Upload failed';
|
||||
@@ -481,11 +477,7 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
||||
};
|
||||
|
||||
const useReferenceTrack = (track: ReferenceTrack) => {
|
||||
if (audioModalTarget === 'reference') {
|
||||
setReferenceAudioUrl(track.audio_url);
|
||||
} else {
|
||||
setSourceAudioUrl(track.audio_url);
|
||||
}
|
||||
applyAudioTargetUrl(audioModalTarget, track.audio_url);
|
||||
setShowAudioModal(false);
|
||||
setPlayingTrackId(null);
|
||||
};
|
||||
@@ -507,17 +499,24 @@ export const CreatePanel: React.FC<CreatePanelProps> = ({ onGenerate, isGenerati
|
||||
|
||||
const applyAudioUrl = () => {
|
||||
if (!tempAudioUrl.trim()) return;
|
||||
if (audioModalTarget === 'reference') {
|
||||
setReferenceAudioUrl(tempAudioUrl.trim());
|
||||
applyAudioTargetUrl(audioModalTarget, tempAudioUrl.trim());
|
||||
setShowAudioModal(false);
|
||||
setTempAudioUrl('');
|
||||
};
|
||||
|
||||
const applyAudioTargetUrl = (target: 'reference' | 'source', url: string) => {
|
||||
if (target === 'reference') {
|
||||
setReferenceAudioUrl(url);
|
||||
setReferenceTime(0);
|
||||
setReferenceDuration(0);
|
||||
} else {
|
||||
setSourceAudioUrl(tempAudioUrl.trim());
|
||||
setSourceAudioUrl(url);
|
||||
setSourceTime(0);
|
||||
setSourceDuration(0);
|
||||
if (taskType === 'text2music') {
|
||||
setTaskType('cover');
|
||||
}
|
||||
}
|
||||
setShowAudioModal(false);
|
||||
setTempAudioUrl('');
|
||||
};
|
||||
|
||||
const formatTime = (time: number) => {
|
||||
|
||||
@@ -39,6 +39,24 @@ export const RightSidebar: React.FC<RightSidebarProps> = ({ song, onClose, onOpe
|
||||
}
|
||||
}, [song, user]);
|
||||
|
||||
const getSourceLabel = (url?: string) => {
|
||||
if (!url) return 'None';
|
||||
try {
|
||||
const parsed = new URL(url, window.location.origin);
|
||||
const name = parsed.pathname.split('/').pop();
|
||||
return decodeURIComponent(name || url);
|
||||
} catch {
|
||||
const parts = url.split('/');
|
||||
return decodeURIComponent(parts[parts.length - 1] || url);
|
||||
}
|
||||
};
|
||||
|
||||
const openSource = (url?: string) => {
|
||||
if (!url) return;
|
||||
const resolved = url.startsWith('http') ? url : `${window.location.origin}${url}`;
|
||||
window.open(resolved, '_blank');
|
||||
};
|
||||
|
||||
|
||||
if (!song) return (
|
||||
<div className="w-full h-full bg-zinc-50 dark:bg-suno-panel border-l border-zinc-200 dark:border-white/5 flex items-center justify-center text-zinc-400 dark:text-zinc-500 text-sm transition-colors duration-300">
|
||||
@@ -239,6 +257,55 @@ export const RightSidebar: React.FC<RightSidebarProps> = ({ song, onClose, onOpe
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(song.generationParams?.referenceAudioUrl || song.generationParams?.sourceAudioUrl) && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-xs font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-wide">
|
||||
<LinkIcon size={14} />
|
||||
Sources
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg border border-zinc-200 dark:border-white/10 bg-white dark:bg-zinc-900/40 px-3 py-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Music size={14} className="text-zinc-400" />
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs text-zinc-500">Reference</div>
|
||||
<div className="text-sm font-medium text-zinc-900 dark:text-white truncate">
|
||||
{getSourceLabel(song.generationParams?.referenceAudioUrl)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{song.generationParams?.referenceAudioUrl && (
|
||||
<button
|
||||
className="text-xs px-2 py-1 rounded-full border border-zinc-200 dark:border-white/10 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors"
|
||||
onClick={() => openSource(song.generationParams?.referenceAudioUrl)}
|
||||
>
|
||||
Open
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg border border-zinc-200 dark:border-white/10 bg-white dark:bg-zinc-900/40 px-3 py-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Layers size={14} className="text-zinc-400" />
|
||||
<div className="min-w-0">
|
||||
<div className="text-xs text-zinc-500">Cover</div>
|
||||
<div className="text-sm font-medium text-zinc-900 dark:text-white truncate">
|
||||
{getSourceLabel(song.generationParams?.sourceAudioUrl)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{song.generationParams?.sourceAudioUrl && (
|
||||
<button
|
||||
className="text-xs px-2 py-1 rounded-full border border-zinc-200 dark:border-white/10 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-white/10 transition-colors"
|
||||
onClick={() => openSource(song.generationParams?.sourceAudioUrl)}
|
||||
>
|
||||
Open
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="h-px bg-zinc-200 dark:bg-white/5 w-full"></div>
|
||||
|
||||
{/* Tags / Style */}
|
||||
@@ -332,4 +399,4 @@ const ActionButton: React.FC<{ icon: React.ReactNode; label?: string; active?: b
|
||||
{icon}
|
||||
{label && <span className="text-xs font-semibold">{label}</span>}
|
||||
</button>
|
||||
);
|
||||
);
|
||||
|
||||
+15
-1
@@ -355,6 +355,20 @@ const SongItem: React.FC<SongItemProps> = ({
|
||||
<p className="text-xs text-zinc-500 dark:text-zinc-500 line-clamp-2 pt-1 font-medium max-w-2xl">
|
||||
{song.style}
|
||||
</p>
|
||||
{song.isGenerating && (
|
||||
<div className="pt-2">
|
||||
<div className="h-1 rounded-full bg-zinc-200/70 dark:bg-white/10 overflow-hidden">
|
||||
{song.progress !== undefined ? (
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-pink-500 to-purple-600 transition-all"
|
||||
style={{ width: `${Math.min(100, Math.max(0, song.progress * 100))}%` }}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full w-1/3 bg-gradient-to-r from-pink-500 to-purple-600 animate-pulse" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions Row - Hidden while generating */}
|
||||
@@ -453,4 +467,4 @@ const SongItem: React.FC<SongItemProps> = ({
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -107,7 +107,7 @@ router.get('/', authMiddleware, async (req: AuthenticatedRequest, res: Response)
|
||||
const result = await pool.query(
|
||||
`SELECT s.id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url,
|
||||
s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.is_public,
|
||||
s.like_count, s.view_count, s.user_id, s.created_at,
|
||||
s.like_count, s.view_count, s.user_id, s.created_at, s.generation_params,
|
||||
COALESCE(u.username, 'Anonymous') as creator
|
||||
FROM songs s
|
||||
LEFT JOIN users u ON s.user_id = u.id
|
||||
@@ -137,7 +137,7 @@ router.get('/public/featured', optionalAuthMiddleware, async (_req: Authenticate
|
||||
const result = await pool.query(
|
||||
`SELECT s.id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url,
|
||||
s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.like_count, s.view_count, s.created_at, s.user_id,
|
||||
COALESCE(u.username, 'Anonymous') as creator, u.avatar_url as creator_avatar
|
||||
COALESCE(u.username, 'Anonymous') as creator, u.avatar_url as creator_avatar, s.generation_params
|
||||
FROM songs s
|
||||
LEFT JOIN users u ON s.user_id = u.id
|
||||
ORDER BY RANDOM()
|
||||
@@ -184,7 +184,7 @@ router.get('/public', optionalAuthMiddleware, async (req: AuthenticatedRequest,
|
||||
const result = await pool.query(
|
||||
`SELECT s.id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url,
|
||||
s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.like_count, s.created_at,
|
||||
COALESCE(u.username, 'Anonymous') as creator
|
||||
COALESCE(u.username, 'Anonymous') as creator, s.generation_params
|
||||
FROM songs s
|
||||
LEFT JOIN users u ON s.user_id = u.id
|
||||
WHERE s.is_public = true
|
||||
@@ -213,7 +213,7 @@ router.get('/:id', optionalAuthMiddleware, async (req: AuthenticatedRequest, res
|
||||
const result = await pool.query(
|
||||
`SELECT s.id, s.user_id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url,
|
||||
s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.is_public, s.like_count, s.view_count, s.created_at,
|
||||
COALESCE(u.username, 'Anonymous') as creator, u.avatar_url as creator_avatar
|
||||
COALESCE(u.username, 'Anonymous') as creator, u.avatar_url as creator_avatar, s.generation_params
|
||||
FROM songs s
|
||||
LEFT JOIN users u ON s.user_id = u.id
|
||||
WHERE s.id = $1`,
|
||||
@@ -252,7 +252,7 @@ router.get('/:id/full', optionalAuthMiddleware, async (req: AuthenticatedRequest
|
||||
pool.query(
|
||||
`SELECT s.id, s.user_id, s.title, s.lyrics, s.style, s.caption, s.cover_url, s.audio_url,
|
||||
s.duration, s.bpm, s.key_scale, s.time_signature, s.tags, s.is_public,
|
||||
s.like_count, s.view_count, s.created_at,
|
||||
s.like_count, s.view_count, s.created_at, s.generation_params,
|
||||
COALESCE(u.username, 'Anonymous') as creator, u.avatar_url as creator_avatar
|
||||
FROM songs s
|
||||
LEFT JOIN users u ON s.user_id = u.id
|
||||
@@ -496,7 +496,7 @@ router.get('/liked/list', authMiddleware, async (req: AuthenticatedRequest, res:
|
||||
const result = await pool.query(
|
||||
`SELECT s.id, s.title, s.lyrics, s.style, s.cover_url, s.audio_url,
|
||||
s.duration, s.tags, s.like_count, s.created_at, s.is_public,
|
||||
COALESCE(u.username, 'Anonymous') as creator
|
||||
COALESCE(u.username, 'Anonymous') as creator, s.generation_params
|
||||
FROM liked_songs ls
|
||||
JOIN songs s ON ls.song_id = s.id
|
||||
LEFT JOIN users u ON s.user_id = u.id
|
||||
|
||||
@@ -255,7 +255,12 @@ async function pollApiResult(taskId: string, maxWaitMs = 600000): Promise<ApiTas
|
||||
|
||||
return { status: 1, audioPaths, metas };
|
||||
} else if (taskData.status === 2) {
|
||||
throw new Error('Generation failed on API side');
|
||||
const details = taskData.error
|
||||
|| taskData.message
|
||||
|| taskData.status_message
|
||||
|| taskData.result
|
||||
|| JSON.stringify(taskData);
|
||||
throw new Error(`Generation failed on API side: ${details}`);
|
||||
}
|
||||
|
||||
// Still processing
|
||||
@@ -382,6 +387,8 @@ interface JobStatus {
|
||||
status: 'queued' | 'running' | 'succeeded' | 'failed';
|
||||
queuePosition?: number;
|
||||
etaSeconds?: number;
|
||||
progress?: number;
|
||||
stage?: string;
|
||||
result?: GenerationResult;
|
||||
error?: string;
|
||||
}
|
||||
@@ -502,6 +509,7 @@ async function processGeneration(
|
||||
try {
|
||||
// Submit to API
|
||||
const { taskId } = await submitToApi(params);
|
||||
job.taskId = taskId;
|
||||
console.log(`Job ${jobId}: Submitted to API as task ${taskId}`);
|
||||
|
||||
// Poll for result
|
||||
@@ -832,6 +840,45 @@ export async function getJobStatus(jobId: string): Promise<JobStatus> {
|
||||
};
|
||||
}
|
||||
|
||||
if (job.status === 'running' && job.taskId) {
|
||||
try {
|
||||
const response = await fetch(`${ACESTEP_API}/query_result`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ task_id_list: [job.taskId] }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
const taskData = result.data?.[0];
|
||||
if (taskData?.result) {
|
||||
let resultData: unknown = taskData.result;
|
||||
if (typeof resultData === 'string') {
|
||||
try {
|
||||
resultData = JSON.parse(resultData);
|
||||
} catch {
|
||||
resultData = null;
|
||||
}
|
||||
}
|
||||
|
||||
const item = Array.isArray(resultData) ? resultData[0] : resultData;
|
||||
if (item && typeof item === 'object') {
|
||||
const progress = typeof (item as any).progress === 'number' ? (item as any).progress : undefined;
|
||||
const stage = typeof (item as any).stage === 'string' ? (item as any).stage : undefined;
|
||||
return {
|
||||
status: job.status,
|
||||
etaSeconds: Math.max(0, 180 - elapsed),
|
||||
progress,
|
||||
stage,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore progress fetch failures, fall back to ETA only
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: job.status,
|
||||
etaSeconds: Math.max(0, 180 - elapsed), // 3 min estimate
|
||||
|
||||
@@ -106,6 +106,7 @@ export interface Song {
|
||||
user_id?: string;
|
||||
created_at: string;
|
||||
creator?: string;
|
||||
generation_params?: any;
|
||||
}
|
||||
|
||||
// Transform songs to have proper audio URLs
|
||||
@@ -266,6 +267,8 @@ export interface GenerationJob {
|
||||
status: 'pending' | 'queued' | 'running' | 'succeeded' | 'failed';
|
||||
queuePosition?: number;
|
||||
etaSeconds?: number;
|
||||
progress?: number;
|
||||
stage?: string;
|
||||
result?: {
|
||||
audioUrls: string[];
|
||||
bpm?: number;
|
||||
|
||||
@@ -8,6 +8,9 @@ export interface Song {
|
||||
createdAt: Date;
|
||||
isGenerating?: boolean;
|
||||
queuePosition?: number; // Position in queue (undefined = actively generating, number = waiting in queue)
|
||||
progress?: number;
|
||||
stage?: string;
|
||||
generationParams?: any;
|
||||
tags: string[];
|
||||
audioUrl?: string;
|
||||
isPublic?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user