XspfPlaylistPlugin.cxx 5.17 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2021 The Music Player Daemon Project
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
 * 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.
 */

20
#include "XspfPlaylistPlugin.hxx"
21 22
#include "../PlaylistPlugin.hxx"
#include "../MemorySongEnumerator.hxx"
23
#include "song/DetachedSong.hxx"
Max Kellermann's avatar
Max Kellermann committed
24
#include "input/InputStream.hxx"
25
#include "tag/Builder.hxx"
26
#include "tag/Table.hxx"
27
#include "util/StringView.hxx"
28
#include "lib/expat/ExpatParser.hxx"
29 30 31 32

#include <string.h>

/**
33
 * This is the state object for our XML parser.
34
 */
35
struct XspfParser {
36 37 38 39
	/**
	 * The list of songs (in reverse order because that's faster
	 * while adding).
	 */
40
	std::forward_list<DetachedSong> songs;
41 42 43 44 45 46

	/**
	 * The current position in the XML file.
	 */
	enum {
		ROOT, PLAYLIST, TRACKLIST, TRACK,
47
		TAG, LOCATION,
48
	} state = ROOT;
49 50 51 52 53 54

	/**
	 * 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.
	 */
55
	TagType tag_type;
56 57

	/**
58
	 * The current song URI.  It is set by the "location" element.
59
	 */
60
	std::string location;
61

62 63
	TagBuilder tag_builder;

64
	std::string value;
65 66
};

67 68 69 70 71 72 73 74 75 76 77
static constexpr struct tag_table xspf_tag_elements[] = {
	{ "title", TAG_TITLE },

	/* TAG_COMPOSER would be more correct according to the XSPF
	   spec */
	{ "creator", TAG_ARTIST },

	{ "annotation", TAG_COMMENT },
	{ "album", TAG_ALBUM },
	{ "trackNum", TAG_TRACK },
	{ nullptr, TAG_NUM_OF_ITEM_TYPES }
78 79
};

80 81
static void XMLCALL
xspf_start_element(void *user_data, const XML_Char *element_name,
Rosen Penev's avatar
Rosen Penev committed
82
		   [[maybe_unused]] const XML_Char **atts)
83
{
Max Kellermann's avatar
Max Kellermann committed
84
	auto *parser = (XspfParser *)user_data;
85
	parser->value.clear();
86 87

	switch (parser->state) {
88
	case XspfParser::ROOT:
89
		if (strcmp(element_name, "playlist") == 0)
90
			parser->state = XspfParser::PLAYLIST;
91 92 93

		break;

94
	case XspfParser::PLAYLIST:
95
		if (strcmp(element_name, "trackList") == 0)
96
			parser->state = XspfParser::TRACKLIST;
97 98 99

		break;

100
	case XspfParser::TRACKLIST:
101
		if (strcmp(element_name, "track") == 0) {
102
			parser->state = XspfParser::TRACK;
103
			parser->location.clear();
104 105 106 107
		}

		break;

108
	case XspfParser::TRACK:
109
		if (strcmp(element_name, "location") == 0)
110
			parser->state = XspfParser::LOCATION;
111
		else if (!parser->location.empty()) {
112 113
			parser->tag_type = tag_table_lookup(xspf_tag_elements,
							    element_name);
114 115 116
			if (parser->tag_type != TAG_NUM_OF_ITEM_TYPES)
				parser->state = XspfParser::TAG;
		}
117 118 119

		break;

120
	case XspfParser::TAG:
121
	case XspfParser::LOCATION:
122 123 124 125
		break;
	}
}

126 127
static void XMLCALL
xspf_end_element(void *user_data, const XML_Char *element_name)
128
{
Max Kellermann's avatar
Max Kellermann committed
129
	auto *parser = (XspfParser *)user_data;
130 131

	switch (parser->state) {
132
	case XspfParser::ROOT:
133 134
		break;

135
	case XspfParser::PLAYLIST:
136
		if (strcmp(element_name, "playlist") == 0)
137
			parser->state = XspfParser::ROOT;
138 139 140

		break;

141
	case XspfParser::TRACKLIST:
142
		if (strcmp(element_name, "tracklist") == 0)
143
			parser->state = XspfParser::PLAYLIST;
144 145 146

		break;

147
	case XspfParser::TRACK:
148
		if (strcmp(element_name, "track") == 0) {
149 150 151
			if (!parser->location.empty())
				parser->songs.emplace_front(std::move(parser->location),
							    parser->tag_builder.Commit());
152

153
			parser->state = XspfParser::TRACKLIST;
154
		}
155 156 157

		break;

158
	case XspfParser::TAG:
159 160 161 162 163 164 165 166
		if (!parser->value.empty())
			parser->tag_builder.AddItem(parser->tag_type,
						    StringView(parser->value.data(),
							       parser->value.length()));

		parser->state = XspfParser::TRACK;
		break;

167
	case XspfParser::LOCATION:
168
		parser->location = std::move(parser->value);
169
		parser->state = XspfParser::TRACK;
170 171
		break;
	}
172 173

	parser->value.clear();
174 175
}

176 177
static void XMLCALL
xspf_char_data(void *user_data, const XML_Char *s, int len)
178
{
Max Kellermann's avatar
Max Kellermann committed
179
	auto *parser = (XspfParser *)user_data;
180 181

	switch (parser->state) {
182 183 184 185
	case XspfParser::ROOT:
	case XspfParser::PLAYLIST:
	case XspfParser::TRACKLIST:
	case XspfParser::TRACK:
186 187
		break;

188
	case XspfParser::TAG:
189
	case XspfParser::LOCATION:
190
		parser->value.append(s, len);
191 192 193 194 195 196 197 198 199
		break;
	}
}

/*
 * The playlist object
 *
 */

200
static std::unique_ptr<SongEnumerator>
201
xspf_open_stream(InputStreamPtr &&is)
202
{
203
	XspfParser parser;
204

205 206 207 208
	{
		ExpatParser expat(&parser);
		expat.SetElementHandler(xspf_start_element, xspf_end_element);
		expat.SetCharacterDataHandler(xspf_char_data);
209
		expat.Parse(*is);
210 211
	}

212
	parser.songs.reverse();
213
	return std::make_unique<MemorySongEnumerator>(std::move(parser.songs));
214 215
}

216
static constexpr const char *xspf_suffixes[] = {
217
	"xspf",
218
	nullptr
219 220
};

221
static constexpr const char *xspf_mime_types[] = {
222
	"application/xspf+xml",
223
	nullptr
224 225
};

226 227 228 229
const PlaylistPlugin xspf_playlist_plugin =
	PlaylistPlugin("xspf", xspf_open_stream)
	.WithSuffixes(xspf_suffixes)
	.WithMimeTypes(xspf_mime_types);