Add in-browser MP3 metadata cleanse, metadata utils, and integrated frontend UI#2
Conversation
Reviewer's GuideAdds a new React/Vite client app that integrates in-browser MP3 metadata cleansing, metadata inspection, and SEO payload generation with the existing auth, usage, and server cleanse flows. Sequence diagram for in-browser and server cleanse flowssequenceDiagram
actor User
participant App
participant MetadataUtils as metadata_utils
participant BrowserURL as URL_API
participant Backend as Backend_API
User->>App: Select audio file (drop or browse)
App->>MetadataUtils: readFileMetadata(file)
MetadataUtils-->>App: MetadataAnalysis
App->>App: setContextFromMetadata
App->>App: setActiveTab(analysis)
User->>App: Edit context and lyrics
User->>App: Request SEO payload
App->>Backend: POST /api/generate-seo
Backend-->>App: SEO payload
App->>App: updateSeoState
App->>App: setActiveTab(seo)
alt Quick Cleanse (Browser)
User->>App: Click Quick Cleanse (Browser)
App->>MetadataUtils: writeMP3Metadata(file, mp3MetadataInput)
MetadataUtils-->>App: Blob
App->>BrowserURL: URL.createObjectURL(blob)
BrowserURL-->>App: objectUrl
App->>App: setProcessedAsset(source browser)
else Full Server Cleanse
User->>App: Click Full Server Cleanse
App->>Backend: POST /api/process (FormData)
Backend-->>App: Response with file Blob
App->>App: updateUsageFromHeaders
App->>BrowserURL: URL.createObjectURL(blob)
BrowserURL-->>App: objectUrl
App->>App: setProcessedAsset(source server)
App->>App: buildForensicReport
end
User->>App: Click Download Processed File
App-->>User: Processed audio download
Class diagram for new React app and metadata utilitiesclassDiagram
class App {
-string token
-User user
-TabKey activeTab
-File file
-any metadataAnalysis
-ContextState context
-SeoState seo
-UsageState usage
-ProcessedAsset processedAsset
-any forensicReport
-boolean showUpgradeModal
-boolean loading
-LogEntry[] logs
-HTMLInputElement fileInputRef
+addLog(message string, level LogLevel)
+logout()
+onFile(selected File)
+generateSeo()
+quickCleanse()
+serverCleanse()
}
class AuthScreen {
-boolean isLogin
-string email
-string password
-string error
+submit()
+onAuthed(token string, user User)
}
class UpgradeModal {
-string creatorLink
-string studioLink
+onClose()
}
class metadata_utils {
+readFileMetadata(file File) Promise~MetadataAnalysis~
+writeMP3Metadata(file File, metadata Mp3MetadataInput) Promise~Blob~
}
class User {
+number id
+string email
+string plan
+string role
}
class ProcessedAsset {
+string url
+string filename
+string source
}
class ContextState {
+string artist
+string title
+string genre
+string vibe
+string lyrics
}
class SeoState {
+string title
+string description
+string tags
+string lyrics
}
class UsageState {
+number used
+number limit
}
class MetadataAnalysis {
+string title
+string artist
+string genre
+string format
+string risk
+string[] detectedMarkers
}
class Mp3MetadataInput {
+string title
+string artist
+string album
+string genre
+string comment
+string lyrics
+number year
}
class LogEntry {
+string ts
+string message
+LogLevel level
}
App --> AuthScreen : renders when unauthenticated
App --> UpgradeModal : renders when showUpgradeModal
App --> metadata_utils : uses
App --> User : holds
App --> ProcessedAsset : holds
App --> ContextState : holds
App --> SeoState : holds
App --> UsageState : holds
metadata_utils --> MetadataAnalysis : returns
metadata_utils --> Mp3MetadataInput : consumes
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- The tab button rendering uses
<t[2] />as a JSX element, which won’t work because JSX components must be identifiers; consider assigningconst Icon = t[2];and rendering<Icon ... />instead to avoid runtime/TSX errors. - App state like
metadataAnalysis,context, andseoare currently typed asany/implicit objects inApp.tsx; defining explicit interfaces for these shapes will catch mistakes (e.g., missing fields or typos) and improve editor tooling. - The
readFileMetadatafunction stringifies all native metadata for marker detection, which may be unnecessarily heavy for large files; consider limiting the serialized subset (or short-circuiting early) to reduce parsing and memory overhead in the browser.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The tab button rendering uses `<t[2] />` as a JSX element, which won’t work because JSX components must be identifiers; consider assigning `const Icon = t[2];` and rendering `<Icon ... />` instead to avoid runtime/TSX errors.
- App state like `metadataAnalysis`, `context`, and `seo` are currently typed as `any`/implicit objects in `App.tsx`; defining explicit interfaces for these shapes will catch mistakes (e.g., missing fields or typos) and improve editor tooling.
- The `readFileMetadata` function stringifies all native metadata for marker detection, which may be unnecessarily heavy for large files; consider limiting the serialized subset (or short-circuiting early) to reduce parsing and memory overhead in the browser.
## Individual Comments
### Comment 1
<location path="client/src/App.tsx" line_range="154" />
<code_context>
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-3">{[['context','Track Context',AudioLines],['seo','SEO Payload',Sparkles],['analysis','File Analysis',FileSearch]].map((t:any)=><button key={t[0]} onClick={()=>setActiveTab(t[0])} className={`p-3 rounded border transition ${activeTab===t[0]?'bg-cyan-600/20 border-cyan-400':'bg-slate-900 border-slate-700'}`}><t[2] className="inline w-4 h-4 mr-2" />{t[1]}</button>)}</div>
</code_context>
<issue_to_address>
**issue (bug_risk):** Using `<t[2] />` as a component will not work; the icon needs to be assigned to a capitalized component variable first.
You can keep the config array but destructure the icon into a capitalized variable when mapping, e.g.:
```tsx
{[
['context', 'Track Context', AudioLines],
['seo', 'SEO Payload', Sparkles],
['analysis', 'File Analysis', FileSearch],
].map(([key, label, Icon]) => (
<button
key={key}
onClick={() => setActiveTab(key as TabKey)}
className={`p-3 rounded border transition ${
activeTab === key
? 'bg-cyan-600/20 border-cyan-400'
: 'bg-slate-900 border-slate-700'
}`}
>
<Icon className="inline w-4 h-4 mr-2" />
{label}
</button>
))}
```
This pattern ensures the icon is a valid JSX component and avoids runtime/type errors.
</issue_to_address>
### Comment 2
<location path="client/src/App.tsx" line_range="71-91" />
<code_context>
+ setLoading(true);
+ try {
+ const promptText = `Artist: ${context.artist}\nTitle: ${context.title}\nGenre: ${context.genre}\nVibe: ${context.vibe}\nLyrics: ${context.lyrics}`;
+ const res = await fetch(`${API_BASE}/api/generate-seo`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', ...authHeader },
+ body: JSON.stringify({ promptText })
+ });
+ if (res.status === 401) return logout();
+ const data = await res.json();
+ setSeo({ title: data.title || '', description: data.description || '', tags: Array.isArray(data.tags) ? data.tags.join(', ') : data.tags || '', lyrics: data.lyrics || context.lyrics });
+ setActiveTab('seo');
</code_context>
<issue_to_address>
**suggestion (bug_risk):** `generateSeo` assumes a successful JSON response even when the status is non-2xx (except 401), which can mask server errors.
Non-401 4xx/5xx responses are still passed to `res.json()` and used to update SEO, which can leave SEO state in an inconsistent or empty state while logs imply success.
Add an explicit `res.ok` check before parsing JSON and updating state, and log/return early on failure, e.g.:
```ts
const res = await fetch(...);
if (res.status === 401) return logout();
if (!res.ok) {
addLog(`SEO generation failed with status ${res.status}`, 'error');
return;
}
const data = await res.json();
```
This keeps error handling explicit and avoids treating error responses as valid SEO payloads.
```suggestion
const generateSeo = async () => {
if (!token) return;
setLoading(true);
try {
const promptText = `Artist: ${context.artist}\nTitle: ${context.title}\nGenre: ${context.genre}\nVibe: ${context.vibe}\nLyrics: ${context.lyrics}`;
const res = await fetch(`${API_BASE}/api/generate-seo`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeader },
body: JSON.stringify({ promptText })
});
if (res.status === 401) return logout();
if (!res.ok) {
addLog(`SEO generation failed with status ${res.status}`, 'error');
return;
}
const data = await res.json();
setSeo({
title: data.title || '',
description: data.description || '',
tags: Array.isArray(data.tags) ? data.tags.join(', ') : data.tags || '',
lyrics: data.lyrics || context.lyrics
});
setActiveTab('seo');
addLog('SEO payload generated', 'success');
} catch {
addLog('Failed to generate SEO payload', 'error');
} finally {
setLoading(false);
}
};
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
|
||
| return <div className="min-h-screen bg-slate-950 text-slate-100 p-6"><div className="max-w-6xl mx-auto space-y-6"> | ||
| <header className="flex items-center justify-between"><h1 className="text-2xl font-bold text-cyan-300">SpectraCleanse AI</h1><div className="flex items-center gap-3"><span className="px-3 py-1 rounded bg-slate-800">{(user.plan || 'free').toUpperCase()}</span><span className="text-sm text-slate-300">Usage {usage.used}/{usage.limit}</span><button onClick={logout} className="px-3 py-2 rounded bg-slate-800 hover:bg-slate-700"><LogOut className="w-4 h-4" /></button></div></header> | ||
| <div className="grid grid-cols-1 md:grid-cols-3 gap-3">{[['context','Track Context',AudioLines],['seo','SEO Payload',Sparkles],['analysis','File Analysis',FileSearch]].map((t:any)=><button key={t[0]} onClick={()=>setActiveTab(t[0])} className={`p-3 rounded border transition ${activeTab===t[0]?'bg-cyan-600/20 border-cyan-400':'bg-slate-900 border-slate-700'}`}><t[2] className="inline w-4 h-4 mr-2" />{t[1]}</button>)}</div> |
There was a problem hiding this comment.
issue (bug_risk): Using <t[2] /> as a component will not work; the icon needs to be assigned to a capitalized component variable first.
You can keep the config array but destructure the icon into a capitalized variable when mapping, e.g.:
{[
['context', 'Track Context', AudioLines],
['seo', 'SEO Payload', Sparkles],
['analysis', 'File Analysis', FileSearch],
].map(([key, label, Icon]) => (
<button
key={key}
onClick={() => setActiveTab(key as TabKey)}
className={`p-3 rounded border transition ${
activeTab === key
? 'bg-cyan-600/20 border-cyan-400'
: 'bg-slate-900 border-slate-700'
}`}
>
<Icon className="inline w-4 h-4 mr-2" />
{label}
</button>
))}This pattern ensures the icon is a valid JSX component and avoids runtime/type errors.
| const generateSeo = async () => { | ||
| if (!token) return; | ||
| setLoading(true); | ||
| try { | ||
| const promptText = `Artist: ${context.artist}\nTitle: ${context.title}\nGenre: ${context.genre}\nVibe: ${context.vibe}\nLyrics: ${context.lyrics}`; | ||
| const res = await fetch(`${API_BASE}/api/generate-seo`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json', ...authHeader }, | ||
| body: JSON.stringify({ promptText }) | ||
| }); | ||
| if (res.status === 401) return logout(); | ||
| const data = await res.json(); | ||
| setSeo({ title: data.title || '', description: data.description || '', tags: Array.isArray(data.tags) ? data.tags.join(', ') : data.tags || '', lyrics: data.lyrics || context.lyrics }); | ||
| setActiveTab('seo'); | ||
| addLog('SEO payload generated', 'success'); | ||
| } catch { | ||
| addLog('Failed to generate SEO payload', 'error'); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; |
There was a problem hiding this comment.
suggestion (bug_risk): generateSeo assumes a successful JSON response even when the status is non-2xx (except 401), which can mask server errors.
Non-401 4xx/5xx responses are still passed to res.json() and used to update SEO, which can leave SEO state in an inconsistent or empty state while logs imply success.
Add an explicit res.ok check before parsing JSON and updating state, and log/return early on failure, e.g.:
const res = await fetch(...);
if (res.status === 401) return logout();
if (!res.ok) {
addLog(`SEO generation failed with status ${res.status}`, 'error');
return;
}
const data = await res.json();This keeps error handling explicit and avoids treating error responses as valid SEO payloads.
| const generateSeo = async () => { | |
| if (!token) return; | |
| setLoading(true); | |
| try { | |
| const promptText = `Artist: ${context.artist}\nTitle: ${context.title}\nGenre: ${context.genre}\nVibe: ${context.vibe}\nLyrics: ${context.lyrics}`; | |
| const res = await fetch(`${API_BASE}/api/generate-seo`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', ...authHeader }, | |
| body: JSON.stringify({ promptText }) | |
| }); | |
| if (res.status === 401) return logout(); | |
| const data = await res.json(); | |
| setSeo({ title: data.title || '', description: data.description || '', tags: Array.isArray(data.tags) ? data.tags.join(', ') : data.tags || '', lyrics: data.lyrics || context.lyrics }); | |
| setActiveTab('seo'); | |
| addLog('SEO payload generated', 'success'); | |
| } catch { | |
| addLog('Failed to generate SEO payload', 'error'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| const generateSeo = async () => { | |
| if (!token) return; | |
| setLoading(true); | |
| try { | |
| const promptText = `Artist: ${context.artist}\nTitle: ${context.title}\nGenre: ${context.genre}\nVibe: ${context.vibe}\nLyrics: ${context.lyrics}`; | |
| const res = await fetch(`${API_BASE}/api/generate-seo`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', ...authHeader }, | |
| body: JSON.stringify({ promptText }) | |
| }); | |
| if (res.status === 401) return logout(); | |
| if (!res.ok) { | |
| addLog(`SEO generation failed with status ${res.status}`, 'error'); | |
| return; | |
| } | |
| const data = await res.json(); | |
| setSeo({ | |
| title: data.title || '', | |
| description: data.description || '', | |
| tags: Array.isArray(data.tags) ? data.tags.join(', ') : data.tags || '', | |
| lyrics: data.lyrics || context.lyrics | |
| }); | |
| setActiveTab('seo'); | |
| addLog('SEO payload generated', 'success'); | |
| } catch { | |
| addLog('Failed to generate SEO payload', 'error'); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 233cb5baee
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const limit = Number(res.headers.get('X-Usage-Limit') || usage.limit); | ||
| setUsage({ used, limit }); |
There was a problem hiding this comment.
Handle unlimited usage header before numeric cast
The server sends X-Usage-Limit: unlimited for non-free plans (server.js), but this code always casts the header with Number(...), which turns that value into NaN. As soon as a paid user runs a cleanse, usage state becomes invalid and the UI renders Usage <used>/NaN, breaking plan/limit feedback for that session.
Useful? React with 👍 / 👎.
| const data = await res.json(); | ||
| setSeo({ title: data.title || '', description: data.description || '', tags: Array.isArray(data.tags) ? data.tags.join(', ') : data.tags || '', lyrics: data.lyrics || context.lyrics }); | ||
| setActiveTab('seo'); | ||
| addLog('SEO payload generated', 'success'); |
There was a problem hiding this comment.
Check SEO response status before treating it as success
This path parses JSON and logs success even when /api/generate-seo returns an error status (e.g., 400 invalid prompt or 500 when GEMINI_API_KEY is unset). In those cases the error payload is interpreted as SEO data, fields are overwritten with empty strings, and the UI switches to the SEO tab with a false success message instead of surfacing the failure.
Useful? React with 👍 / 👎.
Superseded by PR #9, which integrates browser MP3 quick cleanse into the existing root app without creating a separate client/ frontend.