@@ -4,7 +4,7 @@ import {ByteVector, StringType} from "../../byteVector";
44import { Frame , FrameClassType } from "./frame" ;
55import { Id3v2FrameHeader } from "./frameHeader" ;
66import { FrameIdentifier , FrameIdentifiers } from "../frameIdentifiers" ;
7- import { Guards , StringComparison , StringUtils } from "../../utils" ;
7+ import { Guards , StringComparison } from "../../utils" ;
88
99/**
1010 * This class provides support for ID3v2 text information frames (section 4.2) covering `T000` to
@@ -419,65 +419,29 @@ export class TextInformationFrame extends Frame {
419419 // * (CR) - "Cover"
420420 // * (( - used to escape a "(" in a refinement/genre name
421421
422- // NOTE: This encoding has an inherent flaw around how multiple genres should be
423- // encoded. Since multiple genres are already an edge case, I'm just going to
424- // say yolo to this whole block of code copied over from the .NET implementation
425- while ( value . length > 1 && value [ 0 ] === "(" ) {
426- const closing = value . indexOf ( ")" ) ;
427- if ( closing < 0 ) {
428- break ;
422+ // Treat each term separately
423+ const terms = Id3v2Settings . useNonStandardV2V3GenreSeparators
424+ ? value . split ( / [ ; \/ ] / ) . filter ( t => ! ! t )
425+ : [ value ] ;
426+ for ( const term of terms ) {
427+ // Attempt to process it according to our best understanding of the spec
428+ const numericGenres = this . parseTconAsStandardNumeric ( term ) ;
429+ if ( numericGenres !== undefined ) {
430+ fieldList . push ( ... numericGenres ) ;
431+ continue ;
429432 }
430433
431- const number = value . substring ( 1 , closing ) ;
432-
433- let text : string ;
434- if ( number === TextInformationFrame . COVER_ABBREV ) {
435- text = TextInformationFrame . COVER_STRING ;
436- } else if ( number === TextInformationFrame . REMIX_ABBREV ) {
437- text = TextInformationFrame . REMIX_STRING ;
438- } else {
439- text = Genres . indexToAudio ( number , true ) ;
440- }
441-
442- if ( ! text ) {
443- // Number in parentheses was not a numeric genre but part of a larger bit
444- // of text?
445- break ;
446- }
447-
448- // Number in parentheses was a numeric genre
449- fieldList . push ( text ) ;
450- value = StringUtils . trimStart ( value . substring ( closing + 1 ) , "/ " ) ;
451-
452- // Ignore genre if the same genre appears after the numeric genre
453- if ( value . startsWith ( text ) ) {
454- value = StringUtils . trimStart ( value . substring ( text . length ) , "/ " ) ;
455- }
456- }
457-
458- // Process whatever's left
459- if ( value . length > 0 ) {
460- // Split the remaining genre value by dividers if the setting is turned on
461- let splitValue = Id3v2Settings . useNonStandardV2V3GenreSeparators
462- ? value . split ( / [ \/ ; ] / ) . map ( ( v ) => v . trim ( ) ) . filter ( ( v ) => ! ! v )
463- : [ value ] ;
464-
465- splitValue = splitValue . map ( ( v ) => {
466- // Unescape escaped opening parenthesis
467- let v2 = v . replace ( / \( \( / , "(" ) ;
468-
469- // If non-standard numeric genres is enabled, parse them
470- if ( Id3v2Settings . useNonStandardV2V3NumericGenres ) {
471- const text = Genres . indexToAudio ( v2 , false ) ;
472- if ( text ) {
473- v2 = text ;
474- }
434+ if ( Id3v2Settings . useNonStandardV2V3NumericGenres ) {
435+ // Attempt to process it as a non-standard numeric genre
436+ const numericGenre = Genres . indexToAudioDirect ( term ) ;
437+ if ( numericGenre !== undefined ) {
438+ fieldList . push ( numericGenre ) ;
439+ continue ;
475440 }
441+ }
476442
477- return v2 ;
478- } ) ;
479-
480- fieldList . push ( ...splitValue ) ;
443+ // Yeah, we can't do anything smart, just treat it as a string
444+ fieldList . push ( term ) ;
481445 }
482446 } else {
483447 fieldList . push ( value ) ;
@@ -558,24 +522,28 @@ export class TextInformationFrame extends Frame {
558522 const numericGenres = [ ] ;
559523 const textGenres = [ ] ;
560524 for ( const s of text ) {
561- switch ( s ) {
562- case TextInformationFrame . COVER_STRING :
563- numericGenres . push ( `(${ TextInformationFrame . COVER_ABBREV } )` ) ;
564- break ;
565- case TextInformationFrame . REMIX_STRING :
566- numericGenres . push ( `(${ TextInformationFrame . REMIX_ABBREV } )` ) ;
567- break ;
568- default :
569- if ( Id3v2Settings . useNumericGenres ) {
525+ if ( Id3v2Settings . useNumericGenres ) {
526+ // Try to process it as a numeric genre
527+ switch ( s ) {
528+ case TextInformationFrame . COVER_STRING :
529+ numericGenres . push ( `(${ TextInformationFrame . COVER_ABBREV } )` ) ;
530+ continue ;
531+ case TextInformationFrame . REMIX_STRING :
532+ numericGenres . push ( `(${ TextInformationFrame . REMIX_ABBREV } )` ) ;
533+ continue ;
534+ default :
570535 const numericGenre = Genres . audioToIndex ( s ) ;
571536 if ( numericGenre !== 255 ) {
572537 numericGenres . push ( `(${ numericGenre } )` ) ;
573- break ;
538+ continue ;
574539 }
575- }
576- textGenres . push ( s . replace ( / \( / , "((" ) ) ;
577- break ;
540+ break ;
541+ }
578542 }
543+
544+ // Process it as a text genre
545+ const escapedGenre = s . replace ( / \( / g, "((" ) ;
546+ textGenres . push ( escapedGenre ) ;
579547 }
580548
581549 // Put the entire string together
@@ -590,6 +558,101 @@ export class TextInformationFrame extends Frame {
590558 }
591559
592560 // #endregion
561+
562+ private parseTconAsStandardNumeric ( field : string ) : string [ ] | undefined {
563+ // Don't even bother setting up the state machine if we aren't starting with an opening
564+ // parenthesis.
565+ if ( field [ 0 ] !== "(" ) {
566+ return undefined ;
567+ }
568+
569+ const results : string [ ] = [ ] ;
570+ let inParentheses = true ;
571+ let refinementAdded = false ;
572+ let open = 0 ;
573+ let close = 0 ;
574+
575+ const appendToLastResult = ( chunk : string ) : void => {
576+ if ( ! chunk ) {
577+ return ;
578+ }
579+
580+ const lastResult = results [ results . length - 1 ] ;
581+ results [ results . length - 1 ] = refinementAdded
582+ ? `${ lastResult } ${ chunk } `
583+ : `${ lastResult } ${ chunk } ` ;
584+ }
585+
586+ for ( let i = 1 ; i < field . length ; i ++ ) {
587+ if ( inParentheses ) {
588+ // Inside parentheses ----------------------------------
589+ if ( field [ i ] === ")" ) {
590+ // Closing parenthesis found
591+ close = i ;
592+
593+ // Attempt to parse the inside as a number
594+ const parenContents = field . substring ( open + 1 , close ) ;
595+ const numericGenre = Genres . indexToAudioDirect ( parenContents ) ;
596+ if ( numericGenre !== undefined ) {
597+ results . push ( numericGenre ) ;
598+ } else if ( parenContents === TextInformationFrame . COVER_ABBREV ) {
599+ results . push ( TextInformationFrame . COVER_STRING ) ;
600+ } else if ( parenContents === TextInformationFrame . REMIX_ABBREV ) {
601+ results . push ( TextInformationFrame . REMIX_STRING ) ;
602+ } else {
603+ // What we expected to be a numeric genre was not. We will assume this
604+ // field is not using standard numeric genres, and dump the remainder.
605+ break ;
606+ }
607+
608+ // Transition to refinement processing
609+ inParentheses = false ;
610+ refinementAdded = false ;
611+ open = i + 1 ;
612+ }
613+
614+ // If we didn't find the closing paren, just increment and try again.
615+ } else {
616+ // Processing refinement ------------------------------
617+ let refinementChunk : string ;
618+ if ( field [ i ] === "(" ) {
619+ if ( field [ i + 1 ] === "(" ) {
620+ // This is an escape sequence
621+ // Take the current refinement chunk + the first paren (eg: `xyz(`)
622+ refinementChunk = field . substring ( open , i + 1 ) ;
623+
624+
625+ // Skip over the next character (ie, `(`)
626+ open = i + 2 ;
627+ i ++ ;
628+ } else {
629+ // This is possibly the start of a numeric genre.
630+ // Take the current refinement chunk
631+ refinementChunk = field . substring ( open , i ) ;
632+
633+ // Transition back to numeric genre processing.
634+ inParentheses = true ;
635+ open = i ;
636+ }
637+
638+ // Add the refinement chunk to the last result
639+ appendToLastResult ( refinementChunk ) ;
640+ refinementAdded = true ;
641+ }
642+
643+ // If we didn't find an opening paren, just increment and try again
644+ }
645+ }
646+
647+ // Process the remainder
648+ // If we didn't find any results, then just return undefined.
649+ if ( results . length === 0 ) {
650+ return undefined ;
651+ }
652+
653+ appendToLastResult ( field . substring ( open ) ) ;
654+ return results ;
655+ }
593656}
594657
595658export class UserTextInformationFrame extends TextInformationFrame {
0 commit comments