1 /*
2 * playlist.cc
3 * Copyright 2009-2017 John Lindgren
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice,
9 * this list of conditions, and the following disclaimer.
10 *
11 * 2. Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions, and the following disclaimer in the documentation
13 * provided with the distribution.
14 *
15 * This software is provided "as is" and without any warranty, express or
16 * implied. In no event shall the authors be liable for any damages arising from
17 * the use of this software.
18 */
19
20 #include "playlist-internal.h"
21
22 #include <assert.h>
23 #include <stdlib.h>
24 #include <string.h>
25 #include <time.h>
26
27 #include <glib/gstdio.h>
28
29 #include "audstrings.h"
30 #include "drct.h"
31 #include "hook.h"
32 #include "i18n.h"
33 #include "internal.h"
34 #include "list.h"
35 #include "mainloop.h"
36 #include "multihash.h"
37 #include "parse.h"
38 #include "playlist-data.h"
39 #include "runtime.h"
40 #include "threads.h"
41
42 enum
43 {
44 ResumeStop,
45 ResumePlay,
46 ResumePause
47 };
48
49 /* update hooks */
50 enum
51 {
52 SetActive = (1 << 0),
53 SetPlaying = (1 << 1),
54 PlaybackBegin = (1 << 2),
55 PlaybackStop = (1 << 3)
56 };
57
58 enum class UpdateState
59 {
60 None,
61 Delayed,
62 Queued
63 };
64
65 #define STATE_FILE "playlist-state"
66
67 #define ENTER_GET_PLAYLIST(...) \
68 auto mh = mutex.take(); \
69 PlaylistData * playlist = m_id ? m_id->data : nullptr; \
70 if (!playlist) \
71 return __VA_ARGS__
72
73 #define SIMPLE_WRAPPER(type, failcode, func, ...) \
74 ENTER_GET_PLAYLIST(failcode); \
75 return playlist->func(__VA_ARGS__)
76
77 #define SIMPLE_VOID_WRAPPER(func, ...) \
78 ENTER_GET_PLAYLIST(); \
79 playlist->func(__VA_ARGS__)
80
81 static const char * const default_title = N_("New Playlist");
82 static const char * const temp_title = N_("Now Playing");
83
84 static aud::mutex mutex;
85 static aud::condvar condvar;
86
87 /*
88 * Each playlist is associated with its own ID struct, which contains a unique
89 * integer "stamp" (this is the source of the internal filenames 1000.audpl,
90 * 1001.audpl, etc.) The ID struct also serves as a "weak" pointer to the
91 * actual data, and persists even after the playlist itself is destroyed.
92 * The IDs are stored in a hash table, allowing lookup by stamp.
93 *
94 * In brief: Playlist (public handle)
95 * points to ->
96 * Playlist::ID (unique ID / weak pointer)
97 * points to ->
98 * PlaylistData (actual playlist data)
99 */
100 struct Playlist::ID
101 {
102 int stamp; // integer stamp, determines filename
103 int index; // display order
104 PlaylistData * data; // pointer to actual playlist data
105 };
106
107 static SimpleHash<IntHashKey, Playlist::ID> id_table;
108 static int next_stamp = 1000;
109
110 static Index<SmartPtr<PlaylistData>> playlists;
111 static Playlist::ID * active_id = nullptr;
112 static Playlist::ID * playing_id = nullptr;
113 static int resume_playlist = -1;
114 static bool resume_paused = false;
115
116 static QueuedFunc queued_update;
117 static Playlist::UpdateLevel update_level;
118 static int update_hooks;
119 static UpdateState update_state;
120
121 struct ScanItem : public ListNode
122 {
ScanItemScanItem123 ScanItem(PlaylistData * playlist, PlaylistEntry * entry,
124 ScanRequest * request, bool for_playback)
125 : playlist(playlist), entry(entry), request(request),
126 for_playback(for_playback), handled_by_playback(false)
127 {
128 }
129
130 PlaylistData * playlist;
131 PlaylistEntry * entry;
132 ScanRequest * request;
133 bool for_playback;
134 bool handled_by_playback;
135 };
136
137 static bool scan_enabled_nominal, scan_enabled;
138 static int scan_playlist, scan_row;
139 static List<ScanItem> scan_list;
140
141 static void scan_finish(ScanRequest * request);
142 static void scan_cancel(PlaylistEntry * entry);
143 static void scan_restart();
144
145 /* creates a new playlist with the requested stamp (if not already in use) */
create_playlist(int stamp)146 static Playlist::ID * create_playlist(int stamp)
147 {
148 Playlist::ID * id;
149
150 if (stamp >= 0 && !id_table.lookup(stamp))
151 id = id_table.add(stamp, {stamp, -1, nullptr});
152 else
153 {
154 while (id_table.lookup(next_stamp))
155 next_stamp++;
156
157 id = id_table.add(next_stamp, {next_stamp, -1, nullptr});
158 }
159
160 id->data = new PlaylistData(id, _(default_title));
161
162 return id;
163 }
164
number_playlists(int at,int length)165 static void number_playlists(int at, int length)
166 {
167 for (int i = at; i < at + length; i++)
168 playlists[i]->id()->index = i;
169 }
170
process_pending_update()171 EXPORT void Playlist::process_pending_update()
172 {
173 auto mh = mutex.take();
174
175 int hooks = update_hooks;
176 auto level = update_level;
177
178 Index<PlaylistEx> position_change_list;
179
180 for (auto & p : playlists)
181 {
182 bool position_changed = false;
183 p->swap_updates(position_changed);
184
185 if (position_changed)
186 position_change_list.append(p->id());
187 }
188
189 update_hooks = 0;
190 update_level = Playlist::NoUpdate;
191 update_state = UpdateState::None;
192 event_queue_unpause();
193
194 mh.unlock();
195
196 if (level != Playlist::NoUpdate)
197 hook_call("playlist update", aud::to_ptr(level));
198
199 for (PlaylistEx playlist : position_change_list)
200 hook_call("playlist position", aud::to_ptr(playlist));
201
202 if ((hooks & SetActive))
203 hook_call("playlist activate", nullptr);
204 if ((hooks & SetPlaying))
205 hook_call("playlist set playing", nullptr);
206 if ((hooks & PlaybackBegin))
207 hook_call("playback begin", nullptr);
208 if ((hooks & PlaybackStop))
209 hook_call("playback stop", nullptr);
210 }
211
queue_update()212 static void queue_update()
213 {
214 if (update_state < UpdateState::Queued)
215 {
216 event_queue_pause(); // give playlist updates priority
217 queued_update.queue(Playlist::process_pending_update);
218 update_state = UpdateState::Queued;
219 }
220 }
221
queue_update_hooks(int hooks)222 static void queue_update_hooks(int hooks)
223 {
224 if ((hooks & PlaybackBegin))
225 update_hooks &= ~PlaybackStop;
226 if ((hooks & PlaybackStop))
227 update_hooks &= ~PlaybackBegin;
228
229 update_hooks |= hooks;
230 queue_update();
231 }
232
queue_global_update(Playlist::UpdateLevel level,int flags=0)233 static void queue_global_update(Playlist::UpdateLevel level, int flags = 0)
234 {
235 if (level == Playlist::Structure)
236 scan_restart();
237
238 if ((flags & PlaylistData::DelayedUpdate))
239 {
240 if (update_state < UpdateState::Delayed)
241 {
242 queued_update.queue(250, Playlist::process_pending_update);
243 update_state = UpdateState::Delayed;
244 }
245 }
246 else
247 {
248 queue_update();
249 }
250
251 update_level = aud::max(update_level, level);
252 }
253
update_pending_any()254 EXPORT bool Playlist::update_pending_any()
255 {
256 auto mh = mutex.take();
257 return (update_level != Playlist::NoUpdate);
258 }
259
scan_in_progress() const260 EXPORT bool Playlist::scan_in_progress() const
261 {
262 ENTER_GET_PLAYLIST(false);
263 return (playlist->scan_status != PlaylistData::NotScanning);
264 }
265
scan_in_progress_any()266 EXPORT bool Playlist::scan_in_progress_any()
267 {
268 auto mh = mutex.take();
269
270 for (auto & p : playlists)
271 {
272 if (p->scan_status != PlaylistData::NotScanning)
273 return true;
274 }
275
276 return false;
277 }
278
scan_list_find_entry(PlaylistEntry * entry)279 static ScanItem * scan_list_find_entry(PlaylistEntry * entry)
280 {
281 auto match = [entry](const ScanItem & item) { return item.entry == entry; };
282
283 return scan_list.find(match);
284 }
285
scan_queue_entry(PlaylistData * playlist,PlaylistEntry * entry,bool for_playback=false)286 static void scan_queue_entry(PlaylistData * playlist, PlaylistEntry * entry,
287 bool for_playback = false)
288 {
289 int extra_flags = for_playback ? (SCAN_IMAGE | SCAN_FILE) : 0;
290 auto request =
291 playlist->create_scan_request(entry, scan_finish, extra_flags);
292
293 scan_list.append(new ScanItem(playlist, entry, request, for_playback));
294
295 /* playback entry will be scanned by the playback thread */
296 if (!for_playback)
297 scanner_request(request);
298 }
299
scan_reset_playback()300 static void scan_reset_playback()
301 {
302 auto match = [](const ScanItem & item) { return item.for_playback; };
303
304 ScanItem * item = scan_list.find(match);
305 if (!item)
306 return;
307
308 item->for_playback = false;
309
310 /* if playback was canceled before the entry was scanned, requeue it */
311 if (!item->handled_by_playback)
312 scanner_request(item->request);
313 }
314
scan_check_complete(PlaylistData * playlist)315 static void scan_check_complete(PlaylistData * playlist)
316 {
317 auto match = [playlist](const ScanItem & item) {
318 return item.playlist == playlist;
319 };
320
321 if (playlist->scan_status != PlaylistData::ScanEnding ||
322 scan_list.find(match))
323 return;
324
325 playlist->scan_status = PlaylistData::NotScanning;
326
327 if (update_state == UpdateState::Delayed)
328 queue_update();
329
330 event_queue_cancel("playlist scan complete");
331 event_queue("playlist scan complete", nullptr);
332 }
333
scan_queue_next_entry()334 static bool scan_queue_next_entry()
335 {
336 if (!scan_enabled)
337 return false;
338
339 while (scan_playlist < playlists.len())
340 {
341 PlaylistData * playlist = playlists[scan_playlist].get();
342
343 if (playlist->scan_status == PlaylistData::ScanActive)
344 {
345 while (1)
346 {
347 scan_row = playlist->next_unscanned_entry(scan_row);
348 if (scan_row < 0)
349 break;
350
351 auto entry = playlist->entry_at(scan_row);
352 if (!scan_list_find_entry(entry))
353 {
354 scan_queue_entry(playlist, entry);
355 return true;
356 }
357
358 scan_row++;
359 }
360
361 playlist->scan_status = PlaylistData::ScanEnding;
362 scan_check_complete(playlist);
363 }
364
365 scan_playlist++;
366 scan_row = 0;
367 }
368
369 return false;
370 }
371
scan_schedule()372 static void scan_schedule()
373 {
374 int scheduled = 0;
375
376 for (ScanItem * item = scan_list.head(); item; item = scan_list.next(item))
377 {
378 if (++scheduled >= SCAN_THREADS)
379 return;
380 }
381
382 while (scan_queue_next_entry())
383 {
384 if (++scheduled >= SCAN_THREADS)
385 return;
386 }
387 }
388
scan_finish(ScanRequest * request)389 static void scan_finish(ScanRequest * request)
390 {
391 auto mh = mutex.take();
392
393 auto match = [request](const ScanItem & item) {
394 return item.request == request;
395 };
396
397 ScanItem * item = scan_list.find(match);
398 if (!item)
399 return;
400
401 PlaylistData * playlist = item->playlist;
402 PlaylistEntry * entry = item->entry;
403
404 scan_list.remove(item);
405
406 // only use delayed update if a scan is still in progress
407 int update_flags = 0;
408 if (scan_enabled && playlist->scan_status != PlaylistData::NotScanning)
409 update_flags = PlaylistData::DelayedUpdate;
410
411 playlist->update_entry_from_scan(entry, request, update_flags);
412
413 delete item;
414
415 scan_check_complete(playlist);
416 scan_schedule();
417
418 condvar.notify_all();
419 }
420
scan_cancel(PlaylistEntry * entry)421 static void scan_cancel(PlaylistEntry * entry)
422 {
423 ScanItem * item = scan_list_find_entry(entry);
424 if (!item)
425 return;
426
427 scan_list.remove(item);
428 delete (item);
429 }
430
scan_restart()431 static void scan_restart()
432 {
433 scan_playlist = 0;
434 scan_row = 0;
435 scan_schedule();
436 }
437
438 /* mutex may be unlocked during the call */
wait_for_entry(aud::mutex::holder & mh,PlaylistData * playlist,int entry_num,bool need_decoder,bool need_tuple)439 static void wait_for_entry(aud::mutex::holder & mh, PlaylistData * playlist,
440 int entry_num, bool need_decoder, bool need_tuple)
441 {
442 bool scan_started = false;
443
444 while (1)
445 {
446 PlaylistEntry * entry = playlist->entry_at(entry_num);
447
448 // check whether entry is deleted or has already been scanned
449 if (!entry ||
450 !playlist->entry_needs_rescan(entry, need_decoder, need_tuple))
451 return;
452
453 // start scan if not already running ...
454 if (!scan_list_find_entry(entry))
455 {
456 // ... but only once
457 if (scan_started)
458 return;
459
460 scan_queue_entry(playlist, entry);
461 }
462
463 // wait for scan to finish
464 scan_started = true;
465 condvar.wait(mh);
466 }
467 }
468
start_playback_locked(int seek_time,bool pause)469 static void start_playback_locked(int seek_time, bool pause)
470 {
471 art_clear_current();
472 scan_reset_playback();
473
474 playback_play(seek_time, pause);
475
476 auto playlist = playing_id->data;
477 auto entry = playlist->entry_at(playlist->position());
478
479 // playback always begins with a rescan of the current entry in order to
480 // open the file, ensure a valid tuple, and read album art
481 scan_cancel(entry);
482 scan_queue_entry(playlist, entry, true);
483 }
484
stop_playback_locked()485 static void stop_playback_locked()
486 {
487 art_clear_current();
488 scan_reset_playback();
489
490 playback_stop();
491 }
492
pl_signal_entry_deleted(PlaylistEntry * entry)493 void pl_signal_entry_deleted(PlaylistEntry * entry) { scan_cancel(entry); }
494
pl_signal_position_changed(Playlist::ID * id)495 void pl_signal_position_changed(Playlist::ID * id)
496 {
497 queue_update();
498
499 if (id == playing_id)
500 {
501 if (id->data->position() >= 0)
502 {
503 start_playback_locked(0, aud_drct_get_paused());
504 queue_update_hooks(PlaybackBegin);
505 }
506 else
507 {
508 playing_id = nullptr;
509 stop_playback_locked();
510 queue_update_hooks(SetPlaying | PlaybackStop);
511 }
512 }
513 }
514
pl_signal_update_queued(Playlist::ID * id,Playlist::UpdateLevel level,int flags)515 void pl_signal_update_queued(Playlist::ID * id, Playlist::UpdateLevel level,
516 int flags)
517 {
518 auto playlist = id->data;
519
520 if (level == Playlist::Structure)
521 playlist->scan_status = PlaylistData::ScanActive;
522
523 if (level >= Playlist::Metadata)
524 {
525 int pos = playlist->position();
526 if (id == playing_id && pos >= 0)
527 playback_set_info(pos, playlist->entry_tuple(pos));
528
529 playlist->modified = true;
530 }
531
532 queue_global_update(level, flags);
533 }
534
pl_signal_rescan_needed(Playlist::ID * id)535 void pl_signal_rescan_needed(Playlist::ID * id)
536 {
537 id->data->scan_status = PlaylistData::ScanActive;
538 scan_restart();
539 }
540
pl_signal_playlist_deleted(Playlist::ID * id)541 void pl_signal_playlist_deleted(Playlist::ID * id)
542 {
543 /* break weak pointer link */
544 id->data = nullptr;
545 id->index = -1;
546 }
547
pl_hook_reformat_titles(void *,void *)548 static void pl_hook_reformat_titles(void *, void *)
549 {
550 auto mh = mutex.take();
551
552 PlaylistData::update_formatter();
553
554 for (auto & playlist : playlists)
555 playlist->reformat_titles();
556 }
557
pl_hook_trigger_scan(void *,void *)558 static void pl_hook_trigger_scan(void *, void *)
559 {
560 auto mh = mutex.take();
561 scan_enabled = scan_enabled_nominal && !aud_get_bool("metadata_on_play");
562 scan_restart();
563 }
564
playlist_init()565 void playlist_init()
566 {
567 srand(time(nullptr));
568
569 auto mh = mutex.take();
570
571 PlaylistData::update_formatter();
572
573 update_level = Playlist::NoUpdate;
574 update_hooks = 0;
575 update_state = UpdateState::None;
576 scan_enabled = scan_enabled_nominal = false;
577 scan_playlist = scan_row = 0;
578
579 mh.unlock();
580
581 hook_associate("set generic_title_format", pl_hook_reformat_titles,
582 nullptr);
583 hook_associate("set leading_zero", pl_hook_reformat_titles, nullptr);
584 hook_associate("set metadata_fallbacks", pl_hook_reformat_titles, nullptr);
585 hook_associate("set show_hours", pl_hook_reformat_titles, nullptr);
586 hook_associate("set show_numbers_in_pl", pl_hook_reformat_titles, nullptr);
587 hook_associate("set metadata_on_play", pl_hook_trigger_scan, nullptr);
588 }
589
playlist_enable_scan(bool enable)590 void playlist_enable_scan(bool enable)
591 {
592 auto mh = mutex.take();
593
594 scan_enabled_nominal = enable;
595 scan_enabled = scan_enabled_nominal && !aud_get_bool("metadata_on_play");
596 scan_restart();
597 }
598
playlist_clear_updates()599 void playlist_clear_updates()
600 {
601 auto mh = mutex.take();
602
603 /* clear updates queued during init sequence */
604 for (auto & playlist : playlists)
605 playlist->cancel_updates();
606
607 queued_update.stop();
608 update_level = Playlist::NoUpdate;
609 update_hooks = 0;
610 update_state = UpdateState::None;
611 event_queue_unpause();
612 }
613
playlist_end()614 void playlist_end()
615 {
616 hook_dissociate("set generic_title_format", pl_hook_reformat_titles);
617 hook_dissociate("set leading_zero", pl_hook_reformat_titles);
618 hook_dissociate("set metadata_fallbacks", pl_hook_reformat_titles);
619 hook_dissociate("set show_hours", pl_hook_reformat_titles);
620 hook_dissociate("set show_numbers_in_pl", pl_hook_reformat_titles);
621 hook_dissociate("set metadata_on_play", pl_hook_trigger_scan);
622
623 playlist_cache_clear();
624
625 auto mh = mutex.take();
626
627 /* playback should already be stopped */
628 assert(!playing_id);
629 assert(!scan_list.head());
630
631 queued_update.stop();
632
633 active_id = nullptr;
634 resume_playlist = -1;
635 resume_paused = false;
636
637 playlists.clear();
638 id_table.clear();
639
640 PlaylistData::cleanup_formatter();
641 }
642
n_entries() const643 EXPORT int Playlist::n_entries() const { SIMPLE_WRAPPER(int, 0, n_entries); }
remove_entries(int at,int number) const644 EXPORT void Playlist::remove_entries(int at, int number) const
645 {
646 SIMPLE_VOID_WRAPPER(remove_entries, at, number);
647 }
entry_filename(int entry_num) const648 EXPORT String Playlist::entry_filename(int entry_num) const
649 {
650 SIMPLE_WRAPPER(String, String(), entry_filename, entry_num);
651 }
652
get_position() const653 EXPORT int Playlist::get_position() const { SIMPLE_WRAPPER(int, -1, position); }
set_position(int entry_num) const654 EXPORT void Playlist::set_position(int entry_num) const
655 {
656 SIMPLE_VOID_WRAPPER(set_position, entry_num);
657 }
prev_song() const658 EXPORT bool Playlist::prev_song() const
659 {
660 SIMPLE_WRAPPER(bool, false, prev_song);
661 }
prev_album() const662 EXPORT bool Playlist::prev_album() const
663 {
664 SIMPLE_WRAPPER(bool, false, prev_album);
665 }
next_song(bool repeat) const666 EXPORT bool Playlist::next_song(bool repeat) const
667 {
668 SIMPLE_WRAPPER(bool, false, next_song, repeat);
669 }
next_album(bool repeat) const670 EXPORT bool Playlist::next_album(bool repeat) const
671 {
672 SIMPLE_WRAPPER(bool, false, next_album, repeat);
673 }
get_focus() const674 EXPORT int Playlist::get_focus() const { SIMPLE_WRAPPER(int, -1, focus); }
set_focus(int entry_num) const675 EXPORT void Playlist::set_focus(int entry_num) const
676 {
677 SIMPLE_VOID_WRAPPER(set_focus, entry_num);
678 }
entry_selected(int entry_num) const679 EXPORT bool Playlist::entry_selected(int entry_num) const
680 {
681 SIMPLE_WRAPPER(bool, false, entry_selected, entry_num);
682 }
select_entry(int entry_num,bool selected) const683 EXPORT void Playlist::select_entry(int entry_num, bool selected) const
684 {
685 SIMPLE_VOID_WRAPPER(select_entry, entry_num, selected);
686 }
n_selected(int at,int number) const687 EXPORT int Playlist::n_selected(int at, int number) const
688 {
689 SIMPLE_WRAPPER(int, 0, n_selected, at, number);
690 }
select_all(bool selected) const691 EXPORT void Playlist::select_all(bool selected) const
692 {
693 SIMPLE_VOID_WRAPPER(select_all, selected);
694 }
shift_entries(int entry_num,int distance) const695 EXPORT int Playlist::shift_entries(int entry_num, int distance) const
696 {
697 SIMPLE_WRAPPER(int, 0, shift_entries, entry_num, distance);
698 }
remove_selected() const699 EXPORT void Playlist::remove_selected() const
700 {
701 SIMPLE_VOID_WRAPPER(remove_selected);
702 }
703
sort_by_filename(StringCompareFunc compare) const704 EXPORT void Playlist::sort_by_filename(StringCompareFunc compare) const
705 {
706 SIMPLE_VOID_WRAPPER(sort, {compare, nullptr});
707 }
sort_by_tuple(TupleCompareFunc compare) const708 EXPORT void Playlist::sort_by_tuple(TupleCompareFunc compare) const
709 {
710 SIMPLE_VOID_WRAPPER(sort, {nullptr, compare});
711 }
sort_selected_by_filename(StringCompareFunc compare) const712 EXPORT void Playlist::sort_selected_by_filename(StringCompareFunc compare) const
713 {
714 SIMPLE_VOID_WRAPPER(sort_selected, {compare, nullptr});
715 }
sort_selected_by_tuple(TupleCompareFunc compare) const716 EXPORT void Playlist::sort_selected_by_tuple(TupleCompareFunc compare) const
717 {
718 SIMPLE_VOID_WRAPPER(sort_selected, {nullptr, compare});
719 }
reverse_order() const720 EXPORT void Playlist::reverse_order() const
721 {
722 SIMPLE_VOID_WRAPPER(reverse_order);
723 }
reverse_selected() const724 EXPORT void Playlist::reverse_selected() const
725 {
726 SIMPLE_VOID_WRAPPER(reverse_selected);
727 }
randomize_order() const728 EXPORT void Playlist::randomize_order() const
729 {
730 SIMPLE_VOID_WRAPPER(randomize_order);
731 }
randomize_selected() const732 EXPORT void Playlist::randomize_selected() const
733 {
734 SIMPLE_VOID_WRAPPER(randomize_selected);
735 }
736
rescan_all() const737 EXPORT void Playlist::rescan_all() const
738 {
739 SIMPLE_VOID_WRAPPER(reset_tuples, false);
740 }
rescan_selected() const741 EXPORT void Playlist::rescan_selected() const
742 {
743 SIMPLE_VOID_WRAPPER(reset_tuples, true);
744 }
745
total_length_ms() const746 EXPORT int64_t Playlist::total_length_ms() const
747 {
748 SIMPLE_WRAPPER(int64_t, 0, total_length);
749 }
selected_length_ms() const750 EXPORT int64_t Playlist::selected_length_ms() const
751 {
752 SIMPLE_WRAPPER(int64_t, 0, selected_length);
753 }
754
n_queued() const755 EXPORT int Playlist::n_queued() const { SIMPLE_WRAPPER(int, 0, n_queued); }
queue_insert(int at,int entry_num) const756 EXPORT void Playlist::queue_insert(int at, int entry_num) const
757 {
758 SIMPLE_VOID_WRAPPER(queue_insert, at, entry_num);
759 }
queue_insert_selected(int at) const760 EXPORT void Playlist::queue_insert_selected(int at) const
761 {
762 SIMPLE_VOID_WRAPPER(queue_insert_selected, at);
763 }
queue_get_entry(int at) const764 EXPORT int Playlist::queue_get_entry(int at) const
765 {
766 SIMPLE_WRAPPER(int, -1, queue_get_entry, at);
767 }
queue_find_entry(int entry_num) const768 EXPORT int Playlist::queue_find_entry(int entry_num) const
769 {
770 SIMPLE_WRAPPER(int, -1, queue_find_entry, entry_num);
771 }
queue_remove(int at,int number) const772 EXPORT void Playlist::queue_remove(int at, int number) const
773 {
774 SIMPLE_VOID_WRAPPER(queue_remove, at, number);
775 }
queue_remove_selected() const776 EXPORT void Playlist::queue_remove_selected() const
777 {
778 SIMPLE_VOID_WRAPPER(queue_remove_selected);
779 }
780
update_pending() const781 EXPORT bool Playlist::update_pending() const
782 {
783 SIMPLE_WRAPPER(bool, false, update_pending);
784 }
update_detail() const785 EXPORT Playlist::Update Playlist::update_detail() const
786 {
787 SIMPLE_WRAPPER(Update, Update(), last_update);
788 }
789
insert_flat_items(int at,Index<PlaylistAddItem> && items) const790 void PlaylistEx::insert_flat_items(int at,
791 Index<PlaylistAddItem> && items) const
792 {
793 SIMPLE_VOID_WRAPPER(insert_items, at, std::move(items));
794 }
795
index() const796 EXPORT int Playlist::index() const
797 {
798 ENTER_GET_PLAYLIST(-1);
799 return m_id->index;
800 }
801
stamp() const802 EXPORT int PlaylistEx::stamp() const
803 {
804 ENTER_GET_PLAYLIST(-1);
805 return m_id->stamp;
806 }
807
n_playlists()808 EXPORT int Playlist::n_playlists()
809 {
810 auto mh = mutex.take();
811 return playlists.len();
812 }
813
by_index(int at)814 EXPORT Playlist Playlist::by_index(int at)
815 {
816 auto mh = mutex.take();
817 Playlist::ID * id =
818 (at >= 0 && at < playlists.len()) ? playlists[at]->id() : nullptr;
819 return Playlist(id);
820 }
821
insert_playlist_locked(int at,int stamp=-1)822 static Playlist::ID * insert_playlist_locked(int at, int stamp = -1)
823 {
824 if (at < 0 || at > playlists.len())
825 at = playlists.len();
826
827 auto id = create_playlist(stamp);
828
829 playlists.insert(at, 1);
830 playlists[at].capture(id->data);
831
832 number_playlists(at, playlists.len() - at);
833
834 /* this will only happen at startup */
835 if (!active_id)
836 active_id = id;
837
838 queue_global_update(Playlist::Structure);
839
840 return id;
841 }
842
get_blank_locked()843 static Playlist::ID * get_blank_locked()
844 {
845 if (!strcmp(active_id->data->title, _(default_title)) &&
846 !active_id->data->n_entries())
847 return active_id;
848
849 return insert_playlist_locked(active_id->index + 1);
850 }
851
insert_with_stamp(int at,int stamp)852 Playlist PlaylistEx::insert_with_stamp(int at, int stamp)
853 {
854 auto mh = mutex.take();
855 auto id = insert_playlist_locked(at, stamp);
856 return Playlist(id);
857 }
858
insert_playlist(int at)859 EXPORT Playlist Playlist::insert_playlist(int at)
860 {
861 auto mh = mutex.take();
862 auto id = insert_playlist_locked(at);
863 return Playlist(id);
864 }
865
reorder_playlists(int from,int to,int count)866 EXPORT void Playlist::reorder_playlists(int from, int to, int count)
867 {
868 auto mh = mutex.take();
869
870 if (from < 0 || from + count > playlists.len() || to < 0 ||
871 to + count > playlists.len() || count < 0)
872 return;
873
874 Index<SmartPtr<PlaylistData>> displaced;
875
876 if (to < from)
877 displaced.move_from(playlists, to, -1, from - to, true, false);
878 else
879 displaced.move_from(playlists, from + count, -1, to - from, true,
880 false);
881
882 playlists.shift(from, to, count);
883
884 if (to < from)
885 {
886 playlists.move_from(displaced, 0, to + count, from - to, false, true);
887 number_playlists(to, from + count - to);
888 }
889 else
890 {
891 playlists.move_from(displaced, 0, from, to - from, false, true);
892 number_playlists(from, to + count - from);
893 }
894
895 queue_global_update(Structure);
896 }
897
remove_playlist() const898 EXPORT void Playlist::remove_playlist() const
899 {
900 ENTER_GET_PLAYLIST();
901
902 int at = m_id->index;
903 playlists.remove(at, 1);
904
905 if (!playlists.len())
906 playlists.append(create_playlist(-1)->data);
907
908 number_playlists(at, playlists.len() - at);
909
910 if (m_id == active_id)
911 {
912 int active_num = aud::min(at, playlists.len() - 1);
913 active_id = playlists[active_num]->id();
914 queue_update_hooks(SetActive);
915 }
916
917 if (m_id == playing_id)
918 {
919 playing_id = nullptr;
920 stop_playback_locked();
921 queue_update_hooks(SetPlaying | PlaybackStop);
922 }
923
924 queue_global_update(Structure);
925 }
926
set_filename(const char * filename) const927 EXPORT void Playlist::set_filename(const char * filename) const
928 {
929 ENTER_GET_PLAYLIST();
930
931 playlist->filename = String(filename);
932 playlist->modified = true;
933
934 queue_global_update(Metadata);
935 }
936
get_filename() const937 EXPORT String Playlist::get_filename() const
938 {
939 ENTER_GET_PLAYLIST(String());
940 return playlist->filename;
941 }
942
set_title(const char * title) const943 EXPORT void Playlist::set_title(const char * title) const
944 {
945 ENTER_GET_PLAYLIST();
946
947 playlist->title = String(title);
948 playlist->modified = true;
949
950 queue_global_update(Metadata);
951 }
952
get_title() const953 EXPORT String Playlist::get_title() const
954 {
955 ENTER_GET_PLAYLIST(String());
956 return playlist->title;
957 }
958
set_modified(bool modified) const959 void PlaylistEx::set_modified(bool modified) const
960 {
961 ENTER_GET_PLAYLIST();
962 playlist->modified = modified;
963 }
964
get_modified() const965 bool PlaylistEx::get_modified() const
966 {
967 ENTER_GET_PLAYLIST(false);
968 return playlist->modified;
969 }
970
activate() const971 EXPORT void Playlist::activate() const
972 {
973 ENTER_GET_PLAYLIST();
974
975 if (m_id != active_id)
976 {
977 active_id = m_id;
978 queue_update_hooks(SetActive);
979 }
980 }
981
active_playlist()982 EXPORT Playlist Playlist::active_playlist()
983 {
984 auto mh = mutex.take();
985 return Playlist(active_id);
986 }
987
new_playlist()988 EXPORT Playlist Playlist::new_playlist()
989 {
990 auto mh = mutex.take();
991
992 int at = active_id->index + 1;
993 auto id = insert_playlist_locked(at);
994
995 active_id = id;
996 queue_update_hooks(SetActive);
997
998 return Playlist(id);
999 }
1000
set_playing_locked(Playlist::ID * id,bool paused)1001 static void set_playing_locked(Playlist::ID * id, bool paused)
1002 {
1003 if (id == playing_id)
1004 {
1005 /* already playing, just need to pause/unpause */
1006 if (aud_drct_get_paused() != paused)
1007 aud_drct_pause();
1008
1009 return;
1010 }
1011
1012 if (playing_id)
1013 playing_id->data->resume_time = aud_drct_get_time();
1014
1015 /* is there anything to play? */
1016 if (id && id->data->position() < 0 && !id->data->next_song(true))
1017 id = nullptr;
1018
1019 playing_id = id;
1020
1021 if (id)
1022 {
1023 start_playback_locked(id->data->resume_time, paused);
1024 queue_update_hooks(SetPlaying | PlaybackBegin);
1025 }
1026 else
1027 {
1028 stop_playback_locked();
1029 queue_update_hooks(SetPlaying | PlaybackStop);
1030 }
1031 }
1032
start_playback(bool paused) const1033 EXPORT void Playlist::start_playback(bool paused) const
1034 {
1035 ENTER_GET_PLAYLIST();
1036 set_playing_locked(m_id, paused);
1037 }
1038
aud_drct_stop()1039 EXPORT void aud_drct_stop()
1040 {
1041 auto mh = mutex.take();
1042 set_playing_locked(nullptr, false);
1043 }
1044
playing_playlist()1045 EXPORT Playlist Playlist::playing_playlist()
1046 {
1047 auto mh = mutex.take();
1048 return Playlist(playing_id);
1049 }
1050
blank_playlist()1051 EXPORT Playlist Playlist::blank_playlist()
1052 {
1053 auto mh = mutex.take();
1054 auto id = get_blank_locked();
1055 return Playlist(id);
1056 }
1057
temporary_playlist()1058 EXPORT Playlist Playlist::temporary_playlist()
1059 {
1060 auto mh = mutex.take();
1061
1062 const char * title = _(temp_title);
1063 ID * id = nullptr;
1064
1065 for (auto & playlist : playlists)
1066 {
1067 if (!strcmp(playlist->title, title))
1068 {
1069 id = playlist->id();
1070 break;
1071 }
1072 }
1073
1074 if (!id)
1075 {
1076 id = get_blank_locked();
1077 id->data->title = String(title);
1078 }
1079
1080 return Playlist(id);
1081 }
1082
entry_decoder(int entry_num,GetMode mode,String * error) const1083 EXPORT PluginHandle * Playlist::entry_decoder(int entry_num, GetMode mode,
1084 String * error) const
1085 {
1086 ENTER_GET_PLAYLIST(nullptr);
1087 wait_for_entry(mh, playlist, entry_num, (mode == Wait), false);
1088 return playlist->entry_decoder(entry_num, error);
1089 }
1090
entry_tuple(int entry_num,GetMode mode,String * error) const1091 EXPORT Tuple Playlist::entry_tuple(int entry_num, GetMode mode,
1092 String * error) const
1093 {
1094 ENTER_GET_PLAYLIST(Tuple());
1095 wait_for_entry(mh, playlist, entry_num, false, (mode == Wait));
1096 return playlist->entry_tuple(entry_num, error);
1097 }
1098
rescan_file(const char * filename)1099 EXPORT void Playlist::rescan_file(const char * filename)
1100 {
1101 auto mh = mutex.take();
1102
1103 for (auto & playlist : playlists)
1104 playlist->reset_tuple_of_file(filename);
1105 }
1106
1107 // called from playback thread
playback_entry_read(int serial)1108 DecodeInfo playback_entry_read(int serial)
1109 {
1110 auto mh = mutex.take();
1111 DecodeInfo dec;
1112
1113 if (playback_check_serial(serial))
1114 {
1115 auto playlist = playing_id->data;
1116 auto entry = playlist->entry_at(playlist->position());
1117
1118 ScanItem * item = scan_list_find_entry(entry);
1119 assert(item && item->for_playback);
1120
1121 ScanRequest * request = item->request;
1122 item->handled_by_playback = true;
1123
1124 mh.unlock();
1125 request->run();
1126 mh.lock();
1127
1128 if (playback_check_serial(serial))
1129 {
1130 assert(playlist == playing_id->data);
1131
1132 int pos = playlist->position();
1133 playback_set_info(pos, playlist->entry_tuple(pos));
1134
1135 art_cache_current(request->filename, std::move(request->image_data),
1136 std::move(request->image_file));
1137
1138 dec.filename = request->filename;
1139 dec.ip = request->ip;
1140 dec.file = std::move(request->file);
1141 dec.error = std::move(request->error);
1142 }
1143
1144 delete request;
1145 }
1146
1147 return dec;
1148 }
1149
1150 // called from playback thread
playback_entry_set_tuple(int serial,Tuple && tuple)1151 void playback_entry_set_tuple(int serial, Tuple && tuple)
1152 {
1153 auto mh = mutex.take();
1154
1155 if (playback_check_serial(serial))
1156 playing_id->data->update_playback_entry(std::move(tuple));
1157 }
1158
playlist_save_state()1159 void playlist_save_state()
1160 {
1161 /* get playback state before locking playlists */
1162 bool paused = aud_drct_get_paused();
1163 int time = aud_drct_get_time();
1164
1165 auto mh = mutex.take();
1166
1167 const char * user_dir = aud_get_path(AudPath::UserDir);
1168 StringBuf path = filename_build({user_dir, STATE_FILE});
1169
1170 FILE * handle = g_fopen(path, "w");
1171 if (!handle)
1172 return;
1173
1174 fprintf(handle, "active %d\n", active_id ? active_id->index : -1);
1175 fprintf(handle, "playing %d\n", playing_id ? playing_id->index : -1);
1176
1177 for (auto & playlist : playlists)
1178 {
1179 fprintf(handle, "playlist %d\n", playlist->id()->index);
1180
1181 if (playlist->filename)
1182 fprintf(handle, "filename %s\n", (const char *)playlist->filename);
1183
1184 fprintf(handle, "position %d\n", playlist->position());
1185
1186 /* save shuffle history */
1187 auto history = playlist->shuffle_history();
1188
1189 for (int i = 0; i < history.len(); i += 16)
1190 {
1191 int count = aud::min(16, history.len() - i);
1192 auto list = int_array_to_str(&history[i], count);
1193 fprintf(handle, "shuffle %s\n", (const char *)list);
1194 }
1195
1196 /* resume state is stored per-playlist for historical reasons */
1197 bool is_playing = (playlist->id() == playing_id);
1198 fprintf(handle, "resume-state %d\n",
1199 (is_playing && paused) ? ResumePause : ResumePlay);
1200 fprintf(handle, "resume-time %d\n",
1201 is_playing ? time : playlist->resume_time);
1202 }
1203
1204 fclose(handle);
1205 }
1206
playlist_load_state()1207 void playlist_load_state()
1208 {
1209 auto mh = mutex.take();
1210 int playlist_num;
1211
1212 const char * user_dir = aud_get_path(AudPath::UserDir);
1213 StringBuf path = filename_build({user_dir, STATE_FILE});
1214
1215 FILE * handle = g_fopen(path, "r");
1216 if (!handle)
1217 return;
1218
1219 TextParser parser(handle);
1220
1221 if (parser.get_int("active", playlist_num))
1222 {
1223 if (playlist_num >= 0 && playlist_num < playlists.len())
1224 active_id = playlists[playlist_num]->id();
1225
1226 parser.next();
1227 }
1228
1229 if (parser.get_int("playing", resume_playlist))
1230 parser.next();
1231
1232 while (parser.get_int("playlist", playlist_num) && playlist_num >= 0 &&
1233 playlist_num < playlists.len())
1234 {
1235 PlaylistData * playlist = playlists[playlist_num].get();
1236
1237 parser.next();
1238
1239 playlist->filename = parser.get_str("filename");
1240 if (playlist->filename)
1241 parser.next();
1242
1243 int position = -1;
1244 if (parser.get_int("position", position))
1245 {
1246 playlist->set_position(position);
1247 parser.next();
1248 }
1249
1250 /* restore shuffle history */
1251 Index<int> history;
1252
1253 for (String list; (list = parser.get_str("shuffle")); parser.next())
1254 {
1255 auto split = str_list_to_index(list, ", ");
1256 for (auto & str : split)
1257 history.append(str_to_int(str));
1258 }
1259
1260 if (history.len())
1261 playlist->shuffle_replay(history);
1262
1263 /* resume state is stored per-playlist for historical reasons */
1264 int resume_state = ResumePlay;
1265 if (parser.get_int("resume-state", resume_state))
1266 parser.next();
1267
1268 if (playlist_num == resume_playlist)
1269 {
1270 if (resume_state == ResumeStop)
1271 resume_playlist = -1;
1272 if (resume_state == ResumePause)
1273 resume_paused = true;
1274 }
1275
1276 if (parser.get_int("resume-time", playlist->resume_time))
1277 parser.next();
1278 }
1279
1280 fclose(handle);
1281
1282 /* set initial focus and selection */
1283 for (auto & playlist : playlists)
1284 {
1285 int focus = playlist->position();
1286 if (focus < 0 && playlist->n_entries())
1287 focus = 0;
1288
1289 if (focus >= 0)
1290 {
1291 playlist->set_focus(focus);
1292 playlist->select_entry(focus, true);
1293 }
1294 }
1295 }
1296
aud_resume()1297 EXPORT void aud_resume()
1298 {
1299 if (aud_get_bool("always_resume_paused"))
1300 resume_paused = true;
1301
1302 Playlist::by_index(resume_playlist).start_playback(resume_paused);
1303 }
1304