diff --git a/config.def.h b/config.def.h index e3c31674ad..291920f073 100644 --- a/config.def.h +++ b/config.def.h @@ -743,6 +743,8 @@ static const unsigned playlist_sublabel_runtime_type = PLAYLIST_RUNTIME_PER_CORE static const bool playlist_show_sublabels = false; +static const bool playlist_fuzzy_archive_match = false; + /* Show Menu start-up screen on boot. */ static const bool default_menu_show_start_screen = true; diff --git a/configuration.c b/configuration.c index f1a9b4e260..36eb58cf4a 100644 --- a/configuration.c +++ b/configuration.c @@ -1601,6 +1601,7 @@ static struct config_bool_setting *populate_settings_bool(settings_t *settings, SETTING_BOOL("content_runtime_log_aggregate", &settings->bools.content_runtime_log_aggregate, true, content_runtime_log_aggregate, false); SETTING_BOOL("playlist_show_sublabels", &settings->bools.playlist_show_sublabels, true, playlist_show_sublabels, false); SETTING_BOOL("playlist_sort_alphabetical", &settings->bools.playlist_sort_alphabetical, true, playlist_sort_alphabetical, false); + SETTING_BOOL("playlist_fuzzy_archive_match", &settings->bools.playlist_fuzzy_archive_match, true, playlist_fuzzy_archive_match, false); SETTING_BOOL("quit_press_twice", &settings->bools.quit_press_twice, true, quit_press_twice, false); SETTING_BOOL("vibrate_on_keypress", &settings->bools.vibrate_on_keypress, true, vibrate_on_keypress, false); diff --git a/configuration.h b/configuration.h index 3263ddc622..3ffe8c2787 100644 --- a/configuration.h +++ b/configuration.h @@ -317,6 +317,7 @@ typedef struct settings bool playlist_sort_alphabetical; bool playlist_show_sublabels; + bool playlist_fuzzy_archive_match; bool quit_press_twice; bool vibrate_on_keypress; diff --git a/intl/msg_hash_lbl.h b/intl/msg_hash_lbl.h index 3a8408cf7d..ea269418aa 100644 --- a/intl/msg_hash_lbl.h +++ b/intl/msg_hash_lbl.h @@ -1825,6 +1825,8 @@ MSG_HASH(MENU_ENUM_LABEL_PLAYLIST_SORT_ALPHABETICAL, "playlist_sort_alphabetical") MSG_HASH(MENU_ENUM_LABEL_PLAYLIST_SHOW_SUBLABELS, "playlist_show_sublabels") +MSG_HASH(MENU_ENUM_LABEL_PLAYLIST_FUZZY_ARCHIVE_MATCH, + "playlist_fuzzy_archive_match") MSG_HASH(MENU_ENUM_LABEL_PLAYLIST_SUBLABEL_RUNTIME_TYPE, "playlist_sublabel_runtime_type") MSG_HASH(MENU_ENUM_LABEL_HELP_SEND_DEBUG_INFO, diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h index ceb0784410..01f4d3b889 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -8592,6 +8592,14 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_RUNTIME_PER_CORE, "Per Core" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_PLAYLIST_FUZZY_ARCHIVE_MATCH, + "Fuzzy archive matching" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_PLAYLIST_FUZZY_ARCHIVE_MATCH, + "When searching playlists for entries associated with compressed files, match only the archive file name instead of [file name]+[content]. Enable this to avoid duplicate content history entries when loading compressed files." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_PLAYLIST_RUNTIME_AGGREGATE, "Aggregate" diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c index 4ed457b353..9a1e44acb2 100644 --- a/menu/cbs/menu_cbs_sublabel.c +++ b/menu/cbs/menu_cbs_sublabel.c @@ -549,6 +549,7 @@ default_sublabel_macro(action_bind_sublabel_menu_ticker_type, default_sublabel_macro(action_bind_sublabel_menu_ticker_speed, MENU_ENUM_SUBLABEL_MENU_TICKER_SPEED) default_sublabel_macro(action_bind_sublabel_playlist_show_inline_core_name, MENU_ENUM_SUBLABEL_PLAYLIST_SHOW_INLINE_CORE_NAME) default_sublabel_macro(action_bind_sublabel_playlist_sort_alphabetical, MENU_ENUM_SUBLABEL_PLAYLIST_SORT_ALPHABETICAL) +default_sublabel_macro(action_bind_sublabel_playlist_fuzzy_archive_match, MENU_ENUM_SUBLABEL_PLAYLIST_FUZZY_ARCHIVE_MATCH) default_sublabel_macro(action_bind_sublabel_menu_rgui_full_width_layout, MENU_ENUM_SUBLABEL_MENU_RGUI_FULL_WIDTH_LAYOUT) default_sublabel_macro(action_bind_sublabel_menu_rgui_extended_ascii, MENU_ENUM_SUBLABEL_MENU_RGUI_EXTENDED_ASCII) default_sublabel_macro(action_bind_sublabel_help_send_debug_info, MENU_ENUM_SUBLABEL_HELP_SEND_DEBUG_INFO) @@ -2494,6 +2495,9 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, case MENU_ENUM_LABEL_PLAYLIST_SORT_ALPHABETICAL: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_playlist_sort_alphabetical); break; + case MENU_ENUM_LABEL_PLAYLIST_FUZZY_ARCHIVE_MATCH: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_playlist_fuzzy_archive_match); + break; case MENU_ENUM_LABEL_MENU_RGUI_FULL_WIDTH_LAYOUT: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_menu_rgui_full_width_layout); break; diff --git a/menu/menu_displaylist.c b/menu/menu_displaylist.c index 0a22e1a1c5..8ab4957d41 100644 --- a/menu/menu_displaylist.c +++ b/menu/menu_displaylist.c @@ -5460,6 +5460,7 @@ bool menu_displaylist_ctl(enum menu_displaylist_ctl_state type, {MENU_ENUM_LABEL_PLAYLIST_SHOW_INLINE_CORE_NAME, PARSE_ONLY_UINT}, {MENU_ENUM_LABEL_PLAYLIST_SHOW_SUBLABELS, PARSE_ONLY_BOOL}, {MENU_ENUM_LABEL_PLAYLIST_SUBLABEL_RUNTIME_TYPE, PARSE_ONLY_UINT}, + {MENU_ENUM_LABEL_PLAYLIST_FUZZY_ARCHIVE_MATCH, PARSE_ONLY_BOOL}, }; for (i = 0; i < ARRAY_SIZE(build_list); i++) diff --git a/menu/menu_setting.c b/menu/menu_setting.c index 60a907583c..e747f9e56c 100644 --- a/menu/menu_setting.c +++ b/menu/menu_setting.c @@ -12741,6 +12741,22 @@ static bool setting_append_list( &setting_get_string_representation_uint_playlist_inline_core_display_type; menu_settings_list_current_add_range(list, list_info, 0, PLAYLIST_INLINE_CORE_DISPLAY_LAST-1, 1, true, true); + CONFIG_BOOL( + list, list_info, + &settings->bools.playlist_fuzzy_archive_match, + MENU_ENUM_LABEL_PLAYLIST_FUZZY_ARCHIVE_MATCH, + MENU_ENUM_LABEL_VALUE_PLAYLIST_FUZZY_ARCHIVE_MATCH, + playlist_fuzzy_archive_match, + MENU_ENUM_LABEL_VALUE_OFF, + MENU_ENUM_LABEL_VALUE_ON, + &group_info, + &subgroup_info, + parent_group, + general_write_handler, + general_read_handler, + SD_FLAG_NONE + ); + END_SUB_GROUP(list, list_info, parent_group); END_GROUP(list, list_info, parent_group); diff --git a/msg_hash.h b/msg_hash.h index 095a4263c3..aa6c67a4a1 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -2322,6 +2322,7 @@ enum msg_hash_enums MENU_LABEL(PLAYLIST_SHOW_INLINE_CORE_NAME), MENU_LABEL(PLAYLIST_SORT_ALPHABETICAL), MENU_LABEL(PLAYLIST_SHOW_SUBLABELS), + MENU_LABEL(PLAYLIST_FUZZY_ARCHIVE_MATCH), MENU_LABEL(PLAYLIST_SUBLABEL_RUNTIME_TYPE), MENU_ENUM_LABEL_VALUE_PLAYLIST_INLINE_CORE_DISPLAY_HIST_FAV, diff --git a/playlist.c b/playlist.c index 123aef2063..80ffac7bb6 100644 --- a/playlist.c +++ b/playlist.c @@ -74,6 +74,133 @@ typedef int (playlist_sort_fun_t)( const struct playlist_entry *a, const struct playlist_entry *b); +/** + * playlist_path_equal: + * @real_path : 'Real' search path, generated by path_resolve_realpath() + * @entry_path : Existing playlist entry 'path' value + * + * Returns 'true' if real_path matches entry_path + * (Taking into account relative paths, case insensitive + * filesystems, 'incomplete' archive paths) + **/ +static bool playlist_path_equal(const char *real_path, const char *entry_path) +{ + settings_t *settings = config_get_ptr(); + bool real_path_is_compressed; + bool entry_real_path_is_compressed; + char entry_real_path[PATH_MAX_LENGTH]; + + entry_real_path[0] = '\0'; + + /* Sanity check */ + if (string_is_empty(real_path) || string_is_empty(entry_path) || !settings) + return false; + + /* Get entry 'real' path */ + strlcpy(entry_real_path, entry_path, sizeof(entry_real_path)); + path_resolve_realpath(entry_real_path, sizeof(entry_real_path)); + + if (string_is_empty(entry_real_path)) + return false; + + /* First pass comparison */ +#ifdef _WIN32 + /* Handle case-insensitive operating systems*/ + if (string_is_equal_noncase(real_path, entry_real_path)) + return true; +#else + if (string_is_equal(real_path, entry_real_path)) + return true; +#endif + + /* If fuzzy matching is disabled, we can give up now */ + if (!settings->bools.playlist_fuzzy_archive_match) + return false; + + /* If we reach this point, we have to work + * harder... + * Need to handle a rather awkward archive file + * case where: + * - playlist path contains a properly formatted + * [archive_path][delimiter][rom_file] + * - search path is just [archive_path] + * ...or vice versa. + * This pretty much always happens when a playlist + * is generated via scan content (which handles the + * archive paths correctly), but the user subsequently + * loads an archive file via the command line or some + * external launcher (where the [delimiter][rom_file] + * part is almost always omitted) */ + real_path_is_compressed = path_is_compressed_file(real_path); + entry_real_path_is_compressed = path_is_compressed_file(entry_real_path); + + if ((real_path_is_compressed && !entry_real_path_is_compressed) || + (!real_path_is_compressed && entry_real_path_is_compressed)) + { + const char *compressed_path_a = real_path_is_compressed ? real_path : entry_real_path; + const char *full_path = real_path_is_compressed ? entry_real_path : real_path; + const char *delim = path_get_archive_delim(full_path); + + if (delim) + { + char compressed_path_b[PATH_MAX_LENGTH] = {0}; + unsigned len = 1 + delim - full_path; + + strlcpy(compressed_path_b, full_path, + (len < PATH_MAX_LENGTH ? len : PATH_MAX_LENGTH) * sizeof(char)); + +#ifdef _WIN32 + /* Handle case-insensitive operating systems*/ + if (string_is_equal_noncase(compressed_path_a, compressed_path_b)) + return true; +#else + if (string_is_equal(compressed_path_a, compressed_path_b)) + return true; +#endif + } + } + + return false; +} + +/** + * playlist_core_path_equal: + * @real_core_path : 'Real' search path, generated by path_resolve_realpath() + * @entry_core_path : Existing playlist entry 'core path' value + * + * Returns 'true' if real_core_path matches entry_core_path + * (Taking into account relative paths, case insensitive + * filesystems) + **/ +static bool playlist_core_path_equal(const char *real_core_path, const char *entry_core_path) +{ + char entry_real_core_path[PATH_MAX_LENGTH]; + + entry_real_core_path[0] = '\0'; + + /* Sanity check */ + if (string_is_empty(real_core_path) || string_is_empty(entry_core_path)) + return false; + + /* Get entry 'real' core path */ + strlcpy(entry_real_core_path, entry_core_path, sizeof(entry_real_core_path)); + path_resolve_realpath(entry_real_core_path, sizeof(entry_real_core_path)); + + if (string_is_empty(entry_real_core_path)) + return false; + +#ifdef _WIN32 + /* Handle case-insensitive operating systems*/ + if (string_is_equal_noncase(real_core_path, entry_real_core_path)) + return true; +#else + if (string_is_equal(real_core_path, entry_real_core_path)) + return true; +#endif + + return false; +} + uint32_t playlist_get_size(playlist_t *playlist) { if (!playlist) @@ -168,13 +295,20 @@ void playlist_get_index_by_path(playlist_t *playlist, const struct playlist_entry **entry) { size_t i; + char real_search_path[PATH_MAX_LENGTH]; - if (!playlist || !entry) + real_search_path[0] = '\0'; + + if (!playlist || !entry || string_is_empty(search_path)) return; + /* Get 'real' search path */ + strlcpy(real_search_path, search_path, sizeof(real_search_path)); + path_resolve_realpath(real_search_path, sizeof(real_search_path)); + for (i = 0; i < playlist->size; i++) { - if (!string_is_equal(playlist->entries[i].path, search_path)) + if (!playlist_path_equal(real_search_path, playlist->entries[i].path)) continue; *entry = &playlist->entries[i]; @@ -188,11 +322,19 @@ bool playlist_entry_exists(playlist_t *playlist, const char *crc32) { size_t i; - if (!playlist) + char real_search_path[PATH_MAX_LENGTH]; + + real_search_path[0] = '\0'; + + if (!playlist || string_is_empty(path)) return false; + /* Get 'real' search path */ + strlcpy(real_search_path, path, sizeof(real_search_path)); + path_resolve_realpath(real_search_path, sizeof(real_search_path)); + for (i = 0; i < playlist->size; i++) - if (string_is_equal(playlist->entries[i].path, path)) + if (playlist_path_equal(real_search_path, playlist->entries[i].path)) return true; return false; @@ -401,41 +543,52 @@ bool playlist_push_runtime(playlist_t *playlist, unsigned last_played_hour, unsigned last_played_minute, unsigned last_played_second) { size_t i; - bool core_path_empty = string_is_empty(core_path); + char real_path[PATH_MAX_LENGTH]; + char real_core_path[PATH_MAX_LENGTH]; - if (core_path_empty) - { - RARCH_ERR("cannot push NULL or empty core name into the playlist.\n"); - return false; - } - - if (string_is_empty(path)) - path = NULL; + real_path[0] = '\0'; + real_core_path[0] = '\0'; if (!playlist) return false; + if (string_is_empty(core_path)) + { + RARCH_ERR("cannot push NULL or empty core path into the playlist.\n"); + return false; + } + + /* Get 'real' path */ + if (!string_is_empty(path)) + { + strlcpy(real_path, path, sizeof(real_path)); + path_resolve_realpath(real_path, sizeof(real_path)); + } + + /* Get 'real' core path */ + strlcpy(real_core_path, core_path, sizeof(real_core_path)); + path_resolve_realpath(real_core_path, sizeof(real_core_path)); + + if (string_is_empty(real_core_path)) + { + RARCH_ERR("cannot push NULL or empty core path into the playlist.\n"); + return false; + } + for (i = 0; i < playlist->size; i++) { struct playlist_entry tmp; const char *entry_path = playlist->entries[i].path; - bool equal_path = - (!path && !entry_path) || - (path && entry_path && -#ifdef _WIN32 - /*prevent duplicates on case-insensitive operating systems*/ - string_is_equal_noncase(path, entry_path) -#else - string_is_equal(path, entry_path) -#endif - ); + bool equal_path = + (string_is_empty(real_path) && string_is_empty(entry_path)) || + playlist_path_equal(real_path, entry_path); /* Core name can have changed while still being the same core. * Differentiate based on the core path only. */ if (!equal_path) continue; - if (!string_is_equal(playlist->entries[i].core_path, core_path)) + if (!playlist_core_path_equal(real_core_path, playlist->entries[i].core_path)) continue; /* If top entry, we don't want to push a new entry since @@ -469,10 +622,10 @@ bool playlist_push_runtime(playlist_t *playlist, playlist->entries[0].path = NULL; playlist->entries[0].core_path = NULL; - if (!string_is_empty(path)) - playlist->entries[0].path = strdup(path); - if (!string_is_empty(core_path)) - playlist->entries[0].core_path = strdup(core_path); + if (!string_is_empty(real_path)) + playlist->entries[0].path = strdup(real_path); + if (!string_is_empty(real_core_path)) + playlist->entries[0].core_path = strdup(real_core_path); playlist->entries[0].runtime_hours = runtime_hours; playlist->entries[0].runtime_minutes = runtime_minutes; @@ -503,53 +656,67 @@ bool playlist_push(playlist_t *playlist, const struct playlist_entry *entry) { size_t i; - bool core_path_empty = string_is_empty(entry->core_path); - bool core_name_empty = string_is_empty(entry->core_name); + char real_path[PATH_MAX_LENGTH]; + char real_core_path[PATH_MAX_LENGTH]; const char *core_name = entry->core_name; - const char *path = entry->path; + bool entry_updated = false; - if (core_path_empty || core_name_empty) + real_path[0] = '\0'; + real_core_path[0] = '\0'; + + if (!playlist || !entry) + return false; + + if (string_is_empty(entry->core_path)) { - if (core_name_empty && !core_path_empty) - { - static char base_path[255] = {0}; - fill_pathname_base_noext(base_path, entry->core_path, sizeof(base_path)); - core_name = base_path; - } + RARCH_ERR("cannot push NULL or empty core path into the playlist.\n"); + return false; + } - if (core_path_empty || core_name_empty) + /* Get 'real' path */ + if (!string_is_empty(entry->path)) + { + strlcpy(real_path, entry->path, sizeof(real_path)); + path_resolve_realpath(real_path, sizeof(real_path)); + } + + /* Get 'real' core path */ + strlcpy(real_core_path, entry->core_path, sizeof(real_core_path)); + path_resolve_realpath(real_core_path, sizeof(real_core_path)); + + if (string_is_empty(real_core_path)) + { + RARCH_ERR("cannot push NULL or empty core path into the playlist.\n"); + return false; + } + + if (string_is_empty(core_name)) + { + static char base_path[255] = {0}; + fill_pathname_base_noext(base_path, real_core_path, sizeof(base_path)); + core_name = base_path; + + if (string_is_empty(core_name)) { RARCH_ERR("cannot push NULL or empty core name into the playlist.\n"); return false; } } - if (string_is_empty(path)) - path = NULL; - - if (!playlist) - return false; - for (i = 0; i < playlist->size; i++) { struct playlist_entry tmp; const char *entry_path = playlist->entries[i].path; - bool equal_path = (!path && !entry_path) || - (path && entry_path && -#ifdef _WIN32 - /*prevent duplicates on case-insensitive operating systems*/ - string_is_equal_noncase(path, entry_path) -#else - string_is_equal(path, entry_path) -#endif - ); + bool equal_path = + (string_is_empty(real_path) && string_is_empty(entry_path)) || + playlist_path_equal(real_path, entry_path); /* Core name can have changed while still being the same core. * Differentiate based on the core path only. */ if (!equal_path) continue; - if (!string_is_equal(playlist->entries[i].core_path, entry->core_path)) + if (!playlist_core_path_equal(real_core_path, playlist->entries[i].core_path)) continue; if ( !string_is_empty(entry->subsystem_ident) @@ -589,7 +756,17 @@ bool playlist_push(playlist_t *playlist, for (j = 0; j < entry->subsystem_roms->size; j++) { - if (!string_is_equal(entry->subsystem_roms->elems[j].data, roms->elems[j].data)) + char real_rom_path[PATH_MAX_LENGTH]; + + real_rom_path[0] = '\0'; + + if (!string_is_empty(entry->subsystem_roms->elems[j].data)) + { + strlcpy(real_rom_path, entry->subsystem_roms->elems[j].data, sizeof(real_rom_path)); + path_resolve_realpath(real_rom_path, sizeof(real_rom_path)); + } + + if (!playlist_path_equal(real_rom_path, roms->elems[j].data)) { unequal = true; break; @@ -600,10 +777,35 @@ bool playlist_push(playlist_t *playlist, continue; } + /* If content was previously loaded via file browser + * or command line, certain entry values will be missing. + * If we are now loading the same content from a playlist, + * fill in any blanks */ + if ((playlist->entries[i].label == NULL) && !string_is_empty(entry->label)) + { + playlist->entries[i].label = strdup(entry->label); + entry_updated = true; + } + if ((playlist->entries[i].crc32 == NULL) && !string_is_empty(entry->crc32)) + { + playlist->entries[i].crc32 = strdup(entry->crc32); + entry_updated = true; + } + if ((playlist->entries[i].db_name == NULL) && !string_is_empty(entry->db_name)) + { + playlist->entries[i].db_name = strdup(entry->db_name); + entry_updated = true; + } + /* If top entry, we don't want to push a new entry since * the top and the entry to be pushed are the same. */ if (i == 0) + { + if (entry_updated) + goto success; + return false; + } /* Seen it before, bump to top. */ tmp = playlist->entries[i]; @@ -646,12 +848,12 @@ bool playlist_push(playlist_t *playlist, playlist->entries[0].last_played_hour = 0; playlist->entries[0].last_played_minute = 0; playlist->entries[0].last_played_second = 0; - if (!string_is_empty(entry->path)) - playlist->entries[0].path = strdup(entry->path); + if (!string_is_empty(real_path)) + playlist->entries[0].path = strdup(real_path); if (!string_is_empty(entry->label)) playlist->entries[0].label = strdup(entry->label); - if (!string_is_empty(entry->core_path)) - playlist->entries[0].core_path = strdup(entry->core_path); + if (!string_is_empty(real_core_path)) + playlist->entries[0].core_path = strdup(real_core_path); if (!string_is_empty(core_name)) playlist->entries[0].core_name = strdup(core_name); if (!string_is_empty(entry->db_name))