diff options
Diffstat (limited to 'src/queue')
-rw-r--r-- | src/queue/IdTable.hxx | 91 | ||||
-rw-r--r-- | src/queue/Playlist.cxx | 341 | ||||
-rw-r--r-- | src/queue/Playlist.hxx | 301 | ||||
-rw-r--r-- | src/queue/PlaylistControl.cxx | 272 | ||||
-rw-r--r-- | src/queue/PlaylistEdit.cxx | 491 | ||||
-rw-r--r-- | src/queue/PlaylistState.cxx | 244 | ||||
-rw-r--r-- | src/queue/PlaylistState.hxx | 53 | ||||
-rw-r--r-- | src/queue/PlaylistTag.cxx | 93 | ||||
-rw-r--r-- | src/queue/PlaylistUpdate.cxx | 73 | ||||
-rw-r--r-- | src/queue/Queue.cxx | 482 | ||||
-rw-r--r-- | src/queue/Queue.hxx | 378 | ||||
-rw-r--r-- | src/queue/QueuePrint.cxx | 102 | ||||
-rw-r--r-- | src/queue/QueuePrint.hxx | 54 | ||||
-rw-r--r-- | src/queue/QueueSave.cxx | 126 | ||||
-rw-r--r-- | src/queue/QueueSave.hxx | 43 |
15 files changed, 3144 insertions, 0 deletions
diff --git a/src/queue/IdTable.hxx b/src/queue/IdTable.hxx new file mode 100644 index 000000000..8e445243d --- /dev/null +++ b/src/queue/IdTable.hxx @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * 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. + * + * 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. + */ + +#ifndef MPD_ID_TABLE_HXX +#define MPD_ID_TABLE_HXX + +#include "Compiler.h" + +#include <algorithm> + +#include <assert.h> + +/** + * A table that maps id numbers to position numbers. + */ +class IdTable { + unsigned size; + + unsigned next; + + int *data; + +public: + IdTable(unsigned _size):size(_size), next(1), data(new int[size]) { + std::fill_n(data, size, -1); + } + + ~IdTable() { + delete[] data; + } + + int IdToPosition(unsigned id) const { + return id < size + ? data[id] + : -1; + } + + unsigned GenerateId() { + assert(next > 0); + assert(next < size); + + while (true) { + unsigned id = next; + + ++next; + if (next == size) + next = 1; + + if (data[id] < 0) + return id; + } + } + + unsigned Insert(unsigned position) { + unsigned id = GenerateId(); + data[id] = position; + return id; + } + + void Move(unsigned id, unsigned position) { + assert(id < size); + assert(data[id] >= 0); + + data[id] = position; + } + + void Erase(unsigned id) { + assert(id < size); + assert(data[id] >= 0); + + data[id] = -1; + } +}; + +#endif diff --git a/src/queue/Playlist.cxx b/src/queue/Playlist.cxx new file mode 100644 index 000000000..b2fd673b4 --- /dev/null +++ b/src/queue/Playlist.cxx @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * 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. + * + * 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. + */ + +#include "config.h" +#include "Playlist.hxx" +#include "PlaylistError.hxx" +#include "PlayerControl.hxx" +#include "DetachedSong.hxx" +#include "Idle.hxx" +#include "Log.hxx" + +#include <assert.h> + +void +playlist::TagModified(DetachedSong &&song) +{ + if (!playing) + return; + + assert(current >= 0); + + DetachedSong ¤t_song = queue.GetOrder(current); + if (song.IsSame(current_song)) + current_song.MoveTagFrom(std::move(song)); + + queue.ModifyAtOrder(current); + queue.IncrementVersion(); + idle_add(IDLE_PLAYLIST); +} + +/** + * Queue a song, addressed by its order number. + */ +static void +playlist_queue_song_order(playlist &playlist, PlayerControl &pc, + unsigned order) +{ + assert(playlist.queue.IsValidOrder(order)); + + playlist.queued = order; + + const DetachedSong &song = playlist.queue.GetOrder(order); + + FormatDebug(playlist_domain, "queue song %i:\"%s\"", + playlist.queued, song.GetURI()); + + pc.EnqueueSong(new DetachedSong(song)); +} + +/** + * Called if the player thread has started playing the "queued" song. + */ +static void +playlist_song_started(playlist &playlist, PlayerControl &pc) +{ + assert(pc.next_song == nullptr); + assert(playlist.queued >= -1); + + /* queued song has started: copy queued to current, + and notify the clients */ + + int current = playlist.current; + playlist.current = playlist.queued; + playlist.queued = -1; + + if(playlist.queue.consume) + playlist.DeleteOrder(pc, current); + + idle_add(IDLE_PLAYER); +} + +const DetachedSong * +playlist::GetQueuedSong() const +{ + return playing && queued >= 0 + ? &queue.GetOrder(queued) + : nullptr; +} + +void +playlist::UpdateQueuedSong(PlayerControl &pc, const DetachedSong *prev) +{ + if (!playing) + return; + + if (prev == nullptr && bulk_edit) + /* postponed until CommitBulk() to avoid always + queueing the first song that is being added (in + random mode) */ + return; + + assert(!queue.IsEmpty()); + assert((queued < 0) == (prev == nullptr)); + + const int next_order = current >= 0 + ? queue.GetNextOrder(current) + : 0; + + if (next_order == 0 && queue.random && !queue.single) { + /* shuffle the song order again, so we get a different + order each time the playlist is played + completely */ + const unsigned current_position = + queue.OrderToPosition(current); + + queue.ShuffleOrder(); + + /* make sure that the current still points to + the current song, after the song order has been + shuffled */ + current = queue.PositionToOrder(current_position); + } + + const DetachedSong *const next_song = next_order >= 0 + ? &queue.GetOrder(next_order) + : nullptr; + + if (prev != nullptr && next_song != prev) { + /* clear the currently queued song */ + pc.Cancel(); + queued = -1; + } + + if (next_order >= 0) { + if (next_song != prev) + playlist_queue_song_order(*this, pc, next_order); + else + queued = next_order; + } +} + +void +playlist::PlayOrder(PlayerControl &pc, int order) +{ + playing = true; + queued = -1; + + const DetachedSong &song = queue.GetOrder(order); + + FormatDebug(playlist_domain, "play %i:\"%s\"", order, song.GetURI()); + + pc.Play(new DetachedSong(song)); + current = order; +} + +static void +playlist_resume_playback(playlist &playlist, PlayerControl &pc); + +void +playlist::SyncWithPlayer(PlayerControl &pc) +{ + if (!playing) + /* this event has reached us out of sync: we aren't + playing anymore; ignore the event */ + return; + + pc.Lock(); + const PlayerState pc_state = pc.GetState(); + const DetachedSong *pc_next_song = pc.next_song; + pc.Unlock(); + + if (pc_state == PlayerState::STOP) + /* the player thread has stopped: check if playback + should be restarted with the next song. That can + happen if the playlist isn't filling the queue fast + enough */ + playlist_resume_playback(*this, pc); + else { + /* check if the player thread has already started + playing the queued song */ + if (pc_next_song == nullptr && queued != -1) + playlist_song_started(*this, pc); + + pc.Lock(); + pc_next_song = pc.next_song; + pc.Unlock(); + + /* make sure the queued song is always set (if + possible) */ + if (pc_next_song == nullptr && queued < 0) + UpdateQueuedSong(pc, nullptr); + } +} + +/** + * The player has stopped for some reason. Check the error, and + * decide whether to re-start playback + */ +static void +playlist_resume_playback(playlist &playlist, PlayerControl &pc) +{ + assert(playlist.playing); + assert(pc.GetState() == PlayerState::STOP); + + const auto error = pc.GetErrorType(); + if (error == PlayerError::NONE) + playlist.error_count = 0; + else + ++playlist.error_count; + + if ((playlist.stop_on_error && error != PlayerError::NONE) || + error == PlayerError::OUTPUT || + playlist.error_count >= playlist.queue.GetLength()) + /* too many errors, or critical error: stop + playback */ + playlist.Stop(pc); + else + /* continue playback at the next song */ + playlist.PlayNext(pc); +} + +void +playlist::SetRepeat(PlayerControl &pc, bool status) +{ + if (status == queue.repeat) + return; + + queue.repeat = status; + + pc.SetBorderPause(queue.single && !queue.repeat); + + /* if the last song is currently being played, the "next song" + might change when repeat mode is toggled */ + UpdateQueuedSong(pc, GetQueuedSong()); + + idle_add(IDLE_OPTIONS); +} + +static void +playlist_order(playlist &playlist) +{ + if (playlist.current >= 0) + /* update playlist.current, order==position now */ + playlist.current = playlist.queue.OrderToPosition(playlist.current); + + playlist.queue.RestoreOrder(); +} + +void +playlist::SetSingle(PlayerControl &pc, bool status) +{ + if (status == queue.single) + return; + + queue.single = status; + + pc.SetBorderPause(queue.single && !queue.repeat); + + /* if the last song is currently being played, the "next song" + might change when single mode is toggled */ + UpdateQueuedSong(pc, GetQueuedSong()); + + idle_add(IDLE_OPTIONS); +} + +void +playlist::SetConsume(bool status) +{ + if (status == queue.consume) + return; + + queue.consume = status; + idle_add(IDLE_OPTIONS); +} + +void +playlist::SetRandom(PlayerControl &pc, bool status) +{ + if (status == queue.random) + return; + + const DetachedSong *const queued_song = GetQueuedSong(); + + queue.random = status; + + if (queue.random) { + /* shuffle the queue order, but preserve current */ + + const int current_position = playing + ? GetCurrentPosition() + : -1; + + queue.ShuffleOrder(); + + if (current_position >= 0) { + /* make sure the current song is the first in + the order list, so the whole rest of the + playlist is played after that */ + unsigned current_order = + queue.PositionToOrder(current_position); + queue.SwapOrders(0, current_order); + current = 0; + } else + current = -1; + } else + playlist_order(*this); + + UpdateQueuedSong(pc, queued_song); + + idle_add(IDLE_OPTIONS); +} + +int +playlist::GetCurrentPosition() const +{ + return current >= 0 + ? queue.OrderToPosition(current) + : -1; +} + +int +playlist::GetNextPosition() const +{ + if (current < 0) + return -1; + + if (queue.single && queue.repeat) + return queue.OrderToPosition(current); + else if (queue.IsValidOrder(current + 1)) + return queue.OrderToPosition(current + 1); + else if (queue.repeat) + return queue.OrderToPosition(0); + + return -1; +} diff --git a/src/queue/Playlist.hxx b/src/queue/Playlist.hxx new file mode 100644 index 000000000..ea19d9bba --- /dev/null +++ b/src/queue/Playlist.hxx @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * 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. + * + * 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. + */ + +#ifndef MPD_PLAYLIST_HXX +#define MPD_PLAYLIST_HXX + +#include "queue/Queue.hxx" +#include "PlaylistError.hxx" + +enum TagType : uint8_t; +struct PlayerControl; +class DetachedSong; +class Database; +class Error; +class SongLoader; +class SongTime; +class SignedSongTime; + +struct playlist { + /** + * The song queue - it contains the "real" playlist. + */ + struct Queue queue; + + /** + * This value is true if the player is currently playing (or + * should be playing). + */ + bool playing; + + /** + * If true, then any error is fatal; if false, MPD will + * attempt to play the next song on non-fatal errors. During + * seeking, this flag is set. + */ + bool stop_on_error; + + /** + * If true, then a bulk edit has been initiated by + * BeginBulk(), and UpdateQueuedSong() and OnModified() will + * be postponed until CommitBulk() + */ + bool bulk_edit; + + /** + * Has the queue been modified during bulk edit mode? + */ + bool bulk_modified; + + /** + * Number of errors since playback was started. If this + * number exceeds the length of the playlist, MPD gives up, + * because all songs have been tried. + */ + unsigned error_count; + + /** + * The "current song pointer". This is the song which is + * played when we get the "play" command. It is also the song + * which is currently being played. + */ + int current; + + /** + * The "next" song to be played, when the current one + * finishes. The decoder thread may start decoding and + * buffering it, while the "current" song is still playing. + * + * This variable is only valid if #playing is true. + */ + int queued; + + playlist(unsigned max_length) + :queue(max_length), playing(false), + bulk_edit(false), + current(-1), queued(-1) { + } + + ~playlist() { + } + + uint32_t GetVersion() const { + return queue.version; + } + + unsigned GetLength() const { + return queue.GetLength(); + } + + unsigned PositionToId(unsigned position) const { + return queue.PositionToId(position); + } + + gcc_pure + int GetCurrentPosition() const; + + gcc_pure + int GetNextPosition() const; + + /** + * Returns the song object which is currently queued. Returns + * none if there is none (yet?) or if MPD isn't playing. + */ + gcc_pure + const DetachedSong *GetQueuedSong() const; + + /** + * This is the "PLAYLIST" event handler. It is invoked by the + * player thread whenever it requests a new queued song, or + * when it exits. + */ + void SyncWithPlayer(PlayerControl &pc); + +protected: + /** + * Called by all editing methods after a modification. + * Updates the queue version and emits #IDLE_PLAYLIST. + */ + void OnModified(); + + /** + * Updates the "queued song". Calculates the next song + * according to the current one (if MPD isn't playing, it + * takes the first song), and queues this song. Clears the + * old queued song if there was one. + * + * @param prev the song which was previously queued, as + * determined by playlist_get_queued_song() + */ + void UpdateQueuedSong(PlayerControl &pc, const DetachedSong *prev); + +public: + void BeginBulk(); + void CommitBulk(PlayerControl &pc); + + void Clear(PlayerControl &pc); + + /** + * A tag in the play queue has been modified by the player + * thread. Apply the given song's tag to the current song if + * the song matches. + */ + void TagModified(DetachedSong &&song); + +#ifdef ENABLE_DATABASE + /** + * The database has been modified. Pull all updates. + */ + void DatabaseModified(const Database &db); +#endif + + /** + * @return the new song id or 0 on error + */ + unsigned AppendSong(PlayerControl &pc, + DetachedSong &&song, + Error &error); + + /** + * @return the new song id or 0 on error + */ + unsigned AppendURI(PlayerControl &pc, + const SongLoader &loader, + const char *uri_utf8, + Error &error); + +protected: + void DeleteInternal(PlayerControl &pc, + unsigned song, const DetachedSong **queued_p); + +public: + PlaylistResult DeletePosition(PlayerControl &pc, + unsigned position); + + PlaylistResult DeleteOrder(PlayerControl &pc, + unsigned order) { + return DeletePosition(pc, queue.OrderToPosition(order)); + } + + PlaylistResult DeleteId(PlayerControl &pc, unsigned id); + + /** + * Deletes a range of songs from the playlist. + * + * @param start the position of the first song to delete + * @param end the position after the last song to delete + */ + PlaylistResult DeleteRange(PlayerControl &pc, + unsigned start, unsigned end); + + void DeleteSong(PlayerControl &pc, const char *uri); + + void Shuffle(PlayerControl &pc, unsigned start, unsigned end); + + PlaylistResult MoveRange(PlayerControl &pc, + unsigned start, unsigned end, int to); + + PlaylistResult MoveId(PlayerControl &pc, unsigned id, int to); + + PlaylistResult SwapPositions(PlayerControl &pc, + unsigned song1, unsigned song2); + + PlaylistResult SwapIds(PlayerControl &pc, + unsigned id1, unsigned id2); + + PlaylistResult SetPriorityRange(PlayerControl &pc, + unsigned start_position, + unsigned end_position, + uint8_t priority); + + PlaylistResult SetPriorityId(PlayerControl &pc, + unsigned song_id, uint8_t priority); + + /** + * Sets the start_time and end_time attributes on the song + * with the specified id. + */ + bool SetSongIdRange(PlayerControl &pc, unsigned id, + SongTime start, SongTime end, + Error &error); + + bool AddSongIdTag(unsigned id, TagType tag_type, const char *value, + Error &error); + bool ClearSongIdTag(unsigned id, TagType tag_type, Error &error); + + void Stop(PlayerControl &pc); + + PlaylistResult PlayPosition(PlayerControl &pc, int position); + + void PlayOrder(PlayerControl &pc, int order); + + PlaylistResult PlayId(PlayerControl &pc, int id); + + void PlayNext(PlayerControl &pc); + + void PlayPrevious(PlayerControl &pc); + + PlaylistResult SeekSongOrder(PlayerControl &pc, + unsigned song_order, + SongTime seek_time); + + PlaylistResult SeekSongPosition(PlayerControl &pc, + unsigned song_position, + SongTime seek_time); + + PlaylistResult SeekSongId(PlayerControl &pc, + unsigned song_id, SongTime seek_time); + + /** + * Seek within the current song. Fails if MPD is not currently + * playing. + * + * @param time the time in seconds + * @param relative if true, then the specified time is relative to the + * current position + */ + PlaylistResult SeekCurrent(PlayerControl &pc, + SignedSongTime seek_time, bool relative); + + bool GetRepeat() const { + return queue.repeat; + } + + void SetRepeat(PlayerControl &pc, bool new_value); + + bool GetRandom() const { + return queue.random; + } + + void SetRandom(PlayerControl &pc, bool new_value); + + bool GetSingle() const { + return queue.single; + } + + void SetSingle(PlayerControl &pc, bool new_value); + + bool GetConsume() const { + return queue.consume; + } + + void SetConsume(bool new_value); +}; + +#endif diff --git a/src/queue/PlaylistControl.cxx b/src/queue/PlaylistControl.cxx new file mode 100644 index 000000000..f7e80dc46 --- /dev/null +++ b/src/queue/PlaylistControl.cxx @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * 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. + * + * 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. + */ + +/* + * Functions for controlling playback on the playlist level. + * + */ + +#include "config.h" +#include "Playlist.hxx" +#include "PlaylistError.hxx" +#include "PlayerControl.hxx" +#include "DetachedSong.hxx" +#include "Log.hxx" + +void +playlist::Stop(PlayerControl &pc) +{ + if (!playing) + return; + + assert(current >= 0); + + FormatDebug(playlist_domain, "stop"); + pc.Stop(); + queued = -1; + playing = false; + + if (queue.random) { + /* shuffle the playlist, so the next playback will + result in a new random order */ + + unsigned current_position = queue.OrderToPosition(current); + + queue.ShuffleOrder(); + + /* make sure that "current" stays valid, and the next + "play" command plays the same song again */ + current = queue.PositionToOrder(current_position); + } +} + +PlaylistResult +playlist::PlayPosition(PlayerControl &pc, int song) +{ + pc.ClearError(); + + unsigned i = song; + if (song == -1) { + /* play any song ("current" song, or the first song */ + + if (queue.IsEmpty()) + return PlaylistResult::SUCCESS; + + if (playing) { + /* already playing: unpause playback, just in + case it was paused, and return */ + pc.SetPause(false); + return PlaylistResult::SUCCESS; + } + + /* select a song: "current" song, or the first one */ + i = current >= 0 + ? current + : 0; + } else if (!queue.IsValidPosition(song)) + return PlaylistResult::BAD_RANGE; + + if (queue.random) { + if (song >= 0) + /* "i" is currently the song position (which + would be equal to the order number in + no-random mode); convert it to a order + number, because random mode is enabled */ + i = queue.PositionToOrder(song); + + if (!playing) + current = 0; + + /* swap the new song with the previous "current" one, + so playback continues as planned */ + queue.SwapOrders(i, current); + i = current; + } + + stop_on_error = false; + error_count = 0; + + PlayOrder(pc, i); + return PlaylistResult::SUCCESS; +} + +PlaylistResult +playlist::PlayId(PlayerControl &pc, int id) +{ + if (id == -1) + return PlayPosition(pc, id); + + int song = queue.IdToPosition(id); + if (song < 0) + return PlaylistResult::NO_SUCH_SONG; + + return PlayPosition(pc, song); +} + +void +playlist::PlayNext(PlayerControl &pc) +{ + if (!playing) + return; + + assert(!queue.IsEmpty()); + assert(queue.IsValidOrder(current)); + + const int old_current = current; + stop_on_error = false; + + /* determine the next song from the queue's order list */ + + const int next_order = queue.GetNextOrder(current); + if (next_order < 0) { + /* no song after this one: stop playback */ + Stop(pc); + + /* reset "current song" */ + current = -1; + } + else + { + if (next_order == 0 && queue.random) { + /* The queue told us that the next song is the first + song. This means we are in repeat mode. Shuffle + the queue order, so this time, the user hears the + songs in a different than before */ + assert(queue.repeat); + + queue.ShuffleOrder(); + + /* note that current and queued are + now invalid, but PlayOrder() will + discard them anyway */ + } + + PlayOrder(pc, next_order); + } + + /* Consume mode removes each played songs. */ + if (queue.consume) + DeleteOrder(pc, old_current); +} + +void +playlist::PlayPrevious(PlayerControl &pc) +{ + if (!playing) + return; + + assert(!queue.IsEmpty()); + + int order; + if (current > 0) { + /* play the preceding song */ + order = current - 1; + } else if (queue.repeat) { + /* play the last song in "repeat" mode */ + order = queue.GetLength() - 1; + } else { + /* re-start playing the current song if it's + the first one */ + order = current; + } + + PlayOrder(pc, order); +} + +PlaylistResult +playlist::SeekSongOrder(PlayerControl &pc, unsigned i, SongTime seek_time) +{ + assert(queue.IsValidOrder(i)); + + const DetachedSong *queued_song = GetQueuedSong(); + + pc.ClearError(); + stop_on_error = true; + error_count = 0; + + if (!playing || (unsigned)current != i) { + /* seeking is not within the current song - prepare + song change */ + + playing = true; + current = i; + + queued_song = nullptr; + } + + if (!pc.Seek(new DetachedSong(queue.GetOrder(i)), seek_time)) { + UpdateQueuedSong(pc, queued_song); + + return PlaylistResult::NOT_PLAYING; + } + + queued = -1; + UpdateQueuedSong(pc, nullptr); + + return PlaylistResult::SUCCESS; +} + +PlaylistResult +playlist::SeekSongPosition(PlayerControl &pc, unsigned song, + SongTime seek_time) +{ + if (!queue.IsValidPosition(song)) + return PlaylistResult::BAD_RANGE; + + unsigned i = queue.random + ? queue.PositionToOrder(song) + : song; + + return SeekSongOrder(pc, i, seek_time); +} + +PlaylistResult +playlist::SeekSongId(PlayerControl &pc, unsigned id, SongTime seek_time) +{ + int song = queue.IdToPosition(id); + if (song < 0) + return PlaylistResult::NO_SUCH_SONG; + + return SeekSongPosition(pc, song, seek_time); +} + +PlaylistResult +playlist::SeekCurrent(PlayerControl &pc, + SignedSongTime seek_time, bool relative) +{ + if (!playing) + return PlaylistResult::NOT_PLAYING; + + if (relative) { + const auto status = pc.GetStatus(); + + if (status.state != PlayerState::PLAY && + status.state != PlayerState::PAUSE) + return PlaylistResult::NOT_PLAYING; + + seek_time += status.elapsed_time; + if (seek_time.IsNegative()) + seek_time = SignedSongTime::zero(); + } + + if (seek_time.IsNegative()) + seek_time = SignedSongTime::zero(); + + return SeekSongOrder(pc, current, SongTime(seek_time)); +} diff --git a/src/queue/PlaylistEdit.cxx b/src/queue/PlaylistEdit.cxx new file mode 100644 index 000000000..22a88dc46 --- /dev/null +++ b/src/queue/PlaylistEdit.cxx @@ -0,0 +1,491 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * 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. + * + * 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. + */ + +/* + * Functions for editing the playlist (adding, removing, reordering + * songs in the queue). + * + */ + +#include "config.h" +#include "Playlist.hxx" +#include "PlaylistError.hxx" +#include "PlayerControl.hxx" +#include "util/UriUtil.hxx" +#include "util/Error.hxx" +#include "DetachedSong.hxx" +#include "SongLoader.hxx" +#include "Idle.hxx" + +#include <stdlib.h> + +void +playlist::OnModified() +{ + if (bulk_edit) { + /* postponed to CommitBulk() */ + bulk_modified = true; + return; + } + + queue.IncrementVersion(); + + idle_add(IDLE_PLAYLIST); +} + +void +playlist::Clear(PlayerControl &pc) +{ + Stop(pc); + + queue.Clear(); + current = -1; + + OnModified(); +} + +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(); +} + +unsigned +playlist::AppendSong(PlayerControl &pc, DetachedSong &&song, Error &error) +{ + unsigned id; + + if (queue.IsFull()) { + error.Set(playlist_domain, int(PlaylistResult::TOO_LARGE), + "Playlist is too large"); + return 0; + } + + const DetachedSong *const queued_song = GetQueuedSong(); + + id = queue.Append(std::move(song), 0); + + if (queue.random) { + /* shuffle the new song into the list of remaining + songs to play */ + + unsigned start; + if (queued >= 0) + start = queued + 1; + else + start = current + 1; + if (start < queue.GetLength()) + queue.ShuffleOrderLast(start, queue.GetLength()); + } + + UpdateQueuedSong(pc, queued_song); + OnModified(); + + return id; +} + +unsigned +playlist::AppendURI(PlayerControl &pc, const SongLoader &loader, + const char *uri, + Error &error) +{ + DetachedSong *song = loader.LoadSong(uri, error); + if (song == nullptr) + return 0; + + unsigned result = AppendSong(pc, std::move(*song), error); + delete song; + + return result; +} + +PlaylistResult +playlist::SwapPositions(PlayerControl &pc, unsigned song1, unsigned song2) +{ + if (!queue.IsValidPosition(song1) || !queue.IsValidPosition(song2)) + return PlaylistResult::BAD_RANGE; + + const DetachedSong *const queued_song = GetQueuedSong(); + + queue.SwapPositions(song1, song2); + + if (queue.random) { + /* update the queue order, so that current + still points to the current song order */ + + queue.SwapOrders(queue.PositionToOrder(song1), + queue.PositionToOrder(song2)); + } else { + /* correct the "current" song order */ + + if (current == (int)song1) + current = song2; + else if (current == (int)song2) + current = song1; + } + + UpdateQueuedSong(pc, queued_song); + OnModified(); + + return PlaylistResult::SUCCESS; +} + +PlaylistResult +playlist::SwapIds(PlayerControl &pc, unsigned id1, unsigned id2) +{ + int song1 = queue.IdToPosition(id1); + int song2 = queue.IdToPosition(id2); + + if (song1 < 0 || song2 < 0) + return PlaylistResult::NO_SUCH_SONG; + + return SwapPositions(pc, song1, song2); +} + +PlaylistResult +playlist::SetPriorityRange(PlayerControl &pc, + unsigned start, unsigned end, + uint8_t priority) +{ + if (start >= GetLength()) + return PlaylistResult::BAD_RANGE; + + if (end > GetLength()) + end = GetLength(); + + if (start >= end) + return PlaylistResult::SUCCESS; + + /* remember "current" and "queued" */ + + const int current_position = GetCurrentPosition(); + const DetachedSong *const queued_song = GetQueuedSong(); + + /* apply the priority changes */ + + queue.SetPriorityRange(start, end, priority, current); + + /* restore "current" and choose a new "queued" */ + + if (current_position >= 0) + current = queue.PositionToOrder(current_position); + + UpdateQueuedSong(pc, queued_song); + OnModified(); + + return PlaylistResult::SUCCESS; +} + +PlaylistResult +playlist::SetPriorityId(PlayerControl &pc, + unsigned song_id, uint8_t priority) +{ + int song_position = queue.IdToPosition(song_id); + if (song_position < 0) + return PlaylistResult::NO_SUCH_SONG; + + return SetPriorityRange(pc, song_position, song_position + 1, + priority); + +} + +void +playlist::DeleteInternal(PlayerControl &pc, + unsigned song, const DetachedSong **queued_p) +{ + assert(song < GetLength()); + + unsigned songOrder = queue.PositionToOrder(song); + + if (playing && current == (int)songOrder) { + const bool paused = pc.GetState() == PlayerState::PAUSE; + + /* the current song is going to be deleted: see which + song is going to be played instead */ + + current = queue.GetNextOrder(current); + if (current == (int)songOrder) + current = -1; + + if (current >= 0 && !paused) + /* play the song after the deleted one */ + PlayOrder(pc, current); + else { + /* stop the player */ + + pc.Stop(); + playing = false; + } + + *queued_p = nullptr; + } else if (current == (int)songOrder) + /* there's a "current song" but we're not playing + currently - clear "current" */ + current = -1; + + /* now do it: remove the song */ + + queue.DeletePosition(song); + + /* update the "current" and "queued" variables */ + + if (current > (int)songOrder) + current--; +} + +PlaylistResult +playlist::DeletePosition(PlayerControl &pc, unsigned song) +{ + if (song >= queue.GetLength()) + return PlaylistResult::BAD_RANGE; + + const DetachedSong *queued_song = GetQueuedSong(); + + DeleteInternal(pc, song, &queued_song); + + UpdateQueuedSong(pc, queued_song); + OnModified(); + + return PlaylistResult::SUCCESS; +} + +PlaylistResult +playlist::DeleteRange(PlayerControl &pc, unsigned start, unsigned end) +{ + if (start >= queue.GetLength()) + return PlaylistResult::BAD_RANGE; + + if (end > queue.GetLength()) + end = queue.GetLength(); + + if (start >= end) + return PlaylistResult::SUCCESS; + + const DetachedSong *queued_song = GetQueuedSong(); + + do { + DeleteInternal(pc, --end, &queued_song); + } while (end != start); + + UpdateQueuedSong(pc, queued_song); + OnModified(); + + return PlaylistResult::SUCCESS; +} + +PlaylistResult +playlist::DeleteId(PlayerControl &pc, unsigned id) +{ + int song = queue.IdToPosition(id); + if (song < 0) + return PlaylistResult::NO_SUCH_SONG; + + return DeletePosition(pc, song); +} + +void +playlist::DeleteSong(PlayerControl &pc, const char *uri) +{ + for (int i = queue.GetLength() - 1; i >= 0; --i) + if (queue.Get(i).IsURI(uri)) + DeletePosition(pc, i); +} + +PlaylistResult +playlist::MoveRange(PlayerControl &pc, unsigned start, unsigned end, int to) +{ + if (!queue.IsValidPosition(start) || !queue.IsValidPosition(end - 1)) + return PlaylistResult::BAD_RANGE; + + if ((to >= 0 && to + end - start - 1 >= GetLength()) || + (to < 0 && unsigned(abs(to)) > GetLength())) + return PlaylistResult::BAD_RANGE; + + if ((int)start == to) + /* nothing happens */ + return PlaylistResult::SUCCESS; + + const DetachedSong *const queued_song = GetQueuedSong(); + + /* + * (to < 0) => move to offset from current song + * (-playlist.length == to) => move to position BEFORE current song + */ + const int currentSong = GetCurrentPosition(); + if (to < 0) { + if (currentSong < 0) + /* can't move relative to current song, + because there is no current song */ + return PlaylistResult::BAD_RANGE; + + if (start <= (unsigned)currentSong && (unsigned)currentSong < end) + /* no-op, can't be moved to offset of itself */ + return PlaylistResult::SUCCESS; + to = (currentSong + abs(to)) % GetLength(); + if (start < (unsigned)to) + to--; + } + + queue.MoveRange(start, end, to); + + if (!queue.random) { + /* update current/queued */ + 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; + } + + UpdateQueuedSong(pc, queued_song); + OnModified(); + + return PlaylistResult::SUCCESS; +} + +PlaylistResult +playlist::MoveId(PlayerControl &pc, unsigned id1, int to) +{ + int song = queue.IdToPosition(id1); + if (song < 0) + return PlaylistResult::NO_SUCH_SONG; + + return MoveRange(pc, song, song + 1, to); +} + +void +playlist::Shuffle(PlayerControl &pc, unsigned start, unsigned end) +{ + if (end > GetLength()) + /* correct the "end" offset */ + end = GetLength(); + + if (start + 1 >= end) + /* needs at least two entries. */ + return; + + const DetachedSong *const queued_song = GetQueuedSong(); + if (playing && current >= 0) { + unsigned current_position = queue.OrderToPosition(current); + + if (current_position >= start && current_position < end) { + /* put current playing song first */ + queue.SwapPositions(start, current_position); + + if (queue.random) { + current = queue.PositionToOrder(start); + } else + current = start; + + /* start shuffle after the current song */ + start++; + } + } else { + /* no playback currently: reset current */ + + current = -1; + } + + queue.ShuffleRange(start, end); + + UpdateQueuedSong(pc, queued_song); + OnModified(); +} + +bool +playlist::SetSongIdRange(PlayerControl &pc, unsigned id, + SongTime start, SongTime end, + Error &error) +{ + assert(end.IsZero() || start < end); + + 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); + + const auto duration = song.GetTag().duration; + if (!duration.IsNegative()) { + /* validate the offsets */ + + if (start > duration) { + error.Set(playlist_domain, + int(PlaylistResult::BAD_RANGE), + "Invalid start offset"); + return false; + } + + if (end >= duration) + end = SongTime::zero(); + } + + /* edit it */ + song.SetStartTime(start); + song.SetEndTime(end); + + /* announce the change to all interested subsystems */ + UpdateQueuedSong(pc, nullptr); + queue.ModifyAtPosition(position); + OnModified(); + return true; +} diff --git a/src/queue/PlaylistState.cxx b/src/queue/PlaylistState.cxx new file mode 100644 index 000000000..6ea86166e --- /dev/null +++ b/src/queue/PlaylistState.cxx @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * 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. + * + * 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. + */ + +/* + * Saving and loading the playlist to/from the state file. + * + */ + +#include "config.h" +#include "PlaylistState.hxx" +#include "PlaylistError.hxx" +#include "Playlist.hxx" +#include "queue/QueueSave.hxx" +#include "fs/io/TextFile.hxx" +#include "fs/io/BufferedOutputStream.hxx" +#include "PlayerControl.hxx" +#include "config/ConfigGlobal.hxx" +#include "config/ConfigOption.hxx" +#include "fs/Limits.hxx" +#include "util/CharUtil.hxx" +#include "util/StringUtil.hxx" +#include "Log.hxx" + +#include <string.h> +#include <stdlib.h> + +#define PLAYLIST_STATE_FILE_STATE "state: " +#define PLAYLIST_STATE_FILE_RANDOM "random: " +#define PLAYLIST_STATE_FILE_REPEAT "repeat: " +#define PLAYLIST_STATE_FILE_SINGLE "single: " +#define PLAYLIST_STATE_FILE_CONSUME "consume: " +#define PLAYLIST_STATE_FILE_CURRENT "current: " +#define PLAYLIST_STATE_FILE_TIME "time: " +#define PLAYLIST_STATE_FILE_CROSSFADE "crossfade: " +#define PLAYLIST_STATE_FILE_MIXRAMPDB "mixrampdb: " +#define PLAYLIST_STATE_FILE_MIXRAMPDELAY "mixrampdelay: " +#define PLAYLIST_STATE_FILE_PLAYLIST_BEGIN "playlist_begin" +#define PLAYLIST_STATE_FILE_PLAYLIST_END "playlist_end" + +#define PLAYLIST_STATE_FILE_STATE_PLAY "play" +#define PLAYLIST_STATE_FILE_STATE_PAUSE "pause" +#define PLAYLIST_STATE_FILE_STATE_STOP "stop" + +#define PLAYLIST_BUFFER_SIZE 2*MPD_PATH_MAX + +void +playlist_state_save(BufferedOutputStream &os, const struct playlist &playlist, + PlayerControl &pc) +{ + const auto player_status = pc.GetStatus(); + + os.Write(PLAYLIST_STATE_FILE_STATE); + + if (playlist.playing) { + switch (player_status.state) { + case PlayerState::PAUSE: + os.Write(PLAYLIST_STATE_FILE_STATE_PAUSE "\n"); + break; + default: + os.Write(PLAYLIST_STATE_FILE_STATE_PLAY "\n"); + } + os.Format(PLAYLIST_STATE_FILE_CURRENT "%i\n", + playlist.queue.OrderToPosition(playlist.current)); + os.Format(PLAYLIST_STATE_FILE_TIME "%f\n", + player_status.elapsed_time.ToDoubleS()); + } else { + os.Write(PLAYLIST_STATE_FILE_STATE_STOP "\n"); + + if (playlist.current >= 0) + os.Format(PLAYLIST_STATE_FILE_CURRENT "%i\n", + playlist.queue.OrderToPosition(playlist.current)); + } + + os.Format(PLAYLIST_STATE_FILE_RANDOM "%i\n", playlist.queue.random); + os.Format(PLAYLIST_STATE_FILE_REPEAT "%i\n", playlist.queue.repeat); + os.Format(PLAYLIST_STATE_FILE_SINGLE "%i\n", playlist.queue.single); + os.Format(PLAYLIST_STATE_FILE_CONSUME "%i\n", playlist.queue.consume); + os.Format(PLAYLIST_STATE_FILE_CROSSFADE "%i\n", + (int)pc.GetCrossFade()); + os.Format(PLAYLIST_STATE_FILE_MIXRAMPDB "%f\n", pc.GetMixRampDb()); + os.Format(PLAYLIST_STATE_FILE_MIXRAMPDELAY "%f\n", + pc.GetMixRampDelay()); + os.Write(PLAYLIST_STATE_FILE_PLAYLIST_BEGIN "\n"); + queue_save(os, playlist.queue); + os.Write(PLAYLIST_STATE_FILE_PLAYLIST_END "\n"); +} + +static void +playlist_state_load(TextFile &file, const SongLoader &song_loader, + struct playlist &playlist) +{ + const char *line = file.ReadLine(); + if (line == nullptr) { + LogWarning(playlist_domain, "No playlist in state file"); + return; + } + + while (!StringStartsWith(line, PLAYLIST_STATE_FILE_PLAYLIST_END)) { + queue_load_song(file, song_loader, line, playlist.queue); + + line = file.ReadLine(); + if (line == nullptr) { + LogWarning(playlist_domain, + "'" PLAYLIST_STATE_FILE_PLAYLIST_END + "' not found in state file"); + break; + } + } + + playlist.queue.IncrementVersion(); +} + +bool +playlist_state_restore(const char *line, TextFile &file, + const SongLoader &song_loader, + struct playlist &playlist, PlayerControl &pc) +{ + int current = -1; + SongTime seek_time = SongTime::zero(); + bool random_mode = false; + + if (!StringStartsWith(line, PLAYLIST_STATE_FILE_STATE)) + return false; + + line += sizeof(PLAYLIST_STATE_FILE_STATE) - 1; + + PlayerState state; + if (strcmp(line, PLAYLIST_STATE_FILE_STATE_PLAY) == 0) + state = PlayerState::PLAY; + else if (strcmp(line, PLAYLIST_STATE_FILE_STATE_PAUSE) == 0) + state = PlayerState::PAUSE; + else + state = PlayerState::STOP; + + while ((line = file.ReadLine()) != nullptr) { + if (StringStartsWith(line, PLAYLIST_STATE_FILE_TIME)) { + double seconds = atof(line + strlen(PLAYLIST_STATE_FILE_TIME)); + seek_time = SongTime::FromS(seconds); + } else if (StringStartsWith(line, PLAYLIST_STATE_FILE_REPEAT)) { + playlist.SetRepeat(pc, + strcmp(&(line[strlen(PLAYLIST_STATE_FILE_REPEAT)]), + "1") == 0); + } else if (StringStartsWith(line, PLAYLIST_STATE_FILE_SINGLE)) { + playlist.SetSingle(pc, + strcmp(&(line[strlen(PLAYLIST_STATE_FILE_SINGLE)]), + "1") == 0); + } else if (StringStartsWith(line, PLAYLIST_STATE_FILE_CONSUME)) { + playlist.SetConsume(strcmp(&(line[strlen(PLAYLIST_STATE_FILE_CONSUME)]), + "1") == 0); + } else if (StringStartsWith(line, PLAYLIST_STATE_FILE_CROSSFADE)) { + pc.SetCrossFade(atoi(line + strlen(PLAYLIST_STATE_FILE_CROSSFADE))); + } else if (StringStartsWith(line, PLAYLIST_STATE_FILE_MIXRAMPDB)) { + pc.SetMixRampDb(atof(line + strlen(PLAYLIST_STATE_FILE_MIXRAMPDB))); + } else if (StringStartsWith(line, PLAYLIST_STATE_FILE_MIXRAMPDELAY)) { + const char *p = line + strlen(PLAYLIST_STATE_FILE_MIXRAMPDELAY); + + /* this check discards "nan" which was used + prior to MPD 0.18 */ + if (IsDigitASCII(*p)) + pc.SetMixRampDelay(atof(p)); + } else if (StringStartsWith(line, PLAYLIST_STATE_FILE_RANDOM)) { + random_mode = + strcmp(line + strlen(PLAYLIST_STATE_FILE_RANDOM), + "1") == 0; + } else if (StringStartsWith(line, PLAYLIST_STATE_FILE_CURRENT)) { + current = atoi(&(line + [strlen + (PLAYLIST_STATE_FILE_CURRENT)])); + } else if (StringStartsWith(line, + PLAYLIST_STATE_FILE_PLAYLIST_BEGIN)) { + playlist_state_load(file, song_loader, playlist); + } + } + + playlist.SetRandom(pc, random_mode); + + if (!playlist.queue.IsEmpty()) { + if (!playlist.queue.IsValidPosition(current)) + current = 0; + + if (state == PlayerState::PLAY && + config_get_bool(CONF_RESTORE_PAUSED, false)) + /* the user doesn't want MPD to auto-start + playback after startup; fall back to + "pause" */ + state = PlayerState::PAUSE; + + /* enable all devices for the first time; this must be + called here, after the audio output states were + restored, before playback begins */ + if (state != PlayerState::STOP) + pc.UpdateAudio(); + + if (state == PlayerState::STOP /* && config_option */) + playlist.current = current; + else if (seek_time.count() == 0) + playlist.PlayPosition(pc, current); + else + playlist.SeekSongPosition(pc, current, seek_time); + + if (state == PlayerState::PAUSE) + pc.Pause(); + } + + return true; +} + +unsigned +playlist_state_get_hash(const playlist &playlist, + PlayerControl &pc) +{ + const auto player_status = pc.GetStatus(); + + return playlist.queue.version ^ + (player_status.state != PlayerState::STOP + ? (player_status.elapsed_time.ToS() << 8) + : 0) ^ + (playlist.current >= 0 + ? (playlist.queue.OrderToPosition(playlist.current) << 16) + : 0) ^ + ((int)pc.GetCrossFade() << 20) ^ + (unsigned(player_status.state) << 24) ^ + (playlist.queue.random << 27) ^ + (playlist.queue.repeat << 28) ^ + (playlist.queue.single << 29) ^ + (playlist.queue.consume << 30) ^ + (playlist.queue.random << 31); +} diff --git a/src/queue/PlaylistState.hxx b/src/queue/PlaylistState.hxx new file mode 100644 index 000000000..3211b1178 --- /dev/null +++ b/src/queue/PlaylistState.hxx @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * 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. + * + * 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. + */ + +/* + * Saving and loading the playlist to/from the state file. + * + */ + +#ifndef MPD_PLAYLIST_STATE_HXX +#define MPD_PLAYLIST_STATE_HXX + +struct playlist; +struct PlayerControl; +class TextFile; +class BufferedOutputStream; +class SongLoader; + +void +playlist_state_save(BufferedOutputStream &os, const playlist &playlist, + PlayerControl &pc); + +bool +playlist_state_restore(const char *line, TextFile &file, + const SongLoader &song_loader, + playlist &playlist, PlayerControl &pc); + +/** + * Generates a hash number for the current state of the playlist and + * the playback options. This is used by timer_save_state_file() to + * determine whether the state has changed and the state file should + * be saved. + */ +unsigned +playlist_state_get_hash(const playlist &playlist, + PlayerControl &c); + +#endif diff --git a/src/queue/PlaylistTag.cxx b/src/queue/PlaylistTag.cxx new file mode 100644 index 000000000..556e7f4e9 --- /dev/null +++ b/src/queue/PlaylistTag.cxx @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * 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. + * + * 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. + */ + +/* + * Functions for editing the playlist (adding, removing, reordering + * songs in the queue). + * + */ + +#include "config.h" +#include "Playlist.hxx" +#include "PlaylistError.hxx" +#include "DetachedSong.hxx" +#include "tag/Tag.hxx" +#include "tag/TagBuilder.hxx" +#include "util/Error.hxx" + +bool +playlist::AddSongIdTag(unsigned id, TagType tag_type, const char *value, + Error &error) +{ + const int position = queue.IdToPosition(id); + if (position < 0) { + error.Set(playlist_domain, int(PlaylistResult::NO_SUCH_SONG), + "No such song"); + return false; + } + + DetachedSong &song = queue.Get(position); + if (song.IsFile()) { + error.Set(playlist_domain, int(PlaylistResult::DENIED), + "Cannot edit tags of local file"); + return false; + } + + { + TagBuilder tag(std::move(song.WritableTag())); + tag.AddItem(tag_type, value); + song.SetTag(tag.Commit()); + } + + queue.ModifyAtPosition(position); + OnModified(); + return true; +} + +bool +playlist::ClearSongIdTag(unsigned id, TagType tag_type, + Error &error) +{ + const int position = queue.IdToPosition(id); + if (position < 0) { + error.Set(playlist_domain, int(PlaylistResult::NO_SUCH_SONG), + "No such song"); + return false; + } + + DetachedSong &song = queue.Get(position); + if (song.IsFile()) { + error.Set(playlist_domain, int(PlaylistResult::DENIED), + "Cannot edit tags of local file"); + return false; + } + + { + TagBuilder tag(std::move(song.WritableTag())); + if (tag_type == TAG_NUM_OF_ITEM_TYPES) + tag.RemoveAll(); + else + tag.RemoveType(tag_type); + song.SetTag(tag.Commit()); + } + + queue.ModifyAtPosition(position); + OnModified(); + return true; +} diff --git a/src/queue/PlaylistUpdate.cxx b/src/queue/PlaylistUpdate.cxx new file mode 100644 index 000000000..8876711ef --- /dev/null +++ b/src/queue/PlaylistUpdate.cxx @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * 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. + * + * 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. + */ + +#include "config.h" +#include "Playlist.hxx" +#include "db/Interface.hxx" +#include "db/LightSong.hxx" +#include "DetachedSong.hxx" +#include "tag/Tag.hxx" +#include "Idle.hxx" +#include "util/Error.hxx" + +static bool +UpdatePlaylistSong(const Database &db, DetachedSong &song) +{ + if (!song.IsInDatabase() || !song.IsFile()) + /* only update Songs instances that are "detached" + from the Database */ + return false; + + const LightSong *original = db.GetSong(song.GetURI(), IgnoreError()); + if (original == nullptr) + /* not found - shouldn't happen, because the update + thread should ensure that all stale Song instances + have been purged */ + return false; + + if (original->mtime == song.GetLastModified()) { + /* not modified */ + db.ReturnSong(original); + return false; + } + + song.SetLastModified(original->mtime); + song.SetTag(*original->tag); + + db.ReturnSong(original); + return true; +} + +void +playlist::DatabaseModified(const Database &db) +{ + bool modified = false; + + for (unsigned i = 0, n = queue.GetLength(); i != n; ++i) { + if (UpdatePlaylistSong(db, queue.Get(i))) { + queue.ModifyAtPosition(i); + modified = true; + } + } + + if (modified) { + queue.IncrementVersion(); + idle_add(IDLE_PLAYLIST); + } +} diff --git a/src/queue/Queue.cxx b/src/queue/Queue.cxx new file mode 100644 index 000000000..99b545ab1 --- /dev/null +++ b/src/queue/Queue.cxx @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * 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. + * + * 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. + */ + +#include "config.h" +#include "Queue.hxx" +#include "DetachedSong.hxx" + +Queue::Queue(unsigned _max_length) + :max_length(_max_length), length(0), + version(1), + items(new Item[max_length]), + order(new unsigned[max_length]), + id_table(max_length * HASH_MULT), + repeat(false), + single(false), + consume(false), + random(false) +{ +} + +Queue::~Queue() +{ + Clear(); + + delete[] items; + delete[] order; +} + +int +Queue::GetNextOrder(unsigned _order) const +{ + assert(_order < length); + + if (single && repeat && !consume) + return _order; + else if (_order + 1 < length) + return _order + 1; + else if (repeat && (_order > 0 || !consume)) + /* restart at first song */ + return 0; + else + /* end of queue */ + return -1; +} + +void +Queue::IncrementVersion() +{ + static unsigned long max = ((uint32_t) 1 << 31) - 1; + + version++; + + if (version >= max) { + for (unsigned i = 0; i < length; i++) + items[i].version = 0; + + version = 1; + } +} + +void +Queue::ModifyAtOrder(unsigned _order) +{ + assert(_order < length); + + unsigned position = order[_order]; + ModifyAtPosition(position); +} + +unsigned +Queue::Append(DetachedSong &&song, uint8_t priority) +{ + assert(!IsFull()); + + const unsigned position = length++; + const unsigned id = id_table.Insert(position); + + auto &item = items[position]; + item.song = new DetachedSong(std::move(song)); + item.id = id; + item.version = version; + item.priority = priority; + + order[position] = position; + + return id; +} + +void +Queue::SwapPositions(unsigned position1, unsigned position2) +{ + unsigned id1 = items[position1].id; + unsigned id2 = items[position2].id; + + std::swap(items[position1], items[position2]); + + items[position1].version = version; + items[position2].version = version; + + id_table.Move(id1, position2); + id_table.Move(id2, position1); +} + +void +Queue::MovePostion(unsigned from, unsigned to) +{ + const Item tmp = items[from]; + + /* move songs to one less in from->to */ + + for (unsigned i = from; i < to; i++) + MoveItemTo(i + 1, i); + + /* move songs to one more in to->from */ + + for (unsigned i = from; i > to; i--) + MoveItemTo(i - 1, i); + + /* put song at _to_ */ + + id_table.Move(tmp.id, to); + items[to] = tmp; + items[to].version = version; + + /* now deal with order */ + + if (random) { + for (unsigned i = 0; i < length; i++) { + if (order[i] > from && order[i] <= to) + order[i]--; + else if (order[i] < from && + order[i] >= to) + order[i]++; + else if (from == order[i]) + order[i] = to; + } + } +} + +void +Queue::MoveRange(unsigned start, unsigned end, unsigned to) +{ + Item tmp[end - start]; + // Copy the original block [start,end-1] + for (unsigned i = start; i < end; i++) + tmp[i - start] = items[i]; + + // If to > start, we need to move to-start items to start, starting from end + for (unsigned i = end; i < end + to - start; i++) + MoveItemTo(i, start + i - end); + + // If to < start, we need to move start-to items to newend (= end + to - start), starting from to + // This is the same as moving items from start-1 to to (decreasing), with start-1 going to end-1 + // We have to iterate in this order to avoid writing over something we haven't yet moved + for (int i = start - 1; i >= int(to); i--) + MoveItemTo(i, i + end - start); + + // Copy the original block back in, starting at to. + for (unsigned i = start; i< end; i++) + { + id_table.Move(tmp[i - start].id, to + i - start); + items[to + i - start] = tmp[i-start]; + items[to + i - start].version = version; + } + + if (random) { + // Update the positions in the queue. + // Note that the ranges for these cases are the same as the ranges of + // the loops above. + for (unsigned i = 0; i < length; i++) { + if (order[i] >= end && order[i] < to + end - start) + order[i] -= end - start; + else if (order[i] < start && + order[i] >= to) + order[i] += end - start; + else if (start <= order[i] && order[i] < end) + order[i] += to - start; + } + } +} + +void +Queue::MoveOrder(unsigned from_order, unsigned to_order) +{ + assert(from_order < length); + assert(to_order <= length); + + const unsigned from_position = OrderToPosition(from_order); + + if (from_order < to_order) { + for (unsigned i = from_order; i < to_order; ++i) + order[i] = order[i + 1]; + } else { + for (unsigned i = from_order; i > to_order; --i) + order[i] = order[i - 1]; + } + + order[to_order] = from_position; +} + +void +Queue::DeletePosition(unsigned position) +{ + assert(position < length); + + delete items[position].song; + + const unsigned id = PositionToId(position); + const unsigned _order = PositionToOrder(position); + + --length; + + /* release the song id */ + + id_table.Erase(id); + + /* delete song from songs array */ + + for (unsigned i = position; i < length; i++) + MoveItemTo(i + 1, i); + + /* delete the entry from the order array */ + + for (unsigned i = _order; i < length; i++) + order[i] = order[i + 1]; + + /* readjust values in the order array */ + + for (unsigned i = 0; i < length; i++) + if (order[i] > position) + --order[i]; +} + +void +Queue::Clear() +{ + for (unsigned i = 0; i < length; i++) { + Item *item = &items[i]; + + delete item->song; + + id_table.Erase(item->id); + } + + length = 0; +} + +static void +queue_sort_order_by_priority(Queue *queue, unsigned start, unsigned end) +{ + assert(queue != nullptr); + assert(queue->random); + assert(start <= end); + assert(end <= queue->length); + + auto cmp = [queue](unsigned a_pos, unsigned b_pos){ + const Queue::Item &a = queue->items[a_pos]; + const Queue::Item &b = queue->items[b_pos]; + + return a.priority > b.priority; + }; + + std::stable_sort(queue->order + start, queue->order + end, cmp); +} + +void +Queue::ShuffleOrderRange(unsigned start, unsigned end) +{ + assert(random); + assert(start <= end); + assert(end <= length); + + rand.AutoCreate(); + std::shuffle(order + start, order + end, rand); +} + +/** + * Sort the "order" of items by priority, and then shuffle each + * priority group. + */ +void +Queue::ShuffleOrderRangeWithPriority(unsigned start, unsigned end) +{ + assert(random); + assert(start <= end); + assert(end <= length); + + if (start == end) + return; + + /* first group the range by priority */ + queue_sort_order_by_priority(this, start, end); + + /* now shuffle each priority group */ + unsigned group_start = start; + uint8_t group_priority = GetOrderPriority(start); + + for (unsigned i = start + 1; i < end; ++i) { + const uint8_t priority = GetOrderPriority(i); + assert(priority <= group_priority); + + if (priority != group_priority) { + /* start of a new group - shuffle the one that + has just ended */ + ShuffleOrderRange(group_start, i); + group_start = i; + group_priority = priority; + } + } + + /* shuffle the last group */ + ShuffleOrderRange(group_start, end); +} + +void +Queue::ShuffleOrder() +{ + ShuffleOrderRangeWithPriority(0, length); +} + +void +Queue::ShuffleOrderFirst(unsigned start, unsigned end) +{ + rand.AutoCreate(); + + std::uniform_int_distribution<unsigned> distribution(start, end - 1); + SwapOrders(start, distribution(rand)); +} + +void +Queue::ShuffleOrderLast(unsigned start, unsigned end) +{ + rand.AutoCreate(); + + std::uniform_int_distribution<unsigned> distribution(start, end - 1); + SwapOrders(end - 1, distribution(rand)); +} + +void +Queue::ShuffleRange(unsigned start, unsigned end) +{ + assert(start <= end); + assert(end <= length); + + rand.AutoCreate(); + + for (unsigned i = start; i < end; i++) { + std::uniform_int_distribution<unsigned> distribution(start, + end - 1); + unsigned ri = distribution(rand); + SwapPositions(i, ri); + } +} + +unsigned +Queue::FindPriorityOrder(unsigned start_order, uint8_t priority, + unsigned exclude_order) const +{ + assert(random); + assert(start_order <= length); + + for (unsigned i = start_order; i < length; ++i) { + const unsigned position = OrderToPosition(i); + const Item *item = &items[position]; + if (item->priority <= priority && i != exclude_order) + return i; + } + + return length; +} + +unsigned +Queue::CountSamePriority(unsigned start_order, uint8_t priority) const +{ + assert(random); + assert(start_order <= length); + + for (unsigned i = start_order; i < length; ++i) { + const unsigned position = OrderToPosition(i); + const Item *item = &items[position]; + if (item->priority != priority) + return i - start_order; + } + + return length - start_order; +} + +bool +Queue::SetPriority(unsigned position, uint8_t priority, int after_order) +{ + assert(position < length); + + Item *item = &items[position]; + uint8_t old_priority = item->priority; + if (old_priority == priority) + return false; + + item->version = version; + item->priority = priority; + + if (!random) + /* don't reorder if not in random mode */ + return true; + + unsigned _order = PositionToOrder(position); + if (after_order >= 0) { + if (_order == (unsigned)after_order) + /* don't reorder the current song */ + return true; + + if (_order < (unsigned)after_order) { + /* the specified song has been played already + - enqueue it only if its priority has just + become bigger than the current one's */ + + const unsigned after_position = + OrderToPosition(after_order); + const Item *after_item = + &items[after_position]; + if (old_priority > after_item->priority || + priority <= after_item->priority) + /* priority hasn't become bigger */ + return true; + } + } + + /* move the item to the beginning of the priority group (or + create a new priority group) */ + + const unsigned before_order = + FindPriorityOrder(after_order + 1, priority, _order); + const unsigned new_order = before_order > _order + ? before_order - 1 + : before_order; + MoveOrder(_order, new_order); + + /* shuffle the song within that priority group */ + + const unsigned priority_count = CountSamePriority(new_order, priority); + assert(priority_count >= 1); + ShuffleOrderFirst(new_order, new_order + priority_count); + + return true; +} + +bool +Queue::SetPriorityRange(unsigned start_position, unsigned end_position, + uint8_t priority, int after_order) +{ + assert(start_position <= end_position); + assert(end_position <= length); + + bool modified = false; + int after_position = after_order >= 0 + ? (int)OrderToPosition(after_order) + : -1; + for (unsigned i = start_position; i < end_position; ++i) { + after_order = after_position >= 0 + ? (int)PositionToOrder(after_position) + : -1; + + modified |= SetPriority(i, priority, after_order); + } + + return modified; +} diff --git a/src/queue/Queue.hxx b/src/queue/Queue.hxx new file mode 100644 index 000000000..016619e65 --- /dev/null +++ b/src/queue/Queue.hxx @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * 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. + * + * 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. + */ + +#ifndef MPD_QUEUE_HXX +#define MPD_QUEUE_HXX + +#include "Compiler.h" +#include "IdTable.hxx" +#include "util/LazyRandomEngine.hxx" + +#include <algorithm> + +#include <assert.h> +#include <stdint.h> + +class DetachedSong; + +/** + * A queue of songs. This is the backend of the playlist: it contains + * an ordered list of songs. + * + * Songs can be addressed in three possible ways: + * + * - the position in the queue + * - the unique id (which stays the same, regardless of moves) + * - the order number (which only differs from "position" in random mode) + */ +struct Queue { + /** + * reserve max_length * HASH_MULT elements in the id + * number space + */ + static constexpr unsigned HASH_MULT = 4; + + /** + * One element of the queue: basically a song plus some queue specific + * information attached. + */ + struct Item { + DetachedSong *song; + + /** the unique id of this item in the queue */ + unsigned id; + + /** when was this item last changed? */ + uint32_t version; + + /** + * The priority of this item, between 0 and 255. High + * priority value means that this song gets played first in + * "random" mode. + */ + uint8_t priority; + }; + + /** configured maximum length of the queue */ + unsigned max_length; + + /** number of songs in the queue */ + unsigned length; + + /** the current version number */ + uint32_t version; + + /** all songs in "position" order */ + Item *items; + + /** map order numbers to positions */ + unsigned *order; + + /** map song ids to positions */ + IdTable id_table; + + /** repeat playback when the end of the queue has been + reached? */ + bool repeat; + + /** play only current song. */ + bool single; + + /** remove each played files. */ + bool consume; + + /** play back songs in random order? */ + bool random; + + /** random number generator for shuffle and random mode */ + LazyRandomEngine rand; + + explicit Queue(unsigned max_length); + + /** + * Deinitializes a queue object. It does not free the queue + * pointer itself. + */ + ~Queue(); + + Queue(const Queue &) = delete; + Queue &operator=(const Queue &) = delete; + + unsigned GetLength() const { + assert(length <= max_length); + + return length; + } + + /** + * Determine if the queue is empty, i.e. there are no songs. + */ + bool IsEmpty() const { + return length == 0; + } + + /** + * Determine if the maximum number of songs has been reached. + */ + bool IsFull() const { + assert(length <= max_length); + + return length >= max_length; + } + + /** + * Is that a valid position number? + */ + bool IsValidPosition(unsigned position) const { + return position < length; + } + + /** + * Is that a valid order number? + */ + bool IsValidOrder(unsigned _order) const { + return _order < length; + } + + int IdToPosition(unsigned id) const { + return id_table.IdToPosition(id); + } + + int PositionToId(unsigned position) const + { + assert(position < length); + + return items[position].id; + } + + gcc_pure + unsigned OrderToPosition(unsigned _order) const { + assert(_order < length); + + return order[_order]; + } + + gcc_pure + unsigned PositionToOrder(unsigned position) const { + assert(position < length); + + for (unsigned i = 0;; ++i) { + assert(i < length); + + if (order[i] == position) + return i; + } + } + + gcc_pure + uint8_t GetPriorityAtPosition(unsigned position) const { + assert(position < length); + + return items[position].priority; + } + + const Item &GetOrderItem(unsigned i) const { + assert(IsValidOrder(i)); + + return items[OrderToPosition(i)]; + } + + uint8_t GetOrderPriority(unsigned i) const { + return GetOrderItem(i).priority; + } + + /** + * Returns the song at the specified position. + */ + DetachedSong &Get(unsigned position) const { + assert(position < length); + + return *items[position].song; + } + + /** + * Returns the song at the specified order number. + */ + DetachedSong &GetOrder(unsigned _order) const { + return Get(OrderToPosition(_order)); + } + + /** + * Is the song at the specified position newer than the specified + * version? + */ + bool IsNewerAtPosition(unsigned position, uint32_t _version) const { + assert(position < length); + + return _version > version || + items[position].version >= _version || + items[position].version == 0; + } + + /** + * Returns the order number following the specified one. This takes + * end of queue and "repeat" mode into account. + * + * @return the next order number, or -1 to stop playback + */ + gcc_pure + int GetNextOrder(unsigned order) const; + + /** + * Increments the queue's version number. This handles integer + * overflow well. + */ + void IncrementVersion(); + + /** + * Marks the specified song as "modified". Call + * IncrementVersion() after all modifications have been made. + * number. + */ + void ModifyAtPosition(unsigned position) { + assert(position < length); + + items[position].version = version; + } + + /** + * Marks the specified song as "modified". Call + * IncrementVersion() after all modifications have been made. + * number. + */ + void ModifyAtOrder(unsigned order); + + /** + * Appends a song to the queue and returns its position. Prior to + * that, the caller must check if the queue is already full. + * + * If a song is not in the database (determined by + * Song::IsInDatabase()), it is freed when removed from the + * queue. + * + * @param priority the priority of this new queue item + */ + unsigned Append(DetachedSong &&song, uint8_t priority); + + /** + * Swaps two songs, addressed by their position. + */ + void SwapPositions(unsigned position1, unsigned position2); + + /** + * Swaps two songs, addressed by their order number. + */ + void SwapOrders(unsigned order1, unsigned order2) { + std::swap(order[order1], order[order2]); + } + + /** + * Moves a song to a new position. + */ + void MovePostion(unsigned from, unsigned to); + + /** + * Moves a range of songs to a new position. + */ + void MoveRange(unsigned start, unsigned end, unsigned to); + + /** + * Removes a song from the playlist. + */ + void DeletePosition(unsigned position); + + /** + * Removes all songs from the playlist. + */ + void Clear(); + + /** + * Initializes the "order" array, and restores "normal" order. + */ + void RestoreOrder() { + for (unsigned i = 0; i < length; ++i) + order[i] = i; + } + + /** + * Shuffle the order of items in the specified range, ignoring + * their priorities. + */ + void ShuffleOrderRange(unsigned start, unsigned end); + + /** + * Shuffle the order of items in the specified range, taking their + * priorities into account. + */ + void ShuffleOrderRangeWithPriority(unsigned start, unsigned end); + + /** + * Shuffles the virtual order of songs, but does not move them + * physically. This is used in random mode. + */ + void ShuffleOrder(); + + void ShuffleOrderFirst(unsigned start, unsigned end); + + /** + * Shuffles the virtual order of the last song in the specified + * (order) range. This is used in random mode after a song has been + * appended by queue_append(). + */ + void ShuffleOrderLast(unsigned start, unsigned end); + + /** + * Shuffles a (position) range in the queue. The songs are physically + * shuffled, not by using the "order" mapping. + */ + void ShuffleRange(unsigned start, unsigned end); + + bool SetPriority(unsigned position, uint8_t priority, int after_order); + + bool SetPriorityRange(unsigned start_position, unsigned end_position, + uint8_t priority, int after_order); + +private: + /** + * Moves a song to a new position in the "order" list. + */ + void MoveOrder(unsigned from_order, unsigned to_order); + + void MoveItemTo(unsigned from, unsigned to) { + unsigned from_id = items[from].id; + + items[to] = items[from]; + items[to].version = version; + id_table.Move(from_id, to); + } + + /** + * Find the first item that has this specified priority or + * higher. + */ + gcc_pure + unsigned FindPriorityOrder(unsigned start_order, uint8_t priority, + unsigned exclude_order) const; + + gcc_pure + unsigned CountSamePriority(unsigned start_order, + uint8_t priority) const; +}; + +#endif diff --git a/src/queue/QueuePrint.cxx b/src/queue/QueuePrint.cxx new file mode 100644 index 000000000..831ecafb9 --- /dev/null +++ b/src/queue/QueuePrint.cxx @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * 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. + * + * 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. + */ + +#include "config.h" +#include "QueuePrint.hxx" +#include "Queue.hxx" +#include "SongFilter.hxx" +#include "SongPrint.hxx" +#include "client/Client.hxx" + +/** + * Send detailed information about a range of songs in the queue to a + * client. + * + * @param client the client which has requested information + * @param start the index of the first song (including) + * @param end the index of the last song (excluding) + */ +static void +queue_print_song_info(Client &client, const Queue &queue, + unsigned position) +{ + song_print_info(client, queue.Get(position)); + client_printf(client, "Pos: %u\nId: %u\n", + position, queue.PositionToId(position)); + + uint8_t priority = queue.GetPriorityAtPosition(position); + if (priority != 0) + client_printf(client, "Prio: %u\n", priority); +} + +void +queue_print_info(Client &client, const Queue &queue, + unsigned start, unsigned end) +{ + assert(start <= end); + assert(end <= queue.GetLength()); + + for (unsigned i = start; i < end; ++i) + queue_print_song_info(client, queue, i); +} + +void +queue_print_uris(Client &client, const Queue &queue, + unsigned start, unsigned end) +{ + assert(start <= end); + assert(end <= queue.GetLength()); + + for (unsigned i = start; i < end; ++i) { + client_printf(client, "%i:", i); + song_print_uri(client, queue.Get(i)); + } +} + +void +queue_print_changes_info(Client &client, const Queue &queue, + uint32_t version) +{ + for (unsigned i = 0; i < queue.GetLength(); i++) { + if (queue.IsNewerAtPosition(i, version)) + queue_print_song_info(client, queue, i); + } +} + +void +queue_print_changes_position(Client &client, const Queue &queue, + uint32_t version) +{ + for (unsigned i = 0; i < queue.GetLength(); i++) + if (queue.IsNewerAtPosition(i, version)) + client_printf(client, "cpos: %i\nId: %i\n", + i, queue.PositionToId(i)); +} + +void +queue_find(Client &client, const Queue &queue, + const SongFilter &filter) +{ + for (unsigned i = 0; i < queue.GetLength(); i++) { + const DetachedSong &song = queue.Get(i); + + if (filter.Match(song)) + queue_print_song_info(client, queue, i); + } +} diff --git a/src/queue/QueuePrint.hxx b/src/queue/QueuePrint.hxx new file mode 100644 index 000000000..1aa876219 --- /dev/null +++ b/src/queue/QueuePrint.hxx @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * 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. + * + * 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. + */ + +/* + * This library sends information about songs in the queue to the + * client. + */ + +#ifndef MPD_QUEUE_PRINT_HXX +#define MPD_QUEUE_PRINT_HXX + +#include <stdint.h> + +struct Queue; +class SongFilter; +class Client; + +void +queue_print_info(Client &client, const Queue &queue, + unsigned start, unsigned end); + +void +queue_print_uris(Client &client, const Queue &queue, + unsigned start, unsigned end); + +void +queue_print_changes_info(Client &client, const Queue &queue, + uint32_t version); + +void +queue_print_changes_position(Client &client, const Queue &queue, + uint32_t version); + +void +queue_find(Client &client, const Queue &queue, + const SongFilter &filter); + +#endif diff --git a/src/queue/QueueSave.cxx b/src/queue/QueueSave.cxx new file mode 100644 index 000000000..bc2702572 --- /dev/null +++ b/src/queue/QueueSave.cxx @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * 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. + * + * 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. + */ + +#include "config.h" +#include "QueueSave.hxx" +#include "Queue.hxx" +#include "PlaylistError.hxx" +#include "DetachedSong.hxx" +#include "SongSave.hxx" +#include "SongLoader.hxx" +#include "playlist/PlaylistSong.hxx" +#include "fs/io/TextFile.hxx" +#include "fs/io/BufferedOutputStream.hxx" +#include "util/StringUtil.hxx" +#include "util/Error.hxx" +#include "fs/Traits.hxx" +#include "Log.hxx" + +#include <stdlib.h> + +#define PRIO_LABEL "Prio: " + +static void +queue_save_database_song(BufferedOutputStream &os, + int idx, const DetachedSong &song) +{ + os.Format("%i:%s\n", idx, song.GetURI()); +} + +static void +queue_save_full_song(BufferedOutputStream &os, const DetachedSong &song) +{ + song_save(os, song); +} + +static void +queue_save_song(BufferedOutputStream &os, int idx, const DetachedSong &song) +{ + if (song.IsInDatabase() && + song.GetStartTime().IsZero() && song.GetEndTime().IsZero()) + /* use the brief format (just the URI) for "full" + database songs */ + queue_save_database_song(os, idx, song); + else + /* use the long format (URI, range, tags) for the + rest, so all metadata survives a MPD restart */ + queue_save_full_song(os, song); +} + +void +queue_save(BufferedOutputStream &os, const Queue &queue) +{ + for (unsigned i = 0; i < queue.GetLength(); i++) { + uint8_t prio = queue.GetPriorityAtPosition(i); + if (prio != 0) + os.Format(PRIO_LABEL "%u\n", prio); + + queue_save_song(os, i, queue.Get(i)); + } +} + +void +queue_load_song(TextFile &file, const SongLoader &loader, + const char *line, Queue &queue) +{ + if (queue.IsFull()) + return; + + uint8_t priority = 0; + if (StringStartsWith(line, PRIO_LABEL)) { + priority = strtoul(line + sizeof(PRIO_LABEL) - 1, nullptr, 10); + + line = file.ReadLine(); + if (line == nullptr) + return; + } + + DetachedSong *song; + + if (StringStartsWith(line, SONG_BEGIN)) { + const char *uri = line + sizeof(SONG_BEGIN) - 1; + + Error error; + song = song_load(file, uri, error); + if (song == nullptr) { + LogError(error); + return; + } + } else { + char *endptr; + long ret = strtol(line, &endptr, 10); + if (ret < 0 || *endptr != ':' || endptr[1] == 0) { + LogError(playlist_domain, + "Malformed playlist line in state file"); + return; + } + + const char *uri = endptr + 1; + + song = new DetachedSong(uri); + } + + if (!playlist_check_translate_song(*song, nullptr, loader)) { + delete song; + return; + } + + queue.Append(std::move(*song), priority); + delete song; +} diff --git a/src/queue/QueueSave.hxx b/src/queue/QueueSave.hxx new file mode 100644 index 000000000..3fb4dc1a6 --- /dev/null +++ b/src/queue/QueueSave.hxx @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * 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. + * + * 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. + */ + +/* + * This library saves the queue into the state file, and also loads it + * back into memory. + */ + +#ifndef MPD_QUEUE_SAVE_HXX +#define MPD_QUEUE_SAVE_HXX + +struct Queue; +class BufferedOutputStream; +class TextFile; +class SongLoader; + +void +queue_save(BufferedOutputStream &os, const Queue &queue); + +/** + * Loads one song from the state file and appends it to the queue. + */ +void +queue_load_song(TextFile &file, const SongLoader &loader, + const char *line, Queue &queue); + +#endif |