@@ -559,6 +559,134 @@ function compareHex(hash, target) {
559559 return false ; // equal, not less
560560}
561561
562+ function normalizeLyricsText ( value ) {
563+ if ( typeof value !== 'string' ) {
564+ return '' ;
565+ }
566+
567+ return value . replace ( / \r \n / g, '\n' ) . trimEnd ( ) ;
568+ }
569+
570+ function parseLrcTimestamp ( token ) {
571+ const match = token . match ( / ^ ( \d { 2 } ) : ( \d { 2 } ) [ . : ] ( \d { 2 , 3 } ) $ / ) ;
572+ if ( ! match ) {
573+ return null ;
574+ }
575+
576+ const minutes = Number . parseInt ( match [ 1 ] , 10 ) ;
577+ const seconds = Number . parseInt ( match [ 2 ] , 10 ) ;
578+ const fractionRaw = match [ 3 ] ;
579+ const fractionMs =
580+ fractionRaw . length === 2
581+ ? Number . parseInt ( fractionRaw , 10 ) * 10
582+ : Number . parseInt ( fractionRaw , 10 ) ;
583+
584+ return ( minutes * 60 + seconds ) * 1000 + fractionMs ;
585+ }
586+
587+ function parseLrcLines ( syncedLyrics ) {
588+ const normalized = normalizeLyricsText ( syncedLyrics ) ;
589+ if ( ! normalized ) {
590+ return [ ] ;
591+ }
592+
593+ const parsed = normalized
594+ . split ( '\n' )
595+ . map ( ( line ) => {
596+ const match = line . match ( / ^ \[ ( [ ^ \] ] + ) \] ( .* ) $ / ) ;
597+ if ( ! match ) {
598+ return null ;
599+ }
600+
601+ const startMs = parseLrcTimestamp ( match [ 1 ] ) ;
602+ if ( startMs === null ) {
603+ return null ;
604+ }
605+
606+ return {
607+ text : ( match [ 2 ] || '' ) . trim ( ) ,
608+ start_ms : startMs ,
609+ } ;
610+ } )
611+ . filter ( ( line ) => line ) ;
612+
613+ return parsed . map ( ( line , index ) => ( {
614+ ...line ,
615+ end_ms : parsed [ index + 1 ] ?. start_ms ,
616+ } ) ) ;
617+ }
618+
619+ function stripTimestamp ( syncedLyrics ) {
620+ const normalized = normalizeLyricsText ( syncedLyrics ) ;
621+ if ( ! normalized ) {
622+ return '' ;
623+ }
624+
625+ return normalized . replace ( / ^ \[ [ ^ \] ] + \] * / gm, '' ) ;
626+ }
627+
628+ function isInstrumentalLyrics ( syncedLyrics ) {
629+ const lowered = normalizeLyricsText ( syncedLyrics ) . toLowerCase ( ) ;
630+ return lowered . includes ( '[au:' ) && lowered . includes ( 'instrumental' ) ;
631+ }
632+
633+ function yamlQuote ( value ) {
634+ return `'${ String ( value ) . replace ( / ' / g, "''" ) } '` ;
635+ }
636+
637+ function indentBlock ( text , spaces = 2 ) {
638+ const indent = ' ' . repeat ( spaces ) ;
639+ return text
640+ . split ( '\n' )
641+ . map ( ( line ) => `${ indent } ${ line } ` )
642+ . join ( '\n' ) ;
643+ }
644+
645+ function buildLyricsfile ( { trackName, artistName, albumName, duration, plainLyrics, syncedLyrics } ) {
646+ const normalizedPlain = normalizeLyricsText ( plainLyrics ) ;
647+ const normalizedSynced = normalizeLyricsText ( syncedLyrics ) ;
648+ const isInstrumental = isInstrumentalLyrics ( normalizedSynced ) ;
649+ const parsedLines = isInstrumental ? [ ] : parseLrcLines ( normalizedSynced ) ;
650+ const plain = isInstrumental
651+ ? ''
652+ : normalizedPlain || stripTimestamp ( normalizedSynced ) ;
653+
654+ const lines = [
655+ `version: ${ yamlQuote ( '1.0' ) } ` ,
656+ 'metadata:' ,
657+ ` title: ${ yamlQuote ( trackName ) } ` ,
658+ ` artist: ${ yamlQuote ( artistName ) } ` ,
659+ ] ;
660+
661+ if ( normalizeLyricsText ( albumName ) . trim ( ) ) {
662+ lines . push ( ` album: ${ yamlQuote ( albumName ) } ` ) ;
663+ }
664+
665+ if ( Number . isFinite ( duration ) && duration > 0 ) {
666+ lines . push ( ` duration_ms: ${ Math . round ( duration * 1000 ) } ` ) ;
667+ }
668+
669+ lines . push ( ` instrumental: ${ isInstrumental ? 'true' : 'false' } ` ) ;
670+
671+ if ( parsedLines . length > 0 ) {
672+ lines . push ( 'lines:' ) ;
673+ parsedLines . forEach ( ( line ) => {
674+ lines . push ( ` - text: ${ yamlQuote ( line . text ) } ` ) ;
675+ lines . push ( ` start_ms: ${ line . start_ms } ` ) ;
676+ if ( line . end_ms !== undefined ) {
677+ lines . push ( ` end_ms: ${ line . end_ms } ` ) ;
678+ }
679+ } ) ;
680+ }
681+
682+ if ( plain ) {
683+ lines . push ( 'plain: |' ) ;
684+ lines . push ( indentBlock ( plain ) ) ;
685+ }
686+
687+ return `${ lines . join ( '\n' ) } \n` ;
688+ }
689+
562690// Multi-worker proof of work solver
563691async function solvePow ( prefix , target , onProgress ) {
564692 const numWorkers = navigator . hardwareConcurrency || 4 ;
@@ -702,13 +830,26 @@ $('#form-publish').addEventListener('submit', async (e) => {
702830 }
703831
704832 const token = `${ prefix } :${ nonce } ` ;
833+ const duration = Number . parseFloat ( data . duration ) ;
834+ if ( ! Number . isFinite ( duration ) || duration <= 0 ) {
835+ showError ( $ ( '#publish-results' ) , 'Please enter a valid duration before publishing' ) ;
836+ return ;
837+ }
705838 const body = {
706839 trackName : data . trackName ,
707840 artistName : data . artistName ,
708841 albumName : data . albumName ,
709- duration : parseInt ( data . duration , 10 ) ,
842+ duration,
710843 plainLyrics : data . plainLyrics || '' ,
711844 syncedLyrics : data . syncedLyrics || '' ,
845+ lyricsfile : buildLyricsfile ( {
846+ trackName : data . trackName ,
847+ artistName : data . artistName ,
848+ albumName : data . albumName ,
849+ duration,
850+ plainLyrics : data . plainLyrics || '' ,
851+ syncedLyrics : data . syncedLyrics || '' ,
852+ } ) ,
712853 } ;
713854
714855 const container = $ ( '#publish-results' ) ;
0 commit comments