aboutsummaryrefslogtreecommitdiffstats
path: root/src/input/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'src/input/plugins')
-rw-r--r--src/input/plugins/AlsaInputPlugin.cxx431
-rw-r--r--src/input/plugins/AlsaInputPlugin.hxx28
-rw-r--r--src/input/plugins/ArchiveInputPlugin.cxx99
-rw-r--r--src/input/plugins/ArchiveInputPlugin.hxx25
-rw-r--r--src/input/plugins/CdioParanoiaInputPlugin.cxx407
-rw-r--r--src/input/plugins/CdioParanoiaInputPlugin.hxx28
-rw-r--r--src/input/plugins/CurlInputPlugin.cxx1170
-rw-r--r--src/input/plugins/CurlInputPlugin.hxx25
-rw-r--r--src/input/plugins/DespotifyInputPlugin.cxx262
-rw-r--r--src/input/plugins/DespotifyInputPlugin.hxx25
-rw-r--r--src/input/plugins/FfmpegInputPlugin.cxx177
-rw-r--r--src/input/plugins/FfmpegInputPlugin.hxx28
-rw-r--r--src/input/plugins/FileInputPlugin.cxx157
-rw-r--r--src/input/plugins/FileInputPlugin.hxx25
-rw-r--r--src/input/plugins/MmsInputPlugin.cxx127
-rw-r--r--src/input/plugins/MmsInputPlugin.hxx25
-rw-r--r--src/input/plugins/RewindInputPlugin.cxx251
-rw-r--r--src/input/plugins/RewindInputPlugin.hxx37
-rw-r--r--src/input/plugins/SmbclientInputPlugin.cxx203
-rw-r--r--src/input/plugins/SmbclientInputPlugin.hxx25
20 files changed, 3555 insertions, 0 deletions
diff --git a/src/input/plugins/AlsaInputPlugin.cxx b/src/input/plugins/AlsaInputPlugin.cxx
new file mode 100644
index 000000000..da2d9a926
--- /dev/null
+++ b/src/input/plugins/AlsaInputPlugin.cxx
@@ -0,0 +1,431 @@
+/*
+ * 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.
+ */
+
+/*
+ * ALSA code based on an example by Paul Davis released under GPL here:
+ * http://equalarea.com/paul/alsa-audio.html
+ * and one by Matthias Nagorni, also GPL, here:
+ * http://alsamodular.sourceforge.net/alsa_programming_howto.html
+ */
+
+#include "config.h"
+#include "AlsaInputPlugin.hxx"
+#include "../InputPlugin.hxx"
+#include "../InputStream.hxx"
+#include "util/Domain.hxx"
+#include "util/Error.hxx"
+#include "util/StringUtil.hxx"
+#include "util/ReusableArray.hxx"
+#include "util/Cast.hxx"
+#include "Log.hxx"
+#include "event/MultiSocketMonitor.hxx"
+#include "event/DeferredMonitor.hxx"
+#include "event/Call.hxx"
+#include "thread/Mutex.hxx"
+#include "thread/Cond.hxx"
+#include "IOThread.hxx"
+
+#include <alsa/asoundlib.h>
+
+#include <assert.h>
+#include <string.h>
+
+static constexpr Domain alsa_input_domain("alsa");
+
+static constexpr const char *default_device = "hw:0,0";
+
+// the following defaults are because the PcmDecoderPlugin forces CD format
+static constexpr snd_pcm_format_t default_format = SND_PCM_FORMAT_S16;
+static constexpr int default_channels = 2; // stereo
+static constexpr unsigned int default_rate = 44100; // cd quality
+
+/**
+ * This value should be the same as the read buffer size defined in
+ * PcmDecoderPlugin.cxx:pcm_stream_decode().
+ * We use it to calculate how many audio frames to buffer in the alsa driver
+ * before reading from the device. snd_pcm_readi() blocks until that many
+ * frames are ready.
+ */
+static constexpr size_t read_buffer_size = 4096;
+
+class AlsaInputStream final : MultiSocketMonitor, DeferredMonitor {
+ InputStream base;
+ snd_pcm_t *capture_handle;
+ size_t frame_size;
+ int frames_to_read;
+ bool eof;
+
+ /**
+ * Is somebody waiting for data? This is set by method
+ * Available().
+ */
+ std::atomic_bool waiting;
+
+ ReusableArray<pollfd> pfd_buffer;
+
+public:
+ AlsaInputStream(EventLoop &loop,
+ const char *uri, Mutex &mutex, Cond &cond,
+ snd_pcm_t *_handle, int _frame_size)
+ :MultiSocketMonitor(loop),
+ DeferredMonitor(loop),
+ base(input_plugin_alsa, uri, mutex, cond),
+ capture_handle(_handle),
+ frame_size(_frame_size),
+ eof(false)
+ {
+ assert(uri != nullptr);
+ assert(_handle != nullptr);
+
+ /* this mime type forces use of the PcmDecoderPlugin.
+ Needs to be generalised when/if that decoder is
+ updated to support other audio formats */
+ base.mime = "audio/x-mpd-cdda-pcm";
+ base.seekable = false;
+ base.size = -1;
+ base.ready = true;
+ frames_to_read = read_buffer_size / frame_size;
+
+ snd_pcm_start(capture_handle);
+
+ DeferredMonitor::Schedule();
+ }
+
+ ~AlsaInputStream() {
+ snd_pcm_close(capture_handle);
+ }
+
+ using DeferredMonitor::GetEventLoop;
+
+ static InputStream *Create(const char *uri, Mutex &mutex, Cond &cond,
+ Error &error);
+
+#if GCC_CHECK_VERSION(4,6) || defined(__clang__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Winvalid-offsetof"
+#endif
+
+ static constexpr AlsaInputStream *Cast(InputStream *is) {
+ return ContainerCast(is, AlsaInputStream, base);
+ }
+
+#if GCC_CHECK_VERSION(4,6) || defined(__clang__)
+#pragma GCC diagnostic pop
+#endif
+
+ bool Available() {
+ if (snd_pcm_avail(capture_handle) > frames_to_read)
+ return true;
+
+ if (!waiting.exchange(true))
+ SafeInvalidateSockets();
+
+ return false;
+ }
+
+ size_t Read(void *ptr, size_t size, Error &error);
+
+ bool IsEOF() {
+ return eof;
+ }
+
+private:
+ static snd_pcm_t *OpenDevice(const char *device, int rate,
+ snd_pcm_format_t format, int channels,
+ Error &error);
+
+ int Recover(int err);
+
+ void SafeInvalidateSockets() {
+ DeferredMonitor::Schedule();
+ }
+
+ virtual void RunDeferred() override {
+ InvalidateSockets();
+ }
+
+ virtual int PrepareSockets() override;
+ virtual void DispatchSockets() override;
+};
+
+inline InputStream *
+AlsaInputStream::Create(const char *uri, Mutex &mutex, Cond &cond,
+ Error &error)
+{
+ const char *const scheme = "alsa://";
+ if (!StringStartsWith(uri, scheme))
+ return nullptr;
+
+ const char *device = uri + strlen(scheme);
+ if (strlen(device) == 0)
+ device = default_device;
+
+ /* placeholders - eventually user-requested audio format will
+ be passed via the URI. For now we just force the
+ defaults */
+ int rate = default_rate;
+ snd_pcm_format_t format = default_format;
+ int channels = default_channels;
+
+ snd_pcm_t *handle = OpenDevice(device, rate, format, channels,
+ error);
+ if (handle == nullptr)
+ return nullptr;
+
+ int frame_size = snd_pcm_format_width(format) / 8 * channels;
+ AlsaInputStream *stream = new AlsaInputStream(io_thread_get(),
+ uri, mutex, cond,
+ handle, frame_size);
+ return &stream->base;
+}
+
+inline size_t
+AlsaInputStream::Read(void *ptr, size_t size, Error &error)
+{
+ assert(ptr != nullptr);
+
+ int num_frames = size / frame_size;
+ int ret;
+ while ((ret = snd_pcm_readi(capture_handle, ptr, num_frames)) < 0) {
+ if (Recover(ret) < 0) {
+ eof = true;
+ error.Format(alsa_input_domain,
+ "PCM error - stream aborted");
+ return 0;
+ }
+ }
+
+ size_t nbytes = ret * frame_size;
+ base.offset += nbytes;
+ return nbytes;
+}
+
+int
+AlsaInputStream::PrepareSockets()
+{
+ if (!waiting) {
+ ClearSocketList();
+ return -1;
+ }
+
+ int count = snd_pcm_poll_descriptors_count(capture_handle);
+ if (count < 0) {
+ ClearSocketList();
+ return -1;
+ }
+
+ struct pollfd *pfds = pfd_buffer.Get(count);
+
+ count = snd_pcm_poll_descriptors(capture_handle, pfds, count);
+ if (count < 0)
+ count = 0;
+
+ ReplaceSocketList(pfds, count);
+ return -1;
+}
+
+void
+AlsaInputStream::DispatchSockets()
+{
+ waiting = false;
+
+ const ScopeLock protect(base.mutex);
+ /* wake up the thread that is waiting for more data */
+ base.cond.broadcast();
+}
+
+inline int
+AlsaInputStream::Recover(int err)
+{
+ switch(err) {
+ case -EPIPE:
+ LogDebug(alsa_input_domain, "Buffer Overrun");
+ // drop through
+ case -ESTRPIPE:
+ case -EINTR:
+ err = snd_pcm_recover(capture_handle, err, 1);
+ break;
+ default:
+ // something broken somewhere, give up
+ err = -1;
+ }
+ return err;
+}
+
+inline snd_pcm_t *
+AlsaInputStream::OpenDevice(const char *device,
+ int rate, snd_pcm_format_t format, int channels,
+ Error &error)
+{
+ snd_pcm_t *capture_handle;
+ int err;
+ if ((err = snd_pcm_open(&capture_handle, device,
+ SND_PCM_STREAM_CAPTURE, 0)) < 0) {
+ error.Format(alsa_input_domain, "Failed to open device: %s (%s)", device, snd_strerror(err));
+ return nullptr;
+ }
+
+ snd_pcm_hw_params_t *hw_params;
+ if ((err = snd_pcm_hw_params_malloc(&hw_params)) < 0) {
+ error.Format(alsa_input_domain, "Cannot allocate hardware parameter structure (%s)", snd_strerror(err));
+ snd_pcm_close(capture_handle);
+ return nullptr;
+ }
+
+ if ((err = snd_pcm_hw_params_any(capture_handle, hw_params)) < 0) {
+ error.Format(alsa_input_domain, "Cannot initialize hardware parameter structure (%s)", snd_strerror(err));
+ snd_pcm_hw_params_free(hw_params);
+ snd_pcm_close(capture_handle);
+ return nullptr;
+ }
+
+ if ((err = snd_pcm_hw_params_set_access(capture_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
+ error.Format(alsa_input_domain, "Cannot set access type (%s)", snd_strerror (err));
+ snd_pcm_hw_params_free(hw_params);
+ snd_pcm_close(capture_handle);
+ return nullptr;
+ }
+
+ if ((err = snd_pcm_hw_params_set_format(capture_handle, hw_params, format)) < 0) {
+ snd_pcm_hw_params_free(hw_params);
+ snd_pcm_close(capture_handle);
+ error.Format(alsa_input_domain, "Cannot set sample format (%s)", snd_strerror (err));
+ return nullptr;
+ }
+
+ if ((err = snd_pcm_hw_params_set_channels(capture_handle, hw_params, channels)) < 0) {
+ snd_pcm_hw_params_free(hw_params);
+ snd_pcm_close(capture_handle);
+ error.Format(alsa_input_domain, "Cannot set channels (%s)", snd_strerror (err));
+ return nullptr;
+ }
+
+ if ((err = snd_pcm_hw_params_set_rate(capture_handle, hw_params, rate, 0)) < 0) {
+ snd_pcm_hw_params_free(hw_params);
+ snd_pcm_close(capture_handle);
+ error.Format(alsa_input_domain, "Cannot set sample rate (%s)", snd_strerror (err));
+ return nullptr;
+ }
+
+ /* period needs to be big enough so that poll() doesn't fire too often,
+ * but small enough that buffer overruns don't occur if Read() is not
+ * invoked often enough.
+ * the calculation here is empirical; however all measurements were
+ * done using 44100:16:2. When we extend this plugin to support
+ * other audio formats then this may need to be revisited */
+ snd_pcm_uframes_t period = read_buffer_size * 2;
+ int direction = -1;
+ if ((err = snd_pcm_hw_params_set_period_size_near(capture_handle, hw_params,
+ &period, &direction)) < 0) {
+ error.Format(alsa_input_domain, "Cannot set period size (%s)",
+ snd_strerror(err));
+ snd_pcm_hw_params_free(hw_params);
+ snd_pcm_close(capture_handle);
+ return nullptr;
+ }
+
+ if ((err = snd_pcm_hw_params(capture_handle, hw_params)) < 0) {
+ error.Format(alsa_input_domain, "Cannot set parameters (%s)",
+ snd_strerror(err));
+ snd_pcm_hw_params_free(hw_params);
+ snd_pcm_close(capture_handle);
+ return nullptr;
+ }
+
+ snd_pcm_hw_params_free (hw_params);
+
+ snd_pcm_sw_params_t *sw_params;
+
+ snd_pcm_sw_params_malloc(&sw_params);
+ snd_pcm_sw_params_current(capture_handle, sw_params);
+
+ if ((err = snd_pcm_sw_params_set_start_threshold(capture_handle, sw_params,
+ period)) < 0) {
+ error.Format(alsa_input_domain,
+ "unable to set start threshold (%s)", snd_strerror(err));
+ snd_pcm_sw_params_free(sw_params);
+ snd_pcm_close(capture_handle);
+ return nullptr;
+ }
+
+ if ((err = snd_pcm_sw_params(capture_handle, sw_params)) < 0) {
+ error.Format(alsa_input_domain,
+ "unable to install sw params (%s)", snd_strerror(err));
+ snd_pcm_sw_params_free(sw_params);
+ snd_pcm_close(capture_handle);
+ return nullptr;
+ }
+
+ snd_pcm_sw_params_free(sw_params);
+
+ snd_pcm_prepare(capture_handle);
+
+ return capture_handle;
+}
+
+/*######################### Plugin Functions ##############################*/
+
+static InputStream *
+alsa_input_open(const char *uri, Mutex &mutex, Cond &cond, Error &error)
+{
+ return AlsaInputStream::Create(uri, mutex, cond, error);
+}
+
+static void
+alsa_input_close(InputStream *is)
+{
+ AlsaInputStream *ais = AlsaInputStream::Cast(is);
+ delete ais;
+}
+
+static bool
+alsa_input_available(InputStream *is)
+{
+ AlsaInputStream *ais = AlsaInputStream::Cast(is);
+ return ais->Available();
+}
+
+static size_t
+alsa_input_read(InputStream *is, void *ptr, size_t size, Error &error)
+{
+ AlsaInputStream *ais = AlsaInputStream::Cast(is);
+ return ais->Read(ptr, size, error);
+}
+
+static bool
+alsa_input_eof(gcc_unused InputStream *is)
+{
+ AlsaInputStream *ais = AlsaInputStream::Cast(is);
+ return ais->IsEOF();
+}
+
+const struct InputPlugin input_plugin_alsa = {
+ "alsa",
+ nullptr,
+ nullptr,
+ alsa_input_open,
+ alsa_input_close,
+ nullptr,
+ nullptr,
+ nullptr,
+ alsa_input_available,
+ alsa_input_read,
+ alsa_input_eof,
+ nullptr,
+};
diff --git a/src/input/plugins/AlsaInputPlugin.hxx b/src/input/plugins/AlsaInputPlugin.hxx
new file mode 100644
index 000000000..dddf7dfd7
--- /dev/null
+++ b/src/input/plugins/AlsaInputPlugin.hxx
@@ -0,0 +1,28 @@
+/*
+ * 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_ALSA_INPUT_PLUGIN_HXX
+#define MPD_ALSA_INPUT_PLUGIN_HXX
+
+#include "../InputPlugin.hxx"
+
+extern const struct InputPlugin input_plugin_alsa;
+
+
+#endif
diff --git a/src/input/plugins/ArchiveInputPlugin.cxx b/src/input/plugins/ArchiveInputPlugin.cxx
new file mode 100644
index 000000000..df1a72585
--- /dev/null
+++ b/src/input/plugins/ArchiveInputPlugin.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 "ArchiveInputPlugin.hxx"
+#include "archive/ArchiveDomain.hxx"
+#include "archive/ArchiveLookup.hxx"
+#include "archive/ArchiveList.hxx"
+#include "archive/ArchivePlugin.hxx"
+#include "archive/ArchiveFile.hxx"
+#include "../InputPlugin.hxx"
+#include "fs/Traits.hxx"
+#include "util/Alloc.hxx"
+#include "Log.hxx"
+
+#include <stdlib.h>
+
+/**
+ * select correct archive plugin to handle the input stream
+ * may allow stacking of archive plugins. for example for handling
+ * tar.gz a gzip handler opens file (through inputfile stream)
+ * then it opens a tar handler and sets gzip inputstream as
+ * parent_stream so tar plugin fetches file data from gzip
+ * plugin and gzip fetches file from disk
+ */
+static InputStream *
+input_archive_open(const char *pathname,
+ Mutex &mutex, Cond &cond,
+ Error &error)
+{
+ const struct archive_plugin *arplug;
+ InputStream *is;
+
+ if (!PathTraitsFS::IsAbsolute(pathname))
+ return nullptr;
+
+ char *pname = strdup(pathname);
+ // archive_lookup will modify pname when true is returned
+ const char *archive, *filename, *suffix;
+ if (!archive_lookup(pname, &archive, &filename, &suffix)) {
+ FormatDebug(archive_domain,
+ "not an archive, lookup %s failed", pname);
+ free(pname);
+ return nullptr;
+ }
+
+ //check which archive plugin to use (by ext)
+ arplug = archive_plugin_from_suffix(suffix);
+ if (!arplug) {
+ FormatWarning(archive_domain,
+ "can't handle archive %s", archive);
+ free(pname);
+ return nullptr;
+ }
+
+ auto file = archive_file_open(arplug, archive, error);
+ if (file == nullptr) {
+ free(pname);
+ return nullptr;
+ }
+
+ //setup fileops
+ is = file->OpenStream(filename, mutex, cond, error);
+ free(pname);
+ file->Close();
+
+ return is;
+}
+
+const InputPlugin input_plugin_archive = {
+ "archive",
+ nullptr,
+ nullptr,
+ input_archive_open,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+};
diff --git a/src/input/plugins/ArchiveInputPlugin.hxx b/src/input/plugins/ArchiveInputPlugin.hxx
new file mode 100644
index 000000000..024723726
--- /dev/null
+++ b/src/input/plugins/ArchiveInputPlugin.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_INPUT_ARCHIVE_HXX
+#define MPD_INPUT_ARCHIVE_HXX
+
+extern const struct InputPlugin input_plugin_archive;
+
+#endif
diff --git a/src/input/plugins/CdioParanoiaInputPlugin.cxx b/src/input/plugins/CdioParanoiaInputPlugin.cxx
new file mode 100644
index 000000000..9267b50e1
--- /dev/null
+++ b/src/input/plugins/CdioParanoiaInputPlugin.cxx
@@ -0,0 +1,407 @@
+/*
+ * 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.
+ */
+
+/**
+ * CD-Audio handling (requires libcdio_paranoia)
+ */
+
+#include "config.h"
+#include "CdioParanoiaInputPlugin.hxx"
+#include "../InputStream.hxx"
+#include "../InputPlugin.hxx"
+#include "util/StringUtil.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "system/ByteOrder.hxx"
+#include "fs/AllocatedPath.hxx"
+#include "Log.hxx"
+#include "config/ConfigData.hxx"
+#include "config/ConfigError.hxx"
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <string.h>
+#include <stdlib.h>
+#include <glib.h>
+#include <assert.h>
+
+#ifdef HAVE_CDIO_PARANOIA_PARANOIA_H
+#include <cdio/paranoia/paranoia.h>
+#else
+#include <cdio/paranoia.h>
+#endif
+
+#include <cdio/cd_types.h>
+
+struct CdioParanoiaInputStream {
+ InputStream base;
+
+ cdrom_drive_t *drv;
+ CdIo_t *cdio;
+ cdrom_paranoia_t *para;
+
+ lsn_t lsn_from, lsn_to;
+ int lsn_relofs;
+
+ int trackno;
+
+ char buffer[CDIO_CD_FRAMESIZE_RAW];
+ int buffer_lsn;
+
+ CdioParanoiaInputStream(const char *uri, Mutex &mutex, Cond &cond,
+ int _trackno)
+ :base(input_plugin_cdio_paranoia, uri, mutex, cond),
+ drv(nullptr), cdio(nullptr), para(nullptr),
+ trackno(_trackno)
+ {
+ }
+
+ ~CdioParanoiaInputStream() {
+ if (para != nullptr)
+ cdio_paranoia_free(para);
+ if (drv != nullptr)
+ cdio_cddap_close_no_free_cdio(drv);
+ if (cdio != nullptr)
+ cdio_destroy(cdio);
+ }
+};
+
+static constexpr Domain cdio_domain("cdio");
+
+static bool default_reverse_endian;
+
+static bool
+input_cdio_init(const config_param &param, Error &error)
+{
+ const char *value = param.GetBlockValue("default_byte_order");
+ if (value != nullptr) {
+ if (strcmp(value, "little_endian") == 0)
+ default_reverse_endian = IsBigEndian();
+ else if (strcmp(value, "big_endian") == 0)
+ default_reverse_endian = IsLittleEndian();
+ else {
+ error.Format(config_domain, 0,
+ "Unrecognized 'default_byte_order' setting: %s",
+ value);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static void
+input_cdio_close(InputStream *is)
+{
+ CdioParanoiaInputStream *i = (CdioParanoiaInputStream *)is;
+
+ delete i;
+}
+
+struct cdio_uri {
+ char device[64];
+ int track;
+};
+
+static bool
+parse_cdio_uri(struct cdio_uri *dest, const char *src, Error &error)
+{
+ if (!StringStartsWith(src, "cdda://"))
+ return false;
+
+ src += 7;
+
+ if (*src == 0) {
+ /* play the whole CD in the default drive */
+ dest->device[0] = 0;
+ dest->track = -1;
+ return true;
+ }
+
+ const char *slash = strrchr(src, '/');
+ if (slash == nullptr) {
+ /* play the whole CD in the specified drive */
+ g_strlcpy(dest->device, src, sizeof(dest->device));
+ dest->track = -1;
+ return true;
+ }
+
+ size_t device_length = slash - src;
+ if (device_length >= sizeof(dest->device))
+ device_length = sizeof(dest->device) - 1;
+
+ memcpy(dest->device, src, device_length);
+ dest->device[device_length] = 0;
+
+ const char *track = slash + 1;
+
+ char *endptr;
+ dest->track = strtoul(track, &endptr, 10);
+ if (*endptr != 0) {
+ error.Set(cdio_domain, "Malformed track number");
+ return false;
+ }
+
+ if (endptr == track)
+ /* play the whole CD */
+ dest->track = -1;
+
+ return true;
+}
+
+static AllocatedPath
+cdio_detect_device(void)
+{
+ char **devices = cdio_get_devices_with_cap(nullptr, CDIO_FS_AUDIO,
+ false);
+ if (devices == nullptr)
+ return AllocatedPath::Null();
+
+ AllocatedPath path = AllocatedPath::FromFS(devices[0]);
+ cdio_free_device_list(devices);
+ return path;
+}
+
+static InputStream *
+input_cdio_open(const char *uri,
+ Mutex &mutex, Cond &cond,
+ Error &error)
+{
+ struct cdio_uri parsed_uri;
+ if (!parse_cdio_uri(&parsed_uri, uri, error))
+ return nullptr;
+
+ CdioParanoiaInputStream *i =
+ new CdioParanoiaInputStream(uri, mutex, cond,
+ parsed_uri.track);
+
+ /* get list of CD's supporting CD-DA */
+ const AllocatedPath device = parsed_uri.device[0] != 0
+ ? AllocatedPath::FromFS(parsed_uri.device)
+ : cdio_detect_device();
+ if (device.IsNull()) {
+ error.Set(cdio_domain,
+ "Unable find or access a CD-ROM drive with an audio CD in it.");
+ delete i;
+ return nullptr;
+ }
+
+ /* Found such a CD-ROM with a CD-DA loaded. Use the first drive in the list. */
+ i->cdio = cdio_open(device.c_str(), DRIVER_UNKNOWN);
+
+ i->drv = cdio_cddap_identify_cdio(i->cdio, 1, nullptr);
+
+ if ( !i->drv ) {
+ error.Set(cdio_domain, "Unable to identify audio CD disc.");
+ delete i;
+ return nullptr;
+ }
+
+ cdda_verbose_set(i->drv, CDDA_MESSAGE_FORGETIT, CDDA_MESSAGE_FORGETIT);
+
+ if ( 0 != cdio_cddap_open(i->drv) ) {
+ error.Set(cdio_domain, "Unable to open disc.");
+ delete i;
+ return nullptr;
+ }
+
+ bool reverse_endian;
+ switch (data_bigendianp(i->drv)) {
+ case -1:
+ LogDebug(cdio_domain, "drive returns unknown audio data");
+ reverse_endian = default_reverse_endian;
+ break;
+
+ case 0:
+ LogDebug(cdio_domain, "drive returns audio data Little Endian");
+ reverse_endian = IsBigEndian();
+ break;
+
+ case 1:
+ LogDebug(cdio_domain, "drive returns audio data Big Endian");
+ reverse_endian = IsLittleEndian();
+ break;
+
+ default:
+ error.Format(cdio_domain, "Drive returns unknown data type %d",
+ data_bigendianp(i->drv));
+ delete i;
+ return nullptr;
+ }
+
+ i->lsn_relofs = 0;
+
+ if (i->trackno >= 0) {
+ i->lsn_from = cdio_get_track_lsn(i->cdio, i->trackno);
+ i->lsn_to = cdio_get_track_last_lsn(i->cdio, i->trackno);
+ } else {
+ i->lsn_from = 0;
+ i->lsn_to = cdio_get_disc_last_lsn(i->cdio);
+ }
+
+ i->para = cdio_paranoia_init(i->drv);
+
+ /* Set reading mode for full paranoia, but allow skipping sectors. */
+ paranoia_modeset(i->para, PARANOIA_MODE_FULL^PARANOIA_MODE_NEVERSKIP);
+
+ /* seek to beginning of the track */
+ cdio_paranoia_seek(i->para, i->lsn_from, SEEK_SET);
+
+ i->base.ready = true;
+ i->base.seekable = true;
+ i->base.size = (i->lsn_to - i->lsn_from + 1) * CDIO_CD_FRAMESIZE_RAW;
+
+ /* hack to make MPD select the "pcm" decoder plugin */
+ i->base.mime = reverse_endian
+ ? "audio/x-mpd-cdda-pcm-reverse"
+ : "audio/x-mpd-cdda-pcm";
+
+ return &i->base;
+}
+
+static bool
+input_cdio_seek(InputStream *is,
+ InputPlugin::offset_type offset, int whence, Error &error)
+{
+ CdioParanoiaInputStream *cis = (CdioParanoiaInputStream *)is;
+
+ /* calculate absolute offset */
+ switch (whence) {
+ case SEEK_SET:
+ break;
+ case SEEK_CUR:
+ offset += cis->base.offset;
+ break;
+ case SEEK_END:
+ offset += cis->base.size;
+ break;
+ }
+
+ if (offset < 0 || offset > cis->base.size) {
+ error.Format(cdio_domain, "Invalid offset to seek %ld (%ld)",
+ (long int)offset, (long int)cis->base.size);
+ return false;
+ }
+
+ /* simple case */
+ if (offset == cis->base.offset)
+ return true;
+
+ /* calculate current LSN */
+ cis->lsn_relofs = offset / CDIO_CD_FRAMESIZE_RAW;
+ cis->base.offset = offset;
+
+ cdio_paranoia_seek(cis->para, cis->lsn_from + cis->lsn_relofs, SEEK_SET);
+
+ return true;
+}
+
+static size_t
+input_cdio_read(InputStream *is, void *ptr, size_t length,
+ Error &error)
+{
+ CdioParanoiaInputStream *cis = (CdioParanoiaInputStream *)is;
+ size_t nbytes = 0;
+ int diff;
+ size_t len, maxwrite;
+ int16_t *rbuf;
+ char *s_err, *s_mess;
+ char *wptr = (char *) ptr;
+
+ while (length > 0) {
+
+
+ /* end of track ? */
+ if (cis->lsn_from + cis->lsn_relofs > cis->lsn_to)
+ break;
+
+ //current sector was changed ?
+ if (cis->lsn_relofs != cis->buffer_lsn) {
+ rbuf = cdio_paranoia_read(cis->para, nullptr);
+
+ s_err = cdda_errors(cis->drv);
+ if (s_err) {
+ FormatError(cdio_domain,
+ "paranoia_read: %s", s_err);
+ free(s_err);
+ }
+ s_mess = cdda_messages(cis->drv);
+ if (s_mess) {
+ free(s_mess);
+ }
+ if (!rbuf) {
+ error.Set(cdio_domain,
+ "paranoia read error. Stopping.");
+ return 0;
+ }
+ //store current buffer
+ memcpy(cis->buffer, rbuf, CDIO_CD_FRAMESIZE_RAW);
+ cis->buffer_lsn = cis->lsn_relofs;
+ } else {
+ //use cached sector
+ rbuf = (int16_t*) cis->buffer;
+ }
+
+ //correct offset
+ diff = cis->base.offset - cis->lsn_relofs * CDIO_CD_FRAMESIZE_RAW;
+
+ assert(diff >= 0 && diff < CDIO_CD_FRAMESIZE_RAW);
+
+ maxwrite = CDIO_CD_FRAMESIZE_RAW - diff; //# of bytes pending in current buffer
+ len = (length < maxwrite? length : maxwrite);
+
+ //skip diff bytes from this lsn
+ memcpy(wptr, ((char*)rbuf) + diff, len);
+ //update pointer
+ wptr += len;
+ nbytes += len;
+
+ //update offset
+ cis->base.offset += len;
+ cis->lsn_relofs = cis->base.offset / CDIO_CD_FRAMESIZE_RAW;
+ //update length
+ length -= len;
+ }
+
+ return nbytes;
+}
+
+static bool
+input_cdio_eof(InputStream *is)
+{
+ CdioParanoiaInputStream *cis = (CdioParanoiaInputStream *)is;
+
+ return (cis->lsn_from + cis->lsn_relofs > cis->lsn_to);
+}
+
+const InputPlugin input_plugin_cdio_paranoia = {
+ "cdio_paranoia",
+ input_cdio_init,
+ nullptr,
+ input_cdio_open,
+ input_cdio_close,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ input_cdio_read,
+ input_cdio_eof,
+ input_cdio_seek,
+};
diff --git a/src/input/plugins/CdioParanoiaInputPlugin.hxx b/src/input/plugins/CdioParanoiaInputPlugin.hxx
new file mode 100644
index 000000000..e2804e8c7
--- /dev/null
+++ b/src/input/plugins/CdioParanoiaInputPlugin.hxx
@@ -0,0 +1,28 @@
+/*
+ * 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_CDIO_PARANOIA_INPUT_PLUGIN_HXX
+#define MPD_CDIO_PARANOIA_INPUT_PLUGIN_HXX
+
+/**
+ * An input plugin based on libcdio_paranoia library.
+ */
+extern const struct InputPlugin input_plugin_cdio_paranoia;
+
+#endif
diff --git a/src/input/plugins/CurlInputPlugin.cxx b/src/input/plugins/CurlInputPlugin.cxx
new file mode 100644
index 000000000..a3c6c1d1a
--- /dev/null
+++ b/src/input/plugins/CurlInputPlugin.cxx
@@ -0,0 +1,1170 @@
+/*
+ * 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 "CurlInputPlugin.hxx"
+#include "../InputStream.hxx"
+#include "../InputPlugin.hxx"
+#include "config/ConfigGlobal.hxx"
+#include "config/ConfigData.hxx"
+#include "tag/Tag.hxx"
+#include "tag/TagBuilder.hxx"
+#include "IcyMetaDataParser.hxx"
+#include "event/SocketMonitor.hxx"
+#include "event/TimeoutMonitor.hxx"
+#include "event/Call.hxx"
+#include "IOThread.hxx"
+#include "util/ASCII.hxx"
+#include "util/CharUtil.hxx"
+#include "util/NumberParser.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "Log.hxx"
+
+#include <assert.h>
+
+#if defined(WIN32)
+ #include <winsock2.h>
+#else
+ #include <sys/select.h>
+#endif
+
+#include <string.h>
+#include <errno.h>
+
+#include <list>
+
+#include <curl/curl.h>
+#include <glib.h>
+
+#if LIBCURL_VERSION_NUM < 0x071200
+#error libcurl is too old
+#endif
+
+/**
+ * Do not buffer more than this number of bytes. It should be a
+ * reasonable limit that doesn't make low-end machines suffer too
+ * much, but doesn't cause stuttering on high-latency lines.
+ */
+static const size_t CURL_MAX_BUFFERED = 512 * 1024;
+
+/**
+ * Resume the stream at this number of bytes after it has been paused.
+ */
+static const size_t CURL_RESUME_AT = 384 * 1024;
+
+/**
+ * Buffers created by input_curl_writefunction().
+ */
+class CurlInputBuffer {
+ /** size of the payload */
+ size_t size;
+
+ /** how much has been consumed yet? */
+ size_t consumed;
+
+ /** the payload */
+ uint8_t *data;
+
+public:
+ CurlInputBuffer(const void *_data, size_t _size)
+ :size(_size), consumed(0), data(new uint8_t[size]) {
+ memcpy(data, _data, size);
+ }
+
+ ~CurlInputBuffer() {
+ delete[] data;
+ }
+
+ CurlInputBuffer(const CurlInputBuffer &) = delete;
+ CurlInputBuffer &operator=(const CurlInputBuffer &) = delete;
+
+ const void *Begin() const {
+ return data + consumed;
+ }
+
+ size_t TotalSize() const {
+ return size;
+ }
+
+ size_t Available() const {
+ return size - consumed;
+ }
+
+ /**
+ * Mark a part of the buffer as consumed.
+ *
+ * @return false if the buffer is now empty
+ */
+ bool Consume(size_t length) {
+ assert(consumed < size);
+
+ consumed += length;
+ if (consumed < size)
+ return true;
+
+ assert(consumed == size);
+ return false;
+ }
+
+ bool Read(void *dest, size_t length) {
+ assert(consumed + length <= size);
+
+ memcpy(dest, data + consumed, length);
+ return Consume(length);
+ }
+};
+
+struct input_curl {
+ InputStream base;
+
+ /* some buffers which were passed to libcurl, which we have
+ too free */
+ char range[32];
+ struct curl_slist *request_headers;
+
+ /** the curl handles */
+ CURL *easy;
+
+ /** list of buffers, where input_curl_writefunction() appends
+ to, and input_curl_read() reads from them */
+ std::list<CurlInputBuffer> buffers;
+
+ /**
+ * Is the connection currently paused? That happens when the
+ * buffer was getting too large. It will be unpaused when the
+ * buffer is below the threshold again.
+ */
+ bool paused;
+
+ /** error message provided by libcurl */
+ char error[CURL_ERROR_SIZE];
+
+ /** parser for icy-metadata */
+ IcyMetaDataParser icy;
+
+ /** the stream name from the icy-name response header */
+ std::string meta_name;
+
+ /** the tag object ready to be requested via
+ InputStream::ReadTag() */
+ Tag *tag;
+
+ Error postponed_error;
+
+ input_curl(const char *url, Mutex &mutex, Cond &cond)
+ :base(input_plugin_curl, url, mutex, cond),
+ request_headers(nullptr),
+ paused(false),
+ tag(nullptr) {}
+
+ ~input_curl();
+
+ input_curl(const input_curl &) = delete;
+ input_curl &operator=(const input_curl &) = delete;
+};
+
+class CurlMulti;
+
+/**
+ * Monitor for one socket created by CURL.
+ */
+class CurlSocket final : SocketMonitor {
+ CurlMulti &multi;
+
+public:
+ CurlSocket(CurlMulti &_multi, EventLoop &_loop, int _fd)
+ :SocketMonitor(_fd, _loop), multi(_multi) {}
+
+ ~CurlSocket() {
+ /* TODO: sometimes, CURL uses CURL_POLL_REMOVE after
+ closing the socket, and sometimes, it uses
+ CURL_POLL_REMOVE just to move the (still open)
+ connection to the pool; in the first case,
+ Abandon() would be most appropriate, but it breaks
+ the second case - is that a CURL bug? is there a
+ better solution? */
+ }
+
+ /**
+ * Callback function for CURLMOPT_SOCKETFUNCTION.
+ */
+ static int SocketFunction(CURL *easy,
+ curl_socket_t s, int action,
+ void *userp, void *socketp);
+
+ virtual bool OnSocketReady(unsigned flags) override;
+
+private:
+ static constexpr int FlagsToCurlCSelect(unsigned flags) {
+ return (flags & (READ | HANGUP) ? CURL_CSELECT_IN : 0) |
+ (flags & WRITE ? CURL_CSELECT_OUT : 0) |
+ (flags & ERROR ? CURL_CSELECT_ERR : 0);
+ }
+
+ gcc_const
+ static unsigned CurlPollToFlags(int action) {
+ switch (action) {
+ case CURL_POLL_NONE:
+ return 0;
+
+ case CURL_POLL_IN:
+ return READ;
+
+ case CURL_POLL_OUT:
+ return WRITE;
+
+ case CURL_POLL_INOUT:
+ return READ|WRITE;
+ }
+
+ assert(false);
+ gcc_unreachable();
+ }
+};
+
+/**
+ * Manager for the global CURLM object.
+ */
+class CurlMulti final : private TimeoutMonitor {
+ CURLM *const multi;
+
+public:
+ CurlMulti(EventLoop &_loop, CURLM *_multi);
+
+ ~CurlMulti() {
+ curl_multi_cleanup(multi);
+ }
+
+ bool Add(input_curl *c, Error &error);
+ void Remove(input_curl *c);
+
+ /**
+ * Check for finished HTTP responses.
+ *
+ * Runs in the I/O thread. The caller must not hold locks.
+ */
+ void ReadInfo();
+
+ void Assign(curl_socket_t fd, CurlSocket &cs) {
+ curl_multi_assign(multi, fd, &cs);
+ }
+
+ void SocketAction(curl_socket_t fd, int ev_bitmask);
+
+ void InvalidateSockets() {
+ SocketAction(CURL_SOCKET_TIMEOUT, 0);
+ }
+
+ /**
+ * This is a kludge to allow pausing/resuming a stream with
+ * libcurl < 7.32.0. Read the curl_easy_pause manpage for
+ * more information.
+ */
+ void ResumeSockets() {
+ int running_handles;
+ curl_multi_socket_all(multi, &running_handles);
+ }
+
+private:
+ static int TimerFunction(CURLM *multi, long timeout_ms, void *userp);
+
+ virtual void OnTimeout() override;
+};
+
+/**
+ * libcurl version number encoded in a 24 bit integer.
+ */
+static unsigned curl_version_num;
+
+/** libcurl should accept "ICY 200 OK" */
+static struct curl_slist *http_200_aliases;
+
+/** HTTP proxy settings */
+static const char *proxy, *proxy_user, *proxy_password;
+static unsigned proxy_port;
+
+static CurlMulti *curl_multi;
+
+static constexpr Domain http_domain("http");
+static constexpr Domain curl_domain("curl");
+static constexpr Domain curlm_domain("curlm");
+
+CurlMulti::CurlMulti(EventLoop &_loop, CURLM *_multi)
+ :TimeoutMonitor(_loop), multi(_multi)
+{
+ curl_multi_setopt(multi, CURLMOPT_SOCKETFUNCTION,
+ CurlSocket::SocketFunction);
+ curl_multi_setopt(multi, CURLMOPT_SOCKETDATA, this);
+
+ curl_multi_setopt(multi, CURLMOPT_TIMERFUNCTION, TimerFunction);
+ curl_multi_setopt(multi, CURLMOPT_TIMERDATA, this);
+}
+
+/**
+ * Find a request by its CURL "easy" handle.
+ *
+ * Runs in the I/O thread. No lock needed.
+ */
+gcc_pure
+static struct input_curl *
+input_curl_find_request(CURL *easy)
+{
+ assert(io_thread_inside());
+
+ void *p;
+ CURLcode code = curl_easy_getinfo(easy, CURLINFO_PRIVATE, &p);
+ if (code != CURLE_OK)
+ return nullptr;
+
+ return (input_curl *)p;
+}
+
+static void
+input_curl_resume(struct input_curl *c)
+{
+ assert(io_thread_inside());
+
+ if (c->paused) {
+ c->paused = false;
+ curl_easy_pause(c->easy, CURLPAUSE_CONT);
+
+ if (curl_version_num < 0x072000)
+ /* libcurl older than 7.32.0 does not update
+ its sockets after curl_easy_pause(); force
+ libcurl to do it now */
+ curl_multi->ResumeSockets();
+
+ curl_multi->InvalidateSockets();
+ }
+}
+
+int
+CurlSocket::SocketFunction(gcc_unused CURL *easy,
+ curl_socket_t s, int action,
+ void *userp, void *socketp) {
+ CurlMulti &multi = *(CurlMulti *)userp;
+ CurlSocket *cs = (CurlSocket *)socketp;
+
+ assert(io_thread_inside());
+
+ if (action == CURL_POLL_REMOVE) {
+ delete cs;
+ return 0;
+ }
+
+ if (cs == nullptr) {
+ cs = new CurlSocket(multi, io_thread_get(), s);
+ multi.Assign(s, *cs);
+ } else {
+#ifdef USE_EPOLL
+ /* when using epoll, we need to unregister the socket
+ each time this callback is invoked, because older
+ CURL versions may omit the CURL_POLL_REMOVE call
+ when the socket has been closed and recreated with
+ the same file number (bug found in CURL 7.26, CURL
+ 7.33 not affected); in that case, epoll refuses the
+ EPOLL_CTL_MOD because it does not know the new
+ socket yet */
+ cs->Cancel();
+#endif
+ }
+
+ unsigned flags = CurlPollToFlags(action);
+ if (flags != 0)
+ cs->Schedule(flags);
+ return 0;
+}
+
+bool
+CurlSocket::OnSocketReady(unsigned flags)
+{
+ assert(io_thread_inside());
+
+ multi.SocketAction(Get(), FlagsToCurlCSelect(flags));
+ return true;
+}
+
+/**
+ * Runs in the I/O thread. No lock needed.
+ */
+inline bool
+CurlMulti::Add(struct input_curl *c, Error &error)
+{
+ assert(io_thread_inside());
+ assert(c != nullptr);
+ assert(c->easy != nullptr);
+
+ CURLMcode mcode = curl_multi_add_handle(multi, c->easy);
+ if (mcode != CURLM_OK) {
+ error.Format(curlm_domain, mcode,
+ "curl_multi_add_handle() failed: %s",
+ curl_multi_strerror(mcode));
+ return false;
+ }
+
+ InvalidateSockets();
+ return true;
+}
+
+/**
+ * Call input_curl_easy_add() in the I/O thread. May be called from
+ * any thread. Caller must not hold a mutex.
+ */
+static bool
+input_curl_easy_add_indirect(struct input_curl *c, Error &error)
+{
+ assert(c != nullptr);
+ assert(c->easy != nullptr);
+
+ bool result;
+ BlockingCall(io_thread_get(), [c, &error, &result](){
+ result = curl_multi->Add(c, error);
+ });
+ return result;
+}
+
+inline void
+CurlMulti::Remove(input_curl *c)
+{
+ curl_multi_remove_handle(multi, c->easy);
+}
+
+/**
+ * Frees the current "libcurl easy" handle, and everything associated
+ * with it.
+ *
+ * Runs in the I/O thread.
+ */
+static void
+input_curl_easy_free(struct input_curl *c)
+{
+ assert(io_thread_inside());
+ assert(c != nullptr);
+
+ if (c->easy == nullptr)
+ return;
+
+ curl_multi->Remove(c);
+
+ curl_easy_cleanup(c->easy);
+ c->easy = nullptr;
+
+ curl_slist_free_all(c->request_headers);
+ c->request_headers = nullptr;
+}
+
+/**
+ * Frees the current "libcurl easy" handle, and everything associated
+ * with it.
+ *
+ * The mutex must not be locked.
+ */
+static void
+input_curl_easy_free_indirect(struct input_curl *c)
+{
+ BlockingCall(io_thread_get(), [c](){
+ input_curl_easy_free(c);
+ curl_multi->InvalidateSockets();
+ });
+
+ assert(c->easy == nullptr);
+}
+
+/**
+ * A HTTP request is finished.
+ *
+ * Runs in the I/O thread. The caller must not hold locks.
+ */
+static void
+input_curl_request_done(struct input_curl *c, CURLcode result, long status)
+{
+ assert(io_thread_inside());
+ assert(c != nullptr);
+ assert(c->easy == nullptr);
+ assert(!c->postponed_error.IsDefined());
+
+ const ScopeLock protect(c->base.mutex);
+
+ if (result != CURLE_OK) {
+ c->postponed_error.Format(curl_domain, result,
+ "curl failed: %s", c->error);
+ } else if (status < 200 || status >= 300) {
+ c->postponed_error.Format(http_domain, status,
+ "got HTTP status %ld",
+ status);
+ }
+
+ c->base.ready = true;
+
+ c->base.cond.broadcast();
+}
+
+static void
+input_curl_handle_done(CURL *easy_handle, CURLcode result)
+{
+ struct input_curl *c = input_curl_find_request(easy_handle);
+ assert(c != nullptr);
+
+ long status = 0;
+ curl_easy_getinfo(easy_handle, CURLINFO_RESPONSE_CODE, &status);
+
+ input_curl_easy_free(c);
+ input_curl_request_done(c, result, status);
+}
+
+void
+CurlMulti::SocketAction(curl_socket_t fd, int ev_bitmask)
+{
+ int running_handles;
+ CURLMcode mcode = curl_multi_socket_action(multi, fd, ev_bitmask,
+ &running_handles);
+ if (mcode != CURLM_OK)
+ FormatError(curlm_domain,
+ "curl_multi_socket_action() failed: %s",
+ curl_multi_strerror(mcode));
+
+ ReadInfo();
+}
+
+/**
+ * Check for finished HTTP responses.
+ *
+ * Runs in the I/O thread. The caller must not hold locks.
+ */
+inline void
+CurlMulti::ReadInfo()
+{
+ assert(io_thread_inside());
+
+ CURLMsg *msg;
+ int msgs_in_queue;
+
+ while ((msg = curl_multi_info_read(multi,
+ &msgs_in_queue)) != nullptr) {
+ if (msg->msg == CURLMSG_DONE)
+ input_curl_handle_done(msg->easy_handle, msg->data.result);
+ }
+}
+
+int
+CurlMulti::TimerFunction(gcc_unused CURLM *_multi, long timeout_ms, void *userp)
+{
+ CurlMulti &multi = *(CurlMulti *)userp;
+ assert(_multi == multi.multi);
+
+ if (timeout_ms < 0) {
+ multi.Cancel();
+ return 0;
+ }
+
+ if (timeout_ms >= 0 && timeout_ms < 10)
+ /* CURL 7.21.1 likes to report "timeout=0", which
+ means we're running in a busy loop. Quite a bad
+ idea to waste so much CPU. Let's use a lower limit
+ of 10ms. */
+ timeout_ms = 10;
+
+ multi.Schedule(timeout_ms);
+ return 0;
+}
+
+void
+CurlMulti::OnTimeout()
+{
+ SocketAction(CURL_SOCKET_TIMEOUT, 0);
+}
+
+/*
+ * InputPlugin methods
+ *
+ */
+
+static bool
+input_curl_init(const config_param &param, Error &error)
+{
+ CURLcode code = curl_global_init(CURL_GLOBAL_ALL);
+ if (code != CURLE_OK) {
+ error.Format(curl_domain, code,
+ "curl_global_init() failed: %s",
+ curl_easy_strerror(code));
+ return false;
+ }
+
+ const auto version_info = curl_version_info(CURLVERSION_FIRST);
+ if (version_info != nullptr) {
+ FormatDebug(curl_domain, "version %s", version_info->version);
+ if (version_info->features & CURL_VERSION_SSL)
+ FormatDebug(curl_domain, "with %s",
+ version_info->ssl_version);
+
+ curl_version_num = version_info->version_num;
+ }
+
+ http_200_aliases = curl_slist_append(http_200_aliases, "ICY 200 OK");
+
+ proxy = param.GetBlockValue("proxy");
+ proxy_port = param.GetBlockValue("proxy_port", 0u);
+ proxy_user = param.GetBlockValue("proxy_user");
+ proxy_password = param.GetBlockValue("proxy_password");
+
+ if (proxy == nullptr) {
+ /* deprecated proxy configuration */
+ proxy = config_get_string(CONF_HTTP_PROXY_HOST, nullptr);
+ proxy_port = config_get_positive(CONF_HTTP_PROXY_PORT, 0);
+ proxy_user = config_get_string(CONF_HTTP_PROXY_USER, nullptr);
+ proxy_password = config_get_string(CONF_HTTP_PROXY_PASSWORD,
+ "");
+ }
+
+ CURLM *multi = curl_multi_init();
+ if (multi == nullptr) {
+ error.Set(curl_domain, 0, "curl_multi_init() failed");
+ return false;
+ }
+
+ curl_multi = new CurlMulti(io_thread_get(), multi);
+ return true;
+}
+
+static void
+input_curl_finish(void)
+{
+ BlockingCall(io_thread_get(), [](){
+ delete curl_multi;
+ });
+
+ curl_slist_free_all(http_200_aliases);
+
+ curl_global_cleanup();
+}
+
+/**
+ * Determine the total sizes of all buffers, including portions that
+ * have already been consumed.
+ *
+ * The caller must lock the mutex.
+ */
+gcc_pure
+static size_t
+curl_total_buffer_size(const struct input_curl *c)
+{
+ size_t total = 0;
+
+ for (const auto &i : c->buffers)
+ total += i.TotalSize();
+
+ return total;
+}
+
+input_curl::~input_curl()
+{
+ delete tag;
+
+ input_curl_easy_free_indirect(this);
+}
+
+static bool
+input_curl_check(InputStream *is, Error &error)
+{
+ struct input_curl *c = (struct input_curl *)is;
+
+ bool success = !c->postponed_error.IsDefined();
+ if (!success) {
+ error = std::move(c->postponed_error);
+ c->postponed_error.Clear();
+ }
+
+ return success;
+}
+
+static Tag *
+input_curl_tag(InputStream *is)
+{
+ struct input_curl *c = (struct input_curl *)is;
+ Tag *tag = c->tag;
+
+ c->tag = nullptr;
+ return tag;
+}
+
+static bool
+fill_buffer(struct input_curl *c, Error &error)
+{
+ while (c->easy != nullptr && c->buffers.empty())
+ c->base.cond.wait(c->base.mutex);
+
+ if (c->postponed_error.IsDefined()) {
+ error = std::move(c->postponed_error);
+ c->postponed_error.Clear();
+ return false;
+ }
+
+ return !c->buffers.empty();
+}
+
+static size_t
+read_from_buffer(IcyMetaDataParser &icy, std::list<CurlInputBuffer> &buffers,
+ void *dest0, size_t length)
+{
+ auto &buffer = buffers.front();
+ uint8_t *dest = (uint8_t *)dest0;
+ size_t nbytes = 0;
+
+ if (length > buffer.Available())
+ length = buffer.Available();
+
+ while (true) {
+ size_t chunk;
+
+ chunk = icy.Data(length);
+ if (chunk > 0) {
+ const bool empty = !buffer.Read(dest, chunk);
+
+ nbytes += chunk;
+ dest += chunk;
+ length -= chunk;
+
+ if (empty) {
+ buffers.pop_front();
+ break;
+ }
+
+ if (length == 0)
+ break;
+ }
+
+ chunk = icy.Meta(buffer.Begin(), length);
+ if (chunk > 0) {
+ const bool empty = !buffer.Consume(chunk);
+
+ length -= chunk;
+
+ if (empty) {
+ buffers.pop_front();
+ break;
+ }
+
+ if (length == 0)
+ break;
+ }
+ }
+
+ return nbytes;
+}
+
+static void
+copy_icy_tag(struct input_curl *c)
+{
+ Tag *tag = c->icy.ReadTag();
+
+ if (tag == nullptr)
+ return;
+
+ delete c->tag;
+
+ if (!c->meta_name.empty() && !tag->HasType(TAG_NAME)) {
+ TagBuilder tag_builder(std::move(*tag));
+ tag_builder.AddItem(TAG_NAME, c->meta_name.c_str());
+ *tag = tag_builder.Commit();
+ }
+
+ c->tag = tag;
+}
+
+static bool
+input_curl_available(InputStream *is)
+{
+ struct input_curl *c = (struct input_curl *)is;
+
+ return c->postponed_error.IsDefined() || c->easy == nullptr ||
+ !c->buffers.empty();
+}
+
+static size_t
+input_curl_read(InputStream *is, void *ptr, size_t size,
+ Error &error)
+{
+ struct input_curl *c = (struct input_curl *)is;
+ bool success;
+ size_t nbytes = 0;
+ char *dest = (char *)ptr;
+
+ do {
+ /* fill the buffer */
+
+ success = fill_buffer(c, error);
+ if (!success)
+ return 0;
+
+ /* send buffer contents */
+
+ while (size > 0 && !c->buffers.empty()) {
+ size_t copy = read_from_buffer(c->icy, c->buffers,
+ dest + nbytes, size);
+
+ nbytes += copy;
+ size -= copy;
+ }
+ } while (nbytes == 0);
+
+ if (c->icy.IsDefined())
+ copy_icy_tag(c);
+
+ is->offset += (InputPlugin::offset_type)nbytes;
+
+ if (c->paused && curl_total_buffer_size(c) < CURL_RESUME_AT) {
+ c->base.mutex.unlock();
+
+ BlockingCall(io_thread_get(), [c](){
+ input_curl_resume(c);
+ });
+
+ c->base.mutex.lock();
+ }
+
+ return nbytes;
+}
+
+static void
+input_curl_close(InputStream *is)
+{
+ struct input_curl *c = (struct input_curl *)is;
+
+ delete c;
+}
+
+static bool
+input_curl_eof(gcc_unused InputStream *is)
+{
+ struct input_curl *c = (struct input_curl *)is;
+
+ return c->easy == nullptr && c->buffers.empty();
+}
+
+/** called by curl when new data is available */
+static size_t
+input_curl_headerfunction(void *ptr, size_t size, size_t nmemb, void *stream)
+{
+ struct input_curl *c = (struct input_curl *)stream;
+ char name[64];
+
+ size *= nmemb;
+
+ const char *header = (const char *)ptr;
+ const char *end = header + size;
+
+ const char *value = (const char *)memchr(header, ':', size);
+ if (value == nullptr || (size_t)(value - header) >= sizeof(name))
+ return size;
+
+ memcpy(name, header, value - header);
+ name[value - header] = 0;
+
+ /* skip the colon */
+
+ ++value;
+
+ /* strip the value */
+
+ while (value < end && IsWhitespaceOrNull(*value))
+ ++value;
+
+ while (end > value && IsWhitespaceOrNull(end[-1]))
+ --end;
+
+ if (StringEqualsCaseASCII(name, "accept-ranges")) {
+ /* a stream with icy-metadata is not seekable */
+ if (!c->icy.IsDefined())
+ c->base.seekable = true;
+ } else if (StringEqualsCaseASCII(name, "content-length")) {
+ char buffer[64];
+
+ if ((size_t)(end - header) >= sizeof(buffer))
+ return size;
+
+ memcpy(buffer, value, end - value);
+ buffer[end - value] = 0;
+
+ c->base.size = c->base.offset + ParseUint64(buffer);
+ } else if (StringEqualsCaseASCII(name, "content-type")) {
+ c->base.mime.assign(value, end);
+ } else if (StringEqualsCaseASCII(name, "icy-name") ||
+ StringEqualsCaseASCII(name, "ice-name") ||
+ StringEqualsCaseASCII(name, "x-audiocast-name")) {
+ c->meta_name.assign(value, end);
+
+ delete c->tag;
+
+ TagBuilder tag_builder;
+ tag_builder.AddItem(TAG_NAME, c->meta_name.c_str());
+
+ c->tag = tag_builder.CommitNew();
+ } else if (StringEqualsCaseASCII(name, "icy-metaint")) {
+ char buffer[64];
+ size_t icy_metaint;
+
+ if ((size_t)(end - header) >= sizeof(buffer) ||
+ c->icy.IsDefined())
+ return size;
+
+ memcpy(buffer, value, end - value);
+ buffer[end - value] = 0;
+
+ icy_metaint = ParseUint64(buffer);
+ FormatDebug(curl_domain, "icy-metaint=%zu", icy_metaint);
+
+ if (icy_metaint > 0) {
+ c->icy.Start(icy_metaint);
+
+ /* a stream with icy-metadata is not
+ seekable */
+ c->base.seekable = false;
+ }
+ }
+
+ return size;
+}
+
+/** called by curl when new data is available */
+static size_t
+input_curl_writefunction(void *ptr, size_t size, size_t nmemb, void *stream)
+{
+ struct input_curl *c = (struct input_curl *)stream;
+
+ size *= nmemb;
+ if (size == 0)
+ return 0;
+
+ const ScopeLock protect(c->base.mutex);
+
+ if (curl_total_buffer_size(c) + size >= CURL_MAX_BUFFERED) {
+ c->paused = true;
+ return CURL_WRITEFUNC_PAUSE;
+ }
+
+ c->buffers.emplace_back(ptr, size);
+ c->base.ready = true;
+
+ c->base.cond.broadcast();
+ return size;
+}
+
+static bool
+input_curl_easy_init(struct input_curl *c, Error &error)
+{
+ CURLcode code;
+
+ c->easy = curl_easy_init();
+ if (c->easy == nullptr) {
+ error.Set(curl_domain, "curl_easy_init() failed");
+ return false;
+ }
+
+ curl_easy_setopt(c->easy, CURLOPT_PRIVATE, (void *)c);
+ curl_easy_setopt(c->easy, CURLOPT_USERAGENT,
+ "Music Player Daemon " VERSION);
+ curl_easy_setopt(c->easy, CURLOPT_HEADERFUNCTION,
+ input_curl_headerfunction);
+ curl_easy_setopt(c->easy, CURLOPT_WRITEHEADER, c);
+ curl_easy_setopt(c->easy, CURLOPT_WRITEFUNCTION,
+ input_curl_writefunction);
+ curl_easy_setopt(c->easy, CURLOPT_WRITEDATA, c);
+ curl_easy_setopt(c->easy, CURLOPT_HTTP200ALIASES, http_200_aliases);
+ curl_easy_setopt(c->easy, CURLOPT_FOLLOWLOCATION, 1);
+ curl_easy_setopt(c->easy, CURLOPT_NETRC, 1);
+ curl_easy_setopt(c->easy, CURLOPT_MAXREDIRS, 5);
+ curl_easy_setopt(c->easy, CURLOPT_FAILONERROR, true);
+ curl_easy_setopt(c->easy, CURLOPT_ERRORBUFFER, c->error);
+ curl_easy_setopt(c->easy, CURLOPT_NOPROGRESS, 1l);
+ curl_easy_setopt(c->easy, CURLOPT_NOSIGNAL, 1l);
+ curl_easy_setopt(c->easy, CURLOPT_CONNECTTIMEOUT, 10l);
+
+ if (proxy != nullptr)
+ curl_easy_setopt(c->easy, CURLOPT_PROXY, proxy);
+
+ if (proxy_port > 0)
+ curl_easy_setopt(c->easy, CURLOPT_PROXYPORT, (long)proxy_port);
+
+ if (proxy_user != nullptr && proxy_password != nullptr) {
+ char proxy_auth_str[1024];
+ snprintf(proxy_auth_str, sizeof(proxy_auth_str),
+ "%s:%s",
+ proxy_user, proxy_password);
+ curl_easy_setopt(c->easy, CURLOPT_PROXYUSERPWD, proxy_auth_str);
+ }
+
+ code = curl_easy_setopt(c->easy, CURLOPT_URL, c->base.uri.c_str());
+ if (code != CURLE_OK) {
+ error.Format(curl_domain, code,
+ "curl_easy_setopt() failed: %s",
+ curl_easy_strerror(code));
+ return false;
+ }
+
+ c->request_headers = nullptr;
+ c->request_headers = curl_slist_append(c->request_headers,
+ "Icy-Metadata: 1");
+ curl_easy_setopt(c->easy, CURLOPT_HTTPHEADER, c->request_headers);
+
+ return true;
+}
+
+static bool
+input_curl_seek(InputStream *is, InputPlugin::offset_type offset,
+ int whence,
+ Error &error)
+{
+ struct input_curl *c = (struct input_curl *)is;
+ bool ret;
+
+ assert(is->ready);
+
+ if (whence == SEEK_SET && offset == is->offset)
+ /* no-op */
+ return true;
+
+ if (!is->seekable)
+ return false;
+
+ /* calculate the absolute offset */
+
+ switch (whence) {
+ case SEEK_SET:
+ break;
+
+ case SEEK_CUR:
+ offset += is->offset;
+ break;
+
+ case SEEK_END:
+ if (is->size < 0)
+ /* stream size is not known */
+ return false;
+
+ offset += is->size;
+ break;
+
+ default:
+ return false;
+ }
+
+ if (offset < 0)
+ return false;
+
+ /* check if we can fast-forward the buffer */
+
+ while (offset > is->offset && !c->buffers.empty()) {
+ auto &buffer = c->buffers.front();
+ size_t length = buffer.Available();
+ if (offset - is->offset < (InputPlugin::offset_type)length)
+ length = offset - is->offset;
+
+ const bool empty = !buffer.Consume(length);
+ if (empty)
+ c->buffers.pop_front();
+
+ is->offset += length;
+ }
+
+ if (offset == is->offset)
+ return true;
+
+ /* close the old connection and open a new one */
+
+ c->base.mutex.unlock();
+
+ input_curl_easy_free_indirect(c);
+ c->buffers.clear();
+
+ is->offset = offset;
+ if (is->offset == is->size) {
+ /* seek to EOF: simulate empty result; avoid
+ triggering a "416 Requested Range Not Satisfiable"
+ response */
+ return true;
+ }
+
+ ret = input_curl_easy_init(c, error);
+ if (!ret)
+ return false;
+
+ /* send the "Range" header */
+
+ if (is->offset > 0) {
+ sprintf(c->range, "%lld-", (long long)is->offset);
+ curl_easy_setopt(c->easy, CURLOPT_RANGE, c->range);
+ }
+
+ c->base.ready = false;
+
+ if (!input_curl_easy_add_indirect(c, error))
+ return false;
+
+ c->base.mutex.lock();
+
+ while (!c->base.ready)
+ c->base.cond.wait(c->base.mutex);
+
+ if (c->postponed_error.IsDefined()) {
+ error = std::move(c->postponed_error);
+ c->postponed_error.Clear();
+ return false;
+ }
+
+ return true;
+}
+
+static InputStream *
+input_curl_open(const char *url, Mutex &mutex, Cond &cond,
+ Error &error)
+{
+ if (memcmp(url, "http://", 7) != 0 &&
+ memcmp(url, "https://", 8) != 0)
+ return nullptr;
+
+ struct input_curl *c = new input_curl(url, mutex, cond);
+
+ if (!input_curl_easy_init(c, error)) {
+ delete c;
+ return nullptr;
+ }
+
+ if (!input_curl_easy_add_indirect(c, error)) {
+ delete c;
+ return nullptr;
+ }
+
+ return &c->base;
+}
+
+const struct InputPlugin input_plugin_curl = {
+ "curl",
+ input_curl_init,
+ input_curl_finish,
+ input_curl_open,
+ input_curl_close,
+ input_curl_check,
+ nullptr,
+ input_curl_tag,
+ input_curl_available,
+ input_curl_read,
+ input_curl_eof,
+ input_curl_seek,
+};
diff --git a/src/input/plugins/CurlInputPlugin.hxx b/src/input/plugins/CurlInputPlugin.hxx
new file mode 100644
index 000000000..4acb18bfc
--- /dev/null
+++ b/src/input/plugins/CurlInputPlugin.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_INPUT_CURL_HXX
+#define MPD_INPUT_CURL_HXX
+
+extern const struct InputPlugin input_plugin_curl;
+
+#endif
diff --git a/src/input/plugins/DespotifyInputPlugin.cxx b/src/input/plugins/DespotifyInputPlugin.cxx
new file mode 100644
index 000000000..a01ba3759
--- /dev/null
+++ b/src/input/plugins/DespotifyInputPlugin.cxx
@@ -0,0 +1,262 @@
+/*
+ * 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 "DespotifyInputPlugin.hxx"
+#include "DespotifyUtils.hxx"
+#include "../InputStream.hxx"
+#include "../InputPlugin.hxx"
+#include "tag/Tag.hxx"
+#include "util/StringUtil.hxx"
+#include "Log.hxx"
+
+extern "C" {
+#include <despotify.h>
+}
+
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+
+#include <stdio.h>
+
+class DespotifyInputStream {
+ InputStream base;
+
+ struct despotify_session *session;
+ struct ds_track *track;
+ Tag tag;
+ struct ds_pcm_data pcm;
+ size_t len_available;
+ bool eof;
+
+ DespotifyInputStream(const char *uri,
+ Mutex &mutex, Cond &cond,
+ despotify_session *_session,
+ ds_track *_track)
+ :base(input_plugin_despotify, uri, mutex, cond),
+ session(_session), track(_track),
+ tag(mpd_despotify_tag_from_track(*track)),
+ len_available(0), eof(false) {
+
+ memset(&pcm, 0, sizeof(pcm));
+
+ /* Despotify outputs pcm data */
+ base.mime = "audio/x-mpd-cdda-pcm";
+ base.ready = true;
+ }
+
+public:
+ ~DespotifyInputStream() {
+ despotify_free_track(track);
+ }
+
+ static InputStream *Open(const char *url, Mutex &mutex, Cond &cond,
+ Error &error);
+
+ bool IsEOF() const {
+ return eof;
+ }
+
+ size_t Read(void *ptr, size_t size, Error &error);
+
+ Tag *ReadTag() {
+ if (tag.IsEmpty())
+ return nullptr;
+
+ Tag *result = new Tag(std::move(tag));
+ tag.Clear();
+ return result;
+ }
+
+ void Callback(int sig);
+
+private:
+ void FillBuffer();
+};
+
+inline void
+DespotifyInputStream::FillBuffer()
+{
+ /* Wait until there is data */
+ while (1) {
+ int rc = despotify_get_pcm(session, &pcm);
+
+ if (rc == 0 && pcm.len) {
+ len_available = pcm.len;
+ break;
+ }
+
+ if (eof == true)
+ break;
+
+ if (rc < 0) {
+ LogDebug(despotify_domain, "despotify_get_pcm error");
+ eof = true;
+ break;
+ }
+
+ /* Wait a while until next iteration */
+ usleep(50 * 1000);
+ }
+}
+
+inline void
+DespotifyInputStream::Callback(int sig)
+{
+ switch (sig) {
+ case DESPOTIFY_NEW_TRACK:
+ break;
+
+ case DESPOTIFY_TIME_TELL:
+ break;
+
+ case DESPOTIFY_TRACK_PLAY_ERROR:
+ LogWarning(despotify_domain, "Track play error");
+ eof = true;
+ len_available = 0;
+ break;
+
+ case DESPOTIFY_END_OF_PLAYLIST:
+ eof = true;
+ LogDebug(despotify_domain, "End of playlist");
+ break;
+ }
+}
+
+static void callback(gcc_unused struct despotify_session* ds,
+ int sig, gcc_unused void* data, void* callback_data)
+{
+ DespotifyInputStream *ctx = (DespotifyInputStream *)callback_data;
+
+ ctx->Callback(sig);
+}
+
+inline InputStream *
+DespotifyInputStream::Open(const char *url,
+ Mutex &mutex, Cond &cond,
+ gcc_unused Error &error)
+{
+ if (!StringStartsWith(url, "spt://"))
+ return nullptr;
+
+ despotify_session *session = mpd_despotify_get_session();
+ if (session == nullptr)
+ return nullptr;
+
+ ds_link *ds_link = despotify_link_from_uri(url + 6);
+ if (!ds_link) {
+ FormatDebug(despotify_domain, "Can't find %s", url);
+ return nullptr;
+ }
+ if (ds_link->type != LINK_TYPE_TRACK) {
+ despotify_free_link(ds_link);
+ return nullptr;
+ }
+
+ ds_track *track = despotify_link_get_track(session, ds_link);
+ despotify_free_link(ds_link);
+ if (!track)
+ return nullptr;
+
+ DespotifyInputStream *ctx =
+ new DespotifyInputStream(url, mutex, cond,
+ session, track);
+
+ if (!mpd_despotify_register_callback(callback, ctx)) {
+ delete ctx;
+ return nullptr;
+ }
+
+ if (despotify_play(ctx->session, ctx->track, false) == false) {
+ mpd_despotify_unregister_callback(callback);
+ delete ctx;
+ return nullptr;
+ }
+
+ return &ctx->base;
+}
+
+static InputStream *
+input_despotify_open(const char *url, Mutex &mutex, Cond &cond, Error &error)
+{
+ return DespotifyInputStream::Open(url, mutex, cond, error);
+}
+
+inline size_t
+DespotifyInputStream::Read(void *ptr, size_t size, gcc_unused Error &error)
+{
+ if (len_available == 0)
+ FillBuffer();
+
+ size_t to_cpy = std::min(size, len_available);
+ memcpy(ptr, pcm.buf, to_cpy);
+ len_available -= to_cpy;
+
+ base.offset += to_cpy;
+
+ return to_cpy;
+}
+
+static size_t
+input_despotify_read(InputStream *is, void *ptr, size_t size, Error &error)
+{
+ DespotifyInputStream *ctx = (DespotifyInputStream *)is;
+ return ctx->Read(ptr, size, error);
+}
+
+static void
+input_despotify_close(InputStream *is)
+{
+ DespotifyInputStream *ctx = (DespotifyInputStream *)is;
+
+ mpd_despotify_unregister_callback(callback);
+ delete ctx;
+}
+
+static bool
+input_despotify_eof(InputStream *is)
+{
+ DespotifyInputStream *ctx = (DespotifyInputStream *)is;
+
+ return ctx->IsEOF();
+}
+
+static Tag *
+input_despotify_tag(InputStream *is)
+{
+ DespotifyInputStream *ctx = (DespotifyInputStream *)is;
+
+ return ctx->ReadTag();
+}
+
+const InputPlugin input_plugin_despotify = {
+ "despotify",
+ nullptr,
+ nullptr,
+ input_despotify_open,
+ input_despotify_close,
+ nullptr,
+ nullptr,
+ input_despotify_tag,
+ nullptr,
+ input_despotify_read,
+ input_despotify_eof,
+ nullptr,
+};
diff --git a/src/input/plugins/DespotifyInputPlugin.hxx b/src/input/plugins/DespotifyInputPlugin.hxx
new file mode 100644
index 000000000..83f963520
--- /dev/null
+++ b/src/input/plugins/DespotifyInputPlugin.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 INPUT_DESPOTIFY_HXX
+#define INPUT_DESPOTIFY_HXX
+
+extern const struct InputPlugin input_plugin_despotify;
+
+#endif
diff --git a/src/input/plugins/FfmpegInputPlugin.cxx b/src/input/plugins/FfmpegInputPlugin.cxx
new file mode 100644
index 000000000..0d0a4375e
--- /dev/null
+++ b/src/input/plugins/FfmpegInputPlugin.cxx
@@ -0,0 +1,177 @@
+/*
+ * 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.
+ */
+
+/* necessary because libavutil/common.h uses UINT64_C */
+#define __STDC_CONSTANT_MACROS
+
+#include "config.h"
+#include "FfmpegInputPlugin.hxx"
+#include "../InputStream.hxx"
+#include "../InputPlugin.hxx"
+#include "util/StringUtil.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+
+extern "C" {
+#include <libavformat/avio.h>
+#include <libavformat/avformat.h>
+}
+
+struct FfmpegInputStream {
+ InputStream base;
+
+ AVIOContext *h;
+
+ bool eof;
+
+ FfmpegInputStream(const char *uri, Mutex &mutex, Cond &cond,
+ AVIOContext *_h)
+ :base(input_plugin_ffmpeg, uri, mutex, cond),
+ h(_h), eof(false) {
+ base.ready = true;
+ base.seekable = (h->seekable & AVIO_SEEKABLE_NORMAL) != 0;
+ base.size = avio_size(h);
+
+ /* hack to make MPD select the "ffmpeg" decoder plugin
+ - since avio.h doesn't tell us the MIME type of the
+ resource, we can't select a decoder plugin, but the
+ "ffmpeg" plugin is quite good at auto-detection */
+ base.mime = "audio/x-mpd-ffmpeg";
+ }
+
+ ~FfmpegInputStream() {
+ avio_close(h);
+ }
+};
+
+static constexpr Domain ffmpeg_domain("ffmpeg");
+
+static inline bool
+input_ffmpeg_supported(void)
+{
+ void *opaque = nullptr;
+ return avio_enum_protocols(&opaque, 0) != nullptr;
+}
+
+static bool
+input_ffmpeg_init(gcc_unused const config_param &param,
+ Error &error)
+{
+ av_register_all();
+
+ /* disable this plugin if there's no registered protocol */
+ if (!input_ffmpeg_supported()) {
+ error.Set(ffmpeg_domain, "No protocol");
+ return false;
+ }
+
+ return true;
+}
+
+static InputStream *
+input_ffmpeg_open(const char *uri,
+ Mutex &mutex, Cond &cond,
+ Error &error)
+{
+ if (!StringStartsWith(uri, "gopher://") &&
+ !StringStartsWith(uri, "rtp://") &&
+ !StringStartsWith(uri, "rtsp://") &&
+ !StringStartsWith(uri, "rtmp://") &&
+ !StringStartsWith(uri, "rtmpt://") &&
+ !StringStartsWith(uri, "rtmps://"))
+ return nullptr;
+
+ AVIOContext *h;
+ int ret = avio_open(&h, uri, AVIO_FLAG_READ);
+ if (ret != 0) {
+ error.Set(ffmpeg_domain, ret,
+ "libavformat failed to open the URI");
+ return nullptr;
+ }
+
+ auto *i = new FfmpegInputStream(uri, mutex, cond, h);
+ return &i->base;
+}
+
+static size_t
+input_ffmpeg_read(InputStream *is, void *ptr, size_t size,
+ Error &error)
+{
+ FfmpegInputStream *i = (FfmpegInputStream *)is;
+
+ int ret = avio_read(i->h, (unsigned char *)ptr, size);
+ if (ret <= 0) {
+ if (ret < 0)
+ error.Set(ffmpeg_domain, "avio_read() failed");
+
+ i->eof = true;
+ return false;
+ }
+
+ is->offset += ret;
+ return (size_t)ret;
+}
+
+static void
+input_ffmpeg_close(InputStream *is)
+{
+ FfmpegInputStream *i = (FfmpegInputStream *)is;
+
+ delete i;
+}
+
+static bool
+input_ffmpeg_eof(InputStream *is)
+{
+ FfmpegInputStream *i = (FfmpegInputStream *)is;
+
+ return i->eof;
+}
+
+static bool
+input_ffmpeg_seek(InputStream *is, InputPlugin::offset_type offset,
+ int whence,
+ Error &error)
+{
+ FfmpegInputStream *i = (FfmpegInputStream *)is;
+ int64_t ret = avio_seek(i->h, offset, whence);
+
+ if (ret >= 0) {
+ i->eof = false;
+ return true;
+ } else {
+ error.Set(ffmpeg_domain, "avio_seek() failed");
+ return false;
+ }
+}
+
+const InputPlugin input_plugin_ffmpeg = {
+ "ffmpeg",
+ input_ffmpeg_init,
+ nullptr,
+ input_ffmpeg_open,
+ input_ffmpeg_close,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ input_ffmpeg_read,
+ input_ffmpeg_eof,
+ input_ffmpeg_seek,
+};
diff --git a/src/input/plugins/FfmpegInputPlugin.hxx b/src/input/plugins/FfmpegInputPlugin.hxx
new file mode 100644
index 000000000..43f829e89
--- /dev/null
+++ b/src/input/plugins/FfmpegInputPlugin.hxx
@@ -0,0 +1,28 @@
+/*
+ * 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_FFMPEG_INPUT_PLUGIN_HXX
+#define MPD_FFMPEG_INPUT_PLUGIN_HXX
+
+/**
+ * An input plugin based on libavformat's "avio" library.
+ */
+extern const struct InputPlugin input_plugin_ffmpeg;
+
+#endif
diff --git a/src/input/plugins/FileInputPlugin.cxx b/src/input/plugins/FileInputPlugin.cxx
new file mode 100644
index 000000000..780e93263
--- /dev/null
+++ b/src/input/plugins/FileInputPlugin.cxx
@@ -0,0 +1,157 @@
+/*
+ * 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" /* must be first for large file support */
+#include "FileInputPlugin.hxx"
+#include "../InputStream.hxx"
+#include "../InputPlugin.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "fs/Traits.hxx"
+#include "system/fd_util.h"
+#include "open.h"
+
+#include <sys/stat.h>
+#include <unistd.h>
+#include <errno.h>
+
+static constexpr Domain file_domain("file");
+
+struct FileInputStream {
+ InputStream base;
+
+ int fd;
+
+ FileInputStream(const char *path, int _fd, off_t size,
+ Mutex &mutex, Cond &cond)
+ :base(input_plugin_file, path, mutex, cond),
+ fd(_fd) {
+ base.size = size;
+ base.seekable = true;
+ base.ready = true;
+ }
+
+ ~FileInputStream() {
+ close(fd);
+ }
+};
+
+static InputStream *
+input_file_open(const char *filename,
+ Mutex &mutex, Cond &cond,
+ Error &error)
+{
+ int fd, ret;
+ struct stat st;
+
+ if (!PathTraitsFS::IsAbsolute(filename))
+ return nullptr;
+
+ fd = open_cloexec(filename, O_RDONLY|O_BINARY, 0);
+ if (fd < 0) {
+ if (errno != ENOENT && errno != ENOTDIR)
+ error.FormatErrno("Failed to open \"%s\"",
+ filename);
+ return nullptr;
+ }
+
+ ret = fstat(fd, &st);
+ if (ret < 0) {
+ error.FormatErrno("Failed to stat \"%s\"", filename);
+ close(fd);
+ return nullptr;
+ }
+
+ if (!S_ISREG(st.st_mode)) {
+ error.Format(file_domain, "Not a regular file: %s", filename);
+ close(fd);
+ return nullptr;
+ }
+
+#ifdef POSIX_FADV_SEQUENTIAL
+ posix_fadvise(fd, (off_t)0, st.st_size, POSIX_FADV_SEQUENTIAL);
+#endif
+
+ FileInputStream *fis = new FileInputStream(filename, fd, st.st_size,
+ mutex, cond);
+ return &fis->base;
+}
+
+static bool
+input_file_seek(InputStream *is, InputPlugin::offset_type offset,
+ int whence,
+ Error &error)
+{
+ FileInputStream *fis = (FileInputStream *)is;
+
+ offset = (InputPlugin::offset_type)lseek(fis->fd, (off_t)offset, whence);
+ if (offset < 0) {
+ error.SetErrno("Failed to seek");
+ return false;
+ }
+
+ is->offset = offset;
+ return true;
+}
+
+static size_t
+input_file_read(InputStream *is, void *ptr, size_t size,
+ Error &error)
+{
+ FileInputStream *fis = (FileInputStream *)is;
+ ssize_t nbytes;
+
+ nbytes = read(fis->fd, ptr, size);
+ if (nbytes < 0) {
+ error.SetErrno("Failed to read");
+ return 0;
+ }
+
+ is->offset += nbytes;
+ return (size_t)nbytes;
+}
+
+static void
+input_file_close(InputStream *is)
+{
+ FileInputStream *fis = (FileInputStream *)is;
+
+ delete fis;
+}
+
+static bool
+input_file_eof(InputStream *is)
+{
+ return is->offset >= is->size;
+}
+
+const InputPlugin input_plugin_file = {
+ "file",
+ nullptr,
+ nullptr,
+ input_file_open,
+ input_file_close,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ input_file_read,
+ input_file_eof,
+ input_file_seek,
+};
diff --git a/src/input/plugins/FileInputPlugin.hxx b/src/input/plugins/FileInputPlugin.hxx
new file mode 100644
index 000000000..4aef94637
--- /dev/null
+++ b/src/input/plugins/FileInputPlugin.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_INPUT_FILE_HXX
+#define MPD_INPUT_FILE_HXX
+
+extern const struct InputPlugin input_plugin_file;
+
+#endif
diff --git a/src/input/plugins/MmsInputPlugin.cxx b/src/input/plugins/MmsInputPlugin.cxx
new file mode 100644
index 000000000..a7068cb2b
--- /dev/null
+++ b/src/input/plugins/MmsInputPlugin.cxx
@@ -0,0 +1,127 @@
+/*
+ * 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 "MmsInputPlugin.hxx"
+#include "../InputStream.hxx"
+#include "../InputPlugin.hxx"
+#include "util/StringUtil.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+
+#include <libmms/mmsx.h>
+
+struct MmsInputStream {
+ InputStream base;
+
+ mmsx_t *mms;
+
+ bool eof;
+
+ MmsInputStream(const char *uri,
+ Mutex &mutex, Cond &cond,
+ mmsx_t *_mms)
+ :base(input_plugin_mms, uri, mutex, cond),
+ mms(_mms), eof(false) {
+ /* XX is this correct? at least this selects the ffmpeg
+ decoder, which seems to work fine*/
+ base.mime = "audio/x-ms-wma";
+
+ base.ready = true;
+ }
+
+ ~MmsInputStream() {
+ mmsx_close(mms);
+ }
+};
+
+static constexpr Domain mms_domain("mms");
+
+static InputStream *
+input_mms_open(const char *url,
+ Mutex &mutex, Cond &cond,
+ Error &error)
+{
+ if (!StringStartsWith(url, "mms://") &&
+ !StringStartsWith(url, "mmsh://") &&
+ !StringStartsWith(url, "mmst://") &&
+ !StringStartsWith(url, "mmsu://"))
+ return nullptr;
+
+ const auto mms = mmsx_connect(nullptr, nullptr, url, 128 * 1024);
+ if (mms == nullptr) {
+ error.Set(mms_domain, "mmsx_connect() failed");
+ return nullptr;
+ }
+
+ auto m = new MmsInputStream(url, mutex, cond, mms);
+ return &m->base;
+}
+
+static size_t
+input_mms_read(InputStream *is, void *ptr, size_t size,
+ Error &error)
+{
+ MmsInputStream *m = (MmsInputStream *)is;
+ int ret;
+
+ ret = mmsx_read(nullptr, m->mms, (char *)ptr, size);
+ if (ret <= 0) {
+ if (ret < 0)
+ error.SetErrno("mmsx_read() failed");
+
+ m->eof = true;
+ return false;
+ }
+
+ is->offset += ret;
+
+ return (size_t)ret;
+}
+
+static void
+input_mms_close(InputStream *is)
+{
+ MmsInputStream *m = (MmsInputStream *)is;
+
+ delete m;
+}
+
+static bool
+input_mms_eof(InputStream *is)
+{
+ MmsInputStream *m = (MmsInputStream *)is;
+
+ return m->eof;
+}
+
+const InputPlugin input_plugin_mms = {
+ "mms",
+ nullptr,
+ nullptr,
+ input_mms_open,
+ input_mms_close,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ input_mms_read,
+ input_mms_eof,
+ nullptr,
+};
diff --git a/src/input/plugins/MmsInputPlugin.hxx b/src/input/plugins/MmsInputPlugin.hxx
new file mode 100644
index 000000000..b4017ffd6
--- /dev/null
+++ b/src/input/plugins/MmsInputPlugin.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 INPUT_MMS_H
+#define INPUT_MMS_H
+
+extern const struct InputPlugin input_plugin_mms;
+
+#endif
diff --git a/src/input/plugins/RewindInputPlugin.cxx b/src/input/plugins/RewindInputPlugin.cxx
new file mode 100644
index 000000000..1a930ac53
--- /dev/null
+++ b/src/input/plugins/RewindInputPlugin.cxx
@@ -0,0 +1,251 @@
+/*
+ * 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 "RewindInputPlugin.hxx"
+#include "../InputStream.hxx"
+#include "../InputPlugin.hxx"
+
+#include <assert.h>
+#include <string.h>
+#include <stdio.h>
+
+extern const InputPlugin rewind_input_plugin;
+
+struct RewindInputStream {
+ InputStream base;
+
+ InputStream *input;
+
+ /**
+ * The read position within the buffer. Undefined as long as
+ * ReadingFromBuffer() returns false.
+ */
+ size_t head;
+
+ /**
+ * The write/append position within the buffer.
+ */
+ size_t tail;
+
+ /**
+ * The size of this buffer is the maximum number of bytes
+ * which can be rewinded cheaply without passing the "seek"
+ * call to CURL.
+ *
+ * The origin of this buffer is always the beginning of the
+ * stream (offset 0).
+ */
+ char buffer[64 * 1024];
+
+ RewindInputStream(InputStream *_input)
+ :base(rewind_input_plugin, _input->uri.c_str(),
+ _input->mutex, _input->cond),
+ input(_input), tail(0) {
+ }
+
+ ~RewindInputStream() {
+ input->Close();
+ }
+
+ /**
+ * Are we currently reading from the buffer, and does the
+ * buffer contain more data for the next read operation?
+ */
+ bool ReadingFromBuffer() const {
+ return tail > 0 && base.offset < input->offset;
+ }
+
+ /**
+ * Copy public attributes from the underlying input stream to the
+ * "rewind" input stream. This function is called when a method of
+ * the underlying stream has returned, which may have modified these
+ * attributes.
+ */
+ void CopyAttributes() {
+ InputStream *dest = &base;
+ const InputStream *src = input;
+
+ assert(dest != src);
+
+ bool dest_ready = dest->ready;
+
+ dest->ready = src->ready;
+ dest->seekable = src->seekable;
+ dest->size = src->size;
+ dest->offset = src->offset;
+
+ if (!dest_ready && src->ready)
+ dest->mime = src->mime;
+ }
+};
+
+static void
+input_rewind_close(InputStream *is)
+{
+ RewindInputStream *r = (RewindInputStream *)is;
+
+ delete r;
+}
+
+static bool
+input_rewind_check(InputStream *is, Error &error)
+{
+ RewindInputStream *r = (RewindInputStream *)is;
+
+ return r->input->Check(error);
+}
+
+static void
+input_rewind_update(InputStream *is)
+{
+ RewindInputStream *r = (RewindInputStream *)is;
+
+ if (!r->ReadingFromBuffer())
+ r->CopyAttributes();
+}
+
+static Tag *
+input_rewind_tag(InputStream *is)
+{
+ RewindInputStream *r = (RewindInputStream *)is;
+
+ return r->input->ReadTag();
+}
+
+static bool
+input_rewind_available(InputStream *is)
+{
+ RewindInputStream *r = (RewindInputStream *)is;
+
+ return r->input->IsAvailable();
+}
+
+static size_t
+input_rewind_read(InputStream *is, void *ptr, size_t size,
+ Error &error)
+{
+ RewindInputStream *r = (RewindInputStream *)is;
+
+ if (r->ReadingFromBuffer()) {
+ /* buffered read */
+
+ assert(r->head == (size_t)is->offset);
+ assert(r->tail == (size_t)r->input->offset);
+
+ if (size > r->tail - r->head)
+ size = r->tail - r->head;
+
+ memcpy(ptr, r->buffer + r->head, size);
+ r->head += size;
+ is->offset += size;
+
+ return size;
+ } else {
+ /* pass method call to underlying stream */
+
+ size_t nbytes = r->input->Read(ptr, size, error);
+
+ if (r->input->offset > (InputPlugin::offset_type)sizeof(r->buffer))
+ /* disable buffering */
+ r->tail = 0;
+ else if (r->tail == (size_t)is->offset) {
+ /* append to buffer */
+
+ memcpy(r->buffer + r->tail, ptr, nbytes);
+ r->tail += nbytes;
+
+ assert(r->tail == (size_t)r->input->offset);
+ }
+
+ r->CopyAttributes();
+
+ return nbytes;
+ }
+}
+
+static bool
+input_rewind_eof(InputStream *is)
+{
+ RewindInputStream *r = (RewindInputStream *)is;
+
+ return !r->ReadingFromBuffer() && r->input->IsEOF();
+}
+
+static bool
+input_rewind_seek(InputStream *is, InputPlugin::offset_type offset,
+ int whence,
+ Error &error)
+{
+ RewindInputStream *r = (RewindInputStream *)is;
+
+ assert(is->ready);
+
+ if (whence == SEEK_SET && r->tail > 0 &&
+ offset <= (InputPlugin::offset_type)r->tail) {
+ /* buffered seek */
+
+ assert(!r->ReadingFromBuffer() ||
+ r->head == (size_t)is->offset);
+ assert(r->tail == (size_t)r->input->offset);
+
+ r->head = (size_t)offset;
+ is->offset = offset;
+
+ return true;
+ } else {
+ bool success = r->input->Seek(offset, whence, error);
+ r->CopyAttributes();
+
+ /* disable the buffer, because r->input has left the
+ buffered range now */
+ r->tail = 0;
+
+ return success;
+ }
+}
+
+const InputPlugin rewind_input_plugin = {
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ input_rewind_close,
+ input_rewind_check,
+ input_rewind_update,
+ input_rewind_tag,
+ input_rewind_available,
+ input_rewind_read,
+ input_rewind_eof,
+ input_rewind_seek,
+};
+
+InputStream *
+input_rewind_open(InputStream *is)
+{
+ assert(is != nullptr);
+ assert(is->offset == 0);
+
+ if (is->seekable)
+ /* seekable resources don't need this plugin */
+ return is;
+
+ RewindInputStream *c = new RewindInputStream(is);
+ return &c->base;
+}
diff --git a/src/input/plugins/RewindInputPlugin.hxx b/src/input/plugins/RewindInputPlugin.hxx
new file mode 100644
index 000000000..f19705154
--- /dev/null
+++ b/src/input/plugins/RewindInputPlugin.hxx
@@ -0,0 +1,37 @@
+/*
+ * 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
+ *
+ * A wrapper for an input_stream object which allows cheap buffered
+ * rewinding. This is useful while detecting the stream codec (let
+ * each decoder plugin peek a portion from the stream).
+ */
+
+#ifndef MPD_INPUT_REWIND_HXX
+#define MPD_INPUT_REWIND_HXX
+
+#include "check.h"
+
+struct InputStream;
+
+InputStream *
+input_rewind_open(InputStream *is);
+
+#endif
diff --git a/src/input/plugins/SmbclientInputPlugin.cxx b/src/input/plugins/SmbclientInputPlugin.cxx
new file mode 100644
index 000000000..9fbe72b59
--- /dev/null
+++ b/src/input/plugins/SmbclientInputPlugin.cxx
@@ -0,0 +1,203 @@
+/*
+ * 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 "SmbclientInputPlugin.hxx"
+#include "../InputStream.hxx"
+#include "../InputPlugin.hxx"
+#include "util/StringUtil.hxx"
+#include "util/Error.hxx"
+
+#include <libsmbclient.h>
+
+#include <string.h>
+
+class SmbclientInputStream {
+ InputStream base;
+
+ SMBCCTX *ctx;
+ int fd;
+
+public:
+ SmbclientInputStream(const char *uri,
+ Mutex &mutex, Cond &cond,
+ SMBCCTX *_ctx, int _fd, const struct stat &st)
+ :base(input_plugin_smbclient, uri, mutex, cond),
+ ctx(_ctx), fd(_fd) {
+ base.ready = true;
+ base.seekable = true;
+ base.size = st.st_size;
+ }
+
+ ~SmbclientInputStream() {
+ smbc_close(fd);
+ smbc_free_context(ctx, 1);
+ }
+
+ InputStream *GetBase() {
+ return &base;
+ }
+
+ bool IsEOF() const {
+ return base.offset >= base.size;
+ }
+
+ size_t Read(void *ptr, size_t size, Error &error) {
+ ssize_t nbytes = smbc_read(fd, ptr, size);
+ if (nbytes < 0) {
+ error.SetErrno("smbc_read() failed");
+ nbytes = 0;
+ }
+
+ return nbytes;
+ }
+
+ bool Seek(InputStream::offset_type offset, int whence, Error &error) {
+ off_t result = smbc_lseek(fd, offset, whence);
+ if (result < 0) {
+ error.SetErrno("smbc_lseek() failed");
+ return false;
+ }
+
+ base.offset = result;
+ return true;
+ }
+};
+
+static void
+mpd_smbc_get_auth_data(gcc_unused const char *srv,
+ gcc_unused const char *shr,
+ char *wg, gcc_unused int wglen,
+ char *un, gcc_unused int unlen,
+ char *pw, gcc_unused int pwlen)
+{
+ // TODO: implement
+ strcpy(wg, "WORKGROUP");
+ strcpy(un, "foo");
+ strcpy(pw, "bar");
+}
+
+/*
+ * InputPlugin methods
+ *
+ */
+
+static bool
+input_smbclient_init(gcc_unused const config_param &param, Error &error)
+{
+ constexpr int debug = 0;
+ if (smbc_init(mpd_smbc_get_auth_data, debug) < 0) {
+ error.SetErrno("smbc_init() failed");
+ return false;
+ }
+
+ // TODO: create one global SMBCCTX here?
+
+ // TODO: evaluate config_param, call smbc_setOption*()
+
+ return true;
+}
+
+static InputStream *
+input_smbclient_open(const char *uri,
+ Mutex &mutex, Cond &cond,
+ Error &error)
+{
+ if (!StringStartsWith(uri, "smb://"))
+ return nullptr;
+
+ SMBCCTX *ctx = smbc_new_context();
+ if (ctx == nullptr) {
+ error.SetErrno("smbc_new_context() failed");
+ return nullptr;
+ }
+
+ SMBCCTX *ctx2 = smbc_init_context(ctx);
+ if (ctx2 == nullptr) {
+ error.SetErrno("smbc_init_context() failed");
+ smbc_free_context(ctx, 1);
+ return nullptr;
+ }
+
+ ctx = ctx2;
+
+ int fd = smbc_open(uri, O_RDONLY, 0);
+ if (fd < 0) {
+ error.SetErrno("smbc_open() failed");
+ smbc_free_context(ctx, 1);
+ return nullptr;
+ }
+
+ struct stat st;
+ if (smbc_fstat(fd, &st) < 0) {
+ error.SetErrno("smbc_fstat() failed");
+ smbc_close(fd);
+ smbc_free_context(ctx, 1);
+ return nullptr;
+ }
+
+ auto s = new SmbclientInputStream(uri, mutex, cond, ctx, fd, st);
+ return s->GetBase();
+}
+
+static size_t
+input_smbclient_read(InputStream *is, void *ptr, size_t size,
+ Error &error)
+{
+ SmbclientInputStream &s = *(SmbclientInputStream *)is;
+ return s.Read(ptr, size, error);
+}
+
+static void
+input_smbclient_close(InputStream *is)
+{
+ SmbclientInputStream *s = (SmbclientInputStream *)is;
+ delete s;
+}
+
+static bool
+input_smbclient_eof(InputStream *is)
+{
+ SmbclientInputStream &s = *(SmbclientInputStream *)is;
+ return s.IsEOF();
+}
+
+static bool
+input_smbclient_seek(InputStream *is,
+ InputPlugin::offset_type offset, int whence,
+ Error &error)
+{
+ SmbclientInputStream &s = *(SmbclientInputStream *)is;
+ return s.Seek(offset, whence, error);
+}
+
+const InputPlugin input_plugin_smbclient = {
+ "smbclient",
+ input_smbclient_init,
+ nullptr,
+ input_smbclient_open,
+ input_smbclient_close,
+ nullptr,
+ nullptr,
+ nullptr,
+ nullptr,
+ input_smbclient_read,
+ input_smbclient_eof,
+ input_smbclient_seek,
+};
diff --git a/src/input/plugins/SmbclientInputPlugin.hxx b/src/input/plugins/SmbclientInputPlugin.hxx
new file mode 100644
index 000000000..a0539d020
--- /dev/null
+++ b/src/input/plugins/SmbclientInputPlugin.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_INPUT_SMBCLIENT_H
+#define MPD_INPUT_SMBCLIENT_H
+
+extern const struct InputPlugin input_plugin_smbclient;
+
+#endif