PlaylistEdit.cxx 10.7 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright (C) 2003-2015 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 "PlaylistError.hxx"
29
#include "player/Control.hxx"
Max Kellermann's avatar
Max Kellermann committed
30
#include "util/UriUtil.hxx"
31
#include "util/Error.hxx"
32
#include "DetachedSong.hxx"
33
#include "SongLoader.hxx"
Max Kellermann's avatar
Max Kellermann committed
34
#include "Idle.hxx"
35

36 37
#include <stdlib.h>

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

47
	queue.IncrementVersion();
48 49 50 51

	idle_add(IDLE_PLAYLIST);
}

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

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

60
	OnModified();
61 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
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();
}

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

97 98 99 100 101
	if (queue.IsFull()) {
		error.Set(playlist_domain, int(PlaylistResult::TOO_LARGE),
			  "Playlist is too large");
		return 0;
	}
102

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

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

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

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

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

123
	return id;
124 125
}

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

135
	unsigned result = AppendSong(pc, std::move(*song), error);
136
	delete song;
137 138

	return result;
Max Kellermann's avatar
Max Kellermann committed
139 140
}

141
PlaylistResult
142
playlist::SwapPositions(PlayerControl &pc, unsigned song1, unsigned song2)
143
{
144
	if (!queue.IsValidPosition(song1) || !queue.IsValidPosition(song2))
145
		return PlaylistResult::BAD_RANGE;
146

147
	const DetachedSong *const queued_song = GetQueuedSong();
148

149
	queue.SwapPositions(song1, song2);
150

151 152
	if (queue.random) {
		/* update the queue order, so that current
153 154
		   still points to the current song order */

155 156
		queue.SwapOrders(queue.PositionToOrder(song1),
				 queue.PositionToOrder(song2));
157 158 159
	} else {
		/* correct the "current" song order */

160 161 162 163
		if (current == (int)song1)
			current = song2;
		else if (current == (int)song2)
			current = song1;
164 165
	}

166 167
	UpdateQueuedSong(pc, queued_song);
	OnModified();
168

169
	return PlaylistResult::SUCCESS;
170 171
}

172
PlaylistResult
173
playlist::SwapIds(PlayerControl &pc, unsigned id1, unsigned id2)
174
{
175 176
	int song1 = queue.IdToPosition(id1);
	int song2 = queue.IdToPosition(id2);
177 178

	if (song1 < 0 || song2 < 0)
179
		return PlaylistResult::NO_SUCH_SONG;
180

181
	return SwapPositions(pc, song1, song2);
182 183
}

184
PlaylistResult
185
playlist::SetPriorityRange(PlayerControl &pc,
186 187
			   unsigned start, unsigned end,
			   uint8_t priority)
188
{
189
	if (start >= GetLength())
190
		return PlaylistResult::BAD_RANGE;
191

192 193
	if (end > GetLength())
		end = GetLength();
194 195

	if (start >= end)
196
		return PlaylistResult::SUCCESS;
197 198 199

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

200
	const int current_position = GetCurrentPosition();
201
	const DetachedSong *const queued_song = GetQueuedSong();
202 203 204

	/* apply the priority changes */

205
	queue.SetPriorityRange(start, end, priority, current);
206 207 208 209

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

	if (current_position >= 0)
210
		current = queue.PositionToOrder(current_position);
211

212 213
	UpdateQueuedSong(pc, queued_song);
	OnModified();
214

215
	return PlaylistResult::SUCCESS;
216 217
}

218
PlaylistResult
219
playlist::SetPriorityId(PlayerControl &pc,
220
			unsigned song_id, uint8_t priority)
221
{
222
	int song_position = queue.IdToPosition(song_id);
223
	if (song_position < 0)
224
		return PlaylistResult::NO_SUCH_SONG;
225

226 227
	return SetPriorityRange(pc, song_position, song_position + 1,
				priority);
228 229 230

}

231
void
232
playlist::DeleteInternal(PlayerControl &pc,
233
			 unsigned song, const DetachedSong **queued_p)
234
{
235
	assert(song < GetLength());
236

237
	unsigned songOrder = queue.PositionToOrder(song);
238

239
	if (playing && current == (int)songOrder) {
240
		const bool paused = pc.GetState() == PlayerState::PAUSE;
241

242 243
		/* the current song is going to be deleted: see which
		   song is going to be played instead */
244

245 246 247
		current = queue.GetNextOrder(current);
		if (current == (int)songOrder)
			current = -1;
248

249
		if (current >= 0 && !paused)
250
			/* play the song after the deleted one */
251
			PlayOrder(pc, current);
252 253 254 255 256 257
		else {
			/* stop the player */

			pc.Stop();
			playing = false;
		}
258

259
		*queued_p = nullptr;
260
	} else if (current == (int)songOrder)
261 262
		/* there's a "current song" but we're not playing
		   currently - clear "current" */
263
		current = -1;
264 265 266

	/* now do it: remove the song */

267
	queue.DeletePosition(song);
268 269 270

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

271 272
	if (current > (int)songOrder)
		current--;
273 274
}

275
PlaylistResult
276
playlist::DeletePosition(PlayerControl &pc, unsigned song)
277
{
278
	if (song >= queue.GetLength())
279
		return PlaylistResult::BAD_RANGE;
280

281
	const DetachedSong *queued_song = GetQueuedSong();
282

283
	DeleteInternal(pc, song, &queued_song);
284

285 286
	UpdateQueuedSong(pc, queued_song);
	OnModified();
287

288
	return PlaylistResult::SUCCESS;
289 290
}

291
PlaylistResult
292
playlist::DeleteRange(PlayerControl &pc, unsigned start, unsigned end)
293
{
294
	if (start >= queue.GetLength())
295
		return PlaylistResult::BAD_RANGE;
296

297 298
	if (end > queue.GetLength())
		end = queue.GetLength();
299 300

	if (start >= end)
301
		return PlaylistResult::SUCCESS;
302

303
	const DetachedSong *queued_song = GetQueuedSong();
304 305

	do {
306
		DeleteInternal(pc, --end, &queued_song);
307 308
	} while (end != start);

309 310
	UpdateQueuedSong(pc, queued_song);
	OnModified();
311

312
	return PlaylistResult::SUCCESS;
313 314
}

315
PlaylistResult
316
playlist::DeleteId(PlayerControl &pc, unsigned id)
317
{
318
	int song = queue.IdToPosition(id);
319
	if (song < 0)
320
		return PlaylistResult::NO_SUCH_SONG;
321

322
	return DeletePosition(pc, song);
323 324 325
}

void
326
playlist::DeleteSong(PlayerControl &pc, const char *uri)
327
{
328
	for (int i = queue.GetLength() - 1; i >= 0; --i)
329
		if (queue.Get(i).IsURI(uri))
330
			DeletePosition(pc, i);
331 332
}

333
PlaylistResult
334
playlist::MoveRange(PlayerControl &pc, unsigned start, unsigned end, int to)
335
{
336
	if (!queue.IsValidPosition(start) || !queue.IsValidPosition(end - 1))
337
		return PlaylistResult::BAD_RANGE;
338

339 340
	if ((to >= 0 && to + end - start - 1 >= GetLength()) ||
	    (to < 0 && unsigned(abs(to)) > GetLength()))
341
		return PlaylistResult::BAD_RANGE;
342

343 344
	if ((int)start == to)
		/* nothing happens */
345
		return PlaylistResult::SUCCESS;
346

347
	const DetachedSong *const queued_song = GetQueuedSong();
348 349 350 351 352

	/*
	 * (to < 0) => move to offset from current song
	 * (-playlist.length == to) => move to position BEFORE current song
	 */
353
	const int currentSong = GetCurrentPosition();
354 355 356 357
	if (to < 0) {
		if (currentSong < 0)
			/* can't move relative to current song,
			   because there is no current song */
358
			return PlaylistResult::BAD_RANGE;
359

360
		if (start <= (unsigned)currentSong && (unsigned)currentSong < end)
361
			/* no-op, can't be moved to offset of itself */
362
			return PlaylistResult::SUCCESS;
363
		to = (currentSong + abs(to)) % GetLength();
364 365
		if (start < (unsigned)to)
			to--;
366 367
	}

368
	queue.MoveRange(start, end, to);
369

370
	if (!queue.random) {
371
		/* update current/queued */
372 373 374 375 376 377
		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;
378 379
	}

380 381
	UpdateQueuedSong(pc, queued_song);
	OnModified();
382

383
	return PlaylistResult::SUCCESS;
384 385
}

386
PlaylistResult
387
playlist::MoveId(PlayerControl &pc, unsigned id1, int to)
388
{
389
	int song = queue.IdToPosition(id1);
390
	if (song < 0)
391
		return PlaylistResult::NO_SUCH_SONG;
392

393
	return MoveRange(pc, song, song + 1, to);
394 395
}

Max Kellermann's avatar
Max Kellermann committed
396
void
397
playlist::Shuffle(PlayerControl &pc, unsigned start, unsigned end)
398
{
399
	if (end > GetLength())
400
		/* correct the "end" offset */
401
		end = GetLength();
402

403
	if (start + 1 >= end)
404
		/* needs at least two entries. */
405 406
		return;

407
	const DetachedSong *const queued_song = GetQueuedSong();
408 409
	if (playing && current >= 0) {
		unsigned current_position = queue.OrderToPosition(current);
410

411
		if (current_position >= start && current_position < end) {
412
			/* put current playing song first */
413
			queue.SwapPositions(start, current_position);
414

415 416
			if (queue.random) {
				current = queue.PositionToOrder(start);
417
			} else
418
				current = start;
419 420 421 422

			/* start shuffle after the current song */
			start++;
		}
423
	} else {
424
		/* no playback currently: reset current */
425

426
		current = -1;
427 428
	}

429
	queue.ShuffleRange(start, end);
430

431 432
	UpdateQueuedSong(pc, queued_song);
	OnModified();
433
}
434 435 436

bool
playlist::SetSongIdRange(PlayerControl &pc, unsigned id,
437
			 SongTime start, SongTime end,
438 439
			 Error &error)
{
440
	assert(end.IsZero() || start < end);
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465

	int position = queue.IdToPosition(id);
	if (position < 0) {
		error.Set(playlist_domain, int(PlaylistResult::NO_SUCH_SONG),
			  "No such song");
		return false;
	}

	if (playing) {
		if (position == current) {
			error.Set(playlist_domain, int(PlaylistResult::DENIED),
				  "Cannot edit the current song");
			return false;
		}

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

	DetachedSong &song = queue.Get(position);
466 467 468

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

471
		if (start > duration) {
472 473 474 475 476 477
			error.Set(playlist_domain,
				  int(PlaylistResult::BAD_RANGE),
				  "Invalid start offset");
			return false;
		}

478
		if (end >= duration)
479
			end = SongTime::zero();
480 481 482
	}

	/* edit it */
483 484
	song.SetStartTime(start);
	song.SetEndTime(end);
485 486 487 488 489 490 491

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