diff --git a/client/metadata.h b/client/metadata.h index 84b9c164..7643eae6 100644 --- a/client/metadata.h +++ b/client/metadata.h @@ -55,7 +55,7 @@ public: std::string serialize() { - return METADATA + ":" + _msg->dump() + "\n"; + return METADATA + ":" + _msg->dump(); } void tag(std::string name, std::string value) diff --git a/server/streamServer.cpp b/server/streamServer.cpp index b6b36feb..a1586115 100644 --- a/server/streamServer.cpp +++ b/server/streamServer.cpp @@ -385,10 +385,14 @@ void StreamServer::ProcessRequest(const jsonrpcpp::request_ptr request, jsonrpcp { if (request->method.find("Stream.SetMeta") == 0) { - /// Request: {"id":4,"jsonrpc":"2.0","method":"Stream.SetMeta","params":{"id":"Spotify","meta": {"album": "some album", "artist": "some artist", "track": "some track"...}}} - /// Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"stream 1"}} + /// Request: {"id":4,"jsonrpc":"2.0","method":"Stream.SetMeta","params":{"stream_id":"Spotify", + /// "meta": {"album": "some album", "artist": "some artist", "track": "some track"...}}} + /// + /// Response: {"id":4,"jsonrpc":"2.0","result":{"stream_id":"Spotify"}} /// Call onMetaChanged(const PcmStream* pcmStream) for updates and notifications + LOG(INFO) << "Stream.SetMeta(" << request->params.get("stream_id") << ")" << request->params.get("meta") <<"\n"; + // Find stream string streamId = request->params.get("stream_id"); PcmStreamPtr stream = streamManager_->getStream(streamId); @@ -396,14 +400,10 @@ void StreamServer::ProcessRequest(const jsonrpcpp::request_ptr request, jsonrpcp throw jsonrpcpp::InternalErrorException("Stream not found", request->id); // Set metadata from request - // stream-> + stream->setMeta(request->params.get("meta")); - // Update clients // Setup response - - // Trigger notifications - // onMetaChanged(stream); - // notification.reset(new jsonrpcpp::Notification("Client.OnLatencyChanged", jsonrpcpp::Parameter("id", clientInfo->id, "latency", clientInfo->config.latency))); + result["stream_id"] = streamId; } else throw jsonrpcpp::MethodNotFoundException(request->id); diff --git a/server/streamreader/README.meta b/server/streamreader/README.meta new file mode 100644 index 00000000..9852e501 --- /dev/null +++ b/server/streamreader/README.meta @@ -0,0 +1,31 @@ +Metadata tags +============= +The metatag interface will propegate anything to the client, ie no restrictions on the tags. +But, to avoid total confusion in the tag display app (like Kodi), we need some groundrules. + +My suggestion is to stick with the Vorbis standard as described in: + + http://www.xiph.org/vorbis/doc/v-comment.html + https://wiki.xiph.org/Field_names + +In addition we should accept unique identifiers of the stream such as Spotify track ID or +MusicBrainz id which can be used to lookup additional information online. + +Example: + ARTIST: Pink Floyd + ALBUM: Dark Side of the Moon + TITLE: Money + METADATA_BLOCK_PICTURE: base64 (https://xiph.org/flac/format.html#metadata_block_picture) + + +Parsing tags from streams +========================= +I've implemented parsing tags from Librespot, but, as Librespot doesn't export anything more than +track title I added a patch 'librespot-meta.patch' to apply to get artist/title. + +TBH, the meta tag output from players are a mess, neither Librespot nor Shairport-sync associate +metadata with their audio interfaces, ie they don't even export metadata when the audio interface +supports it, at least Shairport-sync exports a ritch set of data. + +So, to get artist/album apply the patch to librespot and recompile, otherwise you will only get the +track title. diff --git a/server/streamreader/librespot-meta.patch b/server/streamreader/librespot-meta.patch new file mode 100644 index 00000000..a371a08b --- /dev/null +++ b/server/streamreader/librespot-meta.patch @@ -0,0 +1,31 @@ +*** librespot/src/player.rs 2017-11-30 08:16:40.865939287 +0100 +--- librespot.meta/src/player.rs 2017-11-30 08:14:39.232954039 +0100 +*************** +*** 13,19 **** + use audio_backend::Sink; + use audio::{AudioFile, AudioDecrypt}; + use audio::{VorbisDecoder, VorbisPacket}; +! use metadata::{FileFormat, Track, Metadata}; + use mixer::AudioFilter; + + #[derive(Clone)] +--- 13,19 ---- + use audio_backend::Sink; + use audio::{AudioFile, AudioDecrypt}; + use audio::{VorbisDecoder, VorbisPacket}; +! use metadata::{Artist, FileFormat, Track, Metadata}; + use mixer::AudioFilter; + + #[derive(Clone)] +*************** +*** 384,389 **** +--- 384,392 ---- + + info!("Track \"{}\" loaded", track.name); + ++ let artist = Artist::get(&self.session, track.artists[0]).wait().unwrap(); ++ info!("metadata:{{\"ARTIST\":\"{}\",\"TITLE\":\"{}\"}}", artist.name, track.name); ++ + Some(decoder) + } + } diff --git a/server/streamreader/pcmStream.cpp b/server/streamreader/pcmStream.cpp index 0c699129..1762e5f1 100644 --- a/server/streamreader/pcmStream.cpp +++ b/server/streamreader/pcmStream.cpp @@ -179,3 +179,17 @@ json PcmStream::toJson() const return j; } +std::shared_ptr PcmStream::getMeta() const +{ + return meta_; +} + +void PcmStream::setMeta(json jtag) +{ + meta_.reset(new msg::StreamTags(jtag)); + + // Trigger a stream update + if (pcmListener_) + pcmListener_->onMetaChanged(this); +} + diff --git a/server/streamreader/pcmStream.h b/server/streamreader/pcmStream.h index 4810c5e0..7c4d4917 100644 --- a/server/streamreader/pcmStream.h +++ b/server/streamreader/pcmStream.h @@ -83,15 +83,12 @@ public: virtual const std::string& getId() const; virtual const SampleFormat& getSampleFormat() const; + std::shared_ptr getMeta() const; + void setMeta(json j); + virtual ReaderState getState() const; virtual json toJson() const; - //const msg::StreamTags *getMeta() - std::shared_ptr getMeta() const - { - return meta_; - } - protected: std::condition_variable cv_; @@ -99,6 +96,7 @@ protected: std::thread thread_; std::atomic active_; + virtual void worker() = 0; virtual bool sleep(int32_t ms); void setState(const ReaderState& newState); @@ -112,8 +110,6 @@ protected: std::unique_ptr encoder_; std::string name_; ReaderState state_; - - // Stream metadata std::shared_ptr meta_; }; diff --git a/server/streamreader/spotifyStream.cpp b/server/streamreader/spotifyStream.cpp index 48f64908..dd30c28a 100644 --- a/server/streamreader/spotifyStream.cpp +++ b/server/streamreader/spotifyStream.cpp @@ -93,6 +93,9 @@ void SpotifyStream::initExeAndPath(const std::string& filename) void SpotifyStream::onStderrMsg(const char* buffer, size_t n) { + static bool libreelec_patched = false; + smatch m; + // Watch stderr for 'Loading track' messages and set the stream metadata // For more than track name check: https://github.com/plietar/librespot/issues/154 @@ -124,29 +127,37 @@ void SpotifyStream::onStderrMsg(const char* buffer, size_t n) LOG(INFO) << "(" << getName() << ") " << logmsg << "\n"; } - // Track tags "Julia Michaels" "Issues - Acoustic" - if (logmsg.find("Track tags") != string::npos) - { - // Traditional Libreelec meta interface, only track name - regex re("Track tags \"(.*)\" \"(.*)\""); - smatch m; + // Librespot patch: + // info!("metadata:{{\"ARTIST\":\"{}\",\"TITLE\":\"{}\"}}", artist.name, track.name); + // non patched: + // info!("Track \"{}\" loaded", track.name); - if (regex_search(logmsg, m, re)) + // If we detect a patched libreelec we don't want to bother with this anymoer + // to avoid duplicate metadata pushes + if (!libreelec_patched) + { + static regex re_nonpatched("Track \"(.*)\" loaded"); + if(regex_search(logmsg, m, re_nonpatched)) { - // Create a new meta struct? - LOG(INFO) << "Loading track <" << m[1] << "> <" << m[2] << ">\n"; + LOG(INFO) << "metadata: <" << m[1] << ">\n"; json jtag = { - {"artist", (string)m[1]}, - {"track", (string)m[2]} + {"TITLE", (string)m[1]} }; - meta_.reset(new msg::StreamTags(jtag)); - - // Trigger a stream update - if (pcmListener_) - pcmListener_->onMetaChanged(this); + setMeta(jtag); } } + + // Parse the patched version + static regex re_patched("metadata:(.*)"); + if (regex_search(logmsg, m, re_patched)) + { + LOG(INFO) << "metadata: <" << m[1] << ">\n"; + + json jtag; + setMeta(jtag.parse(m[1])); + libreelec_patched = true; + } }