From 69830c9beeb22f7f7404bc021278daddd751ca99 Mon Sep 17 00:00:00 2001 From: Triple7 Date: Mon, 4 May 2026 03:08:43 -0700 Subject: [PATCH] Integrate browser MP3 quick cleanse into root app --- app.tsx | 63 +++++++-- package-lock.json | 279 ++++++++++++++++++++++++++++++++++++++++ package.json | 4 +- src/utils/metadata.d.ts | 12 ++ src/utils/metadata.js | 47 +++++++ 5 files changed, 396 insertions(+), 9 deletions(-) create mode 100644 src/utils/metadata.d.ts create mode 100644 src/utils/metadata.js diff --git a/app.tsx b/app.tsx index d9c3463..35574d0 100644 --- a/app.tsx +++ b/app.tsx @@ -5,12 +5,13 @@ import { LogOut, User, Lock, Mail, Eye, EyeOff, Sparkles, ArrowUpCircle, Crown, Star, X, } from 'lucide-react'; -import * as mm from 'music-metadata'; +import { readFileMetadata, writeMP3Metadata } from './src/utils/metadata'; const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3001'; const PLATFORMS = ['General', 'YouTube', 'Spotify', 'Apple Music', 'TikTok'] as const; type Platform = typeof PLATFORMS[number]; type ItemStatus = 'pending' | 'analyzing' | 'processing' | 'done' | 'error'; +type RiskLevel = 'High' | 'Low'; // ───────────────────────────────────────────────────────────────────────────── // Types @@ -35,6 +36,8 @@ interface QueueItem { downloadName: string | null; report: { removedCount: number; removedTags: string[]; timestamp: string } | null; error: string | null; + analysis: { format: string; title: string; artist: string; genre: string; provenanceRisk: RiskLevel; detectedMarkers: string[] } | null; + logs: string[]; } // ───────────────────────────────────────────────────────────────────────────── @@ -561,7 +564,7 @@ export default function App() { file, status: 'pending' as ItemStatus, seo: { title: file.name.replace(/\.[^.]+$/, ''), description: '', tags: '' }, - downloadUrl: null, downloadName: null, report: null, error: null, + downloadUrl: null, downloadName: null, report: null, error: null, analysis: null, logs: [], })); if (newItems.length === 0) return; setQueue(prev => [...prev, ...newItems].slice(0, 20)); @@ -577,15 +580,17 @@ export default function App() { setActiveId(prev => prev === id ? null : prev); }; + const addLog = (id: string, message: string) => { + const stamp = new Date().toLocaleTimeString(); + updateItem(id, { logs: [...(queue.find(i => i.id === id)?.logs || []), `[${stamp}] ${message}`] }); + }; + const analyzeFile = async (item: QueueItem): Promise> => { try { - const parsed = await mm.parseBlob(item.file); + const parsed = await readFileMetadata(item.file); return { - seo: { - title: parsed.common.title || item.file.name.replace(/\.[^.]+$/, ''), - description: parsed.common.comment?.[0]?.text || '', - tags: parsed.common.genre?.[0] || '', - }, + seo: { title: parsed.title, description: '', tags: parsed.genre || '' }, + analysis: { format: parsed.format, title: parsed.title, artist: parsed.artist, genre: parsed.genre, provenanceRisk: parsed.provenanceRisk, detectedMarkers: parsed.detectedMarkers }, }; } catch { return {}; } }; @@ -601,10 +606,12 @@ export default function App() { if (cancelRef.cancelled) break; updateItem(item.id, { status: 'analyzing', error: null }); + addLog(item.id, 'Reading local metadata for analysis'); const analyzed = await analyzeFile(item); if (cancelRef.cancelled) break; updateItem(item.id, { ...analyzed, status: 'processing' }); + addLog(item.id, 'Starting server cleanse via /api/process'); // Grab the latest SEO values from state (user may have edited them) const currentSeo = await new Promise(resolve => { @@ -923,6 +930,10 @@ export default function App() { onChange={e => updateItem(activeItem.id, { seo: { ...activeItem.seo, description: e.target.value } })} className="w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:border-cyan-500 outline-none resize-none" /> +
+ +
+
+ + +
+ {activeItem.downloadUrl && ( + Manual Download Link + )} +
+ +
+

Analysis

+
+
Format: {activeItem.analysis?.format || '—'}
Title: {activeItem.analysis?.title || '—'}
+
Artist: {activeItem.analysis?.artist || '—'}
Genre: {activeItem.analysis?.genre || '—'}
+
Provenance Risk: {activeItem.analysis?.provenanceRisk || 'Low'}
+
Markers: {(activeItem.analysis?.detectedMarkers || []).join(', ') || 'none'}
+
+
+ +

System Log

{activeItem.logs.map((l, i) =>
{l}
)}
+ {/* Forensic report */} {activeItem.report && (
diff --git a/package-lock.json b/package-lock.json index 54d2098..d1cb62d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "autoprefixer": "^10.4.19", "bcryptjs": "^2.4.3", "better-sqlite3": "^9.6.0", + "browser-id3-writer": "4.4.0", "cors": "^2.8.5", "dotenv": "^16.4.5", "exiftool-vendored": "^28.3.1", @@ -26,6 +27,7 @@ "lucide-react": "^0.390.0", "multer": "^2.0.0", "music-metadata": "^11.12.3", + "music-metadata-browser": "2.5.11", "postcss": "^8.4.38", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -1004,6 +1006,18 @@ "vite": "^4.2.0 || ^5.0.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1213,6 +1227,12 @@ "node": ">=8" } }, + "node_modules/browser-id3-writer": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/browser-id3-writer/-/browser-id3-writer-4.4.0.tgz", + "integrity": "sha512-8xce9wo4VoKNR4udEGOAf8vndYxhToqQS+1wyrjdYVPQKRc4Wm6xwGG6XrKYgax28y5AvrbCkqK6t1RplPN2Ew==", + "license": "MIT" + }, "node_modules/browserslist": { "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", @@ -1688,6 +1708,24 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/exiftool-vendored": { "version": "28.8.0", "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.8.0.tgz", @@ -2486,6 +2524,182 @@ "node": ">=18" } }, + "node_modules/music-metadata-browser": { + "version": "2.5.11", + "resolved": "https://registry.npmjs.org/music-metadata-browser/-/music-metadata-browser-2.5.11.tgz", + "integrity": "sha512-Khq5nYapffIet0PUVb5J69pZPgqgn+/yoEr0jkO/OjH5xwfdz6rdwj0zsWPaqo3ylv+OthXoGjT6EegVHbMkJQ==", + "deprecated": "No longer support, superseded by music-metadata", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "debug": "^4.3.4", + "music-metadata": "^7.13.3", + "readable-stream": "^4.3.0", + "readable-web-to-node-stream": "^3.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/music-metadata-browser/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/music-metadata-browser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/music-metadata-browser/node_modules/file-type": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "license": "MIT", + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/music-metadata-browser/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/music-metadata-browser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/music-metadata-browser/node_modules/music-metadata": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-7.14.0.tgz", + "integrity": "sha512-xrm3w7SV0Wk+OythZcSbaI8mcr/KHd0knJieu8bVpaPfMv/Agz5EooCAPz3OR5hbYMiUG6dgAPKZKnMzV+3amA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "content-type": "^1.0.5", + "debug": "^4.3.4", + "file-type": "^16.5.4", + "media-typer": "^1.1.0", + "strtok3": "^6.3.0", + "token-types": "^4.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/music-metadata-browser/node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/music-metadata-browser/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/music-metadata-browser/node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/music-metadata-browser/node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/music-metadata/node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2862,6 +3076,15 @@ "node": ">=10" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3004,6 +3227,62 @@ "node": ">= 6" } }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", diff --git a/package.json b/package.json index ba066c7..28c0369 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,9 @@ "postcss": "^8.4.38", "tailwindcss": "^3.4.4", "typescript": "^5.5.2", - "vite": "^4.5.3" + "vite": "^4.5.3", + "browser-id3-writer": "4.4.0", + "music-metadata-browser": "2.5.11" }, "devDependencies": {}, "license": "MIT" diff --git a/src/utils/metadata.d.ts b/src/utils/metadata.d.ts new file mode 100644 index 0000000..2702ff4 --- /dev/null +++ b/src/utils/metadata.d.ts @@ -0,0 +1,12 @@ +export interface FileMetadataResult { + format: string; + title: string; + artist: string; + genre: string; + detectedMarkers: string[]; + provenanceRisk: 'High' | 'Low'; + raw: unknown; +} + +export function readFileMetadata(file: File): Promise; +export function writeMP3Metadata(file: File, metadata: { title?: string; artist?: string; genre?: string }): Promise; diff --git a/src/utils/metadata.js b/src/utils/metadata.js new file mode 100644 index 0000000..a921079 --- /dev/null +++ b/src/utils/metadata.js @@ -0,0 +1,47 @@ +import { parseBlob } from 'music-metadata-browser'; +import ID3Writer from 'browser-id3-writer'; + +const AI_MARKERS = ['ai','generated','suno','udio','boomy','aiva','soundraw','mubert','stable audio','provenance','c2pa','content credentials','watermark','synthetic','elevenlabs']; + +function collectStrings(metadata) { + const common = metadata?.common || {}; + const native = metadata?.native || {}; + const values = [common.title,common.artist,common.album,...(common.genre || []),...(common.comment || []),common.encodedby,common.publisher] + .filter(Boolean).map(v => String(v)); + Object.values(native).forEach((frames) => { + (frames || []).forEach((frame) => { + if (frame?.id) values.push(String(frame.id)); + if (typeof frame?.value === 'string') values.push(frame.value); + if (Array.isArray(frame?.value)) frame.value.forEach(v => values.push(String(v))); + if (frame?.value && typeof frame.value === 'object') values.push(JSON.stringify(frame.value)); + }); + }); + return values.join(' | ').toLowerCase(); +} + +export async function readFileMetadata(file) { + const parsed = await parseBlob(file); + const searchable = collectStrings(parsed); + const detectedMarkers = AI_MARKERS.filter(marker => searchable.includes(marker)); + return { + format: parsed.format?.container || file.type || 'unknown', + title: parsed.common?.title || file.name.replace(/\.[^.]+$/, ''), + artist: parsed.common?.artist || '', + genre: parsed.common?.genre?.[0] || '', + detectedMarkers, + provenanceRisk: detectedMarkers.length > 0 ? 'High' : 'Low', + raw: parsed, + }; +} + +export async function writeMP3Metadata(file, metadata) { + const buffer = await file.arrayBuffer(); + const writer = new ID3Writer(buffer); + writer.removeTag(); + if (metadata.title) writer.setFrame('TIT2', metadata.title); + if (metadata.artist) writer.setFrame('TPE1', [metadata.artist]); + if (metadata.genre) writer.setFrame('TCON', [metadata.genre]); + writer.setFrame('TENC', 'SpectraCleanseAI Browser Quick Cleanse'); + writer.addTag(); + return new Blob([writer.getBlob()], { type: 'audio/mpeg' }); +}