A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd
at master 767 lines 22 kB view raw
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}