diff options
Diffstat (limited to 'src/playlist')
44 files changed, 2488 insertions, 936 deletions
diff --git a/src/playlist/AsxPlaylistPlugin.cxx b/src/playlist/AsxPlaylistPlugin.cxx deleted file mode 100644 index 94198b8c3..000000000 --- a/src/playlist/AsxPlaylistPlugin.cxx +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright (C) 2003-2013 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 "AsxPlaylistPlugin.hxx" -#include "PlaylistPlugin.hxx" -#include "MemorySongEnumerator.hxx" -#include "InputStream.hxx" -#include "Song.hxx" -#include "tag/Tag.hxx" -#include "util/ASCII.hxx" -#include "util/Error.hxx" -#include "util/Domain.hxx" -#include "Log.hxx" - -#include <glib.h> - -#include <assert.h> -#include <string.h> - -static constexpr Domain asx_domain("asx"); - -/** - * This is the state object for the GLib XML parser. - */ -struct AsxParser { - /** - * The list of songs (in reverse order because that's faster - * while adding). - */ - std::forward_list<SongPointer> songs; - - /** - * The current position in the XML file. - */ - enum { - ROOT, ENTRY, - } state; - - /** - * The current tag within the "entry" element. This is only - * valid if state==ENTRY. TAG_NUM_OF_ITEM_TYPES means there - * is no (known) tag. - */ - TagType tag; - - /** - * The current song. It is allocated after the "location" - * element. - */ - Song *song; - - AsxParser() - :state(ROOT) {} - -}; - -static const gchar * -get_attribute(const gchar **attribute_names, const gchar **attribute_values, - const gchar *name) -{ - for (unsigned i = 0; attribute_names[i] != nullptr; ++i) - if (StringEqualsCaseASCII(attribute_names[i], name)) - return attribute_values[i]; - - return nullptr; -} - -static void -asx_start_element(gcc_unused GMarkupParseContext *context, - const gchar *element_name, - const gchar **attribute_names, - const gchar **attribute_values, - gpointer user_data, gcc_unused GError **error) -{ - AsxParser *parser = (AsxParser *)user_data; - - switch (parser->state) { - case AsxParser::ROOT: - if (StringEqualsCaseASCII(element_name, "entry")) { - parser->state = AsxParser::ENTRY; - parser->song = Song::NewRemote("asx:"); - parser->tag = TAG_NUM_OF_ITEM_TYPES; - } - - break; - - case AsxParser::ENTRY: - if (StringEqualsCaseASCII(element_name, "ref")) { - const gchar *href = get_attribute(attribute_names, - attribute_values, - "href"); - if (href != nullptr) { - /* create new song object, and copy - the existing tag over; we cannot - replace the existing song's URI, - because that attribute is - immutable */ - Song *song = Song::NewRemote(href); - - if (parser->song != nullptr) { - song->tag = parser->song->tag; - parser->song->tag = nullptr; - parser->song->Free(); - } - - parser->song = song; - } - } else if (StringEqualsCaseASCII(element_name, "author")) - /* is that correct? or should it be COMPOSER - or PERFORMER? */ - parser->tag = TAG_ARTIST; - else if (StringEqualsCaseASCII(element_name, "title")) - parser->tag = TAG_TITLE; - - break; - } -} - -static void -asx_end_element(gcc_unused GMarkupParseContext *context, - const gchar *element_name, - gpointer user_data, gcc_unused GError **error) -{ - AsxParser *parser = (AsxParser *)user_data; - - switch (parser->state) { - case AsxParser::ROOT: - break; - - case AsxParser::ENTRY: - if (StringEqualsCaseASCII(element_name, "entry")) { - if (strcmp(parser->song->uri, "asx:") != 0) - parser->songs.emplace_front(parser->song); - else - parser->song->Free(); - - parser->state = AsxParser::ROOT; - } else - parser->tag = TAG_NUM_OF_ITEM_TYPES; - - break; - } -} - -static void -asx_text(gcc_unused GMarkupParseContext *context, - const gchar *text, gsize text_len, - gpointer user_data, gcc_unused GError **error) -{ - AsxParser *parser = (AsxParser *)user_data; - - switch (parser->state) { - case AsxParser::ROOT: - break; - - case AsxParser::ENTRY: - if (parser->tag != TAG_NUM_OF_ITEM_TYPES) { - if (parser->song->tag == nullptr) - parser->song->tag = new Tag(); - parser->song->tag->AddItem(parser->tag, - text, text_len); - } - - break; - } -} - -static const GMarkupParser asx_parser = { - asx_start_element, - asx_end_element, - asx_text, - nullptr, - nullptr, -}; - -static void -asx_parser_destroy(gpointer data) -{ - AsxParser *parser = (AsxParser *)data; - - if (parser->state >= AsxParser::ENTRY) - parser->song->Free(); -} - -/* - * The playlist object - * - */ - -static SongEnumerator * -asx_open_stream(InputStream &is) -{ - AsxParser parser; - GMarkupParseContext *context; - char buffer[1024]; - size_t nbytes; - bool success; - Error error2; - GError *error = nullptr; - - /* parse the ASX XML file */ - - context = g_markup_parse_context_new(&asx_parser, - G_MARKUP_TREAT_CDATA_AS_TEXT, - &parser, asx_parser_destroy); - - while (true) { - nbytes = is.LockRead(buffer, sizeof(buffer), error2); - if (nbytes == 0) { - if (error2.IsDefined()) { - g_markup_parse_context_free(context); - LogError(error2); - return nullptr; - } - - break; - } - - success = g_markup_parse_context_parse(context, buffer, nbytes, - &error); - if (!success) { - FormatErrno(asx_domain, - "XML parser failed: %s", error->message); - g_error_free(error); - g_markup_parse_context_free(context); - return nullptr; - } - } - - success = g_markup_parse_context_end_parse(context, &error); - if (!success) { - FormatErrno(asx_domain, - "XML parser failed: %s", error->message); - g_error_free(error); - g_markup_parse_context_free(context); - return nullptr; - } - - parser.songs.reverse(); - MemorySongEnumerator *playlist = - new MemorySongEnumerator(std::move(parser.songs)); - - g_markup_parse_context_free(context); - - return playlist; -} - -static const char *const asx_suffixes[] = { - "asx", - nullptr -}; - -static const char *const asx_mime_types[] = { - "video/x-ms-asf", - nullptr -}; - -const struct playlist_plugin asx_playlist_plugin = { - "asx", - - nullptr, - nullptr, - nullptr, - asx_open_stream, - - nullptr, - asx_suffixes, - asx_mime_types, -}; diff --git a/src/playlist/CloseSongEnumerator.cxx b/src/playlist/CloseSongEnumerator.cxx new file mode 100644 index 000000000..2dddef823 --- /dev/null +++ b/src/playlist/CloseSongEnumerator.cxx @@ -0,0 +1,34 @@ +/* + * 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 "CloseSongEnumerator.hxx" +#include "input/InputStream.hxx" + +CloseSongEnumerator::~CloseSongEnumerator() +{ + delete other; + delete is; +} + +DetachedSong * +CloseSongEnumerator::NextSong() +{ + return other->NextSong(); +} diff --git a/src/playlist/CloseSongEnumerator.hxx b/src/playlist/CloseSongEnumerator.hxx new file mode 100644 index 000000000..17f015394 --- /dev/null +++ b/src/playlist/CloseSongEnumerator.hxx @@ -0,0 +1,47 @@ +/* + * 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_CLOSE_SONG_ENUMERATOR_HXX +#define MPD_CLOSE_SONG_ENUMERATOR_HXX + +#include "SongEnumerator.hxx" +#include "Compiler.h" + +class InputStream; + +/** + * A #SongEnumerator wrapper that closes an #InputStream automatically + * after deleting the #SongEnumerator + */ +class CloseSongEnumerator final : public SongEnumerator { + SongEnumerator *const other; + + InputStream *const is; + +public: + gcc_nonnull_all + CloseSongEnumerator(SongEnumerator *_other, InputStream *const _is) + :other(_other), is(_is) {} + + virtual ~CloseSongEnumerator(); + + virtual DetachedSong *NextSong() override; +}; + +#endif diff --git a/src/playlist/MemorySongEnumerator.cxx b/src/playlist/MemorySongEnumerator.cxx new file mode 100644 index 000000000..c3127c2bf --- /dev/null +++ b/src/playlist/MemorySongEnumerator.cxx @@ -0,0 +1,32 @@ +/* + * 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 "MemorySongEnumerator.hxx" + +DetachedSong * +MemorySongEnumerator::NextSong() +{ + if (songs.empty()) + return nullptr; + + auto result = new DetachedSong(std::move(songs.front())); + songs.pop_front(); + return result; +} diff --git a/src/playlist/MemorySongEnumerator.hxx b/src/playlist/MemorySongEnumerator.hxx new file mode 100644 index 000000000..d1259f011 --- /dev/null +++ b/src/playlist/MemorySongEnumerator.hxx @@ -0,0 +1,39 @@ +/* + * 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_MEMORY_PLAYLIST_PROVIDER_HXX +#define MPD_MEMORY_PLAYLIST_PROVIDER_HXX + +#include "SongEnumerator.hxx" +#include "DetachedSong.hxx" +#include "Compiler.h" + +#include <forward_list> + +class MemorySongEnumerator final : public SongEnumerator { + std::forward_list<DetachedSong> songs; + +public: + MemorySongEnumerator(std::forward_list<DetachedSong> &&_songs) + :songs(std::move(_songs)) {} + + virtual DetachedSong *NextSong() override; +}; + +#endif diff --git a/src/playlist/PlaylistAny.cxx b/src/playlist/PlaylistAny.cxx new file mode 100644 index 000000000..7093fb99a --- /dev/null +++ b/src/playlist/PlaylistAny.cxx @@ -0,0 +1,40 @@ +/* + * 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 "PlaylistAny.hxx" +#include "PlaylistStream.hxx" +#include "PlaylistMapper.hxx" +#include "util/UriUtil.hxx" + +SongEnumerator * +playlist_open_any(const char *uri, +#ifdef ENABLE_DATABASE + const Storage *storage, +#endif + Mutex &mutex, Cond &cond) +{ + return uri_has_scheme(uri) + ? playlist_open_remote(uri, mutex, cond) + : playlist_mapper_open(uri, +#ifdef ENABLE_DATABASE + storage, +#endif + mutex, cond); +} diff --git a/src/playlist/PlaylistAny.hxx b/src/playlist/PlaylistAny.hxx new file mode 100644 index 000000000..23b0075b6 --- /dev/null +++ b/src/playlist/PlaylistAny.hxx @@ -0,0 +1,40 @@ +/* + * 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_ANY_HXX +#define MPD_PLAYLIST_ANY_HXX + +class Mutex; +class Cond; +class SongEnumerator; +class Storage; + +/** + * Opens a playlist from the specified URI, which can be either an + * absolute remote URI (with a scheme) or a relative path to the + * music orplaylist directory. + */ +SongEnumerator * +playlist_open_any(const char *uri, +#ifdef ENABLE_DATABASE + const Storage *storage, +#endif + Mutex &mutex, Cond &cond); + +#endif diff --git a/src/playlist/PlaylistMapper.cxx b/src/playlist/PlaylistMapper.cxx new file mode 100644 index 000000000..042a39d34 --- /dev/null +++ b/src/playlist/PlaylistMapper.cxx @@ -0,0 +1,97 @@ +/* + * 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 "PlaylistMapper.hxx" +#include "PlaylistFile.hxx" +#include "PlaylistStream.hxx" +#include "PlaylistRegistry.hxx" +#include "Mapper.hxx" +#include "fs/AllocatedPath.hxx" +#include "storage/StorageInterface.hxx" +#include "util/UriUtil.hxx" + +#include <assert.h> + +/** + * Load a playlist from the configured playlist directory. + */ +static SongEnumerator * +playlist_open_in_playlist_dir(const char *uri, Mutex &mutex, Cond &cond) +{ + assert(spl_valid_name(uri)); + + const auto path_fs = map_spl_utf8_to_fs(uri); + if (path_fs.IsNull()) + return nullptr; + + return playlist_open_path(path_fs, mutex, cond); +} + +#ifdef ENABLE_DATABASE + +/** + * Load a playlist from the configured music directory. + */ +static SongEnumerator * +playlist_open_in_storage(const char *uri, const Storage *storage, + Mutex &mutex, Cond &cond) +{ + assert(uri_safe_local(uri)); + + if (storage == nullptr) + return nullptr; + + { + const auto path = storage->MapFS(uri); + if (!path.IsNull()) + return playlist_open_path(path, mutex, cond); + } + + const auto uri2 = storage->MapUTF8(uri); + return playlist_open_remote(uri2.c_str(), mutex, cond); +} + +#endif + +SongEnumerator * +playlist_mapper_open(const char *uri, +#ifdef ENABLE_DATABASE + const Storage *storage, +#endif + Mutex &mutex, Cond &cond) +{ + if (spl_valid_name(uri)) { + auto playlist = playlist_open_in_playlist_dir(uri, + mutex, cond); + if (playlist != nullptr) + return playlist; + } + +#ifdef ENABLE_DATABASE + if (uri_safe_local(uri)) { + auto playlist = playlist_open_in_storage(uri, storage, + mutex, cond); + if (playlist != nullptr) + return playlist; + } +#endif + + return nullptr; +} diff --git a/src/playlist/PlaylistMapper.hxx b/src/playlist/PlaylistMapper.hxx new file mode 100644 index 000000000..29ce45083 --- /dev/null +++ b/src/playlist/PlaylistMapper.hxx @@ -0,0 +1,41 @@ +/* + * 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_MAPPER_HXX +#define MPD_PLAYLIST_MAPPER_HXX + +#include "check.h" + +class Mutex; +class Cond; +class SongEnumerator; +class Storage; + +/** + * Opens a playlist from an URI relative to the playlist or music + * directory. + */ +SongEnumerator * +playlist_mapper_open(const char *uri, +#ifdef ENABLE_DATABASE + const Storage *storage, +#endif + Mutex &mutex, Cond &cond); + +#endif diff --git a/src/playlist/PlaylistPlugin.hxx b/src/playlist/PlaylistPlugin.hxx new file mode 100644 index 000000000..fd779ad8d --- /dev/null +++ b/src/playlist/PlaylistPlugin.hxx @@ -0,0 +1,109 @@ +/* + * 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_PLUGIN_HXX +#define MPD_PLAYLIST_PLUGIN_HXX + +struct config_param; +class InputStream; +struct Tag; +class Mutex; +class Cond; +class SongEnumerator; + +struct playlist_plugin { + const char *name; + + /** + * Initialize the plugin. Optional method. + * + * @param param a configuration block for this plugin, or nullptr + * if none is configured + * @return true if the plugin was initialized successfully, + * false if the plugin is not available + */ + bool (*init)(const config_param ¶m); + + /** + * Deinitialize a plugin which was initialized successfully. + * Optional method. + */ + void (*finish)(void); + + /** + * Opens the playlist on the specified URI. This URI has + * either matched one of the schemes or one of the suffixes. + */ + SongEnumerator *(*open_uri)(const char *uri, + Mutex &mutex, Cond &cond); + + /** + * Opens the playlist in the specified input stream. It has + * either matched one of the suffixes or one of the MIME + * types. + */ + SongEnumerator *(*open_stream)(InputStream &is); + + const char *const*schemes; + const char *const*suffixes; + const char *const*mime_types; +}; + +/** + * Initialize a plugin. + * + * @param param a configuration block for this plugin, or nullptr if none + * is configured + * @return true if the plugin was initialized successfully, false if + * the plugin is not available + */ +static inline bool +playlist_plugin_init(const struct playlist_plugin *plugin, + const config_param ¶m) +{ + return plugin->init != nullptr + ? plugin->init(param) + : true; +} + +/** + * Deinitialize a plugin which was initialized successfully. + */ +static inline void +playlist_plugin_finish(const struct playlist_plugin *plugin) +{ + if (plugin->finish != nullptr) + plugin->finish(); +} + +static inline SongEnumerator * +playlist_plugin_open_uri(const struct playlist_plugin *plugin, const char *uri, + Mutex &mutex, Cond &cond) +{ + return plugin->open_uri(uri, mutex, cond); +} + +static inline SongEnumerator * +playlist_plugin_open_stream(const struct playlist_plugin *plugin, + InputStream &is) +{ + return plugin->open_stream(is); +} + +#endif diff --git a/src/playlist/PlaylistQueue.cxx b/src/playlist/PlaylistQueue.cxx new file mode 100644 index 000000000..b10a26172 --- /dev/null +++ b/src/playlist/PlaylistQueue.cxx @@ -0,0 +1,99 @@ +/* + * 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 "PlaylistQueue.hxx" +#include "PlaylistAny.hxx" +#include "PlaylistSong.hxx" +#include "queue/Playlist.hxx" +#include "SongEnumerator.hxx" +#include "DetachedSong.hxx" +#include "thread/Mutex.hxx" +#include "thread/Cond.hxx" +#include "fs/Traits.hxx" +#include "util/Error.hxx" + +#ifdef ENABLE_DATABASE +#include "SongLoader.hxx" +#endif + +bool +playlist_load_into_queue(const char *uri, SongEnumerator &e, + unsigned start_index, unsigned end_index, + playlist &dest, PlayerControl &pc, + const SongLoader &loader, + Error &error) +{ + const std::string base_uri = uri != nullptr + ? PathTraitsUTF8::GetParent(uri) + : std::string("."); + + DetachedSong *song; + for (unsigned i = 0; + i < end_index && (song = e.NextSong()) != nullptr; + ++i) { + if (i < start_index) { + /* skip songs before the start index */ + delete song; + continue; + } + + if (!playlist_check_translate_song(*song, base_uri.c_str(), + loader)) { + delete song; + continue; + } + + unsigned id = dest.AppendSong(pc, std::move(*song), error); + delete song; + if (id == 0) + return false; + } + + return true; +} + +bool +playlist_open_into_queue(const char *uri, + unsigned start_index, unsigned end_index, + playlist &dest, PlayerControl &pc, + const SongLoader &loader, + Error &error) +{ + Mutex mutex; + Cond cond; + + auto playlist = playlist_open_any(uri, +#ifdef ENABLE_DATABASE + loader.GetStorage(), +#endif + mutex, cond); + if (playlist == nullptr) { + error.Set(playlist_domain, int(PlaylistResult::NO_SUCH_LIST), + "No such playlist"); + return false; + } + + bool result = + playlist_load_into_queue(uri, *playlist, + start_index, end_index, + dest, pc, loader, error); + delete playlist; + return result; +} diff --git a/src/playlist/PlaylistQueue.hxx b/src/playlist/PlaylistQueue.hxx new file mode 100644 index 000000000..28eb86fcc --- /dev/null +++ b/src/playlist/PlaylistQueue.hxx @@ -0,0 +1,62 @@ +/* + * 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. + */ + +/*! \file + * \brief Glue between playlist plugin and the play queue + */ + +#ifndef MPD_PLAYLIST_QUEUE_HXX +#define MPD_PLAYLIST_QUEUE_HXX + +#include "PlaylistError.hxx" + +class Error; +class SongLoader; +class SongEnumerator; +struct playlist; +struct PlayerControl; + +/** + * Loads the contents of a playlist and append it to the specified + * play queue. + * + * @param uri the URI of the playlist, used to resolve relative song + * URIs + * @param start_index the index of the first song + * @param end_index the index of the last song (excluding) + */ +bool +playlist_load_into_queue(const char *uri, SongEnumerator &e, + unsigned start_index, unsigned end_index, + playlist &dest, PlayerControl &pc, + const SongLoader &loader, + Error &error); + +/** + * Opens a playlist with a playlist plugin and append to the specified + * play queue. + */ +bool +playlist_open_into_queue(const char *uri, + unsigned start_index, unsigned end_index, + playlist &dest, PlayerControl &pc, + const SongLoader &loader, Error &error); + +#endif + diff --git a/src/playlist/PlaylistRegistry.cxx b/src/playlist/PlaylistRegistry.cxx new file mode 100644 index 000000000..4e9ef890e --- /dev/null +++ b/src/playlist/PlaylistRegistry.cxx @@ -0,0 +1,285 @@ +/* + * 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 "PlaylistRegistry.hxx" +#include "PlaylistPlugin.hxx" +#include "plugins/ExtM3uPlaylistPlugin.hxx" +#include "plugins/M3uPlaylistPlugin.hxx" +#include "plugins/XspfPlaylistPlugin.hxx" +#include "plugins/DespotifyPlaylistPlugin.hxx" +#include "plugins/SoundCloudPlaylistPlugin.hxx" +#include "plugins/PlsPlaylistPlugin.hxx" +#include "plugins/AsxPlaylistPlugin.hxx" +#include "plugins/RssPlaylistPlugin.hxx" +#include "plugins/CuePlaylistPlugin.hxx" +#include "plugins/EmbeddedCuePlaylistPlugin.hxx" +#include "input/InputStream.hxx" +#include "util/UriUtil.hxx" +#include "util/StringUtil.hxx" +#include "util/Error.hxx" +#include "util/Macros.hxx" +#include "config/ConfigGlobal.hxx" +#include "config/ConfigData.hxx" +#include "Log.hxx" + +#include <assert.h> +#include <string.h> + +const struct playlist_plugin *const playlist_plugins[] = { + &extm3u_playlist_plugin, + &m3u_playlist_plugin, +#ifdef HAVE_GLIB + // TODO: enable without GLib + &pls_playlist_plugin, +#endif +#ifdef HAVE_EXPAT + &xspf_playlist_plugin, + &asx_playlist_plugin, + &rss_playlist_plugin, +#endif +#ifdef ENABLE_DESPOTIFY + &despotify_playlist_plugin, +#endif +#ifdef ENABLE_SOUNDCLOUD + &soundcloud_playlist_plugin, +#endif + &cue_playlist_plugin, + &embcue_playlist_plugin, + nullptr +}; + +static constexpr unsigned n_playlist_plugins = + ARRAY_SIZE(playlist_plugins) - 1; + +/** which plugins have been initialized successfully? */ +static bool playlist_plugins_enabled[n_playlist_plugins]; + +#define playlist_plugins_for_each_enabled(plugin) \ + playlist_plugins_for_each(plugin) \ + if (playlist_plugins_enabled[playlist_plugin_iterator - playlist_plugins]) + +void +playlist_list_global_init(void) +{ + const config_param empty; + + for (unsigned i = 0; playlist_plugins[i] != nullptr; ++i) { + const struct playlist_plugin *plugin = playlist_plugins[i]; + const struct config_param *param = + config_find_block(CONF_PLAYLIST_PLUGIN, "name", + plugin->name); + if (param == nullptr) + param = ∅ + else if (!param->GetBlockValue("enabled", true)) + /* the plugin is disabled in mpd.conf */ + continue; + + playlist_plugins_enabled[i] = + playlist_plugin_init(playlist_plugins[i], *param); + } +} + +void +playlist_list_global_finish(void) +{ + playlist_plugins_for_each_enabled(plugin) + playlist_plugin_finish(plugin); +} + +static SongEnumerator * +playlist_list_open_uri_scheme(const char *uri, Mutex &mutex, Cond &cond, + bool *tried) +{ + SongEnumerator *playlist = nullptr; + + assert(uri != nullptr); + + const auto scheme = uri_get_scheme(uri); + if (scheme.empty()) + return nullptr; + + for (unsigned i = 0; playlist_plugins[i] != nullptr; ++i) { + const struct playlist_plugin *plugin = playlist_plugins[i]; + + assert(!tried[i]); + + if (playlist_plugins_enabled[i] && plugin->open_uri != nullptr && + plugin->schemes != nullptr && + string_array_contains(plugin->schemes, scheme.c_str())) { + playlist = playlist_plugin_open_uri(plugin, uri, + mutex, cond); + if (playlist != nullptr) + break; + + tried[i] = true; + } + } + + return playlist; +} + +static SongEnumerator * +playlist_list_open_uri_suffix(const char *uri, Mutex &mutex, Cond &cond, + const bool *tried) +{ + SongEnumerator *playlist = nullptr; + + assert(uri != nullptr); + + UriSuffixBuffer suffix_buffer; + const char *const suffix = uri_get_suffix(uri, suffix_buffer); + if (suffix == nullptr) + return nullptr; + + for (unsigned i = 0; playlist_plugins[i] != nullptr; ++i) { + const struct playlist_plugin *plugin = playlist_plugins[i]; + + if (playlist_plugins_enabled[i] && !tried[i] && + plugin->open_uri != nullptr && plugin->suffixes != nullptr && + string_array_contains(plugin->suffixes, suffix)) { + playlist = playlist_plugin_open_uri(plugin, uri, + mutex, cond); + if (playlist != nullptr) + break; + } + } + + return playlist; +} + +SongEnumerator * +playlist_list_open_uri(const char *uri, Mutex &mutex, Cond &cond) +{ + /** this array tracks which plugins have already been tried by + playlist_list_open_uri_scheme() */ + bool tried[n_playlist_plugins]; + + assert(uri != nullptr); + + memset(tried, false, sizeof(tried)); + + auto playlist = playlist_list_open_uri_scheme(uri, mutex, cond, tried); + if (playlist == nullptr) + playlist = playlist_list_open_uri_suffix(uri, mutex, cond, + tried); + + return playlist; +} + +static SongEnumerator * +playlist_list_open_stream_mime2(InputStream &is, const char *mime) +{ + assert(mime != nullptr); + + playlist_plugins_for_each_enabled(plugin) { + if (plugin->open_stream != nullptr && + plugin->mime_types != nullptr && + string_array_contains(plugin->mime_types, mime)) { + /* rewind the stream, so each plugin gets a + fresh start */ + is.Rewind(IgnoreError()); + + auto playlist = playlist_plugin_open_stream(plugin, + is); + if (playlist != nullptr) + return playlist; + } + } + + return nullptr; +} + +static SongEnumerator * +playlist_list_open_stream_mime(InputStream &is, const char *full_mime) +{ + assert(full_mime != nullptr); + + const char *semicolon = strchr(full_mime, ';'); + if (semicolon == nullptr) + return playlist_list_open_stream_mime2(is, full_mime); + + if (semicolon == full_mime) + return nullptr; + + /* probe only the portion before the semicolon*/ + const std::string mime(full_mime, semicolon); + return playlist_list_open_stream_mime2(is, mime.c_str()); +} + +SongEnumerator * +playlist_list_open_stream_suffix(InputStream &is, const char *suffix) +{ + assert(suffix != nullptr); + + playlist_plugins_for_each_enabled(plugin) { + if (plugin->open_stream != nullptr && + plugin->suffixes != nullptr && + string_array_contains(plugin->suffixes, suffix)) { + /* rewind the stream, so each plugin gets a + fresh start */ + is.Rewind(IgnoreError()); + + auto playlist = playlist_plugin_open_stream(plugin, is); + if (playlist != nullptr) + return playlist; + } + } + + return nullptr; +} + +SongEnumerator * +playlist_list_open_stream(InputStream &is, const char *uri) +{ + assert(is.IsReady()); + + const char *const mime = is.GetMimeType(); + if (mime != nullptr) { + auto playlist = playlist_list_open_stream_mime(is, mime); + if (playlist != nullptr) + return playlist; + } + + UriSuffixBuffer suffix_buffer; + const char *suffix = uri != nullptr + ? uri_get_suffix(uri, suffix_buffer) + : nullptr; + if (suffix != nullptr) { + auto playlist = playlist_list_open_stream_suffix(is, suffix); + if (playlist != nullptr) + return playlist; + } + + return nullptr; +} + +bool +playlist_suffix_supported(const char *suffix) +{ + assert(suffix != nullptr); + + playlist_plugins_for_each_enabled(plugin) { + if (plugin->suffixes != nullptr && + string_array_contains(plugin->suffixes, suffix)) + return true; + } + + return false; +} diff --git a/src/playlist/PlaylistRegistry.hxx b/src/playlist/PlaylistRegistry.hxx new file mode 100644 index 000000000..7ce559baa --- /dev/null +++ b/src/playlist/PlaylistRegistry.hxx @@ -0,0 +1,74 @@ +/* + * 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_REGISTRY_HXX +#define MPD_PLAYLIST_REGISTRY_HXX + +class Mutex; +class Cond; +class SongEnumerator; +class InputStream; + +extern const struct playlist_plugin *const playlist_plugins[]; + +#define playlist_plugins_for_each(plugin) \ + for (const struct playlist_plugin *plugin, \ + *const*playlist_plugin_iterator = &playlist_plugins[0]; \ + (plugin = *playlist_plugin_iterator) != nullptr; \ + ++playlist_plugin_iterator) + +/** + * Initializes all playlist plugins. + */ +void +playlist_list_global_init(void); + +/** + * Deinitializes all playlist plugins. + */ +void +playlist_list_global_finish(void); + +/** + * Opens a playlist by its URI. + */ +SongEnumerator * +playlist_list_open_uri(const char *uri, Mutex &mutex, Cond &cond); + +SongEnumerator * +playlist_list_open_stream_suffix(InputStream &is, const char *suffix); + +/** + * Opens a playlist from an input stream. + * + * @param is an #input_stream object which is open and ready + * @param uri optional URI which was used to open the stream; may be + * used to select the appropriate playlist plugin + */ +SongEnumerator * +playlist_list_open_stream(InputStream &is, const char *uri); + +/** + * Determines if there is a playlist plugin which can handle the + * specified file name suffix. + */ +bool +playlist_suffix_supported(const char *suffix); + +#endif diff --git a/src/playlist/PlaylistSong.cxx b/src/playlist/PlaylistSong.cxx new file mode 100644 index 000000000..3603c1add --- /dev/null +++ b/src/playlist/PlaylistSong.cxx @@ -0,0 +1,78 @@ +/* + * 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 "PlaylistSong.hxx" +#include "SongLoader.hxx" +#include "tag/Tag.hxx" +#include "tag/TagBuilder.hxx" +#include "fs/Traits.hxx" +#include "util/UriUtil.hxx" +#include "util/Error.hxx" +#include "DetachedSong.hxx" + +#include <assert.h> +#include <string.h> + +static void +merge_song_metadata(DetachedSong &add, const DetachedSong &base) +{ + if (base.GetTag().IsDefined()) { + TagBuilder builder(add.GetTag()); + builder.Complement(base.GetTag()); + add.SetTag(builder.Commit()); + } + + add.SetLastModified(base.GetLastModified()); +} + +static bool +playlist_check_load_song(DetachedSong &song, const SongLoader &loader) +{ + DetachedSong *tmp = loader.LoadSong(song.GetURI(), IgnoreError()); + if (tmp == nullptr) + return false; + + song.SetURI(tmp->GetURI()); + if (!song.HasRealURI() && tmp->HasRealURI()) + song.SetRealURI(tmp->GetRealURI()); + + merge_song_metadata(song, *tmp); + delete tmp; + return true; +} + +bool +playlist_check_translate_song(DetachedSong &song, const char *base_uri, + const SongLoader &loader) +{ + if (base_uri != nullptr && strcmp(base_uri, ".") == 0) + /* PathTraitsUTF8::GetParent() returns "." when there + is no directory name in the given path; clear that + now, because it would break the database lookup + functions */ + base_uri = nullptr; + + const char *uri = song.GetURI(); + if (base_uri != nullptr && !uri_has_scheme(uri) && + !PathTraitsUTF8::IsAbsolute(uri)) + song.SetURI(PathTraitsUTF8::Build(base_uri, uri)); + + return playlist_check_load_song(song, loader); +} diff --git a/src/playlist/PlaylistSong.hxx b/src/playlist/PlaylistSong.hxx new file mode 100644 index 000000000..278df46a8 --- /dev/null +++ b/src/playlist/PlaylistSong.hxx @@ -0,0 +1,36 @@ +/* + * 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_SONG_HXX +#define MPD_PLAYLIST_SONG_HXX + +class SongLoader; +class DetachedSong; + +/** + * Verifies the song, returns false if it is unsafe. Translate the + * song to a song within the database, if it is a local file. + * + * @return true on success, false if the song should not be used + */ +bool +playlist_check_translate_song(DetachedSong &song, const char *base_uri, + const SongLoader &loader); + +#endif diff --git a/src/playlist/PlaylistStream.cxx b/src/playlist/PlaylistStream.cxx new file mode 100644 index 000000000..074f39d66 --- /dev/null +++ b/src/playlist/PlaylistStream.cxx @@ -0,0 +1,98 @@ +/* + * 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 "PlaylistStream.hxx" +#include "PlaylistRegistry.hxx" +#include "CloseSongEnumerator.hxx" +#include "util/UriUtil.hxx" +#include "util/Error.hxx" +#include "input/InputStream.hxx" +#include "input/LocalOpen.hxx" +#include "fs/Path.hxx" +#include "Log.hxx" + +#include <assert.h> + +static SongEnumerator * +playlist_open_path_suffix(Path path, Mutex &mutex, Cond &cond) +{ + assert(!path.IsNull()); + + const char *suffix = uri_get_suffix(path.c_str()); + if (suffix == nullptr || !playlist_suffix_supported(suffix)) + return nullptr; + + Error error; + InputStream *is = OpenLocalInputStream(path, mutex, cond, error); + if (is == nullptr) { + LogError(error); + return nullptr; + } + + auto playlist = playlist_list_open_stream_suffix(*is, suffix); + if (playlist != nullptr) + playlist = new CloseSongEnumerator(playlist, is); + else + delete is; + + return playlist; +} + +SongEnumerator * +playlist_open_path(Path path, Mutex &mutex, Cond &cond) +{ + assert(!path.IsNull()); + + const std::string uri_utf8 = path.ToUTF8(); + auto playlist = !uri_utf8.empty() + ? playlist_list_open_uri(uri_utf8.c_str(), mutex, cond) + : nullptr; + if (playlist == nullptr) + playlist = playlist_open_path_suffix(path, mutex, cond); + + return playlist; +} + +SongEnumerator * +playlist_open_remote(const char *uri, Mutex &mutex, Cond &cond) +{ + assert(uri_has_scheme(uri)); + + SongEnumerator *playlist = playlist_list_open_uri(uri, mutex, cond); + if (playlist != nullptr) + return playlist; + + Error error; + InputStream *is = InputStream::OpenReady(uri, mutex, cond, error); + if (is == nullptr) { + if (error.IsDefined()) + FormatError(error, "Failed to open %s", uri); + + return nullptr; + } + + playlist = playlist_list_open_stream(*is, uri); + if (playlist == nullptr) { + delete is; + return nullptr; + } + + return new CloseSongEnumerator(playlist, is); +} diff --git a/src/playlist/PlaylistStream.hxx b/src/playlist/PlaylistStream.hxx new file mode 100644 index 000000000..c07ae0b09 --- /dev/null +++ b/src/playlist/PlaylistStream.hxx @@ -0,0 +1,46 @@ +/* + * 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_STREAM_HXX +#define MPD_PLAYLIST_STREAM_HXX + +#include "Compiler.h" + +class Mutex; +class Cond; +class SongEnumerator; +class Path; + +/** + * Opens a playlist from a local file. + * + * @param path the path of the playlist file + * @param is_r on success, an input_stream object is returned here, + * which must be closed after the playlist_provider object is freed + * @return a playlist, or nullptr on error + */ +gcc_nonnull_all +SongEnumerator * +playlist_open_path(Path path, Mutex &mutex, Cond &cond); + +gcc_nonnull_all +SongEnumerator * +playlist_open_remote(const char *uri, Mutex &mutex, Cond &cond); + +#endif diff --git a/src/playlist/Print.cxx b/src/playlist/Print.cxx new file mode 100644 index 000000000..0db2a4ab0 --- /dev/null +++ b/src/playlist/Print.cxx @@ -0,0 +1,74 @@ +/* + * 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 "Print.hxx" +#include "PlaylistAny.hxx" +#include "PlaylistSong.hxx" +#include "SongEnumerator.hxx" +#include "SongPrint.hxx" +#include "DetachedSong.hxx" +#include "SongLoader.hxx" +#include "fs/Traits.hxx" +#include "thread/Mutex.hxx" +#include "thread/Cond.hxx" +#include "client/Client.hxx" + +static void +playlist_provider_print(Client &client, const char *uri, + SongEnumerator &e, bool detail) +{ + const std::string base_uri = uri != nullptr + ? PathTraitsUTF8::GetParent(uri) + : std::string("."); + + const SongLoader loader(client); + + DetachedSong *song; + while ((song = e.NextSong()) != nullptr) { + if (playlist_check_translate_song(*song, base_uri.c_str(), + loader)) { + if (detail) + song_print_info(client, *song); + else + song_print_uri(client, *song); + } + + delete song; + } +} + +bool +playlist_file_print(Client &client, const char *uri, bool detail) +{ + Mutex mutex; + Cond cond; + + SongEnumerator *playlist = playlist_open_any(uri, +#ifdef ENABLE_DATABASE + client.GetStorage(), +#endif + mutex, cond); + if (playlist == nullptr) + return false; + + playlist_provider_print(client, uri, *playlist, detail); + delete playlist; + return true; +} diff --git a/src/playlist/Print.hxx b/src/playlist/Print.hxx new file mode 100644 index 000000000..c2fff5475 --- /dev/null +++ b/src/playlist/Print.hxx @@ -0,0 +1,36 @@ +/* + * 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__PRINT_HXX +#define MPD_PLAYLIST__PRINT_HXX + +class Client; + +/** + * Send the playlist file to the client. + * + * @param client the client which requested the playlist + * @param uri the URI of the playlist file in UTF-8 encoding + * @param detail true if all details should be printed + * @return true on success, false if the playlist does not exist + */ +bool +playlist_file_print(Client &client, const char *uri, bool detail); + +#endif diff --git a/src/playlist/RssPlaylistPlugin.cxx b/src/playlist/RssPlaylistPlugin.cxx deleted file mode 100644 index e2a44bfd3..000000000 --- a/src/playlist/RssPlaylistPlugin.cxx +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright (C) 2003-2013 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 "RssPlaylistPlugin.hxx" -#include "PlaylistPlugin.hxx" -#include "MemorySongEnumerator.hxx" -#include "InputStream.hxx" -#include "Song.hxx" -#include "tag/Tag.hxx" -#include "util/ASCII.hxx" -#include "util/Error.hxx" -#include "util/Domain.hxx" -#include "Log.hxx" - -#include <glib.h> - -#include <assert.h> -#include <string.h> - -static constexpr Domain rss_domain("rss"); - -/** - * This is the state object for the GLib XML parser. - */ -struct RssParser { - /** - * The list of songs (in reverse order because that's faster - * while adding). - */ - std::forward_list<SongPointer> songs; - - /** - * The current position in the XML file. - */ - enum { - ROOT, ITEM, - } state; - - /** - * The current tag within the "entry" element. This is only - * valid if state==ITEM. TAG_NUM_OF_ITEM_TYPES means there - * is no (known) tag. - */ - TagType tag; - - /** - * The current song. It is allocated after the "location" - * element. - */ - Song *song; - - RssParser() - :state(ROOT) {} -}; - -static const gchar * -get_attribute(const gchar **attribute_names, const gchar **attribute_values, - const gchar *name) -{ - for (unsigned i = 0; attribute_names[i] != nullptr; ++i) - if (StringEqualsCaseASCII(attribute_names[i], name)) - return attribute_values[i]; - - return nullptr; -} - -static void -rss_start_element(gcc_unused GMarkupParseContext *context, - const gchar *element_name, - const gchar **attribute_names, - const gchar **attribute_values, - gpointer user_data, gcc_unused GError **error) -{ - RssParser *parser = (RssParser *)user_data; - - switch (parser->state) { - case RssParser::ROOT: - if (StringEqualsCaseASCII(element_name, "item")) { - parser->state = RssParser::ITEM; - parser->song = Song::NewRemote("rss:"); - parser->tag = TAG_NUM_OF_ITEM_TYPES; - } - - break; - - case RssParser::ITEM: - if (StringEqualsCaseASCII(element_name, "enclosure")) { - const gchar *href = get_attribute(attribute_names, - attribute_values, - "url"); - if (href != nullptr) { - /* create new song object, and copy - the existing tag over; we cannot - replace the existing song's URI, - because that attribute is - immutable */ - Song *song = Song::NewRemote(href); - - if (parser->song != nullptr) { - song->tag = parser->song->tag; - parser->song->tag = nullptr; - parser->song->Free(); - } - - parser->song = song; - } - } else if (StringEqualsCaseASCII(element_name, "title")) - parser->tag = TAG_TITLE; - else if (StringEqualsCaseASCII(element_name, "itunes:author")) - parser->tag = TAG_ARTIST; - - break; - } -} - -static void -rss_end_element(gcc_unused GMarkupParseContext *context, - const gchar *element_name, - gpointer user_data, gcc_unused GError **error) -{ - RssParser *parser = (RssParser *)user_data; - - switch (parser->state) { - case RssParser::ROOT: - break; - - case RssParser::ITEM: - if (StringEqualsCaseASCII(element_name, "item")) { - if (strcmp(parser->song->uri, "rss:") != 0) - parser->songs.emplace_front(parser->song); - else - parser->song->Free(); - - parser->state = RssParser::ROOT; - } else - parser->tag = TAG_NUM_OF_ITEM_TYPES; - - break; - } -} - -static void -rss_text(gcc_unused GMarkupParseContext *context, - const gchar *text, gsize text_len, - gpointer user_data, gcc_unused GError **error) -{ - RssParser *parser = (RssParser *)user_data; - - switch (parser->state) { - case RssParser::ROOT: - break; - - case RssParser::ITEM: - if (parser->tag != TAG_NUM_OF_ITEM_TYPES) { - if (parser->song->tag == nullptr) - parser->song->tag = new Tag(); - parser->song->tag->AddItem(parser->tag, - text, text_len); - } - - break; - } -} - -static const GMarkupParser rss_parser = { - rss_start_element, - rss_end_element, - rss_text, - nullptr, - nullptr, -}; - -static void -rss_parser_destroy(gpointer data) -{ - RssParser *parser = (RssParser *)data; - - if (parser->state >= RssParser::ITEM) - parser->song->Free(); -} - -/* - * The playlist object - * - */ - -static SongEnumerator * -rss_open_stream(InputStream &is) -{ - RssParser parser; - GMarkupParseContext *context; - char buffer[1024]; - size_t nbytes; - bool success; - Error error2; - GError *error = nullptr; - - /* parse the RSS XML file */ - - context = g_markup_parse_context_new(&rss_parser, - G_MARKUP_TREAT_CDATA_AS_TEXT, - &parser, rss_parser_destroy); - - while (true) { - nbytes = is.LockRead(buffer, sizeof(buffer), error2); - if (nbytes == 0) { - if (error2.IsDefined()) { - g_markup_parse_context_free(context); - LogError(error2); - return nullptr; - } - - break; - } - - success = g_markup_parse_context_parse(context, buffer, nbytes, - &error); - if (!success) { - FormatError(rss_domain, - "XML parser failed: %s", error->message); - g_error_free(error); - g_markup_parse_context_free(context); - return nullptr; - } - } - - success = g_markup_parse_context_end_parse(context, &error); - if (!success) { - FormatError(rss_domain, - "XML parser failed: %s", error->message); - g_error_free(error); - g_markup_parse_context_free(context); - return nullptr; - } - - parser.songs.reverse(); - MemorySongEnumerator *playlist = - new MemorySongEnumerator(std::move(parser.songs)); - - g_markup_parse_context_free(context); - - return playlist; -} - -static const char *const rss_suffixes[] = { - "rss", - nullptr -}; - -static const char *const rss_mime_types[] = { - "application/rss+xml", - "text/xml", - nullptr -}; - -const struct playlist_plugin rss_playlist_plugin = { - "rss", - - nullptr, - nullptr, - nullptr, - rss_open_stream, - - nullptr, - rss_suffixes, - rss_mime_types, -}; diff --git a/src/playlist/SongEnumerator.hxx b/src/playlist/SongEnumerator.hxx new file mode 100644 index 000000000..75295add1 --- /dev/null +++ b/src/playlist/SongEnumerator.hxx @@ -0,0 +1,41 @@ +/* + * 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_SONG_ENUMERATOR_HXX +#define MPD_SONG_ENUMERATOR_HXX + +class DetachedSong; + +/** + * An object which provides serial access to a number of #Song + * objects. It is used to enumerate the contents of a playlist file. + */ +class SongEnumerator { +public: + virtual ~SongEnumerator() {} + + /** + * Obtain the next song. The caller is responsible for + * freeing the returned #Song object. Returns nullptr if + * there are no more songs. + */ + virtual DetachedSong *NextSong() = 0; +}; + +#endif diff --git a/src/playlist/cue/CueParser.cxx b/src/playlist/cue/CueParser.cxx new file mode 100644 index 000000000..372c90b78 --- /dev/null +++ b/src/playlist/cue/CueParser.cxx @@ -0,0 +1,316 @@ +/* + * 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 "CueParser.hxx" +#include "util/Alloc.hxx" +#include "util/StringUtil.hxx" +#include "util/CharUtil.hxx" +#include "DetachedSong.hxx" +#include "tag/Tag.hxx" + +#include <assert.h> +#include <string.h> +#include <stdlib.h> + +CueParser::CueParser() + :state(HEADER), + current(nullptr), + previous(nullptr), + finished(nullptr), + end(false) {} + +CueParser::~CueParser() +{ + delete current; + delete previous; + delete finished; +} + +static const char * +cue_next_word(char *p, char **pp) +{ + assert(p >= *pp); + assert(!IsWhitespaceNotNull(*p)); + + const char *word = p; + while (!IsWhitespaceOrNull(*p)) + ++p; + + *p = 0; + *pp = p + 1; + return word; +} + +static const char * +cue_next_quoted(char *p, char **pp) +{ + assert(p >= *pp); + assert(p[-1] == '"'); + + char *end = strchr(p, '"'); + if (end == nullptr) { + /* syntax error - ignore it silently */ + *pp = p + strlen(p); + return p; + } + + *end = 0; + *pp = end + 1; + + return p; +} + +static const char * +cue_next_token(char **pp) +{ + char *p = StripLeft(*pp); + if (*p == 0) + return nullptr; + + return cue_next_word(p, pp); +} + +static const char * +cue_next_value(char **pp) +{ + char *p = StripLeft(*pp); + if (*p == 0) + return nullptr; + + if (*p == '"') + return cue_next_quoted(p + 1, pp); + else + return cue_next_word(p, pp); +} + +static void +cue_add_tag(TagBuilder &tag, TagType type, char *p) +{ + const char *value = cue_next_value(&p); + if (value != nullptr) + tag.AddItem(type, value); + +} + +static void +cue_parse_rem(char *p, TagBuilder &tag) +{ + const char *type = cue_next_token(&p); + if (type == nullptr) + return; + + TagType type2 = tag_name_parse_i(type); + if (type2 != TAG_NUM_OF_ITEM_TYPES) + cue_add_tag(tag, type2, p); +} + +TagBuilder * +CueParser::GetCurrentTag() +{ + if (state == HEADER) + return &header_tag; + else if (state == TRACK) + return &song_tag; + else + return nullptr; +} + +static int +cue_parse_position(const char *p) +{ + char *endptr; + unsigned long minutes = strtoul(p, &endptr, 10); + if (endptr == p || *endptr != ':') + return -1; + + p = endptr + 1; + unsigned long seconds = strtoul(p, &endptr, 10); + if (endptr == p || *endptr != ':') + return -1; + + p = endptr + 1; + unsigned long frames = strtoul(p, &endptr, 10); + if (endptr == p || *endptr != 0) + return -1; + + return minutes * 60000 + seconds * 1000 + frames * 1000 / 75; +} + +void +CueParser::Commit() +{ + /* the caller of this library must call cue_parser_get() often + enough */ + assert(finished == nullptr); + assert(!end); + + if (current == nullptr) + return; + + assert(!current->GetTag().IsDefined()); + current->SetTag(song_tag.Commit()); + + finished = previous; + previous = current; + current = nullptr; +} + +void +CueParser::Feed2(char *p) +{ + assert(!end); + assert(p != nullptr); + + const char *command = cue_next_token(&p); + if (command == nullptr) + return; + + if (strcmp(command, "REM") == 0) { + TagBuilder *tag = GetCurrentTag(); + if (tag != nullptr) + cue_parse_rem(p, *tag); + } else if (strcmp(command, "PERFORMER") == 0) { + /* MPD knows a "performer" tag, but it is not a good + match for this CUE tag; from the Hydrogenaudio + Knowledgebase: "At top-level this will specify the + CD artist, while at track-level it specifies the + track artist." */ + + TagType type = state == TRACK + ? TAG_ARTIST + : TAG_ALBUM_ARTIST; + + TagBuilder *tag = GetCurrentTag(); + if (tag != nullptr) + cue_add_tag(*tag, type, p); + } else if (strcmp(command, "TITLE") == 0) { + if (state == HEADER) + cue_add_tag(header_tag, TAG_ALBUM, p); + else if (state == TRACK) + cue_add_tag(song_tag, TAG_TITLE, p); + } else if (strcmp(command, "FILE") == 0) { + Commit(); + + const char *new_filename = cue_next_value(&p); + if (new_filename == nullptr) + return; + + const char *type = cue_next_token(&p); + if (type == nullptr) + return; + + if (strcmp(type, "WAVE") != 0 && + strcmp(type, "MP3") != 0 && + strcmp(type, "AIFF") != 0) { + state = IGNORE_FILE; + return; + } + + state = WAVE; + filename = new_filename; + } else if (state == IGNORE_FILE) { + return; + } else if (strcmp(command, "TRACK") == 0) { + Commit(); + + const char *nr = cue_next_token(&p); + if (nr == nullptr) + return; + + const char *type = cue_next_token(&p); + if (type == nullptr) + return; + + if (strcmp(type, "AUDIO") != 0) { + state = IGNORE_TRACK; + return; + } + + state = TRACK; + current = new DetachedSong(filename); + assert(!current->GetTag().IsDefined()); + + song_tag = header_tag; + song_tag.AddItem(TAG_TRACK, nr); + + last_updated = false; + } else if (state == IGNORE_TRACK) { + return; + } else if (state == TRACK && strcmp(command, "INDEX") == 0) { + const char *nr = cue_next_token(&p); + if (nr == nullptr) + return; + + const char *position = cue_next_token(&p); + if (position == nullptr) + return; + + int position_ms = cue_parse_position(position); + if (position_ms < 0) + return; + + if (!last_updated && previous != nullptr && + previous->GetStartTime().ToMS() < (unsigned)position_ms) { + last_updated = true; + previous->SetEndTime(SongTime::FromMS(position_ms)); + } + + current->SetStartTime(SongTime::FromMS(position_ms)); + } +} + +void +CueParser::Feed(const char *line) +{ + assert(!end); + assert(line != nullptr); + + char *allocated = xstrdup(line); + Feed2(allocated); + free(allocated); +} + +void +CueParser::Finish() +{ + if (end) + /* has already been called, ignore */ + return; + + Commit(); + end = true; +} + +DetachedSong * +CueParser::Get() +{ + if (finished == nullptr && end) { + /* cue_parser_finish() has been called already: + deliver all remaining (partial) results */ + assert(current == nullptr); + + finished = previous; + previous = nullptr; + } + + DetachedSong *song = finished; + finished = nullptr; + return song; +} diff --git a/src/playlist/cue/CueParser.hxx b/src/playlist/cue/CueParser.hxx new file mode 100644 index 000000000..7e040169b --- /dev/null +++ b/src/playlist/cue/CueParser.hxx @@ -0,0 +1,144 @@ +/* + * 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_CUE_PARSER_HXX +#define MPD_CUE_PARSER_HXX + +#include "check.h" +#include "tag/TagBuilder.hxx" +#include "Compiler.h" + +#include <string> + +class DetachedSong; +struct Tag; + +class CueParser { + enum { + /** + * Parsing the CUE header. + */ + HEADER, + + /** + * Parsing a "FILE ... WAVE". + */ + WAVE, + + /** + * Ignore everything until the next "FILE". + */ + IGNORE_FILE, + + /** + * Parsing a "TRACK ... AUDIO". + */ + TRACK, + + /** + * Ignore everything until the next "TRACK". + */ + IGNORE_TRACK, + } state; + + /** + * Tags read from the CUE header. + */ + TagBuilder header_tag; + + /** + * Tags read for the current song (attribute #current). When + * #current gets moved to #previous, TagBuilder::Commit() will + * be called. + */ + TagBuilder song_tag; + + std::string filename; + + /** + * The song currently being edited. + */ + DetachedSong *current; + + /** + * The previous song. It is remembered because its end_time + * will be set to the current song's start time. + */ + DetachedSong *previous; + + /** + * A song that is completely finished and can be returned to + * the caller via cue_parser_get(). + */ + DetachedSong *finished; + + /** + * Set to true after previous.end_time has been updated to the + * start time of the current song. + */ + bool last_updated; + + /** + * Tracks whether cue_parser_finish() has been called. If + * true, then all remaining (partial) results will be + * delivered by cue_parser_get(). + */ + bool end; + +public: + CueParser(); + ~CueParser(); + + /** + * Feed a text line from the CUE file into the parser. Call + * cue_parser_get() after this to see if a song has been finished. + */ + void Feed(const char *line); + + /** + * Tell the parser that the end of the file has been reached. Call + * cue_parser_get() after this to see if a song has been finished. + * This procedure must be done twice! + */ + void Finish(); + + /** + * Check if a song was finished by the last cue_parser_feed() or + * cue_parser_finish() call. + * + * @return a song object that must be freed by the caller, or NULL if + * no song was finished at this time + */ + DetachedSong *Get(); + +private: + gcc_pure + TagBuilder *GetCurrentTag(); + + /** + * Commit the current song. It will be moved to "previous", + * so the next song may soon edit its end time (using the next + * song's start time). + */ + void Commit(); + + void Feed2(char *p); +}; + +#endif diff --git a/src/playlist/plugins/AsxPlaylistPlugin.cxx b/src/playlist/plugins/AsxPlaylistPlugin.cxx new file mode 100644 index 000000000..3185a8144 --- /dev/null +++ b/src/playlist/plugins/AsxPlaylistPlugin.cxx @@ -0,0 +1,186 @@ +/* + * 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 "AsxPlaylistPlugin.hxx" +#include "../PlaylistPlugin.hxx" +#include "../MemorySongEnumerator.hxx" +#include "tag/TagBuilder.hxx" +#include "util/ASCII.hxx" +#include "util/Error.hxx" +#include "lib/expat/ExpatParser.hxx" +#include "Log.hxx" + +/** + * This is the state object for the GLib XML parser. + */ +struct AsxParser { + /** + * The list of songs (in reverse order because that's faster + * while adding). + */ + std::forward_list<DetachedSong> songs; + + /** + * The current position in the XML file. + */ + enum { + ROOT, ENTRY, + } state; + + /** + * The current tag within the "entry" element. This is only + * valid if state==ENTRY. TAG_NUM_OF_ITEM_TYPES means there + * is no (known) tag. + */ + TagType tag_type; + + /** + * The current song URI. It is set by the "ref" element. + */ + std::string location; + + TagBuilder tag_builder; + + AsxParser() + :state(ROOT) {} + +}; + +static void XMLCALL +asx_start_element(void *user_data, const XML_Char *element_name, + const XML_Char **atts) +{ + AsxParser *parser = (AsxParser *)user_data; + + switch (parser->state) { + case AsxParser::ROOT: + if (StringEqualsCaseASCII(element_name, "entry")) { + parser->state = AsxParser::ENTRY; + parser->location.clear(); + parser->tag_type = TAG_NUM_OF_ITEM_TYPES; + } + + break; + + case AsxParser::ENTRY: + if (StringEqualsCaseASCII(element_name, "ref")) { + const char *href = + ExpatParser::GetAttributeCase(atts, "href"); + if (href != nullptr) + parser->location = href; + } else if (StringEqualsCaseASCII(element_name, "author")) + /* is that correct? or should it be COMPOSER + or PERFORMER? */ + parser->tag_type = TAG_ARTIST; + else if (StringEqualsCaseASCII(element_name, "title")) + parser->tag_type = TAG_TITLE; + + break; + } +} + +static void XMLCALL +asx_end_element(void *user_data, const XML_Char *element_name) +{ + AsxParser *parser = (AsxParser *)user_data; + + switch (parser->state) { + case AsxParser::ROOT: + break; + + case AsxParser::ENTRY: + if (StringEqualsCaseASCII(element_name, "entry")) { + if (!parser->location.empty()) + parser->songs.emplace_front(std::move(parser->location), + parser->tag_builder.Commit()); + + parser->state = AsxParser::ROOT; + } else + parser->tag_type = TAG_NUM_OF_ITEM_TYPES; + + break; + } +} + +static void XMLCALL +asx_char_data(void *user_data, const XML_Char *s, int len) +{ + AsxParser *parser = (AsxParser *)user_data; + + switch (parser->state) { + case AsxParser::ROOT: + break; + + case AsxParser::ENTRY: + if (parser->tag_type != TAG_NUM_OF_ITEM_TYPES) + parser->tag_builder.AddItem(parser->tag_type, s, len); + + break; + } +} + +/* + * The playlist object + * + */ + +static SongEnumerator * +asx_open_stream(InputStream &is) +{ + AsxParser parser; + + { + ExpatParser expat(&parser); + expat.SetElementHandler(asx_start_element, asx_end_element); + expat.SetCharacterDataHandler(asx_char_data); + + Error error; + if (!expat.Parse(is, error)) { + LogError(error); + return nullptr; + } + } + + parser.songs.reverse(); + return new MemorySongEnumerator(std::move(parser.songs)); +} + +static const char *const asx_suffixes[] = { + "asx", + nullptr +}; + +static const char *const asx_mime_types[] = { + "video/x-ms-asf", + nullptr +}; + +const struct playlist_plugin asx_playlist_plugin = { + "asx", + + nullptr, + nullptr, + nullptr, + asx_open_stream, + + nullptr, + asx_suffixes, + asx_mime_types, +}; diff --git a/src/playlist/AsxPlaylistPlugin.hxx b/src/playlist/plugins/AsxPlaylistPlugin.hxx index 240c1824a..63371be0f 100644 --- a/src/playlist/AsxPlaylistPlugin.hxx +++ b/src/playlist/plugins/AsxPlaylistPlugin.hxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2013 The Music Player Daemon Project + * 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 diff --git a/src/playlist/CuePlaylistPlugin.cxx b/src/playlist/plugins/CuePlaylistPlugin.cxx index 42a43bbad..b907d34d0 100644 --- a/src/playlist/CuePlaylistPlugin.cxx +++ b/src/playlist/plugins/CuePlaylistPlugin.cxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2013 The Music Player Daemon Project + * 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 @@ -19,15 +19,12 @@ #include "config.h" #include "CuePlaylistPlugin.hxx" -#include "PlaylistPlugin.hxx" -#include "SongEnumerator.hxx" -#include "tag/Tag.hxx" -#include "Song.hxx" -#include "cue/CueParser.hxx" -#include "TextInputStream.hxx" +#include "../PlaylistPlugin.hxx" +#include "../SongEnumerator.hxx" +#include "../cue/CueParser.hxx" +#include "input/TextInputStream.hxx" -#include <assert.h> -#include <string.h> +#include <string> class CuePlaylist final : public SongEnumerator { InputStream &is; @@ -39,7 +36,7 @@ class CuePlaylist final : public SongEnumerator { :is(_is), tis(is) { } - virtual Song *NextSong() override; + virtual DetachedSong *NextSong() override; }; static SongEnumerator * @@ -48,16 +45,16 @@ cue_playlist_open_stream(InputStream &is) return new CuePlaylist(is); } -Song * +DetachedSong * CuePlaylist::NextSong() { - Song *song = parser.Get(); + DetachedSong *song = parser.Get(); if (song != nullptr) return song; - std::string line; - while (tis.ReadLine(line)) { - parser.Feed(line.c_str()); + const char *line; + while ((line = tis.ReadLine()) != nullptr) { + parser.Feed(line); song = parser.Get(); if (song != nullptr) return song; diff --git a/src/playlist/CuePlaylistPlugin.hxx b/src/playlist/plugins/CuePlaylistPlugin.hxx index cf5e3a8f0..4d833bfc2 100644 --- a/src/playlist/CuePlaylistPlugin.hxx +++ b/src/playlist/plugins/CuePlaylistPlugin.hxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2013 The Music Player Daemon Project + * 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 diff --git a/src/playlist/DespotifyPlaylistPlugin.cxx b/src/playlist/plugins/DespotifyPlaylistPlugin.cxx index a1a865c08..636f64bc6 100644 --- a/src/playlist/DespotifyPlaylistPlugin.cxx +++ b/src/playlist/plugins/DespotifyPlaylistPlugin.cxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2013 The Music Player Daemon Project + * 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 @@ -19,11 +19,11 @@ #include "config.h" #include "DespotifyPlaylistPlugin.hxx" -#include "DespotifyUtils.hxx" -#include "PlaylistPlugin.hxx" -#include "MemorySongEnumerator.hxx" +#include "lib/despotify/DespotifyUtils.hxx" +#include "../PlaylistPlugin.hxx" +#include "../MemorySongEnumerator.hxx" #include "tag/Tag.hxx" -#include "Song.hxx" +#include "DetachedSong.hxx" #include "Log.hxx" extern "C" { @@ -32,12 +32,12 @@ extern "C" { #include <string.h> #include <stdlib.h> +#include <string.h> static void -add_song(std::forward_list<SongPointer> &songs, struct ds_track *track) +add_song(std::forward_list<DetachedSong> &songs, ds_track &track) { const char *dsp_scheme = despotify_playlist_plugin.schemes[0]; - Song *song; char uri[128]; char *ds_uri; @@ -45,35 +45,32 @@ add_song(std::forward_list<SongPointer> &songs, struct ds_track *track) snprintf(uri, sizeof(uri), "%s://", dsp_scheme); ds_uri = uri + strlen(dsp_scheme) + 3; - if (despotify_track_to_uri(track, ds_uri) != ds_uri) { + if (despotify_track_to_uri(&track, ds_uri) != ds_uri) { /* Should never really fail, but let's be sure */ FormatDebug(despotify_domain, - "Can't add track %s", track->title); + "Can't add track %s", track.title); return; } - song = Song::NewRemote(uri); - song->tag = mpd_despotify_tag_from_track(track); - - songs.emplace_front(song); + songs.emplace_front(uri, mpd_despotify_tag_from_track(track)); } static bool parse_track(struct despotify_session *session, - std::forward_list<SongPointer> &songs, + std::forward_list<DetachedSong> &songs, struct ds_link *link) { struct ds_track *track = despotify_link_get_track(session, link); if (track == nullptr) return false; - add_song(songs, track); + add_song(songs, *track); return true; } static bool parse_playlist(struct despotify_session *session, - std::forward_list<SongPointer> &songs, + std::forward_list<DetachedSong> &songs, struct ds_link *link) { ds_playlist *playlist = despotify_link_get_playlist(session, link); @@ -82,7 +79,7 @@ parse_playlist(struct despotify_session *session, for (ds_track *track = playlist->tracks; track != nullptr; track = track->next) - add_song(songs, track); + add_song(songs, *track); return true; } @@ -103,7 +100,7 @@ despotify_playlist_open_uri(const char *url, return nullptr; } - std::forward_list<SongPointer> songs; + std::forward_list<DetachedSong> songs; bool parse_result; switch (link->type) { diff --git a/src/playlist/DespotifyPlaylistPlugin.hxx b/src/playlist/plugins/DespotifyPlaylistPlugin.hxx index c1e5b7f39..6acfd40f4 100644 --- a/src/playlist/DespotifyPlaylistPlugin.hxx +++ b/src/playlist/plugins/DespotifyPlaylistPlugin.hxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2013 The Music Player Daemon Project + * 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 diff --git a/src/playlist/EmbeddedCuePlaylistPlugin.cxx b/src/playlist/plugins/EmbeddedCuePlaylistPlugin.cxx index 2734fa59e..9e8f91e05 100644 --- a/src/playlist/EmbeddedCuePlaylistPlugin.cxx +++ b/src/playlist/plugins/EmbeddedCuePlaylistPlugin.cxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2013 The Music Player Daemon Project + * 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 @@ -25,20 +25,18 @@ #include "config.h" #include "EmbeddedCuePlaylistPlugin.hxx" -#include "PlaylistPlugin.hxx" -#include "SongEnumerator.hxx" -#include "tag/Tag.hxx" +#include "../PlaylistPlugin.hxx" +#include "../SongEnumerator.hxx" +#include "../cue/CueParser.hxx" #include "tag/TagHandler.hxx" #include "tag/TagId3.hxx" #include "tag/ApeTag.hxx" -#include "Song.hxx" +#include "DetachedSong.hxx" #include "TagFile.hxx" -#include "cue/CueParser.hxx" #include "fs/Traits.hxx" #include "fs/AllocatedPath.hxx" #include "util/ASCII.hxx" -#include <assert.h> #include <string.h> class EmbeddedCuePlaylist final : public SongEnumerator { @@ -71,7 +69,7 @@ public: delete parser; } - virtual Song *NextSong() override; + virtual DetachedSong *NextSong() override; }; static void @@ -95,7 +93,7 @@ embcue_playlist_open_uri(const char *uri, gcc_unused Mutex &mutex, gcc_unused Cond &cond) { - if (!PathTraits::IsAbsoluteUTF8(uri)) + if (!PathTraitsUTF8::IsAbsolute(uri)) /* only local files supported */ return nullptr; @@ -105,7 +103,7 @@ embcue_playlist_open_uri(const char *uri, const auto playlist = new EmbeddedCuePlaylist(); - tag_file_scan(path_fs, &embcue_tag_handler, playlist); + tag_file_scan(path_fs, embcue_tag_handler, playlist); if (playlist->cuesheet.empty()) { tag_ape_scan2(path_fs, &embcue_tag_handler, playlist); if (playlist->cuesheet.empty()) @@ -118,7 +116,7 @@ embcue_playlist_open_uri(const char *uri, return nullptr; } - playlist->filename = PathTraits::GetBaseUTF8(uri); + playlist->filename = PathTraitsUTF8::GetBase(uri); playlist->next = &playlist->cuesheet[0]; playlist->parser = new CueParser(); @@ -126,10 +124,10 @@ embcue_playlist_open_uri(const char *uri, return playlist; } -Song * +DetachedSong * EmbeddedCuePlaylist::NextSong() { - Song *song = parser->Get(); + DetachedSong *song = parser->Get(); if (song != nullptr) return song; @@ -147,14 +145,16 @@ EmbeddedCuePlaylist::NextSong() parser->Feed(line); song = parser->Get(); - if (song != nullptr) - return song->ReplaceURI(filename.c_str()); + if (song != nullptr) { + song->SetURI(filename); + return song; + } } parser->Finish(); song = parser->Get(); if (song != nullptr) - song = song->ReplaceURI(filename.c_str()); + song->SetURI(filename); return song; } diff --git a/src/playlist/EmbeddedCuePlaylistPlugin.hxx b/src/playlist/plugins/EmbeddedCuePlaylistPlugin.hxx index e306730f4..5eedf3f13 100644 --- a/src/playlist/EmbeddedCuePlaylistPlugin.hxx +++ b/src/playlist/plugins/EmbeddedCuePlaylistPlugin.hxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2013 The Music Player Daemon Project + * 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 diff --git a/src/playlist/ExtM3uPlaylistPlugin.cxx b/src/playlist/plugins/ExtM3uPlaylistPlugin.cxx index 5ef010bda..93316ca6c 100644 --- a/src/playlist/ExtM3uPlaylistPlugin.cxx +++ b/src/playlist/plugins/ExtM3uPlaylistPlugin.cxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2013 The Music Player Daemon Project + * 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 @@ -19,14 +19,13 @@ #include "config.h" #include "ExtM3uPlaylistPlugin.hxx" -#include "PlaylistPlugin.hxx" -#include "SongEnumerator.hxx" -#include "Song.hxx" +#include "../PlaylistPlugin.hxx" +#include "../SongEnumerator.hxx" +#include "DetachedSong.hxx" #include "tag/Tag.hxx" +#include "tag/TagBuilder.hxx" #include "util/StringUtil.hxx" -#include "TextInputStream.hxx" - -#include <glib.h> +#include "input/TextInputStream.hxx" #include <string.h> #include <stdlib.h> @@ -40,12 +39,15 @@ public: } bool CheckFirstLine() { - std::string line; - return tis.ReadLine(line) && - strcmp(line.c_str(), "#EXTM3U") == 0; + char *line = tis.ReadLine(); + if (line == nullptr) + return false; + + StripRight(line); + return strcmp(line, "#EXTM3U") == 0; } - virtual Song *NextSong() override; + virtual DetachedSong *NextSong() override; }; static SongEnumerator * @@ -57,7 +59,7 @@ extm3u_open_stream(InputStream &is) /* no EXTM3U header: fall back to the plain m3u plugin */ delete playlist; - return NULL; + return nullptr; } return playlist; @@ -68,69 +70,62 @@ extm3u_open_stream(InputStream &is) * * @param line the rest of the input line after the colon */ -static Tag * +static Tag extm3u_parse_tag(const char *line) { long duration; char *endptr; const char *name; - Tag *tag; duration = strtol(line, &endptr, 10); if (endptr[0] != ',') /* malformed line */ - return NULL; + return Tag(); if (duration < 0) /* 0 means unknown duration */ duration = 0; - name = strchug_fast(endptr + 1); + name = StripLeft(endptr + 1); if (*name == 0 && duration == 0) /* no information available; don't allocate a tag object */ - return NULL; + return Tag(); - tag = new Tag(); - tag->time = duration; + TagBuilder tag; + tag.SetDuration(SignedSongTime::FromS(unsigned(duration))); /* unfortunately, there is no real specification for the EXTM3U format, so we must assume that the string after the comma is opaque, and is just the song name*/ if (*name != 0) - tag->AddItem(TAG_NAME, name); + tag.AddItem(TAG_NAME, name); - return tag; + return tag.Commit(); } -Song * +DetachedSong * ExtM3uPlaylist::NextSong() { - Tag *tag = NULL; - std::string line; - const char *line_s; - Song *song; + Tag tag; + char *line_s; do { - if (!tis.ReadLine(line)) { - delete tag; - return NULL; - } - - line_s = line.c_str(); + line_s = tis.ReadLine(); + if (line_s == nullptr) + return nullptr; - if (g_str_has_prefix(line_s, "#EXTINF:")) { - delete tag; + StripRight(line_s); + + if (StringStartsWith(line_s, "#EXTINF:")) { tag = extm3u_parse_tag(line_s + 8); continue; } - line_s = strchug_fast(line_s); + line_s = StripLeft(line_s); } while (line_s[0] == '#' || *line_s == 0); - song = Song::NewRemote(line_s); - song->tag = tag; - return song; + return new DetachedSong(line_s, std::move(tag)); } static const char *const extm3u_suffixes[] = { @@ -141,7 +136,7 @@ static const char *const extm3u_suffixes[] = { static const char *const extm3u_mime_types[] = { "audio/x-mpegurl", - NULL + nullptr }; const struct playlist_plugin extm3u_playlist_plugin = { diff --git a/src/playlist/ExtM3uPlaylistPlugin.hxx b/src/playlist/plugins/ExtM3uPlaylistPlugin.hxx index 844fba15c..5743ded43 100644 --- a/src/playlist/ExtM3uPlaylistPlugin.hxx +++ b/src/playlist/plugins/ExtM3uPlaylistPlugin.hxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2013 The Music Player Daemon Project + * 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 diff --git a/src/playlist/M3uPlaylistPlugin.cxx b/src/playlist/plugins/M3uPlaylistPlugin.cxx index 8b6adc2b6..0428d291a 100644 --- a/src/playlist/M3uPlaylistPlugin.cxx +++ b/src/playlist/plugins/M3uPlaylistPlugin.cxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2013 The Music Player Daemon Project + * 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 @@ -19,11 +19,11 @@ #include "config.h" #include "M3uPlaylistPlugin.hxx" -#include "PlaylistPlugin.hxx" -#include "SongEnumerator.hxx" -#include "Song.hxx" +#include "../PlaylistPlugin.hxx" +#include "../SongEnumerator.hxx" +#include "DetachedSong.hxx" #include "util/StringUtil.hxx" -#include "TextInputStream.hxx" +#include "input/TextInputStream.hxx" class M3uPlaylist final : public SongEnumerator { TextInputStream tis; @@ -33,7 +33,7 @@ public: :tis(is) { } - virtual Song *NextSong() override; + virtual DetachedSong *NextSong() override; }; static SongEnumerator * @@ -42,21 +42,20 @@ m3u_open_stream(InputStream &is) return new M3uPlaylist(is); } -Song * +DetachedSong * M3uPlaylist::NextSong() { - std::string line; - const char *line_s; + char *line_s; do { - if (!tis.ReadLine(line)) + line_s = tis.ReadLine(); + if (line_s == nullptr) return nullptr; - line_s = line.c_str(); - line_s = strchug_fast(line_s); + line_s = Strip(line_s); } while (line_s[0] == '#' || *line_s == 0); - return Song::NewRemote(line_s); + return new DetachedSong(line_s); } static const char *const m3u_suffixes[] = { diff --git a/src/playlist/M3uPlaylistPlugin.hxx b/src/playlist/plugins/M3uPlaylistPlugin.hxx index a2058bb29..f1ad14069 100644 --- a/src/playlist/M3uPlaylistPlugin.hxx +++ b/src/playlist/plugins/M3uPlaylistPlugin.hxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2013 The Music Player Daemon Project + * 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 diff --git a/src/playlist/PlsPlaylistPlugin.cxx b/src/playlist/plugins/PlsPlaylistPlugin.cxx index 7b5c8824c..f7724f522 100644 --- a/src/playlist/PlsPlaylistPlugin.cxx +++ b/src/playlist/plugins/PlsPlaylistPlugin.cxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2013 The Music Player Daemon Project + * 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 @@ -19,11 +19,11 @@ #include "config.h" #include "PlsPlaylistPlugin.hxx" -#include "PlaylistPlugin.hxx" -#include "MemorySongEnumerator.hxx" -#include "InputStream.hxx" -#include "Song.hxx" -#include "tag/Tag.hxx" +#include "../PlaylistPlugin.hxx" +#include "../MemorySongEnumerator.hxx" +#include "input/InputStream.hxx" +#include "DetachedSong.hxx" +#include "tag/TagBuilder.hxx" #include "util/Error.hxx" #include "util/Domain.hxx" #include "Log.hxx" @@ -33,13 +33,14 @@ #include <string> #include <stdio.h> +#include <stdio.h> + static constexpr Domain pls_domain("pls"); static void -pls_parser(GKeyFile *keyfile, std::forward_list<SongPointer> &songs) +pls_parser(GKeyFile *keyfile, std::forward_list<DetachedSong> &songs) { gchar *value; - int length; GError *error = nullptr; int num_entries = g_key_file_get_integer(keyfile, "playlist", "NumberOfEntries", &error); @@ -58,13 +59,11 @@ pls_parser(GKeyFile *keyfile, std::forward_list<SongPointer> &songs) } } - while (num_entries > 0) { - Song *song; - + for (; num_entries > 0; --num_entries) { char key[64]; sprintf(key, "File%u", num_entries); - value = g_key_file_get_string(keyfile, "playlist", key, - &error); + char *uri = g_key_file_get_string(keyfile, "playlist", key, + &error); if(error) { FormatError(pls_domain, "Invalid PLS entry %s: '%s'", key, error->message); @@ -72,36 +71,24 @@ pls_parser(GKeyFile *keyfile, std::forward_list<SongPointer> &songs) return; } - song = Song::NewRemote(value); - g_free(value); + TagBuilder tag; sprintf(key, "Title%u", num_entries); value = g_key_file_get_string(keyfile, "playlist", key, - &error); - if(error == nullptr && value){ - if (song->tag == nullptr) - song->tag = new Tag(); - song->tag->AddItem(TAG_TITLE, value); - } - /* Ignore errors? Most likely value not present */ - if(error) g_error_free(error); - error = nullptr; + nullptr); + if (value != nullptr) + tag.AddItem(TAG_TITLE, value); + g_free(value); sprintf(key, "Length%u", num_entries); - length = g_key_file_get_integer(keyfile, "playlist", key, - &error); - if(error == nullptr && length > 0){ - if (song->tag == nullptr) - song->tag = new Tag(); - song->tag->time = length; - } - /* Ignore errors? Most likely value not present */ - if(error) g_error_free(error); - error = nullptr; + int length = g_key_file_get_integer(keyfile, "playlist", key, + nullptr); + if (length > 0) + tag.SetDuration(SignedSongTime::FromS(length)); - songs.emplace_front(song); - num_entries--; + songs.emplace_front(uri, tag.Commit()); + g_free(uri); } } @@ -111,15 +98,12 @@ pls_open_stream(InputStream &is) { GError *error = nullptr; Error error2; - size_t nbytes; - char buffer[1024]; - bool success; - GKeyFile *keyfile; std::string kf_data; do { - nbytes = is.LockRead(buffer, sizeof(buffer), error2); + char buffer[1024]; + size_t nbytes = is.LockRead(buffer, sizeof(buffer), error2); if (nbytes == 0) { if (error2.IsDefined()) { LogError(error2); @@ -138,12 +122,10 @@ pls_open_stream(InputStream &is) return nullptr; } - keyfile = g_key_file_new(); - success = g_key_file_load_from_data(keyfile, - kf_data.data(), kf_data.length(), - G_KEY_FILE_NONE, &error); - - if (!success) { + GKeyFile *keyfile = g_key_file_new(); + if (!g_key_file_load_from_data(keyfile, + kf_data.data(), kf_data.length(), + G_KEY_FILE_NONE, &error)) { FormatError(pls_domain, "KeyFile parser failed: %s", error->message); g_error_free(error); @@ -151,7 +133,7 @@ pls_open_stream(InputStream &is) return nullptr; } - std::forward_list<SongPointer> songs; + std::forward_list<DetachedSong> songs; pls_parser(keyfile, songs); g_key_file_free(keyfile); diff --git a/src/playlist/PlsPlaylistPlugin.hxx b/src/playlist/plugins/PlsPlaylistPlugin.hxx index 3fafd36d0..1a3f33873 100644 --- a/src/playlist/PlsPlaylistPlugin.hxx +++ b/src/playlist/plugins/PlsPlaylistPlugin.hxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2013 The Music Player Daemon Project + * 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 diff --git a/src/playlist/plugins/RssPlaylistPlugin.cxx b/src/playlist/plugins/RssPlaylistPlugin.cxx new file mode 100644 index 000000000..6f9aad54b --- /dev/null +++ b/src/playlist/plugins/RssPlaylistPlugin.cxx @@ -0,0 +1,185 @@ +/* + * 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 "RssPlaylistPlugin.hxx" +#include "../PlaylistPlugin.hxx" +#include "../MemorySongEnumerator.hxx" +#include "tag/TagBuilder.hxx" +#include "util/ASCII.hxx" +#include "util/Error.hxx" +#include "lib/expat/ExpatParser.hxx" +#include "Log.hxx" + +/** + * This is the state object for the GLib XML parser. + */ +struct RssParser { + /** + * The list of songs (in reverse order because that's faster + * while adding). + */ + std::forward_list<DetachedSong> songs; + + /** + * The current position in the XML file. + */ + enum { + ROOT, ITEM, + } state; + + /** + * The current tag within the "entry" element. This is only + * valid if state==ITEM. TAG_NUM_OF_ITEM_TYPES means there + * is no (known) tag. + */ + TagType tag_type; + + /** + * The current song URI. It is set by the "enclosure" + * element. + */ + std::string location; + + TagBuilder tag_builder; + + RssParser() + :state(ROOT) {} +}; + +static void XMLCALL +rss_start_element(void *user_data, const XML_Char *element_name, + const XML_Char **atts) +{ + RssParser *parser = (RssParser *)user_data; + + switch (parser->state) { + case RssParser::ROOT: + if (StringEqualsCaseASCII(element_name, "item")) { + parser->state = RssParser::ITEM; + parser->location.clear(); + parser->tag_type = TAG_NUM_OF_ITEM_TYPES; + } + + break; + + case RssParser::ITEM: + if (StringEqualsCaseASCII(element_name, "enclosure")) { + const char *href = + ExpatParser::GetAttributeCase(atts, "url"); + if (href != nullptr) + parser->location = href; + } else if (StringEqualsCaseASCII(element_name, "title")) + parser->tag_type = TAG_TITLE; + else if (StringEqualsCaseASCII(element_name, "itunes:author")) + parser->tag_type = TAG_ARTIST; + + break; + } +} + +static void XMLCALL +rss_end_element(void *user_data, const XML_Char *element_name) +{ + RssParser *parser = (RssParser *)user_data; + + switch (parser->state) { + case RssParser::ROOT: + break; + + case RssParser::ITEM: + if (StringEqualsCaseASCII(element_name, "item")) { + if (!parser->location.empty()) + parser->songs.emplace_front(std::move(parser->location), + parser->tag_builder.Commit()); + + parser->state = RssParser::ROOT; + } else + parser->tag_type = TAG_NUM_OF_ITEM_TYPES; + + break; + } +} + +static void XMLCALL +rss_char_data(void *user_data, const XML_Char *s, int len) +{ + RssParser *parser = (RssParser *)user_data; + + switch (parser->state) { + case RssParser::ROOT: + break; + + case RssParser::ITEM: + if (parser->tag_type != TAG_NUM_OF_ITEM_TYPES) + parser->tag_builder.AddItem(parser->tag_type, s, len); + + break; + } +} + +/* + * The playlist object + * + */ + +static SongEnumerator * +rss_open_stream(InputStream &is) +{ + RssParser parser; + + { + ExpatParser expat(&parser); + expat.SetElementHandler(rss_start_element, rss_end_element); + expat.SetCharacterDataHandler(rss_char_data); + + Error error; + if (!expat.Parse(is, error)) { + LogError(error); + return nullptr; + } + } + + parser.songs.reverse(); + return new MemorySongEnumerator(std::move(parser.songs)); +} + +static const char *const rss_suffixes[] = { + "rss", + nullptr +}; + +static const char *const rss_mime_types[] = { + "application/rss+xml", + "text/xml", + nullptr +}; + +const struct playlist_plugin rss_playlist_plugin = { + "rss", + + nullptr, + nullptr, + nullptr, + rss_open_stream, + + nullptr, + rss_suffixes, + rss_mime_types, +}; diff --git a/src/playlist/RssPlaylistPlugin.hxx b/src/playlist/plugins/RssPlaylistPlugin.hxx index f49f7e9cf..a00a5a898 100644 --- a/src/playlist/RssPlaylistPlugin.hxx +++ b/src/playlist/plugins/RssPlaylistPlugin.hxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2013 The Music Player Daemon Project + * 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 diff --git a/src/playlist/SoundCloudPlaylistPlugin.cxx b/src/playlist/plugins/SoundCloudPlaylistPlugin.cxx index f6797b14d..ec4d240a5 100644 --- a/src/playlist/SoundCloudPlaylistPlugin.cxx +++ b/src/playlist/plugins/SoundCloudPlaylistPlugin.cxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2013 The Music Player Daemon Project + * 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 @@ -19,12 +19,12 @@ #include "config.h" #include "SoundCloudPlaylistPlugin.hxx" -#include "PlaylistPlugin.hxx" -#include "MemorySongEnumerator.hxx" -#include "ConfigData.hxx" -#include "InputStream.hxx" -#include "Song.hxx" -#include "tag/Tag.hxx" +#include "../PlaylistPlugin.hxx" +#include "../MemorySongEnumerator.hxx" +#include "config/ConfigData.hxx" +#include "input/InputStream.hxx" +#include "tag/TagBuilder.hxx" +#include "util/StringUtil.hxx" #include "util/Error.hxx" #include "util/Domain.hxx" #include "Log.hxx" @@ -45,7 +45,8 @@ static constexpr Domain soundcloud_domain("soundcloud"); static bool soundcloud_init(const config_param ¶m) { - soundcloud_config.apikey = param.GetBlockValue("apikey", ""); + // APIKEY for MPD application, registered under DarkFox' account. + soundcloud_config.apikey = param.GetBlockValue("apikey", "a25e51780f7f86af0afa91f241d091f8"); if (soundcloud_config.apikey.empty()) { LogDebug(soundcloud_domain, "disabling the soundcloud playlist plugin " @@ -62,19 +63,20 @@ soundcloud_init(const config_param ¶m) * @return Constructed URL. Must be freed with g_free. */ static char * -soundcloud_resolve(const char* uri) { +soundcloud_resolve(const char* uri) +{ char *u, *ru; - if (g_str_has_prefix(uri, "http://")) { + if (StringStartsWith(uri, "https://")) { u = g_strdup(uri); - } else if (g_str_has_prefix(uri, "soundcloud.com")) { - u = g_strconcat("http://", uri, nullptr); + } else if (StringStartsWith(uri, "soundcloud.com")) { + u = g_strconcat("https://", uri, nullptr); } else { /* assume it's just a path on soundcloud.com */ - u = g_strconcat("http://soundcloud.com/", uri, nullptr); + u = g_strconcat("https://soundcloud.com/", uri, nullptr); } - ru = g_strconcat("http://api.soundcloud.com/resolve.json?url=", + ru = g_strconcat("https://api.soundcloud.com/resolve.json?url=", u, "&client_id=", soundcloud_config.apikey.c_str(), nullptr); g_free(u); @@ -105,15 +107,16 @@ struct parse_data { char* title; int got_url; /* nesting level of last stream_url */ - std::forward_list<SongPointer> songs; + std::forward_list<DetachedSong> songs; }; -static int handle_integer(void *ctx, - long +static int +handle_integer(void *ctx, + long #ifndef HAVE_YAJL1 - long + long #endif - intval) + intval) { struct parse_data *data = (struct parse_data *) ctx; @@ -128,26 +131,25 @@ static int handle_integer(void *ctx, return 1; } -static int handle_string(void *ctx, const unsigned char* stringval, +static int +handle_string(void *ctx, const unsigned char* stringval, #ifdef HAVE_YAJL1 - unsigned int + unsigned int #else - size_t + size_t #endif - stringlen) + stringlen) { struct parse_data *data = (struct parse_data *) ctx; const char *s = (const char *) stringval; switch (data->key) { case Title: - if (data->title != nullptr) - g_free(data->title); + g_free(data->title); data->title = g_strndup(s, stringlen); break; case Stream_URL: - if (data->stream_url != nullptr) - g_free(data->stream_url); + g_free(data->stream_url); data->stream_url = g_strndup(s, stringlen); data->got_url = 1; break; @@ -158,13 +160,14 @@ static int handle_string(void *ctx, const unsigned char* stringval, return 1; } -static int handle_mapkey(void *ctx, const unsigned char* stringval, +static int +handle_mapkey(void *ctx, const unsigned char* stringval, #ifdef HAVE_YAJL1 - unsigned int + unsigned int #else - size_t + size_t #endif - stringlen) + stringlen) { struct parse_data *data = (struct parse_data *) ctx; @@ -181,7 +184,8 @@ static int handle_mapkey(void *ctx, const unsigned char* stringval, return 1; } -static int handle_start_map(void *ctx) +static int +handle_start_map(void *ctx) { struct parse_data *data = (struct parse_data *) ctx; @@ -191,7 +195,8 @@ static int handle_start_map(void *ctx) return 1; } -static int handle_end_map(void *ctx) +static int +handle_end_map(void *ctx) { struct parse_data *data = (struct parse_data *) ctx; @@ -206,21 +211,16 @@ static int handle_end_map(void *ctx) /* got_url == 1, track finished, make it into a song */ data->got_url = 0; - Song *s; - char *u; + char *u = g_strconcat(data->stream_url, "?client_id=", + soundcloud_config.apikey.c_str(), nullptr); - u = g_strconcat(data->stream_url, "?client_id=", - soundcloud_config.apikey.c_str(), nullptr); - s = Song::NewRemote(u); - g_free(u); - - Tag *t = new Tag(); - t->time = data->duration / 1000; + TagBuilder tag; + tag.SetDuration(SignedSongTime::FromMS(data->duration)); if (data->title != nullptr) - t->AddItem(TAG_NAME, data->title); - s->tag = t; + tag.AddItem(TAG_NAME, data->title); - data->songs.emplace_front(s); + data->songs.emplace_front(u, tag.Commit()); + g_free(u); return 1; } @@ -249,12 +249,9 @@ static int soundcloud_parse_json(const char *url, yajl_handle hand, Mutex &mutex, Cond &cond) { - char buffer[4096]; - unsigned char *ubuffer = (unsigned char *)buffer; - Error error; - InputStream *input_stream = InputStream::Open(url, mutex, cond, - error); + InputStream *input_stream = InputStream::OpenReady(url, mutex, cond, + error); if (input_stream == nullptr) { if (error.IsDefined()) LogError(error); @@ -262,12 +259,13 @@ soundcloud_parse_json(const char *url, yajl_handle hand, } mutex.lock(); - input_stream->WaitReady(); yajl_status stat; int done = 0; while (!done) { + char buffer[4096]; + unsigned char *ubuffer = (unsigned char *)buffer; const size_t nbytes = input_stream->Read(buffer, sizeof(buffer), error); if (nbytes == 0) { @@ -278,7 +276,7 @@ soundcloud_parse_json(const char *url, yajl_handle hand, done = true; } else { mutex.unlock(); - input_stream->Close(); + delete input_stream; return -1; } } @@ -306,7 +304,7 @@ soundcloud_parse_json(const char *url, yajl_handle hand, } mutex.unlock(); - input_stream->Close(); + delete input_stream; return 0; } @@ -318,80 +316,62 @@ soundcloud_parse_json(const char *url, yajl_handle hand, * soundcloud://playlist/<playlist-id> * soundcloud://url/<url or path of soundcloud page> */ - static SongEnumerator * soundcloud_open_uri(const char *uri, Mutex &mutex, Cond &cond) { - char *s, *p; - char *scheme, *arg, *rest; - s = g_strdup(uri); - scheme = s; - for (p = s; *p; p++) { - if (*p == ':' && *(p+1) == '/' && *(p+2) == '/') { - *p = 0; - p += 3; - break; - } - } - arg = p; - for (; *p; p++) { - if (*p == '/') { - *p = 0; - p++; - break; - } - } - rest = p; - - if (strcmp(scheme, "soundcloud") != 0) { - FormatWarning(soundcloud_domain, - "incompatible scheme for soundcloud plugin: %s", - scheme); - g_free(s); - return nullptr; - } + assert(memcmp(uri, "soundcloud://", 13) == 0); + uri += 13; char *u = nullptr; - if (strcmp(arg, "track") == 0) { - u = g_strconcat("http://api.soundcloud.com/tracks/", + if (memcmp(uri, "track/", 6) == 0) { + const char *rest = uri + 6; + u = g_strconcat("https://api.soundcloud.com/tracks/", rest, ".json?client_id=", soundcloud_config.apikey.c_str(), nullptr); - } else if (strcmp(arg, "playlist") == 0) { - u = g_strconcat("http://api.soundcloud.com/playlists/", + } else if (memcmp(uri, "playlist/", 9) == 0) { + const char *rest = uri + 9; + u = g_strconcat("https://api.soundcloud.com/playlists/", rest, ".json?client_id=", soundcloud_config.apikey.c_str(), nullptr); - } else if (strcmp(arg, "url") == 0) { + } else if (memcmp(uri, "user/", 5) == 0) { + const char *rest = uri + 5; + u = g_strconcat("https://api.soundcloud.com/users/", + rest, "/tracks.json?client_id=", + soundcloud_config.apikey.c_str(), nullptr); + } else if (memcmp(uri, "search/", 7) == 0) { + const char *rest = uri + 7; + u = g_strconcat("https://api.soundcloud.com/tracks.json?q=", + rest, "&client_id=", + soundcloud_config.apikey.c_str(), nullptr); + } else if (memcmp(uri, "url/", 4) == 0) { + const char *rest = uri + 4; /* Translate to soundcloud resolver call. libcurl will automatically follow the redirect to the right resource. */ u = soundcloud_resolve(rest); } - g_free(s); if (u == nullptr) { LogWarning(soundcloud_domain, "unknown soundcloud URI"); return nullptr; } - yajl_handle hand; struct parse_data data; - data.got_url = 0; data.title = nullptr; data.stream_url = nullptr; #ifdef HAVE_YAJL1 - hand = yajl_alloc(&parse_callbacks, nullptr, nullptr, (void *) &data); + yajl_handle hand = yajl_alloc(&parse_callbacks, nullptr, nullptr, + &data); #else - hand = yajl_alloc(&parse_callbacks, nullptr, (void *) &data); + yajl_handle hand = yajl_alloc(&parse_callbacks, nullptr, &data); #endif int ret = soundcloud_parse_json(u, hand, mutex, cond); g_free(u); yajl_free(hand); - if (data.title != nullptr) - g_free(data.title); - if (data.stream_url != nullptr) - g_free(data.stream_url); + g_free(data.title); + g_free(data.stream_url); if (ret == -1) return nullptr; diff --git a/src/playlist/SoundCloudPlaylistPlugin.hxx b/src/playlist/plugins/SoundCloudPlaylistPlugin.hxx index 7c121328c..b355b477a 100644 --- a/src/playlist/SoundCloudPlaylistPlugin.hxx +++ b/src/playlist/plugins/SoundCloudPlaylistPlugin.hxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2013 The Music Player Daemon Project + * 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 diff --git a/src/playlist/XspfPlaylistPlugin.cxx b/src/playlist/plugins/XspfPlaylistPlugin.cxx index dcfab5a80..5b6010b53 100644 --- a/src/playlist/XspfPlaylistPlugin.cxx +++ b/src/playlist/plugins/XspfPlaylistPlugin.cxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2013 The Music Player Daemon Project + * 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 @@ -19,17 +19,16 @@ #include "config.h" #include "XspfPlaylistPlugin.hxx" -#include "PlaylistPlugin.hxx" -#include "MemorySongEnumerator.hxx" -#include "InputStream.hxx" -#include "tag/Tag.hxx" +#include "../PlaylistPlugin.hxx" +#include "../MemorySongEnumerator.hxx" +#include "DetachedSong.hxx" +#include "input/InputStream.hxx" +#include "tag/TagBuilder.hxx" #include "util/Error.hxx" #include "util/Domain.hxx" +#include "lib/expat/ExpatParser.hxx" #include "Log.hxx" -#include <glib.h> - -#include <assert.h> #include <string.h> static constexpr Domain xspf_domain("xspf"); @@ -42,7 +41,7 @@ struct XspfParser { * The list of songs (in reverse order because that's faster * while adding). */ - std::forward_list<SongPointer> songs; + std::forward_list<DetachedSong> songs; /** * The current position in the XML file. @@ -57,24 +56,22 @@ struct XspfParser { * valid if state==TRACK. TAG_NUM_OF_ITEM_TYPES means there * is no (known) tag. */ - TagType tag; + TagType tag_type; /** - * The current song. It is allocated after the "location" - * element. + * The current song URI. It is set by the "location" element. */ - Song *song; + std::string location; + + TagBuilder tag_builder; XspfParser() :state(ROOT) {} }; -static void -xspf_start_element(gcc_unused GMarkupParseContext *context, - const gchar *element_name, - gcc_unused const gchar **attribute_names, - gcc_unused const gchar **attribute_values, - gpointer user_data, gcc_unused GError **error) +static void XMLCALL +xspf_start_element(void *user_data, const XML_Char *element_name, + gcc_unused const XML_Char **atts) { XspfParser *parser = (XspfParser *)user_data; @@ -94,8 +91,8 @@ xspf_start_element(gcc_unused GMarkupParseContext *context, case XspfParser::TRACKLIST: if (strcmp(element_name, "track") == 0) { parser->state = XspfParser::TRACK; - parser->song = nullptr; - parser->tag = TAG_NUM_OF_ITEM_TYPES; + parser->location.clear(); + parser->tag_type = TAG_NUM_OF_ITEM_TYPES; } break; @@ -104,17 +101,17 @@ xspf_start_element(gcc_unused GMarkupParseContext *context, if (strcmp(element_name, "location") == 0) parser->state = XspfParser::LOCATION; else if (strcmp(element_name, "title") == 0) - parser->tag = TAG_TITLE; + parser->tag_type = TAG_TITLE; else if (strcmp(element_name, "creator") == 0) /* TAG_COMPOSER would be more correct according to the XSPF spec */ - parser->tag = TAG_ARTIST; + parser->tag_type = TAG_ARTIST; else if (strcmp(element_name, "annotation") == 0) - parser->tag = TAG_COMMENT; + parser->tag_type = TAG_COMMENT; else if (strcmp(element_name, "album") == 0) - parser->tag = TAG_ALBUM; + parser->tag_type = TAG_ALBUM; else if (strcmp(element_name, "trackNum") == 0) - parser->tag = TAG_TRACK; + parser->tag_type = TAG_TRACK; break; @@ -123,10 +120,8 @@ xspf_start_element(gcc_unused GMarkupParseContext *context, } } -static void -xspf_end_element(gcc_unused GMarkupParseContext *context, - const gchar *element_name, - gpointer user_data, gcc_unused GError **error) +static void XMLCALL +xspf_end_element(void *user_data, const XML_Char *element_name) { XspfParser *parser = (XspfParser *)user_data; @@ -148,12 +143,13 @@ xspf_end_element(gcc_unused GMarkupParseContext *context, case XspfParser::TRACK: if (strcmp(element_name, "track") == 0) { - if (parser->song != nullptr) - parser->songs.emplace_front(parser->song); + if (!parser->location.empty()) + parser->songs.emplace_front(std::move(parser->location), + parser->tag_builder.Commit()); parser->state = XspfParser::TRACKLIST; } else - parser->tag = TAG_NUM_OF_ITEM_TYPES; + parser->tag_type = TAG_NUM_OF_ITEM_TYPES; break; @@ -163,10 +159,8 @@ xspf_end_element(gcc_unused GMarkupParseContext *context, } } -static void -xspf_text(gcc_unused GMarkupParseContext *context, - const gchar *text, gsize text_len, - gpointer user_data, gcc_unused GError **error) +static void XMLCALL +xspf_char_data(void *user_data, const XML_Char *s, int len) { XspfParser *parser = (XspfParser *)user_data; @@ -177,43 +171,19 @@ xspf_text(gcc_unused GMarkupParseContext *context, break; case XspfParser::TRACK: - if (parser->song != nullptr && - parser->tag != TAG_NUM_OF_ITEM_TYPES) { - if (parser->song->tag == nullptr) - parser->song->tag = new Tag(); - parser->song->tag->AddItem(parser->tag, text, text_len); - } + if (!parser->location.empty() && + parser->tag_type != TAG_NUM_OF_ITEM_TYPES) + parser->tag_builder.AddItem(parser->tag_type, s, len); break; case XspfParser::LOCATION: - if (parser->song == nullptr) { - char *uri = g_strndup(text, text_len); - parser->song = Song::NewRemote(uri); - g_free(uri); - } + parser->location.assign(s, len); break; } } -static const GMarkupParser xspf_parser = { - xspf_start_element, - xspf_end_element, - xspf_text, - nullptr, - nullptr, -}; - -static void -xspf_parser_destroy(gpointer data) -{ - XspfParser *parser = (XspfParser *)data; - - if (parser->state >= XspfParser::TRACK && parser->song != nullptr) - parser->song->Free(); -} - /* * The playlist object * @@ -223,58 +193,21 @@ static SongEnumerator * xspf_open_stream(InputStream &is) { XspfParser parser; - GMarkupParseContext *context; - char buffer[1024]; - size_t nbytes; - bool success; - Error error2; - GError *error = nullptr; - - /* parse the XSPF XML file */ - - context = g_markup_parse_context_new(&xspf_parser, - G_MARKUP_TREAT_CDATA_AS_TEXT, - &parser, xspf_parser_destroy); - - while (true) { - nbytes = is.LockRead(buffer, sizeof(buffer), error2); - if (nbytes == 0) { - if (error2.IsDefined()) { - g_markup_parse_context_free(context); - LogError(error2); - return nullptr; - } - - break; - } - success = g_markup_parse_context_parse(context, buffer, nbytes, - &error); - if (!success) { - FormatError(xspf_domain, - "XML parser failed: %s", error->message); - g_error_free(error); - g_markup_parse_context_free(context); + { + ExpatParser expat(&parser); + expat.SetElementHandler(xspf_start_element, xspf_end_element); + expat.SetCharacterDataHandler(xspf_char_data); + + Error error; + if (!expat.Parse(is, error)) { + LogError(error); return nullptr; } } - success = g_markup_parse_context_end_parse(context, &error); - if (!success) { - FormatError(xspf_domain, - "XML parser failed: %s", error->message); - g_error_free(error); - g_markup_parse_context_free(context); - return nullptr; - } - parser.songs.reverse(); - MemorySongEnumerator *playlist = - new MemorySongEnumerator(std::move(parser.songs)); - - g_markup_parse_context_free(context); - - return playlist; + return new MemorySongEnumerator(std::move(parser.songs)); } static const char *const xspf_suffixes[] = { diff --git a/src/playlist/XspfPlaylistPlugin.hxx b/src/playlist/plugins/XspfPlaylistPlugin.hxx index fc9bbd2c6..6b08a6be6 100644 --- a/src/playlist/XspfPlaylistPlugin.hxx +++ b/src/playlist/plugins/XspfPlaylistPlugin.hxx @@ -1,5 +1,5 @@ /* - * Copyright (C) 2003-2013 The Music Player Daemon Project + * 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 |