PlaylistEdit.cxx 10.1 KB
Newer Older
1
/*
2
 * Copyright 2003-2016 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 "config.h"
27
#include "Playlist.hxx"
28
#include "Listener.hxx"
29
#include "PlaylistError.hxx"
30
#include "player/Control.hxx"
31
#include "util/Error.hxx"
32
#include "DetachedSong.hxx"
33
#include "SongLoader.hxx"
34

35 36
#include <memory>

37 38
#include <stdlib.h>

39 40
void
playlist::OnModified()
41
{
42 43 44 45 46 47
	if (bulk_edit) {
		/* postponed to CommitBulk() */
		bulk_modified = true;
		return;
	}

48
	queue.IncrementVersion();
49

50
	listener.OnQueueModified();
51 52
}

53
void
54
playlist::Clear(PlayerControl &pc)
55
{
56
	Stop(pc);
57

58 59
	queue.Clear();
	current = -1;
60

61
	OnModified();
62 63
}

64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
void
playlist::BeginBulk()
{
	assert(!bulk_edit);

	bulk_edit = true;
	bulk_modified = false;
}

void
playlist::CommitBulk(PlayerControl &pc)
{
	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();
}

93
unsigned
94
playlist::AppendSong(PlayerControl &pc, DetachedSong &&song)
95 96 97
{
	unsigned id;

98 99 100
	if (queue.IsFull())
		throw PlaylistError(PlaylistResult::TOO_LARGE,
				    "Playlist is too large");
101

102
	const DetachedSong *const queued_song = GetQueuedSong();
103

104
	id = queue.Append(std::move(song), 0);
105

106
	if (queue.random) {
107
		/* shuffle the new song into the list of remaining
108 109 110
		   songs to play */

		unsigned start;
111 112
		if (queued >= 0)
			start = queued + 1;
113
		else
114 115 116
			start = current + 1;
		if (start < queue.GetLength())
			queue.ShuffleOrderLast(start, queue.GetLength());
117 118
	}

119 120
	UpdateQueuedSong(pc, queued_song);
	OnModified();
121

122
	return id;
123 124
}

125 126 127 128
unsigned
playlist::AppendURI(PlayerControl &pc, const SongLoader &loader,
		    const char *uri,
		    Error &error)
Max Kellermann's avatar
Max Kellermann committed
129
{
130
	std::unique_ptr<DetachedSong> song(loader.LoadSong(uri, error));
131 132
	if (song == nullptr)
		return 0;
Max Kellermann's avatar
Max Kellermann committed
133

134
	return AppendSong(pc, std::move(*song));
Max Kellermann's avatar
Max Kellermann committed
135 136
}

137
void
138
playlist::SwapPositions(PlayerControl &pc, unsigned song1, unsigned song2)
139
{
140
	if (!queue.IsValidPosition(song1) || !queue.IsValidPosition(song2))
141
		throw PlaylistError::BadRange();
142

143
	const DetachedSong *const queued_song = GetQueuedSong();
144

145
	queue.SwapPositions(song1, song2);
146

147 148
	if (queue.random) {
		/* update the queue order, so that current
149 150
		   still points to the current song order */

151 152
		queue.SwapOrders(queue.PositionToOrder(song1),
				 queue.PositionToOrder(song2));
153 154 155
	} else {
		/* correct the "current" song order */

156 157 158 159
		if (current == (int)song1)
			current = song2;
		else if (current == (int)song2)
			current = song1;
160 161
	}

162 163
	UpdateQueuedSong(pc, queued_song);
	OnModified();
164 165
}

166
void
167
playlist::SwapIds(PlayerControl &pc, unsigned id1, unsigned id2)
168
{
169 170
	int song1 = queue.IdToPosition(id1);
	int song2 = queue.IdToPosition(id2);
171 172

	if (song1 < 0 || song2 < 0)
173
		throw PlaylistError::NoSuchSong();
174

175
	SwapPositions(pc, song1, song2);
176 177
}

178
void
179
playlist::SetPriorityRange(PlayerControl &pc,
180 181
			   unsigned start, unsigned end,
			   uint8_t priority)
182
{
183
	if (start >= GetLength())
184
		throw PlaylistError::BadRange();
185

186 187
	if (end > GetLength())
		end = GetLength();
188 189

	if (start >= end)
190
		return;
191 192 193

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

194
	const int current_position = GetCurrentPosition();
195
	const DetachedSong *const queued_song = GetQueuedSong();
196 197 198

	/* apply the priority changes */

199
	queue.SetPriorityRange(start, end, priority, current);
200 201 202 203

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

	if (current_position >= 0)
204
		current = queue.PositionToOrder(current_position);
205

206 207
	UpdateQueuedSong(pc, queued_song);
	OnModified();
208 209
}

210
void
211
playlist::SetPriorityId(PlayerControl &pc,
212
			unsigned song_id, uint8_t priority)
213
{
214
	int song_position = queue.IdToPosition(song_id);
215
	if (song_position < 0)
216
		throw PlaylistError::NoSuchSong();
217

218
	SetPriorityRange(pc, song_position, song_position + 1, priority);
219 220
}

221
void
222
playlist::DeleteInternal(PlayerControl &pc,
223
			 unsigned song, const DetachedSong **queued_p)
224
{
225
	assert(song < GetLength());
226

227
	unsigned songOrder = queue.PositionToOrder(song);
228

229
	if (playing && current == (int)songOrder) {
230
		const bool paused = pc.GetState() == PlayerState::PAUSE;
231

232 233
		/* the current song is going to be deleted: see which
		   song is going to be played instead */
234

235 236 237
		current = queue.GetNextOrder(current);
		if (current == (int)songOrder)
			current = -1;
238

239
		if (current >= 0 && !paused)
240
			/* play the song after the deleted one */
241 242
			/* TODO: log error? */
			PlayOrder(pc, current, IgnoreError());
243 244 245
		else {
			/* stop the player */

246
			pc.LockStop();
247 248
			playing = false;
		}
249

250
		*queued_p = nullptr;
251
	} else if (current == (int)songOrder)
252 253
		/* there's a "current song" but we're not playing
		   currently - clear "current" */
254
		current = -1;
255 256 257

	/* now do it: remove the song */

258
	queue.DeletePosition(song);
259 260 261

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

262 263
	if (current > (int)songOrder)
		current--;
264 265
}

266
void
267
playlist::DeletePosition(PlayerControl &pc, unsigned song)
268
{
269
	if (song >= queue.GetLength())
270
		throw PlaylistError::BadRange();
271

272
	const DetachedSong *queued_song = GetQueuedSong();
273

274
	DeleteInternal(pc, song, &queued_song);
275

276 277
	UpdateQueuedSong(pc, queued_song);
	OnModified();
278 279
}

280
void
281
playlist::DeleteRange(PlayerControl &pc, unsigned start, unsigned end)
282
{
283
	if (start >= queue.GetLength())
284
		throw PlaylistError::BadRange();
285

286 287
	if (end > queue.GetLength())
		end = queue.GetLength();
288 289

	if (start >= end)
290
		return;
291

292
	const DetachedSong *queued_song = GetQueuedSong();
293 294

	do {
295
		DeleteInternal(pc, --end, &queued_song);
296 297
	} while (end != start);

298 299
	UpdateQueuedSong(pc, queued_song);
	OnModified();
300 301
}

302
void
303
playlist::DeleteId(PlayerControl &pc, unsigned id)
304
{
305
	int song = queue.IdToPosition(id);
306
	if (song < 0)
307
		throw PlaylistError::NoSuchSong();
308

309
	DeletePosition(pc, song);
310 311 312
}

void
313
playlist::DeleteSong(PlayerControl &pc, const char *uri)
314
{
315
	for (int i = queue.GetLength() - 1; i >= 0; --i)
316
		if (queue.Get(i).IsURI(uri))
317
			DeletePosition(pc, i);
318 319
}

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

326 327
	if ((to >= 0 && to + end - start - 1 >= GetLength()) ||
	    (to < 0 && unsigned(abs(to)) > GetLength()))
328
		throw PlaylistError::BadRange();
329

330 331
	if ((int)start == to)
		/* nothing happens */
332
		return;
333

334
	const DetachedSong *const queued_song = GetQueuedSong();
335 336 337 338 339

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

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

355
	queue.MoveRange(start, end, to);
356

357
	if (!queue.random) {
358
		/* update current/queued */
359 360 361 362 363 364
		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;
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, song, song + 1, to);
379 380
}

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

388
	if (start + 1 >= end)
389
		/* needs at least two entries. */
390 391
		return;

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

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

400 401
			if (queue.random) {
				current = queue.PositionToOrder(start);
402
			} else
403
				current = start;
404 405 406 407

			/* start shuffle after the current song */
			start++;
		}
408
	} else {
409
		/* no playback currently: reset current */
410

411
		current = -1;
412 413
	}

414
	queue.ShuffleRange(start, end);
415

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

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

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

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

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

	DetachedSong &song = queue.Get(position);
445 446 447

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

450 451 452
		if (start > duration)
			throw PlaylistError(PlaylistResult::BAD_RANGE,
					    "Invalid start offset");
453

454
		if (end >= duration)
455
			end = SongTime::zero();
456 457 458
	}

	/* edit it */
459 460
	song.SetStartTime(start);
	song.SetEndTime(end);
461 462 463 464 465 466

	/* announce the change to all interested subsystems */
	UpdateQueuedSong(pc, nullptr);
	queue.ModifyAtPosition(position);
	OnModified();
}