PlaylistEdit.cxx 10.6 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2021 The Music Player Daemon Project
3 4 5 6 7 8 9 10 11 12 13
 * http://www.musicpd.org
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
14 15 16 17
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 19 20 21 22 23 24 25
 */

/*
 * Functions for editing the playlist (adding, removing, reordering
 * songs in the queue).
 *
 */

26
#include "Playlist.hxx"
27
#include "Listener.hxx"
28
#include "PlaylistError.hxx"
29
#include "player/Control.hxx"
30
#include "protocol/RangeArg.hxx"
31
#include "song/DetachedSong.hxx"
32
#include "SongLoader.hxx"
33

34 35
#include <stdlib.h>

36
void
37
playlist::OnModified() noexcept
38
{
39 40 41 42 43 44
	if (bulk_edit) {
		/* postponed to CommitBulk() */
		bulk_modified = true;
		return;
	}

45
	queue.IncrementVersion();
46

47
	listener.OnQueueModified();
48 49
}

50
void
51
playlist::Clear(PlayerControl &pc) noexcept
52
{
53
	Stop(pc);
54

55 56
	queue.Clear();
	current = -1;
57

58
	OnModified();
59 60
}

61
void
62
playlist::BeginBulk() noexcept
63 64 65 66 67 68 69 70
{
	assert(!bulk_edit);

	bulk_edit = true;
	bulk_modified = false;
}

void
71
playlist::CommitBulk(PlayerControl &pc) noexcept
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
{
	assert(bulk_edit);

	bulk_edit = false;
	if (!bulk_modified)
		return;

	if (queued < 0)
		/* if no song was queued, UpdateQueuedSong() is being
		   ignored in "bulk" edit mode; now that we have
		   shuffled all new songs, we can pick a random one
		   (instead of always picking the first one that was
		   added) */
		UpdateQueuedSong(pc, nullptr);

	OnModified();
}

90
unsigned
91
playlist::AppendSong(PlayerControl &pc, DetachedSong &&song)
92 93 94
{
	unsigned id;

95 96 97
	if (queue.IsFull())
		throw PlaylistError(PlaylistResult::TOO_LARGE,
				    "Playlist is too large");
98

99
	const DetachedSong *const queued_song = GetQueuedSong();
100

101
	id = queue.Append(std::move(song), 0);
102

103
	if (queue.random) {
104
		/* shuffle the new song into the list of remaining
105 106 107
		   songs to play */

		unsigned start;
108 109
		if (queued >= 0)
			start = queued + 1;
110
		else
111 112
			start = current + 1;
		if (start < queue.GetLength())
113
			queue.ShuffleOrderLastWithPriority(start, queue.GetLength());
114 115
	}

116 117
	UpdateQueuedSong(pc, queued_song);
	OnModified();
118

119
	return id;
120 121
}

122 123
unsigned
playlist::AppendURI(PlayerControl &pc, const SongLoader &loader,
124
		    const char *uri)
Max Kellermann's avatar
Max Kellermann committed
125
{
126
	return AppendSong(pc, loader.LoadSong(uri));
Max Kellermann's avatar
Max Kellermann committed
127 128
}

129
void
130
playlist::SwapPositions(PlayerControl &pc, unsigned song1, unsigned song2)
131
{
132
	if (!queue.IsValidPosition(song1) || !queue.IsValidPosition(song2))
133
		throw PlaylistError::BadRange();
134

135
	const DetachedSong *const queued_song = GetQueuedSong();
136

137
	queue.SwapPositions(song1, song2);
138

139 140
	if (queue.random) {
		/* update the queue order, so that current
141 142
		   still points to the current song order */

143 144
		queue.SwapOrders(queue.PositionToOrder(song1),
				 queue.PositionToOrder(song2));
145
	} else if (current >= 0){
146 147
		/* correct the "current" song order */

148
		if (unsigned(current) == song1)
149
			current = song2;
150
		else if (unsigned(current) == song2)
151
			current = song1;
152 153
	}

154 155
	UpdateQueuedSong(pc, queued_song);
	OnModified();
156 157
}

158
void
159
playlist::SwapIds(PlayerControl &pc, unsigned id1, unsigned id2)
160
{
161 162
	int song1 = queue.IdToPosition(id1);
	int song2 = queue.IdToPosition(id2);
163 164

	if (song1 < 0 || song2 < 0)
165
		throw PlaylistError::NoSuchSong();
166

167
	SwapPositions(pc, song1, song2);
168 169
}

170
void
171
playlist::SetPriorityRange(PlayerControl &pc,
172
			   RangeArg range,
173
			   uint8_t priority)
174
{
175
	if (!range.CheckClip(GetLength()))
176
		throw PlaylistError::BadRange();
177

178
	if (range.IsEmpty())
179
		return;
180 181 182

	/* remember "current" and "queued" */

183
	const int current_position = GetCurrentPosition();
184
	const DetachedSong *const queued_song = GetQueuedSong();
185 186 187

	/* apply the priority changes */

188
	queue.SetPriorityRange(range.start, range.end, priority, current);
189 190 191 192

	/* restore "current" and choose a new "queued" */

	if (current_position >= 0)
193
		current = queue.PositionToOrder(current_position);
194

195 196
	UpdateQueuedSong(pc, queued_song);
	OnModified();
197 198
}

199
void
200
playlist::SetPriorityId(PlayerControl &pc,
201
			unsigned song_id, uint8_t priority)
202
{
203
	int song_position = queue.IdToPosition(song_id);
204
	if (song_position < 0)
205
		throw PlaylistError::NoSuchSong();
206

207
	SetPriorityRange(pc, {unsigned(song_position), song_position + 1U}, priority);
208 209
}

210
void
211
playlist::DeleteInternal(PlayerControl &pc,
212
			 unsigned song, const DetachedSong **queued_p) noexcept
213
{
214
	assert(song < GetLength());
215

216
	unsigned songOrder = queue.PositionToOrder(song);
217

218
	if (playing && current == (int)songOrder) {
219
		const bool paused = pc.GetState() == PlayerState::PAUSE;
220

221 222
		/* the current song is going to be deleted: see which
		   song is going to be played instead */
223

224 225 226
		current = queue.GetNextOrder(current);
		if (current == (int)songOrder)
			current = -1;
227

228
		if (current >= 0 && !paused)
229
			/* play the song after the deleted one */
230 231 232 233 234
			try {
				PlayOrder(pc, current);
			} catch (...) {
				/* TODO: log error? */
			}
235 236 237
		else {
			/* stop the player */

238
			pc.LockStop();
239 240
			playing = false;
		}
241

242
		*queued_p = nullptr;
243
	} else if (current == (int)songOrder)
244 245
		/* there's a "current song" but we're not playing
		   currently - clear "current" */
246
		current = -1;
247 248 249

	/* now do it: remove the song */

250
	queue.DeletePosition(song);
251 252 253

	/* update the "current" and "queued" variables */

254 255
	if (current > (int)songOrder)
		current--;
256 257
}

258
void
259
playlist::DeletePosition(PlayerControl &pc, unsigned song)
260
{
261
	if (song >= queue.GetLength())
262
		throw PlaylistError::BadRange();
263

264
	const DetachedSong *queued_song = GetQueuedSong();
265

266
	DeleteInternal(pc, song, &queued_song);
267

268 269
	UpdateQueuedSong(pc, queued_song);
	OnModified();
270 271
}

272
void
273
playlist::DeleteRange(PlayerControl &pc, RangeArg range)
274
{
275
	if (!range.CheckClip(GetLength()))
276
		throw PlaylistError::BadRange();
277

278
	if (range.IsEmpty())
279
		return;
280

281
	const DetachedSong *queued_song = GetQueuedSong();
282 283

	do {
284 285
		DeleteInternal(pc, --range.end, &queued_song);
	} while (range.end != range.start);
286

287 288
	UpdateQueuedSong(pc, queued_song);
	OnModified();
289 290
}

291
void
292
playlist::DeleteId(PlayerControl &pc, unsigned id)
293
{
294
	int song = queue.IdToPosition(id);
295
	if (song < 0)
296
		throw PlaylistError::NoSuchSong();
297

298
	DeletePosition(pc, song);
299 300 301
}

void
302
playlist::StaleSong(PlayerControl &pc, const char *uri) noexcept
303
{
304 305 306 307 308 309 310 311
	/* don't remove the song if it's currently being played, to
	   avoid disrupting playback; a deleted file may still be
	   played if it's still open */
	// TODO: mark the song as "stale" and postpone deletion
	int current_position = playing
		? GetCurrentPosition()
		: -1;

312
	for (int i = queue.GetLength() - 1; i >= 0; --i)
313
		if (i != current_position && queue.Get(i).IsURI(uri))
314
			DeletePosition(pc, i);
315 316
}

317
void
318
playlist::MoveRange(PlayerControl &pc, RangeArg range, int to)
319
{
320 321
	if (!queue.IsValidPosition(range.start) ||
	    !queue.IsValidPosition(range.end - 1))
322
		throw PlaylistError::BadRange();
323

324
	if ((to >= 0 && to + range.Count() - 1 >= GetLength()) ||
325
	    (to < 0 && unsigned(std::abs(to)) > GetLength()))
326
		throw PlaylistError::BadRange();
327

328
	if ((int)range.start == to)
329
		/* nothing happens */
330
		return;
331

332
	const DetachedSong *const queued_song = GetQueuedSong();
333 334 335 336 337

	/*
	 * (to < 0) => move to offset from current song
	 * (-playlist.length == to) => move to position BEFORE current song
	 */
338
	const int currentSong = GetCurrentPosition();
339 340 341 342
	if (to < 0) {
		if (currentSong < 0)
			/* can't move relative to current song,
			   because there is no current song */
343
			throw PlaylistError::BadRange();
344

345
		if (range.Contains(currentSong))
346
			/* no-op, can't be moved to offset of itself */
347
			return;
348
		to = (currentSong + std::abs(to)) % GetLength();
349 350
		if (range.start < (unsigned)to)
			to -= range.Count();
351 352
	}

353
	queue.MoveRange(range.start, range.end, to);
354

355 356
	if (!queue.random && current >= 0) {
		/* update current */
357 358 359
		if (range.Contains(current))
			current += unsigned(to) - range.start;
		else if (unsigned(current) >= range.end &&
360
			 unsigned(current) <= unsigned(to))
361
			current -= range.Count();
362
		else if (unsigned(current) >= unsigned(to) &&
363 364
			 unsigned(current) < range.start)
			current += range.Count();
365 366
	}

367 368
	UpdateQueuedSong(pc, queued_song);
	OnModified();
369 370
}

371
void
372
playlist::MoveId(PlayerControl &pc, unsigned id1, int to)
373
{
374
	int song = queue.IdToPosition(id1);
375
	if (song < 0)
376
		throw PlaylistError::NoSuchSong();
377

378
	MoveRange(pc, RangeArg::Single(song), to);
379 380
}

Max Kellermann's avatar
Max Kellermann committed
381
void
382
playlist::Shuffle(PlayerControl &pc, RangeArg range)
383
{
384 385
	if (!range.CheckClip(GetLength()))
		throw PlaylistError::BadRange();
386

387
	if (range.HasAtLeast(2))
388
		/* needs at least two entries. */
389 390
		return;

391
	const DetachedSong *const queued_song = GetQueuedSong();
392 393
	if (playing && current >= 0) {
		unsigned current_position = queue.OrderToPosition(current);
394

395
		if (range.Contains(current_position)) {
396
			/* put current playing song first */
397
			queue.SwapPositions(range.start, current_position);
398

399
			if (queue.random) {
400
				current = queue.PositionToOrder(range.start);
401
			} else
402
				current = range.start;
403 404

			/* start shuffle after the current song */
405
			range.start++;
406
		}
407
	} else {
408
		/* no playback currently: reset current */
409

410
		current = -1;
411 412
	}

413
	queue.ShuffleRange(range.start, range.end);
414

415 416
	UpdateQueuedSong(pc, queued_song);
	OnModified();
417
}
418

419
void
420
playlist::SetSongIdRange(PlayerControl &pc, unsigned id,
421
			 SongTime start, SongTime end)
422
{
423
	assert(end.IsZero() || start < end);
424 425

	int position = queue.IdToPosition(id);
426 427
	if (position < 0)
		throw PlaylistError::NoSuchSong();
428

429 430
	bool was_queued = false;

431
	if (playing) {
432 433 434
		if (position == current)
			throw PlaylistError(PlaylistResult::DENIED,
					    "Cannot edit the current song");
435 436 437 438 439

		if (position == queued) {
			/* if we're manipulating the "queued" song,
			   the decoder thread may be decoding it
			   already; cancel that */
440
			pc.LockCancel();
441
			queued = -1;
442 443 444 445

			/* schedule a call to UpdateQueuedSong() to
			   re-queue the song with its new range */
			was_queued = true;
446 447 448 449
		}
	}

	DetachedSong &song = queue.Get(position);
450 451 452

	const auto duration = song.GetTag().duration;
	if (!duration.IsNegative()) {
453 454
		/* validate the offsets */

455 456 457
		if (start > duration)
			throw PlaylistError(PlaylistResult::BAD_RANGE,
					    "Invalid start offset");
458

459
		if (end >= duration)
460
			end = SongTime::zero();
461 462 463
	}

	/* edit it */
464 465
	song.SetStartTime(start);
	song.SetEndTime(end);
466 467

	/* announce the change to all interested subsystems */
468 469
	if (was_queued)
		UpdateQueuedSong(pc, nullptr);
470 471 472
	queue.ModifyAtPosition(position);
	OnModified();
}