Add support for cover raw images

This commit is contained in:
badaix 2021-09-06 22:19:35 +02:00
parent a5f79cdf90
commit befc8da440
12 changed files with 210 additions and 27 deletions

View file

@ -21,6 +21,7 @@
#include "control_session_ws.hpp"
#include "message/pcm_chunk.hpp"
#include "stream_session_ws.hpp"
#include <boost/beast/http/buffer_body.hpp>
#include <boost/beast/http/file_body.hpp>
#include <iostream>
@ -139,7 +140,7 @@ ControlSessionHttp::ControlSessionHttp(ControlMessageReceiver* receiver, boost::
const ServerSettings::Http& settings)
: ControlSession(receiver), socket_(std::move(socket)), settings_(settings), strand_(ioc)
{
LOG(DEBUG, LOG_TAG) << "ControlSessionHttp\n";
LOG(DEBUG, LOG_TAG) << "ControlSessionHttp, Local IP: " << socket_.local_endpoint().address().to_string() << "\n";
}
@ -233,11 +234,36 @@ void ControlSessionHttp::handle_request(http::request<Body, http::basic_fields<A
}
// Request path must be absolute and not contain "..".
if (req.target().empty() || req.target()[0] != '/' || req.target().find("..") != beast::string_view::npos)
auto target = req.target();
if (target.empty() || target[0] != '/' || target.find("..") != beast::string_view::npos)
return send(bad_request("Illegal request-target"));
static string image_cache_target = "/__image_cache?name=";
auto pos = target.find(image_cache_target);
if (pos != std::string::npos)
{
pos += image_cache_target.size();
target = target.substr(pos);
auto image = settings_.image_cache.getImage(std::string(target));
LOG(DEBUG, LOG_TAG) << "image cache: " << target << ", found: " << image.has_value() << "\n";
if (image.has_value())
{
http::response<http::buffer_body> res{http::status::ok, req.version()};
res.body().data = image.value().data();
const auto size = image.value().size();
res.body().size = size;
res.body().more = false;
res.set(http::field::server, HTTP_SERVER_NAME);
res.set(http::field::content_type, mime_type(target));
res.content_length(size);
res.keep_alive(req.keep_alive());
return send(std::move(res));
}
return send(not_found(req.target()));
}
// Build the path to the requested file
std::string path = path_cat(settings_.doc_root, req.target());
std::string path = path_cat(settings_.doc_root, target);
if (req.target().back() == '/')
path.append("index.html");

View file

@ -66,6 +66,12 @@
# serve a website from the doc_root location
# disabled if commented or empty
doc_root = /usr/share/snapserver/snapweb
# Hostname or IP under which clients can reach this host
# used to serve cached cover art
# use <hostname> as placeholder for your actual host name
#host = <hostname>
#
###############################################################################

93
server/image_cache.hpp Normal file
View file

@ -0,0 +1,93 @@
/***
This file is part of snapcast
Copyright (C) 2014-2021 Johannes Pohl
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 3 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, see <http://www.gnu.org/licenses/>.
***/
#ifndef IMAGE_CACHE_HPP
#define IMAGE_CACHE_HPP
#include <map>
#include <mutex>
#include <optional>
#include <string>
#include <boost/algorithm/hex.hpp>
#include <boost/uuid/detail/md5.hpp>
class ImageCache
{
public:
ImageCache() = default;
virtual ~ImageCache() = default;
std::string setImage(const std::string& key, std::string image, const std::string& extension)
{
if (image.empty())
{
clear(key);
return "";
}
using boost::uuids::detail::md5;
md5 hash;
md5::digest_type digest;
hash.process_bytes(image.data(), image.size());
hash.get_digest(digest);
std::string filename;
const auto intDigest = reinterpret_cast<const int*>(&digest);
boost::algorithm::hex_lower(intDigest, intDigest + (sizeof(md5::digest_type) / sizeof(int)), std::back_inserter(filename));
auto ext = extension;
if (ext.find('.') == 0)
ext = ext.substr(1);
filename += "." + ext;
key_to_url_[key] = filename;
url_to_data_[filename] = image;
return filename;
};
void clear(const std::string& key)
{
std::lock_guard<std::mutex> lock(mutex_);
auto iter = key_to_url_.find(key);
if (iter != key_to_url_.end())
{
auto url = *iter;
auto url_iter = url_to_data_.find(url.second);
if (url_iter != url_to_data_.end())
url_to_data_.erase(url_iter);
key_to_url_.erase(iter);
}
}
std::optional<std::string> getImage(const std::string& url)
{
std::lock_guard<std::mutex> lock(mutex_);
auto iter = url_to_data_.find(url);
if (iter == url_to_data_.end())
return std::nullopt;
else
return iter->second;
}
private:
std::map<std::string, std::string> key_to_url_;
std::map<std::string, std::string> url_to_data_;
std::mutex mutex_;
};
#endif

View file

@ -1,6 +1,6 @@
/***
This file is part of snapcast
Copyright (C) 2014-2020 Johannes Pohl
Copyright (C) 2014-2021 Johannes Pohl
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
@ -22,6 +22,9 @@
#include <string>
#include <vector>
#include "image_cache.hpp"
struct ServerSettings
{
struct Server
@ -39,6 +42,8 @@ struct ServerSettings
size_t port{1780};
std::vector<std::string> bind_to_address{{"0.0.0.0"}};
std::string doc_root{""};
std::string host{"<hostname>"};
inline static ImageCache image_cache;
};
struct Tcp

View file

@ -84,6 +84,7 @@ int main(int argc, char* argv[])
auto http_bind_to_address = conf.add<Value<string>>("", "http.bind_to_address", "address for the server to listen on",
settings.http.bind_to_address.front(), &settings.http.bind_to_address[0]);
conf.add<Implicit<string>>("", "http.doc_root", "serve a website from the doc_root location", settings.http.doc_root, &settings.http.doc_root);
conf.add<Value<string>>("", "http.host", "Hostname or IP under which clients can reach this host", settings.http.host, &settings.http.host);
// TCP RPC settings
conf.add<Value<bool>>("", "tcp.enabled", "enable TCP Json RPC)", settings.tcp.enabled, &settings.tcp.enabled);
@ -281,6 +282,11 @@ int main(int argc, char* argv[])
if (settings.http.enabled)
{
dns_services.emplace_back("_snapcast-http._tcp", settings.http.port);
if ((settings.http.host == "<hostname>") || settings.http.host.empty())
{
settings.http.host = boost::asio::ip::host_name();
LOG(INFO, LOG_TAG) << "Using HTTP host name: " << settings.http.host << "\n";
}
}
publishZeroConfg->publish(dns_services);
#endif

View file

@ -67,11 +67,6 @@ AirplayStream::AirplayStream(PcmListener* pcmListener, boost::asio::io_context&
#ifdef HAS_EXPAT
createParser();
metadata_dirty_ = false;
metadata_ = json();
metadata_["ALBUM"] = "";
metadata_["ARTIST"] = "";
metadata_["TITLE"] = "";
metadata_["COVER"] = "";
#else
LOG(INFO, LOG_TAG) << "Metadata support not enabled (HAS_EXPAT not defined)"
<< "\n";
@ -165,7 +160,7 @@ void AirplayStream::push()
if (is_cover)
{
setMetaData("COVER", data);
setMetaData(meta_.art_data, Metatags::ArtData{data, "jpg"});
// LOG(INFO, LOG_TAG) << "Metadata type: " << entry_->type << " code: " << entry_->code << " data length: " << data.length() << "\n";
}
else
@ -174,28 +169,30 @@ void AirplayStream::push()
}
if (entry_->type == "core" && entry_->code == "asal")
setMetaData("ALBUM", data);
if (entry_->type == "core" && entry_->code == "asar")
setMetaData("ARTIST", data);
if (entry_->type == "core" && entry_->code == "minm")
setMetaData("TITLE", data);
setMetaData(meta_.album, data);
else if (entry_->type == "core" && entry_->code == "asar")
setMetaData(meta_.artist, {data});
else if (entry_->type == "core" && entry_->code == "minm")
setMetaData(meta_.title, data);
// mden = metadata end, pcen == picture end
if (metadata_dirty_ && entry_->type == "ssnc" && (entry_->code == "mden" || entry_->code == "pcen"))
{
// setMetadata(metadata_);
Properties properties;
properties.metatags = meta_;
setProperties(properties);
metadata_dirty_ = false;
}
}
void AirplayStream::setMetaData(const string& key, const string& newValue)
template <typename T>
void AirplayStream::setMetaData(std::optional<T>& meta_value, const T& value)
{
// Only overwrite metadata and set metadata_dirty_ if the metadata has changed.
// This avoids multiple unnecessary transmissions of the same metadata.
const auto& oldValue = metadata_[key];
if (oldValue != newValue)
if (!meta_value.has_value() || (meta_value.value() != value))
{
metadata_[key] = newValue;
meta_value = value;
metadata_dirty_ = true;
}
}

View file

@ -67,8 +67,8 @@ protected:
XML_Parser parser_;
std::unique_ptr<TageEntry> entry_;
std::string buf_;
json metadata_;
/// set whenever metadata_ has changed
Metatags meta_;
bool metadata_dirty_;
#endif
@ -77,7 +77,9 @@ protected:
int parse(const std::string& line);
void createParser();
void push();
void setMetaData(const std::string& key, const std::string& newValue);
template <typename T>
void setMetaData(std::optional<T>& meta_value, const T& value);
#endif
void setParamsAndPipePathFromPort();

View file

@ -79,6 +79,8 @@ std::string base64_encode(unsigned char const* bytes_to_encode, unsigned int in_
return ret;
}
std::string base64_decode(std::string const& encoded_string)
{
int in_len = encoded_string.size();

View file

@ -161,7 +161,9 @@ void LibrespotStream::onStderrMsg(const std::string& line)
Metatags meta;
meta.artist = std::vector<std::string>{j["ARTIST"].get<std::string>()};
meta.title = j["TITLE"].get<std::string>();
// TODO setMetadata(meta);
Properties properties;
properties.metatags = meta;
setProperties(properties);
}
else if (regex_search(line, m, re_track_loaded))
{
@ -169,10 +171,8 @@ void LibrespotStream::onStderrMsg(const std::string& line)
Metatags meta;
meta.title = string(m[1]);
meta.duration = cpt::stod(m[2]) / 1000.;
// TODO setMetadata(meta);
Properties properties;
// properties.can_seek = true;
// properties.can_control = true;
properties.metatags = meta;
setProperties(properties);
}
}

View file

@ -106,6 +106,10 @@ json Metatags::toJson() const
addTag(j, "trackNumber", track_number);
addTag(j, "url", url);
addTag(j, "artUrl", art_url);
if (art_data.has_value())
{
j["artData"] = {{"data", art_data->data}, {"extension", art_data->extension}};
}
addTag(j, "useCount", use_count);
addTag(j, "userRating", user_rating);
addTag(j, "spotifyArtistId", spotify_artist_id);
@ -153,6 +157,7 @@ void Metatags::fromJson(const json& j)
"trackNumber",
"url",
"artUrl",
"artData",
"useCount",
"userRating",
"spotifyArtistId",
@ -200,6 +205,11 @@ void Metatags::fromJson(const json& j)
readTag(j, "trackNumber", track_number);
readTag(j, "url", url);
readTag(j, "artUrl", art_url);
art_data = std::nullopt;
if (j.contains("artData") && j["artData"].contains("data") && j["artData"].contains("extension"))
{
art_data = ArtData{j["artData"]["data"].get<std::string>(), j["artData"]["extension"].get<std::string>()};
}
readTag(j, "useCount", use_count);
readTag(j, "userRating", user_rating);
readTag(j, "spotifyArtistId", spotify_artist_id);

View file

@ -31,6 +31,22 @@ using json = nlohmann::json;
class Metatags
{
public:
struct ArtData
{
std::string data;
std::string extension;
bool operator==(const ArtData& other) const
{
return ((other.data == data) && (other.extension == extension));
}
bool operator!=(const ArtData& other) const
{
return !(other == *this);
}
};
Metatags() = default;
Metatags(const json& j);
@ -85,6 +101,8 @@ public:
/// URI: The location of an image representing the track or album. Clients should not assume this will continue to exist when the media player stops giving
/// out the URL
std::optional<std::string> art_url;
/// Base64 encoded data of an image representing the track or album
std::optional<ArtData> art_data;
/// The track lyrics
std::optional<std::string> lyrics;
/// The speed of the music, in beats per minute

View file

@ -20,6 +20,9 @@
#include <memory>
#include <sys/stat.h>
#include <boost/asio/ip/host_name.hpp>
#include "base64.h"
#include "common/aixlog.hpp"
#include "common/error_code.hpp"
#include "common/snap_exception.hpp"
@ -447,11 +450,26 @@ void PcmStream::setProperties(const Properties& properties)
std::lock_guard<std::recursive_mutex> lock(mutex_);
Properties props = properties;
// Missing metadata means, they didn't change, so
// Missing metadata means the data didn't change, so
// enrich the new properites with old metadata
if (!props.metatags.has_value() && properties_.metatags.has_value())
props.metatags = properties_.metatags;
// If the cover image is availbale as raw data, cache it on the HTTP Server to make it also available via HTTP
if (props.metatags.has_value() && props.metatags->art_data.has_value() && !props.metatags->art_url.has_value())
{
auto data = base64_decode(props.metatags->art_data.value().data);
auto md5 = server_settings_.http.image_cache.setImage(getName(), std::move(data), props.metatags->art_data.value().extension);
std::stringstream url;
url << "http://" << server_settings_.http.host << ":" << server_settings_.http.port << "/__image_cache?name=" << md5;
props.metatags->art_url = url.str();
}
else if (!props.metatags->art_data.has_value())
{
server_settings_.http.image_cache.clear(getName());
}
if (props == properties_)
{
LOG(DEBUG, LOG_TAG) << "setProperties: Properties did not change\n";