Skip to content

Add in-browser MP3 metadata cleanse, metadata utils, and integrated frontend UI#2

Closed
ChrisAdamsdevelopment wants to merge 1 commit into
codex/improve-ui-designfrom
codex/merge-in-browser-mp3-cleaning-into-app
Closed

Add in-browser MP3 metadata cleanse, metadata utils, and integrated frontend UI#2
ChrisAdamsdevelopment wants to merge 1 commit into
codex/improve-ui-designfrom
codex/merge-in-browser-mp3-cleaning-into-app

Conversation

@ChrisAdamsdevelopment
Copy link
Copy Markdown
Owner

@ChrisAdamsdevelopment ChrisAdamsdevelopment commented May 4, 2026

Superseded by PR #9, which integrates browser MP3 quick cleanse into the existing root app without creating a separate client/ frontend.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 4, 2026

Reviewer's Guide

Adds 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 flows

sequenceDiagram
  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
Loading

Class diagram for new React app and metadata utilities

classDiagram
  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
Loading

File-Level Changes

Change Details Files
Introduce React/Vite client app with tabbed UX, auth/session handling, file upload, and integrated cleanse workflows.
  • Build a new App component that restores auth/session from localStorage, fetches /api/me, and preserves logout behavior.
  • Add three-tab layout (Track Context, SEO Payload, File Analysis) with Tailwind-based dark theme and Lucide icons.
  • Implement drag-and-drop and file input for multiple audio formats, wiring selection to metadata analysis and context state.
  • Wire Quick Cleanse and Full Server Cleanse actions, including usage header handling, upgrade modal, and processed file download link.
  • Add system log panel and server forensic report display, including safe revocation of object URLs on replacement/unmount.
client/src/App.tsx
Add browser-side metadata utilities for inspecting and rewriting MP3 ID3 tags.
  • Implement readFileMetadata to parse audio metadata with music-metadata-browser, detect AI provenance markers, and compute a simple risk flag.
  • Normalize parsed metadata into a compact object used to prefill context and analysis views.
  • Implement writeMP3Metadata using browser-id3-writer to overwrite key ID3v2.3 frames (title, artist, album, genre, comment, lyrics, year) and return a new Blob.
client/src/utils/metadata.js
Set up client package configuration and dependencies for the new React/Tailwind/Vite frontend and metadata libraries.
  • Create client/package.json with React 18, Vite, TypeScript, Tailwind, PostCSS, and Lucide React dependencies.
  • Add browser-id3-writer and music-metadata-browser as runtime dependencies for client-side metadata parsing and writing.
  • Configure basic npm scripts for dev, build, and preview flows.
client/package.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread client/src/App.tsx

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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread client/src/App.tsx
Comment on lines +71 to +91
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);
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);
}
};

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread client/src/App.tsx
Comment on lines +135 to +136
const limit = Number(res.headers.get('X-Usage-Limit') || usage.limit);
setUsage({ used, limit });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread client/src/App.tsx
Comment on lines +82 to +85
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');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant