A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita
audio
rust
zig
deno
mpris
rockbox
mpd
1/***************************************************************************
2 * __________ __ ___.
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7 * \/ \/ \/ \/ \/
8 * $Id$
9 *
10 * Copyright (C) 2005 David Dent
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation; either version 2
15 * of the License, or (at your option) any later version.
16 *
17 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
18 * KIND, either express or implied.
19 *
20 ****************************************************************************/
21
22#include "plugin.h"
23#include "errno.h"
24#include "lib/playback_control.h"
25#include "lib/display_text.h"
26
27#define DEFAULT_FILES PLUGIN_APPS_DATA_DIR "/disktidy.config"
28#define CUSTOM_FILES PLUGIN_APPS_DATA_DIR "/disktidy_custom.config"
29#define LAST_RUN_STATS_FILE PLUGIN_APPS_DATA_DIR "/disktidy.stats"
30#define DIR_STACK_SIZE 25
31
32struct dir_info {
33 DIR *dir;
34 int path_length;
35 long size;
36};
37
38/* Store directory info when traversing file system */
39struct dir_stack {
40 struct dir_info dirs[DIR_STACK_SIZE];
41 int size;
42};
43
44struct run_statistics {
45 int files_removed; /* Number of files removed */
46 int dirs_removed; /* Number of directories removed */
47 int run_duration; /* Duration of last run in seconds */
48 double removed_size; /* Size of items removed */
49#if CONFIG_RTC
50 struct tm last_run_time; /* Last time disktidy was run */
51#endif
52};
53
54struct tidy_type {
55 char filestring[64];
56 int pre;
57 int post;
58 bool directory;
59 bool remove;
60} tidy_types[64];
61
62static struct run_statistics run_stats;
63static size_t tidy_type_count;
64static bool user_abort;
65static bool tidy_loaded_and_changed = false;
66static bool stats_file_exists = false;
67
68static void dir_stack_init(struct dir_stack *dstack)
69{
70 dstack->size = 0;
71}
72
73static inline int dir_stack_size(struct dir_stack *dstack)
74{
75 return dstack->size;
76}
77
78static inline bool dir_stack_push(struct dir_stack *dstack, struct dir_info dinfo)
79{
80 if (dstack->size == DIR_STACK_SIZE) {
81 return false;
82 }
83
84 dstack->dirs[dstack->size++] = dinfo;
85 return true;
86}
87
88static inline bool dir_stack_pop(struct dir_stack *dstack, struct dir_info *dinfo)
89{
90 if (dstack->size == 0) {
91 return false;
92 }
93
94 *dinfo = dstack->dirs[--dstack->size];
95 return true;
96}
97
98static void add_item(const char* name, int index)
99{
100 struct tidy_type *entry = &tidy_types[index];
101 rb->strcpy(entry->filestring, name);
102 if (name[rb->strlen(name)-1] == '/')
103 {
104 entry->directory = true;
105 entry->filestring[rb->strlen(name)-1] = '\0';
106 }
107 else
108 entry->directory = false;
109
110 char *a = rb->strchr(entry->filestring, '*');
111 if (a)
112 {
113 entry->pre = a - entry->filestring;
114 entry->post = rb->strlen(a+1);
115 }
116 else
117 {
118 entry->pre = -1;
119 entry->post = -1;
120 }
121}
122
123static int find_file_string(const char *file, char *last_group)
124{
125 char temp[MAX_PATH];
126 int idx_last_group = -1;
127 bool folder = false;
128 rb->strcpy(temp, file);
129 if (temp[rb->strlen(temp)-1] == '/')
130 {
131 folder = true;
132 temp[rb->strlen(temp)-1] = '\0';
133 }
134
135 for (unsigned i = 0; i < tidy_type_count; i++)
136 if (!rb->strcmp(tidy_types[i].filestring, temp) && folder == tidy_types[i].directory)
137 return i;
138 else if (!rb->strcmp(tidy_types[i].filestring, last_group))
139 idx_last_group = i;
140
141 if (file[0] == '<' || idx_last_group == -1)
142 return tidy_type_count;
143
144
145 /* not found, so insert it into its group */
146 for (unsigned i=idx_last_group; i<tidy_type_count; i++)
147 if (tidy_types[i].filestring[0] == '<')
148 {
149 idx_last_group = i;
150 break;
151 }
152
153 /* shift items up one */
154 for (int i=tidy_type_count;i>idx_last_group;i--)
155 rb->memcpy(&tidy_types[i], &tidy_types[i-1], sizeof(struct tidy_type));
156
157 tidy_type_count++;
158 add_item(file, idx_last_group+1);
159 return idx_last_group+1;
160}
161
162static void tidy_load_file(const char* file)
163{
164 int fd = rb->open(file, O_RDONLY);
165 char buf[MAX_PATH], *str, *remove;
166 char last_group[MAX_PATH] = "";
167 if (fd < 0)
168 return;
169
170 while ((tidy_type_count < sizeof(tidy_types) / sizeof(tidy_types[0])) && rb->read_line(fd, buf, MAX_PATH))
171 {
172 if (!rb->settings_parseline(buf, &str, &remove))
173 continue;
174
175 if (*str == '\\') /* escape first character ? */
176 str++;
177 unsigned i = find_file_string(str, last_group);
178
179 tidy_types[i].remove = !rb->strcmp(remove, "yes");
180
181 if (i >= tidy_type_count)
182 {
183 i = tidy_type_count;
184 add_item(str, i);
185 tidy_type_count++;
186 }
187 if (str[0] == '<')
188 rb->strcpy(last_group, str);
189 }
190 rb->close(fd);
191}
192
193static bool save_run_stats(void)
194{
195 int fd = rb->open(LAST_RUN_STATS_FILE, O_WRONLY|O_CREAT, 0666);
196
197 if (fd < 0) {
198 return false;
199 }
200
201 bool save_success = rb->write(fd, &run_stats,
202 sizeof(struct run_statistics)) > 0;
203
204 rb->close(fd);
205
206 return save_success;
207}
208
209static bool load_run_stats(void)
210{
211 int fd = rb->open(LAST_RUN_STATS_FILE, O_RDONLY);
212
213 if (fd < 0) {
214 return false;
215 }
216
217 bool load_success = rb->read(fd, &run_stats,
218 sizeof(struct run_statistics)) == sizeof(struct run_statistics);
219
220 rb->close(fd);
221
222 return load_success;
223}
224
225static enum plugin_status display_run_stats(void)
226{
227 if (!load_run_stats()) {
228 rb->splash(HZ * 2, "Unable to load last run stats");
229 return PLUGIN_OK;
230 }
231
232#if CONFIG_RTC
233 static const char *months[] = {
234 "Jan", "Feb", "Mar", "Apr", "May", "Jun",
235 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
236 };
237#endif
238 static const char *size_units[] = {
239 "B", "KB", "MB", "GB", "TB", "PB"
240 };
241
242 int magnitude = 0;
243 double rm_size = run_stats.removed_size;
244
245 while (rm_size >= 1000) {
246 rm_size /= 1024;
247 magnitude++;
248 }
249
250 char total_removed[8];
251 rb->snprintf(total_removed, sizeof(total_removed), "%d",
252 run_stats.files_removed + run_stats.dirs_removed);
253
254 char files_removed[8];
255 rb->snprintf(files_removed, sizeof(files_removed), "%d",
256 run_stats.files_removed);
257
258 char dirs_removed[8];
259 rb->snprintf(dirs_removed, sizeof(dirs_removed), "%d",
260 run_stats.dirs_removed);
261
262 char removed_size[11];
263 rb->snprintf(removed_size, sizeof(removed_size), "(%d.%d%s)",
264 (int)rm_size, (int)((rm_size - (int)rm_size) * 100),
265 size_units[magnitude]);
266
267 char run_time[12];
268 rb->snprintf(run_time, sizeof(run_time), "in %02d:%02d:%02d",
269 run_stats.run_duration / 3600, run_stats.run_duration / 60,
270 run_stats.run_duration % 60);
271
272#if CONFIG_RTC
273 char last_run[18];
274 rb->snprintf(last_run, sizeof(last_run), "%02d:%02d %d/%s/%d",
275 run_stats.last_run_time.tm_hour,
276 run_stats.last_run_time.tm_min, run_stats.last_run_time.tm_mday,
277 months[run_stats.last_run_time.tm_mon],
278 2000 + (run_stats.last_run_time.tm_year % 100));
279#endif
280
281 char* last_run_text[] = {
282#if CONFIG_RTC
283 last_run, "",
284#endif
285 total_removed, "removed", removed_size, "",
286 files_removed, run_stats.files_removed == 1 ? "file" : "files,",
287 dirs_removed , run_stats.dirs_removed == 1 ? "dir" : "dirs", "",
288 run_time , "",
289 };
290
291 static struct style_text display_style[] = {
292#if CONFIG_RTC
293 { 0, TEXT_CENTER },
294#endif
295 LAST_STYLE_ITEM
296 };
297
298 struct viewport vp;
299 rb->viewport_set_defaults(&vp, SCREEN_MAIN);
300
301 if (display_text(ARRAYLEN(last_run_text), last_run_text,
302 display_style, &vp, false)) {
303 return PLUGIN_USB_CONNECTED;
304 }
305 while (true) /* keep info on screen until cancelled */
306 {
307 int button = rb->get_action(CONTEXT_STD, HZ/2);
308 if (button == ACTION_STD_CANCEL || button == ACTION_STD_MENU)
309 break;
310
311 if (rb->default_event_handler(button) == SYS_USB_CONNECTED)
312 return PLUGIN_USB_CONNECTED;
313 }
314 return PLUGIN_OK;
315}
316
317static bool match(struct tidy_type *tidy_type, const char *string, int len)
318{
319 char *pattern = tidy_type->filestring;
320
321 if (tidy_type->pre < 0) /* no '*', just compare. */
322 return !rb->strcmp(pattern, string);
323
324 /* pattern is too long for the string. avoid 'ab*bc' matching 'abc'. */
325 if (len < tidy_type->pre + tidy_type->post)
326 return false;
327
328 /* pattern has '*', compare former part of '*' to the begining of
329 the string and compare next part of '*' to the end of string. */
330 return !rb->strncmp(pattern, string, tidy_type->pre) &&
331 !rb->strcmp(pattern + tidy_type->pre + 1, string + len - tidy_type->post);
332}
333
334static bool tidy_remove_item(const char *item, int attr)
335{
336 for (struct tidy_type *t = &tidy_types[0]; t < &tidy_types[tidy_type_count]; t++)
337 if (match(t, item, rb->strlen(item)))
338 return t->remove && ((!!(attr&ATTR_DIRECTORY)) == t->directory);
339
340 return false;
341}
342
343static void tidy_lcd_status(const char *name, struct viewport *vp)
344{
345 static long next_tick;
346
347 if (TIME_AFTER(next_tick, *rb->current_tick))
348 return;
349
350 next_tick = *rb->current_tick + HZ/10;
351
352 struct screen *display = rb->screens[SCREEN_MAIN];
353 struct viewport *last_vp = display->set_viewport(vp);
354
355 display->clear_viewport();
356 display->puts(0, 0, "Cleaning...");
357 display->puts(0, 1, name);
358 display->putsf(0, 2, "%d items removed",
359 run_stats.files_removed + run_stats.dirs_removed);
360 display->update_viewport();
361
362 display->set_viewport(last_vp);
363}
364
365static int tidy_path_append_entry(char *path, struct dirent *entry, int *path_length)
366{
367 int name_len = rb->strlen(entry->d_name);
368 /* for the special case of path="/" this is one bigger but it's not a problem */
369 int new_length = *path_length + name_len + 1;
370
371 /* check overflow (keep space for trailing zero) */
372 if(new_length >= MAX_PATH)
373 return 0;
374
375 /* special case for path <> "/" */
376 if(rb->strcmp(path, "/") != 0)
377 {
378 rb->strcat(path + *path_length, "/");
379 (*path_length)++;
380 }
381 /* strcat is unsafe but the previous check normally avoid any problem */
382 /* use path_length to optimise */
383
384 rb->strcat(path + *path_length, entry->d_name);
385 *path_length += name_len;
386
387 return 1;
388}
389
390static void tidy_path_remove_entry(char *path, int old_path_length, int *path_length)
391{
392 path[old_path_length] = '\0';
393 *path_length = old_path_length;
394}
395
396/* Cleanup when user abort or USB event during tidy_clean */
397static void tidy_clean_cleanup(struct dir_stack *dstack, DIR *dir) {
398 struct dir_info dinfo;
399
400 rb->closedir(dir);
401
402 while (dir_stack_pop(dstack, &dinfo)) {
403 rb->closedir(dinfo.dir);
404 }
405}
406
407/* Perform iterative depth-first search for files to clean */
408static enum plugin_status tidy_clean(char *path, int *path_length) {
409 struct dir_stack dstack;
410 struct dir_info dinfo;
411 struct dirent *entry;
412 struct dirinfo info;
413 struct viewport vp;
414 rb->viewport_set_defaults(&vp, SCREEN_MAIN);
415 DIR *dir, *dir_test;
416 /* Set to true when directory and its contents are to be deleted */
417 bool rm_all = false;
418 /* Used to mark where rm_all starts and ends */
419 int rm_all_start_depth = 0;
420 int button;
421 bool remove;
422 int old_path_length;
423
424 dir_stack_init(&dstack);
425 dir = rb->opendir(path);
426
427 if (!dir) {
428 /* If can't open / then immediately stop */
429 return PLUGIN_ERROR;
430 }
431
432 dinfo.dir = dir;
433 dinfo.path_length = *path_length;
434 /* Size only used when deleting directory so value here doesn't matter */
435 dinfo.size = 0;
436
437 dir_stack_push(&dstack, dinfo);
438
439 while (dir_stack_pop(&dstack, &dinfo)) {
440 /* Restore path to poped dir */
441 tidy_path_remove_entry(path, dinfo.path_length, path_length);
442 dir = dinfo.dir;
443 tidy_lcd_status(path, &vp);
444
445 while ((entry = rb->readdir(dir))) {
446 /* Check for user input and usb connect */
447 button = rb->get_action(CONTEXT_STD, TIMEOUT_NOBLOCK);
448
449 if (button == ACTION_STD_CANCEL) {
450 tidy_clean_cleanup(&dstack, dir);
451 user_abort = true;
452 return PLUGIN_OK;
453 }
454 if (rb->default_event_handler(button) == SYS_USB_CONNECTED) {
455 tidy_clean_cleanup(&dstack, dir);
456 return PLUGIN_USB_CONNECTED;
457 }
458
459 rb->yield();
460
461 old_path_length = *path_length;
462 info = rb->dir_get_info(dir, entry);
463
464 remove = rm_all || tidy_remove_item(entry->d_name, info.attribute);
465
466 if (info.attribute & ATTR_DIRECTORY) {
467 if (rb->strcmp(entry->d_name, ".") == 0 ||
468 rb->strcmp(entry->d_name, "..") == 0) {
469 continue;
470 }
471
472 if (!remove) {
473 /* Get absolute path, returns an error if path is too long */
474 if (!tidy_path_append_entry(path, entry, path_length)) {
475 continue; /* Silent error */
476 }
477
478 dinfo.dir = dir;
479 dinfo.path_length = old_path_length;
480 dinfo.size = 0;
481
482 /* This directory doesn't need to be deleted, so try to add
483 the current directory we're in to the stack and search
484 this one for files/directories to delete. If we can't
485 add the current directory to the stack or open the new
486 directory to search then continue on in the current
487 directory. */
488 if (dir_stack_push(&dstack, dinfo)) {
489 dir_test = rb->opendir(path);
490
491 if (dir_test) {
492 dir = dir_test;
493 tidy_lcd_status(path, &vp);
494 }
495 }
496 }
497 }
498
499 if (!remove) {
500 continue;
501 }
502
503 /* Get absolute path, returns an error if path is too long */
504 if (!tidy_path_append_entry(path, entry, path_length)) {
505 continue; /* Silent error */
506 }
507
508 if (info.attribute & ATTR_DIRECTORY) {
509 /* Remove this directory and all files/directories it contains */
510 dinfo.dir = dir;
511 dinfo.path_length = old_path_length;
512 dinfo.size = info.size;
513
514 if (dir_stack_push(&dstack, dinfo)) {
515 dir_test = rb->opendir(path);
516
517 if (dir_test) {
518 dir = dir_test;
519
520 if (!rm_all) {
521 rm_all = true;
522 rm_all_start_depth = dir_stack_size(&dstack);
523 }
524
525 tidy_lcd_status(path, &vp);
526 }
527 }
528 } else {
529 run_stats.files_removed++;
530 run_stats.removed_size += info.size;
531 rb->remove(path);
532
533 /* Restore path */
534 tidy_path_remove_entry(path, old_path_length, path_length);
535 }
536 }
537
538 rb->closedir(dir);
539
540 if (rm_all) {
541 /* Check if returned to the directory we started rm_all from */
542 if (rm_all_start_depth == dir_stack_size(&dstack)) {
543 rm_all = false;
544 }
545
546 rb->rmdir(path);
547 run_stats.dirs_removed++;
548 run_stats.removed_size += dinfo.size;
549 }
550 }
551
552 return PLUGIN_OK;
553}
554
555static enum plugin_status tidy_do(void)
556{
557 /* clean disk and display num of items removed */
558 char path[MAX_PATH];
559
560 run_stats.files_removed = 0;
561 run_stats.dirs_removed = 0;
562 run_stats.removed_size = 0;
563 long start_time = *rb->current_tick;
564
565#if CONFIG_RTC
566 run_stats.last_run_time = *rb->get_time();
567#endif
568
569#ifdef HAVE_ADJUSTABLE_CPU_FREQ
570 rb->cpu_boost(true);
571#endif
572
573 rb->strcpy(path, "/");
574 int path_length = rb->strlen(path);
575 enum plugin_status status = tidy_clean(path, &path_length);
576
577#ifdef HAVE_ADJUSTABLE_CPU_FREQ
578 rb->cpu_boost(false);
579#endif
580
581 run_stats.run_duration = (*rb->current_tick - start_time) / HZ;
582 stats_file_exists = save_run_stats();
583
584 if (status == PLUGIN_OK)
585 {
586 if (user_abort)
587 {
588 user_abort = false;
589 rb->splash(HZ, ID2P(LANG_CANCEL));
590 }
591 }
592
593 return status;
594}
595
596static enum themable_icons get_icon(int item, void * data)
597{
598 (void)data;
599 if (tidy_types[item].filestring[0] == '<') /* special type */
600 return Icon_Folder;
601 else if (tidy_types[item].remove)
602 return Icon_Cursor;
603 else
604 return Icon_NOICON;
605}
606
607static const char* get_name(int selected_item, void * data,
608 char * buffer, size_t buffer_len)
609{
610 (void)data;
611 if (tidy_types[selected_item].directory)
612 {
613 rb->snprintf(buffer, buffer_len, "%s/",
614 tidy_types[selected_item].filestring);
615 return buffer;
616 }
617 return tidy_types[selected_item].filestring;
618}
619
620static int list_action_callback(int action, struct gui_synclist *lists)
621{
622 if (action != ACTION_STD_OK)
623 return action;
624
625 unsigned selection = rb->gui_synclist_get_sel_pos(lists);
626 if (tidy_types[selection].filestring[0] == '<')
627 {
628 bool all = !rb->strcmp(tidy_types[selection].filestring, "< ALL >");
629 bool none= !rb->strcmp(tidy_types[selection].filestring, "< NONE >");
630
631 if (all || none)
632 {
633 for (unsigned i=0; i<tidy_type_count; i++)
634 if (tidy_types[i].filestring[0] != '<')
635 tidy_types[i].remove = all;
636 }
637 else /* toggle all untill the next <> */
638 while (++selection < tidy_type_count && tidy_types[selection].filestring[0] != '<')
639 tidy_types[selection].remove = !tidy_types[selection].remove;
640 }
641 else
642 tidy_types[selection].remove = !tidy_types[selection].remove;
643 tidy_loaded_and_changed = true;
644 return ACTION_REDRAW;
645}
646
647static bool tidy_types_selected(void)
648{
649 for (unsigned int i = 0; i < tidy_type_count; i++) {
650 if (tidy_types[i].filestring[0] != '<' && tidy_types[i].remove) {
651 return true;
652 }
653 }
654
655 return false;
656}
657
658static int disktidy_menu_cb(int action,
659 const struct menu_item_ex *this_item,
660 struct gui_synclist *this_list)
661{
662 (void)this_list;
663 int item = ((intptr_t)this_item);
664
665 if (action == ACTION_REQUEST_MENUITEM &&
666 !stats_file_exists && item == 2) {
667
668 return ACTION_EXIT_MENUITEM;
669 }
670
671 return action;
672}
673
674static enum plugin_status tidy_lcd_menu(void)
675{
676 enum plugin_status disktidy_status = PLUGIN_OK;
677 bool exit = false;
678 int selection = 0;
679 struct simplelist_info list;
680
681 MENUITEM_STRINGLIST(menu, "Disktidy", disktidy_menu_cb,
682 "Start Cleaning", "Files to Clean", "Last Run Stats",
683 "Playback Control", "Quit");
684
685 while (!exit && disktidy_status == PLUGIN_OK) {
686 switch(rb->do_menu(&menu, &selection, NULL, false)) {
687 case 0:
688 if (tidy_types_selected()) {
689 disktidy_status = tidy_do();
690 if (disktidy_status == PLUGIN_OK)
691 disktidy_status = display_run_stats();
692 } else {
693 rb->splash(HZ * 2, "Select at least one file type to clean");
694 }
695
696 break;
697 case 1:
698 rb->simplelist_info_init(&list, "Files to Clean",
699 tidy_type_count, NULL);
700 list.get_icon = get_icon;
701 list.get_name = get_name;
702 list.action_callback = list_action_callback;
703 bool show_icons = rb->global_settings->show_icons;
704 rb->global_settings->show_icons = true;
705 rb->simplelist_show_list(&list);
706 rb->global_settings->show_icons = show_icons;
707 break;
708 case 2:
709 disktidy_status = display_run_stats();
710 break;
711 case 3:
712 if (playback_control(NULL)) {
713 disktidy_status = PLUGIN_USB_CONNECTED;
714 }
715
716 break;
717 default:
718 exit = true;
719 break;
720 }
721 }
722
723 return disktidy_status;
724}
725
726/* Creates a file and writes information about what files to
727 delete and what to keep to it.
728*/
729static void save_config(void)
730{
731 int fd = rb->creat(CUSTOM_FILES, 0666);
732 if (fd < 0)
733 return;
734
735 for (unsigned i=0; i<tidy_type_count; i++)
736 rb->fdprintf(fd, "%s%s%s: %s\n",
737 tidy_types[i].filestring[0] == '#' ? "\\" : "",
738 tidy_types[i].filestring,
739 tidy_types[i].directory ? "/" : "",
740 tidy_types[i].remove ? "yes" : "no");
741 rb->close(fd);
742}
743
744/* this is the plugin entry point */
745enum plugin_status plugin_start(const void* parameter)
746{
747 (void)parameter;
748
749 tidy_load_file(DEFAULT_FILES);
750 tidy_load_file(CUSTOM_FILES);
751
752 if (tidy_type_count == 0)
753 {
754 rb->splash(3*HZ, "Missing disktidy.config file");
755 return PLUGIN_ERROR;
756 }
757
758 stats_file_exists = rb->file_exists(LAST_RUN_STATS_FILE);
759
760 enum plugin_status disktidy_status = tidy_lcd_menu();
761
762 if (tidy_loaded_and_changed) {
763 save_config();
764 }
765
766 return disktidy_status;
767}