/* the Music Player Daemon (MPD) * Copyright (C) 2003-2007 by Warren Dukes (warren.dukes@gmail.com) * This project's homepage is: 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "playlist.h" #include "player_error.h" #include "command.h" #include "ls.h" #include "tag.h" #include "conf.h" #include "database.h" #include "log.h" #include "path.h" #include "utils.h" #include "state_file.h" #include "storedPlaylist.h" #include "ack.h" #include "myfprintf.h" #include "os_compat.h" #include "main_notify.h" #include "metadata_pipe.h" enum _playlist_state { PLAYLIST_STATE_STOP = 0, PLAYLIST_STATE_PLAY = 1 }; static enum _playlist_state playlist_state; struct _playlist { struct mpd_song **songs; /* holds version a song was modified on */ uint32_t *songMod; int *order; int *positionToId; int *idToPosition; int length; int current; int queued; /* to be decoded */ int repeat; int random; uint32_t version; }; #define PLAYLIST_PREV_UNLESS_ELAPSED 10 #define PLAYLIST_STATE_FILE_STATE "state: " #define PLAYLIST_STATE_FILE_RANDOM "random: " #define PLAYLIST_STATE_FILE_REPEAT "repeat: " #define PLAYLIST_STATE_FILE_CURRENT "current: " #define PLAYLIST_STATE_FILE_TIME "time: " #define PLAYLIST_STATE_FILE_CROSSFADE "crossfade: " #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 #define PLAYLIST_HASH_MULT 4 #define DEFAULT_PLAYLIST_MAX_LENGTH (1024*16) #define DEFAULT_PLAYLIST_SAVE_ABSOLUTE_PATHS 0 static struct _playlist playlist; int playlist_max_length = DEFAULT_PLAYLIST_MAX_LENGTH; static int playlist_stopOnError; static int playlist_errorCount; /* * queue_lock is to prevent ourselves from modifying playlist.queued * while the decoder is decoding the song. The main_thread in mpd is * the only modifier of playlist.queued. However, we may modify * playlist.queued "in-place" without locking if it points to the same * song (during move or shuffle). */ static pthread_mutex_t queue_lock = PTHREAD_MUTEX_INITIALIZER; int playlist_saveAbsolutePaths = DEFAULT_PLAYLIST_SAVE_ABSOLUTE_PATHS; static void swapOrder(int a, int b); static void play_order_num(int order_num, float seek_time); static void randomizeOrder(int start, int end); static void incrPlaylistVersion(void) { static unsigned long max = ((uint32_t) 1 << 31) - 1; if (++playlist.version >= max) { memset(playlist.songMod, 0, playlist.length * sizeof(*playlist.songMod)); playlist.version = 1; } } void playlistVersionChange(void) { int i; for (i = playlist.length; --i >= 0; ) playlist.songMod[i] = playlist.version; incrPlaylistVersion(); } static void incrPlaylistCurrent(void) { if (playlist.current < 0) return; if (playlist.current >= playlist.length - 1) playlist.current = playlist.repeat ? 0 : -1; else playlist.current++; } void initPlaylist(void) { char *test; int i; ConfigParam *param; playlist.length = 0; playlist.repeat = 0; playlist.version = 1; playlist.random = 0; playlist.queued = -1; playlist.current = -1; param = getConfigParam(CONF_MAX_PLAYLIST_LENGTH); if (param) { playlist_max_length = strtol(param->value, &test, 10); if (*test != '\0') FATAL("max playlist length \"%s\" is not an integer, " "line %i\n", param->value, param->line); } playlist_saveAbsolutePaths = getBoolConfigParam( CONF_SAVE_ABSOLUTE_PATHS, 1); if (playlist_saveAbsolutePaths == CONF_BOOL_UNSET) playlist_saveAbsolutePaths = DEFAULT_PLAYLIST_SAVE_ABSOLUTE_PATHS; playlist.songs = xcalloc(playlist_max_length, sizeof(struct mpd_song *)); playlist.songMod = xmalloc(sizeof(uint32_t) * playlist_max_length); playlist.order = xmalloc(sizeof(int) * playlist_max_length); playlist.idToPosition = xmalloc(sizeof(int) * playlist_max_length * PLAYLIST_HASH_MULT); playlist.positionToId = xmalloc(sizeof(int) * playlist_max_length); srandom(time(NULL)); for (i = playlist_max_length * PLAYLIST_HASH_MULT; --i >= 0; ) playlist.idToPosition[i] = -1; } static int getNextId(void) { static int cur = -1; do { if (++cur >= playlist_max_length * PLAYLIST_HASH_MULT) cur = 0; } while (playlist.idToPosition[cur] != -1); return cur; } void finishPlaylist(void) { int i; for (i = playlist.length; --i >= 0; ) { if (!song_is_file(playlist.songs[i])) song_free(playlist.songs[i]); } playlist.length = 0; free(playlist.songs); playlist.songs = NULL; free(playlist.songMod); playlist.songMod = NULL; free(playlist.order); playlist.order = NULL; free(playlist.idToPosition); playlist.idToPosition = NULL; free(playlist.positionToId); playlist.positionToId = NULL; } void clearPlaylist(void) { int i; stopPlaylist(); for (i = playlist.length; --i >= 0 ; ) { if (!song_is_file(playlist.songs[i])) song_free(playlist.songs[i]); playlist.idToPosition[playlist.positionToId[i]] = -1; playlist.songs[i] = NULL; } playlist.length = 0; playlist.current = -1; incrPlaylistVersion(); } int clearStoredPlaylist(const char *utf8file) { return removeAllFromStoredPlaylistByPath(utf8file); } void showPlaylist(int fd) { int i; char path_max_tmp[MPD_PATH_MAX]; for (i = 0; i < playlist.length; i++) fdprintf(fd, "%i:%s\n", i, song_get_url(playlist.songs[i], path_max_tmp)); } void savePlaylistState(int fd) { fdprintf(fd, PLAYLIST_STATE_FILE_STATE); switch (playlist_state) { case PLAYLIST_STATE_PLAY: switch (ob_get_state()) { case OB_STATE_PAUSE: fdprintf(fd, PLAYLIST_STATE_FILE_STATE_PAUSE "\n"); break; default: fdprintf(fd, PLAYLIST_STATE_FILE_STATE_PLAY "\n"); } fdprintf(fd, PLAYLIST_STATE_FILE_CURRENT "%i\n" PLAYLIST_STATE_FILE_TIME "%lu\n", playlist.order[playlist.current], ob_get_elapsed_time()); break; default: fdprintf(fd, PLAYLIST_STATE_FILE_STATE_STOP "\n"); break; } fdprintf(fd, PLAYLIST_STATE_FILE_RANDOM "%i\n" PLAYLIST_STATE_FILE_REPEAT "%i\n" PLAYLIST_STATE_FILE_CROSSFADE "%i\n" PLAYLIST_STATE_FILE_PLAYLIST_BEGIN "\n", playlist.random, playlist.repeat, (int)(ob_get_xfade())); showPlaylist(fd); fdprintf(fd, PLAYLIST_STATE_FILE_PLAYLIST_END "\n"); } static void loadPlaylistFromStateFile(FILE *fp, char *buffer, enum ob_state state, int current, int seek_time) { char *temp; int song; if (!myFgets(buffer, PLAYLIST_BUFFER_SIZE, fp)) state_file_fatal(); while (strcmp(buffer, PLAYLIST_STATE_FILE_PLAYLIST_END)) { temp = strtok(buffer, ":"); if (temp == NULL) state_file_fatal(); song = atoi(temp); if (!(temp = strtok(NULL, ""))) state_file_fatal(); if (addToPlaylist(temp, NULL) == PLAYLIST_RESULT_SUCCESS && current == song) { if (state == OB_STATE_PAUSE) ob_trigger_action(OB_ACTION_PAUSE_SET); if (state != OB_STATE_STOP) seekSongInPlaylist(playlist.length - 1, seek_time); } if (!myFgets(buffer, PLAYLIST_BUFFER_SIZE, fp)) state_file_fatal(); } } void readPlaylistState(FILE *fp) { int current = -1; int seek_time = 0; enum ob_state state = OB_STATE_STOP; char buffer[PLAYLIST_BUFFER_SIZE]; while (myFgets(buffer, PLAYLIST_BUFFER_SIZE, fp)) { if (!prefixcmp(buffer, PLAYLIST_STATE_FILE_STATE)) { if (strcmp(&(buffer[strlen(PLAYLIST_STATE_FILE_STATE)]), PLAYLIST_STATE_FILE_STATE_PLAY) == 0) { state = OB_STATE_PLAY; } else if (strcmp (&(buffer[strlen(PLAYLIST_STATE_FILE_STATE)]), PLAYLIST_STATE_FILE_STATE_PAUSE) == 0) { state = OB_STATE_PAUSE; } } else if (!prefixcmp(buffer, PLAYLIST_STATE_FILE_TIME)) { seek_time = atoi(&(buffer[strlen(PLAYLIST_STATE_FILE_TIME)])); } else if (!prefixcmp(buffer, PLAYLIST_STATE_FILE_REPEAT)) { if (strcmp (&(buffer[strlen(PLAYLIST_STATE_FILE_REPEAT)]), "1") == 0) { setPlaylistRepeatStatus(1); } else setPlaylistRepeatStatus(0); } else if (!prefixcmp(buffer, PLAYLIST_STATE_FILE_CROSSFADE)) { ob_set_xfade(atoi(&(buffer [strlen (PLAYLIST_STATE_FILE_CROSSFADE)]))); } else if (!prefixcmp(buffer, PLAYLIST_STATE_FILE_RANDOM)) { if (strcmp (& (buffer [strlen(PLAYLIST_STATE_FILE_RANDOM)]), "1") == 0) { setPlaylistRandomStatus(1); } else setPlaylistRandomStatus(0); } else if (!prefixcmp(buffer, PLAYLIST_STATE_FILE_CURRENT)) { if (strlen(buffer) == strlen(PLAYLIST_STATE_FILE_CURRENT)) state_file_fatal(); current = atoi(&(buffer [strlen (PLAYLIST_STATE_FILE_CURRENT)])); } else if (!prefixcmp(buffer, PLAYLIST_STATE_FILE_PLAYLIST_BEGIN)) { if (state == OB_STATE_STOP) current = -1; loadPlaylistFromStateFile(fp, buffer, state, current, seek_time); } } } static void printPlaylistSongInfo(int fd, int song) { song_print_info(playlist.songs[song], fd); fdprintf(fd, "Pos: %i\nId: %i\n", song, playlist.positionToId[song]); } int playlistChanges(int fd, uint32_t version) { int i; for (i = 0; i < playlist.length; i++) { if (version > playlist.version || playlist.songMod[i] >= version || playlist.songMod[i] == 0) printPlaylistSongInfo(fd, i); } return 0; } int playlistChangesPosId(int fd, uint32_t version) { int i; for (i = 0; i < playlist.length; i++) { if (version > playlist.version || playlist.songMod[i] >= version || playlist.songMod[i] == 0) fdprintf(fd, "cpos: %i\nId: %i\n", i, playlist.positionToId[i]); } return 0; } enum playlist_result playlistInfo(int fd, int song) { int i; int begin = 0; int end = playlist.length; if (song >= 0) { begin = song; end = song + 1; } if (song >= playlist.length) return PLAYLIST_RESULT_BAD_RANGE; for (i = begin; i < end; i++) printPlaylistSongInfo(fd, i); return PLAYLIST_RESULT_SUCCESS; } static int song_id_exists(int id) { return id >= 0 && id < PLAYLIST_HASH_MULT*playlist_max_length && playlist.idToPosition[id] != -1; } enum playlist_result playlistId(int fd, int id) { int i; int begin = 0; int end = playlist.length; if (id >= 0) { if (!song_id_exists(id)) return PLAYLIST_RESULT_NO_SUCH_SONG; begin = playlist.idToPosition[id]; end = begin + 1; } for (i = begin; i < end; i++) printPlaylistSongInfo(fd, i); return PLAYLIST_RESULT_SUCCESS; } static void swapSongs(int song1, int song2) { struct mpd_song *sTemp; int iTemp; assert(song1 < playlist.length); assert(song1 >= 0); assert(song2 < playlist.length); assert(song2 >= 0); sTemp = playlist.songs[song1]; playlist.songs[song1] = playlist.songs[song2]; playlist.songs[song2] = sTemp; playlist.songMod[song1] = playlist.version; playlist.songMod[song2] = playlist.version; playlist.idToPosition[playlist.positionToId[song1]] = song2; playlist.idToPosition[playlist.positionToId[song2]] = song1; iTemp = playlist.positionToId[song1]; playlist.positionToId[song1] = playlist.positionToId[song2]; playlist.positionToId[song2] = iTemp; } static struct mpd_song *song_at(int order_num) { if (order_num >= 0 && order_num < playlist.length) { assert(playlist.songs[playlist.order[order_num]]); return playlist.songs[playlist.order[order_num]]; } return NULL; } static int next_order_num(void) { if (playlist.current < playlist.length - 1) { return playlist.current + 1; } else if (playlist.length && playlist.repeat) { if (playlist.length > 1 && playlist.random) randomizeOrder(0, playlist.length - 1); return 0; } return -1; } static void queueNextSongInPlaylist(void) { if (playlist_state != PLAYLIST_STATE_PLAY) return; /* DEBUG("%s:%d\n", __func__, __LINE__); */ if (pthread_mutex_trylock(&queue_lock) == EBUSY) return; /* still decoding */ DEBUG("%s:%d\n", __func__, __LINE__); playlist.queued = next_order_num(); pthread_mutex_unlock(&queue_lock); if (playlist.queued < 0) { playlist_state = PLAYLIST_STATE_STOP; if (playlist.length > 0) { if (playlist.random) randomizeOrder(0, playlist.length - 1); else if (ob_get_state() == OB_STATE_STOP) playlist.current = -1; } } else if (dc.state == DC_STATE_STOP) { dc_trigger_action(DC_ACTION_START, 0); } } static void syncPlaylistWithQueue(void) { assert(playlist_state == PLAYLIST_STATE_PLAY); if (!ob_synced()) return; if (player_errno != PLAYER_ERROR_NONE) { DEBUG("playlist: error: %s\n", player_strerror()); playlist.current = playlist.queued; player_clearerror(); } else if (playlist.queued >= 0 && playlist.current != playlist.queued) { DEBUG("playlist: now playing queued song\n"); DEBUG("%s:%d queued: %d\n",__func__,__LINE__,playlist.queued); playlist.current = playlist.queued; } queueNextSongInPlaylist(); } void playlist_queue_next(void) { assert(pthread_equal(pthread_self(), dc.thread)); pthread_mutex_unlock(&queue_lock); wakeup_main_task(); } char *playlist_queued_url(char utf8url[MPD_PATH_MAX]) { struct mpd_song *song; assert(pthread_equal(pthread_self(), dc.thread)); pthread_mutex_lock(&queue_lock); song = song_at(playlist.queued); return song ? song_get_url(song, utf8url) : NULL; } static void queue_song_locked(int order_num) { pthread_mutex_lock(&queue_lock); playlist.queued = order_num; pthread_mutex_unlock(&queue_lock); } /* * stops decoder iff we're decoding a song we haven't played yet * Returns the currently queued song, -1 if we cleared the queue * This will not affect the currently playing song */ static int clear_queue(void) { if (playlist.queued >= 0 && playlist.current != playlist.queued) { dc_trigger_action(DC_ACTION_STOP, 0); assert(dc.state == DC_STATE_STOP); ob_drop_audio(OB_DROP_DECODED); queue_song_locked(-1); } return playlist.queued; } enum playlist_result addToPlaylist(const char *url, int *added_id) { struct mpd_song *song; DEBUG("add to playlist: %s\n", url); if ((song = db_get_song(url))) { } else if (!(isValidRemoteUtf8Url(url) && (song = song_remote_new(url)))) { return PLAYLIST_RESULT_NO_SUCH_SONG; } return addSongToPlaylist(song, added_id); } int addToStoredPlaylist(const char *url, const char *utf8file) { struct mpd_song *song; DEBUG("add to stored playlist: %s\n", url); if ((song = db_get_song(url))) return appendSongToStoredPlaylistByPath(utf8file, song); if (!isValidRemoteUtf8Url(url)) return ACK_ERROR_NO_EXIST; if ((song = song_remote_new(url))) { int ret = appendSongToStoredPlaylistByPath(utf8file, song); song_free(song); return ret; } return ACK_ERROR_NO_EXIST; } enum playlist_result addSongToPlaylist(struct mpd_song * song, int *added_id) { int id; if (playlist.length == playlist_max_length) return PLAYLIST_RESULT_TOO_LARGE; if (playlist_state == PLAYLIST_STATE_PLAY && playlist.queued >= 0 && playlist.current == playlist.length - 1) clear_queue(); id = getNextId(); playlist.songs[playlist.length] = song; playlist.songMod[playlist.length] = playlist.version; playlist.order[playlist.length] = playlist.length; playlist.positionToId[playlist.length] = id; playlist.idToPosition[playlist.positionToId[playlist.length]] = playlist.length; playlist.length++; if (playlist.random) { int swap; int start; if (playlist.queued >= 0) start = playlist.queued + 1; else start = playlist.current + 1; if (start < playlist.length) { swap = random() % (playlist.length - start); swap += start; swapOrder(playlist.length - 1, swap); } } incrPlaylistVersion(); /* we could be playing (but done decoding) the last song */ if (ob_get_state() != OB_STATE_STOP) { playlist_state = PLAYLIST_STATE_PLAY; queueNextSongInPlaylist(); } if (added_id) *added_id = id; return PLAYLIST_RESULT_SUCCESS; } enum playlist_result swapSongsInPlaylist(int song1, int song2) { int queuedSong = -1; int currentSong; int queued_is_current = (playlist.queued == playlist.current); if (song1 < 0 || song1 >= playlist.length || song2 < 0 || song2 >= playlist.length) return PLAYLIST_RESULT_BAD_RANGE; if (playlist_state == PLAYLIST_STATE_PLAY) { if (playlist.queued >= 0) queuedSong = playlist.order[playlist.queued]; assert(playlist.current >= 0 && playlist.current < playlist.length); currentSong = playlist.order[playlist.current]; if (queuedSong == song1 || queuedSong == song2 || currentSong == song1 || currentSong == song2) clear_queue(); } swapSongs(song1, song2); if (playlist.random) { int i; int k; int j = -1; for (i = 0; playlist.order[i] != song1; i++) { if (playlist.order[i] == song2) j = i; } k = i; for (; j == -1; i++) if (playlist.order[i] == song2) j = i; swapOrder(k, j); } else { if (playlist.current == song1) playlist.current = song2; else if (playlist.current == song2) playlist.current = song1; if (queued_is_current) playlist.queued = playlist.current; } queueNextSongInPlaylist(); incrPlaylistVersion(); return PLAYLIST_RESULT_SUCCESS; } enum playlist_result swapSongsInPlaylistById(int id1, int id2) { if (!song_id_exists(id1) || !song_id_exists(id2)) return PLAYLIST_RESULT_NO_SUCH_SONG; return swapSongsInPlaylist(playlist.idToPosition[id1], playlist.idToPosition[id2]); } #define moveSongFromTo(from, to) { \ playlist.idToPosition[playlist.positionToId[from]] = to; \ playlist.positionToId[to] = playlist.positionToId[from]; \ playlist.songs[to] = playlist.songs[from]; \ playlist.songMod[to] = playlist.version; \ } enum playlist_result deleteFromPlaylist(int song) { int i; int songOrder; int stop_current = 0; int prev_queued = playlist.queued; if (song < 0 || song >= playlist.length) return PLAYLIST_RESULT_BAD_RANGE; if (playlist_state == PLAYLIST_STATE_PLAY) { if (prev_queued >= 0 && (playlist.order[prev_queued] == song || playlist.order[playlist.current] == song)) { clear_queue(); } } if (!song_is_file(playlist.songs[song])) song_free(playlist.songs[song]); playlist.idToPosition[playlist.positionToId[song]] = -1; /* delete song from songs array */ for (i = song; i < playlist.length - 1; i++) moveSongFromTo(i + 1, i); /* now find it in the order array */ for (i = 0; i < playlist.length - 1; i++) { if (playlist.order[i] == song) break; } songOrder = i; /* delete the entry from the order array */ for (; i < playlist.length - 1; i++) playlist.order[i] = playlist.order[i + 1]; /* readjust values in the order array */ for (i = 0; i < playlist.length - 1; i++) { if (playlist.order[i] > song) playlist.order[i]--; } /* now take care of other misc stuff */ playlist.songs[playlist.length - 1] = NULL; playlist.length--; incrPlaylistVersion(); if (playlist_state != PLAYLIST_STATE_STOP && playlist.current == songOrder) stop_current = 1; if (playlist.current > songOrder) { if (playlist.current == prev_queued) playlist.queued = playlist.current - 1; playlist.current--; } else if (playlist.current >= playlist.length) { incrPlaylistCurrent(); } if (stop_current) { if (playlist.current >= 0 && songOrder > 0) play_order_num(playlist.current, 0); else stopPlaylist(); } else { queueNextSongInPlaylist(); } return PLAYLIST_RESULT_SUCCESS; } enum playlist_result deleteFromPlaylistById(int id) { if (!song_id_exists(id)) return PLAYLIST_RESULT_NO_SUCH_SONG; return deleteFromPlaylist(playlist.idToPosition[id]); } void deleteASongFromPlaylist(const struct mpd_song * song) { int i; if (NULL == playlist.songs) return; for (i = playlist.length; --i >= 0; ) { if (mpd_unlikely(song == playlist.songs[i])) deleteFromPlaylist(i); } } void stopPlaylist(void) { DEBUG("playlist: stop\n"); DEBUG("%s:%d\n", __func__, __LINE__); dc_trigger_action(DC_ACTION_STOP, 0); DEBUG("%s:%d\n", __func__, __LINE__); assert(dc.state == DC_STATE_STOP); DEBUG("%s:%d\n", __func__, __LINE__); ob_trigger_action(OB_ACTION_STOP); assert(ob_get_state() == OB_STATE_STOP); DEBUG("%s:%d\n", __func__, __LINE__); queue_song_locked(-1); playlist_state = PLAYLIST_STATE_STOP; if (playlist.random) randomizeOrder(0, playlist.length - 1); } static void play_order_num(int order_num, float seek_time) { char path[MPD_PATH_MAX]; enum dc_action action = seek_time ? DC_ACTION_SEEK : DC_ACTION_START; playlist_state = PLAYLIST_STATE_PLAY; assert(order_num >= 0); assert(seek_time >= 0); assert(song_at(order_num)); DEBUG("playlist: play %i:\"%s\"\n", order_num, song_get_url(song_at(order_num), path)); dc_trigger_action(DC_ACTION_STOP, 0); queue_song_locked(order_num); ob_trigger_action(OB_ACTION_RESET); dc_trigger_action(action, seek_time); if (dc.seek_where >= 0) playlist.current = order_num; } enum playlist_result playPlaylist(int song, int stopOnError) { int i = song; player_clearerror(); if (song == -1) { if (playlist.length == 0) return PLAYLIST_RESULT_SUCCESS; if (playlist_state == PLAYLIST_STATE_PLAY) { ob_trigger_action(OB_ACTION_PAUSE_UNSET); return PLAYLIST_RESULT_SUCCESS; } if (playlist_state == PLAYLIST_STATE_STOP && playlist.length > 0 && playlist.current == playlist.length - 1) { i = 0; } else if (playlist.current >= 0 && playlist.current < playlist.length) { i = playlist.current; } else { i = 0; } } else if (song < 0 || song >= playlist.length) { return PLAYLIST_RESULT_BAD_RANGE; } if (playlist.random) { if (song == -1) { randomizeOrder(0, playlist.length - 1); } else { if (song >= 0) for (i = 0; song != playlist.order[i]; i++) ; playlist.current = 0; swapOrder(i, playlist.current); i = playlist.current; randomizeOrder(i + 1, playlist.length - 1); } } playlist_stopOnError = stopOnError; playlist_errorCount = 0; ERROR(__FILE__ ": %d current:%d\n", __LINE__, playlist.current); ob_trigger_action(OB_ACTION_PAUSE_UNSET); play_order_num(i, 0); return PLAYLIST_RESULT_SUCCESS; } enum playlist_result playPlaylistById(int id, int stopOnError) { if (id == -1) return playPlaylist(id, stopOnError); if (!song_id_exists(id)) return PLAYLIST_RESULT_NO_SUCH_SONG; return playPlaylist(playlist.idToPosition[id], stopOnError); } /* This is used when we stream data out to shout while playing static files */ struct mpd_tag *playlist_current_tag(void) { struct mpd_song *song = song_at(playlist.current); /* Non-file song tags can get swept out from under us */ return (song && song_is_file(song)) ? song->tag : NULL; } /* This receives dynamic metadata updates from streams */ static void sync_metadata(void) { struct mpd_song *song; struct mpd_tag *tag; if (!(tag = metadata_pipe_current())) return; song = song_at(playlist.current); if (!song || song_is_file(song) || tag_equal(song->tag, tag)) { tag_free(tag); return; } if (song->tag) tag_free(song->tag); song->tag = tag; playlist.songMod[playlist.order[playlist.current]] = playlist.version; incrPlaylistVersion(); } void syncPlayerAndPlaylist(void) { if (playlist_state != PLAYLIST_STATE_PLAY) return; syncPlaylistWithQueue(); sync_metadata(); /* DEBUG("queued:%d current:%d\n", playlist.queued, playlist.current); */ if (playlist_state == PLAYLIST_STATE_PLAY && playlist.queued >= 0 && playlist.queued != playlist.current && ob_synced() && dc.state == DC_STATE_STOP && ob_get_state() != OB_STATE_PAUSE) dc_trigger_action(DC_ACTION_START, 0); } void nextSongInPlaylist(void) { int next; if (playlist_state != PLAYLIST_STATE_PLAY) return; playlist_stopOnError = 0; next = next_order_num(); if (next < 0) { /* we were already at last song w/o repeat: */ incrPlaylistCurrent(); stopPlaylist(); return; } ob_trigger_action(OB_ACTION_PAUSE_UNSET); play_order_num(next, 0); } int getPlaylistRepeatStatus(void) { return playlist.repeat; } int getPlaylistRandomStatus(void) { return playlist.random; } void setPlaylistRepeatStatus(int status) { if (playlist_state == PLAYLIST_STATE_PLAY && playlist.repeat && !status && playlist.queued == 0) clear_queue(); playlist.repeat = status; } enum playlist_result moveSongInPlaylist(int from, int to) { int i; struct mpd_song *tmpSong; int tmpId; int currentSong; int queued_is_current = (playlist.queued == playlist.current); if (from < 0 || from >= playlist.length) return PLAYLIST_RESULT_BAD_RANGE; if ((to >= 0 && to >= playlist.length) || (to < 0 && abs(to) > playlist.length)) return PLAYLIST_RESULT_BAD_RANGE; if (from == to) /* no-op */ return PLAYLIST_RESULT_SUCCESS; /* * (to < 0) => move to offset from current song * (-playlist.length == to) => move to position BEFORE current song */ currentSong = playlist.order[playlist.current]; if (to < 0 && playlist.current >= 0) { if (currentSong == from) /* no-op, can't be moved to offset of itself */ return PLAYLIST_RESULT_SUCCESS; to = (currentSong + abs(to)) % playlist.length; } if (playlist_state == PLAYLIST_STATE_PLAY && !queued_is_current) { int queuedSong = -1; if (playlist.queued >= 0) queuedSong = playlist.order[playlist.queued]; if (queuedSong == from || queuedSong == to || currentSong == from || currentSong == to) clear_queue(); } tmpSong = playlist.songs[from]; tmpId = playlist.positionToId[from]; /* move songs to one less in from->to */ for (i = from; i < to; i++) moveSongFromTo(i + 1, i); /* move songs to one more in to->from */ for (i = from; i > to; i--) { moveSongFromTo(i - 1, i); } /* put song at _to_ */ playlist.idToPosition[tmpId] = to; playlist.positionToId[to] = tmpId; playlist.songs[to] = tmpSong; playlist.songMod[to] = playlist.version; /* now deal with order */ if (playlist.random) { for (i = 0; i < playlist.length; i++) { if (playlist.order[i] > from && playlist.order[i] <= to) { playlist.order[i]--; } else if (playlist.order[i] < from && playlist.order[i] >= to) { playlist.order[i]++; } else if (from == playlist.order[i]) { playlist.order[i] = to; } } } else { if (playlist.current == from) playlist.current = to; else if (playlist.current > from && playlist.current <= to) playlist.current--; else if (playlist.current >= to && playlist.current < from) playlist.current++; if (queued_is_current) playlist.queued = playlist.current; } queueNextSongInPlaylist(); incrPlaylistVersion(); return PLAYLIST_RESULT_SUCCESS; } enum playlist_result moveSongInPlaylistById(int id1, int to) { if (!song_id_exists(id1)) return PLAYLIST_RESULT_NO_SUCH_SONG; return moveSongInPlaylist(playlist.idToPosition[id1], to); } static void orderPlaylist(void) { int i; int queued_is_current = (playlist.queued == playlist.current); if (!queued_is_current && playlist_state == PLAYLIST_STATE_PLAY && playlist.queued >= 0) clear_queue(); if (playlist.current >= 0 && playlist.current < playlist.length) { playlist.current = playlist.order[playlist.current]; if (queued_is_current) playlist.queued = playlist.current; } for (i = playlist.length; --i >= 0; ) playlist.order[i] = i; } static void swapOrder(int a, int b) { int bak; assert(a < playlist.length); assert(a >= 0); assert(b < playlist.length); assert(b >= 0); bak = playlist.order[a]; playlist.order[a] = playlist.order[b]; playlist.order[b] = bak; } static void randomizeOrder(int start, int end) { int i; int ri; int queued_is_current = (playlist.queued == playlist.current); DEBUG("playlist: randomize from %i to %i\n", start, end); if (!queued_is_current && playlist_state == PLAYLIST_STATE_PLAY && playlist.queued >= start && playlist.queued <= end) clear_queue(); for (i = start; i < end; i++) { ri = random() % (end - i) + start; if (ri == playlist.current) playlist.current = i; else if (i == playlist.current) playlist.current = ri; swapOrder(i, ri); } if (queued_is_current) playlist.queued = playlist.current; } void setPlaylistRandomStatus(int status) { if (status != playlist.random) { if (playlist.random) randomizeOrder(0, playlist.length - 1); else orderPlaylist(); if (playlist_state == PLAYLIST_STATE_PLAY) queueNextSongInPlaylist(); } } void previousSongInPlaylist(void) { static time_t lastTime; time_t diff = time(NULL) - lastTime; int prev_order_num; lastTime += diff; if (playlist_state != PLAYLIST_STATE_PLAY) return; syncPlaylistWithQueue(); if (diff && ob_get_elapsed_time() > PLAYLIST_PREV_UNLESS_ELAPSED) { prev_order_num = playlist.current; } else { if (playlist.current > 0) prev_order_num = playlist.current - 1; else if (playlist.repeat) prev_order_num = playlist.length - 1; else prev_order_num = playlist.current; } ob_trigger_action(OB_ACTION_PAUSE_UNSET); play_order_num(prev_order_num, 0); } void shufflePlaylist(void) { int i; int ri; int playing_queued = 0; int length = playlist.length; if (length > 1) { if (playlist_state == PLAYLIST_STATE_PLAY) { if (playlist.queued == playlist.current) playing_queued = 1; else clear_queue(); /* put current playing song first */ swapSongs(0, playlist.order[playlist.current]); if (playlist.random) { int j; for (j = 0; 0 != playlist.order[j]; j++) ; playlist.current = j; } else playlist.current = 0; if (playing_queued) playlist.queued = playlist.current; i = 1; } else { i = 0; playlist.current = -1; } /* shuffle the rest of the list */ for (; --length > 0; ++i) { ri = random() % length + 1; swapSongs(i, ri); } incrPlaylistVersion(); if (playlist_state == PLAYLIST_STATE_PLAY) queueNextSongInPlaylist(); } } enum playlist_result deletePlaylist(const char *utf8file) { char path_max_tmp[MPD_PATH_MAX]; utf8_to_fs_playlist_path(path_max_tmp, utf8file); if (!isPlaylist(path_max_tmp)) return PLAYLIST_RESULT_NO_SUCH_LIST; if (unlink(path_max_tmp) < 0) return PLAYLIST_RESULT_ERRNO; return PLAYLIST_RESULT_SUCCESS; } enum playlist_result savePlaylist(const char *utf8file) { FILE *fp; int i; struct stat sb; char path_max_tmp[MPD_PATH_MAX]; if (!is_valid_playlist_name(utf8file)) return PLAYLIST_RESULT_BAD_NAME; utf8_to_fs_playlist_path(path_max_tmp, utf8file); if (!stat(path_max_tmp, &sb)) return PLAYLIST_RESULT_LIST_EXISTS; while (!(fp = fopen(path_max_tmp, "w")) && errno == EINTR); if (fp == NULL) return PLAYLIST_RESULT_ERRNO; for (i = 0; i < playlist.length; i++) { char tmp[MPD_PATH_MAX]; song_get_url(playlist.songs[i], path_max_tmp); utf8_to_fs_charset(tmp, path_max_tmp); if (playlist_saveAbsolutePaths && song_is_file(playlist.songs[i])) fprintf(fp, "%s\n", rmp2amp_r(tmp, tmp)); else fprintf(fp, "%s\n", tmp); } while (fclose(fp) && errno == EINTR) ; return PLAYLIST_RESULT_SUCCESS; } int getPlaylistCurrentSong(void) { DEBUG("%s:%d current: %d\n", __func__, __LINE__, playlist.current); if (ob_get_state() != OB_STATE_STOP && playlist.current >= 0 && playlist.current < playlist.length) return playlist.order[playlist.current]; return -1; } unsigned long getPlaylistVersion(void) { return playlist.version; } int getPlaylistLength(void) { return playlist.length; } /* * This command will always return 0 regardless of whether or * not the seek succeeded (it's always been the case, apparently) */ enum playlist_result seekSongInPlaylist(int song, float seek_time) { int i = song; char path[MPD_PATH_MAX]; if (song < 0 || song >= playlist.length) return PLAYLIST_RESULT_BAD_RANGE; if (playlist.random) for (i = 0; song != playlist.order[i]; i++) ; player_clearerror(); playlist_stopOnError = 1; playlist_errorCount = 0; if (playlist_state == PLAYLIST_STATE_PLAY && (playlist.current == i && playlist.queued == i)) { dc_trigger_action(DC_ACTION_SEEK, seek_time); if (dc.seek_where != DC_SEEK_MISMATCH) return PLAYLIST_RESULT_SUCCESS; /* * if near end of decoding can cause seek to fail (since we're * already on another song) (leading to DC_SEEK_MISMATCH), * so fall through to restarting the decoder below. */ } DEBUG("playlist: seek %i:\"%s\"\n", i, song_get_url(song_at(i), path)); play_order_num(i, seek_time); return PLAYLIST_RESULT_SUCCESS; } enum playlist_result seekSongInPlaylistById(int id, float seek_time) { if (!song_id_exists(id)) return PLAYLIST_RESULT_NO_SUCH_SONG; return seekSongInPlaylist(playlist.idToPosition[id], seek_time); } int getPlaylistSongId(int song) { return playlist.positionToId[song]; } int PlaylistInfo(int fd, const char *utf8file, int detail) { ListNode *node; List *list; if (!(list = loadStoredPlaylist(utf8file))) return -1; node = list->firstNode; while (node != NULL) { char *temp = node->data; int wrote = 0; if (detail) { struct mpd_song *song = db_get_song(temp); if (song) { song_print_info(song, fd); wrote = 1; } } if (!wrote) fdprintf(fd, SONG_FILE "%s\n", temp); node = node->nextNode; } freeList(list); return 0; } enum playlist_result loadPlaylist(int fd, const char *utf8file) { ListNode *node; List *list; if (!(list = loadStoredPlaylist(utf8file))) return PLAYLIST_RESULT_NO_SUCH_LIST; node = list->firstNode; while (node != NULL) { char *temp = node->data; if ((addToPlaylist(temp, NULL)) != PLAYLIST_RESULT_SUCCESS) { /* for windows compatibility, convert slashes */ char *temp2 = xstrdup(temp); char *p = temp2; while (*p) { if (*p == '\\') *p = '/'; p++; } if ((addToPlaylist(temp, NULL)) != PLAYLIST_RESULT_SUCCESS) { commandError(fd, ACK_ERROR_PLAYLIST_LOAD, "can't add file \"%s\"", temp2); } free(temp2); } node = node->nextNode; } freeList(list); return PLAYLIST_RESULT_SUCCESS; } void searchForSongsInPlaylist(int fd, int numItems, LocateTagItem * items) { int i; char **originalNeedles = xmalloc(numItems * sizeof(char *)); for (i = 0; i < numItems; i++) { originalNeedles[i] = items[i].needle; items[i].needle = strDupToUpper(originalNeedles[i]); } for (i = 0; i < playlist.length; i++) { if (strstrSearchTags(playlist.songs[i], numItems, items)) printPlaylistSongInfo(fd, i); } for (i = 0; i < numItems; i++) { free(items[i].needle); items[i].needle = originalNeedles[i]; } free(originalNeedles); } void findSongsInPlaylist(int fd, int numItems, LocateTagItem * items) { int i; for (i = 0; i < playlist.length; i++) { if (tagItemsFoundAndMatches(playlist.songs[i], numItems, items)) printPlaylistSongInfo(fd, i); } } /* * Not supporting '/' was done out of laziness, and we should really * strive to support it in the future. * * Not supporting '\r' and '\n' is done out of protocol limitations (and * arguably laziness), but bending over head over heels to modify the * protocol (and compatibility with all clients) to support idiots who * put '\r' and '\n' in filenames isn't going to happen, either. */ int is_valid_playlist_name(const char *utf8path) { return strchr(utf8path, '/') == NULL && strchr(utf8path, '\n') == NULL && strchr(utf8path, '\r') == NULL; } int playlist_playing(void) { return (playlist_state == PLAYLIST_STATE_PLAY); }