@@ -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 ) || / l y r i c s / 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 || / l y r i c s / i. test ( line ) ) continue ;
219235 if ( / D e s c r i p t i o n | C o m m e n t / . 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 || / l y r i c s / 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 ( / D e s c r i p t i o n | C o m m e n t / . 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