task_manual_content_scan.c 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. /* RetroArch - A frontend for libretro.
  2. * Copyright (C) 2011-2017 - Daniel De Matteis
  3. * Copyright (C) 2014-2017 - Jean-André Santoni
  4. * Copyright (C) 2016-2019 - Brad Parker
  5. * Copyright (C) 2019 - James Leaver
  6. *
  7. * RetroArch is free software: you can redistribute it and/or modify it under the terms
  8. * of the GNU General Public License as published by the Free Software Found-
  9. * ation, either version 3 of the License, or (at your option) any later version.
  10. *
  11. * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
  12. * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
  13. * PURPOSE. See the GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along with RetroArch.
  16. * If not, see <http://www.gnu.org/licenses/>.
  17. */
  18. #include <stdint.h>
  19. #include <stdlib.h>
  20. #include <string.h>
  21. #include <ctype.h>
  22. #include <boolean.h>
  23. #include <string/stdstring.h>
  24. #include <lists/string_list.h>
  25. #include <file/file_path.h>
  26. #include <formats/logiqx_dat.h>
  27. #include <formats/m3u_file.h>
  28. #include "tasks_internal.h"
  29. #include "../msg_hash.h"
  30. #include "../playlist.h"
  31. #include "../manual_content_scan.h"
  32. #ifdef RARCH_INTERNAL
  33. #ifdef HAVE_MENU
  34. #include "../menu/menu_driver.h"
  35. #endif
  36. #endif
  37. enum manual_scan_status
  38. {
  39. MANUAL_SCAN_BEGIN = 0,
  40. MANUAL_SCAN_ITERATE_CLEAN,
  41. MANUAL_SCAN_ITERATE_CONTENT,
  42. MANUAL_SCAN_ITERATE_M3U,
  43. MANUAL_SCAN_END
  44. };
  45. typedef struct manual_scan_handle
  46. {
  47. manual_content_scan_task_config_t *task_config;
  48. playlist_t *playlist;
  49. struct string_list *file_exts_list;
  50. struct string_list *content_list;
  51. logiqx_dat_t *dat_file;
  52. struct string_list *m3u_list;
  53. playlist_config_t playlist_config; /* size_t alignment */
  54. size_t playlist_size;
  55. size_t playlist_index;
  56. size_t content_list_size;
  57. size_t content_list_index;
  58. size_t m3u_index;
  59. enum manual_scan_status status;
  60. } manual_scan_handle_t;
  61. /* Frees task handle + all constituent objects */
  62. static void free_manual_content_scan_handle(manual_scan_handle_t *manual_scan)
  63. {
  64. if (!manual_scan)
  65. return;
  66. if (manual_scan->task_config)
  67. {
  68. free(manual_scan->task_config);
  69. manual_scan->task_config = NULL;
  70. }
  71. if (manual_scan->playlist)
  72. {
  73. playlist_free(manual_scan->playlist);
  74. manual_scan->playlist = NULL;
  75. }
  76. if (manual_scan->file_exts_list)
  77. {
  78. string_list_free(manual_scan->file_exts_list);
  79. manual_scan->file_exts_list = NULL;
  80. }
  81. if (manual_scan->content_list)
  82. {
  83. string_list_free(manual_scan->content_list);
  84. manual_scan->content_list = NULL;
  85. }
  86. if (manual_scan->m3u_list)
  87. {
  88. string_list_free(manual_scan->m3u_list);
  89. manual_scan->m3u_list = NULL;
  90. }
  91. if (manual_scan->dat_file)
  92. {
  93. logiqx_dat_free(manual_scan->dat_file);
  94. manual_scan->dat_file = NULL;
  95. }
  96. free(manual_scan);
  97. manual_scan = NULL;
  98. }
  99. static void cb_task_manual_content_scan(
  100. retro_task_t *task, void *task_data,
  101. void *user_data, const char *err)
  102. {
  103. manual_scan_handle_t *manual_scan = NULL;
  104. playlist_t *cached_playlist = playlist_get_cached();
  105. #if defined(RARCH_INTERNAL) && defined(HAVE_MENU)
  106. struct menu_state *menu_st = menu_state_get_ptr();
  107. if (!task)
  108. goto end;
  109. #else
  110. if (!task)
  111. return;
  112. #endif
  113. if (!(manual_scan = (manual_scan_handle_t*)task->state))
  114. {
  115. #if defined(RARCH_INTERNAL) && defined(HAVE_MENU)
  116. goto end;
  117. #else
  118. return;
  119. #endif
  120. }
  121. /* If the manual content scan task has modified the
  122. * currently cached playlist, then it must be re-cached
  123. * (otherwise changes will be lost if the currently
  124. * cached playlist is saved to disk for any reason...) */
  125. if (cached_playlist)
  126. {
  127. if (string_is_equal(
  128. manual_scan->playlist_config.path,
  129. playlist_get_conf_path(cached_playlist)))
  130. {
  131. playlist_config_t playlist_config;
  132. /* Copy configuration of cached playlist
  133. * (could use manual_scan->playlist_config,
  134. * but doing it this way guarantees that
  135. * the cached playlist is preserved in
  136. * its original state) */
  137. if (playlist_config_copy(
  138. playlist_get_config(cached_playlist),
  139. &playlist_config))
  140. {
  141. playlist_free_cached();
  142. playlist_init_cached(&playlist_config);
  143. }
  144. }
  145. }
  146. #if defined(RARCH_INTERNAL) && defined(HAVE_MENU)
  147. end:
  148. /* When creating playlists, the playlist tabs of
  149. * any active menu driver must be refreshed */
  150. if (menu_st->driver_ctx->environ_cb)
  151. menu_st->driver_ctx->environ_cb(MENU_ENVIRON_RESET_HORIZONTAL_LIST,
  152. NULL, menu_st->userdata);
  153. #endif
  154. }
  155. static void task_manual_content_scan_free(retro_task_t *task)
  156. {
  157. manual_scan_handle_t *manual_scan = NULL;
  158. if (!task)
  159. return;
  160. manual_scan = (manual_scan_handle_t*)task->state;
  161. free_manual_content_scan_handle(manual_scan);
  162. }
  163. static void task_manual_content_scan_handler(retro_task_t *task)
  164. {
  165. manual_scan_handle_t *manual_scan = NULL;
  166. if (!task)
  167. goto task_finished;
  168. if (!(manual_scan = (manual_scan_handle_t*)task->state))
  169. goto task_finished;
  170. if (task_get_cancelled(task))
  171. goto task_finished;
  172. switch (manual_scan->status)
  173. {
  174. case MANUAL_SCAN_BEGIN:
  175. {
  176. /* Get allowed file extensions list */
  177. if (!string_is_empty(manual_scan->task_config->file_exts))
  178. manual_scan->file_exts_list = string_split(
  179. manual_scan->task_config->file_exts, "|");
  180. /* Get content list */
  181. if (!(manual_scan->content_list
  182. = manual_content_scan_get_content_list(
  183. manual_scan->task_config)))
  184. {
  185. runloop_msg_queue_push(
  186. msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_INVALID_CONTENT),
  187. 1, 100, true,
  188. NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
  189. goto task_finished;
  190. }
  191. manual_scan->content_list_size = manual_scan->content_list->size;
  192. /* Load DAT file, if required */
  193. if (!string_is_empty(manual_scan->task_config->dat_file_path))
  194. {
  195. if (!(manual_scan->dat_file =
  196. logiqx_dat_init(
  197. manual_scan->task_config->dat_file_path)))
  198. {
  199. runloop_msg_queue_push(
  200. msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_DAT_FILE_LOAD_ERROR),
  201. 1, 100, true,
  202. NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
  203. goto task_finished;
  204. }
  205. }
  206. /* Open playlist */
  207. if (!(manual_scan->playlist =
  208. playlist_init(&manual_scan->playlist_config)))
  209. goto task_finished;
  210. /* Reset playlist, if required */
  211. if (manual_scan->task_config->overwrite_playlist)
  212. playlist_clear(manual_scan->playlist);
  213. /* Get initial playlist size */
  214. manual_scan->playlist_size =
  215. playlist_size(manual_scan->playlist);
  216. /* Set default core, if required */
  217. if (manual_scan->task_config->core_set)
  218. {
  219. playlist_set_default_core_path(manual_scan->playlist,
  220. manual_scan->task_config->core_path);
  221. playlist_set_default_core_name(manual_scan->playlist,
  222. manual_scan->task_config->core_name);
  223. }
  224. /* Record remaining scan parameters to enable
  225. * subsequent 'refresh playlist' operations */
  226. playlist_set_scan_content_dir(manual_scan->playlist,
  227. manual_scan->task_config->content_dir);
  228. playlist_set_scan_file_exts(manual_scan->playlist,
  229. manual_scan->task_config->file_exts_custom_set ?
  230. manual_scan->task_config->file_exts : NULL);
  231. playlist_set_scan_dat_file_path(manual_scan->playlist,
  232. manual_scan->task_config->dat_file_path);
  233. playlist_set_scan_search_recursively(manual_scan->playlist,
  234. manual_scan->task_config->search_recursively);
  235. playlist_set_scan_search_archives(manual_scan->playlist,
  236. manual_scan->task_config->search_archives);
  237. playlist_set_scan_filter_dat_content(manual_scan->playlist,
  238. manual_scan->task_config->filter_dat_content);
  239. /* All good - can start iterating
  240. * > If playlist has content and 'validate
  241. * entries' is enabled, go to clean-up phase
  242. * > Otherwise go straight to content scan phase */
  243. if (manual_scan->task_config->validate_entries &&
  244. (manual_scan->playlist_size > 0))
  245. manual_scan->status = MANUAL_SCAN_ITERATE_CLEAN;
  246. else
  247. manual_scan->status = MANUAL_SCAN_ITERATE_CONTENT;
  248. }
  249. break;
  250. case MANUAL_SCAN_ITERATE_CLEAN:
  251. {
  252. const struct playlist_entry *entry = NULL;
  253. bool delete_entry = false;
  254. /* Get current entry */
  255. playlist_get_index(manual_scan->playlist,
  256. manual_scan->playlist_index, &entry);
  257. if (entry)
  258. {
  259. const char *entry_file = NULL;
  260. const char *entry_file_ext = NULL;
  261. char task_title[PATH_MAX_LENGTH];
  262. /* Update progress display */
  263. task_free_title(task);
  264. strlcpy(task_title,
  265. msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_PLAYLIST_CLEANUP),
  266. sizeof(task_title));
  267. if (!string_is_empty(entry->path) &&
  268. (entry_file = path_basename(entry->path)))
  269. strlcat(task_title, entry_file, sizeof(task_title));
  270. task_set_title(task, strdup(task_title));
  271. task_set_progress(task, (manual_scan->playlist_index * 100) /
  272. manual_scan->playlist_size);
  273. /* Check whether playlist content exists on
  274. * the filesystem */
  275. if (!playlist_content_path_is_valid(entry->path))
  276. delete_entry = true;
  277. /* If file exists, check whether it has a
  278. * permitted file extension */
  279. else if (manual_scan->file_exts_list &&
  280. (entry_file_ext = path_get_extension(entry->path)) &&
  281. !string_list_find_elem_prefix(
  282. manual_scan->file_exts_list,
  283. ".", entry_file_ext))
  284. delete_entry = true;
  285. if (delete_entry)
  286. {
  287. /* Invalid content - delete entry */
  288. playlist_delete_index(manual_scan->playlist,
  289. manual_scan->playlist_index);
  290. /* Update playlist_size */
  291. manual_scan->playlist_size = playlist_size(manual_scan->playlist);
  292. }
  293. }
  294. /* Increment entry index *if* current entry still
  295. * exists (i.e. if entry was deleted, current index
  296. * will already point to the *next* entry) */
  297. if (!delete_entry)
  298. manual_scan->playlist_index++;
  299. if (manual_scan->playlist_index >=
  300. manual_scan->playlist_size)
  301. manual_scan->status = MANUAL_SCAN_ITERATE_CONTENT;
  302. }
  303. break;
  304. case MANUAL_SCAN_ITERATE_CONTENT:
  305. {
  306. const char *content_path = manual_scan->content_list->elems[
  307. manual_scan->content_list_index].data;
  308. int content_type = manual_scan->content_list->elems[
  309. manual_scan->content_list_index].attr.i;
  310. if (!string_is_empty(content_path))
  311. {
  312. char task_title[PATH_MAX_LENGTH];
  313. const char *content_file = path_basename(content_path);
  314. /* Update progress display */
  315. task_free_title(task);
  316. strlcpy(task_title,
  317. msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_IN_PROGRESS),
  318. sizeof(task_title));
  319. if (!string_is_empty(content_file))
  320. strlcat(task_title, content_file, sizeof(task_title));
  321. task_set_title(task, strdup(task_title));
  322. task_set_progress(task,
  323. (manual_scan->content_list_index * 100) /
  324. manual_scan->content_list_size);
  325. /* Add content to playlist */
  326. manual_content_scan_add_content_to_playlist(
  327. manual_scan->task_config, manual_scan->playlist,
  328. content_path, content_type, manual_scan->dat_file);
  329. /* If this is an M3U file, add it to the
  330. * M3U list for later processing */
  331. if (m3u_file_is_m3u(content_path))
  332. {
  333. union string_list_elem_attr attr;
  334. attr.i = 0;
  335. /* Note: If string_list_append() fails, there is
  336. * really nothing we can do. The M3U file will
  337. * just be ignored... */
  338. string_list_append(
  339. manual_scan->m3u_list, content_path, attr);
  340. }
  341. }
  342. /* Increment content index */
  343. manual_scan->content_list_index++;
  344. if (manual_scan->content_list_index >=
  345. manual_scan->content_list_size)
  346. {
  347. /* Check whether we have any M3U files
  348. * to process */
  349. if (manual_scan->m3u_list->size > 0)
  350. manual_scan->status = MANUAL_SCAN_ITERATE_M3U;
  351. else
  352. manual_scan->status = MANUAL_SCAN_END;
  353. }
  354. }
  355. break;
  356. case MANUAL_SCAN_ITERATE_M3U:
  357. {
  358. const char *m3u_path = manual_scan->m3u_list->elems[
  359. manual_scan->m3u_index].data;
  360. if (!string_is_empty(m3u_path))
  361. {
  362. char task_title[PATH_MAX_LENGTH];
  363. const char *m3u_name = path_basename_nocompression(m3u_path);
  364. m3u_file_t *m3u_file = NULL;
  365. /* Update progress display */
  366. task_free_title(task);
  367. strlcpy(task_title,
  368. msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_M3U_CLEANUP),
  369. sizeof(task_title));
  370. if (!string_is_empty(m3u_name))
  371. strlcat(task_title, m3u_name, sizeof(task_title));
  372. task_set_title(task, strdup(task_title));
  373. task_set_progress(task, (manual_scan->m3u_index * 100) /
  374. manual_scan->m3u_list->size);
  375. /* Load M3U file */
  376. if ((m3u_file = m3u_file_init(m3u_path)))
  377. {
  378. size_t i;
  379. /* Loop over M3U entries */
  380. for (i = 0; i < m3u_file_get_size(m3u_file); i++)
  381. {
  382. m3u_file_entry_t *m3u_entry = NULL;
  383. /* Delete any playlist items matching the
  384. * content path of the M3U entry */
  385. if (m3u_file_get_entry(m3u_file, i, &m3u_entry))
  386. playlist_delete_by_path(
  387. manual_scan->playlist, m3u_entry->full_path);
  388. }
  389. m3u_file_free(m3u_file);
  390. }
  391. }
  392. /* Increment M3U file index */
  393. manual_scan->m3u_index++;
  394. if (manual_scan->m3u_index >= manual_scan->m3u_list->size)
  395. manual_scan->status = MANUAL_SCAN_END;
  396. }
  397. break;
  398. case MANUAL_SCAN_END:
  399. {
  400. char task_title[PATH_MAX_LENGTH];
  401. /* Ensure playlist is alphabetically sorted
  402. * > Override user settings here */
  403. playlist_set_sort_mode(manual_scan->playlist, PLAYLIST_SORT_MODE_DEFAULT);
  404. playlist_qsort(manual_scan->playlist);
  405. /* Save playlist changes to disk */
  406. playlist_write_file(manual_scan->playlist);
  407. /* Update progress display */
  408. task_free_title(task);
  409. strlcpy(
  410. task_title, msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_END),
  411. sizeof(task_title));
  412. strlcat(task_title, manual_scan->task_config->system_name,
  413. sizeof(task_title));
  414. task_set_title(task, strdup(task_title));
  415. }
  416. /* fall-through */
  417. default:
  418. task_set_progress(task, 100);
  419. goto task_finished;
  420. }
  421. return;
  422. task_finished:
  423. if (task)
  424. task_set_finished(task, true);
  425. }
  426. static bool task_manual_content_scan_finder(retro_task_t *task, void *user_data)
  427. {
  428. manual_scan_handle_t *manual_scan = NULL;
  429. if (!task || !user_data)
  430. return false;
  431. if (task->handler != task_manual_content_scan_handler)
  432. return false;
  433. if (!(manual_scan = (manual_scan_handle_t*)task->state))
  434. return false;
  435. return string_is_equal(
  436. (const char*)user_data, manual_scan->playlist_config.path);
  437. }
  438. bool task_push_manual_content_scan(
  439. const playlist_config_t *playlist_config,
  440. const char *playlist_directory)
  441. {
  442. task_finder_data_t find_data;
  443. char task_title[PATH_MAX_LENGTH];
  444. retro_task_t *task = NULL;
  445. manual_scan_handle_t *manual_scan = NULL;
  446. /* Sanity check */
  447. if ( !playlist_config
  448. || string_is_empty(playlist_directory))
  449. return false;
  450. if (!(manual_scan = (manual_scan_handle_t*)
  451. calloc(1, sizeof(manual_scan_handle_t))))
  452. return false;
  453. /* Configure handle */
  454. manual_scan->task_config = NULL;
  455. manual_scan->playlist = NULL;
  456. manual_scan->file_exts_list = NULL;
  457. manual_scan->content_list = NULL;
  458. manual_scan->dat_file = NULL;
  459. manual_scan->playlist_size = 0;
  460. manual_scan->playlist_index = 0;
  461. manual_scan->content_list_size = 0;
  462. manual_scan->content_list_index = 0;
  463. manual_scan->status = MANUAL_SCAN_BEGIN;
  464. manual_scan->m3u_index = 0;
  465. manual_scan->m3u_list = string_list_new();
  466. if (!manual_scan->m3u_list)
  467. goto error;
  468. /* > Get current manual content scan configuration */
  469. if (!(manual_scan->task_config = (manual_content_scan_task_config_t*)
  470. calloc(1, sizeof(manual_content_scan_task_config_t))))
  471. goto error;
  472. if ( !manual_content_scan_get_task_config(
  473. manual_scan->task_config, playlist_directory))
  474. {
  475. runloop_msg_queue_push(
  476. msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_INVALID_CONFIG),
  477. 1, 100, true,
  478. NULL, MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
  479. goto error;
  480. }
  481. /* > Cache playlist configuration */
  482. if (!playlist_config_copy(playlist_config,
  483. &manual_scan->playlist_config))
  484. goto error;
  485. playlist_config_set_path(
  486. &manual_scan->playlist_config,
  487. manual_scan->task_config->playlist_file);
  488. /* Concurrent scanning of content to the same
  489. * playlist is not allowed */
  490. find_data.func = task_manual_content_scan_finder;
  491. find_data.userdata = (void*)manual_scan->playlist_config.path;
  492. if (task_queue_find(&find_data))
  493. goto error;
  494. /* Create task */
  495. if (!(task = task_init()))
  496. goto error;
  497. /* > Get task title */
  498. strlcpy(
  499. task_title, msg_hash_to_str(MSG_MANUAL_CONTENT_SCAN_START),
  500. sizeof(task_title));
  501. strlcat(task_title, manual_scan->task_config->system_name,
  502. sizeof(task_title));
  503. /* > Configure task */
  504. task->handler = task_manual_content_scan_handler;
  505. task->state = manual_scan;
  506. task->title = strdup(task_title);
  507. task->alternative_look = true;
  508. task->progress = 0;
  509. task->callback = cb_task_manual_content_scan;
  510. task->cleanup = task_manual_content_scan_free;
  511. /* > Push task */
  512. task_queue_push(task);
  513. return true;
  514. error:
  515. /* Clean up handle */
  516. free_manual_content_scan_handle(manual_scan);
  517. manual_scan = NULL;
  518. return false;
  519. }