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