-
Notifications
You must be signed in to change notification settings - Fork 0
Harden metadata parsing and marker detection; sanitize Quick Cleanse ID3 writes #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,37 @@ | ||||||||||||||||||||
| 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']; | ||||||||||||||||||||
| const MARKER_REGEX_CACHE = new Map(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function collectStrings(metadata) { | ||||||||||||||||||||
| let parseBlobLoader = null; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| async function getParseBlob() { | ||||||||||||||||||||
| if (parseBlobLoader) return parseBlobLoader; | ||||||||||||||||||||
| parseBlobLoader = import('music-metadata-browser').then((mod) => { | ||||||||||||||||||||
| const fn = mod?.parseBlob || mod?.default?.parseBlob; | ||||||||||||||||||||
| if (typeof fn !== 'function') { | ||||||||||||||||||||
| throw new Error('music-metadata-browser parseBlob export not found'); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return fn; | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| return parseBlobLoader; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function escapeRegex(value) { | ||||||||||||||||||||
| return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function markerToRegex(marker) { | ||||||||||||||||||||
| if (MARKER_REGEX_CACHE.has(marker)) return MARKER_REGEX_CACHE.get(marker); | ||||||||||||||||||||
| const escaped = escapeRegex(marker); | ||||||||||||||||||||
| const regex = marker.length <= 2 | ||||||||||||||||||||
| ? new RegExp(`\\b${escaped}\\b`, 'i') | ||||||||||||||||||||
| : new RegExp(`(?:^|\\W)${escaped}(?:$|\\W)`, 'i'); | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The boundary regex for markers longer than 2 chars uses Useful? React with 👍 / 👎. |
||||||||||||||||||||
| MARKER_REGEX_CACHE.set(marker, regex); | ||||||||||||||||||||
| return regex; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| function collectStrings(metadata, fileName = '') { | ||||||||||||||||||||
| const common = metadata?.common || {}; | ||||||||||||||||||||
| const native = metadata?.native || {}; | ||||||||||||||||||||
| const values = [common.title,common.artist,common.album,...(common.genre || []),...(common.comment || []),common.encodedby,common.publisher] | ||||||||||||||||||||
|
|
@@ -16,32 +44,53 @@ function collectStrings(metadata) { | |||||||||||||||||||
| if (frame?.value && typeof frame.value === 'object') values.push(JSON.stringify(frame.value)); | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| if (fileName) values.push(String(fileName)); | ||||||||||||||||||||
| 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)); | ||||||||||||||||||||
| let parsed = null; | ||||||||||||||||||||
| let parseError = null; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| try { | ||||||||||||||||||||
| const parseBlob = await getParseBlob(); | ||||||||||||||||||||
| parsed = await parseBlob(file); | ||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||
| parseError = error; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const searchable = collectStrings(parsed, file?.name || ''); | ||||||||||||||||||||
| const detectedMarkers = AI_MARKERS.filter((marker) => markerToRegex(marker).test(searchable)); | ||||||||||||||||||||
| return { | ||||||||||||||||||||
| format: parsed.format?.container || file.type || 'unknown', | ||||||||||||||||||||
| title: parsed.common?.title || file.name.replace(/\.[^.]+$/, ''), | ||||||||||||||||||||
| artist: parsed.common?.artist || '', | ||||||||||||||||||||
| genre: parsed.common?.genre?.[0] || '', | ||||||||||||||||||||
| 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', | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||||||||||||||||||||
| raw: parsed, | ||||||||||||||||||||
| parseError: parseError ? String(parseError?.message || parseError) : null, | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| 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'); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const safeText = (value) => { | ||||||||||||||||||||
| if (typeof value !== 'string') return ''; | ||||||||||||||||||||
| return value.replace(/\u0000/g, '').trim().slice(0, 500); | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
Comment on lines
+81
to
+84
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (bug_risk): Currently, non-string inputs result in
Suggested change
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| const title = safeText(metadata?.title); | ||||||||||||||||||||
| const artist = safeText(metadata?.artist); | ||||||||||||||||||||
| const genre = safeText(metadata?.genre); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if (title) writer.setFrame('TIT2', title); | ||||||||||||||||||||
| if (artist) writer.setFrame('TPE1', [artist]); | ||||||||||||||||||||
| if (genre) writer.setFrame('TCON', [genre]); | ||||||||||||||||||||
| if (title || artist || genre) writer.setFrame('TENC', 'SpectraCleanseAI Browser Quick Cleanse'); | ||||||||||||||||||||
| writer.addTag(); | ||||||||||||||||||||
| return new Blob([writer.getBlob()], { type: 'audio/mpeg' }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the dynamic
import('music-metadata-browser')fails once (for example due to a transient chunk/network load error in the browser),parseBlobLoaderis left as a rejected promise and every laterreadFileMetadata()call will fail in the same session even after conditions recover. This turns a temporary import hiccup into a persistent metadata outage until page reload; clear or rebuild the cache on rejection so subsequent calls can retry.Useful? React with 👍 / 👎.