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 &current_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