diff options
Diffstat (limited to 'src/playlist/plugins')
18 files changed, 1903 insertions, 0 deletions
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/plugins/AsxPlaylistPlugin.hxx b/src/playlist/plugins/AsxPlaylistPlugin.hxx new file mode 100644 index 000000000..63371be0f --- /dev/null +++ b/src/playlist/plugins/AsxPlaylistPlugin.hxx @@ -0,0 +1,25 @@ +/* + * 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_ASX_PLAYLIST_PLUGIN_HXX +#define MPD_ASX_PLAYLIST_PLUGIN_HXX + +extern const struct playlist_plugin asx_playlist_plugin; + +#endif diff --git a/src/playlist/plugins/CuePlaylistPlugin.cxx b/src/playlist/plugins/CuePlaylistPlugin.cxx new file mode 100644 index 000000000..b907d34d0 --- /dev/null +++ b/src/playlist/plugins/CuePlaylistPlugin.cxx @@ -0,0 +1,88 @@ +/* + * 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 "CuePlaylistPlugin.hxx" +#include "../PlaylistPlugin.hxx" +#include "../SongEnumerator.hxx" +#include "../cue/CueParser.hxx" +#include "input/TextInputStream.hxx" + +#include <string> + +class CuePlaylist final : public SongEnumerator { + InputStream &is; + TextInputStream tis; + CueParser parser; + + public: + CuePlaylist(InputStream &_is) + :is(_is), tis(is) { + } + + virtual DetachedSong *NextSong() override; +}; + +static SongEnumerator * +cue_playlist_open_stream(InputStream &is) +{ + return new CuePlaylist(is); +} + +DetachedSong * +CuePlaylist::NextSong() +{ + DetachedSong *song = parser.Get(); + if (song != nullptr) + return song; + + const char *line; + while ((line = tis.ReadLine()) != nullptr) { + parser.Feed(line); + song = parser.Get(); + if (song != nullptr) + return song; + } + + parser.Finish(); + return parser.Get(); +} + +static const char *const cue_playlist_suffixes[] = { + "cue", + nullptr +}; + +static const char *const cue_playlist_mime_types[] = { + "application/x-cue", + nullptr +}; + +const struct playlist_plugin cue_playlist_plugin = { + "cue", + + nullptr, + nullptr, + nullptr, + cue_playlist_open_stream, + + nullptr, + cue_playlist_suffixes, + cue_playlist_mime_types, +}; diff --git a/src/playlist/plugins/CuePlaylistPlugin.hxx b/src/playlist/plugins/CuePlaylistPlugin.hxx new file mode 100644 index 000000000..4d833bfc2 --- /dev/null +++ b/src/playlist/plugins/CuePlaylistPlugin.hxx @@ -0,0 +1,25 @@ +/* + * 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_PLAYLIST_PLUGIN_HXX +#define MPD_CUE_PLAYLIST_PLUGIN_HXX + +extern const struct playlist_plugin cue_playlist_plugin; + +#endif diff --git a/src/playlist/plugins/EmbeddedCuePlaylistPlugin.cxx b/src/playlist/plugins/EmbeddedCuePlaylistPlugin.cxx new file mode 100644 index 000000000..8baa11c03 --- /dev/null +++ b/src/playlist/plugins/EmbeddedCuePlaylistPlugin.cxx @@ -0,0 +1,184 @@ +/* + * 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 + * + * Playlist plugin that reads embedded cue sheets from the "CUESHEET" + * tag of a music file. + */ + +#include "config.h" +#include "EmbeddedCuePlaylistPlugin.hxx" +#include "../PlaylistPlugin.hxx" +#include "../SongEnumerator.hxx" +#include "../cue/CueParser.hxx" +#include "tag/TagHandler.hxx" +#include "tag/TagId3.hxx" +#include "tag/ApeTag.hxx" +#include "DetachedSong.hxx" +#include "TagFile.hxx" +#include "fs/Traits.hxx" +#include "fs/AllocatedPath.hxx" +#include "util/ASCII.hxx" + +#include <string.h> + +class EmbeddedCuePlaylist final : public SongEnumerator { +public: + /** + * This is an override for the CUE's "FILE". An embedded CUE + * sheet must always point to the song file it is contained + * in. + */ + std::string filename; + + /** + * The value of the file's "CUESHEET" tag. + */ + std::string cuesheet; + + /** + * The offset of the next line within "cuesheet". + */ + char *next; + + CueParser *parser; + +public: + EmbeddedCuePlaylist() + :parser(nullptr) { + } + + virtual ~EmbeddedCuePlaylist() { + delete parser; + } + + virtual DetachedSong *NextSong() override; +}; + +static void +embcue_tag_pair(const char *name, const char *value, void *ctx) +{ + EmbeddedCuePlaylist *playlist = (EmbeddedCuePlaylist *)ctx; + + if (playlist->cuesheet.empty() && + StringEqualsCaseASCII(name, "cuesheet")) + playlist->cuesheet = value; +} + +static const struct tag_handler embcue_tag_handler = { + nullptr, + nullptr, + embcue_tag_pair, +}; + +static SongEnumerator * +embcue_playlist_open_uri(const char *uri, + gcc_unused Mutex &mutex, + gcc_unused Cond &cond) +{ + if (!PathTraitsUTF8::IsAbsolute(uri)) + /* only local files supported */ + return nullptr; + + const auto path_fs = AllocatedPath::FromUTF8(uri); + if (path_fs.IsNull()) + return nullptr; + + const auto playlist = new EmbeddedCuePlaylist(); + + 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()) + tag_id3_scan(path_fs, &embcue_tag_handler, playlist); + } + + if (playlist->cuesheet.empty()) { + /* no "CUESHEET" tag found */ + delete playlist; + return nullptr; + } + + playlist->filename = PathTraitsUTF8::GetBase(uri); + + playlist->next = &playlist->cuesheet[0]; + playlist->parser = new CueParser(); + + return playlist; +} + +DetachedSong * +EmbeddedCuePlaylist::NextSong() +{ + DetachedSong *song = parser->Get(); + if (song != nullptr) + return song; + + while (*next != 0) { + const char *line = next; + char *eol = strpbrk(next, "\r\n"); + if (eol != nullptr) { + /* null-terminate the line */ + *eol = 0; + next = eol + 1; + } else + /* last line; put the "next" pointer to the + end of the buffer */ + next += strlen(line); + + parser->Feed(line); + song = parser->Get(); + if (song != nullptr) { + song->SetURI(filename); + return song; + } + } + + parser->Finish(); + song = parser->Get(); + if (song != nullptr) + song->SetURI(filename); + return song; +} + +static const char *const embcue_playlist_suffixes[] = { + /* a few codecs that are known to be supported; there are + probably many more */ + "flac", + "mp3", "mp2", + "mp4", "mp4a", "m4b", + "ape", + "wv", + "ogg", "oga", + nullptr +}; + +const struct playlist_plugin embcue_playlist_plugin = { + "embcue", + + nullptr, + nullptr, + embcue_playlist_open_uri, + nullptr, + + nullptr, + embcue_playlist_suffixes, + nullptr, +}; diff --git a/src/playlist/plugins/EmbeddedCuePlaylistPlugin.hxx b/src/playlist/plugins/EmbeddedCuePlaylistPlugin.hxx new file mode 100644 index 000000000..5eedf3f13 --- /dev/null +++ b/src/playlist/plugins/EmbeddedCuePlaylistPlugin.hxx @@ -0,0 +1,25 @@ +/* + * 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_EMBCUE_PLAYLIST_PLUGIN_HXX +#define MPD_EMBCUE_PLAYLIST_PLUGIN_HXX + +extern const struct playlist_plugin embcue_playlist_plugin; + +#endif diff --git a/src/playlist/plugins/ExtM3uPlaylistPlugin.cxx b/src/playlist/plugins/ExtM3uPlaylistPlugin.cxx new file mode 100644 index 000000000..93316ca6c --- /dev/null +++ b/src/playlist/plugins/ExtM3uPlaylistPlugin.cxx @@ -0,0 +1,153 @@ +/* + * 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 "ExtM3uPlaylistPlugin.hxx" +#include "../PlaylistPlugin.hxx" +#include "../SongEnumerator.hxx" +#include "DetachedSong.hxx" +#include "tag/Tag.hxx" +#include "tag/TagBuilder.hxx" +#include "util/StringUtil.hxx" +#include "input/TextInputStream.hxx" + +#include <string.h> +#include <stdlib.h> + +class ExtM3uPlaylist final : public SongEnumerator { + TextInputStream tis; + +public: + ExtM3uPlaylist(InputStream &is) + :tis(is) { + } + + bool CheckFirstLine() { + char *line = tis.ReadLine(); + if (line == nullptr) + return false; + + StripRight(line); + return strcmp(line, "#EXTM3U") == 0; + } + + virtual DetachedSong *NextSong() override; +}; + +static SongEnumerator * +extm3u_open_stream(InputStream &is) +{ + ExtM3uPlaylist *playlist = new ExtM3uPlaylist(is); + + if (!playlist->CheckFirstLine()) { + /* no EXTM3U header: fall back to the plain m3u + plugin */ + delete playlist; + return nullptr; + } + + return playlist; +} + +/** + * Parse a EXTINF line. + * + * @param line the rest of the input line after the colon + */ +static Tag +extm3u_parse_tag(const char *line) +{ + long duration; + char *endptr; + const char *name; + + duration = strtol(line, &endptr, 10); + if (endptr[0] != ',') + /* malformed line */ + return Tag(); + + if (duration < 0) + /* 0 means unknown duration */ + duration = 0; + + name = StripLeft(endptr + 1); + if (*name == 0 && duration == 0) + /* no information available; don't allocate a tag + object */ + return Tag(); + + 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); + + return tag.Commit(); +} + +DetachedSong * +ExtM3uPlaylist::NextSong() +{ + Tag tag; + char *line_s; + + do { + line_s = tis.ReadLine(); + if (line_s == nullptr) + return nullptr; + + StripRight(line_s); + + if (StringStartsWith(line_s, "#EXTINF:")) { + tag = extm3u_parse_tag(line_s + 8); + continue; + } + + line_s = StripLeft(line_s); + } while (line_s[0] == '#' || *line_s == 0); + + return new DetachedSong(line_s, std::move(tag)); +} + +static const char *const extm3u_suffixes[] = { + "m3u", + "m3u8", + nullptr +}; + +static const char *const extm3u_mime_types[] = { + "audio/x-mpegurl", + nullptr +}; + +const struct playlist_plugin extm3u_playlist_plugin = { + "extm3u", + + nullptr, + nullptr, + nullptr, + extm3u_open_stream, + + nullptr, + extm3u_suffixes, + extm3u_mime_types, +}; diff --git a/src/playlist/plugins/ExtM3uPlaylistPlugin.hxx b/src/playlist/plugins/ExtM3uPlaylistPlugin.hxx new file mode 100644 index 000000000..5743ded43 --- /dev/null +++ b/src/playlist/plugins/ExtM3uPlaylistPlugin.hxx @@ -0,0 +1,25 @@ +/* + * 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_EXTM3U_PLAYLIST_PLUGIN_HXX +#define MPD_EXTM3U_PLAYLIST_PLUGIN_HXX + +extern const struct playlist_plugin extm3u_playlist_plugin; + +#endif diff --git a/src/playlist/plugins/M3uPlaylistPlugin.cxx b/src/playlist/plugins/M3uPlaylistPlugin.cxx new file mode 100644 index 000000000..0428d291a --- /dev/null +++ b/src/playlist/plugins/M3uPlaylistPlugin.cxx @@ -0,0 +1,83 @@ +/* + * 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 "M3uPlaylistPlugin.hxx" +#include "../PlaylistPlugin.hxx" +#include "../SongEnumerator.hxx" +#include "DetachedSong.hxx" +#include "util/StringUtil.hxx" +#include "input/TextInputStream.hxx" + +class M3uPlaylist final : public SongEnumerator { + TextInputStream tis; + +public: + M3uPlaylist(InputStream &is) + :tis(is) { + } + + virtual DetachedSong *NextSong() override; +}; + +static SongEnumerator * +m3u_open_stream(InputStream &is) +{ + return new M3uPlaylist(is); +} + +DetachedSong * +M3uPlaylist::NextSong() +{ + char *line_s; + + do { + line_s = tis.ReadLine(); + if (line_s == nullptr) + return nullptr; + + line_s = Strip(line_s); + } while (line_s[0] == '#' || *line_s == 0); + + return new DetachedSong(line_s); +} + +static const char *const m3u_suffixes[] = { + "m3u", + "m3u8", + nullptr +}; + +static const char *const m3u_mime_types[] = { + "audio/x-mpegurl", + nullptr +}; + +const struct playlist_plugin m3u_playlist_plugin = { + "m3u", + + nullptr, + nullptr, + nullptr, + m3u_open_stream, + + nullptr, + m3u_suffixes, + m3u_mime_types, +}; diff --git a/src/playlist/plugins/M3uPlaylistPlugin.hxx b/src/playlist/plugins/M3uPlaylistPlugin.hxx new file mode 100644 index 000000000..f1ad14069 --- /dev/null +++ b/src/playlist/plugins/M3uPlaylistPlugin.hxx @@ -0,0 +1,25 @@ +/* + * 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_M3U_PLAYLIST_PLUGIN_HXX +#define MPD_M3U_PLAYLIST_PLUGIN_HXX + +extern const struct playlist_plugin m3u_playlist_plugin; + +#endif diff --git a/src/playlist/plugins/PlsPlaylistPlugin.cxx b/src/playlist/plugins/PlsPlaylistPlugin.cxx new file mode 100644 index 000000000..f7724f522 --- /dev/null +++ b/src/playlist/plugins/PlsPlaylistPlugin.cxx @@ -0,0 +1,164 @@ +/* + * 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 "PlsPlaylistPlugin.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" + +#include <glib.h> + +#include <string> +#include <stdio.h> + +#include <stdio.h> + +static constexpr Domain pls_domain("pls"); + +static void +pls_parser(GKeyFile *keyfile, std::forward_list<DetachedSong> &songs) +{ + gchar *value; + GError *error = nullptr; + int num_entries = g_key_file_get_integer(keyfile, "playlist", + "NumberOfEntries", &error); + if (error) { + FormatError(pls_domain, + "Invalid PLS file: '%s'", error->message); + g_error_free(error); + error = nullptr; + + /* Hack to work around shoutcast failure to comform to spec */ + num_entries = g_key_file_get_integer(keyfile, "playlist", + "numberofentries", &error); + if (error) { + g_error_free(error); + error = nullptr; + } + } + + for (; num_entries > 0; --num_entries) { + char key[64]; + sprintf(key, "File%u", num_entries); + char *uri = g_key_file_get_string(keyfile, "playlist", key, + &error); + if(error) { + FormatError(pls_domain, "Invalid PLS entry %s: '%s'", + key, error->message); + g_error_free(error); + return; + } + + TagBuilder tag; + + sprintf(key, "Title%u", num_entries); + value = g_key_file_get_string(keyfile, "playlist", key, + nullptr); + if (value != nullptr) + tag.AddItem(TAG_TITLE, value); + + g_free(value); + + sprintf(key, "Length%u", num_entries); + int length = g_key_file_get_integer(keyfile, "playlist", key, + nullptr); + if (length > 0) + tag.SetDuration(SignedSongTime::FromS(length)); + + songs.emplace_front(uri, tag.Commit()); + g_free(uri); + } + +} + +static SongEnumerator * +pls_open_stream(InputStream &is) +{ + GError *error = nullptr; + Error error2; + + std::string kf_data; + + do { + char buffer[1024]; + size_t nbytes = is.LockRead(buffer, sizeof(buffer), error2); + if (nbytes == 0) { + if (error2.IsDefined()) { + LogError(error2); + return nullptr; + } + + break; + } + + kf_data.append(buffer, nbytes); + /* Limit to 64k */ + } while (kf_data.length() < 65536); + + if (kf_data.empty()) { + LogWarning(pls_domain, "KeyFile parser failed: No Data"); + return nullptr; + } + + 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); + g_key_file_free(keyfile); + return nullptr; + } + + std::forward_list<DetachedSong> songs; + pls_parser(keyfile, songs); + g_key_file_free(keyfile); + + return new MemorySongEnumerator(std::move(songs)); +} + +static const char *const pls_suffixes[] = { + "pls", + nullptr +}; + +static const char *const pls_mime_types[] = { + "audio/x-scpls", + nullptr +}; + +const struct playlist_plugin pls_playlist_plugin = { + "pls", + + nullptr, + nullptr, + nullptr, + pls_open_stream, + + nullptr, + pls_suffixes, + pls_mime_types, +}; diff --git a/src/playlist/plugins/PlsPlaylistPlugin.hxx b/src/playlist/plugins/PlsPlaylistPlugin.hxx new file mode 100644 index 000000000..1a3f33873 --- /dev/null +++ b/src/playlist/plugins/PlsPlaylistPlugin.hxx @@ -0,0 +1,25 @@ +/* + * 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_PLS_PLAYLIST_PLUGIN_HXX +#define MPD_PLS_PLAYLIST_PLUGIN_HXX + +extern const struct playlist_plugin pls_playlist_plugin; + +#endif 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/plugins/RssPlaylistPlugin.hxx b/src/playlist/plugins/RssPlaylistPlugin.hxx new file mode 100644 index 000000000..a00a5a898 --- /dev/null +++ b/src/playlist/plugins/RssPlaylistPlugin.hxx @@ -0,0 +1,25 @@ +/* + * 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_RSS_PLAYLIST_PLUGIN_HXX +#define MPD_RSS_PLAYLIST_PLUGIN_HXX + +extern const struct playlist_plugin rss_playlist_plugin; + +#endif diff --git a/src/playlist/plugins/SoundCloudPlaylistPlugin.cxx b/src/playlist/plugins/SoundCloudPlaylistPlugin.cxx new file mode 100644 index 000000000..ec4d240a5 --- /dev/null +++ b/src/playlist/plugins/SoundCloudPlaylistPlugin.cxx @@ -0,0 +1,401 @@ +/* + * 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 "SoundCloudPlaylistPlugin.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" + +#include <glib.h> +#include <yajl/yajl_parse.h> + +#include <string> + +#include <string.h> + +static struct { + std::string apikey; +} soundcloud_config; + +static constexpr Domain soundcloud_domain("soundcloud"); + +static bool +soundcloud_init(const config_param ¶m) +{ + // 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 " + "because API key is not set"); + return false; + } + + return true; +} + +/** + * Construct a full soundcloud resolver URL from the given fragment. + * @param uri uri of a soundcloud page (or just the path) + * @return Constructed URL. Must be freed with g_free. + */ +static char * +soundcloud_resolve(const char* uri) +{ + char *u, *ru; + + if (StringStartsWith(uri, "https://")) { + u = g_strdup(uri); + } 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("https://soundcloud.com/", uri, nullptr); + } + + ru = g_strconcat("https://api.soundcloud.com/resolve.json?url=", + u, "&client_id=", + soundcloud_config.apikey.c_str(), nullptr); + g_free(u); + + return ru; +} + +/* YAJL parser for track data from both /tracks/ and /playlists/ JSON */ + +enum key { + Duration, + Title, + Stream_URL, + Other, +}; + +const char* key_str[] = { + "duration", + "title", + "stream_url", + nullptr, +}; + +struct parse_data { + int key; + char* stream_url; + long duration; + char* title; + int got_url; /* nesting level of last stream_url */ + + std::forward_list<DetachedSong> songs; +}; + +static int +handle_integer(void *ctx, + long +#ifndef HAVE_YAJL1 + long +#endif + intval) +{ + struct parse_data *data = (struct parse_data *) ctx; + + switch (data->key) { + case Duration: + data->duration = intval; + break; + default: + break; + } + + return 1; +} + +static int +handle_string(void *ctx, const unsigned char* stringval, +#ifdef HAVE_YAJL1 + unsigned int +#else + size_t +#endif + stringlen) +{ + struct parse_data *data = (struct parse_data *) ctx; + const char *s = (const char *) stringval; + + switch (data->key) { + case Title: + g_free(data->title); + data->title = g_strndup(s, stringlen); + break; + case Stream_URL: + g_free(data->stream_url); + data->stream_url = g_strndup(s, stringlen); + data->got_url = 1; + break; + default: + break; + } + + return 1; +} + +static int +handle_mapkey(void *ctx, const unsigned char* stringval, +#ifdef HAVE_YAJL1 + unsigned int +#else + size_t +#endif + stringlen) +{ + struct parse_data *data = (struct parse_data *) ctx; + + int i; + data->key = Other; + + for (i = 0; i < Other; ++i) { + if (memcmp((const char *)stringval, key_str[i], stringlen) == 0) { + data->key = i; + break; + } + } + + return 1; +} + +static int +handle_start_map(void *ctx) +{ + struct parse_data *data = (struct parse_data *) ctx; + + if (data->got_url > 0) + data->got_url++; + + return 1; +} + +static int +handle_end_map(void *ctx) +{ + struct parse_data *data = (struct parse_data *) ctx; + + if (data->got_url > 1) { + data->got_url--; + return 1; + } + + if (data->got_url == 0) + return 1; + + /* got_url == 1, track finished, make it into a song */ + data->got_url = 0; + + char *u = g_strconcat(data->stream_url, "?client_id=", + soundcloud_config.apikey.c_str(), nullptr); + + TagBuilder tag; + tag.SetDuration(SignedSongTime::FromMS(data->duration)); + if (data->title != nullptr) + tag.AddItem(TAG_NAME, data->title); + + data->songs.emplace_front(u, tag.Commit()); + g_free(u); + + return 1; +} + +static yajl_callbacks parse_callbacks = { + nullptr, + nullptr, + handle_integer, + nullptr, + nullptr, + handle_string, + handle_start_map, + handle_mapkey, + handle_end_map, + nullptr, + nullptr, +}; + +/** + * Read JSON data and parse it using the given YAJL parser. + * @param url URL of the JSON data. + * @param hand YAJL parser handle. + * @return -1 on error, 0 on success. + */ +static int +soundcloud_parse_json(const char *url, yajl_handle hand, + Mutex &mutex, Cond &cond) +{ + Error error; + InputStream *input_stream = InputStream::OpenReady(url, mutex, cond, + error); + if (input_stream == nullptr) { + if (error.IsDefined()) + LogError(error); + return -1; + } + + mutex.lock(); + + 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) { + if (error.IsDefined()) + LogError(error); + + if (input_stream->IsEOF()) { + done = true; + } else { + mutex.unlock(); + delete input_stream; + return -1; + } + } + + if (done) { +#ifdef HAVE_YAJL1 + stat = yajl_parse_complete(hand); +#else + stat = yajl_complete_parse(hand); +#endif + } else + stat = yajl_parse(hand, ubuffer, nbytes); + + if (stat != yajl_status_ok +#ifdef HAVE_YAJL1 + && stat != yajl_status_insufficient_data +#endif + ) + { + unsigned char *str = yajl_get_error(hand, 1, ubuffer, nbytes); + LogError(soundcloud_domain, (const char *)str); + yajl_free_error(hand, str); + break; + } + } + + mutex.unlock(); + delete input_stream; + + return 0; +} + +/** + * Parse a soundcloud:// URL and create a playlist. + * @param uri A soundcloud URL. Accepted forms: + * soundcloud://track/<track-id> + * soundcloud://playlist/<playlist-id> + * soundcloud://url/<url or path of soundcloud page> + */ +static SongEnumerator * +soundcloud_open_uri(const char *uri, Mutex &mutex, Cond &cond) +{ + assert(memcmp(uri, "soundcloud://", 13) == 0); + uri += 13; + + char *u = nullptr; + 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 (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 (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); + } + + if (u == nullptr) { + LogWarning(soundcloud_domain, "unknown soundcloud URI"); + return nullptr; + } + + struct parse_data data; + data.got_url = 0; + data.title = nullptr; + data.stream_url = nullptr; +#ifdef HAVE_YAJL1 + yajl_handle hand = yajl_alloc(&parse_callbacks, nullptr, nullptr, + &data); +#else + 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); + g_free(data.title); + g_free(data.stream_url); + + if (ret == -1) + return nullptr; + + data.songs.reverse(); + return new MemorySongEnumerator(std::move(data.songs)); +} + +static const char *const soundcloud_schemes[] = { + "soundcloud", + nullptr +}; + +const struct playlist_plugin soundcloud_playlist_plugin = { + "soundcloud", + + soundcloud_init, + nullptr, + soundcloud_open_uri, + nullptr, + + soundcloud_schemes, + nullptr, + nullptr, +}; + + diff --git a/src/playlist/plugins/SoundCloudPlaylistPlugin.hxx b/src/playlist/plugins/SoundCloudPlaylistPlugin.hxx new file mode 100644 index 000000000..b355b477a --- /dev/null +++ b/src/playlist/plugins/SoundCloudPlaylistPlugin.hxx @@ -0,0 +1,25 @@ +/* + * 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_SOUNDCLOUD_PLAYLIST_PLUGIN_HXX +#define MPD_SOUNDCLOUD_PLAYLIST_PLUGIN_HXX + +extern const struct playlist_plugin soundcloud_playlist_plugin; + +#endif diff --git a/src/playlist/plugins/XspfPlaylistPlugin.cxx b/src/playlist/plugins/XspfPlaylistPlugin.cxx new file mode 100644 index 000000000..5b6010b53 --- /dev/null +++ b/src/playlist/plugins/XspfPlaylistPlugin.cxx @@ -0,0 +1,234 @@ +/* + * 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 "XspfPlaylistPlugin.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 <string.h> + +static constexpr Domain xspf_domain("xspf"); + +/** + * This is the state object for the GLib XML parser. + */ +struct XspfParser { + /** + * 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, PLAYLIST, TRACKLIST, TRACK, + LOCATION, + } state; + + /** + * The current tag within the "track" element. This is only + * valid if state==TRACK. TAG_NUM_OF_ITEM_TYPES means there + * is no (known) tag. + */ + TagType tag_type; + + /** + * The current song URI. It is set by the "location" element. + */ + std::string location; + + TagBuilder tag_builder; + + XspfParser() + :state(ROOT) {} +}; + +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; + + switch (parser->state) { + case XspfParser::ROOT: + if (strcmp(element_name, "playlist") == 0) + parser->state = XspfParser::PLAYLIST; + + break; + + case XspfParser::PLAYLIST: + if (strcmp(element_name, "trackList") == 0) + parser->state = XspfParser::TRACKLIST; + + break; + + case XspfParser::TRACKLIST: + if (strcmp(element_name, "track") == 0) { + parser->state = XspfParser::TRACK; + parser->location.clear(); + parser->tag_type = TAG_NUM_OF_ITEM_TYPES; + } + + break; + + case XspfParser::TRACK: + if (strcmp(element_name, "location") == 0) + parser->state = XspfParser::LOCATION; + else if (strcmp(element_name, "title") == 0) + 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_type = TAG_ARTIST; + else if (strcmp(element_name, "annotation") == 0) + parser->tag_type = TAG_COMMENT; + else if (strcmp(element_name, "album") == 0) + parser->tag_type = TAG_ALBUM; + else if (strcmp(element_name, "trackNum") == 0) + parser->tag_type = TAG_TRACK; + + break; + + case XspfParser::LOCATION: + break; + } +} + +static void XMLCALL +xspf_end_element(void *user_data, const XML_Char *element_name) +{ + XspfParser *parser = (XspfParser *)user_data; + + switch (parser->state) { + case XspfParser::ROOT: + break; + + case XspfParser::PLAYLIST: + if (strcmp(element_name, "playlist") == 0) + parser->state = XspfParser::ROOT; + + break; + + case XspfParser::TRACKLIST: + if (strcmp(element_name, "tracklist") == 0) + parser->state = XspfParser::PLAYLIST; + + break; + + case XspfParser::TRACK: + if (strcmp(element_name, "track") == 0) { + if (!parser->location.empty()) + parser->songs.emplace_front(std::move(parser->location), + parser->tag_builder.Commit()); + + parser->state = XspfParser::TRACKLIST; + } else + parser->tag_type = TAG_NUM_OF_ITEM_TYPES; + + break; + + case XspfParser::LOCATION: + parser->state = XspfParser::TRACK; + break; + } +} + +static void XMLCALL +xspf_char_data(void *user_data, const XML_Char *s, int len) +{ + XspfParser *parser = (XspfParser *)user_data; + + switch (parser->state) { + case XspfParser::ROOT: + case XspfParser::PLAYLIST: + case XspfParser::TRACKLIST: + break; + + case XspfParser::TRACK: + 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: + parser->location.assign(s, len); + + break; + } +} + +/* + * The playlist object + * + */ + +static SongEnumerator * +xspf_open_stream(InputStream &is) +{ + XspfParser parser; + + { + 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; + } + } + + parser.songs.reverse(); + return new MemorySongEnumerator(std::move(parser.songs)); +} + +static const char *const xspf_suffixes[] = { + "xspf", + nullptr +}; + +static const char *const xspf_mime_types[] = { + "application/xspf+xml", + nullptr +}; + +const struct playlist_plugin xspf_playlist_plugin = { + "xspf", + + nullptr, + nullptr, + nullptr, + xspf_open_stream, + + nullptr, + xspf_suffixes, + xspf_mime_types, +}; diff --git a/src/playlist/plugins/XspfPlaylistPlugin.hxx b/src/playlist/plugins/XspfPlaylistPlugin.hxx new file mode 100644 index 000000000..6b08a6be6 --- /dev/null +++ b/src/playlist/plugins/XspfPlaylistPlugin.hxx @@ -0,0 +1,25 @@ +/* + * 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_XSPF_PLAYLIST_PLUGIN_HXX +#define MPD_XSPF_PLAYLIST_PLUGIN_HXX + +extern const struct playlist_plugin xspf_playlist_plugin; + +#endif |