A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd
at master 655 lines 19 kB view raw
1/*************************************************************************** 2 * __________ __ ___. 3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___ 4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / 5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < 6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ 7 * \/ \/ \/ \/ \/ 8 * 9 * Copyright (C) 2012 Jonathan Gordon 10 * Copyright (C) 2012 Thomas Martitz 11* * Copyright (C) 2021 William Wilgus 12 * 13 * This program is free software; you can redistribute it and/or 14 * modify it under the terms of the GNU General Public License 15 * as published by the Free Software Foundation; either version 2 16 * of the License, or (at your option) any later version. 17 * 18 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY 19 * KIND, either express or implied. 20 * 21 ****************************************************************************/ 22#include "plugin.h" 23#ifdef ROCKBOX_HAS_LOGF 24#define logf rb->logf 25#else 26#define logf(...) do { } while(0) 27#endif 28 29/* 30 * Order for changing child states: 31 * 1) expand folder (skip to 3 if empty, skip to 4 if cannot be opened) 32 * 2) collapse and select 33 * 3) unselect (skip to 1) 34 * 4) do nothing 35 */ 36 37enum child_state { 38 EXPANDED, 39 SELECTED, 40 COLLAPSED, 41 EACCESS, 42}; 43 44struct child { 45 char* name; 46 struct folder *folder; 47 enum child_state state; 48}; 49 50struct folder { 51 char *name; 52 struct child *children; 53 struct folder* previous; 54 uint16_t children_count; 55 uint16_t depth; 56}; 57 58static char *buffer_front, *buffer_end; 59 60static struct 61{ 62 int32_t len; /* keeps count versus maxlen to give buffer full notification */ 63 uint32_t val; /* hash of all selected items */ 64 char buf[3];/* address used as identifier -- only \0 written to it */ 65 char maxlen_exceeded; /*0,1*/ 66} hashed; 67 68static inline void get_hash(const char *key, uint32_t *hash, int len) 69{ 70 *hash = rb->crc_32(key, len, *hash); 71} 72 73static char* folder_alloc(size_t size) 74{ 75 char* retval; 76 /* 32-bit aligned */ 77 size = ALIGN_UP(size, 4); 78 if (buffer_front + size > buffer_end) 79 { 80 return NULL; 81 } 82 retval = buffer_front; 83 buffer_front += size; 84 return retval; 85} 86 87static char* folder_alloc_from_end(size_t size) 88{ 89 if (buffer_end - size < buffer_front) 90 { 91 return NULL; 92 } 93 buffer_end -= size; 94 return buffer_end; 95} 96#if 0 97/* returns the buffer size required to store the path + \0 */ 98static int get_full_pathsz(struct folder *start) 99{ 100 int reql = 0; 101 struct folder *next = start; 102 do 103 { 104 reql += rb->strlen(next->name) + 1; 105 } while ((next = next->previous)); 106 107 if (start->name[0] != '/') reql--; 108 if (--reql < 0) reql = 0; 109 return reql; 110} 111#endif 112 113static size_t get_full_path(struct folder *start, char *dst, size_t dst_sz) 114{ 115 size_t pos = 0; 116 struct folder *prev, *cur = NULL, *next = start; 117 dst[0] = '\0'; /* for rb->strlcat to do its thing */ 118 /* First traversal R->L mutate nodes->previous to point at child */ 119 while (next->previous != NULL) /* stop at the root */ 120 { 121#define PATHMUTATE() \ 122 ({ \ 123 prev = cur; \ 124 cur = next; \ 125 next = cur->previous;\ 126 cur->previous = prev; \ 127 }) 128 PATHMUTATE(); 129 } 130 /*swap the next and cur nodes to reverse direction */ 131 prev = next; 132 next = cur; 133 cur = prev; 134 /* Second traversal L->R mutate nodes->previous to point back at parent 135 * copy strings to buf as they go by */ 136 while (next != NULL) 137 { 138 PATHMUTATE(); 139 pos = rb->strlcat(dst, cur->name, dst_sz); 140 /* do not append slash to paths starting with slash */ 141 if (cur->name[0] != '/') 142 pos = rb->strlcat(dst, "/", dst_sz); 143 } 144 logf("get_full_path: (%d)[%s]", (int)pos, dst); 145 return pos; 146#undef PATHMUTATE 147} 148 149/* support function for rb->qsort() */ 150static int compare(const void* p1, const void* p2) 151{ 152 struct child *left = (struct child*)p1; 153 struct child *right = (struct child*)p2; 154 return rb->strcasecmp(left->name, right->name); 155} 156 157static struct folder* load_folder(struct folder* parent, char *folder) 158{ 159 DIR *dir; 160 char fullpath[MAX_PATH]; 161 162 struct dirent *entry; 163 int child_count = 0; 164 char *first_child = NULL; 165 size_t len = 0; 166 167 struct folder* this = (struct folder*)folder_alloc(sizeof(struct folder)); 168 if (this == NULL) 169 goto fail; 170 171 if (parent) 172 { 173 len = get_full_path(parent, fullpath, sizeof(fullpath)); 174 if (len >= sizeof(fullpath)) 175 goto fail; 176 } 177 rb->strlcpy(&fullpath[len], folder, sizeof(fullpath) - len); 178 logf("load_folder: [%s]", fullpath); 179 180 dir = rb->opendir(fullpath); 181 if (dir == NULL) 182 goto fail; 183 this->previous = parent; 184 this->name = folder; 185 this->children = NULL; 186 this->children_count = 0; 187 if (parent) 188 this->depth = parent->depth + 1; 189 190 while ((entry = rb->readdir(dir))) { 191 /* skip anything not a directory */ 192 if ((rb->dir_get_info(dir, entry).attribute & ATTR_DIRECTORY) == 0) { 193 continue; 194 } 195 /* skip . and .. */ 196 char *dn = entry->d_name; 197 if ((dn[0] == '.') && (dn[1] == '\0' || (dn[1] == '.' && dn[2] == '\0'))) 198 continue; 199 /* copy entry name to end of buffer, save pointer */ 200 int len = rb->strlen((char *)entry->d_name); 201 char *name = folder_alloc_from_end(len+1); /*for NULL*/ 202 if (name == NULL) 203 { 204 rb->closedir(dir); 205 goto fail; 206 } 207 memcpy(name, (char *)entry->d_name, len+1); 208 child_count++; 209 first_child = name; 210 } 211 rb->closedir(dir); 212 /* now put the names in the array */ 213 this->children = (struct child*)folder_alloc(sizeof(struct child) * child_count); 214 215 if (this->children == NULL) 216 goto fail; 217 218 while (child_count) 219 { 220 struct child *child = &this->children[this->children_count++]; 221 child->name = first_child; 222 child->folder = NULL; 223 child->state = COLLAPSED; 224 while(*first_child++ != '\0'){};/* move to next name entry */ 225 child_count--; 226 } 227 rb->qsort(this->children, this->children_count, sizeof(struct child), compare); 228 229 return this; 230fail: 231 return NULL; 232} 233 234struct folder* load_root(void) 235{ 236 static struct child root_child; 237 /* reset the root for each call */ 238 root_child.name = "/"; 239 root_child.folder = NULL; 240 root_child.state = COLLAPSED; 241 242 static struct folder root = { 243 .name = "", 244 .children = &root_child, 245 .children_count = 1, 246 .depth = 0, 247 .previous = NULL, 248 }; 249 250 return &root; 251} 252 253static int count_items(struct folder *start) 254{ 255 int count = 0; 256 int i; 257 258 for (i=0; i<start->children_count; i++) 259 { 260 struct child *foo = &start->children[i]; 261 if (foo->state == EXPANDED) 262 count += count_items(foo->folder); 263 count++; 264 } 265 return count; 266} 267 268static struct child* find_index(struct folder *start, int index, struct folder **parent) 269{ 270 int i = 0; 271 *parent = NULL; 272 273 while (i < start->children_count) 274 { 275 struct child *foo = &start->children[i]; 276 if (i == index) 277 { 278 *parent = start; 279 return foo; 280 } 281 i++; 282 if (foo->state == EXPANDED) 283 { 284 struct child *bar = find_index(foo->folder, index - i, parent); 285 if (bar) 286 { 287 return bar; 288 } 289 index -= count_items(foo->folder); 290 } 291 } 292 return NULL; 293} 294 295static const char * folder_get_name(int selected_item, void * data, 296 char * buffer, size_t buffer_len) 297{ 298 struct folder *root = (struct folder*)data; 299 struct folder *parent; 300 struct child *this = find_index(root, selected_item , &parent); 301 302 char *buf = buffer; 303 if ((int)buffer_len > parent->depth) 304 { 305 int i = parent->depth; 306 while(--i > 0) /* don't indent the parent /folders */ 307 *buf++ = '\t'; 308 } 309 *buf = '\0'; 310 rb->strlcat(buffer, this->name, buffer_len); 311 312 if (this->state == EACCESS) 313 { /* append error message to the entry if unaccessible */ 314 size_t len = rb->strlcat(buffer, " ( ", buffer_len); 315 if (buffer_len > len) 316 { 317 rb->snprintf(&buffer[len], buffer_len - len, rb->str(LANG_READ_FAILED), ")"); 318 } 319 } 320 321 return buffer; 322} 323 324static enum themable_icons folder_get_icon(int selected_item, void * data) 325{ 326 struct folder *root = (struct folder*)data; 327 struct folder *parent; 328 struct child *this = find_index(root, selected_item, &parent); 329 330 switch (this->state) 331 { 332 case SELECTED: 333 return Icon_Cursor; 334 case COLLAPSED: 335 return Icon_Folder; 336 case EXPANDED: 337 return Icon_Submenu; 338 case EACCESS: 339 return Icon_Questionmark; 340 } 341 return Icon_NOICON; 342} 343 344static int child_set_state_expand(struct child *this, struct folder *parent) 345{ 346 int newstate = EACCESS; 347 if (this->folder == NULL) 348 this->folder = load_folder(parent, this->name); 349 350 if (this->folder != NULL) 351 { 352 if(this->folder->children_count == 0) 353 newstate = SELECTED; 354 else 355 newstate = EXPANDED; 356 } 357 this->state = newstate; 358 return newstate; 359} 360 361static int folder_action_callback(int action, struct gui_synclist *list) 362{ 363 struct folder *root = (struct folder*)list->data; 364 struct folder *parent; 365 struct child *this = find_index(root, list->selected_item, &parent), *child; 366 int i; 367 368 if (action == ACTION_STD_OK) 369 { 370 switch (this->state) 371 { 372 case EXPANDED: 373 this->state = SELECTED; 374 break; 375 case SELECTED: 376 this->state = COLLAPSED; 377 break; 378 case COLLAPSED: 379 child_set_state_expand(this, parent); 380 break; 381 case EACCESS: 382 /* cannot open, do nothing */ 383 return action; 384 } 385 action = ACTION_REDRAW; 386 } 387 else if (action == ACTION_STD_CONTEXT) 388 { 389 switch (this->state) 390 { 391 case EXPANDED: 392 for (i = 0; i < this->folder->children_count; i++) 393 { 394 child = &this->folder->children[i]; 395 switch (child->state) 396 { 397 case SELECTED: 398 case EXPANDED: 399 child->state = COLLAPSED; 400 break; 401 case COLLAPSED: 402 child->state = SELECTED; 403 break; 404 case EACCESS: 405 break; 406 } 407 } 408 break; 409 case SELECTED: 410 case COLLAPSED: 411 if (child_set_state_expand(this, parent) != EACCESS) 412 { 413 for (i = 0; i < (this->folder->children_count); i++) 414 { 415 child = &this->folder->children[i]; 416 child->state = SELECTED; 417 } 418 } 419 break; 420 case EACCESS: 421 /* cannot open, do nothing */ 422 return action; 423 } 424 action = ACTION_REDRAW; 425 } 426 if (action == ACTION_REDRAW) 427 list->nb_items = count_items(root); 428 return action; 429} 430 431static struct child* find_from_filename(const char* filename, struct folder *root) 432{ 433 if (!root) 434 return NULL; 435 const char *slash = rb->strchr(filename, '/'); 436 struct child *this; 437 438 /* filenames beginning with a / are specially treated as the 439 * loop below can't handle them. they can only occur on the first, 440 * and not recursive, calls to this function.*/ 441 if (filename[0] == '/') /* in the loop nothing starts with '/' */ 442 { 443 logf("find_from_filename [%s]", filename); 444 /* filename begins with /. in this case root must be the 445 * top level folder */ 446 this = &root->children[0]; 447 if (filename[1] == '\0') 448 { /* filename == "/" */ 449 return this; 450 } 451 else /* filename == "/XXX/YYY". cascade down */ 452 goto cascade; 453 } 454 455 for (int i = 0; i < root->children_count; i++) 456 { 457 this = &root->children[i]; 458 /* when slash == NULL n will be really large but \0 stops the compare */ 459 if (rb->strncasecmp(this->name, filename, slash - filename) == 0) 460 { 461 if (slash == NULL) 462 { /* filename == XXX */ 463 return this; 464 } 465 else 466 goto cascade; 467 } 468 } 469 return NULL; 470 471cascade: 472 /* filename == XXX/YYY. cascade down */ 473 child_set_state_expand(this, root); 474 while (slash[0] == '/') slash++; /* eat slashes */ 475 return find_from_filename(slash, this->folder); 476} 477 478static int select_paths(struct folder* root, const char* filenames) 479{ 480 /* Takes a list of filenames in a ':' delimited string 481 splits filenames at the ':' character loads each into buffer 482 selects each file in the folder list 483 484 if last item or only item the rest of the string is copied to the buffer 485 *End the last item WITHOUT the ':' character /.rockbox/eqs:/.rockbox/wps\0* 486 */ 487 char buf[MAX_PATH]; 488 const int buflen = sizeof(buf); 489 490 const char *fnp = filenames; 491 const char *lastfnp = fnp; 492 const char *sstr; 493 off_t len; 494 495 while (fnp) 496 { 497 fnp = rb->strchr(fnp, ':'); 498 if (fnp) 499 { 500 len = fnp - lastfnp; 501 fnp++; 502 } 503 else /* no ':' get the rest of the string */ 504 len = rb->strlen(lastfnp); 505 506 sstr = lastfnp; 507 lastfnp = fnp; 508 if (len <= 0 || len > buflen) 509 continue; 510 rb->strlcpy(buf, sstr, len + 1); 511 struct child *item = find_from_filename(buf, root); 512 if (item) 513 item->state = SELECTED; 514 } 515 516 return 0; 517} 518 519static void save_folders_r(struct folder *root, char* dst, size_t maxlen, size_t buflen) 520{ 521 size_t len; 522 struct folder *curfolder; 523 char* name; 524 525 for (int i = 0; i < root->children_count; i++) 526 { 527 struct child *this = &root->children[i]; 528 if (this->state == SELECTED) 529 { 530 if (this->folder == NULL) 531 { 532 curfolder = root; 533 name = this->name; 534 logf("save_folders_r: this->name[%s]", name); 535 } 536 else 537 { 538 curfolder = this->folder->previous; 539 name = this->folder->name; 540 logf("save_folders_r: this->folder->name[%s]", name); 541 } 542 543 len = get_full_path(curfolder, buffer_front, buflen); 544 545 if (len + 2 >= buflen) 546 continue; 547 548 len += rb->snprintf(&buffer_front[len], buflen - len, "%s:", name); 549 logf("save_folders_r: [%s]", buffer_front); 550 if (dst != hashed.buf) 551 { 552 int dlen = rb->strlen(dst); 553 if (dlen + len >= maxlen) 554 continue; 555 rb->strlcpy(&dst[dlen], buffer_front, maxlen - dlen); 556 } 557 else 558 { 559 if (hashed.len + len >= maxlen) 560 { 561 hashed.maxlen_exceeded = 1; 562 continue; 563 } 564 get_hash(buffer_front, &hashed.val, len); 565 hashed.len += len; 566 } 567 } 568 else if (this->state == EXPANDED) 569 save_folders_r(this->folder, dst, maxlen, buflen); 570 } 571} 572 573static uint32_t save_folders(struct folder *root, char* dst, size_t maxlen) 574{ 575 hashed.len = 0; 576 hashed.val = 0; 577 hashed.maxlen_exceeded = 0; 578 size_t len = buffer_end - buffer_front; 579 dst[0] = '\0'; 580 save_folders_r(root, dst, maxlen, len); 581 len = rb->strlen(dst); 582 /* fix trailing ':' */ 583 if (len > 1) dst[len-1] = '\0'; 584 /*Notify - user will probably not see save dialog if nothing new got added*/ 585 if (hashed.maxlen_exceeded > 0) rb->splash(HZ *2, ID2P(LANG_SHOWDIR_BUFFER_FULL)); 586 return hashed.val; 587} 588 589bool folder_select(char * header_text, char* setting, int setting_len) 590{ 591 struct folder *root; 592 struct simplelist_info info; 593 size_t buf_size; 594 595 buffer_front = rb->plugin_get_buffer(&buf_size); 596 buffer_end = buffer_front + buf_size; 597 logf("folder_select %d bytes free", (int)(buffer_end - buffer_front)); 598 root = load_root(); 599 600 logf("folders in: %s", setting); 601 /* Load previous selection(s) */ 602 select_paths(root, setting); 603 /* get current hash to check for changes later */ 604 uint32_t hash = save_folders(root, hashed.buf, setting_len); 605 rb->simplelist_info_init(&info, header_text, 606 count_items(root), root); 607 info.get_name = folder_get_name; 608 info.action_callback = folder_action_callback; 609 info.get_icon = folder_get_icon; 610 bool show_icons = rb->global_settings->show_icons; 611 rb->global_settings->show_icons = true; 612 rb->simplelist_show_list(&info); 613 rb->global_settings->show_icons = show_icons; 614 logf("folder_select %d bytes free", (int)(buffer_end - buffer_front)); 615 /* done editing. check for changes */ 616 if (hash != save_folders(root, hashed.buf, setting_len)) 617 { /* prompt for saving changes and commit if yes */ 618 if (rb->yesno_pop(ID2P(LANG_SAVE_CHANGES))) 619 { 620 save_folders(root, setting, setting_len); 621 rb->settings_save(); 622 logf("folders out: %s", setting); 623 return true; 624 } 625 } 626 return false; 627} 628 629/* plugin entry point */ 630enum plugin_status plugin_start(const void* parameter) 631{ 632 (void) parameter; 633 634 if(parameter) 635 { 636 637 if (rb->strcmp(parameter, rb->str(LANG_AUTORESUME)) == 0) 638 { 639 if (folder_select(rb->str(LANG_AUTORESUME), 640 rb->global_settings->autoresume_paths, 641 MAX_PATHNAME+1)) 642 { 643 return 1; 644 } 645 } 646 } 647 else if (folder_select(rb->str(LANG_SELECT_FOLDER), 648 rb->global_settings->tagcache_scan_paths, 649 sizeof(rb->global_settings->tagcache_scan_paths))) 650 { 651 return 1; 652 } 653 654 return PLUGIN_OK; 655}