Skip to content

Commit e7d8acb

Browse files
Fix MP4/M4A metadata persistence classification diagnostics
1 parent 2655a1e commit e7d8acb

1 file changed

Lines changed: 35 additions & 6 deletions

File tree

server/processor.js

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189,15 +189,25 @@ function hasDescriptiveMetadata(snapshot = {}) {
189189
return Boolean(snapshot.Title || snapshot.Artist || snapshot.Producer || snapshot.Copyright || snapshot.Genre || snapshot.Keyword || snapshot.Keywords || snapshot.Description || snapshot.Comment);
190190
}
191191

192-
function classifyMetadataPersistenceStage(snapshots = {}, hashes = {}) {
192+
function hasExpectedCoreMetadata(snapshot = {}) {
193+
return Boolean(snapshot.Title && snapshot.Artist && snapshot.Producer && snapshot.Copyright);
194+
}
195+
196+
function classifyMetadataPersistenceStage(snapshots = {}, hashes = {}, mismatchEvidence = {}) {
193197
const hasAfterWrite = hasDescriptiveMetadata(snapshots.after_descriptive_metadata_write);
194198
const hasAfterXmp = hasDescriptiveMetadata(snapshots.after_xmp_cleanup);
195199
const hasFinal = hasDescriptiveMetadata(snapshots.after_timestamp_write_final);
200+
const finalHasExpectedCore = hasExpectedCoreMetadata(snapshots.after_timestamp_write_final);
196201
if (!hasAfterWrite) return 'metadata_missing_after_descriptive_write';
197202
if (hasAfterWrite && !hasAfterXmp) return 'metadata_removed_by_xmp_cleanup';
198203
if (hasAfterXmp && !hasFinal) return 'metadata_removed_by_timestamp_write';
199-
const mismatch = hashes.after_xmp_cleanup && hashes.after_timestamp_write_final && hashes.after_xmp_cleanup !== hashes.after_timestamp_write_final && hasAfterXmp && hasFinal;
200-
if (mismatch) return 'metadata_present_in_snapshots_but_report_or_download_mismatch';
204+
if (finalHasExpectedCore) return 'metadata_present_and_verified';
205+
const hasExternalMismatchEvidence = Boolean(
206+
mismatchEvidence.clientHashMismatch
207+
|| mismatchEvidence.externalReportContradictsFinalSnapshot
208+
|| mismatchEvidence.downloadVerificationMismatch
209+
);
210+
if (hasExternalMismatchEvidence) return 'metadata_present_in_snapshots_but_report_or_download_mismatch';
201211
return 'metadata_present_and_verified';
202212
}
203213

@@ -212,16 +222,35 @@ async function deepSnapshot(stage, outputPath, runId, exiftoolVersion) {
212222
const includePrefixes = ['ItemList:', 'Keys:', 'UserData:', 'QuickTime:', 'Track1:', 'Track2:', 'XMP-', 'XMP:'];
213223
const includeFields = ['Title', 'DisplayName', 'Artist', 'AlbumArtist', 'Author', 'Producer', 'Copyright', 'Genre', 'Keyword', 'Keywords', 'Description', 'Comment', 'CreateDate', 'ModifyDate', 'TrackCreateDate', 'TrackModifyDate', 'MediaCreateDate', 'MediaModifyDate', 'XMPToolkit', 'Image::ExifTool'];
214224
const selectedMetadata = [];
225+
const seenSelected = new Set();
226+
const addSelected = (entry) => {
227+
if (!entry || seenSelected.has(entry) || /lyrics/i.test(entry)) return;
228+
seenSelected.add(entry);
229+
selectedMetadata.push(entry);
230+
};
215231
for (const line of lines) {
216232
if (!line || !line.includes(':')) continue;
217233
const isMatch = includePrefixes.some((p) => line.includes(p)) || includeFields.some((f) => line.includes(f));
218234
if (!isMatch || /lyrics/i.test(line)) continue;
219235
if (/Description|Comment/.test(line)) {
220236
const [, rawValue = ''] = line.split(/:\s+(.+)/);
221-
selectedMetadata.push(`${line.split(/:\s+/)[0]}: [redacted length=${rawValue.length} sha256=${sha256Text(rawValue)}]`);
237+
addSelected(`${line.split(/:\s+/)[0]}: [redacted length=${rawValue.length} sha256=${sha256Text(rawValue)}]`);
222238
continue;
223239
}
224-
selectedMetadata.push(line);
240+
addSelected(line);
241+
}
242+
if (selectedMetadata.length === 0 && raw && !Array.isArray(raw) && typeof raw === 'object') {
243+
for (const [key, value] of Object.entries(raw)) {
244+
if (!key || /lyrics/i.test(key)) continue;
245+
const isMatch = includePrefixes.some((p) => key.includes(p)) || includeFields.some((f) => key.includes(f));
246+
if (!isMatch) continue;
247+
if (/Description|Comment/.test(key)) {
248+
const redacted = redactLongTextField(value);
249+
addSelected(`${key}: [redacted length=${redacted.length} sha256=${redacted.sha256}]`);
250+
} else {
251+
addSelected(`${key}: ${stringifyValue(value)}`);
252+
}
253+
}
225254
}
226255
return {
227256
runId,
@@ -367,7 +396,7 @@ async function processMediaFile({ outputPath, platform = 'General', metadata = {
367396
after_descriptive_metadata_write: afterMetadataWriteSnapshot,
368397
after_xmp_cleanup: afterXmpCleanupSnapshot,
369398
after_timestamp_write_final: finalMetadataSnapshot,
370-
}, fileHashesByStage);
399+
}, fileHashesByStage, {});
371400
// Future fallback options (diagnostics-first): GPAC/MP4Box (strong candidate for descriptive QT/iTunes tags incl. producer),
372401
// AtomicParsley (good iTunes-style coverage, producer may be limited), FFmpeg mdta remux (easy but mapping can vary),
373402
// Bento4 (low-level ISO BMFF control via custom sidecar strategy).

0 commit comments

Comments
 (0)