Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ The source code is available at [github.com/ChrisAdamsdevelopment/SpectraCleanse

For a step-by-step manual validation flow (local, API smoke, auth, billing, upload, cleanse, Docker, and production readiness), see [`docs/manual-qa-checklist.md`](docs/manual-qa-checklist.md).

- Browser metadata analysis uses maintained `music-metadata` with graceful fallback (`parseError`) when parsing fails, times out, or is skipped for very large files.
- Quick Cleanse metadata writing remains local/browser-side (MP3 via `browser-id3-writer`), while Full Server Cleanse still runs through `/api/process`.

---

## Contact
Expand Down
4 changes: 2 additions & 2 deletions docs/manual-qa-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,6 @@ Run these checks against your local server:
## 13) Known warnings

- `npm audit` currently reports vulnerabilities and should be triaged.
- `music-metadata-browser` is deprecated and currently used only for browser-side metadata analysis; plan a replacement/redesign in a future PR.
- Treat suspicious or corrupt media samples as high-risk during manual QA and verify metadata analysis paths carefully.
- Browser-side metadata analysis now uses maintained `music-metadata` with lazy loading, size bounds, and timeout fallback behavior.
- Treat suspicious, corrupt, or unusually large media samples as high-risk during manual QA and verify metadata analysis fallback behavior (`parseError`) carefully.
- Docker build/runtime validation still requires a real Docker-capable environment.
272 changes: 0 additions & 272 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"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",
Expand Down
27 changes: 24 additions & 3 deletions src/utils/metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,39 @@ 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'];
const MARKER_REGEX_CACHE = new Map();
const MAX_BROWSER_PARSE_BYTES = 100 * 1024 * 1024;
const PARSE_TIMEOUT_MS = 8000;

let parseBlobLoader = null;

async function getParseBlob() {
if (parseBlobLoader) return parseBlobLoader;
parseBlobLoader = import('music-metadata-browser').then((mod) => {
parseBlobLoader = import('music-metadata').then((mod) => {
const fn = mod?.parseBlob || mod?.default?.parseBlob;
if (typeof fn !== 'function') {
throw new Error('music-metadata-browser parseBlob export not found');
throw new Error('music-metadata parseBlob export not found');
}
return fn;
});
return parseBlobLoader;
}

function withTimeout(promise, timeoutMs) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(`Metadata parse timed out after ${timeoutMs}ms`)), timeoutMs);
promise.then(
(value) => {
clearTimeout(timer);
resolve(value);
},
(error) => {
clearTimeout(timer);
reject(error);
}
);
});
}

function escapeRegex(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
Expand Down Expand Up @@ -53,8 +71,11 @@ export async function readFileMetadata(file) {
let parseError = null;

try {
if ((file?.size || 0) > MAX_BROWSER_PARSE_BYTES) {
throw new Error(`File too large for browser metadata analysis (${Math.round(file.size / (1024 * 1024))}MB > ${Math.round(MAX_BROWSER_PARSE_BYTES / (1024 * 1024))}MB)`);
}
const parseBlob = await getParseBlob();
parsed = await parseBlob(file);
parsed = await withTimeout(parseBlob(file), PARSE_TIMEOUT_MS);
} catch (error) {
parseError = error;
}
Expand Down
Loading