playlist/pls: custom INI parser
Don't use GLib.
This commit is contained in:
parent
4b70f9d213
commit
aa4c7055f8
@ -1397,6 +1397,8 @@ libplaylist_plugins_a_SOURCES = \
|
|||||||
src/playlist/plugins/CuePlaylistPlugin.hxx \
|
src/playlist/plugins/CuePlaylistPlugin.hxx \
|
||||||
src/playlist/plugins/EmbeddedCuePlaylistPlugin.cxx \
|
src/playlist/plugins/EmbeddedCuePlaylistPlugin.cxx \
|
||||||
src/playlist/plugins/EmbeddedCuePlaylistPlugin.hxx \
|
src/playlist/plugins/EmbeddedCuePlaylistPlugin.hxx \
|
||||||
|
src/playlist/plugins/PlsPlaylistPlugin.cxx \
|
||||||
|
src/playlist/plugins/PlsPlaylistPlugin.hxx \
|
||||||
src/playlist/PlaylistRegistry.cxx src/playlist/PlaylistRegistry.hxx
|
src/playlist/PlaylistRegistry.cxx src/playlist/PlaylistRegistry.hxx
|
||||||
libplaylist_plugins_a_CPPFLAGS = $(AM_CPPFLAGS) \
|
libplaylist_plugins_a_CPPFLAGS = $(AM_CPPFLAGS) \
|
||||||
$(EXPAT_CFLAGS) \
|
$(EXPAT_CFLAGS) \
|
||||||
@ -1434,12 +1436,6 @@ libplaylist_plugins_a_SOURCES += \
|
|||||||
src/playlist/plugins/RssPlaylistPlugin.hxx
|
src/playlist/plugins/RssPlaylistPlugin.hxx
|
||||||
endif
|
endif
|
||||||
|
|
||||||
if HAVE_GLIB
|
|
||||||
libplaylist_plugins_a_SOURCES += \
|
|
||||||
src/playlist/plugins/PlsPlaylistPlugin.cxx \
|
|
||||||
src/playlist/plugins/PlsPlaylistPlugin.hxx
|
|
||||||
endif
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Filter plugins
|
# Filter plugins
|
||||||
#
|
#
|
||||||
|
@ -45,10 +45,7 @@
|
|||||||
const struct playlist_plugin *const playlist_plugins[] = {
|
const struct playlist_plugin *const playlist_plugins[] = {
|
||||||
&extm3u_playlist_plugin,
|
&extm3u_playlist_plugin,
|
||||||
&m3u_playlist_plugin,
|
&m3u_playlist_plugin,
|
||||||
#ifdef HAVE_GLIB
|
|
||||||
// TODO: enable without GLib
|
|
||||||
&pls_playlist_plugin,
|
&pls_playlist_plugin,
|
||||||
#endif
|
|
||||||
#ifdef ENABLE_EXPAT
|
#ifdef ENABLE_EXPAT
|
||||||
&xspf_playlist_plugin,
|
&xspf_playlist_plugin,
|
||||||
&asx_playlist_plugin,
|
&asx_playlist_plugin,
|
||||||
|
@ -21,121 +21,142 @@
|
|||||||
#include "PlsPlaylistPlugin.hxx"
|
#include "PlsPlaylistPlugin.hxx"
|
||||||
#include "../PlaylistPlugin.hxx"
|
#include "../PlaylistPlugin.hxx"
|
||||||
#include "../MemorySongEnumerator.hxx"
|
#include "../MemorySongEnumerator.hxx"
|
||||||
#include "input/InputStream.hxx"
|
#include "input/TextInputStream.hxx"
|
||||||
#include "DetachedSong.hxx"
|
#include "DetachedSong.hxx"
|
||||||
#include "tag/TagBuilder.hxx"
|
#include "tag/TagBuilder.hxx"
|
||||||
|
#include "util/ASCII.hxx"
|
||||||
|
#include "util/StringUtil.hxx"
|
||||||
|
#include "util/DivideString.hxx"
|
||||||
#include "util/Error.hxx"
|
#include "util/Error.hxx"
|
||||||
#include "util/Domain.hxx"
|
#include "util/Domain.hxx"
|
||||||
#include "Log.hxx"
|
|
||||||
|
|
||||||
#include <glib.h>
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
static constexpr Domain pls_domain("pls");
|
static constexpr Domain pls_domain("pls");
|
||||||
|
|
||||||
static void
|
static bool
|
||||||
pls_parser(GKeyFile *keyfile, std::forward_list<DetachedSong> &songs)
|
FindPlaylistSection(TextInputStream &is)
|
||||||
{
|
{
|
||||||
gchar *value;
|
char *line;
|
||||||
GError *error = nullptr;
|
while ((line = is.ReadLine()) != nullptr) {
|
||||||
int num_entries = g_key_file_get_integer(keyfile, "playlist",
|
line = Strip(line);
|
||||||
"NumberOfEntries", &error);
|
if (StringEqualsCaseASCII(line, "[playlist]"))
|
||||||
if (error) {
|
return true;
|
||||||
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 */
|
return false;
|
||||||
num_entries = g_key_file_get_integer(keyfile, "playlist",
|
}
|
||||||
"numberofentries", &error);
|
|
||||||
if (error) {
|
static bool
|
||||||
g_error_free(error);
|
ParsePls(TextInputStream &is, std::forward_list<DetachedSong> &songs)
|
||||||
error = nullptr;
|
{
|
||||||
|
assert(songs.empty());
|
||||||
|
|
||||||
|
if (!FindPlaylistSection(is))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
unsigned n_entries = 0;
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
std::string file, title;
|
||||||
|
int length;
|
||||||
|
|
||||||
|
Entry():length(-1) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr unsigned MAX_ENTRIES = 65536;
|
||||||
|
|
||||||
|
std::vector<Entry> entries;
|
||||||
|
|
||||||
|
char *line;
|
||||||
|
while ((line = is.ReadLine()) != nullptr) {
|
||||||
|
line = Strip(line);
|
||||||
|
|
||||||
|
if (*line == 0 || *line == ';')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (*line == '[')
|
||||||
|
/* another section starts; we only want
|
||||||
|
[Playlist], so stop here */
|
||||||
|
break;
|
||||||
|
|
||||||
|
const DivideString ds(line, '=', true);
|
||||||
|
if (!ds.IsDefined())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const char *const name = ds.GetFirst();
|
||||||
|
const char *const value = ds.GetSecond();
|
||||||
|
|
||||||
|
if (StringEqualsCaseASCII(name, "NumberOfEntries")) {
|
||||||
|
n_entries = strtoul(value, nullptr, 10);
|
||||||
|
if (n_entries == 0)
|
||||||
|
/* empty file - nothing remains to be
|
||||||
|
done */
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (n_entries > MAX_ENTRIES)
|
||||||
|
n_entries = MAX_ENTRIES;
|
||||||
|
entries.resize(n_entries);
|
||||||
|
} else if (StringEqualsCaseASCII(name, "File", 4)) {
|
||||||
|
unsigned i = strtoul(name + 4, nullptr, 10);
|
||||||
|
if (i >= 1 && i <= (n_entries > 0 ? n_entries : MAX_ENTRIES)) {
|
||||||
|
if (entries.size() < i)
|
||||||
|
entries.resize(i);
|
||||||
|
entries[i - 1].file = value;
|
||||||
|
}
|
||||||
|
} else if (StringEqualsCaseASCII(name, "Title", 5)) {
|
||||||
|
unsigned i = strtoul(name + 5, nullptr, 10);
|
||||||
|
if (i >= 1 && i <= (n_entries > 0 ? n_entries : MAX_ENTRIES)) {
|
||||||
|
if (entries.size() < i)
|
||||||
|
entries.resize(i);
|
||||||
|
entries[i - 1].title = value;
|
||||||
|
}
|
||||||
|
} else if (StringEqualsCaseASCII(name, "Length", 6)) {
|
||||||
|
unsigned i = strtoul(name + 6, nullptr, 10);
|
||||||
|
if (i >= 1 && i <= (n_entries > 0 ? n_entries : MAX_ENTRIES)) {
|
||||||
|
if (entries.size() < i)
|
||||||
|
entries.resize(i);
|
||||||
|
entries[i - 1].length = atoi(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (; num_entries > 0; --num_entries) {
|
if (n_entries == 0)
|
||||||
char key[64];
|
/* no "NumberOfEntries" found */
|
||||||
sprintf(key, "File%u", num_entries);
|
return false;
|
||||||
char *uri = g_key_file_get_string(keyfile, "playlist", key,
|
|
||||||
&error);
|
auto i = songs.before_begin();
|
||||||
if(error) {
|
for (const auto &entry : entries) {
|
||||||
FormatError(pls_domain, "Invalid PLS entry %s: '%s'",
|
const char *uri = entry.file.c_str();
|
||||||
key, error->message);
|
|
||||||
g_error_free(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TagBuilder tag;
|
TagBuilder tag;
|
||||||
|
if (!entry.title.empty())
|
||||||
|
tag.AddItem(TAG_TITLE, entry.title.c_str());
|
||||||
|
|
||||||
sprintf(key, "Title%u", num_entries);
|
if (entry.length > 0)
|
||||||
value = g_key_file_get_string(keyfile, "playlist", key,
|
tag.SetDuration(SignedSongTime::FromS(entry.length));
|
||||||
nullptr);
|
|
||||||
if (value != nullptr)
|
|
||||||
tag.AddItem(TAG_TITLE, value);
|
|
||||||
|
|
||||||
g_free(value);
|
i = songs.emplace_after(i, uri, tag.Commit());
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
ParsePls(InputStream &is, std::forward_list<DetachedSong> &songs)
|
||||||
|
{
|
||||||
|
TextInputStream tis(is);
|
||||||
|
return ParsePls(tis, songs);
|
||||||
}
|
}
|
||||||
|
|
||||||
static SongEnumerator *
|
static SongEnumerator *
|
||||||
pls_open_stream(InputStream &is)
|
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;
|
std::forward_list<DetachedSong> songs;
|
||||||
pls_parser(keyfile, songs);
|
if (!ParsePls(is, songs))
|
||||||
g_key_file_free(keyfile);
|
return nullptr;
|
||||||
|
|
||||||
return new MemorySongEnumerator(std::move(songs));
|
return new MemorySongEnumerator(std::move(songs));
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user