rockbox/apps/plugins/random_folder_advance_config.c
Christian Soffke f0d99391de plugins: random folder advance: misc improvements
fixes:
- traverse_dir was called recursively using two
  MAX_PATH local buffers => possible stack overflow
- The import function inserted additional, incorrect,
  entries when the whole line buffer was filled
- "Dirs not found" message overlapped number
  of folders when generating folder list
- Final number of scanned folders wasn't displayed
- Wouldn't stop inserting when maximum number
  of files in playlist was already reached
- Prevent buffer overflow when importing
- Don't write to fd after opening failed
- Use whole buffer with read_line, instead
  of subtracting 1. Remove hard coded sizes
- CRs don't need to be removed in import function
  (already handled by read_line)

features:
- Use insert context for *much* faster insertion
  of large # of folders, and nicer progress display
- Use UI viewport
- Add progress indicator when saving or loading
- Display number of folders in edit list title
- Go back to plugin's main menu from edit list
- Only ask to save changes if list is dirty
- Warn before erasing modified playlist
- Flag successfully created playlist as modified
- Make folder scan wait for dircache
- Shorten menu item names
- Put "Play Shuffled" menu item first
- Remember selection when returning from submenus
- Go to WPS after ACTION_TREE_WPS
- Exit menu when pressing Back
- Perform an initial scan, if no data file exists
  yet, when "Play Shuffled" or "Edit" is selected

Change-Id: I7df76f8fb8387888ce491d8b74b01b481e9997d3
2026-01-16 16:49:38 +01:00

782 lines
22 KiB
C

/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2006 Jonathan Gordon
*
* This program 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 Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
#include "config.h"
#include "plugin.h"
#include "file.h"
#define RFA_LIST_DAT ROCKBOX_DIR "/folder_advance_list.dat" /* Folder data */
#define RFA_LIST_TXT ROCKBOX_DIR "/folder_advance_list.txt" /* Exported txt */
#define RFA_CSTM_DIR ROCKBOX_DIR "/folder_advance_dir.txt" /* Custom dirs */
#define MAX_IGNORED_DIRS 10 /* Maximum number of folders that are skipped */
struct rfa_dirs /* File format for folder data */
{
int count;
char dir[][MAX_PATH];
};
struct rfa_scan /* Context for single run */
{
char ignored_dirs[MAX_IGNORED_DIRS][MAX_PATH];
char path[MAX_PATH];
struct rfa_dirs *dirs;
char *err_dir;
unsigned long last_update;
int num_ignored_dirs;
int num_dirs;
int num_err;
int fd; /* writes to RFA_LIST_DAT */
bool cancel;
bool dirty;
};
static void update_screen(struct rfa_scan *scan)
{
char buf[15];
struct screen *display;
struct viewport vp, *last_vp;
rb->snprintf(buf, sizeof buf, "Folders: %d ", scan->num_dirs);
FOR_NB_SCREENS(i)
{
display = rb->screens[i];
rb->viewport_set_defaults(&vp, i);
last_vp = display->set_viewport(&vp);
if(!scan->path[0])
display->clear_viewport();
display->puts(0, 0, buf);
if (scan->num_err && scan->err_dir)
{
display->puts(0, 1, "Not found:");
display->puts(0, 1 + scan->num_err, scan->err_dir);
}
display->update_viewport();
display->set_viewport(last_vp);
}
scan->last_update = *rb->current_tick;
}
static void traverse_path(struct rfa_scan *scan)
{
DIR* dir;
struct dirent *entry;
char *p;
int i, dirlen;
bool skip;
dir = rb->opendir(scan->path);
/* remove slash from root dir; will be prepended later */
if (scan->path[0] == '/' && scan->path[1] == '\0')
scan->path[0] = dirlen = 0;
else
dirlen = rb->strlen(scan->path);
if (!dir)
return;
while ((entry = rb->readdir(dir)))
{
if (scan->cancel)
break;
/* Don't check special dirs */
if (entry->d_name[0] == '.')
{
if (!rb->strcmp(entry->d_name, ".") ||
!rb->strcmp(entry->d_name, "..") ||
!rb->strcmp(entry->d_name, ".rockbox"))
continue;
}
struct dirinfo info = rb->dir_get_info(dir, entry);
if (info.attribute & ATTR_DIRECTORY)
{
/* Append name to full path */
rb->snprintf(scan->path + dirlen, sizeof(scan->path) - dirlen,
"/%s", entry->d_name);
/* Skip ignored dirs */
skip = false;
for(i = 0; i < scan->num_ignored_dirs; i++)
if(!rb->strcmp(scan->path, scan->ignored_dirs[i]))
{
skip = true;
break;
}
if (skip)
continue;
scan->num_dirs++;
/* Write full path to file (erase end of buffer) */
p = &scan->path[rb->strlen(scan->path)];
rb->memset(p, 0, &scan->path[sizeof(scan->path) - 1] - p);
rb->write(scan->fd, scan->path, sizeof scan->path);
/* Recursion */
traverse_path(scan);
}
if (*rb->current_tick - scan->last_update > (HZ/2))
{
update_screen(scan);
if (rb->action_userabort(TIMEOUT_NOBLOCK))
{
scan->cancel = true;
break;
}
}
rb->yield();
}
rb->closedir(dir);
}
static bool custom_paths(struct rfa_scan *scan)
{
char line[MAX_PATH], *p;
int i, fd;
bool write_line;
fd = rb->open(RFA_CSTM_DIR, O_RDONLY);
if (fd < 0)
return false;
/* Populate ignored dirs array */
while ((rb->read_line(fd, line, sizeof line)) > 0)
if ((line[0] == '-') && (line[1] == '/') &&
(scan->num_ignored_dirs < MAX_IGNORED_DIRS))
{
rb->strlcpy(scan->ignored_dirs[scan->num_ignored_dirs],
line + 1, sizeof(*scan->ignored_dirs));
scan->num_ignored_dirs++;
}
/* Check which paths to traverse */
rb->lseek(fd, 0, SEEK_SET);
while ((rb->read_line(fd, line, sizeof line)) > 0)
{
/* Skip blank lines or dirs to ignore */
if (line[0] == '\0' || ((line[0] == '-') && (line[1] == '/')))
continue;
/* Set path to traverse */
p = line;
while(*p == '/')
p++;
rb->snprintf(scan->path, sizeof scan->path, "/%s", p);
if (rb->dir_exists(scan->path))
{
/* Erase end of path buffer */
p = &scan->path[rb->strlen(scan->path)];
rb->memset(p, 0, &scan->path[sizeof(scan->path) - 1] - p);
/* Don't write ignored dirs to file */
write_line = true;
for(i = 0; i < scan->num_ignored_dirs; i++)
if(!rb->strcmp(scan->path, scan->ignored_dirs[i]))
{
write_line = false;
break;
}
if(write_line)
{
scan->num_dirs++;
rb->write(scan->fd, scan->path, sizeof scan->path);
}
traverse_path(scan);
update_screen(scan);
}
else
{
scan->num_err++;
scan->err_dir = scan->path;
update_screen(scan);
scan->err_dir = NULL;
}
}
rb->close(fd);
if(scan->num_err)
rb->get_action(CONTEXT_STD, TIMEOUT_BLOCK);
return true;
}
static void generate(struct rfa_scan *scan)
{
bool update_title = false;
/* Reset run context */
rb->memset(scan, 0, sizeof(struct rfa_scan));
FOR_NB_SCREENS(i)
update_title = rb->sb_set_title_text("Scanning...", Icon_NOICON, i) ||
update_title;
if (update_title)
rb->send_event(GUI_EVENT_ACTIONUPDATE, (void*)1);
#ifdef HAVE_DIRCACHE
rb->splash(0, ID2P(LANG_WAIT));
rb->dircache_wait();
#endif
scan->fd = rb->open(RFA_LIST_DAT, O_CREAT|O_WRONLY, 0666);
if (scan->fd < 0)
{
rb->splashf(HZ, "Couldnt open %s", RFA_LIST_DAT);
return;
}
/* Write placeholder for number of folders */
rb->write(scan->fd, &scan->num_dirs, sizeof(int));
update_screen(scan);
if(!custom_paths(scan))
{
scan->path[0] = '/';
scan->path[1] = '\0';
traverse_path(scan);
update_screen(scan);
}
rb->lseek(scan->fd, 0, SEEK_SET);
rb->write(scan->fd, &scan->num_dirs, sizeof(int));
rb->close(scan->fd);
rb->splash(HZ, "Done");
}
static const char* list_get_name_cb(int selected_item, void* data,
char* buf, size_t buf_len)
{
struct rfa_scan *scan = (struct rfa_scan *)data;
rb->strlcpy(buf, scan->dirs->dir[selected_item], buf_len);
return buf;
}
static int load_dirs(struct rfa_scan *scan)
{
char *buf;
size_t buf_sz;
int fd = rb->open(RFA_LIST_DAT , O_RDONLY);
if (fd < 0)
return -1;
buf = rb->plugin_get_audio_buffer(&buf_sz);
if (!buf)
return -2;
rb->read(fd, buf, buf_sz);
rb->close(fd);
scan->dirs = (struct rfa_dirs *) buf;
return 0;
}
static int save_dirs(struct rfa_scan *scan)
{
int num_dirs = 0, i = 0;
int fd = rb->creat(RFA_LIST_DAT, 0666);
if (fd < 0)
{
rb->splash(HZ, "Could Not Open " RFA_LIST_DAT);
return -1;
}
rb->splash_progress_set_delay(HZ/2);
rb->write(fd, &num_dirs, sizeof(int));
for ( ; i < scan->dirs->count; i++)
{
/* (voiced) */
rb->splash_progress(i, scan->dirs->count, "%s", rb->str(LANG_WAIT));
if (scan->dirs->dir[i][0] != ' ')
{
num_dirs++;
rb->write(fd, scan->dirs->dir[i], sizeof(*scan->dirs->dir));
}
}
rb->lseek(fd, 0, SEEK_SET);
rb->write(fd, &num_dirs, sizeof(int));
rb->close(fd);
return 1;
}
static bool disable_dir(int i, struct rfa_scan *scan)
{
if (scan->dirs->dir[i][0] != ' ')
{
scan->dirty = true;
scan->dirs->dir[i][0] = ' ';
scan->dirs->dir[i][1] = '\0';
return true;
}
return false;
}
static void set_title(struct gui_synclist *lists, bool update, int num_dirs)
{
static int title_count;
static char title[15];
if (update)
title_count += num_dirs;
else if (num_dirs >= 0)
title_count = num_dirs;
rb->snprintf(title, sizeof title, title_count == 1 ?
"%d Folder" : "%d Folders", title_count);
rb->gui_synclist_set_title(lists, title, Icon_NOICON);
}
static int edit(struct rfa_scan *scan)
{
struct gui_synclist lists;
int button, i, j, selection;
/* load the dat file if not already done */
if (!scan->dirs || !scan->dirs->count)
{
if (!rb->file_exists(RFA_LIST_DAT))
generate(scan);
if ((i = load_dirs(scan)) != 0)
{
rb->splashf(HZ*2, "Could not load %s, err %d", RFA_LIST_DAT, i);
return -1;
}
}
scan->dirty = false;
rb->gui_synclist_init(&lists, list_get_name_cb, (void *) scan,
false, 1, NULL);
rb->gui_synclist_set_nb_items(&lists, scan->dirs->count);
rb->gui_synclist_select_item(&lists, 0);
set_title(&lists, false, scan->dirs->count);
while (true)
{
rb->gui_synclist_draw(&lists);
button = rb->get_action(CONTEXT_LIST,TIMEOUT_BLOCK);
if (rb->gui_synclist_do_button(&lists, &button))
continue;
selection = rb->gui_synclist_get_sel_pos(&lists);
switch (button)
{
case ACTION_STD_OK:
if (disable_dir(selection, scan))
set_title(&lists, true, -1);
break;
case ACTION_STD_CONTEXT:
{
int len;
MENUITEM_STRINGLIST(menu, "Remove", NULL,
"Remove Folder", "Remove Folder Tree");
switch (rb->do_menu(&menu, NULL, NULL, false))
{
case 0:
if (disable_dir(selection, scan))
set_title(&lists, true, -1);
break;
case 1:
rb->strcpy(scan->path, scan->dirs->dir[selection]);
len = rb->strlen(scan->path);
j = 0;
for (i = 0; i < scan->dirs->count; i++)
if (!rb->strncmp(scan->dirs->dir[i], scan->path, len))
if (disable_dir(i, scan))
j++;
set_title(&lists, true, -j);
break;
default:
set_title(&lists, false, -1);
break;
}
}
break;
case ACTION_STD_CANCEL:
{
MENUITEM_STRINGLIST(menu, "Save Changes?", NULL,
"Save", "Ignore Changes");
if (!scan->dirty)
return 0;
switch (rb->do_menu(&menu, NULL, NULL, false))
{
case 0:
save_dirs(scan);
/* fallthrough */
case 1:
scan->dirs = NULL;
return 0;
default:
set_title(&lists, false, -1);
break;
}
}
break;
}
}
return 0;
}
static int export(struct rfa_scan *scan)
{
int i, fd;
/* load the dat file if not already done */
if (!scan->dirs || !scan->dirs->count)
if ((i = load_dirs(scan)) != 0)
{
rb->splashf(HZ*2, "Could not load %s, err %d", RFA_LIST_DAT, i);
return 0;
}
if (scan->dirs->count <= 0)
{
rb->splashf(HZ*2, "no dirs in list file: %s", RFA_LIST_DAT);
return 0;
}
/* create and open the file */
fd = rb->creat(RFA_LIST_TXT, 0666);
if (fd < 0)
{
rb->splashf(HZ*4, "failed to open: fd %d, file %s",
fd, RFA_LIST_TXT);
return -1;
}
/* write each directory to file */
rb->splash_progress_set_delay(HZ/2);
for (i = 0; i < scan->dirs->count; i++)
{
/* (voiced) */
rb->splash_progress(i, scan->dirs->count, "%s", rb->str(LANG_WAIT));
if (scan->dirs->dir[i][0] != ' ')
rb->fdprintf(fd, "%s\n", scan->dirs->dir[i]);
}
rb->close(fd);
rb->splash(HZ, "Done");
return 1;
}
static int import(struct rfa_scan *scan)
{
char *buf;
size_t buf_sz;
int fd;
buf = rb->plugin_get_audio_buffer(&buf_sz);
if (!buf || buf_sz < sizeof(int))
{
rb->splash(HZ*2, "failed to get audio buffer");
return -1;
}
fd = rb->open(RFA_LIST_TXT, O_RDONLY);
if (fd < 0)
{
rb->splashf(HZ*2, "failed to open: %s", RFA_LIST_TXT);
return -1;
}
rb->splash(0, ID2P(LANG_WAIT));
/* set the list structure, and initialize count */
scan->dirs = (struct rfa_dirs *)buf;
scan->dirs->count = 0;
buf_sz -= sizeof(int);
while (buf_sz >= sizeof(*scan->dirs->dir))
{
int numread = rb->read_line(fd, scan->dirs->dir[scan->dirs->count],
sizeof(*scan->dirs->dir));
if ((numread) <= 0)
break;
/* if the line we read fills up buffer, the next call to read_line
will return an empty string for a remaining newline character. */
if (scan->dirs->dir[scan->dirs->count][0])
{
scan->dirs->count++;
buf_sz -= sizeof(*scan->dirs->dir);
}
}
rb->close(fd);
if (!scan->dirs->count)
load_dirs(scan);
else
save_dirs(scan);
rb->splash(HZ, "Done");
return scan->dirs->count;
}
static int play_shuffled(struct rfa_scan *scan)
{
struct playlist_insert_context pl_context;
int *order, i = 0, ret = -1;
size_t max_shuffle_size;
/* load the dat file if not already done */
if (!scan->dirs || !scan->dirs->count)
{
if (!rb->file_exists(RFA_LIST_DAT))
generate(scan);
if ((i = load_dirs(scan)) != 0)
{
rb->splashf(HZ*2, "Could not load %s, err %d", RFA_LIST_DAT, i);
return ret;
}
}
if (scan->dirs->count <= 0)
{
rb->splashf(HZ*2, "No dirs in list file: %s", RFA_LIST_DAT);
return ret;
}
/* get memory for shuffling */
order = rb->plugin_get_buffer(&max_shuffle_size);
max_shuffle_size /= sizeof(int);
if (!order || !max_shuffle_size)
{
rb->splashf(HZ*2, "Not enough memory for shuffling");
return ret;
}
/* shuffle the thing */
rb->srand(*rb->current_tick);
if(scan->dirs->count > (int) max_shuffle_size)
{
rb->splashf(HZ*2, "Too many folders: %d (room for %d)",
scan->dirs->count, (int) max_shuffle_size);
return ret;
}
for(i = 0; i < scan->dirs->count; i++)
order[i] = i;
for(i = scan->dirs->count - 1; i >= 0; i--)
{
/* the rand is from 0 to RAND_MAX, so adjust to our value range */
int candidate = rb->rand() % (i + 1);
/* now swap the values at the 'i' and 'candidate' positions */
int store = order[candidate];
order[candidate] = order[i];
order[i] = store;
}
if (!(rb->playlist_remove_all_tracks(NULL) == 0
&& rb->playlist_create(NULL, NULL) == 0))
{
rb->splashf(HZ*2, "Could not clear playlist");
return ret;
}
rb->splash_progress_set_delay(HZ/2);
/* Create insert context, disable progress for individual dirs */
if (rb->playlist_insert_context_create(NULL, &pl_context, PLAYLIST_INSERT_LAST,
false, false) < 0)
{
rb->playlist_insert_context_release(&pl_context);
rb->splashf(HZ*2, "Could not insert directories");
return ret;
}
for (i = 0; i < scan->dirs->count; i++)
{
/* (voiced) */
rb->splash_progress(i, scan->dirs->count, "%s (%s)",
rb->str(LANG_WAIT), rb->str(LANG_OFF_ABORT));
if (rb->action_userabort(TIMEOUT_NOBLOCK))
{
ret = 0;
break;
}
if (scan->dirs->dir[order[i]][0] != ' ')
{
ret = rb->playlist_insert_directory(NULL, scan->dirs->dir[order[i]],
PLAYLIST_INSERT_LAST, false,
false, &pl_context);
if (ret < 0)
{
if (ret == -2) /* User canceled */
break;
else if (rb->playlist_amount() >=
rb->global_settings->max_files_in_playlist)
{
rb->splashf(HZ, "Reached maximum number of files in playlist (%d)",
rb->global_settings->max_files_in_playlist);
ret = 1;
break;
}
else
rb->splashf(HZ, "Error inserting: %s", scan->dirs->dir[order[i]]);
}
ret = 1;
}
}
rb->playlist_insert_context_release(&pl_context);
if (ret > 0)
{
/* the core needs the audio buffer back in order to start playback. */
scan->dirs = NULL;
rb->plugin_release_audio_buffer();
rb->playlist_set_modified(NULL, true);
rb->playlist_start(0, 0, 0);
}
return ret;
}
static enum plugin_status main_menu(void)
{
static struct rfa_scan scan;
int selected = 0;
MENUITEM_STRINGLIST(menu, "Random Folder Advance", NULL,
"Play Shuffled",
"Scan Folders",
"Edit",
"Export to Text File",
"Import from Text File",
"Quit");
while (true)
{
switch (rb->do_menu(&menu, &selected, NULL, false))
{
case GO_TO_PREVIOUS_MUSIC:
return PLUGIN_GOTO_WPS;
case GO_TO_ROOT:
return PLUGIN_GOTO_ROOT;
case GO_TO_PREVIOUS:
return PLUGIN_OK;
case 0: /* Play Shuffled */
if (!rb->warn_on_pl_erase())
break;
#ifdef HAVE_ADJUSTABLE_CPU_FREQ
rb->cpu_boost(true);
#endif
int ret = play_shuffled(&scan);
#ifdef HAVE_ADJUSTABLE_CPU_FREQ
rb->cpu_boost(false);
#endif
if (ret > 0)
return PLUGIN_GOTO_WPS;
break;
case 1: /* Scan Folders */
#ifdef HAVE_ADJUSTABLE_CPU_FREQ
rb->cpu_boost(true);
#endif
generate(&scan);
#ifdef HAVE_ADJUSTABLE_CPU_FREQ
rb->cpu_boost(false);
#endif
#ifdef HAVE_BACKLIGHT
#ifdef HAVE_REMOTE_LCD
rb->remote_backlight_on();
#endif
rb->backlight_on();
#endif
break;
case 2: /* Edit */
#ifdef HAVE_ADJUSTABLE_CPU_FREQ
rb->cpu_boost(true);
#endif
edit(&scan);
#ifdef HAVE_ADJUSTABLE_CPU_FREQ
rb->cpu_boost(false);
#endif
#ifdef HAVE_BACKLIGHT
#ifdef HAVE_REMOTE_LCD
rb->remote_backlight_on();
#endif
rb->backlight_on();
#endif
break;
case 3: /* Export to Text File */
#ifdef HAVE_ADJUSTABLE_CPU_FREQ
rb->cpu_boost(true);
#endif
export(&scan);
#ifdef HAVE_ADJUSTABLE_CPU_FREQ
rb->cpu_boost(false);
#endif
#ifdef HAVE_BACKLIGHT
#ifdef HAVE_REMOTE_LCD
rb->remote_backlight_on();
#endif
rb->backlight_on();
#endif
break;
case 4: /* Import from Text File */
#ifdef HAVE_ADJUSTABLE_CPU_FREQ
rb->cpu_boost(true);
#endif
import(&scan);
#ifdef HAVE_ADJUSTABLE_CPU_FREQ
rb->cpu_boost(false);
#endif
#ifdef HAVE_BACKLIGHT
#ifdef HAVE_REMOTE_LCD
rb->remote_backlight_on();
#endif
rb->backlight_on();
#endif
break;
case 5: /* Quit */
return PLUGIN_OK;
}
}
return PLUGIN_OK;
}
enum plugin_status plugin_start(const void* parameter)
{
(void)parameter;
enum plugin_status ret;
#ifdef HAVE_TOUCHSCREEN
rb->touchscreen_set_mode(rb->global_settings->touch_mode);
#endif
FOR_NB_SCREENS(i)
rb->viewportmanager_theme_enable(i, true, NULL);
ret = main_menu();
FOR_NB_SCREENS(i)
rb->viewportmanager_theme_undo(i, false);
return ret;
}