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