@@ -49,7 +49,7 @@ const ProviderMusixmatch = (() => {
4949 q_duration : durr ,
5050 f_subtitle_length : Math . floor ( durr ) ,
5151 usertoken : CONFIG . providers . musixmatch . token ,
52- part : "track_lyrics_translation_status" ,
52+ part : "track_lyrics_translation_status,track_structure,track_performer_tagging " ,
5353 } ;
5454
5555 const finalURL =
@@ -91,6 +91,130 @@ const ProviderMusixmatch = (() => {
9191 return body ;
9292 }
9393
94+ function parsePerformerData ( meta ) {
95+ if ( ! meta || ! meta . track || ! meta . track . performer_tagging ) {
96+ return [ ] ;
97+ }
98+
99+ const tagging = meta . track . performer_tagging ;
100+ const miscTags = meta . track . performer_tagging_misc_tags || { } ;
101+ let performerMap = [ ] ;
102+ if ( tagging && tagging . content && tagging . content . length > 0 ) {
103+ const resources = tagging . resources ?. artists || [ ] ;
104+ const resourcesList = Array . isArray ( resources ) ? resources : Object . values ( resources ) ;
105+
106+ performerMap = tagging . content
107+ . map ( ( c ) => {
108+ if ( ! c . performers || c . performers . length === 0 ) return null ;
109+
110+ const resolvedPerformers = c . performers
111+ . map ( ( p ) => {
112+ let name = "Unknown" ;
113+ if ( p . type === "artist" ) {
114+ const fqid = p . fqid ;
115+ const idFromFqid = fqid ? parseInt ( fqid . split ( ":" ) [ 2 ] ) : null ;
116+
117+ const artist = resourcesList . find ( ( r ) => r . artist_id === idFromFqid ) ;
118+ if ( artist ) name = artist . artist_name ;
119+ } else if ( miscTags [ p . type ] ) {
120+ name = miscTags [ p . type ] ;
121+ }
122+ return {
123+ fqid : p . fqid ,
124+ artist_id : p . fqid ? parseInt ( p . fqid . split ( ":" ) [ 2 ] ) : null ,
125+ name : name ,
126+ } ;
127+ } )
128+ . filter ( ( p ) => p . name !== "Unknown" ) ;
129+
130+ const names = resolvedPerformers . map ( ( p ) => p . name ) ;
131+ if ( names . length === 0 ) return null ;
132+
133+ return {
134+ name : names . join ( ", " ) ,
135+ snippet : c . snippet ,
136+ performers : resolvedPerformers ,
137+ } ;
138+ } )
139+ . filter ( Boolean ) ;
140+ }
141+
142+ const normalizeForMatch = ( text ) => text . replace ( / \s + / g, "" ) . toLowerCase ( ) ;
143+
144+ const snippetQueue = [ ] ;
145+ if ( performerMap . length > 0 ) {
146+ for ( const tag of performerMap ) {
147+ if ( ! tag . snippet ) continue ;
148+ const snippetLines = tag . snippet
149+ . split ( / \n + / )
150+ . map ( ( s ) => s . trim ( ) )
151+ . filter ( Boolean ) ;
152+ for ( const sLine of snippetLines ) {
153+ if ( sLine . length < 2 && ! / ^ [ \u3131 - \uD79D ] / . test ( sLine ) ) continue ;
154+ snippetQueue . push ( {
155+ text : normalizeForMatch ( sLine ) ,
156+ raw : sLine ,
157+ performers : tag . performers ,
158+ } ) ;
159+ }
160+ }
161+ }
162+ return snippetQueue ;
163+ }
164+
165+ function matchSequential ( lyricsLines , snippetQueue , getTextCallback = ( l ) => l . text ) {
166+ if ( ! snippetQueue || snippetQueue . length === 0 ) return lyricsLines ;
167+
168+ const normalizeForMatch = ( text ) => text . replace ( / \s + / g, "" ) . toLowerCase ( ) ;
169+ let queueCursor = 0 ;
170+ const LOOKAHEAD = 5 ;
171+
172+ return lyricsLines . map ( ( line ) => {
173+ const lineText = getTextCallback ( line ) || "♪" ;
174+ let normalizedLine = normalizeForMatch ( lineText ) ;
175+
176+ let matchedPerformers = [ ] ;
177+
178+ while ( queueCursor < snippetQueue . length ) {
179+ let matchFoundAtOffset = - 1 ;
180+
181+ for ( let i = 0 ; i < LOOKAHEAD && queueCursor + i < snippetQueue . length ; i ++ ) {
182+ const snippet = snippetQueue [ queueCursor + i ] ;
183+
184+ if ( normalizedLine . includes ( snippet . text ) && snippet . text . length > 0 ) {
185+ matchFoundAtOffset = i ;
186+ break ;
187+ }
188+ }
189+
190+ if ( matchFoundAtOffset !== - 1 ) {
191+ queueCursor += matchFoundAtOffset ;
192+ const matchedSnippet = snippetQueue [ queueCursor ] ;
193+ matchedPerformers . push ( ...matchedSnippet . performers ) ;
194+ normalizedLine = normalizedLine . replace ( matchedSnippet . text , "" ) ;
195+ queueCursor ++ ;
196+ } else {
197+ break ;
198+ }
199+ }
200+
201+ const uniquePerformers = [ ] ;
202+ const sawMap = new Set ( ) ;
203+ for ( const p of matchedPerformers ) {
204+ const key = p . fqid || p . name ;
205+ if ( ! sawMap . has ( key ) ) {
206+ sawMap . add ( key ) ;
207+ uniquePerformers . push ( p ) ;
208+ }
209+ }
210+
211+ return {
212+ ...line ,
213+ performers : uniquePerformers ,
214+ } ;
215+ } ) ;
216+ }
217+
94218 async function getKaraoke ( body ) {
95219 const meta = body ?. [ "matcher.track.get" ] ?. message ?. body ;
96220 if ( ! meta ) {
@@ -124,6 +248,8 @@ const ProviderMusixmatch = (() => {
124248
125249 result = result . message . body ;
126250
251+ const snippetQueue = parsePerformerData ( meta ) ;
252+
127253 const parsedKaraoke = JSON . parse ( result . richsync . richsync_body ) . map ( ( line ) => {
128254 const startTime = line . ts * 1000 ;
129255 const endTime = line . te * 1000 ;
@@ -143,11 +269,26 @@ const ProviderMusixmatch = (() => {
143269 } ) ;
144270 return {
145271 startTime,
272+ endTime,
146273 text,
147274 } ;
148275 } ) ;
149276
150- return parsedKaraoke ;
277+ return matchSequential ( parsedKaraoke , snippetQueue , ( line ) => {
278+ if ( Array . isArray ( line . text ) ) {
279+ return line . text . map ( ( t ) => t . word ) . join ( "" ) ;
280+ }
281+ return line . text ;
282+ } ) . map ( ( line ) => {
283+ const performerNames = ( line . performers || [ ] )
284+ . map ( ( p ) => p . name )
285+ . filter ( Boolean )
286+ . join ( ", " ) ;
287+ return {
288+ ...line ,
289+ performer : performerNames || null ,
290+ } ;
291+ } ) ;
151292 }
152293
153294 function getSynced ( body ) {
@@ -169,10 +310,22 @@ const ProviderMusixmatch = (() => {
169310 return null ;
170311 }
171312
172- return JSON . parse ( subtitle . subtitle_body ) . map ( ( line ) => ( {
173- text : line . text || "♪" ,
174- startTime : line . time . total * 1000 ,
175- } ) ) ;
313+ const snippetQueue = parsePerformerData ( meta ) ;
314+ const rawLines = JSON . parse ( subtitle . subtitle_body ) ;
315+
316+ return matchSequential ( rawLines , snippetQueue , ( l ) => l . text ) . map ( ( line ) => {
317+ const lineText = line . text || "♪" ;
318+ const performerNames = ( line . performers || [ ] )
319+ . map ( ( p ) => p . name )
320+ . filter ( Boolean )
321+ . join ( ", " ) ;
322+
323+ return {
324+ text : lineText ,
325+ startTime : line . time . total * 1000 ,
326+ performer : performerNames || null ,
327+ } ;
328+ } ) ;
176329 }
177330
178331 return null ;
@@ -196,7 +349,21 @@ const ProviderMusixmatch = (() => {
196349 if ( ! lyrics ) {
197350 return null ;
198351 }
199- return lyrics . split ( "\n" ) . map ( ( text ) => ( { text } ) ) ;
352+
353+ const snippetQueue = parsePerformerData ( meta ) ;
354+ const rawLines = lyrics . split ( "\n" ) . map ( ( text ) => ( { text } ) ) ;
355+
356+ return matchSequential ( rawLines , snippetQueue , ( l ) => l . text ) . map ( ( line ) => {
357+ const performerNames = ( line . performers || [ ] )
358+ . map ( ( p ) => p . name )
359+ . filter ( Boolean )
360+ . join ( ", " ) ;
361+
362+ return {
363+ ...line ,
364+ performer : performerNames || null ,
365+ } ;
366+ } ) ;
200367 }
201368
202369 return null ;
0 commit comments