Skip to content

Commit 8618e55

Browse files
authored
m3u handling during db scan (#18467)
1 parent 6bb72e8 commit 8618e55

1 file changed

Lines changed: 276 additions & 0 deletions

File tree

tasks/task_database.c

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
380620
static 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

Comments
 (0)