1 /*
2 * Copyright 2003-2021 The Music Player Daemon Project
3 * http://www.musicpd.org
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20 #include "Playlist.hxx"
21 #include "Listener.hxx"
22 #include "PlaylistError.hxx"
23 #include "player/Control.hxx"
24 #include "song/DetachedSong.hxx"
25 #include "SingleMode.hxx"
26 #include "Log.hxx"
27
28 #include <cassert>
29
30 void
TagModified(DetachedSong && song)31 playlist::TagModified(DetachedSong &&song) noexcept
32 {
33 if (!playing)
34 return;
35
36 assert(current >= 0);
37
38 DetachedSong ¤t_song = queue.GetOrder(current);
39 if (song.IsSame(current_song))
40 current_song.MoveTagItemsFrom(std::move(song));
41
42 queue.ModifyAtOrder(current);
43 OnModified();
44 }
45
46 void
TagModified(const char * real_uri,const Tag & tag)47 playlist::TagModified(const char *real_uri, const Tag &tag) noexcept
48 {
49 bool modified = false;
50
51 for (unsigned i = 0; i < queue.length; ++i) {
52 auto &song = *queue.items[i].song;
53 if (song.IsRealURI(real_uri)) {
54 song.SetTag(tag);
55 queue.ModifyAtPosition(i);
56 modified = true;
57 }
58 }
59
60 if (modified)
61 OnModified();
62 }
63
64 inline void
QueueSongOrder(PlayerControl & pc,unsigned order)65 playlist::QueueSongOrder(PlayerControl &pc, unsigned order) noexcept
66
67 {
68 assert(queue.IsValidOrder(order));
69
70 queued = order;
71
72 const DetachedSong &song = queue.GetOrder(order);
73
74 FmtDebug(playlist_domain, "queue song {}:\"{}\"",
75 queued, song.GetURI());
76
77 pc.LockEnqueueSong(std::make_unique<DetachedSong>(song));
78 }
79
80 void
SongStarted()81 playlist::SongStarted() noexcept
82 {
83 assert(current >= 0);
84
85 /* reset a song's "priority" when playback starts */
86 if (queue.SetPriority(queue.OrderToPosition(current), 0, -1, false))
87 OnModified();
88 }
89
90 inline void
QueuedSongStarted(PlayerControl & pc)91 playlist::QueuedSongStarted(PlayerControl &pc) noexcept
92 {
93 assert(!pc.LockGetSyncInfo().has_next_song);
94 assert(queued >= -1);
95 assert(current >= 0);
96
97 /* queued song has started: copy queued to current,
98 and notify the clients */
99
100 const int old_current = current;
101 current = queued;
102 queued = -1;
103
104 if (queue.consume)
105 DeleteOrder(pc, old_current);
106
107 listener.OnQueueSongStarted();
108
109 SongStarted();
110 }
111
112 const DetachedSong *
GetQueuedSong() const113 playlist::GetQueuedSong() const noexcept
114 {
115 return playing && queued >= 0
116 ? &queue.GetOrder(queued)
117 : nullptr;
118 }
119
120 void
UpdateQueuedSong(PlayerControl & pc,const DetachedSong * prev)121 playlist::UpdateQueuedSong(PlayerControl &pc,
122 const DetachedSong *prev) noexcept
123 {
124 if (!playing)
125 return;
126
127 if (prev == nullptr && bulk_edit)
128 /* postponed until CommitBulk() to avoid always
129 queueing the first song that is being added (in
130 random mode) */
131 return;
132
133 assert(!queue.IsEmpty());
134 assert((queued < 0) == (prev == nullptr));
135
136 const int next_order = current >= 0
137 ? queue.GetNextOrder(current)
138 : 0;
139
140 if (next_order == 0 && queue.random && queue.single == SingleMode::OFF) {
141 /* shuffle the song order again, so we get a different
142 order each time the playlist is played
143 completely */
144 const unsigned current_position =
145 queue.OrderToPosition(current);
146
147 queue.ShuffleOrder();
148
149 /* make sure that the current still points to
150 the current song, after the song order has been
151 shuffled */
152 current = queue.PositionToOrder(current_position);
153 }
154
155 const DetachedSong *const next_song = next_order >= 0
156 ? &queue.GetOrder(next_order)
157 : nullptr;
158
159 if (prev != nullptr && next_song != prev) {
160 /* clear the currently queued song */
161 pc.LockCancel();
162 queued = -1;
163 }
164
165 if (next_order >= 0) {
166 if (next_song != prev)
167 QueueSongOrder(pc, next_order);
168 else
169 queued = next_order;
170 }
171 }
172
173 void
PlayOrder(PlayerControl & pc,unsigned order)174 playlist::PlayOrder(PlayerControl &pc, unsigned order)
175 {
176 playing = true;
177 queued = -1;
178
179 const DetachedSong &song = queue.GetOrder(order);
180
181 FmtDebug(playlist_domain, "play {}:\"{}\"", order, song.GetURI());
182
183 current = order;
184
185 pc.Play(std::make_unique<DetachedSong>(song));
186
187 SongStarted();
188 }
189
190 void
SyncWithPlayer(PlayerControl & pc)191 playlist::SyncWithPlayer(PlayerControl &pc) noexcept
192 {
193 if (!playing)
194 /* this event has reached us out of sync: we aren't
195 playing anymore; ignore the event */
196 return;
197
198 const auto i = pc.LockGetSyncInfo();
199
200 if (i.state == PlayerState::STOP)
201 /* the player thread has stopped: check if playback
202 should be restarted with the next song. That can
203 happen if the playlist isn't filling the queue fast
204 enough */
205 ResumePlayback(pc);
206 else {
207 /* check if the player thread has already started
208 playing the queued song */
209 if (!i.has_next_song && queued != -1)
210 QueuedSongStarted(pc);
211
212 /* make sure the queued song is always set (if
213 possible) */
214 if (!pc.LockGetSyncInfo().has_next_song && queued < 0)
215 UpdateQueuedSong(pc, nullptr);
216 }
217 }
218
219 inline void
ResumePlayback(PlayerControl & pc)220 playlist::ResumePlayback(PlayerControl &pc) noexcept
221 {
222 assert(playing);
223 assert(pc.GetState() == PlayerState::STOP);
224
225 const auto error = pc.GetErrorType();
226 if (error == PlayerError::NONE)
227 error_count = 0;
228 else
229 ++error_count;
230
231 if ((stop_on_error && error != PlayerError::NONE) ||
232 error == PlayerError::OUTPUT ||
233 error_count >= queue.GetLength())
234 /* too many errors, or critical error: stop
235 playback */
236 Stop(pc);
237 else
238 /* continue playback at the next song */
239 try {
240 PlayNext(pc);
241 } catch (...) {
242 /* TODO: log error? */
243 }
244 }
245
246 void
SetRepeat(PlayerControl & pc,bool status)247 playlist::SetRepeat(PlayerControl &pc, bool status) noexcept
248 {
249 if (status == queue.repeat)
250 return;
251
252 queue.repeat = status;
253
254 pc.LockSetBorderPause(queue.single != SingleMode::OFF && !queue.repeat);
255
256 /* if the last song is currently being played, the "next song"
257 might change when repeat mode is toggled */
258 UpdateQueuedSong(pc, GetQueuedSong());
259
260 listener.OnQueueOptionsChanged();
261 }
262
263 static void
playlist_order(playlist & playlist)264 playlist_order(playlist &playlist) noexcept
265 {
266 if (playlist.current >= 0)
267 /* update playlist.current, order==position now */
268 playlist.current = playlist.queue.OrderToPosition(playlist.current);
269
270 playlist.queue.RestoreOrder();
271 }
272
273 void
SetSingle(PlayerControl & pc,SingleMode status)274 playlist::SetSingle(PlayerControl &pc, SingleMode status) noexcept
275 {
276 if (status == queue.single)
277 return;
278
279 queue.single = status;
280
281
282 pc.LockSetBorderPause(queue.single != SingleMode::OFF && !queue.repeat);
283
284 /* if the last song is currently being played, the "next song"
285 might change when single mode is toggled */
286 UpdateQueuedSong(pc, GetQueuedSong());
287
288 listener.OnQueueOptionsChanged();
289 }
290
291 void
SetConsume(bool status)292 playlist::SetConsume(bool status) noexcept
293 {
294 if (status == queue.consume)
295 return;
296
297 queue.consume = status;
298 listener.OnQueueOptionsChanged();
299 }
300
301 void
SetRandom(PlayerControl & pc,bool status)302 playlist::SetRandom(PlayerControl &pc, bool status) noexcept
303 {
304 if (status == queue.random)
305 return;
306
307 const DetachedSong *const queued_song = GetQueuedSong();
308
309 queue.random = status;
310
311 if (queue.random) {
312 /* shuffle the queue order, but preserve current */
313
314 const int current_position = playing
315 ? GetCurrentPosition()
316 : -1;
317
318 queue.ShuffleOrder();
319
320 if (current_position >= 0) {
321 /* make sure the current song is the first in
322 the order list, so the whole rest of the
323 playlist is played after that */
324 unsigned current_order =
325 queue.PositionToOrder(current_position);
326 current = queue.MoveOrder(current_order, 0);
327 } else
328 current = -1;
329 } else
330 playlist_order(*this);
331
332 UpdateQueuedSong(pc, queued_song);
333
334 listener.OnQueueOptionsChanged();
335 }
336
337 int
GetCurrentPosition() const338 playlist::GetCurrentPosition() const noexcept
339 {
340 return current >= 0
341 ? queue.OrderToPosition(current)
342 : -1;
343 }
344
345 int
GetNextPosition() const346 playlist::GetNextPosition() const noexcept
347 {
348 if (current < 0)
349 return -1;
350
351 if (queue.single != SingleMode::OFF && queue.repeat)
352 return queue.OrderToPosition(current);
353 else if (queue.IsValidOrder(current + 1))
354 return queue.OrderToPosition(current + 1);
355 else if (queue.repeat)
356 return queue.OrderToPosition(0);
357
358 return -1;
359 }
360
361 void
BorderPause(PlayerControl & pc)362 playlist::BorderPause(PlayerControl &pc) noexcept
363 {
364 if (queue.single == SingleMode::ONE_SHOT) {
365 queue.single = SingleMode::OFF;
366 pc.LockSetBorderPause(false);
367
368 listener.OnQueueOptionsChanged();
369 }
370 }
371