From 170397456c106ef07cea76cecf13548bd2611135 Mon Sep 17 00:00:00 2001
From: jdgleaver <james@leaver.myzen.co.uk>
Date: Fri, 29 Nov 2019 17:13:35 +0000
Subject: [PATCH] Add manual content scanner

---
 Makefile.common                            |   5 +-
 file_path_special.h                        |   1 +
 file_path_str.c                            |   3 +
 griffin/griffin.c                          |   6 +
 intl/msg_hash_lbl.h                        |  22 +
 intl/msg_hash_us.h                         |  96 +++
 libretro-common/include/string/stdstring.h |   4 +
 libretro-common/string/stdstring.c         |  13 +
 manual_content_scan.c                      | 875 +++++++++++++++++++++
 manual_content_scan.h                      | 201 +++++
 menu/cbs/menu_cbs_deferred_push.c          |  20 +
 menu/cbs/menu_cbs_get_value.c              |  73 ++
 menu/cbs/menu_cbs_left.c                   | 118 +++
 menu/cbs/menu_cbs_ok.c                     | 192 +++++
 menu/cbs/menu_cbs_right.c                  | 136 ++++
 menu/cbs/menu_cbs_start.c                  |  33 +
 menu/cbs/menu_cbs_sublabel.c               |  32 +
 menu/cbs/menu_cbs_title.c                  |  39 +-
 menu/drivers/materialui.c                  |  21 +-
 menu/drivers/ozone/ozone_texture.c         |   1 +
 menu/drivers/xmb.c                         |   1 +
 menu/menu_cbs.h                            |   6 +-
 menu/menu_displaylist.c                    | 176 ++++-
 menu/menu_displaylist.h                    |   3 +
 menu/menu_driver.h                         |   7 +
 menu/menu_setting.c                        |  75 +-
 menu/widgets/menu_filebrowser.c            |  10 +
 menu/widgets/menu_filebrowser.h            |   1 +
 msg_hash.h                                 |  32 +
 playlist.c                                 |   3 +-
 playlist.h                                 |   3 +-
 tasks/task_database.c                      |   5 +-
 tasks/task_manual_content_scan.c           | 354 +++++++++
 tasks/tasks_internal.h                     |   2 +
 34 files changed, 2551 insertions(+), 18 deletions(-)
 create mode 100644 manual_content_scan.c
 create mode 100644 manual_content_scan.h
 create mode 100644 tasks/task_manual_content_scan.c

diff --git a/Makefile.common b/Makefile.common
index 65eafce53f..f611336b3b 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -163,6 +163,7 @@ OBJ += frontend/frontend_driver.o \
        tasks/task_file_transfer.o \
        tasks/task_image.o \
        tasks/task_playlist_manager.o \
+       tasks/task_manual_content_scan.o \
        $(LIBRETRO_COMM_DIR)/encodings/encoding_utf.o \
        $(LIBRETRO_COMM_DIR)/encodings/encoding_crc32.o \
        $(LIBRETRO_COMM_DIR)/encodings/encoding_base64.o \
@@ -258,8 +259,8 @@ OBJ += \
        performance_counters.o \
        verbosity.o \
        midi/drivers/null_midi.o \
-       $(LIBRETRO_COMM_DIR)/playlists/label_sanitization.o
-
+       $(LIBRETRO_COMM_DIR)/playlists/label_sanitization.o \
+       manual_content_scan.o
 
 ifeq ($(HAVE_AUDIOMIXER), 1)
    DEFINES += -DHAVE_AUDIOMIXER
diff --git a/file_path_special.h b/file_path_special.h
index c9e531056d..a6b13d2671 100644
--- a/file_path_special.h
+++ b/file_path_special.h
@@ -79,6 +79,7 @@ enum file_path_enum
    FILE_PATH_LPL_EXTENSION,
    FILE_PATH_LPL_EXTENSION_NO_DOT,
    FILE_PATH_RDB_EXTENSION,
+   FILE_PATH_RDB_EXTENSION_NO_DOT,
    FILE_PATH_BSV_EXTENSION,
    FILE_PATH_AUTO_EXTENSION,
    FILE_PATH_ZIP_EXTENSION,
diff --git a/file_path_str.c b/file_path_str.c
index 8409f66356..97e7e16887 100644
--- a/file_path_str.c
+++ b/file_path_str.c
@@ -130,6 +130,9 @@ const char *file_path_str(enum file_path_enum enum_idx)
       case FILE_PATH_RDB_EXTENSION:
          str = ".rdb";
          break;
+      case FILE_PATH_RDB_EXTENSION_NO_DOT:
+         str = "rdb";
+         break;
       case FILE_PATH_ZIP_EXTENSION:
          str = ".zip";
          break;
diff --git a/griffin/griffin.c b/griffin/griffin.c
index 2ca9826e74..74308b86ff 100644
--- a/griffin/griffin.c
+++ b/griffin/griffin.c
@@ -1205,6 +1205,7 @@ DATA RUNLOOP
 #include "../tasks/task_image.c"
 #include "../tasks/task_file_transfer.c"
 #include "../tasks/task_playlist_manager.c"
+#include "../tasks/task_manual_content_scan.c"
 #ifdef HAVE_ZLIB
 #include "../tasks/task_decompress.c"
 #endif
@@ -1634,3 +1635,8 @@ SSL
 PLAYLIST NAME SANITIZATION
 ============================================================ */
 #include "../libretro-common/playlists/label_sanitization.c"
+
+/*============================================================
+MANUAL CONTENT SCAN
+============================================================ */
+#include "../manual_content_scan.c"
diff --git a/intl/msg_hash_lbl.h b/intl/msg_hash_lbl.h
index a4a6c06989..e608b0118f 100644
--- a/intl/msg_hash_lbl.h
+++ b/intl/msg_hash_lbl.h
@@ -2105,3 +2105,25 @@ MSG_HASH(MENU_ENUM_LABEL_DRIVER_SWITCH_ENABLE,
       "driver_switch_enable")
 MSG_HASH(MENU_ENUM_LABEL_AI_SERVICE_PAUSE,
       "ai_service_pause")
+MSG_HASH(MENU_ENUM_LABEL_DEFERRED_MANUAL_CONTENT_SCAN_LIST,
+      "deferred_manual_content_scan_list")
+MSG_HASH(MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_LIST,
+      "manual_content_scan_list")
+MSG_HASH(MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_DIR,
+      "manual_content_scan_dir")
+MSG_HASH(MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME,
+      "manual_content_scan_system_name")
+MSG_HASH(MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_SYSTEM_NAME,
+      "deferred_dropdown_box_list_manual_content_scan_system_name")
+MSG_HASH(MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM,
+      "manual_content_scan_system_name_custom")
+MSG_HASH(MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_CORE_NAME,
+      "manual_content_scan_core_name")
+MSG_HASH(MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_CORE_NAME,
+      "deferred_dropdown_box_list_manual_content_scan_core_name")
+MSG_HASH(MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_FILE_EXTS,
+      "manual_content_scan_file_exts")
+MSG_HASH(MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_OVERWRITE,
+      "manual_content_scan_overwrite")
+MSG_HASH(MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_START,
+      "manual_content_scan_start")
diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h
index 9b84829d65..c0543cf1e6 100644
--- a/intl/msg_hash_us.h
+++ b/intl/msg_hash_us.h
@@ -10040,3 +10040,99 @@ MSG_HASH(
     MENU_ENUM_SUBLABEL_AI_SERVICE_PAUSE,
     "Pauses core while screen is translated."
     )
+MSG_HASH(
+    MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_LIST,
+    "Manual Scan"
+    )
+MSG_HASH(
+    MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_LIST,
+    "Configurable scan based on content file names. Does not require content to match the database."
+    )
+MSG_HASH(
+    MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_DIR,
+    "Content Directory"
+    )
+MSG_HASH(
+    MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_DIR,
+    "Selects a directory to scan for content."
+    )
+MSG_HASH(
+    MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_SYSTEM_NAME,
+    "System Name"
+    )
+MSG_HASH(
+    MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME,
+    "Specify a 'system name' with which to associate scanned content. Used to name to the generated playlist file and to identify playlist thumbnails."
+    )
+MSG_HASH(
+    MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM,
+    "Custom System Name"
+    )
+MSG_HASH(
+    MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM,
+    "Manually specify a 'system name' for scanned content. Only used when 'System Name' is set to '<Custom>'."
+    )
+MSG_HASH(
+    MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_CORE_NAME,
+    "Core"
+    )
+MSG_HASH(
+    MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_CORE_NAME,
+    "Select a default core to use when launching scanned content."
+    )
+MSG_HASH(
+    MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_FILE_EXTS,
+    "File Extensions"
+    )
+MSG_HASH(
+    MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_FILE_EXTS,
+    "Space-delimited list of file types to include in the scan. If empty, includes all files - or if a core is specified, all files supported by the core."
+    )
+MSG_HASH(
+    MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_OVERWRITE,
+    "Overwrite Existing Playlist"
+    )
+MSG_HASH(
+    MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_OVERWRITE,
+    "When enabled, any existing playlist will be deleted before scanning content. When disabled, existing playlist entries are preserved and only content currently missing from the playlist will be added."
+    )
+MSG_HASH(
+    MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_START,
+    "Start Scan"
+    )
+MSG_HASH(
+    MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_START,
+    "Scan selected content."
+    )
+MSG_HASH(
+    MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_SYSTEM_NAME_USE_CONTENT_DIR,
+    "<Content Directory>"
+    )
+MSG_HASH(
+    MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_SYSTEM_NAME_USE_CUSTOM,
+    "<Custom>"
+    )
+MSG_HASH(
+    MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_CORE_NAME_DETECT,
+    "<Unspecified>"
+    )
+MSG_HASH(
+    MSG_MANUAL_CONTENT_SCAN_INVALID_CONFIG,
+    "Invalid manual scan configuration"
+    )
+MSG_HASH(
+    MSG_MANUAL_CONTENT_SCAN_INVALID_CONTENT,
+    "No valid content detected"
+    )
+MSG_HASH(
+    MSG_MANUAL_CONTENT_SCAN_START,
+    "Scanning content: "
+    )
+MSG_HASH(
+    MSG_MANUAL_CONTENT_SCAN_IN_PROGRESS,
+    "Scanning: "
+    )
+MSG_HASH(
+    MSG_MANUAL_CONTENT_SCAN_END,
+    "Scan complete: "
+    )
diff --git a/libretro-common/include/string/stdstring.h b/libretro-common/include/string/stdstring.h
index 1e5e0a3bd3..29e3428259 100644
--- a/libretro-common/include/string/stdstring.h
+++ b/libretro-common/include/string/stdstring.h
@@ -133,6 +133,10 @@ char* string_tokenize(char **str, const char *delim);
 /* Removes every instance of character 'c' from 'str' */
 void string_remove_all_chars(char *str, char c);
 
+/* Replaces every instance of character 'find' in 'str'
+ * with character 'replace' */
+void string_replace_all_chars(char *str, char find, char replace);
+
 /* Converts string to unsigned integer.
  * Returns 0 if string is invalid  */
 unsigned string_to_unsigned(const char *str);
diff --git a/libretro-common/string/stdstring.c b/libretro-common/string/stdstring.c
index 51258ea424..507aa9fafd 100644
--- a/libretro-common/string/stdstring.c
+++ b/libretro-common/string/stdstring.c
@@ -319,6 +319,19 @@ void string_remove_all_chars(char *str, char c)
    *write_ptr = '\0';
 }
 
+/* Replaces every instance of character 'find' in 'str'
+ * with character 'replace' */
+void string_replace_all_chars(char *str, char find, char replace)
+{
+   char *str_ptr = str;
+
+   if (string_is_empty(str))
+      return;
+
+   while((str_ptr = strchr(str_ptr, find)) != NULL)
+      *str_ptr++ = replace;
+}
+
 /* Converts string to unsigned integer.
  * Returns 0 if string is invalid  */
 unsigned string_to_unsigned(const char *str)
diff --git a/manual_content_scan.c b/manual_content_scan.c
new file mode 100644
index 0000000000..239897627a
--- /dev/null
+++ b/manual_content_scan.c
@@ -0,0 +1,875 @@
+/* Copyright  (C) 2010-2019 The RetroArch team
+ *
+ * ---------------------------------------------------------------------------------------
+ * The following license statement only applies to this file (manual_content_scan.c).
+ * ---------------------------------------------------------------------------------------
+ *
+ * Permission is hereby granted, free of charge,
+ * to any person obtaining a copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
+ * and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <file/file_path.h>
+#include <string/stdstring.h>
+#include <lists/dir_list.h>
+#include <retro_miscellaneous.h>
+
+#include "configuration.h"
+#include "msg_hash.h"
+#include "list_special.h"
+#include "core_info.h"
+#include "file_path_special.h"
+
+#include "manual_content_scan.h"
+
+/* Holds all configuration parameters associated
+ * with a manual content scan */
+typedef struct
+{
+   char content_dir[PATH_MAX_LENGTH];
+   char system_name_content_dir[PATH_MAX_LENGTH];
+   char system_name_database[PATH_MAX_LENGTH];
+   char system_name_custom[PATH_MAX_LENGTH];
+   char core_name[PATH_MAX_LENGTH];
+   char core_path[PATH_MAX_LENGTH];
+   char file_exts_core[PATH_MAX_LENGTH];
+   char file_exts_custom[PATH_MAX_LENGTH];
+   enum manual_content_scan_system_name_type system_name_type;
+   enum manual_content_scan_core_type core_type;
+   bool overwrite_playlist;
+} scan_settings_t;
+
+/* Static settings object
+ * > Provides easy access to settings parameters
+ *   when creating associated menu entries
+ * > We are handling this in almost exactly the same
+ *   way as the regular global 'static settings_t *configuration_settings;'
+ *   object in retroarch.c. This means it is not inherently thread safe,
+ *   but this should not be an issue (i.e. regular configuration_settings
+ *   are not thread safe, but we only access them when pushing a
+ *   task, not in the task thread itself, so all is well) */
+static scan_settings_t scan_settings = {
+   "",                                          /* content_dir */
+   "",                                          /* system_name_content_dir */
+   "",                                          /* system_name_database */
+   "",                                          /* system_name_custom */
+   "",                                          /* core_name */
+   "",                                          /* core_path */
+   "",                                          /* file_exts_core */
+   "",                                          /* file_exts_custom */
+   MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR, /* system_name_type */
+   MANUAL_CONTENT_SCAN_CORE_DETECT,             /* core_type */
+   false                                        /* overwrite_playlist */
+};
+
+/*****************/
+/* Configuration */
+/*****************/
+
+/* Pointer access */
+
+/* Returns a pointer to the internal
+ * 'system_name_custom' string */
+char *manual_content_scan_get_system_name_custom_ptr(void)
+{
+   return scan_settings.system_name_custom;
+}
+
+/* Returns size of the internal
+ * 'system_name_custom' string */
+size_t manual_content_scan_get_system_name_custom_size(void)
+{
+   return sizeof(scan_settings.system_name_custom);
+}
+
+/* Returns a pointer to the internal
+ * 'file_exts_custom' string */
+char *manual_content_scan_get_file_exts_custom_ptr(void)
+{
+   return scan_settings.file_exts_custom;
+}
+
+/* Returns size of the internal
+ * 'file_exts_custom' string */
+size_t manual_content_scan_get_file_exts_custom_size(void)
+{
+   return sizeof(scan_settings.file_exts_custom);
+}
+
+/* Returns a pointer to the internal
+ * 'overwrite_playlist' bool */
+bool *manual_content_scan_get_overwrite_playlist_ptr(void)
+{
+   return &scan_settings.overwrite_playlist;
+}
+
+/* Sanitisation */
+
+/* Sanitises file extensions list string:
+ * > Removes period (full stop) characters
+ * > Converts to lower case
+ * > Trims leading/trailing whitespace */
+static void manual_content_scan_scrub_file_exts(char *file_exts)
+{
+   if (string_is_empty(file_exts))
+      return;
+
+   string_remove_all_chars(file_exts, '.');
+   string_to_lower(file_exts);
+   string_trim_whitespace(file_exts);
+}
+
+/* Removes invalid characters from
+ * 'system_name_custom' string */
+void manual_content_scan_scrub_system_name_custom(void)
+{
+   char *scrub_char_pointer = NULL;
+
+   if (string_is_empty(scan_settings.system_name_custom))
+      return;
+
+   /* Scrub characters that are not cross-platform
+    * and/or violate the No-Intro filename standard:
+    * http://datomatic.no-intro.org/stuff/The%20Official%20No-Intro%20Convention%20(20071030).zip
+    * Replace these characters with underscores */
+   while((scrub_char_pointer = strpbrk(scan_settings.system_name_custom, "&*/:`\"<>?\\|")))
+      *scrub_char_pointer = '_';
+}
+
+/* Removes period (full stop) characters from
+ * 'file_exts_custom' string and converts to
+ * lower case */
+void manual_content_scan_scrub_file_exts_custom(void)
+{
+   manual_content_scan_scrub_file_exts(scan_settings.file_exts_custom);
+}
+
+/* Menu setters */
+
+/* Sets content directory for next manual scan
+ * operation.
+ * Returns true if content directory is valid. */
+bool manual_content_scan_set_menu_content_dir(const char *content_dir)
+{
+   const char *dir_name = NULL;
+   size_t len;
+
+   /* Sanity check */
+   if (string_is_empty(content_dir))
+      goto error;
+
+   if (!path_is_directory(content_dir))
+      goto error;
+
+   /* Copy directory path to settings struct */
+   strlcpy(
+         scan_settings.content_dir,
+         content_dir,
+         sizeof(scan_settings.content_dir));
+
+   /* Remove trailing slash, if required */
+   len = strlen(scan_settings.content_dir);
+   if (len > 0)
+   {
+      if (scan_settings.content_dir[len - 1] == path_default_slash_c())
+         scan_settings.content_dir[len - 1] = '\0';
+   }
+   else
+      goto error;
+
+   /* Handle case where path was a single slash... */
+   if (string_is_empty(scan_settings.content_dir))
+      goto error;
+
+   /* Get directory name (used as system name
+    * when scan_settings.system_name_type ==
+    * MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR) */
+   dir_name = path_basename(scan_settings.content_dir);
+
+   if (string_is_empty(dir_name))
+      goto error;
+
+   /* Copy directory name to settings struct */
+   strlcpy(
+         scan_settings.system_name_content_dir,
+         dir_name,
+         sizeof(scan_settings.system_name_content_dir));
+
+   return true;
+
+error:
+   /* Directory is invalid - reset internal
+    * content directory and associated 'directory'
+    * system name */
+   scan_settings.content_dir[0]             = '\0';
+   scan_settings.system_name_content_dir[0] = '\0';
+   return false;
+}
+
+/* Sets system name for the next manual scan
+ * operation.
+ * Returns true if system name is valid.
+ * NOTE:
+ * > Only sets 'system_name_type' and 'system_name_database'
+ * > 'system_name_content_dir' and 'system_name_custom' are
+ *   (by necessity) handled elsewhere
+ * > This may look fishy, but it's not - it's a menu-specific
+ *   function, and this is simply the cleanest way to handle
+ *   the setting... */
+bool manual_content_scan_set_menu_system_name(
+      enum manual_content_scan_system_name_type system_name_type,
+      const char *system_name)
+{
+   /* Sanity check */
+   if (system_name_type > MANUAL_CONTENT_SCAN_SYSTEM_NAME_DATABASE)
+      goto error;
+
+   /* Cache system name 'type' */
+   scan_settings.system_name_type = system_name_type;
+
+   /* Check if we are using a non-database name */
+   if ((scan_settings.system_name_type == MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR) ||
+       (scan_settings.system_name_type == MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM))
+      scan_settings.system_name_database[0] = '\0';
+   else
+   {
+      /* We are using a database name... */
+      if (string_is_empty(system_name))
+         goto error;
+
+      /* Copy database name to settings struct */
+      strlcpy(
+            scan_settings.system_name_database,
+            system_name,
+            sizeof(scan_settings.system_name_database));
+   }
+
+   return true;
+
+error:
+   /* Input parameters are invalid - reset internal
+    * 'system_name_type' and 'system_name_database' */
+   scan_settings.system_name_type        = MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR;
+   scan_settings.system_name_database[0] = '\0';
+   return false;
+}
+
+/* Sets core name for the next manual scan
+ * operation (+ core path and other associated
+ * parameters).
+ * Returns true if core name is valid. */
+bool manual_content_scan_set_menu_core_name(
+      enum manual_content_scan_core_type core_type,
+      const char *core_name)
+{
+   /* Sanity check */
+   if (core_type > MANUAL_CONTENT_SCAN_CORE_SET)
+      goto error;
+
+   /* Cache core 'type' */
+   scan_settings.core_type = core_type;
+
+   /* Check if we are using core autodetection */
+   if (scan_settings.core_type == MANUAL_CONTENT_SCAN_CORE_DETECT)
+   {
+      scan_settings.core_name[0]      = '\0';
+      scan_settings.core_path[0]      = '\0';
+      scan_settings.file_exts_core[0] = '\0';
+   }
+   else
+   {
+      core_info_list_t *core_info_list = NULL;
+      core_info_t *core_info           = NULL;
+      bool core_found                  = false;
+      size_t i;
+
+      /* We are using a manually set core... */
+      if (string_is_empty(core_name))
+         goto error;
+
+      /* Get core list */
+      core_info_get_list(&core_info_list);
+
+      if (!core_info_list)
+         goto error;
+
+      /* Search for the specified core name */
+      for (i = 0; i < core_info_list->count; i++)
+      {
+         core_info = NULL;
+         core_info = core_info_get(core_info_list, i);
+
+         if (core_info)
+         {
+            if (string_is_equal(core_info->display_name, core_name))
+            {
+               /* Core has been found */
+               core_found = true;
+
+               /* Copy core path to settings struct */
+               if (string_is_empty(core_info->path))
+                  goto error;
+
+               strlcpy(
+                     scan_settings.core_path,
+                     core_info->path,
+                     sizeof(scan_settings.core_path));
+
+               /* Copy core name to settings struct */
+               strlcpy(
+                     scan_settings.core_name,
+                     core_info->display_name,
+                     sizeof(scan_settings.core_name));
+
+               /* Copy supported extensions to settings
+                * struct, if required */
+               if (!string_is_empty(core_info->supported_extensions))
+               {
+                  strlcpy(
+                        scan_settings.file_exts_core,
+                        core_info->supported_extensions,
+                        sizeof(scan_settings.file_exts_core));
+
+                  /* Core info extensions are delimited by
+                   * vertical bars. For internal consistency,
+                   * replace them with spaces */
+                  string_replace_all_chars(scan_settings.file_exts_core, '|', ' ');
+
+                  /* Apply standard scrubbing/clean-up
+                   * (should not be required, but must handle the
+                   * case where a core info file is incorrectly
+                   * formatted) */
+                  manual_content_scan_scrub_file_exts(scan_settings.file_exts_core);
+               }
+               else
+                  scan_settings.file_exts_core[0] = '\0';
+
+               break;
+            }
+         }
+      }
+
+      /* Sanity check */
+      if (!core_found)
+         goto error;
+   }
+
+   return true;
+
+error:
+   /* Input parameters are invalid - reset internal
+    * core values */
+   scan_settings.core_type         = MANUAL_CONTENT_SCAN_CORE_DETECT;
+   scan_settings.core_name[0]      = '\0';
+   scan_settings.core_path[0]      = '\0';
+   scan_settings.file_exts_core[0] = '\0';
+   return false;
+}
+
+/* Menu getters */
+
+/* Fetches content directory for next manual scan
+ * operation.
+ * Returns true if content directory is valid. */
+bool manual_content_scan_get_menu_content_dir(const char **content_dir)
+{
+   if (!content_dir)
+      return false;
+
+   if (string_is_empty(scan_settings.content_dir))
+      return false;
+
+   *content_dir = scan_settings.content_dir;
+
+   return true;
+}
+
+/* Fetches system name for the next manual scan operation.
+ * Returns true if system name is valid.
+ * NOTE: This corresponds to the 'System Name' value
+ * displayed in menus - this is not identical to the
+ * actual system name used when generating the playlist */
+bool manual_content_scan_get_menu_system_name(const char **system_name)
+{
+   if (!system_name)
+      return false;
+
+   switch (scan_settings.system_name_type)
+   {
+      case MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR:
+         *system_name = msg_hash_to_str(
+               MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_SYSTEM_NAME_USE_CONTENT_DIR);
+         return true;
+      case MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM:
+         *system_name = msg_hash_to_str(
+               MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_SYSTEM_NAME_USE_CUSTOM);
+         return true;
+      case MANUAL_CONTENT_SCAN_SYSTEM_NAME_DATABASE:
+         if (string_is_empty(scan_settings.system_name_database))
+            return false;
+         else
+         {
+            *system_name = scan_settings.system_name_database;
+            return true;
+         }
+      default:
+         break;
+   }
+
+   return false;
+}
+
+/* Fetches core name for the next manual scan operation.
+ * Returns true if core name is valid. */
+bool manual_content_scan_get_menu_core_name(const char **core_name)
+{
+   if (!core_name)
+      return false;
+
+   switch (scan_settings.core_type)
+   {
+      case MANUAL_CONTENT_SCAN_CORE_DETECT:
+         *core_name = msg_hash_to_str(
+               MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_CORE_NAME_DETECT);
+         return true;
+      case MANUAL_CONTENT_SCAN_CORE_SET:
+         if (string_is_empty(scan_settings.core_name))
+            return false;
+         else
+         {
+            *core_name = scan_settings.core_name;
+            return true;
+         }
+      default:
+         break;
+   }
+
+   return false;
+}
+
+/* Menu utility functions */
+
+/* Creates a list of all possible 'system name' menu
+ * strings, for use in 'menu_displaylist' drop-down
+ * lists and 'menu_cbs_left/right'
+ * > Returns NULL in the event of failure
+ * > Returned string list must be free()'d */
+struct string_list *manual_content_scan_get_menu_system_name_list(void)
+{
+   settings_t *settings          = config_get_ptr();
+   struct string_list *name_list = string_list_new();
+   union string_list_elem_attr attr;
+
+   /* Sanity check */
+   if (!name_list)
+      goto error;
+
+   attr.i = 0;
+
+   /* Add 'use content directory' entry */
+   if (!string_list_append(name_list, msg_hash_to_str(
+         MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_SYSTEM_NAME_USE_CONTENT_DIR), attr))
+      goto error;
+
+   /* Add 'use custom' entry */
+   if (!string_list_append(name_list, msg_hash_to_str(
+         MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_SYSTEM_NAME_USE_CUSTOM), attr))
+      goto error;
+
+#ifdef HAVE_LIBRETRODB
+
+   /* If platform has database support, get names
+    * of all installed database files */
+   if (settings)
+   {
+      /* Note: dir_list_new_special() is well behaved - the
+       * returned string list will only include database
+       * files (i.e. don't have to check for directories,
+       * or verify file extensions) */
+      struct string_list *rdb_list = dir_list_new_special(
+            settings->paths.path_content_database,
+            DIR_LIST_DATABASES, NULL);
+
+      if (rdb_list && rdb_list->size)
+      {
+         unsigned i;
+
+         /* Ensure database list is in alphabetical order */
+         dir_list_sort(rdb_list, true);
+
+         /* Loop over database files */
+         for (i = 0; i < rdb_list->size; i++)
+         {
+            const char *rdb_path = rdb_list->elems[i].data;
+            const char *rdb_file = NULL;
+            char rdb_name[PATH_MAX_LENGTH];
+
+            rdb_name[0] = '\0';
+
+            /* Sanity check */
+            if (string_is_empty(rdb_path))
+               continue;
+
+            rdb_file = path_basename(rdb_path);
+
+            if (string_is_empty(rdb_file))
+               continue;
+
+            /* Remove file extension */
+            strlcpy(rdb_name, rdb_file, sizeof(rdb_name));
+            path_remove_extension(rdb_name);
+
+            if (string_is_empty(rdb_name))
+               continue;
+
+            /* Add database name to list */
+            if (!string_list_append(name_list, rdb_name, attr))
+               goto error;
+         }
+      }
+
+      /* Clean up */
+      string_list_free(rdb_list);
+   }
+
+#endif
+
+   return name_list;
+
+error:
+   if (name_list)
+      string_list_free(name_list);
+   return NULL;
+}
+
+/* Creates a list of all possible 'core name' menu
+ * strings, for use in 'menu_displaylist' drop-down
+ * lists and 'menu_cbs_left/right'
+ * > Returns NULL in the event of failure
+ * > Returned string list must be free()'d */
+struct string_list *manual_content_scan_get_menu_core_name_list(void)
+{
+   struct string_list *name_list    = string_list_new();
+   core_info_list_t *core_info_list = NULL;
+   union string_list_elem_attr attr;
+
+   /* Sanity check */
+   if (!name_list)
+      goto error;
+
+   attr.i = 0;
+
+   /* Add 'DETECT' entry */
+   if (!string_list_append(name_list, msg_hash_to_str(
+         MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_CORE_NAME_DETECT), attr))
+      goto error;
+
+   /* Get core list */
+   core_info_get_list(&core_info_list);
+
+   if (core_info_list)
+   {
+      core_info_t *core_info = NULL;
+      size_t i;
+
+      /* Sort cores alphabetically */
+      core_info_qsort(core_info_list, CORE_INFO_LIST_SORT_DISPLAY_NAME);
+
+      /* Loop through cores */
+      for (i = 0; i < core_info_list->count; i++)
+      {
+         core_info = NULL;
+         core_info = core_info_get(core_info_list, i);
+
+         if (core_info)
+         {
+            if (string_is_empty(core_info->display_name))
+               continue;
+
+            /* Add core name to list */
+            if (!string_list_append(name_list, core_info->display_name, attr))
+               goto error;
+         }
+      }
+   }
+
+   return name_list;
+
+error:
+   if (name_list)
+      string_list_free(name_list);
+   return NULL;
+}
+
+/****************/
+/* Task Helpers */
+/****************/
+
+/* Parses current manual content scan settings,
+ * and extracts all information required to configure
+ * a manual content scan task.
+ * Returns false if current settings are invalid. */
+bool manual_content_scan_get_task_config(manual_content_scan_task_config_t *task_config)
+{
+   settings_t *settings = config_get_ptr();
+
+   if (!task_config || !settings)
+      return false;
+
+   /* Ensure all 'task_config' strings are
+    * correctly initialised */
+   task_config->playlist_file[0] = '\0';
+   task_config->content_dir[0]   = '\0';
+   task_config->system_name[0]   = '\0';
+   task_config->database_name[0] = '\0';
+   task_config->core_name[0]     = '\0';
+   task_config->core_path[0]     = '\0';
+   task_config->file_exts[0]     = '\0';
+
+   /* Get content directory */
+   if (string_is_empty(scan_settings.content_dir))
+      return false;
+
+   if (!path_is_directory(scan_settings.content_dir))
+      return false;
+
+   strlcpy(
+         task_config->content_dir,
+         scan_settings.content_dir,
+         sizeof(task_config->content_dir));
+
+   /* Get system name */
+   switch (scan_settings.system_name_type)
+   {
+      case MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR:
+         if (string_is_empty(scan_settings.system_name_content_dir))
+            return false;
+
+         strlcpy(
+               task_config->system_name,
+               scan_settings.system_name_content_dir,
+               sizeof(task_config->system_name));
+
+         break;
+      case MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM:
+         if (string_is_empty(scan_settings.system_name_custom))
+            return false;
+
+         strlcpy(
+               task_config->system_name,
+               scan_settings.system_name_custom,
+               sizeof(task_config->system_name));
+
+         break;
+      case MANUAL_CONTENT_SCAN_SYSTEM_NAME_DATABASE:
+         if (string_is_empty(scan_settings.system_name_database))
+            return false;
+
+         strlcpy(
+               task_config->system_name,
+               scan_settings.system_name_database,
+               sizeof(task_config->system_name));
+
+         break;
+      default:
+         return false;
+   }
+
+   /* Now we have a valid system name, can generate
+    * a 'database' name... */
+   strlcpy(
+         task_config->database_name,
+         task_config->system_name,
+         sizeof(task_config->database_name));
+
+   strlcat(
+         task_config->database_name,
+         file_path_str(FILE_PATH_LPL_EXTENSION),
+         sizeof(task_config->database_name));
+
+   /* ...which can in turn be used to generate the
+    * playlist path */
+   if (string_is_empty(settings->paths.directory_playlist))
+      return false;
+
+   fill_pathname_join(
+         task_config->playlist_file,
+         settings->paths.directory_playlist,
+         task_config->database_name,
+         sizeof(task_config->playlist_file));
+
+   if (string_is_empty(task_config->playlist_file))
+      return false;
+
+   /* Get core name and path */
+   switch (scan_settings.core_type)
+   {
+      case MANUAL_CONTENT_SCAN_CORE_DETECT:
+         task_config->core_set = false;
+         break;
+      case MANUAL_CONTENT_SCAN_CORE_SET:
+         task_config->core_set = true;
+
+         if (string_is_empty(scan_settings.core_name))
+            return false;
+         if (string_is_empty(scan_settings.core_path))
+            return false;
+
+         strlcpy(
+               task_config->core_name,
+               scan_settings.core_name,
+               sizeof(task_config->core_name));
+
+         strlcpy(
+               task_config->core_path,
+               scan_settings.core_path,
+               sizeof(task_config->core_path));
+
+         break;
+      default:
+         return false;
+   }
+
+   /* Get file extensions list
+    * > Note that compressed files are included by
+    *   default, regardless of extension filter
+    *   (since these can always be handled by the
+    *   frontend) */
+   task_config->include_compressed_content = true;
+
+   if (!string_is_empty(scan_settings.file_exts_custom))
+   {
+      strlcpy(
+            task_config->file_exts,
+            scan_settings.file_exts_custom,
+            sizeof(task_config->file_exts));
+
+      /* User has explicitly specified which file
+       * types are allowed - have to exclude compressed
+       * content when calling dir_list_new() */
+      task_config->include_compressed_content = false;
+   }
+   else if (scan_settings.core_type == MANUAL_CONTENT_SCAN_CORE_SET)
+   {
+      if (!string_is_empty(scan_settings.file_exts_core))
+         strlcpy(
+               task_config->file_exts,
+               scan_settings.file_exts_core,
+               sizeof(task_config->file_exts));
+   }
+
+   /* Our extension lists are space delimited
+    * > dir_list_new() expects vertical bar
+    *   delimiters, so find and replace */
+   if (!string_is_empty(task_config->file_exts))
+      string_replace_all_chars(task_config->file_exts, ' ', '|');
+
+   /* Copy 'overwrite playlist' setting */
+   task_config->overwrite_playlist = scan_settings.overwrite_playlist;
+
+   return true;
+}
+
+/* Creates a list of all valid content in the specified
+ * content directory
+ * > Returns NULL in the event of failure
+ * > Returned string list must be free()'d */
+struct string_list *manual_content_scan_get_content_list(manual_content_scan_task_config_t *task_config)
+{
+   struct string_list *dir_list = NULL;
+   bool filter_exts;
+
+   /* Sanity check */
+   if (!task_config)
+      goto error;
+
+   if (string_is_empty(task_config->content_dir))
+      goto error;
+
+   /* Get directory listing
+    * > Exclude directories and hidden files
+    * > Scan recursively */
+   dir_list = dir_list_new(
+         task_config->content_dir,
+         string_is_empty(task_config->file_exts) ? NULL : task_config->file_exts,
+         false, /* include_dirs */
+         false, /* include_hidden */
+         task_config->include_compressed_content,
+         true   /* recursive */
+   );
+
+   /* Sanity check */
+   if (!dir_list)
+      goto error;
+
+   if (dir_list->size < 1)
+      goto error;
+
+   /* Ensure list is in alphabetical order
+    * > Not strictly required, but task status
+    *   messages will be unintuitive if we leave
+    *   the order 'random' */
+   dir_list_sort(dir_list, true);
+
+   return dir_list;
+
+error:
+   if (dir_list)
+      string_list_free(dir_list);
+   return NULL;
+}
+
+/* Adds specified content to playlist, if not already
+ * present */
+void manual_content_scan_add_content_to_playlist(
+      manual_content_scan_task_config_t *task_config,
+      playlist_t *playlist, const char *content_path)
+{
+   /* Sanity check */
+   if (!task_config || !playlist || string_is_empty(content_path))
+      return;
+
+   if (!path_is_valid(content_path))
+      return;
+
+   /* Check whether content is already included
+    * in playlist */
+   if (!playlist_entry_exists(playlist, content_path))
+   {
+      struct playlist_entry entry = {0};
+      char label[PATH_MAX_LENGTH];
+
+      label[0] = '\0';
+
+      /* Get entry label */
+      fill_short_pathname_representation(
+            label, content_path, sizeof(label));
+
+      if (string_is_empty(label))
+         return;
+
+      /* Configure playlist entry
+       * > The push function reads our entry as const,
+       *   so these casts are safe */
+      entry.path      = (char*)content_path;
+      entry.label     = label;
+      entry.core_path = (char*)"DETECT";
+      entry.core_name = (char*)"DETECT";
+      entry.crc32     = (char*)"00000000|crc";
+      entry.db_name   = task_config->database_name;
+
+      /* Add entry to playlist */
+      playlist_push(playlist, &entry);
+   }
+}
diff --git a/manual_content_scan.h b/manual_content_scan.h
new file mode 100644
index 0000000000..648879a707
--- /dev/null
+++ b/manual_content_scan.h
@@ -0,0 +1,201 @@
+/* Copyright  (C) 2010-2019 The RetroArch team
+ *
+ * ---------------------------------------------------------------------------------------
+ * The following license statement only applies to this file (manual_content_scan.c).
+ * ---------------------------------------------------------------------------------------
+ *
+ * Permission is hereby granted, free of charge,
+ * to any person obtaining a copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
+ * and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef __MANUAL_CONTENT_SCAN_H
+#define __MANUAL_CONTENT_SCAN_H
+
+#include <retro_common_api.h>
+#include <libretro.h>
+
+#include <boolean.h>
+
+#include <lists/string_list.h>
+
+#include "playlist.h"
+
+RETRO_BEGIN_DECLS
+
+/* Defines all possible system name types
+ * > Use content directory name
+ * > Use custom name
+ * > Use database name */
+enum manual_content_scan_system_name_type
+{
+   MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR = 0,
+   MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM,
+   MANUAL_CONTENT_SCAN_SYSTEM_NAME_DATABASE
+};
+
+/* Defines all possible core name types
+ * > Autodetect core (DETECT)
+ * > Use manually set core */
+enum manual_content_scan_core_type
+{
+   MANUAL_CONTENT_SCAN_CORE_DETECT = 0,
+   MANUAL_CONTENT_SCAN_CORE_SET
+};
+
+/* Holds all configuration parameters required
+ * for a manual content scan task */
+typedef struct
+{
+   char playlist_file[PATH_MAX_LENGTH];
+   char content_dir[PATH_MAX_LENGTH];
+   char system_name[PATH_MAX_LENGTH];
+   char database_name[PATH_MAX_LENGTH];
+   char core_name[PATH_MAX_LENGTH];
+   char core_path[PATH_MAX_LENGTH];
+   char file_exts[PATH_MAX_LENGTH];
+   bool core_set;
+   bool overwrite_playlist;
+   bool include_compressed_content;
+} manual_content_scan_task_config_t;
+
+/*****************/
+/* Configuration */
+/*****************/
+
+/* Pointer access
+ * > This is a little ugly, but it allows us to
+ *   make use of standard 'menu_settings' code
+ *   for several config parameters (rather than
+ *   implementing unnecessary custom menu entries) */
+
+/* Returns a pointer to the internal
+ * 'system_name_custom' string */
+char *manual_content_scan_get_system_name_custom_ptr(void);
+
+/* Returns size of the internal
+ * 'system_name_custom' string */
+size_t manual_content_scan_get_system_name_custom_size(void);
+
+/* Returns a pointer to the internal
+ * 'file_exts_custom' string */
+char *manual_content_scan_get_file_exts_custom_ptr(void);
+
+/* Returns size of the internal
+ * 'file_exts_custom' string */
+size_t manual_content_scan_get_file_exts_custom_size(void);
+
+/* Returns a pointer to the internal
+ * 'overwrite_playlist' bool */
+bool *manual_content_scan_get_overwrite_playlist_ptr(void);
+
+/* Sanitisation */
+
+/* Removes invalid characters from
+ * 'system_name_custom' string */
+void manual_content_scan_scrub_system_name_custom(void);
+
+/* Removes period (full stop) characters from
+ * 'file_exts_custom' string and converts to
+ * lower case */
+void manual_content_scan_scrub_file_exts_custom(void);
+
+/* Menu setters */
+
+/* Sets content directory for next manual scan
+ * operation.
+ * Returns true if content directory is valid. */
+bool manual_content_scan_set_menu_content_dir(const char *content_dir);
+
+/* Sets system name for the next manual scan
+ * operation.
+ * Returns true if system name is valid.
+ * NOTE:
+ * > Only sets 'system_name_type' and 'system_name_database'
+ * > 'system_name_content_dir' and 'system_name_custom' are
+ *   (by necessity) handled elsewhere
+ * > This may look fishy, but it's not - it's a menu-specific
+ *   function, and this is simply the cleanest way to handle
+ *   the setting... */
+bool manual_content_scan_set_menu_system_name(
+      enum manual_content_scan_system_name_type system_name_type,
+      const char *system_name);
+
+/* Sets core name for the next manual scan
+ * operation (+ core path and other associated
+ * parameters).
+ * Returns true if core name is valid. */
+bool manual_content_scan_set_menu_core_name(
+      enum manual_content_scan_core_type core_type,
+      const char *core_name);
+
+/* Menu getters */
+
+/* Fetches content directory for next manual scan
+ * operation.
+ * Returns true if content directory is valid. */
+bool manual_content_scan_get_menu_content_dir(const char **content_dir);
+
+/* Fetches system name for the next manual scan operation.
+ * Returns true if system name is valid.
+ * NOTE: This corresponds to the 'System Name' value
+ * displayed in menus - this is not identical to the
+ * actual system name used when generating the playlist */
+bool manual_content_scan_get_menu_system_name(const char **system_name);
+
+/* Fetches core name for the next manual scan operation.
+ * Returns true if core name is valid. */
+bool manual_content_scan_get_menu_core_name(const char **core_name);
+
+/* Menu utility functions */
+
+/* Creates a list of all possible 'system name' menu
+ * strings, for use in 'menu_displaylist' drop-down
+ * lists and 'menu_cbs_left/right'
+ * > Returns NULL in the event of failure
+ * > Returned string list must be free()'d */
+struct string_list *manual_content_scan_get_menu_system_name_list(void);
+
+/* Creates a list of all possible 'core name' menu
+ * strings, for use in 'menu_displaylist' drop-down
+ * lists and 'menu_cbs_left/right'
+ * > Returns NULL in the event of failure
+ * > Returned string list must be free()'d */
+struct string_list *manual_content_scan_get_menu_core_name_list(void);
+
+/****************/
+/* Task Helpers */
+/****************/
+
+/* Parses current manual content scan settings,
+ * and extracts all information required to configure
+ * a manual content scan task.
+ * Returns false if current settings are invalid. */
+bool manual_content_scan_get_task_config(manual_content_scan_task_config_t *task_config);
+
+/* Creates a list of all valid content in the specified
+ * content directory
+ * > Returns NULL in the event of failure
+ * > Returned string list must be free()'d */
+struct string_list *manual_content_scan_get_content_list(manual_content_scan_task_config_t *task_config);
+
+/* Adds specified content to playlist, if not already
+ * present */
+void manual_content_scan_add_content_to_playlist(
+      manual_content_scan_task_config_t *task_config,
+      playlist_t *playlist, const char *content_path);
+
+RETRO_END_DECLS
+
+#endif
diff --git a/menu/cbs/menu_cbs_deferred_push.c b/menu/cbs/menu_cbs_deferred_push.c
index da7fbb14d9..f9b7f977bf 100644
--- a/menu/cbs/menu_cbs_deferred_push.c
+++ b/menu/cbs/menu_cbs_deferred_push.c
@@ -217,6 +217,8 @@ generic_deferred_push(deferred_push_switch_gpu_profile,             DISPLAYLIST_
 generic_deferred_push(deferred_push_switch_backlight_control,       DISPLAYLIST_SWITCH_BACKLIGHT_CONTROL)
 #endif
 
+generic_deferred_push(deferred_push_manual_content_scan_list,       DISPLAYLIST_MANUAL_CONTENT_SCAN_LIST)
+
 static int deferred_push_cursor_manager_list_deferred(
       menu_displaylist_info_t *info)
 {
@@ -636,6 +638,8 @@ generic_deferred_push_clear_general(deferred_push_dropdown_box_list_playlist_def
 generic_deferred_push_clear_general(deferred_push_dropdown_box_list_playlist_label_display_mode, PUSH_DEFAULT, DISPLAYLIST_DROPDOWN_LIST_PLAYLIST_LABEL_DISPLAY_MODE)
 generic_deferred_push_clear_general(deferred_push_dropdown_box_list_playlist_right_thumbnail_mode, PUSH_DEFAULT, DISPLAYLIST_DROPDOWN_LIST_PLAYLIST_RIGHT_THUMBNAIL_MODE)
 generic_deferred_push_clear_general(deferred_push_dropdown_box_list_playlist_left_thumbnail_mode, PUSH_DEFAULT, DISPLAYLIST_DROPDOWN_LIST_PLAYLIST_LEFT_THUMBNAIL_MODE)
+generic_deferred_push_clear_general(deferred_push_dropdown_box_list_manual_content_scan_system_name, PUSH_DEFAULT, DISPLAYLIST_DROPDOWN_LIST_MANUAL_CONTENT_SCAN_SYSTEM_NAME)
+generic_deferred_push_clear_general(deferred_push_dropdown_box_list_manual_content_scan_core_name, PUSH_DEFAULT, DISPLAYLIST_DROPDOWN_LIST_MANUAL_CONTENT_SCAN_CORE_NAME)
 
 static int menu_cbs_init_bind_deferred_push_compare_label(
       menu_file_list_cbs_t *cbs,
@@ -691,6 +695,16 @@ static int menu_cbs_init_bind_deferred_push_compare_label(
       BIND_ACTION_DEFERRED_PUSH(cbs, deferred_push_dropdown_box_list_playlist_left_thumbnail_mode);
       return 0;
    }
+   else if (string_is_equal(label, msg_hash_to_str(MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_SYSTEM_NAME)))
+   {
+      BIND_ACTION_DEFERRED_PUSH(cbs, deferred_push_dropdown_box_list_manual_content_scan_system_name);
+      return 0;
+   }
+   else if (string_is_equal(label, msg_hash_to_str(MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_CORE_NAME)))
+   {
+      BIND_ACTION_DEFERRED_PUSH(cbs, deferred_push_dropdown_box_list_manual_content_scan_core_name);
+      return 0;
+   }
    else if (string_is_equal(label, msg_hash_to_str(MENU_ENUM_LABEL_DEFERRED_BROWSE_URL_LIST)))
    {
       BIND_ACTION_DEFERRED_PUSH(cbs, deferred_push_browse_url_list);
@@ -1439,6 +1453,9 @@ static int menu_cbs_init_bind_deferred_push_compare_label(
             case MENU_ENUM_LABEL_FAVORITES:
                BIND_ACTION_DEFERRED_PUSH(cbs, deferred_push_detect_core_list);
                break;
+            case MENU_ENUM_LABEL_DEFERRED_MANUAL_CONTENT_SCAN_LIST:
+               BIND_ACTION_DEFERRED_PUSH(cbs, deferred_push_manual_content_scan_list);
+               break;
             default:
                return -1;
          }
@@ -1655,6 +1672,9 @@ static int menu_cbs_init_bind_deferred_push_compare_label(
             case MENU_LABEL_FAVORITES:
                BIND_ACTION_DEFERRED_PUSH(cbs, deferred_push_detect_core_list);
                break;
+            case MENU_LABEL_DEFERRED_MANUAL_CONTENT_SCAN_LIST:
+               BIND_ACTION_DEFERRED_PUSH(cbs, deferred_push_manual_content_scan_list);
+               break;
             default:
                return -1;
          }
diff --git a/menu/cbs/menu_cbs_get_value.c b/menu/cbs/menu_cbs_get_value.c
index 934b44ba37..6239bff489 100644
--- a/menu/cbs/menu_cbs_get_value.c
+++ b/menu/cbs/menu_cbs_get_value.c
@@ -43,6 +43,7 @@
 #include "../../verbosity.h"
 #include "../../wifi/wifi_driver.h"
 #include "../../playlist.h"
+#include "../../manual_content_scan.h"
 
 #ifdef HAVE_NETWORKING
 #include "../../network/netplay/netplay.h"
@@ -1292,6 +1293,66 @@ static void menu_action_setting_disp_set_label_achievement_information(
    strlcpy(s2, path, len2);
 }
 
+static void menu_action_setting_disp_set_label_manual_content_scan_dir(file_list_t* list,
+      unsigned *w, unsigned type, unsigned i,
+      const char *label,
+      char *s, size_t len,
+      const char *path,
+      char *s2, size_t len2)
+{
+   const char *content_dir = NULL;
+
+   *s = '\0';
+   *w = 19;
+
+   strlcpy(s2, path, len2);
+
+   if (!manual_content_scan_get_menu_content_dir(&content_dir))
+      return;
+
+   strlcpy(s, content_dir, len);
+}
+
+static void menu_action_setting_disp_set_label_manual_content_scan_system_name(file_list_t* list,
+      unsigned *w, unsigned type, unsigned i,
+      const char *label,
+      char *s, size_t len,
+      const char *path,
+      char *s2, size_t len2)
+{
+   const char *system_name = NULL;
+
+   *s = '\0';
+   *w = 19;
+
+   strlcpy(s2, path, len2);
+
+   if (!manual_content_scan_get_menu_system_name(&system_name))
+      return;
+
+   strlcpy(s, system_name, len);
+}
+
+static void menu_action_setting_disp_set_label_manual_content_scan_core_name(file_list_t* list,
+      unsigned *w, unsigned type, unsigned i,
+      const char *label,
+      char *s, size_t len,
+      const char *path,
+      char *s2, size_t len2)
+{
+   const char *core_name = NULL;
+
+   *s = '\0';
+   *w = 19;
+
+   strlcpy(s2, path, len2);
+
+   if (!manual_content_scan_get_menu_core_name(&core_name))
+      return;
+
+   strlcpy(s, core_name, len);
+}
+
 static void menu_action_setting_disp_set_label_no_items(
       file_list_t* list,
       unsigned *w, unsigned type, unsigned i,
@@ -1502,6 +1563,18 @@ static int menu_cbs_init_bind_get_string_representation_compare_label(
             BIND_ACTION_GET_VALUE(cbs,
                   menu_action_setting_disp_set_label_playlist_left_thumbnail_mode);
             break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_DIR:
+            BIND_ACTION_GET_VALUE(cbs,
+                  menu_action_setting_disp_set_label_manual_content_scan_dir);
+            break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME:
+            BIND_ACTION_GET_VALUE(cbs,
+                  menu_action_setting_disp_set_label_manual_content_scan_system_name);
+            break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_CORE_NAME:
+            BIND_ACTION_GET_VALUE(cbs,
+                  menu_action_setting_disp_set_label_manual_content_scan_core_name);
+            break;
          default:
             return - 1;
       }
diff --git a/menu/cbs/menu_cbs_left.c b/menu/cbs/menu_cbs_left.c
index f8451ff12b..a160a4d494 100644
--- a/menu/cbs/menu_cbs_left.c
+++ b/menu/cbs/menu_cbs_left.c
@@ -41,6 +41,7 @@
 #include "../../retroarch.h"
 #include "../../network/netplay/netplay.h"
 #include "../../playlist.h"
+#include "../../manual_content_scan.h"
 
 #ifndef BIND_ACTION_LEFT
 #define BIND_ACTION_LEFT(cbs, name) \
@@ -515,6 +516,116 @@ static int playlist_left_thumbnail_mode_left(unsigned type, const char *label,
    return 0;
 }
 
+static int manual_content_scan_system_name_left(unsigned type, const char *label,
+      bool wraparound)
+{
+   struct string_list *system_name_list                            =
+         manual_content_scan_get_menu_system_name_list();
+   const char *current_system_name                                 = NULL;
+   enum manual_content_scan_system_name_type next_system_name_type =
+         MANUAL_CONTENT_SCAN_SYSTEM_NAME_DATABASE;
+   const char *next_system_name                                    = NULL;
+   unsigned current_index                                          = 0;
+   unsigned next_index                                             = 0;
+   unsigned i;
+
+   if (!system_name_list)
+      return -1;
+
+   /* Get currently selected system name */
+   if (manual_content_scan_get_menu_system_name(&current_system_name))
+   {
+      /* Get index of currently selected system name */
+      for (i = 0; i < system_name_list->size; i++)
+      {
+         const char *system_name = system_name_list->elems[i].data;
+
+         if (string_is_equal(current_system_name, system_name))
+         {
+            current_index = i;
+            break;
+         }
+      }
+
+      /* Decrement index */
+      if (current_index > 0)
+         next_index = current_index - 1;
+      else if (wraparound && (system_name_list->size > 1))
+         next_index = system_name_list->size - 1;
+   }
+
+   /* Get new system name parameters */
+   if (next_index == (unsigned)MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR)
+      next_system_name_type = MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR;
+   else if (next_index == (unsigned)MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM)
+      next_system_name_type = MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM;
+
+   next_system_name = system_name_list->elems[next_index].data;
+
+   /* Set system name */
+   manual_content_scan_set_menu_system_name(
+         next_system_name_type, next_system_name);
+
+   /* Clean up */
+   string_list_free(system_name_list);
+
+   return 0;
+}
+
+static int manual_content_scan_core_name_left(unsigned type, const char *label,
+      bool wraparound)
+{
+   struct string_list *core_name_list                =
+         manual_content_scan_get_menu_core_name_list();
+   const char *current_core_name                     = NULL;
+   enum manual_content_scan_core_type next_core_type =
+         MANUAL_CONTENT_SCAN_CORE_SET;
+   const char *next_core_name                        = NULL;
+   unsigned current_index                            = 0;
+   unsigned next_index                               = 0;
+   unsigned i;
+
+   if (!core_name_list)
+      return -1;
+
+   /* Get currently selected core name */
+   if (manual_content_scan_get_menu_core_name(&current_core_name))
+   {
+      /* Get index of currently selected core name */
+      for (i = 0; i < core_name_list->size; i++)
+      {
+         const char *core_name = core_name_list->elems[i].data;
+
+         if (string_is_equal(current_core_name, core_name))
+         {
+            current_index = i;
+            break;
+         }
+      }
+
+      /* Decrement index */
+      if (current_index > 0)
+         next_index = current_index - 1;
+      else if (wraparound && (core_name_list->size > 1))
+         next_index = core_name_list->size - 1;
+   }
+
+   /* Get new core name parameters */
+   if (next_index == (unsigned)MANUAL_CONTENT_SCAN_CORE_DETECT)
+      next_core_type = MANUAL_CONTENT_SCAN_CORE_DETECT;
+
+   next_core_name = core_name_list->elems[next_index].data;
+
+   /* Set core name */
+   manual_content_scan_set_menu_core_name(
+         next_core_type, next_core_name);
+
+   /* Clean up */
+   string_list_free(core_name_list);
+
+   return 0;
+}
+
 static int core_setting_left(unsigned type, const char *label,
       bool wraparound)
 {
@@ -766,6 +877,12 @@ static int menu_cbs_init_bind_left_compare_label(menu_file_list_cbs_t *cbs,
             case MENU_ENUM_LABEL_PLAYLIST_MANAGER_LEFT_THUMBNAIL_MODE:
                BIND_ACTION_LEFT(cbs, playlist_left_thumbnail_mode_left);
                break;
+            case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME:
+               BIND_ACTION_LEFT(cbs, manual_content_scan_system_name_left);
+               break;
+            case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_CORE_NAME:
+               BIND_ACTION_LEFT(cbs, manual_content_scan_core_name_left);
+               break;
             default:
                return -1;
          }
@@ -861,6 +978,7 @@ static int menu_cbs_init_bind_left_compare_type(menu_file_list_cbs_t *cbs,
          case FILE_TYPE_DOWNLOAD_THUMBNAIL_CONTENT:
          case FILE_TYPE_DOWNLOAD_URL:
          case FILE_TYPE_SCAN_DIRECTORY:
+         case FILE_TYPE_MANUAL_SCAN_DIRECTORY:
          case FILE_TYPE_FONT:
          case MENU_SETTING_GROUP:
          case MENU_SETTINGS_CORE_INFO_NONE:
diff --git a/menu/cbs/menu_cbs_ok.c b/menu/cbs/menu_cbs_ok.c
index 233bf7ca56..43c0e327fa 100644
--- a/menu/cbs/menu_cbs_ok.c
+++ b/menu/cbs/menu_cbs_ok.c
@@ -75,6 +75,7 @@
 #include "../../lakka.h"
 #include "../../wifi/wifi_driver.h"
 #include "../../gfx/video_display_server.h"
+#include "../../manual_content_scan.h"
 
 #include <net/net_http.h>
 
@@ -182,6 +183,10 @@ static enum msg_hash_enums action_ok_dl_to_enum(unsigned lbl)
          return MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_PLAYLIST_RIGHT_THUMBNAIL_MODE;
       case ACTION_OK_DL_DROPDOWN_BOX_LIST_PLAYLIST_LEFT_THUMBNAIL_MODE:
          return MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_PLAYLIST_LEFT_THUMBNAIL_MODE;
+      case ACTION_OK_DL_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_SYSTEM_NAME:
+         return MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_SYSTEM_NAME;
+      case ACTION_OK_DL_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_CORE_NAME:
+         return MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_CORE_NAME;
       case ACTION_OK_DL_MIXER_STREAM_SETTINGS_LIST:
          return MENU_ENUM_LABEL_DEFERRED_MIXER_STREAM_SETTINGS_LIST;
       case ACTION_OK_DL_ACCOUNTS_LIST:
@@ -306,6 +311,8 @@ static enum msg_hash_enums action_ok_dl_to_enum(unsigned lbl)
          return MENU_ENUM_LABEL_DEFERRED_VIDEO_SHADER_PRESET_SAVE_LIST;
       case ACTION_OK_DL_SHADER_PRESET_REMOVE:
          return MENU_ENUM_LABEL_DEFERRED_VIDEO_SHADER_PRESET_REMOVE_LIST;
+      case ACTION_OK_DL_MANUAL_CONTENT_SCAN_LIST:
+         return MENU_ENUM_LABEL_DEFERRED_MANUAL_CONTENT_SCAN_LIST;
       default:
          break;
    }
@@ -440,6 +447,24 @@ int generic_action_ok_displaylist_push(const char *path,
          info.enum_idx      = MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_PLAYLIST_LEFT_THUMBNAIL_MODE;
          dl_type            = DISPLAYLIST_GENERIC;
          break;
+      case ACTION_OK_DL_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_SYSTEM_NAME:
+         info.type          = type;
+         info.directory_ptr = idx;
+         info_path          = path;
+         info_label         = msg_hash_to_str(
+               MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_SYSTEM_NAME);
+         info.enum_idx      = MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_SYSTEM_NAME;
+         dl_type            = DISPLAYLIST_GENERIC;
+         break;
+      case ACTION_OK_DL_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_CORE_NAME:
+         info.type          = type;
+         info.directory_ptr = idx;
+         info_path          = path;
+         info_label         = msg_hash_to_str(
+               MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_CORE_NAME);
+         info.enum_idx      = MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_CORE_NAME;
+         dl_type            = DISPLAYLIST_GENERIC;
+         break;
       case ACTION_OK_DL_USER_BINDS_LIST:
          info.type          = type;
          info.directory_ptr = idx;
@@ -600,6 +625,14 @@ int generic_action_ok_displaylist_push(const char *path,
          info_label         = label;
          dl_type            = DISPLAYLIST_FILE_BROWSER_SCAN_DIR;
          break;
+      case ACTION_OK_DL_MANUAL_SCAN_DIR_LIST:
+         filebrowser_set_type(FILEBROWSER_MANUAL_SCAN_DIR);
+         info.type          = FILE_TYPE_DIRECTORY;
+         info.directory_ptr = idx;
+         info_path          = new_path;
+         info_label         = label;
+         dl_type            = DISPLAYLIST_FILE_BROWSER_SELECT_DIR;
+         break;
       case ACTION_OK_DL_REMAP_FILE:
          filebrowser_clear_type();
          info.type          = type;
@@ -1019,6 +1052,7 @@ int generic_action_ok_displaylist_push(const char *path,
       case ACTION_OK_DL_SHADER_PRESET_REMOVE:
       case ACTION_OK_DL_SHADER_PRESET_SAVE:
       case ACTION_OK_DL_CDROM_INFO_LIST:
+      case ACTION_OK_DL_MANUAL_CONTENT_SCAN_LIST:
          action_ok_dl_lbl(action_ok_dl_to_enum(action_type), DISPLAYLIST_GENERIC);
          break;
       case ACTION_OK_DL_CDROM_INFO_DETAIL_LIST:
@@ -2989,6 +3023,47 @@ static int action_ok_path_scan_directory(const char *path,
 }
 #endif
 
+static int action_ok_path_manual_scan_directory(const char *path,
+      const char *label, unsigned type, size_t idx, size_t entry_idx)
+{
+   const char *flush_char = msg_hash_to_str(MENU_ENUM_LABEL_DEFERRED_MANUAL_CONTENT_SCAN_LIST);
+   unsigned flush_type    = 0;
+   const char *menu_path  = NULL;
+   char content_dir[PATH_MAX_LENGTH];
+
+   content_dir[0] = '\0';
+
+   /* 'Reset' file browser */
+   filebrowser_clear_type();
+
+   /* Get user-selected scan directory */
+   menu_entries_get_last_stack(&menu_path,
+         NULL, NULL, NULL, NULL);
+
+   if (!string_is_empty(menu_path))
+      strlcpy(content_dir, menu_path, sizeof(content_dir));
+
+#ifdef HAVE_COCOATOUCH
+   {
+      /* For iOS, set the path using realpath because the path name
+       * can start with /private and this ensures the path starts with it.
+       * This will allow the path to be properly substituted when
+       * fill_pathname_expand_special() is called. */
+      char real_content_dir[PATH_MAX_LENGTH] = {0};
+      realpath(content_dir, real_content_dir);
+      strlcpy(content_dir, real_content_dir, sizeof(content_dir));
+   }
+#endif
+
+   /* Update manual content scan settings */
+   manual_content_scan_set_menu_content_dir(content_dir);
+
+   /* Return to 'manual content scan' menu */
+   menu_entries_flush_stack(flush_char, flush_type);
+
+   return 0;
+}
+
 static int action_ok_core_deferred_set(const char *new_core_path,
       const char *content_label, unsigned type, size_t idx, size_t entry_idx)
 {
@@ -4671,6 +4746,7 @@ default_action_ok_func(action_ok_push_load_disc_list, ACTION_OK_DL_LOAD_DISC_LIS
 default_action_ok_func(action_ok_open_archive, ACTION_OK_DL_OPEN_ARCHIVE)
 default_action_ok_func(action_ok_rgui_menu_theme_preset, ACTION_OK_DL_RGUI_MENU_THEME_PRESET)
 default_action_ok_func(action_ok_pl_thumbnails_updater_list, ACTION_OK_DL_PL_THUMBNAILS_UPDATER_LIST)
+default_action_ok_func(action_ok_push_manual_content_scan_list, ACTION_OK_DL_MANUAL_CONTENT_SCAN_LIST)
 
 static int action_ok_open_uwp_permission_settings(const char *path,
    const char *label, unsigned type, size_t idx, size_t entry_idx)
@@ -5161,6 +5237,17 @@ int action_ok_push_filebrowser_list_file_select(const char *path,
          entry_idx, ACTION_OK_DL_FILE_BROWSER_SELECT_DIR);
 }
 
+int action_ok_push_manual_content_scan_dir_select(const char *path,
+      const char *label, unsigned type, size_t idx, size_t entry_idx)
+{
+   settings_t            *settings   = config_get_ptr();
+
+   filebrowser_clear_type();
+   return generic_action_ok_displaylist_push(path,
+         settings->paths.directory_menu_content, label, type, idx,
+         entry_idx, ACTION_OK_DL_MANUAL_SCAN_DIR_LIST);
+}
+
 /* TODO/FIXME */
 static int action_ok_push_dropdown_setting_core_options_item_special(const char *path,
       const char *label, unsigned type, size_t idx, size_t entry_idx)
@@ -5532,6 +5619,54 @@ static int action_ok_push_dropdown_item_playlist_left_thumbnail_mode(const char
    return action_cancel_pop_default(NULL, NULL, 0, 0);
 }
 
+static int action_ok_push_dropdown_item_manual_content_scan_system_name(const char *path,
+      const char *label, unsigned type, size_t idx, size_t entry_idx)
+{
+   const char* system_name                                    = path;
+   enum manual_content_scan_system_name_type system_name_type =
+         MANUAL_CONTENT_SCAN_SYSTEM_NAME_DATABASE;
+
+   (void)label;
+   (void)type;
+   (void)entry_idx;
+
+   /* Get system name type (i.e. check if setting is
+    * 'use content directory' or 'use custom') */
+   if (idx == (size_t)MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR)
+      system_name_type = MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR;
+   else if (idx == (size_t)MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM)
+      system_name_type = MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM;
+
+   /* Set system name */
+   manual_content_scan_set_menu_system_name(
+      system_name_type, system_name);
+
+   return action_cancel_pop_default(NULL, NULL, 0, 0);
+}
+
+static int action_ok_push_dropdown_item_manual_content_scan_core_name(const char *path,
+      const char *label, unsigned type, size_t idx, size_t entry_idx)
+{
+   const char* core_name                        = path;
+   enum manual_content_scan_core_type core_type =
+         MANUAL_CONTENT_SCAN_CORE_SET;
+
+   (void)label;
+   (void)type;
+   (void)entry_idx;
+
+   /* Get core type (i.e. check if setting is
+    * DETECT/Unspecified) */
+   if (idx == (size_t)MANUAL_CONTENT_SCAN_CORE_DETECT)
+      core_type = MANUAL_CONTENT_SCAN_CORE_DETECT;
+
+   /* Set core name */
+   manual_content_scan_set_menu_core_name(
+      core_type, core_name);
+
+   return action_cancel_pop_default(NULL, NULL, 0, 0);
+}
+
 static int action_ok_push_default(const char *path,
       const char *label, unsigned type, size_t idx, size_t entry_idx)
 {
@@ -5750,6 +5885,33 @@ static int action_ok_playlist_left_thumbnail_mode(const char *path,
    return 0;
 }
 
+static int action_ok_manual_content_scan_system_name(const char *path,
+      const char *label, unsigned type, size_t idx, size_t entry_idx)
+{
+   generic_action_ok_displaylist_push(
+         NULL,
+         NULL, NULL, 0, idx, 0,
+         ACTION_OK_DL_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_SYSTEM_NAME);
+   return 0;
+}
+
+static int action_ok_manual_content_scan_core_name(const char *path,
+      const char *label, unsigned type, size_t idx, size_t entry_idx)
+{
+   generic_action_ok_displaylist_push(
+         NULL,
+         NULL, NULL, 0, idx, 0,
+         ACTION_OK_DL_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_CORE_NAME);
+   return 0;
+}
+
+static int action_ok_manual_content_scan_start(const char *path,
+      const char *label, unsigned type, size_t idx, size_t entry_idx)
+{
+   task_push_manual_content_scan();
+   return 0;
+}
+
 static int action_ok_netplay_enable_host(const char *path,
       const char *label, unsigned type, size_t idx, size_t entry_idx)
 {
@@ -6698,6 +6860,21 @@ static int menu_cbs_init_bind_ok_compare_label(menu_file_list_cbs_t *cbs,
          case MENU_ENUM_LABEL_ACHIEVEMENT_RESUME:
             BIND_ACTION_OK(cbs, action_ok_cheevos_toggle_hardcore_mode);
             break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_LIST:
+            BIND_ACTION_OK(cbs, action_ok_push_manual_content_scan_list);
+            break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_DIR:
+            BIND_ACTION_OK(cbs, action_ok_push_manual_content_scan_dir_select);
+            break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME:
+            BIND_ACTION_OK(cbs, action_ok_manual_content_scan_system_name);
+            break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_CORE_NAME:
+            BIND_ACTION_OK(cbs, action_ok_manual_content_scan_core_name);
+            break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_START:
+            BIND_ACTION_OK(cbs, action_ok_manual_content_scan_start);
+            break;
          default:
             return -1;
       }
@@ -6826,6 +7003,12 @@ static int menu_cbs_init_bind_ok_compare_label(menu_file_list_cbs_t *cbs,
          case MENU_LABEL_PLAYLIST_MANAGER_LEFT_THUMBNAIL_MODE:
             BIND_ACTION_OK(cbs, action_ok_playlist_left_thumbnail_mode);
             break;
+         case MENU_LABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME:
+            BIND_ACTION_OK(cbs, action_ok_manual_content_scan_system_name);
+            break;
+         case MENU_LABEL_MANUAL_CONTENT_SCAN_CORE_NAME:
+            BIND_ACTION_OK(cbs, action_ok_manual_content_scan_core_name);
+            break;
          default:
             return -1;
       }
@@ -6958,6 +7141,12 @@ static int menu_cbs_init_bind_ok_compare_type(menu_file_list_cbs_t *cbs,
          case MENU_SETTING_DROPDOWN_ITEM_PLAYLIST_LEFT_THUMBNAIL_MODE:
             BIND_ACTION_OK(cbs, action_ok_push_dropdown_item_playlist_left_thumbnail_mode);
             break;
+         case MENU_SETTING_DROPDOWN_ITEM_MANUAL_CONTENT_SCAN_SYSTEM_NAME:
+            BIND_ACTION_OK(cbs, action_ok_push_dropdown_item_manual_content_scan_system_name);
+            break;
+         case MENU_SETTING_DROPDOWN_ITEM_MANUAL_CONTENT_SCAN_CORE_NAME:
+            BIND_ACTION_OK(cbs, action_ok_push_dropdown_item_manual_content_scan_core_name);
+            break;
          case MENU_SETTING_ACTION_CORE_DISK_OPTIONS:
             BIND_ACTION_OK(cbs, action_ok_push_default);
             break;
@@ -7043,6 +7232,9 @@ static int menu_cbs_init_bind_ok_compare_type(menu_file_list_cbs_t *cbs,
             BIND_ACTION_OK(cbs, action_ok_path_scan_directory);
             break;
 #endif
+         case FILE_TYPE_MANUAL_SCAN_DIRECTORY:
+            BIND_ACTION_OK(cbs, action_ok_path_manual_scan_directory);
+            break;
          case FILE_TYPE_CONFIG:
             BIND_ACTION_OK(cbs, action_ok_config_load);
             break;
diff --git a/menu/cbs/menu_cbs_right.c b/menu/cbs/menu_cbs_right.c
index 3354a343c7..d04f931291 100644
--- a/menu/cbs/menu_cbs_right.c
+++ b/menu/cbs/menu_cbs_right.c
@@ -42,6 +42,7 @@
 #include "../../ui/ui_companion_driver.h"
 #include "../../network/netplay/netplay.h"
 #include "../../playlist.h"
+#include "../../manual_content_scan.h"
 
 #ifndef BIND_ACTION_RIGHT
 #define BIND_ACTION_RIGHT(cbs, name) \
@@ -628,6 +629,134 @@ static int playlist_left_thumbnail_mode_right(unsigned type, const char *label,
    return 0;
 }
 
+static int manual_content_scan_system_name_right(unsigned type, const char *label,
+      bool wraparound)
+{
+   struct string_list *system_name_list                            =
+         manual_content_scan_get_menu_system_name_list();
+   const char *current_system_name                                 = NULL;
+   enum manual_content_scan_system_name_type next_system_name_type =
+         MANUAL_CONTENT_SCAN_SYSTEM_NAME_DATABASE;
+   const char *next_system_name                                    = NULL;
+   unsigned current_index                                          = 0;
+   unsigned next_index                                             = 0;
+   unsigned i;
+
+   if (!system_name_list)
+      return -1;
+
+   /* Get currently selected system name */
+   if (manual_content_scan_get_menu_system_name(&current_system_name))
+   {
+      /* Get index of currently selected system name */
+      for (i = 0; i < system_name_list->size; i++)
+      {
+         const char *system_name = system_name_list->elems[i].data;
+
+         if (string_is_equal(current_system_name, system_name))
+         {
+            current_index = i;
+            break;
+         }
+      }
+
+      /* Increment index */
+      next_index = current_index + 1;
+      if (next_index >= system_name_list->size)
+      {
+         if (wraparound)
+            next_index = 0;
+         else
+         {
+            if (system_name_list->size > 0)
+               next_index = system_name_list->size - 1;
+            else
+               next_index = 0;
+         }
+      }
+   }
+
+   /* Get new system name parameters */
+   if (next_index == (unsigned)MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR)
+      next_system_name_type = MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR;
+   else if (next_index == (unsigned)MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM)
+      next_system_name_type = MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM;
+
+   next_system_name = system_name_list->elems[next_index].data;
+
+   /* Set system name */
+   manual_content_scan_set_menu_system_name(
+         next_system_name_type, next_system_name);
+
+   /* Clean up */
+   string_list_free(system_name_list);
+
+   return 0;
+}
+
+static int manual_content_scan_core_name_right(unsigned type, const char *label,
+      bool wraparound)
+{
+   struct string_list *core_name_list                =
+         manual_content_scan_get_menu_core_name_list();
+   const char *current_core_name                     = NULL;
+   enum manual_content_scan_core_type next_core_type =
+         MANUAL_CONTENT_SCAN_CORE_SET;
+   const char *next_core_name                        = NULL;
+   unsigned current_index                            = 0;
+   unsigned next_index                               = 0;
+   unsigned i;
+
+   if (!core_name_list)
+      return -1;
+
+   /* Get currently selected core name */
+   if (manual_content_scan_get_menu_core_name(&current_core_name))
+   {
+      /* Get index of currently selected core name */
+      for (i = 0; i < core_name_list->size; i++)
+      {
+         const char *core_name = core_name_list->elems[i].data;
+
+         if (string_is_equal(current_core_name, core_name))
+         {
+            current_index = i;
+            break;
+         }
+      }
+
+      /* Increment index */
+      next_index = current_index + 1;
+      if (next_index >= core_name_list->size)
+      {
+         if (wraparound)
+            next_index = 0;
+         else
+         {
+            if (core_name_list->size > 0)
+               next_index = core_name_list->size - 1;
+            else
+               next_index = 0;
+         }
+      }
+   }
+
+   /* Get new core name parameters */
+   if (next_index == (unsigned)MANUAL_CONTENT_SCAN_CORE_DETECT)
+      next_core_type = MANUAL_CONTENT_SCAN_CORE_DETECT;
+
+   next_core_name = core_name_list->elems[next_index].data;
+
+   /* Set core name */
+   manual_content_scan_set_menu_core_name(
+         next_core_type, next_core_name);
+
+   /* Clean up */
+   string_list_free(core_name_list);
+
+   return 0;
+}
+
 int core_setting_right(unsigned type, const char *label,
       bool wraparound)
 {
@@ -735,6 +864,7 @@ static int menu_cbs_init_bind_right_compare_type(menu_file_list_cbs_t *cbs,
          case FILE_TYPE_DOWNLOAD_THUMBNAIL_CONTENT:
          case FILE_TYPE_DOWNLOAD_URL:
          case FILE_TYPE_SCAN_DIRECTORY:
+         case FILE_TYPE_MANUAL_SCAN_DIRECTORY:
          case FILE_TYPE_FONT:
          case MENU_SETTING_GROUP:
          case MENU_SETTINGS_CORE_INFO_NONE:
@@ -915,6 +1045,12 @@ static int menu_cbs_init_bind_right_compare_label(menu_file_list_cbs_t *cbs,
             case MENU_ENUM_LABEL_PLAYLIST_MANAGER_LEFT_THUMBNAIL_MODE:
                BIND_ACTION_RIGHT(cbs, playlist_left_thumbnail_mode_right);
                break;
+            case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME:
+               BIND_ACTION_RIGHT(cbs, manual_content_scan_system_name_right);
+               break;
+            case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_CORE_NAME:
+               BIND_ACTION_RIGHT(cbs, manual_content_scan_core_name_right);
+               break;
             default:
                return -1;
          }
diff --git a/menu/cbs/menu_cbs_start.c b/menu/cbs/menu_cbs_start.c
index 65bfb90e93..09ab6cdf07 100644
--- a/menu/cbs/menu_cbs_start.c
+++ b/menu/cbs/menu_cbs_start.c
@@ -38,6 +38,7 @@
 #include "../../retroarch.h"
 #include "../../performance_counters.h"
 #include "../../playlist.h"
+#include "../../manual_content_scan.h"
 
 #include "../../input/input_remapping.h"
 
@@ -302,6 +303,29 @@ static int action_start_playlist_left_thumbnail_mode(unsigned type, const char *
    return 0;
 }
 
+static int action_start_manual_content_scan_dir(unsigned type, const char *label)
+{
+   /* Reset content directory */
+   manual_content_scan_set_menu_content_dir("");
+   return 0;
+}
+
+static int action_start_manual_content_scan_system_name(unsigned type, const char *label)
+{
+   /* Reset system name */
+   manual_content_scan_set_menu_system_name(
+         MANUAL_CONTENT_SCAN_SYSTEM_NAME_CONTENT_DIR, "");
+   return 0;
+}
+
+static int action_start_manual_content_scan_core_name(unsigned type, const char *label)
+{
+   /* Reset core name */
+   manual_content_scan_set_menu_core_name(
+         MANUAL_CONTENT_SCAN_CORE_DETECT, "");
+   return 0;
+}
+
 static int action_start_video_resolution(unsigned type, const char *label)
 {
    unsigned width = 0, height = 0;
@@ -405,6 +429,15 @@ static int menu_cbs_init_bind_start_compare_label(menu_file_list_cbs_t *cbs)
          case MENU_ENUM_LABEL_PLAYLIST_MANAGER_LEFT_THUMBNAIL_MODE:
             BIND_ACTION_START(cbs, action_start_playlist_left_thumbnail_mode);
             break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_DIR:
+            BIND_ACTION_START(cbs, action_start_manual_content_scan_dir);
+            break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME:
+            BIND_ACTION_START(cbs, action_start_manual_content_scan_system_name);
+            break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_CORE_NAME:
+            BIND_ACTION_START(cbs, action_start_manual_content_scan_core_name);
+            break;
          default:
             return -1;
       }
diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c
index a16209b09c..24a2977756 100644
--- a/menu/cbs/menu_cbs_sublabel.c
+++ b/menu/cbs/menu_cbs_sublabel.c
@@ -714,6 +714,14 @@ default_sublabel_macro(action_bind_sublabel_thumbnails_updater_list,
 default_sublabel_macro(action_bind_sublabel_pl_thumbnails_updater_list,                    MENU_ENUM_SUBLABEL_PL_THUMBNAILS_UPDATER_LIST)
 default_sublabel_macro(action_bind_sublabel_help_send_debug_info,                          MENU_ENUM_SUBLABEL_HELP_SEND_DEBUG_INFO)
 default_sublabel_macro(action_bind_sublabel_rdb_entry_detail,                              MENU_ENUM_SUBLABEL_RDB_ENTRY_DETAIL)
+default_sublabel_macro(action_bind_sublabel_manual_content_scan_list,                      MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_LIST)
+default_sublabel_macro(action_bind_sublabel_manual_content_scan_dir,                       MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_DIR)
+default_sublabel_macro(action_bind_sublabel_manual_content_scan_system_name,               MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME)
+default_sublabel_macro(action_bind_sublabel_manual_content_scan_system_name_custom,        MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM)
+default_sublabel_macro(action_bind_sublabel_manual_content_scan_core_name,                 MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_CORE_NAME)
+default_sublabel_macro(action_bind_sublabel_manual_content_scan_file_exts,                 MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_FILE_EXTS)
+default_sublabel_macro(action_bind_sublabel_manual_content_scan_overwrite,                 MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_OVERWRITE)
+default_sublabel_macro(action_bind_sublabel_manual_content_scan_start,                     MENU_ENUM_SUBLABEL_MANUAL_CONTENT_SCAN_START)
 
 static int action_bind_sublabel_systeminfo_controller_entry(
       file_list_t *list,
@@ -3049,6 +3057,30 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs,
          case MENU_ENUM_LABEL_RDB_ENTRY_DETAIL:
             BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_rdb_entry_detail);
             break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_LIST:
+            BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_manual_content_scan_list);
+            break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_DIR:
+            BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_manual_content_scan_dir);
+            break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME:
+            BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_manual_content_scan_system_name);
+            break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM:
+            BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_manual_content_scan_system_name_custom);
+            break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_CORE_NAME:
+            BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_manual_content_scan_core_name);
+            break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_FILE_EXTS:
+            BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_manual_content_scan_file_exts);
+            break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_OVERWRITE:
+            BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_manual_content_scan_overwrite);
+            break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_START:
+            BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_manual_content_scan_start);
+            break;
          default:
          case MSG_UNKNOWN:
             return -1;
diff --git a/menu/cbs/menu_cbs_title.c b/menu/cbs/menu_cbs_title.c
index a3ba581b9f..b8af91998d 100644
--- a/menu/cbs/menu_cbs_title.c
+++ b/menu/cbs/menu_cbs_title.c
@@ -65,6 +65,8 @@ static int action_get_title_action_generic(const char *path, const char *label,
    const char *title = msg_hash_to_str(lbl); \
    if (!string_is_empty(path) && !string_is_empty(title)) \
       fill_pathname_join_delim(s, title, path, ' ', len); \
+   else if (!string_is_empty(title)) \
+      strlcpy(s, title, len); \
    return 1; \
 }
 
@@ -396,10 +398,12 @@ default_title_macro(action_get_title_goto_music,                MENU_ENUM_LABEL_
 default_title_macro(action_get_title_goto_video,                MENU_ENUM_LABEL_VALUE_GOTO_VIDEO)
 default_title_macro(action_get_title_collection,                MENU_ENUM_LABEL_VALUE_PLAYLISTS_TAB)
 default_title_macro(action_get_title_deferred_core_list,        MENU_ENUM_LABEL_VALUE_SUPPORTED_CORES)
-
 default_title_macro(action_get_title_dropdown_resolution_item,  MENU_ENUM_LABEL_VALUE_SCREEN_RESOLUTION)
 default_title_macro(action_get_title_dropdown_playlist_default_core_item, MENU_ENUM_LABEL_VALUE_PLAYLIST_MANAGER_DEFAULT_CORE)
 default_title_macro(action_get_title_dropdown_playlist_label_display_mode_item, MENU_ENUM_LABEL_VALUE_PLAYLIST_MANAGER_LABEL_DISPLAY_MODE)
+default_title_macro(action_get_title_manual_content_scan_list,  MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_LIST)
+default_title_macro(action_get_title_dropdown_manual_content_scan_system_name_item, MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_SYSTEM_NAME)
+default_title_macro(action_get_title_dropdown_manual_content_scan_core_name_item, MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_CORE_NAME)
 
 default_fill_title_macro(action_get_title_disk_image_append,    MENU_ENUM_LABEL_VALUE_DISK_IMAGE_APPEND)
 default_fill_title_macro(action_get_title_cheat_file_load,      MENU_ENUM_LABEL_VALUE_CHEAT_FILE)
@@ -444,6 +448,7 @@ default_fill_title_macro(action_get_title_extraction_directory,   MENU_ENUM_LABE
 default_fill_title_macro(action_get_title_menu,                   MENU_ENUM_LABEL_VALUE_MENU_SETTINGS)
 default_fill_title_macro(action_get_title_font_path,              MENU_ENUM_LABEL_VALUE_XMB_FONT)
 default_fill_title_macro(action_get_title_log_dir,                MENU_ENUM_LABEL_VALUE_LOG_DIR)
+default_fill_title_macro(action_get_title_manual_content_scan_dir, MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_DIR)
 
 default_title_copy_macro(action_get_title_help,                   MENU_ENUM_LABEL_VALUE_HELP_LIST)
 default_title_copy_macro(action_get_title_input_settings,         MENU_ENUM_LABEL_VALUE_INPUT_SETTINGS)
@@ -1226,6 +1231,12 @@ static int menu_cbs_init_bind_title_compare_label(menu_file_list_cbs_t *cbs,
             BIND_ACTION_GET_TITLE(cbs, action_get_title_switch_backlight_control);
             break;
 #endif
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_LIST:
+            BIND_ACTION_GET_TITLE(cbs, action_get_title_manual_content_scan_list);
+            break;
+         case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_DIR:
+            BIND_ACTION_GET_TITLE(cbs, action_get_title_manual_content_scan_dir);
+            break;
          default:
             return -1;
       }
@@ -1363,6 +1374,9 @@ static int menu_cbs_init_bind_title_compare_label(menu_file_list_cbs_t *cbs,
          case MENU_LABEL_CORE_ASSETS_DIRECTORY:
             BIND_ACTION_GET_TITLE(cbs, action_get_title_core_assets_directory);
             break;
+         case MENU_LABEL_THUMBNAILS_DIRECTORY:
+            BIND_ACTION_GET_TITLE(cbs, action_get_title_thumbnail_directory);
+            break;
          case MENU_LABEL_RGUI_CONFIG_DIRECTORY:
             BIND_ACTION_GET_TITLE(cbs, action_get_title_config_directory);
             break;
@@ -1543,6 +1557,12 @@ static int menu_cbs_init_bind_title_compare_label(menu_file_list_cbs_t *cbs,
             BIND_ACTION_GET_TITLE(cbs, action_get_title_switch_backlight_control);
             break;
 #endif
+         case MENU_LABEL_DEFERRED_MANUAL_CONTENT_SCAN_LIST:
+            BIND_ACTION_GET_TITLE(cbs, action_get_title_manual_content_scan_list);
+            break;
+         case MENU_LABEL_MANUAL_CONTENT_SCAN_DIR:
+            BIND_ACTION_GET_TITLE(cbs, action_get_title_manual_content_scan_dir);
+            break;
          default:
             return -1;
       }
@@ -1651,6 +1671,18 @@ int menu_cbs_init_bind_title(menu_file_list_cbs_t *cbs,
       BIND_ACTION_GET_TITLE(cbs, action_get_title_dropdown_playlist_left_thumbnail_mode_item);
       return 0;
    }
+   if (string_is_equal(label,
+            msg_hash_to_str(MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_SYSTEM_NAME)))
+   {
+      BIND_ACTION_GET_TITLE(cbs, action_get_title_dropdown_manual_content_scan_system_name_item);
+      return 0;
+   }
+   if (string_is_equal(label,
+            msg_hash_to_str(MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_CORE_NAME)))
+   {
+      BIND_ACTION_GET_TITLE(cbs, action_get_title_dropdown_manual_content_scan_core_name_item);
+      return 0;
+   }
    if (string_is_equal(label, msg_hash_to_str(MENU_ENUM_LABEL_DEFERRED_RPL_ENTRY_ACTIONS)))
    {
       BIND_ACTION_GET_TITLE(cbs, action_get_quick_menu_views_settings_list);
@@ -1676,6 +1708,11 @@ int menu_cbs_init_bind_title(menu_file_list_cbs_t *cbs,
       BIND_ACTION_GET_TITLE(cbs, action_get_title_collection);
       return 0;
    }
+   if (string_is_equal(label, msg_hash_to_str(MENU_ENUM_LABEL_DEFERRED_MANUAL_CONTENT_SCAN_LIST)))
+   {
+      BIND_ACTION_GET_TITLE(cbs, action_get_title_manual_content_scan_list);
+      return 0;
+   }
 
    return -1;
 }
diff --git a/menu/drivers/materialui.c b/menu/drivers/materialui.c
index 6313f1610f..852aba24cc 100644
--- a/menu/drivers/materialui.c
+++ b/menu/drivers/materialui.c
@@ -2552,7 +2552,6 @@ enum materialui_entry_value_type materialui_get_entry_value_type(
          switch (entry_file_type)
          {
             case FILE_TYPE_IN_CARCHIVE:
-            case FILE_TYPE_COMPRESSED:
             case FILE_TYPE_MORE:
             case FILE_TYPE_CORE:
             case FILE_TYPE_DIRECT_LOAD:
@@ -2564,6 +2563,15 @@ enum materialui_entry_value_type materialui_get_entry_value_type(
             case FILE_TYPE_IMAGE:
             case FILE_TYPE_MOVIE:
                break;
+            case FILE_TYPE_COMPRESSED:
+               /* Note that we have to perform a backup check here,
+                * since the 'manual content scan - file extensions'
+                * setting may have a value of 'zip' or '7z' etc, which
+                * means it would otherwise get incorreclty identified as
+                * an achive file... */
+               if (entry_type != FILE_TYPE_CARCHIVE)
+                  value_type = MUI_ENTRY_VALUE_TEXT;
+               break;
             default:
                value_type = MUI_ENTRY_VALUE_TEXT;
                break;
@@ -2693,7 +2701,13 @@ static void materialui_render_menu_entry_default(
       switch (entry_file_type)
       {
          case FILE_TYPE_COMPRESSED:
-            icon_texture = mui->textures.list[MUI_TEXTURE_ARCHIVE];
+            /* Note that we have to perform a backup check here,
+             * since the 'manual content scan - file extensions'
+             * setting may have a value of 'zip' or '7z' etc, which
+             * means it would otherwise get incorreclty identified as
+             * an achive file... */
+            if (entry_type == FILE_TYPE_CARCHIVE)
+               icon_texture = mui->textures.list[MUI_TEXTURE_ARCHIVE];
             break;
          case FILE_TYPE_IMAGE:
             icon_texture = mui->textures.list[MUI_TEXTURE_IMAGE];
@@ -7613,7 +7627,8 @@ static void materialui_list_insert(
                      node->has_icon           = true;
                   }
             else if (string_is_equal(label, msg_hash_to_str(MENU_ENUM_LABEL_SCAN_DIRECTORY)) ||
-                  string_is_equal(label, msg_hash_to_str(MENU_ENUM_LABEL_SCAN_FILE))
+                     string_is_equal(label, msg_hash_to_str(MENU_ENUM_LABEL_SCAN_FILE)) ||
+                     string_is_equal(label, msg_hash_to_str(MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_LIST))
                   )
             {
                node->icon_texture_index = MUI_TEXTURE_ADD;
diff --git a/menu/drivers/ozone/ozone_texture.c b/menu/drivers/ozone/ozone_texture.c
index 8f4dc74acd..6eac3a3cb8 100644
--- a/menu/drivers/ozone/ozone_texture.c
+++ b/menu/drivers/ozone/ozone_texture.c
@@ -250,6 +250,7 @@ menu_texture_item ozone_entries_icon_get_texture(ozone_handle_t *ozone,
             return ozone->icons_textures[OZONE_ENTRIES_ICONS_TEXTURE_USER];
       case MENU_ENUM_LABEL_DIRECTORY_SETTINGS:
       case MENU_ENUM_LABEL_SCAN_DIRECTORY:
+      case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_LIST:
       case MENU_ENUM_LABEL_REMAP_FILE_SAVE_CONTENT_DIR:
       case MENU_ENUM_LABEL_SAVE_CURRENT_CONFIG_OVERRIDE_CONTENT_DIR:
       case MENU_ENUM_LABEL_VIDEO_SHADER_PRESET_SAVE_PARENT:
diff --git a/menu/drivers/xmb.c b/menu/drivers/xmb.c
index c0c8465e15..772abbe3bc 100644
--- a/menu/drivers/xmb.c
+++ b/menu/drivers/xmb.c
@@ -2379,6 +2379,7 @@ static uintptr_t xmb_icon_get_id(xmb_handle_t *xmb,
          return xmb->textures.list[XMB_TEXTURE_RESUME];
       case MENU_ENUM_LABEL_DIRECTORY_SETTINGS:
       case MENU_ENUM_LABEL_SCAN_DIRECTORY:
+      case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_LIST:
       case MENU_ENUM_LABEL_REMAP_FILE_SAVE_CONTENT_DIR:
       case MENU_ENUM_LABEL_SAVE_CURRENT_CONFIG_OVERRIDE_CONTENT_DIR:
       case MENU_ENUM_LABEL_VIDEO_SHADER_PRESET_SAVE_PARENT:
diff --git a/menu/menu_cbs.h b/menu/menu_cbs.h
index 31ab3ef2f7..87edd85558 100644
--- a/menu/menu_cbs.h
+++ b/menu/menu_cbs.h
@@ -52,11 +52,14 @@ enum
    ACTION_OK_DL_DROPDOWN_BOX_LIST_PLAYLIST_LABEL_DISPLAY_MODE,
    ACTION_OK_DL_DROPDOWN_BOX_LIST_PLAYLIST_RIGHT_THUMBNAIL_MODE,
    ACTION_OK_DL_DROPDOWN_BOX_LIST_PLAYLIST_LEFT_THUMBNAIL_MODE,
+   ACTION_OK_DL_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_SYSTEM_NAME,
+   ACTION_OK_DL_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_CORE_NAME,
    ACTION_OK_DL_OPEN_ARCHIVE,
    ACTION_OK_DL_OPEN_ARCHIVE_DETECT_CORE,
    ACTION_OK_DL_MUSIC,
    ACTION_OK_DL_NETPLAY,
    ACTION_OK_DL_SCAN_DIR_LIST,
+   ACTION_OK_DL_MANUAL_SCAN_DIR_LIST,
    ACTION_OK_DL_HELP,
    ACTION_OK_DL_RPL_ENTRY,
    ACTION_OK_DL_RDB_ENTRY,
@@ -166,7 +169,8 @@ enum
    ACTION_OK_DL_BROWSE_URL_START,
    ACTION_OK_DL_CONTENT_SETTINGS,
    ACTION_OK_DL_CDROM_INFO_DETAIL_LIST,
-   ACTION_OK_DL_RGUI_MENU_THEME_PRESET
+   ACTION_OK_DL_RGUI_MENU_THEME_PRESET,
+   ACTION_OK_DL_MANUAL_CONTENT_SCAN_LIST
 };
 
 /* Function callbacks */
diff --git a/menu/menu_displaylist.c b/menu/menu_displaylist.c
index 2170f1ee41..c7eb571857 100644
--- a/menu/menu_displaylist.c
+++ b/menu/menu_displaylist.c
@@ -98,6 +98,7 @@
 #include "../tasks/tasks_internal.h"
 #include "../dynamic.h"
 #include "../runtime_file.h"
+#include "../manual_content_scan.h"
 
 static char new_path_entry[4096]        = {0};
 static char new_lbl_entry[4096]         = {0};
@@ -2334,9 +2335,9 @@ static unsigned menu_displaylist_parse_playlists(
 
    if (!horizontal)
    {
-#ifdef HAVE_LIBRETRODB
       if (settings->bools.menu_content_show_add)
       {
+#ifdef HAVE_LIBRETRODB
          if (menu_entries_append_enum(info->list,
                msg_hash_to_str(MENU_ENUM_LABEL_VALUE_SCAN_DIRECTORY),
                msg_hash_to_str(MENU_ENUM_LABEL_SCAN_DIRECTORY),
@@ -2349,8 +2350,15 @@ static unsigned menu_displaylist_parse_playlists(
                MENU_ENUM_LABEL_SCAN_FILE,
                MENU_SETTING_ACTION, 0, 0))
             count++;
-      }
 #endif
+         if (menu_entries_append_enum(info->list,
+               msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_LIST),
+               msg_hash_to_str(MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_LIST),
+               MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_LIST,
+               MENU_SETTING_ACTION, 0, 0))
+            count++;
+      }
+
      if (settings->bools.menu_content_show_favorites)
       if (menu_entries_append_enum(info->list,
             msg_hash_to_str(MENU_ENUM_LABEL_VALUE_GOTO_FAVORITES),
@@ -3667,6 +3675,64 @@ static unsigned populate_playlist_thumbnail_mode_dropdown_list(
    return count;
 }
 
+static bool menu_displaylist_parse_manual_content_scan_list(
+      menu_displaylist_info_t *info)
+{
+   unsigned count = 0;
+
+   /* Content directory */
+   if (menu_entries_append_enum(info->list,
+         msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_DIR),
+         msg_hash_to_str(MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_DIR),
+         MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_DIR,
+         MENU_SETTING_MANUAL_CONTENT_SCAN_DIR, 0, 0))
+      count++;
+
+   /* System name */
+   if (menu_entries_append_enum(info->list,
+         msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_SYSTEM_NAME),
+         msg_hash_to_str(MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME),
+         MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME,
+         MENU_SETTING_MANUAL_CONTENT_SCAN_SYSTEM_NAME, 0, 0))
+      count++;
+
+   /* Custom system name */
+   if (menu_displaylist_parse_settings_enum(info->list,
+         MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM, PARSE_ONLY_STRING,
+         false) == 0)
+      count++;
+
+   /* Core name */
+   if (menu_entries_append_enum(info->list,
+         msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_CORE_NAME),
+         msg_hash_to_str(MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_CORE_NAME),
+         MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_CORE_NAME,
+         MENU_SETTING_MANUAL_CONTENT_SCAN_CORE_NAME, 0, 0))
+      count++;
+
+   /* File extensions */
+   if (menu_displaylist_parse_settings_enum(info->list,
+         MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_FILE_EXTS, PARSE_ONLY_STRING,
+         false) == 0)
+      count++;
+
+   /* Overwrite playlist */
+   if (menu_displaylist_parse_settings_enum(info->list,
+         MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_OVERWRITE, PARSE_ONLY_BOOL,
+         false) == 0)
+      count++;
+
+   /* Start scan */
+   if (menu_entries_append_enum(info->list,
+         msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_START),
+         msg_hash_to_str(MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_START),
+         MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_START,
+         MENU_SETTING_ACTION_MANUAL_CONTENT_SCAN_START, 0, 0))
+      count++;
+
+   return (count > 0);
+}
+
 unsigned menu_displaylist_build_list(file_list_t *list, enum menu_displaylist_ctl_state type)
 {
    unsigned i;
@@ -3863,6 +3929,12 @@ unsigned menu_displaylist_build_list(file_list_t *list, enum menu_displaylist_ct
                MENU_SETTING_ACTION, 0, 0))
             count++;
 #endif
+         if (menu_entries_append_enum(list,
+               msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_LIST),
+               msg_hash_to_str(MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_LIST),
+               MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_LIST,
+               MENU_SETTING_ACTION, 0, 0))
+            count++;
          break;
       case DISPLAYLIST_NETWORK_INFO:
 #if defined(HAVE_NETWORKING) && !defined(HAVE_SOCKET_LEGACY) && (!defined(SWITCH) || defined(SWITCH) && defined(HAVE_LIBNX))
@@ -4125,6 +4197,84 @@ unsigned menu_displaylist_build_list(file_list_t *list, enum menu_displaylist_ct
       case DISPLAYLIST_DROPDOWN_LIST_PLAYLIST_LEFT_THUMBNAIL_MODE:
          count = populate_playlist_thumbnail_mode_dropdown_list(list, PLAYLIST_THUMBNAIL_LEFT);
          break;
+      case DISPLAYLIST_DROPDOWN_LIST_MANUAL_CONTENT_SCAN_SYSTEM_NAME:
+         {
+            /* Get system name list */
+            struct string_list *system_name_list = manual_content_scan_get_menu_system_name_list();
+
+            if (system_name_list)
+            {
+               const char *current_system_name = NULL;
+               unsigned i;
+
+               /* Get currently selected system name */
+               manual_content_scan_get_menu_system_name(&current_system_name);
+
+               /* Loop through names */
+               for (i = 0; i < system_name_list->size; i++)
+               {
+                  /* Note: manual_content_scan_get_system_name_list()
+                   * ensures that system_name cannot be empty here */
+                  const char *system_name = system_name_list->elems[i].data;
+
+                  /* Add menu entry */
+                  if (menu_entries_append_enum(list,
+                        system_name,
+                        "",
+                        MENU_ENUM_LABEL_NO_ITEMS,
+                        MENU_SETTING_DROPDOWN_ITEM_MANUAL_CONTENT_SCAN_SYSTEM_NAME,
+                        i, 0))
+                     count++;
+
+                  /* Check whether current entry is checked */
+                  if (string_is_equal(current_system_name, system_name))
+                     menu_entries_set_checked(list, i, true);
+               }
+
+               /* Clean up */
+               string_list_free(system_name_list);
+            }
+         }
+         break;
+      case DISPLAYLIST_DROPDOWN_LIST_MANUAL_CONTENT_SCAN_CORE_NAME:
+         {
+            /* Get core name list */
+            struct string_list *core_name_list = manual_content_scan_get_menu_core_name_list();
+
+            if (core_name_list)
+            {
+               const char *current_core_name = NULL;
+               unsigned i;
+
+               /* Get currently selected core name */
+               manual_content_scan_get_menu_core_name(&current_core_name);
+
+               /* Loop through names */
+               for (i = 0; i < core_name_list->size; i++)
+               {
+                  /* Note: manual_content_scan_get_core_name_list()
+                   * ensures that core_name cannot be empty here */
+                  const char *core_name = core_name_list->elems[i].data;
+
+                  /* Add menu entry */
+                  if (menu_entries_append_enum(list,
+                        core_name,
+                        "",
+                        MENU_ENUM_LABEL_NO_ITEMS,
+                        MENU_SETTING_DROPDOWN_ITEM_MANUAL_CONTENT_SCAN_CORE_NAME,
+                        i, 0))
+                     count++;
+
+                  /* Check whether current entry is checked */
+                  if (string_is_equal(current_core_name, core_name))
+                     menu_entries_set_checked(list, i, true);
+               }
+
+               /* Clean up */
+               string_list_free(core_name_list);
+            }
+         }
+         break;
       case DISPLAYLIST_PERFCOUNTERS_CORE:
       case DISPLAYLIST_PERFCOUNTERS_FRONTEND:
          {
@@ -7213,6 +7363,8 @@ bool menu_displaylist_ctl(enum menu_displaylist_ctl_state type,
       case DISPLAYLIST_DROPDOWN_LIST_PLAYLIST_LABEL_DISPLAY_MODE:
       case DISPLAYLIST_DROPDOWN_LIST_PLAYLIST_RIGHT_THUMBNAIL_MODE:
       case DISPLAYLIST_DROPDOWN_LIST_PLAYLIST_LEFT_THUMBNAIL_MODE:
+      case DISPLAYLIST_DROPDOWN_LIST_MANUAL_CONTENT_SCAN_SYSTEM_NAME:
+      case DISPLAYLIST_DROPDOWN_LIST_MANUAL_CONTENT_SCAN_CORE_NAME:
       case DISPLAYLIST_PERFCOUNTERS_CORE:
       case DISPLAYLIST_PERFCOUNTERS_FRONTEND:
       case DISPLAYLIST_MENU_SETTINGS_LIST:
@@ -7238,6 +7390,8 @@ bool menu_displaylist_ctl(enum menu_displaylist_ctl_state type,
                case DISPLAYLIST_DROPDOWN_LIST_PLAYLIST_LABEL_DISPLAY_MODE:
                case DISPLAYLIST_DROPDOWN_LIST_PLAYLIST_RIGHT_THUMBNAIL_MODE:
                case DISPLAYLIST_DROPDOWN_LIST_PLAYLIST_LEFT_THUMBNAIL_MODE:
+               case DISPLAYLIST_DROPDOWN_LIST_MANUAL_CONTENT_SCAN_SYSTEM_NAME:
+               case DISPLAYLIST_DROPDOWN_LIST_MANUAL_CONTENT_SCAN_CORE_NAME:
                   menu_entries_append_enum(info->list,
                         msg_hash_to_str(MENU_ENUM_LABEL_VALUE_NO_ENTRIES_TO_DISPLAY),
                         msg_hash_to_str(MENU_ENUM_LABEL_NO_ENTRIES_TO_DISPLAY),
@@ -7701,6 +7855,12 @@ bool menu_displaylist_ctl(enum menu_displaylist_ctl_state type,
                MENU_SETTING_ACTION, 0, 0))
             count++;
 #endif
+         if (menu_entries_append_enum(info->list,
+               msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_LIST),
+               msg_hash_to_str(MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_LIST),
+               MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_LIST,
+               MENU_SETTING_ACTION, 0, 0))
+            count++;
 
          if (count == 0)
             menu_entries_append_enum(info->list,
@@ -8967,6 +9127,18 @@ bool menu_displaylist_ctl(enum menu_displaylist_ctl_state type,
          menu_entries_ctl(MENU_ENTRIES_CTL_CLEAR, info->list);
          use_filebrowser = true;
          break;
+      case DISPLAYLIST_MANUAL_CONTENT_SCAN_LIST:
+         menu_entries_ctl(MENU_ENTRIES_CTL_CLEAR, info->list);
+
+         if (!menu_displaylist_parse_manual_content_scan_list(info))
+            menu_entries_append_enum(info->list,
+                  msg_hash_to_str(MENU_ENUM_LABEL_VALUE_NO_ENTRIES_TO_DISPLAY),
+                  msg_hash_to_str(MENU_ENUM_LABEL_NO_ENTRIES_TO_DISPLAY),
+                  MENU_ENUM_LABEL_NO_ENTRIES_TO_DISPLAY,
+                  FILE_TYPE_NONE, 0, 0);
+
+         info->need_push    = true;
+         break;
       case DISPLAYLIST_DROPDOWN_LIST:
          {
             menu_entries_ctl(MENU_ENTRIES_CTL_CLEAR, info->list);
diff --git a/menu/menu_displaylist.h b/menu/menu_displaylist.h
index b86f9d87cf..aced47b137 100644
--- a/menu/menu_displaylist.h
+++ b/menu/menu_displaylist.h
@@ -60,6 +60,8 @@ enum menu_displaylist_ctl_state
    DISPLAYLIST_DROPDOWN_LIST_PLAYLIST_LABEL_DISPLAY_MODE,
    DISPLAYLIST_DROPDOWN_LIST_PLAYLIST_RIGHT_THUMBNAIL_MODE,
    DISPLAYLIST_DROPDOWN_LIST_PLAYLIST_LEFT_THUMBNAIL_MODE,
+   DISPLAYLIST_DROPDOWN_LIST_MANUAL_CONTENT_SCAN_SYSTEM_NAME,
+   DISPLAYLIST_DROPDOWN_LIST_MANUAL_CONTENT_SCAN_CORE_NAME,
    DISPLAYLIST_CDROM_DETAIL_INFO,
    DISPLAYLIST_INFO,
    DISPLAYLIST_HELP,
@@ -212,6 +214,7 @@ enum menu_displaylist_ctl_state
 #if defined(HAVE_LAKKA_SWITCH) || defined(HAVE_LIBNX)
    DISPLAYLIST_SWITCH_CPU_PROFILE,
 #endif
+   DISPLAYLIST_MANUAL_CONTENT_SCAN_LIST,
    DISPLAYLIST_PENDING_CLEAR
 };
 
diff --git a/menu/menu_driver.h b/menu/menu_driver.h
index dfb1525160..57dbafc211 100644
--- a/menu/menu_driver.h
+++ b/menu/menu_driver.h
@@ -91,6 +91,8 @@ enum menu_settings_type
    MENU_SETTING_DROPDOWN_ITEM_PLAYLIST_LABEL_DISPLAY_MODE,
    MENU_SETTING_DROPDOWN_ITEM_PLAYLIST_RIGHT_THUMBNAIL_MODE,
    MENU_SETTING_DROPDOWN_ITEM_PLAYLIST_LEFT_THUMBNAIL_MODE,
+   MENU_SETTING_DROPDOWN_ITEM_MANUAL_CONTENT_SCAN_SYSTEM_NAME,
+   MENU_SETTING_DROPDOWN_ITEM_MANUAL_CONTENT_SCAN_CORE_NAME,
    MENU_SETTING_DROPDOWN_SETTING_CORE_OPTIONS_ITEM,
    MENU_SETTING_DROPDOWN_SETTING_STRING_OPTIONS_ITEM,
    MENU_SETTING_DROPDOWN_SETTING_FLOAT_ITEM,
@@ -206,6 +208,11 @@ enum menu_settings_type
    MENU_SET_CDROM_INFO,
    MENU_SETTING_ACTION_DELETE_PLAYLIST,
 
+   MENU_SETTING_MANUAL_CONTENT_SCAN_DIR,
+   MENU_SETTING_MANUAL_CONTENT_SCAN_SYSTEM_NAME,
+   MENU_SETTING_MANUAL_CONTENT_SCAN_CORE_NAME,
+   MENU_SETTING_ACTION_MANUAL_CONTENT_SCAN_START,
+
    MENU_SETTINGS_LAST
 };
 
diff --git a/menu/menu_setting.c b/menu/menu_setting.c
index 3fa6f3da7f..f82abea93a 100644
--- a/menu/menu_setting.c
+++ b/menu/menu_setting.c
@@ -95,6 +95,7 @@
 #include "../managers/cheat_manager.h"
 #include "../verbosity.h"
 #include "../playlist.h"
+#include "../manual_content_scan.h"
 
 #include "../tasks/tasks_internal.h"
 
@@ -157,7 +158,8 @@ enum settings_list_type
    SETTINGS_LIST_USER_ACCOUNTS_TWITCH,
    SETTINGS_LIST_DIRECTORY,
    SETTINGS_LIST_PRIVACY,
-   SETTINGS_LIST_MIDI
+   SETTINGS_LIST_MIDI,
+   SETTINGS_LIST_MANUAL_CONTENT_SCAN
 };
 
 struct bool_entry
@@ -6679,6 +6681,17 @@ void general_write_handler(rarch_setting_t *setting)
             }
          }
          break;
+      case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM:
+         /* Ensure that custom system name includes no
+          * invalid characters */
+         manual_content_scan_scrub_system_name_custom();
+         break;
+      case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_FILE_EXTS:
+         /* Ensure that custom file extension list includes
+          * no period (full stop) characters, and converts
+          * string to lower case */
+         manual_content_scan_scrub_file_exts_custom();
+         break;
       default:
          break;
    }
@@ -16430,6 +16443,63 @@ static bool setting_append_list(
          (*list)[list_info->index - 1].action_ok     = &setting_action_ok_uint;
          menu_settings_list_current_add_range(list, list_info, 0.0f, 100.0f, 1.0f, true, true);
 
+         END_SUB_GROUP(list, list_info, parent_group);
+         END_GROUP(list, list_info, parent_group);
+         break;
+      case SETTINGS_LIST_MANUAL_CONTENT_SCAN:
+         START_GROUP(list, list_info, &group_info,
+               msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_LIST), parent_group);
+
+         parent_group = msg_hash_to_str(MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_LIST);
+
+         START_SUB_GROUP(list, list_info, "State",
+               &group_info, &subgroup_info, parent_group);
+
+         CONFIG_STRING(
+               list, list_info,
+               manual_content_scan_get_system_name_custom_ptr(),
+               manual_content_scan_get_system_name_custom_size(),
+               MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM,
+               MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM,
+               "",
+               &group_info,
+               &subgroup_info,
+               parent_group,
+               general_write_handler,
+               general_read_handler);
+         SETTINGS_DATA_LIST_CURRENT_ADD_FLAGS(list, list_info, SD_FLAG_ALLOW_INPUT);
+         (*list)[list_info->index - 1].ui_type       = ST_UI_TYPE_STRING_LINE_EDIT;
+
+         CONFIG_STRING(
+               list, list_info,
+               manual_content_scan_get_file_exts_custom_ptr(),
+               manual_content_scan_get_file_exts_custom_size(),
+               MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_FILE_EXTS,
+               MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_FILE_EXTS,
+               "",
+               &group_info,
+               &subgroup_info,
+               parent_group,
+               general_write_handler,
+               general_read_handler);
+         SETTINGS_DATA_LIST_CURRENT_ADD_FLAGS(list, list_info, SD_FLAG_ALLOW_INPUT);
+         (*list)[list_info->index - 1].ui_type       = ST_UI_TYPE_STRING_LINE_EDIT;
+
+         CONFIG_BOOL(
+               list, list_info,
+               manual_content_scan_get_overwrite_playlist_ptr(),
+               MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_OVERWRITE,
+               MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_OVERWRITE,
+               false,
+               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);
          break;
@@ -16566,7 +16636,8 @@ static rarch_setting_t *menu_setting_new_internal(rarch_setting_info_t *list_inf
       SETTINGS_LIST_USER_ACCOUNTS_TWITCH,
       SETTINGS_LIST_DIRECTORY,
       SETTINGS_LIST_PRIVACY,
-      SETTINGS_LIST_MIDI
+      SETTINGS_LIST_MIDI,
+      SETTINGS_LIST_MANUAL_CONTENT_SCAN
    };
    const char *root                     = msg_hash_to_str(MENU_ENUM_LABEL_MAIN_MENU);
    rarch_setting_t *list                = (rarch_setting_t*)calloc(
diff --git a/menu/widgets/menu_filebrowser.c b/menu/widgets/menu_filebrowser.c
index c0620af240..1f165e4bd9 100644
--- a/menu/widgets/menu_filebrowser.c
+++ b/menu/widgets/menu_filebrowser.c
@@ -127,6 +127,14 @@ void filebrowser_parse(menu_displaylist_info_t *info, unsigned type_data)
                   FILE_TYPE_SCAN_DIRECTORY, 0 ,0);
 #endif
          break;
+      case FILEBROWSER_MANUAL_SCAN_DIR:
+         if (info)
+            menu_entries_prepend(info->list,
+                  msg_hash_to_str(MENU_ENUM_LABEL_VALUE_SCAN_THIS_DIRECTORY),
+                  msg_hash_to_str(MENU_ENUM_LABEL_SCAN_THIS_DIRECTORY),
+                  MENU_ENUM_LABEL_SCAN_THIS_DIRECTORY,
+                  FILE_TYPE_MANUAL_SCAN_DIRECTORY, 0 ,0);
+         break;
       case FILEBROWSER_SELECT_DIR:
          if (info)
             menu_entries_prepend(info->list,
@@ -212,6 +220,8 @@ void filebrowser_parse(menu_displaylist_info_t *info, unsigned type_data)
                continue;
             if (filebrowser_types == FILEBROWSER_SCAN_DIR)
                continue;
+            if (filebrowser_types == FILEBROWSER_MANUAL_SCAN_DIR)
+               continue;
          }
 
          /* Need to preserve slash first time. */
diff --git a/menu/widgets/menu_filebrowser.h b/menu/widgets/menu_filebrowser.h
index 86a5702fdf..43875a4164 100644
--- a/menu/widgets/menu_filebrowser.h
+++ b/menu/widgets/menu_filebrowser.h
@@ -32,6 +32,7 @@ enum filebrowser_enums
    FILEBROWSER_SELECT_DIR,
    FILEBROWSER_SCAN_DIR,
    FILEBROWSER_SCAN_FILE,
+   FILEBROWSER_MANUAL_SCAN_DIR,
    FILEBROWSER_SELECT_FILE,
    FILEBROWSER_SELECT_FILE_SUBSYSTEM,
    FILEBROWSER_SELECT_IMAGE,
diff --git a/msg_hash.h b/msg_hash.h
index 2d30c4c90d..ae3533f66f 100644
--- a/msg_hash.h
+++ b/msg_hash.h
@@ -151,6 +151,8 @@ enum msg_file_type
     * menu_cbs_init_bind_get_string_representation_compare_type() breaks... */
    FILE_TYPE_DOWNLOAD_PL_THUMBNAIL_CONTENT,
 
+   FILE_TYPE_MANUAL_SCAN_DIRECTORY,
+
    FILE_TYPE_LAST
 };
 
@@ -1245,6 +1247,8 @@ enum msg_hash_enums
    MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_PLAYLIST_LABEL_DISPLAY_MODE,
    MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_PLAYLIST_RIGHT_THUMBNAIL_MODE,
    MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_PLAYLIST_LEFT_THUMBNAIL_MODE,
+   MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_SYSTEM_NAME,
+   MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_MANUAL_CONTENT_SCAN_CORE_NAME,
    MENU_ENUM_LABEL_DEFERRED_MIXER_STREAM_SETTINGS_LIST,
    MENU_ENUM_LABEL_DEFERRED_CONFIGURATIONS_LIST,
    MENU_ENUM_LABEL_DEFERRED_FAVORITES_LIST,
@@ -1343,6 +1347,7 @@ enum msg_hash_enums
    MENU_ENUM_LABEL_DEFERRED_ACCOUNTS_YOUTUBE_LIST,
    MENU_ENUM_LABEL_DEFERRED_ACCOUNTS_LIST,
    MENU_ENUM_LABEL_DEFERRED_INFORMATION,
+   MENU_ENUM_LABEL_DEFERRED_MANUAL_CONTENT_SCAN_LIST,
 
    MENU_LABEL(FILE_DETECT_CORE_LIST_PUSH_DIR),
    MENU_LABEL(DOWNLOADED_FILE_DETECT_CORE_LIST),
@@ -2658,6 +2663,27 @@ enum msg_hash_enums
    MSG_NO_DISC_INSERTED,
    MENU_LABEL(DELETE_PLAYLIST),
 
+   /* Manual content scan */
+   MENU_LABEL(MANUAL_CONTENT_SCAN_LIST),
+   MENU_LABEL(MANUAL_CONTENT_SCAN_DIR),
+   MENU_LABEL(MANUAL_CONTENT_SCAN_SYSTEM_NAME),
+   MENU_LABEL(MANUAL_CONTENT_SCAN_SYSTEM_NAME_CUSTOM),
+   MENU_LABEL(MANUAL_CONTENT_SCAN_CORE_NAME),
+   MENU_LABEL(MANUAL_CONTENT_SCAN_FILE_EXTS),
+   MENU_LABEL(MANUAL_CONTENT_SCAN_OVERWRITE),
+   MENU_LABEL(MANUAL_CONTENT_SCAN_START),
+
+   MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_SYSTEM_NAME_USE_CONTENT_DIR,
+   MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_SYSTEM_NAME_USE_CUSTOM,
+
+   MENU_ENUM_LABEL_VALUE_MANUAL_CONTENT_SCAN_CORE_NAME_DETECT,
+
+   MSG_MANUAL_CONTENT_SCAN_INVALID_CONFIG,
+   MSG_MANUAL_CONTENT_SCAN_INVALID_CONTENT,
+   MSG_MANUAL_CONTENT_SCAN_START,
+   MSG_MANUAL_CONTENT_SCAN_IN_PROGRESS,
+   MSG_MANUAL_CONTENT_SCAN_END,
+
    MSG_LAST
 };
 
@@ -2715,6 +2741,7 @@ enum msg_hash_enums
 #define MENU_LABEL_DEFERRED_CONFIGURATIONS_LIST                                0x679a1b0bU
 #define MENU_LABEL_DEFERRED_BROWSE_URL_START                                   0xcef58296U
 #define MENU_LABEL_DEFERRED_INFORMATION                                        0x3FCC9F2BU
+#define MENU_LABEL_DEFERRED_MANUAL_CONTENT_SCAN_LIST                           0x479546DCU
 
 /* Cheevos settings */
 
@@ -2910,6 +2937,11 @@ enum msg_hash_enums
 #define MENU_LABEL_HELP_CHANGE_VIRTUAL_GAMEPAD                                 0x6e66ef07U
 #define MENU_LABEL_HELP_AUDIO_VIDEO_TROUBLESHOOTING                            0xd44d395cU
 
+/* Manual content scan */
+#define MENU_LABEL_MANUAL_CONTENT_SCAN_DIR                                     0x6674149FU
+#define MENU_LABEL_MANUAL_CONTENT_SCAN_SYSTEM_NAME                             0xA3EC34C5U
+#define MENU_LABEL_MANUAL_CONTENT_SCAN_CORE_NAME                               0xD13B7849U
+
 /* Main menu */
 #define MENU_LABEL_LOAD_CONTENT_LIST                                           0x5745de1fU
 #define MENU_LABEL_LOAD_CONTENT_HISTORY                                        0xfe1d79e5U
diff --git a/playlist.c b/playlist.c
index 5688faa129..65aea43bfb 100644
--- a/playlist.c
+++ b/playlist.c
@@ -368,8 +368,7 @@ void playlist_get_index_by_path(playlist_t *playlist,
 }
 
 bool playlist_entry_exists(playlist_t *playlist,
-      const char *path,
-      const char *crc32)
+      const char *path)
 {
    size_t i;
    char real_search_path[PATH_MAX_LENGTH];
diff --git a/playlist.h b/playlist.h
index 59bcd078d6..39a63c7de2 100644
--- a/playlist.h
+++ b/playlist.h
@@ -214,8 +214,7 @@ void playlist_get_index_by_path(playlist_t *playlist,
       const struct playlist_entry **entry);
 
 bool playlist_entry_exists(playlist_t *playlist,
-      const char *path,
-      const char *crc32);
+      const char *path);
 
 char *playlist_get_conf_path(playlist_t *playlist);
 
diff --git a/tasks/task_database.c b/tasks/task_database.c
index 1b71a34d6b..65ed332d46 100644
--- a/tasks/task_database.c
+++ b/tasks/task_database.c
@@ -850,7 +850,7 @@ static int database_info_list_iterate_found_match(
    fprintf(stderr, "entry path str: %s\n", entry_path_str);
 #endif
 
-   if (!playlist_entry_exists(playlist, entry_path_str, db_crc))
+   if (!playlist_entry_exists(playlist, entry_path_str))
    {
       struct playlist_entry entry;
 
@@ -1052,8 +1052,7 @@ static int task_database_iterate_playlist_lutro(
 
    free(db_playlist_path);
 
-   if (!playlist_entry_exists(playlist,
-            path, "DETECT"))
+   if (!playlist_entry_exists(playlist, path))
    {
       struct playlist_entry entry;
       char *game_title            = (char*)malloc(PATH_MAX_LENGTH * sizeof(char));
diff --git a/tasks/task_manual_content_scan.c b/tasks/task_manual_content_scan.c
new file mode 100644
index 0000000000..71e47bf257
--- /dev/null
+++ b/tasks/task_manual_content_scan.c
@@ -0,0 +1,354 @@
+/*  RetroArch - A frontend for libretro.
+ *  Copyright (C) 2011-2017 - Daniel De Matteis
+ *  Copyright (C) 2014-2017 - Jean-André Santoni
+ *  Copyright (C) 2016-2019 - Brad Parker
+ *
+ *  RetroArch is free software: you can redistribute it and/or modify it under the terms
+ *  of the GNU General Public License as published by the Free Software Found-
+ *  ation, either version 3 of the License, or (at your option) any later version.
+ *
+ *  RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ *  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ *  PURPOSE.  See the GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License along with RetroArch.
+ *  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <ctype.h>
+#include <boolean.h>
+
+#include <string/stdstring.h>
+#include <lists/string_list.h>
+#include <file/file_path.h>
+
+#include "tasks_internal.h"
+
+#include "../retroarch.h"
+#include "../msg_hash.h"
+#include "../playlist.h"
+#include "../manual_content_scan.h"
+
+#ifdef RARCH_INTERNAL
+#ifdef HAVE_MENU
+#include "../menu/menu_driver.h"
+#endif
+#endif
+
+enum manual_scan_status
+{
+   MANUAL_SCAN_BEGIN = 0,
+   MANUAL_SCAN_ITERATE_CONTENT,
+   MANUAL_SCAN_END
+};
+
+typedef struct manual_scan_handle
+{
+   manual_content_scan_task_config_t *task_config;
+   playlist_t *playlist;
+   struct string_list *content_list;
+   size_t list_size;
+   size_t list_index;
+   enum manual_scan_status status;
+} manual_scan_handle_t;
+
+/* Frees task handle + all constituent objects */
+static void free_manual_content_scan_handle(manual_scan_handle_t *manual_scan)
+{
+   if (!manual_scan)
+      return;
+
+   if (manual_scan->task_config)
+   {
+      free(manual_scan->task_config);
+      manual_scan->task_config = NULL;
+   }
+
+   if (manual_scan->playlist)
+   {
+      playlist_free(manual_scan->playlist);
+      manual_scan->playlist = NULL;
+   }
+
+   if (manual_scan->content_list)
+   {
+      string_list_free(manual_scan->content_list);
+      manual_scan->content_list = NULL;
+   }
+
+   free(manual_scan);
+   manual_scan = NULL;
+}
+
+static void task_manual_content_scan_handler(retro_task_t *task)
+{
+   manual_scan_handle_t *manual_scan = NULL;
+
+   if (!task)
+      goto task_finished;
+
+   manual_scan = (manual_scan_handle_t*)task->state;
+
+   if (!manual_scan)
+      goto task_finished;
+
+   if (task_get_cancelled(task))
+      goto task_finished;
+
+   switch (manual_scan->status)
+   {
+      case MANUAL_SCAN_BEGIN:
+         {
+            /* Get content list */
+            manual_scan->content_list = manual_content_scan_get_content_list(
+                  manual_scan->task_config);
+
+            if (!manual_scan->content_list)
+            {
+               runloop_msg_queue_push(
+                     msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_INVALID_CONTENT),
+                     1, 100, true,
+                     NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
+               goto task_finished;
+            }
+
+            manual_scan->list_size = manual_scan->content_list->size;
+
+            /* Open playlist */
+            manual_scan->playlist = playlist_init(
+                  manual_scan->task_config->playlist_file, COLLECTION_SIZE);
+
+            if (!manual_scan->playlist)
+               goto task_finished;
+
+            /* Reset playlist, if required */
+            if (manual_scan->task_config->overwrite_playlist)
+               playlist_clear(manual_scan->playlist);
+
+            /* Set default core, if required */
+            if (manual_scan->task_config->core_set)
+            {
+               playlist_set_default_core_path(
+                     manual_scan->playlist, manual_scan->task_config->core_path);
+               playlist_set_default_core_name(
+                     manual_scan->playlist, manual_scan->task_config->core_name);
+            }
+
+            /* All good - can start iterating */
+            manual_scan->status = MANUAL_SCAN_ITERATE_CONTENT;
+         }
+         break;
+      case MANUAL_SCAN_ITERATE_CONTENT:
+         {
+            const char *content_path =
+                  manual_scan->content_list->elems[manual_scan->list_index].data;
+
+            if (!string_is_empty(content_path))
+            {
+               const char *content_file = path_basename(content_path);
+               char task_title[PATH_MAX_LENGTH];
+
+               task_title[0] = '\0';
+
+               /* Update progress display */
+               task_free_title(task);
+
+               strlcpy(
+                     task_title, msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_IN_PROGRESS),
+                     sizeof(task_title));
+
+               if (!string_is_empty(content_file))
+                  strlcat(task_title, content_file, sizeof(task_title));
+
+               task_set_title(task, strdup(task_title));
+               task_set_progress(task, (manual_scan->list_index * 100) / manual_scan->list_size);
+
+               /* Add content to playlist */
+               manual_content_scan_add_content_to_playlist(
+                     manual_scan->task_config, manual_scan->playlist,
+                     content_path);
+            }
+
+            /* Increment content index */
+            manual_scan->list_index++;
+            if (manual_scan->list_index >= manual_scan->list_size)
+               manual_scan->status = MANUAL_SCAN_END;
+         }
+         break;
+      case MANUAL_SCAN_END:
+         {
+            playlist_t *cached_playlist = playlist_get_cached();
+            char task_title[PATH_MAX_LENGTH];
+
+            task_title[0] = '\0';
+
+            /* Ensure playlist is alphabetically sorted */
+            playlist_qsort(manual_scan->playlist);
+
+            /* Save playlist changes to disk */
+            playlist_write_file(manual_scan->playlist);
+
+            /* If this is the currently cached playlist, then
+             * it must be re-cached (otherwise changes will be
+             * lost if the currently cached playlist is saved
+             * to disk for any reason...) */
+            if (cached_playlist)
+            {
+               if (string_is_equal(
+                     manual_scan->task_config->playlist_file,
+                     playlist_get_conf_path(cached_playlist)))
+               {
+                  playlist_free_cached();
+                  playlist_init_cached(
+                        manual_scan->task_config->playlist_file, COLLECTION_SIZE);
+               }
+            }
+
+            /* Update progress display */
+            task_free_title(task);
+
+            strlcpy(
+                  task_title, msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_END),
+                  sizeof(task_title));
+            strlcat(task_title, manual_scan->task_config->system_name,
+                  sizeof(task_title));
+
+            task_set_title(task, strdup(task_title));
+         }
+         /* fall-through */
+      default:
+         task_set_progress(task, 100);
+         goto task_finished;
+   }
+   
+   return;
+   
+task_finished:
+
+   if (task)
+      task_set_finished(task, true);
+
+   free_manual_content_scan_handle(manual_scan);
+}
+
+static bool task_manual_content_scan_finder(retro_task_t *task, void *user_data)
+{
+   manual_scan_handle_t *manual_scan = NULL;
+
+   if (!task || !user_data)
+      return false;
+
+   if (task->handler != task_manual_content_scan_handler)
+      return false;
+
+   manual_scan = (manual_scan_handle_t*)task->state;
+   if (!manual_scan)
+      return false;
+
+   return string_is_equal(
+         (const char*)user_data, manual_scan->task_config->playlist_file);
+}
+
+static void cb_task_manual_content_scan_refresh_menu(
+      retro_task_t *task, void *task_data,
+      void *user_data, const char *err)
+{
+#if defined(RARCH_INTERNAL) && defined(HAVE_MENU)
+   menu_ctx_environment_t menu_environ;
+   menu_environ.type = MENU_ENVIRON_RESET_HORIZONTAL_LIST;
+   menu_environ.data = NULL;
+
+   menu_driver_ctl(RARCH_MENU_CTL_ENVIRONMENT, &menu_environ);
+#endif
+}
+
+bool task_push_manual_content_scan(void)
+{
+   task_finder_data_t find_data;
+   char task_title[PATH_MAX_LENGTH];
+   retro_task_t *task                = NULL;
+   manual_scan_handle_t *manual_scan = (manual_scan_handle_t*)
+         calloc(1, sizeof(manual_scan_handle_t));
+
+   task_title[0] = '\0';
+
+   /* Sanity check */
+   if (!manual_scan)
+      goto error;
+
+   /* Configure handle */
+   manual_scan->task_config  = NULL;
+   manual_scan->playlist     = NULL;
+   manual_scan->content_list = NULL;
+   manual_scan->list_size    = 0;
+   manual_scan->list_index   = 0;
+   manual_scan->status       = MANUAL_SCAN_BEGIN;
+
+   /* > Get current manual content scan configuration */
+   manual_scan->task_config = (manual_content_scan_task_config_t*)
+         calloc(1, sizeof(manual_content_scan_task_config_t));
+
+   if (!manual_scan->task_config)
+      goto error;
+
+   if (!manual_content_scan_get_task_config(manual_scan->task_config))
+   {
+      runloop_msg_queue_push(
+            msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_INVALID_CONFIG),
+            1, 100, true,
+            NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
+      goto error;
+   }
+
+   /* Concurrent scanning of content to the same
+    * playlist is not allowed */
+   find_data.func     = task_manual_content_scan_finder;
+   find_data.userdata = (void*)manual_scan->task_config->playlist_file;
+
+   if (task_queue_find(&find_data))
+      goto error;
+
+   /* Create task */
+   task = task_init();
+
+   if (!task)
+      goto error;
+
+   /* > Get task title */
+   strlcpy(
+         task_title, msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_START),
+         sizeof(task_title));
+   strlcat(task_title, manual_scan->task_config->system_name,
+         sizeof(task_title));
+
+   /* > Configure task */
+   task->handler                 = task_manual_content_scan_handler;
+   task->state                   = manual_scan;
+   task->title                   = strdup(task_title);
+   task->alternative_look        = true;
+   task->progress                = 0;
+   task->callback                = cb_task_manual_content_scan_refresh_menu;
+
+   /* > Push task */
+   task_queue_push(task);
+
+   return true;
+
+error:
+
+   /* Clean up task */
+   if (task)
+   {
+      free(task);
+      task = NULL;
+   }
+
+   /* Clean up handle */
+   free_manual_content_scan_handle(manual_scan);
+   manual_scan = NULL;
+
+   return false;
+}
diff --git a/tasks/tasks_internal.h b/tasks/tasks_internal.h
index 7669ee96d9..201975c08b 100644
--- a/tasks/tasks_internal.h
+++ b/tasks/tasks_internal.h
@@ -98,6 +98,8 @@ bool task_push_dbscan(
       retro_task_callback_t cb);
 #endif
 
+bool task_push_manual_content_scan(void);
+
 #ifdef HAVE_OVERLAY
 bool task_push_overlay_load_default(
       retro_task_callback_t cb,