PlaylistEdit.cxx 10.5 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2020 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 "song/DetachedSong.hxx"
31
#include "SongLoader.hxx"
32

33 34
#include <stdlib.h>

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

44
	queue.IncrementVersion();
45

46
	listener.OnQueueModified();
47 48
}

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

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

57
	OnModified();
58 59
}

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

	bulk_edit = true;
	bulk_modified = false;
}

void
70
playlist::CommitBulk(PlayerControl &pc) noexcept
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
{
	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();
}

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

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

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

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

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

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

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

118
	return id;
119 120
}

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

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

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

136
	queue.SwapPositions(song1, song2);
137

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

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

147 148 149 150
		if (current == (int)song1)
			current = song2;
		else if (current == (int)song2)
			current = song1;
151 152
	}

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

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

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

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

169
void
170
playlist::SetPriorityRange(PlayerControl &pc,
171 172
			   unsigned start, unsigned end,
			   uint8_t priority)
173
{
174
	if (start >= GetLength())
175
		throw PlaylistError::BadRange();
176

177 178
	if (end > GetLength())
		end = GetLength();
179 180

	if (start >= end)
181
		return;
182 183 184

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

185
	const int current_position = GetCurrentPosition();
186
	const DetachedSong *const queued_song = GetQueuedSong();
187 188 189

	/* apply the priority changes */

190
	queue.SetPriorityRange(start, end, priority, current);
191 192 193 194

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

	if (current_position >= 0)
195
		current = queue.PositionToOrder(current_position);
196

197 198
	UpdateQueuedSong(pc, queued_song);
	OnModified();
199 200
}

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

209
	SetPriorityRange(pc, song_position, song_position + 1, priority);
210 211
}

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

218
	unsigned songOrder = queue.PositionToOrder(song);
219

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

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

226 227 228
		current = queue.GetNextOrder(current);
		if (current == (int)songOrder)
			current = -1;
229

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

240
			pc.LockStop();
241 242
			playing = false;
		}
243

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

	/* now do it: remove the song */

252
	queue.DeletePosition(song);
253 254 255

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

256 257
	if (current > (int)songOrder)
		current--;
258 259
}

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

266
	const DetachedSong *queued_song = GetQueuedSong();
267

268
	DeleteInternal(pc, song, &queued_song);
269

270 271
	UpdateQueuedSong(pc, queued_song);
	OnModified();
272 273
}

274
void
275
playlist::DeleteRange(PlayerControl &pc, unsigned start, unsigned end)
276
{
277
	if (start >= queue.GetLength())
278
		throw PlaylistError::BadRange();
279

280 281
	if (end > queue.GetLength())
		end = queue.GetLength();
282 283

	if (start >= end)
284
		return;
285

286
	const DetachedSong *queued_song = GetQueuedSong();
287 288

	do {
289
		DeleteInternal(pc, --end, &queued_song);
290 291
	} while (end != start);

292 293
	UpdateQueuedSong(pc, queued_song);
	OnModified();
294 295
}

296
void
297
playlist::DeleteId(PlayerControl &pc, unsigned id)
298
{
299
	int song = queue.IdToPosition(id);
300
	if (song < 0)
301
		throw PlaylistError::NoSuchSong();
302

303
	DeletePosition(pc, song);
304 305 306
}

void
307
playlist::StaleSong(PlayerControl &pc, const char *uri) noexcept
308
{
309 310 311 312 313 314 315 316
	/* 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;

317
	for (int i = queue.GetLength() - 1; i >= 0; --i)
318
		if (i != current_position && queue.Get(i).IsURI(uri))
319
			DeletePosition(pc, i);
320 321
}

322
void
323 324
playlist::MoveRange(PlayerControl &pc,
		    unsigned start, unsigned end, int to)
325
{
326
	if (!queue.IsValidPosition(start) || !queue.IsValidPosition(end - 1))
327
		throw PlaylistError::BadRange();
328

329
	if ((to >= 0 && to + end - start - 1 >= GetLength()) ||
330
	    (to < 0 && unsigned(std::abs(to)) > GetLength()))
331
		throw PlaylistError::BadRange();
332

333 334
	if ((int)start == to)
		/* nothing happens */
335
		return;
336

337
	const DetachedSong *const queued_song = GetQueuedSong();
338 339 340 341 342

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

350
		if (start <= (unsigned)currentSong && (unsigned)currentSong < end)
351
			/* no-op, can't be moved to offset of itself */
352
			return;
353
		to = (currentSong + std::abs(to)) % GetLength();
354
		if (start < (unsigned)to)
355
			to -= end - start;
356 357
	}

358
	queue.MoveRange(start, end, to);
359

360
	if (!queue.random) {
361
		/* update current/queued */
362 363 364 365 366 367
		if ((int)start <= current && (unsigned)current < end)
			current += to - start;
		else if (current >= (int)end && current <= to)
			current -= end - start;
		else if (current >= to && current < (int)start)
			current += end - start;
368 369
	}

370 371
	UpdateQueuedSong(pc, queued_song);
	OnModified();
372 373
}

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

381
	MoveRange(pc, song, song + 1, to);
382 383
}

Max Kellermann's avatar
Max Kellermann committed
384
void
385
playlist::Shuffle(PlayerControl &pc, unsigned start, unsigned end) noexcept
386
{
387
	if (end > GetLength())
388
		/* correct the "end" offset */
389
		end = GetLength();
390

391
	if (start + 1 >= end)
392
		/* needs at least two entries. */
393 394
		return;

395
	const DetachedSong *const queued_song = GetQueuedSong();
396 397
	if (playing && current >= 0) {
		unsigned current_position = queue.OrderToPosition(current);
398

399
		if (current_position >= start && current_position < end) {
400
			/* put current playing song first */
401
			queue.SwapPositions(start, current_position);
402

403 404
			if (queue.random) {
				current = queue.PositionToOrder(start);
405
			} else
406
				current = start;
407 408 409 410

			/* start shuffle after the current song */
			start++;
		}
411
	} else {
412
		/* no playback currently: reset current */
413

414
		current = -1;
415 416
	}

417
	queue.ShuffleRange(start, end);
418

419 420
	UpdateQueuedSong(pc, queued_song);
	OnModified();
421
}
422

423
void
424
playlist::SetSongIdRange(PlayerControl &pc, unsigned id,
425
			 SongTime start, SongTime end)
426
{
427
	assert(end.IsZero() || start < end);
428 429

	int position = queue.IdToPosition(id);
430 431
	if (position < 0)
		throw PlaylistError::NoSuchSong();
432

433 434
	bool was_queued = false;

435
	if (playing) {
436 437 438
		if (position == current)
			throw PlaylistError(PlaylistResult::DENIED,
					    "Cannot edit the current song");
439 440 441 442 443

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

			/* schedule a call to UpdateQueuedSong() to
			   re-queue the song with its new range */
			was_queued = true;
450 451 452 453
		}
	}

	DetachedSong &song = queue.Get(position);
454 455 456

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

459 460 461
		if (start > duration)
			throw PlaylistError(PlaylistResult::BAD_RANGE,
					    "Invalid start offset");
462

463
		if (end >= duration)
464
			end = SongTime::zero();
465 466 467
	}

	/* edit it */
468 469
	song.SetStartTime(start);
	song.SetEndTime(end);
470 471

	/* announce the change to all interested subsystems */
472 473
	if (was_queued)
		UpdateQueuedSong(pc, nullptr);
474 475 476
	queue.ModifyAtPosition(position);
	OnModified();
}