@@ -158,6 +158,7 @@ typedef struct database_state_handle
158158{
159159 database_info_list_t * info ;
160160 struct string_list * list ;
161+ struct string_list * m3u_list ; /* List of M3U files found during scan */
161162 uint8_t * buf ;
162163 size_t list_index ;
163164 size_t entry_index ;
@@ -377,6 +378,245 @@ static void task_database_cue_prune(database_info_handle_t *db,
377378 free (fd );
378379}
379380
381+ /* Remove disc indicators from title string */
382+ /* Helper function to validate if a string is a valid disc indicator
383+ * Valid formats:
384+ * - Single/double digit: 0-99
385+ * - Single letter: A-Z
386+ * - Roman numerals: I, II, III, IV, V, VI, VII, VIII, IX, X, etc.
387+ * - "X of Y" format: 1 of 2, 01 of 10, etc.
388+ */
389+ static bool is_valid_disc_indicator (const char * str , size_t len )
390+ {
391+ const char * p = str ;
392+ const char * end = str + len ;
393+
394+ if (len == 0 || len > 10 ) /* Sanity check */
395+ return false;
396+
397+ /* Check for single letter (A-Z) */
398+ if (len == 1 && isalpha ((unsigned char )* p ))
399+ return true;
400+
401+ /* Check for 1-2 digit number (0-99) */
402+ if (len <= 2 && isdigit ((unsigned char )* p ))
403+ {
404+ p ++ ;
405+ if (p == end )
406+ return true; /* Single digit */
407+ if (isdigit ((unsigned char )* p ) && p + 1 == end )
408+ return true; /* Double digit */
409+ return false;
410+ }
411+
412+ /* Check for "X of Y" pattern where X and Y are 1-2 digits */
413+ if (len >= 5 && isdigit ((unsigned char )* p ))
414+ {
415+ /* Parse first number (1-2 digits) */
416+ p ++ ;
417+ if (p < end && isdigit ((unsigned char )* p ))
418+ p ++ ;
419+
420+ /* Check for " of " */
421+ if (p + 4 <= end && strncmp (p , " of " , 4 ) == 0 )
422+ {
423+ p += 4 ;
424+ /* Parse second number (1-2 digits) */
425+ if (p < end && isdigit ((unsigned char )* p ))
426+ {
427+ p ++ ;
428+ if (p < end && isdigit ((unsigned char )* p ))
429+ p ++ ;
430+ if (p == end )
431+ return true;
432+ }
433+ }
434+ return false;
435+ }
436+
437+ /* Check for Roman numerals (I, II, III, IV, V, VI, VII, VIII, IX, X, etc.) */
438+ /* Valid Roman numeral chars: I, V, X (we'll be conservative) */
439+ if (len >= 1 && len <= 4 )
440+ {
441+ bool all_roman = true;
442+ const char * roman_p = str ;
443+ while (roman_p < end )
444+ {
445+ char c = * roman_p ;
446+ if (c != 'I' && c != 'V' && c != 'X' )
447+ {
448+ all_roman = false;
449+ break ;
450+ }
451+ roman_p ++ ;
452+ }
453+ if (all_roman )
454+ return true;
455+ }
456+
457+ return false;
458+ }
459+
460+ static void remove_disc_indicators (char * title , size_t len )
461+ {
462+ char * disc_pos = NULL ;
463+
464+ /* Search for common disc patterns */
465+ if ((disc_pos = strstr (title , " (Disc " )) ||
466+ (disc_pos = strstr (title , " (disc " )) ||
467+ (disc_pos = strstr (title , " (Disk " )) ||
468+ (disc_pos = strstr (title , " (disk " )))
469+ {
470+ /* Find the closing parenthesis */
471+ char * end_pos = strchr (disc_pos , ')' );
472+ if (end_pos )
473+ {
474+ /* Extract the disc indicator text (between " (Disc " and ")") */
475+ const char * indicator_start = disc_pos + 7 ; /* Skip " (Disc " */
476+ size_t indicator_len = end_pos - indicator_start ;
477+
478+ /* Validate this is actually a disc indicator, not arbitrary text */
479+ if (is_valid_disc_indicator (indicator_start , indicator_len ))
480+ {
481+ /* Truncate at the disc indicator */
482+ * disc_pos = '\0' ;
483+ /* Remove trailing whitespace */
484+ string_trim_whitespace_right (title );
485+ }
486+ }
487+ }
488+ }
489+
490+ static void task_database_iterate_m3u (
491+ db_handle_t * _db ,
492+ database_state_handle_t * db_state ,
493+ const char * m3u_path )
494+ {
495+ size_t i , j ;
496+ bool found_match = false;
497+ char first_matched_db [NAME_MAX_LENGTH ];
498+ char first_matched_crc [128 ];
499+ char collapsed_title [NAME_MAX_LENGTH ];
500+ m3u_file_t * m3u_file = NULL ;
501+
502+ first_matched_db [0 ] = '\0' ;
503+ first_matched_crc [0 ] = '\0' ;
504+ collapsed_title [0 ] = '\0' ;
505+
506+ /* Open M3U file */
507+ if (!(m3u_file = m3u_file_init (m3u_path )))
508+ {
509+ RARCH_ERR ("[Scanner] Failed to open M3U file: %s\n" , m3u_path );
510+ return ;
511+ }
512+
513+ /* Scan each referenced file and check if it's in scan_results */
514+ for (i = 0 ; i < m3u_file_get_size (m3u_file ); i ++ )
515+ {
516+ m3u_file_entry_t * entry = NULL ;
517+ const char * ref_path = NULL ;
518+
519+ if (!m3u_file_get_entry (m3u_file , i , & entry ))
520+ continue ;
521+
522+ ref_path = entry -> full_path ;
523+ if (string_is_empty (ref_path ))
524+ continue ;
525+
526+ /* Look for this file in scan results */
527+ for (j = 0 ; j < _db -> scan_results .count ; j ++ )
528+ {
529+ scan_result_t * result = & _db -> scan_results .results [j ];
530+ char result_path_resolved [PATH_MAX_LENGTH ];
531+
532+ result_path_resolved [0 ] = '\0' ;
533+
534+ if (!result -> entry_path )
535+ continue ;
536+
537+ /* Resolve the scan result path to absolute form for comparison */
538+ strlcpy (result_path_resolved , result -> entry_path ,
539+ sizeof (result_path_resolved ));
540+ path_resolve_realpath (result_path_resolved ,
541+ sizeof (result_path_resolved ), false);
542+
543+ if (string_is_equal (ref_path , result_path_resolved ))
544+ {
545+ /* Found a match! */
546+ if (!found_match )
547+ {
548+ /* First match - save the info */
549+ found_match = true;
550+ strlcpy (first_matched_db , result -> db_name ,
551+ sizeof (first_matched_db ));
552+ strlcpy (first_matched_crc , result -> db_crc ,
553+ sizeof (first_matched_crc ));
554+ strlcpy (collapsed_title , result -> entry_label ,
555+ sizeof (collapsed_title ));
556+
557+ /* Remove disc indicator from title */
558+ remove_disc_indicators (collapsed_title ,
559+ sizeof (collapsed_title ));
560+ }
561+
562+ /* Mark this result for removal */
563+ /* We'll remove it by setting entry_path to NULL */
564+ /* and compacting the array later */
565+ if (result -> entry_path )
566+ {
567+ free (result -> entry_path );
568+ result -> entry_path = NULL ;
569+ }
570+ }
571+ }
572+ }
573+
574+ m3u_file_free (m3u_file );
575+
576+ /* If we found at least one match, add M3U entry */
577+ if (found_match )
578+ {
579+ if (!scan_results_add (& _db -> scan_results , m3u_path , collapsed_title ,
580+ first_matched_crc , first_matched_db , NULL ))
581+ {
582+ RARCH_ERR ("[Scanner] Failed to add M3U result: \"%s\"\n" , m3u_path );
583+ }
584+ else
585+ {
586+ RARCH_LOG ("[Scanner] Matched M3U \"%s\" to \"%s\"\n" ,
587+ collapsed_title , first_matched_db );
588+ }
589+ }
590+
591+ /* Compact scan_results to remove NULL entries */
592+ {
593+ size_t write_idx = 0 ;
594+ for (i = 0 ; i < _db -> scan_results .count ; i ++ )
595+ {
596+ if (_db -> scan_results .results [i ].entry_path != NULL )
597+ {
598+ if (write_idx != i )
599+ _db -> scan_results .results [write_idx ] =
600+ _db -> scan_results .results [i ];
601+ write_idx ++ ;
602+ }
603+ else
604+ {
605+ /* Free any remaining allocated fields */
606+ if (_db -> scan_results .results [i ].entry_label )
607+ free (_db -> scan_results .results [i ].entry_label );
608+ if (_db -> scan_results .results [i ].db_crc )
609+ free (_db -> scan_results .results [i ].db_crc );
610+ if (_db -> scan_results .results [i ].db_name )
611+ free (_db -> scan_results .results [i ].db_name );
612+ if (_db -> scan_results .results [i ].archive_name )
613+ free (_db -> scan_results .results [i ].archive_name );
614+ }
615+ }
616+ _db -> scan_results .count = write_idx ;
617+ }
618+ }
619+
380620static void gdi_prune (database_info_handle_t * db , const char * name )
381621{
382622 size_t i ;
@@ -1396,6 +1636,14 @@ static void task_database_handler(retro_task_t *task)
13961636 goto task_finished ;
13971637 }
13981638
1639+ /* Initialize M3U list tracking */
1640+ dbstate -> m3u_list = string_list_new ();
1641+ if (!dbstate -> m3u_list )
1642+ {
1643+ RARCH_ERR ("[Scanner] Failed to initialize M3U list\n" );
1644+ goto task_finished ;
1645+ }
1646+
13991647 RARCH_LOG ("[Scanner] %s\"%s\"...\n" , msg_hash_to_str (MSG_MANUAL_CONTENT_SCAN_START ), db -> fullpath );
14001648 if (retroarch_override_setting_is_set (RARCH_OVERRIDE_SETTING_DATABASE_SCAN , NULL ))
14011649 printf ("%s\"%s\"...\n" , msg_hash_to_str (MSG_MANUAL_CONTENT_SCAN_START ), db -> fullpath );
@@ -1404,6 +1652,16 @@ static void task_database_handler(retro_task_t *task)
14041652 break ;
14051653 case DATABASE_STATUS_ITERATE_START :
14061654 name = database_info_get_current_element_name (dbinfo );
1655+
1656+ /* Check if this is an M3U file and add to list for post-processing */
1657+ if (m3u_file_is_m3u (name ))
1658+ {
1659+ union string_list_elem_attr attr ;
1660+ attr .i = 0 ;
1661+ if (dbstate -> m3u_list )
1662+ string_list_append (dbstate -> m3u_list , name , attr );
1663+ }
1664+
14071665 task_database_cleanup_state (dbstate );
14081666 dbstate -> list_index = 0 ;
14091667 dbstate -> entry_index = 0 ;
@@ -1462,6 +1720,22 @@ static void task_database_handler(retro_task_t *task)
14621720#else
14631721 fprintf (stderr , "msg: %s\n" , msg );
14641722#endif
1723+ /* Process M3U files after main scan completes */
1724+ if (dbstate -> m3u_list && dbstate -> m3u_list -> size > 0 )
1725+ {
1726+ size_t m ;
1727+ RARCH_LOG ("[Scanner] Processing %u M3U files...\n" ,
1728+ (unsigned )dbstate -> m3u_list -> size );
1729+
1730+ /* Scan M3U files and collapse disc entries */
1731+ for (m = 0 ; m < dbstate -> m3u_list -> size ; m ++ )
1732+ {
1733+ const char * m3u_path = dbstate -> m3u_list -> elems [m ].data ;
1734+ if (m3u_path )
1735+ task_database_iterate_m3u (db , dbstate , m3u_path );
1736+ }
1737+ }
1738+
14651739 /* Batch update all playlists with accumulated results */
14661740 if (db -> scan_results .count > 0 )
14671741 scan_results_batch_update_playlists (& db -> scan_results , db );
@@ -1488,6 +1762,8 @@ static void task_database_handler(retro_task_t *task)
14881762 {
14891763 if (dbstate -> list )
14901764 dir_list_free (dbstate -> list );
1765+ if (dbstate -> m3u_list )
1766+ string_list_free (dbstate -> m3u_list );
14911767 }
14921768
14931769 if (db )
0 commit comments