mirror of
https://github.com/badaix/snapcast.git
synced 2025-05-12 00:26:41 +02:00
Move properties and metatags into server dir
This commit is contained in:
parent
004ea21e3f
commit
b348bb3379
11 changed files with 725 additions and 661 deletions
|
@ -1,314 +0,0 @@
|
||||||
/***
|
|
||||||
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 METATAGS_HPP
|
|
||||||
#define METATAGS_HPP
|
|
||||||
|
|
||||||
#include <optional>
|
|
||||||
#include <set>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include "common/aixlog.hpp"
|
|
||||||
#include "common/json.hpp"
|
|
||||||
|
|
||||||
using json = nlohmann::json;
|
|
||||||
|
|
||||||
class Metatags
|
|
||||||
{
|
|
||||||
static constexpr auto LOG_TAG = "Metatags";
|
|
||||||
|
|
||||||
public:
|
|
||||||
Metatags() = default;
|
|
||||||
Metatags(const json& j)
|
|
||||||
{
|
|
||||||
fromJson(j);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// https://www.musicpd.org/doc/html/protocol.html#tags
|
|
||||||
/// the duration of the song
|
|
||||||
std::optional<float> duration;
|
|
||||||
/// the artist name. Its meaning is not well-defined; see “composer” and “performer” for more specific tags.
|
|
||||||
std::optional<std::vector<std::string>> artist;
|
|
||||||
/// same as artist, but for sorting. This usually omits prefixes such as “The”.
|
|
||||||
std::optional<std::vector<std::string>> artist_sort;
|
|
||||||
/// the album name.
|
|
||||||
std::optional<std::string> album;
|
|
||||||
/// same as album, but for sorting.
|
|
||||||
std::optional<std::string> album_sort;
|
|
||||||
/// on multi-artist albums, this is the artist name which shall be used for the whole album. The exact meaning of this tag is not well-defined.
|
|
||||||
std::optional<std::vector<std::string>> album_artist;
|
|
||||||
/// same as albumartist, but for sorting.
|
|
||||||
std::optional<std::vector<std::string>> album_artist_sort;
|
|
||||||
/// a name for this song. This is not the song title. The exact meaning of this tag is not well-defined. It is often used by badly configured internet radio
|
|
||||||
/// stations with broken tags to squeeze both the artist name and the song title in one tag.
|
|
||||||
std::optional<std::string> name;
|
|
||||||
/// the song’s release date. This is usually a 4-digit year.
|
|
||||||
std::optional<std::string> date;
|
|
||||||
/// the song’s original release date.
|
|
||||||
std::optional<std::string> original_date;
|
|
||||||
/// the artist who performed the song.
|
|
||||||
std::optional<std::string> performer;
|
|
||||||
/// the conductor who conducted the song.
|
|
||||||
std::optional<std::string> conductor;
|
|
||||||
/// “a work is a distinct intellectual or artistic creation, which can be expressed in the form of one or more audio recordings”
|
|
||||||
std::optional<std::string> work;
|
|
||||||
/// “used if the sound belongs to a larger category of sounds/music” (from the IDv2.4.0 TIT1 description).
|
|
||||||
std::optional<std::string> grouping;
|
|
||||||
/// the name of the label or publisher.
|
|
||||||
std::optional<std::string> label;
|
|
||||||
/// the artist id in the MusicBrainz database.
|
|
||||||
std::optional<std::string> musicbrainz_artist_id;
|
|
||||||
/// the album id in the MusicBrainz database.
|
|
||||||
std::optional<std::string> musicbrainz_album_id;
|
|
||||||
/// the album artist id in the MusicBrainz database.
|
|
||||||
std::optional<std::string> musicbrainz_album_artist_id;
|
|
||||||
/// the track id in the MusicBrainz database.
|
|
||||||
std::optional<std::string> musicbrainz_track_id;
|
|
||||||
/// the release track id in the MusicBrainz database.
|
|
||||||
std::optional<std::string> musicbrainz_release_track_id;
|
|
||||||
/// the work id in the MusicBrainz database.
|
|
||||||
std::optional<std::string> musicbrainz_work_id;
|
|
||||||
|
|
||||||
/// https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/
|
|
||||||
/// A unique identity for this track within the context of an MPRIS object (eg: tracklist).
|
|
||||||
std::optional<std::string> track_id;
|
|
||||||
/// 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;
|
|
||||||
/// The track lyrics
|
|
||||||
std::optional<std::string> lyrics;
|
|
||||||
/// The speed of the music, in beats per minute
|
|
||||||
std::optional<uint16_t> bpm;
|
|
||||||
/// Float: An automatically-generated rating, based on things such as how often it has been played. This should be in the range 0.0 to 1.0
|
|
||||||
std::optional<float> auto_rating;
|
|
||||||
/// A (list of) freeform comment(s)
|
|
||||||
std::optional<std::vector<std::string>> comment;
|
|
||||||
/// The composer(s) of the track
|
|
||||||
std::optional<std::vector<std::string>> composer;
|
|
||||||
/// Date/Time: When the track was created. Usually only the year component will be useful
|
|
||||||
std::optional<std::string> content_created;
|
|
||||||
/// Integer: The disc number on the album that this track is from
|
|
||||||
std::optional<uint16_t> disc_number;
|
|
||||||
/// Date/Time: When the track was first played
|
|
||||||
std::optional<std::string> first_used;
|
|
||||||
/// List of Strings: The genre(s) of the track
|
|
||||||
std::optional<std::vector<std::string>> genre;
|
|
||||||
/// Date/Time: When the track was last played
|
|
||||||
std::optional<std::string> last_used;
|
|
||||||
/// List of Strings: The lyricist(s) of the track
|
|
||||||
std::optional<std::vector<std::string>> lyricist;
|
|
||||||
/// String: The track title
|
|
||||||
std::optional<std::string> title;
|
|
||||||
/// Integer: The track number on the album disc
|
|
||||||
std::optional<uint16_t> track_number;
|
|
||||||
/// URI: The location of the media file.
|
|
||||||
std::optional<std::string> url;
|
|
||||||
/// Integer: The number of times the track has been played.
|
|
||||||
std::optional<uint16_t> use_count;
|
|
||||||
/// Float: A user-specified rating. This should be in the range 0.0 to 1.0.
|
|
||||||
std::optional<float> user_rating;
|
|
||||||
|
|
||||||
/// Spotify artist id
|
|
||||||
std::optional<std::string> spotify_artist_id;
|
|
||||||
/// Spotify track id
|
|
||||||
std::optional<std::string> spotify_track_id;
|
|
||||||
|
|
||||||
json toJson() const
|
|
||||||
{
|
|
||||||
json j(json::object());
|
|
||||||
addTag(j, "trackId", track_id);
|
|
||||||
addTag(j, "duration", duration);
|
|
||||||
addTag(j, "artist", artist);
|
|
||||||
addTag(j, "artistSort", artist_sort);
|
|
||||||
addTag(j, "album", album);
|
|
||||||
addTag(j, "albumSort", album_sort);
|
|
||||||
addTag(j, "albumArtist", album_artist);
|
|
||||||
addTag(j, "albumArtistSort", album_artist_sort);
|
|
||||||
addTag(j, "name", name);
|
|
||||||
addTag(j, "date", date);
|
|
||||||
addTag(j, "originalDate", original_date);
|
|
||||||
addTag(j, "performer", performer);
|
|
||||||
addTag(j, "conductor", conductor);
|
|
||||||
addTag(j, "work", work);
|
|
||||||
addTag(j, "grouping", grouping);
|
|
||||||
addTag(j, "label", label);
|
|
||||||
addTag(j, "musicbrainzArtistId", musicbrainz_artist_id);
|
|
||||||
addTag(j, "musicbrainzAlbumId", musicbrainz_album_id);
|
|
||||||
addTag(j, "musicbrainzAlbumArtistId", musicbrainz_album_artist_id);
|
|
||||||
addTag(j, "musicbrainzTrackId", musicbrainz_track_id);
|
|
||||||
addTag(j, "musicbrainzReleaseTrackId", musicbrainz_release_track_id);
|
|
||||||
addTag(j, "musicbrainzWorkId", musicbrainz_work_id);
|
|
||||||
addTag(j, "lyrics", lyrics);
|
|
||||||
addTag(j, "bpm", bpm);
|
|
||||||
addTag(j, "autoRating", auto_rating);
|
|
||||||
addTag(j, "comment", comment);
|
|
||||||
addTag(j, "composer", composer);
|
|
||||||
addTag(j, "contentCreated", content_created);
|
|
||||||
addTag(j, "discNumber", disc_number);
|
|
||||||
addTag(j, "firstUsed", first_used);
|
|
||||||
addTag(j, "genre", genre);
|
|
||||||
addTag(j, "lastUsed", last_used);
|
|
||||||
addTag(j, "lyricist", lyricist);
|
|
||||||
addTag(j, "title", title);
|
|
||||||
addTag(j, "trackNumber", track_number);
|
|
||||||
addTag(j, "url", url);
|
|
||||||
addTag(j, "artUrl", art_url);
|
|
||||||
addTag(j, "useCount", use_count);
|
|
||||||
addTag(j, "userRating", user_rating);
|
|
||||||
addTag(j, "spotifyArtistId", spotify_artist_id);
|
|
||||||
addTag(j, "spotifyTrackId", spotify_track_id);
|
|
||||||
return j;
|
|
||||||
}
|
|
||||||
|
|
||||||
void fromJson(const json& j)
|
|
||||||
{
|
|
||||||
static std::set<std::string> supported_tags = {"trackId",
|
|
||||||
"duration",
|
|
||||||
"artist",
|
|
||||||
"artistSort",
|
|
||||||
"album",
|
|
||||||
"albumSort",
|
|
||||||
"albumArtist",
|
|
||||||
"albumArtistSort",
|
|
||||||
"name",
|
|
||||||
"date",
|
|
||||||
"originalDate",
|
|
||||||
"performer",
|
|
||||||
"conductor",
|
|
||||||
"work",
|
|
||||||
"grouping",
|
|
||||||
"label",
|
|
||||||
"musicbrainzArtistId",
|
|
||||||
"musicbrainzAlbumId",
|
|
||||||
"musicbrainzAlbumArtistId",
|
|
||||||
"musicbrainzTrackId",
|
|
||||||
"musicbrainzReleaseTrackId",
|
|
||||||
"musicbrainzWorkId",
|
|
||||||
"lyrics",
|
|
||||||
"bpm",
|
|
||||||
"autoRating",
|
|
||||||
"comment",
|
|
||||||
"composer",
|
|
||||||
"contentCreated",
|
|
||||||
"discNumber",
|
|
||||||
"firstUsed",
|
|
||||||
"genre",
|
|
||||||
"lastUsed",
|
|
||||||
"lyricist",
|
|
||||||
"title",
|
|
||||||
"trackNumber",
|
|
||||||
"url",
|
|
||||||
"artUrl",
|
|
||||||
"useCount",
|
|
||||||
"userRating",
|
|
||||||
"spotifyArtistId",
|
|
||||||
"spotifyTrackId"};
|
|
||||||
for (const auto& element : j.items())
|
|
||||||
{
|
|
||||||
if (supported_tags.find(element.key()) == supported_tags.end())
|
|
||||||
LOG(WARNING, LOG_TAG) << "Tag not supoorted: " << element.key() << "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
readTag(j, "trackId", track_id);
|
|
||||||
readTag(j, "duration", duration);
|
|
||||||
readTag(j, "artist", artist);
|
|
||||||
readTag(j, "artistSort", artist_sort);
|
|
||||||
readTag(j, "album", album);
|
|
||||||
readTag(j, "albumSort", album_sort);
|
|
||||||
readTag(j, "albumArtist", album_artist);
|
|
||||||
readTag(j, "albumArtistSort", album_artist_sort);
|
|
||||||
readTag(j, "name", name);
|
|
||||||
readTag(j, "date", date);
|
|
||||||
readTag(j, "originalDate", original_date);
|
|
||||||
readTag(j, "performer", performer);
|
|
||||||
readTag(j, "conductor", conductor);
|
|
||||||
readTag(j, "work", work);
|
|
||||||
readTag(j, "grouping", grouping);
|
|
||||||
readTag(j, "label", label);
|
|
||||||
readTag(j, "musicbrainzArtistId", musicbrainz_artist_id);
|
|
||||||
readTag(j, "musicbrainzAlbumId", musicbrainz_album_id);
|
|
||||||
readTag(j, "musicbrainzAlbumArtistId", musicbrainz_album_artist_id);
|
|
||||||
readTag(j, "musicbrainzTrackId", musicbrainz_track_id);
|
|
||||||
readTag(j, "musicbrainzReleaseTrackId", musicbrainz_release_track_id);
|
|
||||||
readTag(j, "musicbrainzWorkId", musicbrainz_work_id);
|
|
||||||
readTag(j, "lyrics", lyrics);
|
|
||||||
readTag(j, "bpm", bpm);
|
|
||||||
readTag(j, "autoRating", auto_rating);
|
|
||||||
readTag(j, "comment", comment);
|
|
||||||
readTag(j, "composer", composer);
|
|
||||||
readTag(j, "contentCreated", content_created);
|
|
||||||
readTag(j, "discNumber", disc_number);
|
|
||||||
readTag(j, "firstUsed", first_used);
|
|
||||||
readTag(j, "genre", genre);
|
|
||||||
readTag(j, "lastUsed", last_used);
|
|
||||||
readTag(j, "lyricist", lyricist);
|
|
||||||
readTag(j, "title", title);
|
|
||||||
readTag(j, "trackNumber", track_number);
|
|
||||||
readTag(j, "url", url);
|
|
||||||
readTag(j, "artUrl", art_url);
|
|
||||||
readTag(j, "useCount", use_count);
|
|
||||||
readTag(j, "userRating", user_rating);
|
|
||||||
readTag(j, "spotifyArtistId", spotify_artist_id);
|
|
||||||
readTag(j, "spotifyTrackId", spotify_track_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool operator==(const Metatags& other) const
|
|
||||||
{
|
|
||||||
// expensive, but not called ofetn and less typing
|
|
||||||
return (toJson() == other.toJson());
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
template <typename T>
|
|
||||||
void readTag(const json& j, const std::string& tag, std::optional<T>& dest) const
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!j.contains(tag))
|
|
||||||
dest = std::nullopt;
|
|
||||||
else
|
|
||||||
dest = j[tag].get<T>();
|
|
||||||
}
|
|
||||||
catch (const std::exception& e)
|
|
||||||
{
|
|
||||||
LOG(ERROR, LOG_TAG) << "failed to read tag: '" << tag << "': " << e.what() << '\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
void addTag(json& j, const std::string& tag, const std::optional<T>& source) const
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!source.has_value())
|
|
||||||
{
|
|
||||||
if (j.contains(tag))
|
|
||||||
j.erase(tag);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
j[tag] = source.value();
|
|
||||||
}
|
|
||||||
catch (const std::exception& e)
|
|
||||||
{
|
|
||||||
LOG(ERROR, LOG_TAG) << "failed to add tag: '" << tag << "': " << e.what() << '\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
#endif
|
|
|
@ -1,344 +0,0 @@
|
||||||
/***
|
|
||||||
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 PROPERTIES_HPP
|
|
||||||
#define PROPERTIES_HPP
|
|
||||||
|
|
||||||
#include <set>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include <optional>
|
|
||||||
|
|
||||||
#include "common/aixlog.hpp"
|
|
||||||
#include "common/json.hpp"
|
|
||||||
#include "common/metatags.hpp"
|
|
||||||
|
|
||||||
|
|
||||||
using json = nlohmann::json;
|
|
||||||
|
|
||||||
enum class PlaybackStatus
|
|
||||||
{
|
|
||||||
kPlaying = 0,
|
|
||||||
kPaused = 1,
|
|
||||||
kStopped = 2,
|
|
||||||
kUnknown = 3
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
static std::string to_string(PlaybackStatus playback_status)
|
|
||||||
{
|
|
||||||
switch (playback_status)
|
|
||||||
{
|
|
||||||
case PlaybackStatus::kPlaying:
|
|
||||||
return "playing";
|
|
||||||
case PlaybackStatus::kPaused:
|
|
||||||
return "paused";
|
|
||||||
case PlaybackStatus::kStopped:
|
|
||||||
return "stopped";
|
|
||||||
default:
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static std::ostream& operator<<(std::ostream& os, PlaybackStatus playback_status)
|
|
||||||
{
|
|
||||||
os << to_string(playback_status);
|
|
||||||
return os;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static PlaybackStatus playback_status_from_string(std::string& status)
|
|
||||||
{
|
|
||||||
if (status == "playing")
|
|
||||||
return PlaybackStatus::kPlaying;
|
|
||||||
else if (status == "paused")
|
|
||||||
return PlaybackStatus::kPaused;
|
|
||||||
else if (status == "stopped")
|
|
||||||
return PlaybackStatus::kStopped;
|
|
||||||
else
|
|
||||||
return PlaybackStatus::kUnknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static std::istream& operator>>(std::istream& is, PlaybackStatus& playback_status)
|
|
||||||
{
|
|
||||||
std::string status;
|
|
||||||
playback_status = PlaybackStatus::kUnknown;
|
|
||||||
if (is >> status)
|
|
||||||
playback_status = playback_status_from_string(status);
|
|
||||||
else
|
|
||||||
playback_status = PlaybackStatus::kUnknown;
|
|
||||||
return is;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
enum class LoopStatus
|
|
||||||
{
|
|
||||||
kNone = 0,
|
|
||||||
kTrack = 1,
|
|
||||||
kPlaylist = 2,
|
|
||||||
kUnknown = 3
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
static std::string to_string(LoopStatus loop_status)
|
|
||||||
{
|
|
||||||
switch (loop_status)
|
|
||||||
{
|
|
||||||
case LoopStatus::kNone:
|
|
||||||
return "none";
|
|
||||||
case LoopStatus::kTrack:
|
|
||||||
return "track";
|
|
||||||
case LoopStatus::kPlaylist:
|
|
||||||
return "playlist";
|
|
||||||
default:
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static std::ostream& operator<<(std::ostream& os, LoopStatus loop_status)
|
|
||||||
{
|
|
||||||
os << to_string(loop_status);
|
|
||||||
return os;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static LoopStatus loop_status_from_string(std::string& status)
|
|
||||||
{
|
|
||||||
if (status == "none")
|
|
||||||
return LoopStatus::kNone;
|
|
||||||
else if (status == "track")
|
|
||||||
return LoopStatus::kTrack;
|
|
||||||
else if (status == "playlist")
|
|
||||||
return LoopStatus::kPlaylist;
|
|
||||||
else
|
|
||||||
return LoopStatus::kUnknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static std::istream& operator>>(std::istream& is, LoopStatus& loop_status)
|
|
||||||
{
|
|
||||||
std::string status;
|
|
||||||
if (is >> status)
|
|
||||||
loop_status = loop_status_from_string(status);
|
|
||||||
else
|
|
||||||
loop_status = LoopStatus::kUnknown;
|
|
||||||
|
|
||||||
return is;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Properties
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
Properties() = default;
|
|
||||||
Properties(const json& j)
|
|
||||||
{
|
|
||||||
fromJson(j);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Meta data
|
|
||||||
std::optional<Metatags> metatags;
|
|
||||||
/// https://www.musicpd.org/doc/html/protocol.html#tags
|
|
||||||
/// The current playback status
|
|
||||||
std::optional<PlaybackStatus> playback_status;
|
|
||||||
/// The current loop / repeat status
|
|
||||||
std::optional<LoopStatus> loop_status;
|
|
||||||
/// The current playback rate
|
|
||||||
std::optional<float> rate;
|
|
||||||
/// A value of false indicates that playback is progressing linearly through a playlist, while true means playback is progressing through a playlist in some
|
|
||||||
/// other order.
|
|
||||||
std::optional<bool> shuffle;
|
|
||||||
/// The volume level between 0-100
|
|
||||||
std::optional<int> volume;
|
|
||||||
/// The current track position in seconds
|
|
||||||
std::optional<float> position;
|
|
||||||
/// The minimum value which the Rate property can take. Clients should not attempt to set the Rate property below this value
|
|
||||||
std::optional<float> minimum_rate;
|
|
||||||
/// The maximum value which the Rate property can take. Clients should not attempt to set the Rate property above this value
|
|
||||||
std::optional<float> maximum_rate;
|
|
||||||
/// Whether the client can call the Next method on this interface and expect the current track to change
|
|
||||||
bool can_go_next = false;
|
|
||||||
/// Whether the client can call the Previous method on this interface and expect the current track to change
|
|
||||||
bool can_go_previous = false;
|
|
||||||
/// Whether playback can be started using "play" or "playPause"
|
|
||||||
bool can_play = false;
|
|
||||||
/// Whether playback can be paused using "pause" or "playPause"
|
|
||||||
bool can_pause = false;
|
|
||||||
/// Whether the client can control the playback position using "seek" and "setPosition". This may be different for different tracks
|
|
||||||
bool can_seek = false;
|
|
||||||
/// Whether the media player may be controlled over this interface
|
|
||||||
bool can_control = false;
|
|
||||||
|
|
||||||
json toJson() const
|
|
||||||
{
|
|
||||||
json j;
|
|
||||||
if (playback_status.has_value())
|
|
||||||
addTag(j, "playbackStatus", std::optional<std::string>(to_string(playback_status.value())));
|
|
||||||
if (loop_status.has_value())
|
|
||||||
addTag(j, "loopStatus", std::optional<std::string>(to_string(loop_status.value())));
|
|
||||||
addTag(j, "rate", rate);
|
|
||||||
addTag(j, "shuffle", shuffle);
|
|
||||||
addTag(j, "volume", volume);
|
|
||||||
addTag(j, "position", position);
|
|
||||||
addTag(j, "minimumRate", minimum_rate);
|
|
||||||
addTag(j, "maximumRate", maximum_rate);
|
|
||||||
addTag(j, "canGoNext", can_go_next);
|
|
||||||
addTag(j, "canGoPrevious", can_go_previous);
|
|
||||||
addTag(j, "canPlay", can_play);
|
|
||||||
addTag(j, "canPause", can_pause);
|
|
||||||
addTag(j, "canSeek", can_seek);
|
|
||||||
addTag(j, "canControl", can_control);
|
|
||||||
if (metatags.has_value())
|
|
||||||
addTag(j, "metadata", metatags->toJson());
|
|
||||||
return j;
|
|
||||||
}
|
|
||||||
|
|
||||||
void fromJson(const json& j)
|
|
||||||
{
|
|
||||||
static std::set<std::string> rw_props = {"loopStatus", "shuffle", "volume", "rate"};
|
|
||||||
static std::set<std::string> ro_props = {"playbackStatus", "loopStatus", "shuffle", "volume", "position", "minimumRate", "maximumRate",
|
|
||||||
"canGoNext", "canGoPrevious", "canPlay", "canPause", "canSeek", "canControl", "metadata"};
|
|
||||||
for (const auto& element : j.items())
|
|
||||||
{
|
|
||||||
bool is_rw = (rw_props.find(element.key()) != rw_props.end());
|
|
||||||
bool is_ro = (ro_props.find(element.key()) != ro_props.end());
|
|
||||||
if (!is_rw && !is_ro)
|
|
||||||
LOG(WARNING, LOG_TAG) << "Property not supoorted: " << element.key() << "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<std::string> opt;
|
|
||||||
|
|
||||||
readTag(j, "playbackStatus", opt);
|
|
||||||
if (opt.has_value())
|
|
||||||
playback_status = playback_status_from_string(opt.value());
|
|
||||||
else
|
|
||||||
playback_status = std::nullopt;
|
|
||||||
|
|
||||||
readTag(j, "loopStatus", opt);
|
|
||||||
if (opt.has_value())
|
|
||||||
loop_status = loop_status_from_string(opt.value());
|
|
||||||
else
|
|
||||||
loop_status = std::nullopt;
|
|
||||||
readTag(j, "rate", rate);
|
|
||||||
readTag(j, "shuffle", shuffle);
|
|
||||||
readTag(j, "volume", volume);
|
|
||||||
readTag(j, "position", position);
|
|
||||||
readTag(j, "minimumRate", minimum_rate);
|
|
||||||
readTag(j, "maximumRate", maximum_rate);
|
|
||||||
readTag(j, "canGoNext", can_go_next, false);
|
|
||||||
readTag(j, "canGoPrevious", can_go_previous, false);
|
|
||||||
readTag(j, "canPlay", can_play, false);
|
|
||||||
readTag(j, "canPause", can_pause, false);
|
|
||||||
readTag(j, "canSeek", can_seek, false);
|
|
||||||
readTag(j, "canControl", can_control, false);
|
|
||||||
|
|
||||||
if (j.contains("metadata"))
|
|
||||||
{
|
|
||||||
Metatags m;
|
|
||||||
m.fromJson(j["metadata"]);
|
|
||||||
metatags = m;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
metatags = std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool operator==(const Properties& other) const
|
|
||||||
{
|
|
||||||
// expensive, but not called ofetn and less typing
|
|
||||||
return (toJson() == other.toJson());
|
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
// return (playback_status == other.playback_status &&
|
|
||||||
// loop_status == other.loop_status &&
|
|
||||||
// rate == other.rate &&
|
|
||||||
// shuffle == other.shuffle &&
|
|
||||||
// volume == other.volume &&
|
|
||||||
// position == other.position &&
|
|
||||||
// minimum_rate == other.minimum_rate &&
|
|
||||||
// maximum_rate == other.maximum_rate &&
|
|
||||||
// can_go_next == other.can_go_next &&
|
|
||||||
// can_go_previous == other.can_go_previous &&
|
|
||||||
// can_play == other.can_play &&
|
|
||||||
// can_pause == other.can_pause &&
|
|
||||||
// can_seek == other.can_seek &&
|
|
||||||
// can_control == other.can_control);
|
|
||||||
// clang-format on
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
template <typename T>
|
|
||||||
void readTag(const json& j, const std::string& tag, std::optional<T>& dest) const
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!j.contains(tag))
|
|
||||||
dest = std::nullopt;
|
|
||||||
else
|
|
||||||
dest = j[tag].get<T>();
|
|
||||||
}
|
|
||||||
catch (const std::exception& e)
|
|
||||||
{
|
|
||||||
LOG(ERROR, LOG_TAG) << "failed to read tag: '" << tag << "': " << e.what() << '\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
void readTag(const json& j, const std::string& tag, T& dest, const T& def) const
|
|
||||||
{
|
|
||||||
std::optional<T> val;
|
|
||||||
readTag(j, tag, val);
|
|
||||||
if (val.has_value())
|
|
||||||
dest = val.value();
|
|
||||||
else
|
|
||||||
dest = def;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
void addTag(json& j, const std::string& tag, const std::optional<T>& source) const
|
|
||||||
{
|
|
||||||
if (!source.has_value())
|
|
||||||
{
|
|
||||||
if (j.contains(tag))
|
|
||||||
j.erase(tag);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
addTag(j, tag, source.value());
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
void addTag(json& j, const std::string& tag, const T& source) const
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
j[tag] = source;
|
|
||||||
}
|
|
||||||
catch (const std::exception& e)
|
|
||||||
{
|
|
||||||
LOG(ERROR, LOG_TAG) << "failed to add tag: '" << tag << "': " << e.what() << '\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
static constexpr auto LOG_TAG = "Properties";
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
#endif
|
|
|
@ -27,6 +27,8 @@ set(SERVER_SOURCES
|
||||||
streamreader/librespot_stream.cpp
|
streamreader/librespot_stream.cpp
|
||||||
streamreader/meta_stream.cpp
|
streamreader/meta_stream.cpp
|
||||||
streamreader/watchdog.cpp
|
streamreader/watchdog.cpp
|
||||||
|
streamreader/properties.cpp
|
||||||
|
streamreader/metatags.cpp
|
||||||
streamreader/process_stream.cpp)
|
streamreader/process_stream.cpp)
|
||||||
|
|
||||||
set(SERVER_LIBRARIES
|
set(SERVER_LIBRARIES
|
||||||
|
|
|
@ -44,7 +44,7 @@ endif
|
||||||
|
|
||||||
CXXFLAGS += $(ADD_CFLAGS) -std=c++17 -Wall -Wextra -Wpedantic -Wno-unused-function -DBOOST_ERROR_CODE_HEADER_ONLY -DHAS_FLAC -DHAS_OGG -DHAS_VORBIS -DHAS_VORBIS_ENC -DHAS_OPUS -DHAS_SOXR -DVERSION=\"$(VERSION)\" -I. -I.. -I../common
|
CXXFLAGS += $(ADD_CFLAGS) -std=c++17 -Wall -Wextra -Wpedantic -Wno-unused-function -DBOOST_ERROR_CODE_HEADER_ONLY -DHAS_FLAC -DHAS_OGG -DHAS_VORBIS -DHAS_VORBIS_ENC -DHAS_OPUS -DHAS_SOXR -DVERSION=\"$(VERSION)\" -I. -I.. -I../common
|
||||||
LDFLAGS += $(ADD_LDFLAGS) -lvorbis -lvorbisenc -logg -lFLAC -lopus -lsoxr
|
LDFLAGS += $(ADD_LDFLAGS) -lvorbis -lvorbisenc -logg -lFLAC -lopus -lsoxr
|
||||||
OBJ = snapserver.o server.o config.o control_server.o control_session_tcp.o control_session_http.o control_session_ws.o stream_server.o stream_session.o stream_session_tcp.o stream_session_ws.o streamreader/stream_uri.o streamreader/base64.o streamreader/stream_manager.o streamreader/pcm_stream.o streamreader/posix_stream.o streamreader/pipe_stream.o streamreader/file_stream.o streamreader/tcp_stream.o streamreader/process_stream.o streamreader/airplay_stream.o streamreader/meta_stream.o streamreader/librespot_stream.o streamreader/watchdog.o streamreader/control_error.o streamreader/stream_control.o encoder/encoder_factory.o encoder/flac_encoder.o encoder/opus_encoder.o encoder/pcm_encoder.o encoder/null_encoder.o encoder/ogg_encoder.o ../common/sample_format.o ../common/resampler.o
|
OBJ = snapserver.o server.o config.o control_server.o control_session_tcp.o control_session_http.o control_session_ws.o stream_server.o stream_session.o stream_session_tcp.o stream_session_ws.o streamreader/stream_uri.o streamreader/base64.o streamreader/stream_manager.o streamreader/pcm_stream.o streamreader/posix_stream.o streamreader/pipe_stream.o streamreader/file_stream.o streamreader/tcp_stream.o streamreader/process_stream.o streamreader/airplay_stream.o streamreader/meta_stream.o streamreader/librespot_stream.o streamreader/watchdog.o streamreader/control_error.o streamreader/stream_control.o streamreader/metatags.o streamreader/properties.o encoder/encoder_factory.o encoder/flac_encoder.o encoder/opus_encoder.o encoder/pcm_encoder.o encoder/null_encoder.o encoder/ogg_encoder.o ../common/sample_format.o ../common/resampler.o
|
||||||
|
|
||||||
ifneq (,$(TARGET))
|
ifneq (,$(TARGET))
|
||||||
CXXFLAGS += -D$(TARGET)
|
CXXFLAGS += -D$(TARGET)
|
||||||
|
|
214
server/streamreader/metatags.cpp
Normal file
214
server/streamreader/metatags.cpp
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
/***
|
||||||
|
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/>.
|
||||||
|
***/
|
||||||
|
|
||||||
|
#include "metatags.hpp"
|
||||||
|
|
||||||
|
static constexpr auto LOG_TAG = "Metatags";
|
||||||
|
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
template <typename T>
|
||||||
|
void readTag(const json& j, const std::string& tag, std::optional<T>& dest)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!j.contains(tag))
|
||||||
|
dest = std::nullopt;
|
||||||
|
else
|
||||||
|
dest = j[tag].get<T>();
|
||||||
|
}
|
||||||
|
catch (const std::exception& e)
|
||||||
|
{
|
||||||
|
LOG(ERROR, LOG_TAG) << "failed to read tag: '" << tag << "': " << e.what() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void addTag(json& j, const std::string& tag, const std::optional<T>& source)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!source.has_value())
|
||||||
|
{
|
||||||
|
if (j.contains(tag))
|
||||||
|
j.erase(tag);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
j[tag] = source.value();
|
||||||
|
}
|
||||||
|
catch (const std::exception& e)
|
||||||
|
{
|
||||||
|
LOG(ERROR, LOG_TAG) << "failed to add tag: '" << tag << "': " << e.what() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
Metatags::Metatags(const json& j)
|
||||||
|
{
|
||||||
|
fromJson(j);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
json Metatags::toJson() const
|
||||||
|
{
|
||||||
|
json j(json::object());
|
||||||
|
addTag(j, "trackId", track_id);
|
||||||
|
addTag(j, "duration", duration);
|
||||||
|
addTag(j, "artist", artist);
|
||||||
|
addTag(j, "artistSort", artist_sort);
|
||||||
|
addTag(j, "album", album);
|
||||||
|
addTag(j, "albumSort", album_sort);
|
||||||
|
addTag(j, "albumArtist", album_artist);
|
||||||
|
addTag(j, "albumArtistSort", album_artist_sort);
|
||||||
|
addTag(j, "name", name);
|
||||||
|
addTag(j, "date", date);
|
||||||
|
addTag(j, "originalDate", original_date);
|
||||||
|
addTag(j, "performer", performer);
|
||||||
|
addTag(j, "conductor", conductor);
|
||||||
|
addTag(j, "work", work);
|
||||||
|
addTag(j, "grouping", grouping);
|
||||||
|
addTag(j, "label", label);
|
||||||
|
addTag(j, "musicbrainzArtistId", musicbrainz_artist_id);
|
||||||
|
addTag(j, "musicbrainzAlbumId", musicbrainz_album_id);
|
||||||
|
addTag(j, "musicbrainzAlbumArtistId", musicbrainz_album_artist_id);
|
||||||
|
addTag(j, "musicbrainzTrackId", musicbrainz_track_id);
|
||||||
|
addTag(j, "musicbrainzReleaseTrackId", musicbrainz_release_track_id);
|
||||||
|
addTag(j, "musicbrainzWorkId", musicbrainz_work_id);
|
||||||
|
addTag(j, "lyrics", lyrics);
|
||||||
|
addTag(j, "bpm", bpm);
|
||||||
|
addTag(j, "autoRating", auto_rating);
|
||||||
|
addTag(j, "comment", comment);
|
||||||
|
addTag(j, "composer", composer);
|
||||||
|
addTag(j, "contentCreated", content_created);
|
||||||
|
addTag(j, "discNumber", disc_number);
|
||||||
|
addTag(j, "firstUsed", first_used);
|
||||||
|
addTag(j, "genre", genre);
|
||||||
|
addTag(j, "lastUsed", last_used);
|
||||||
|
addTag(j, "lyricist", lyricist);
|
||||||
|
addTag(j, "title", title);
|
||||||
|
addTag(j, "trackNumber", track_number);
|
||||||
|
addTag(j, "url", url);
|
||||||
|
addTag(j, "artUrl", art_url);
|
||||||
|
addTag(j, "useCount", use_count);
|
||||||
|
addTag(j, "userRating", user_rating);
|
||||||
|
addTag(j, "spotifyArtistId", spotify_artist_id);
|
||||||
|
addTag(j, "spotifyTrackId", spotify_track_id);
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Metatags::fromJson(const json& j)
|
||||||
|
{
|
||||||
|
static std::set<std::string> supported_tags = {"trackId",
|
||||||
|
"duration",
|
||||||
|
"artist",
|
||||||
|
"artistSort",
|
||||||
|
"album",
|
||||||
|
"albumSort",
|
||||||
|
"albumArtist",
|
||||||
|
"albumArtistSort",
|
||||||
|
"name",
|
||||||
|
"date",
|
||||||
|
"originalDate",
|
||||||
|
"performer",
|
||||||
|
"conductor",
|
||||||
|
"work",
|
||||||
|
"grouping",
|
||||||
|
"label",
|
||||||
|
"musicbrainzArtistId",
|
||||||
|
"musicbrainzAlbumId",
|
||||||
|
"musicbrainzAlbumArtistId",
|
||||||
|
"musicbrainzTrackId",
|
||||||
|
"musicbrainzReleaseTrackId",
|
||||||
|
"musicbrainzWorkId",
|
||||||
|
"lyrics",
|
||||||
|
"bpm",
|
||||||
|
"autoRating",
|
||||||
|
"comment",
|
||||||
|
"composer",
|
||||||
|
"contentCreated",
|
||||||
|
"discNumber",
|
||||||
|
"firstUsed",
|
||||||
|
"genre",
|
||||||
|
"lastUsed",
|
||||||
|
"lyricist",
|
||||||
|
"title",
|
||||||
|
"trackNumber",
|
||||||
|
"url",
|
||||||
|
"artUrl",
|
||||||
|
"useCount",
|
||||||
|
"userRating",
|
||||||
|
"spotifyArtistId",
|
||||||
|
"spotifyTrackId"};
|
||||||
|
for (const auto& element : j.items())
|
||||||
|
{
|
||||||
|
if (supported_tags.find(element.key()) == supported_tags.end())
|
||||||
|
LOG(WARNING, LOG_TAG) << "Tag not supoorted: " << element.key() << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
readTag(j, "trackId", track_id);
|
||||||
|
readTag(j, "duration", duration);
|
||||||
|
readTag(j, "artist", artist);
|
||||||
|
readTag(j, "artistSort", artist_sort);
|
||||||
|
readTag(j, "album", album);
|
||||||
|
readTag(j, "albumSort", album_sort);
|
||||||
|
readTag(j, "albumArtist", album_artist);
|
||||||
|
readTag(j, "albumArtistSort", album_artist_sort);
|
||||||
|
readTag(j, "name", name);
|
||||||
|
readTag(j, "date", date);
|
||||||
|
readTag(j, "originalDate", original_date);
|
||||||
|
readTag(j, "performer", performer);
|
||||||
|
readTag(j, "conductor", conductor);
|
||||||
|
readTag(j, "work", work);
|
||||||
|
readTag(j, "grouping", grouping);
|
||||||
|
readTag(j, "label", label);
|
||||||
|
readTag(j, "musicbrainzArtistId", musicbrainz_artist_id);
|
||||||
|
readTag(j, "musicbrainzAlbumId", musicbrainz_album_id);
|
||||||
|
readTag(j, "musicbrainzAlbumArtistId", musicbrainz_album_artist_id);
|
||||||
|
readTag(j, "musicbrainzTrackId", musicbrainz_track_id);
|
||||||
|
readTag(j, "musicbrainzReleaseTrackId", musicbrainz_release_track_id);
|
||||||
|
readTag(j, "musicbrainzWorkId", musicbrainz_work_id);
|
||||||
|
readTag(j, "lyrics", lyrics);
|
||||||
|
readTag(j, "bpm", bpm);
|
||||||
|
readTag(j, "autoRating", auto_rating);
|
||||||
|
readTag(j, "comment", comment);
|
||||||
|
readTag(j, "composer", composer);
|
||||||
|
readTag(j, "contentCreated", content_created);
|
||||||
|
readTag(j, "discNumber", disc_number);
|
||||||
|
readTag(j, "firstUsed", first_used);
|
||||||
|
readTag(j, "genre", genre);
|
||||||
|
readTag(j, "lastUsed", last_used);
|
||||||
|
readTag(j, "lyricist", lyricist);
|
||||||
|
readTag(j, "title", title);
|
||||||
|
readTag(j, "trackNumber", track_number);
|
||||||
|
readTag(j, "url", url);
|
||||||
|
readTag(j, "artUrl", art_url);
|
||||||
|
readTag(j, "useCount", use_count);
|
||||||
|
readTag(j, "userRating", user_rating);
|
||||||
|
readTag(j, "spotifyArtistId", spotify_artist_id);
|
||||||
|
readTag(j, "spotifyTrackId", spotify_track_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool Metatags::operator==(const Metatags& other) const
|
||||||
|
{
|
||||||
|
// expensive, but not called ofetn and less typing
|
||||||
|
return (toJson() == other.toJson());
|
||||||
|
}
|
132
server/streamreader/metatags.hpp
Normal file
132
server/streamreader/metatags.hpp
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
/***
|
||||||
|
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 METATAGS_HPP
|
||||||
|
#define METATAGS_HPP
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "common/aixlog.hpp"
|
||||||
|
#include "common/json.hpp"
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
class Metatags
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Metatags() = default;
|
||||||
|
Metatags(const json& j);
|
||||||
|
|
||||||
|
/// https://www.musicpd.org/doc/html/protocol.html#tags
|
||||||
|
/// the duration of the song
|
||||||
|
std::optional<float> duration;
|
||||||
|
/// the artist name. Its meaning is not well-defined; see “composer” and “performer” for more specific tags.
|
||||||
|
std::optional<std::vector<std::string>> artist;
|
||||||
|
/// same as artist, but for sorting. This usually omits prefixes such as “The”.
|
||||||
|
std::optional<std::vector<std::string>> artist_sort;
|
||||||
|
/// the album name.
|
||||||
|
std::optional<std::string> album;
|
||||||
|
/// same as album, but for sorting.
|
||||||
|
std::optional<std::string> album_sort;
|
||||||
|
/// on multi-artist albums, this is the artist name which shall be used for the whole album. The exact meaning of this tag is not well-defined.
|
||||||
|
std::optional<std::vector<std::string>> album_artist;
|
||||||
|
/// same as albumartist, but for sorting.
|
||||||
|
std::optional<std::vector<std::string>> album_artist_sort;
|
||||||
|
/// a name for this song. This is not the song title. The exact meaning of this tag is not well-defined. It is often used by badly configured internet radio
|
||||||
|
/// stations with broken tags to squeeze both the artist name and the song title in one tag.
|
||||||
|
std::optional<std::string> name;
|
||||||
|
/// the song’s release date. This is usually a 4-digit year.
|
||||||
|
std::optional<std::string> date;
|
||||||
|
/// the song’s original release date.
|
||||||
|
std::optional<std::string> original_date;
|
||||||
|
/// the artist who performed the song.
|
||||||
|
std::optional<std::string> performer;
|
||||||
|
/// the conductor who conducted the song.
|
||||||
|
std::optional<std::string> conductor;
|
||||||
|
/// “a work is a distinct intellectual or artistic creation, which can be expressed in the form of one or more audio recordings”
|
||||||
|
std::optional<std::string> work;
|
||||||
|
/// “used if the sound belongs to a larger category of sounds/music” (from the IDv2.4.0 TIT1 description).
|
||||||
|
std::optional<std::string> grouping;
|
||||||
|
/// the name of the label or publisher.
|
||||||
|
std::optional<std::string> label;
|
||||||
|
/// the artist id in the MusicBrainz database.
|
||||||
|
std::optional<std::string> musicbrainz_artist_id;
|
||||||
|
/// the album id in the MusicBrainz database.
|
||||||
|
std::optional<std::string> musicbrainz_album_id;
|
||||||
|
/// the album artist id in the MusicBrainz database.
|
||||||
|
std::optional<std::string> musicbrainz_album_artist_id;
|
||||||
|
/// the track id in the MusicBrainz database.
|
||||||
|
std::optional<std::string> musicbrainz_track_id;
|
||||||
|
/// the release track id in the MusicBrainz database.
|
||||||
|
std::optional<std::string> musicbrainz_release_track_id;
|
||||||
|
/// the work id in the MusicBrainz database.
|
||||||
|
std::optional<std::string> musicbrainz_work_id;
|
||||||
|
|
||||||
|
/// https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata/
|
||||||
|
/// A unique identity for this track within the context of an MPRIS object (eg: tracklist).
|
||||||
|
std::optional<std::string> track_id;
|
||||||
|
/// 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;
|
||||||
|
/// The track lyrics
|
||||||
|
std::optional<std::string> lyrics;
|
||||||
|
/// The speed of the music, in beats per minute
|
||||||
|
std::optional<uint16_t> bpm;
|
||||||
|
/// Float: An automatically-generated rating, based on things such as how often it has been played. This should be in the range 0.0 to 1.0
|
||||||
|
std::optional<float> auto_rating;
|
||||||
|
/// A (list of) freeform comment(s)
|
||||||
|
std::optional<std::vector<std::string>> comment;
|
||||||
|
/// The composer(s) of the track
|
||||||
|
std::optional<std::vector<std::string>> composer;
|
||||||
|
/// Date/Time: When the track was created. Usually only the year component will be useful
|
||||||
|
std::optional<std::string> content_created;
|
||||||
|
/// Integer: The disc number on the album that this track is from
|
||||||
|
std::optional<uint16_t> disc_number;
|
||||||
|
/// Date/Time: When the track was first played
|
||||||
|
std::optional<std::string> first_used;
|
||||||
|
/// List of Strings: The genre(s) of the track
|
||||||
|
std::optional<std::vector<std::string>> genre;
|
||||||
|
/// Date/Time: When the track was last played
|
||||||
|
std::optional<std::string> last_used;
|
||||||
|
/// List of Strings: The lyricist(s) of the track
|
||||||
|
std::optional<std::vector<std::string>> lyricist;
|
||||||
|
/// String: The track title
|
||||||
|
std::optional<std::string> title;
|
||||||
|
/// Integer: The track number on the album disc
|
||||||
|
std::optional<uint16_t> track_number;
|
||||||
|
/// URI: The location of the media file.
|
||||||
|
std::optional<std::string> url;
|
||||||
|
/// Integer: The number of times the track has been played.
|
||||||
|
std::optional<uint16_t> use_count;
|
||||||
|
/// Float: A user-specified rating. This should be in the range 0.0 to 1.0.
|
||||||
|
std::optional<float> user_rating;
|
||||||
|
|
||||||
|
/// Spotify artist id
|
||||||
|
std::optional<std::string> spotify_artist_id;
|
||||||
|
/// Spotify track id
|
||||||
|
std::optional<std::string> spotify_track_id;
|
||||||
|
|
||||||
|
json toJson() const;
|
||||||
|
void fromJson(const json& j);
|
||||||
|
bool operator==(const Metatags& other) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
|
@ -30,7 +30,6 @@
|
||||||
|
|
||||||
#include "common/error_code.hpp"
|
#include "common/error_code.hpp"
|
||||||
#include "common/json.hpp"
|
#include "common/json.hpp"
|
||||||
#include "common/properties.hpp"
|
|
||||||
#include "common/sample_format.hpp"
|
#include "common/sample_format.hpp"
|
||||||
#include "encoder/encoder.hpp"
|
#include "encoder/encoder.hpp"
|
||||||
#include "jsonrpcpp.hpp"
|
#include "jsonrpcpp.hpp"
|
||||||
|
@ -38,6 +37,7 @@
|
||||||
#include "server_settings.hpp"
|
#include "server_settings.hpp"
|
||||||
#include "stream_control.hpp"
|
#include "stream_control.hpp"
|
||||||
#include "stream_uri.hpp"
|
#include "stream_uri.hpp"
|
||||||
|
#include "properties.hpp"
|
||||||
|
|
||||||
|
|
||||||
namespace bp = boost::process;
|
namespace bp = boost::process;
|
||||||
|
|
179
server/streamreader/properties.cpp
Normal file
179
server/streamreader/properties.cpp
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
/***
|
||||||
|
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/>.
|
||||||
|
***/
|
||||||
|
|
||||||
|
#include "properties.hpp"
|
||||||
|
|
||||||
|
static constexpr auto LOG_TAG = "Properties";
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
template <typename T>
|
||||||
|
void readTag(const json& j, const std::string& tag, std::optional<T>& dest)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!j.contains(tag))
|
||||||
|
dest = std::nullopt;
|
||||||
|
else
|
||||||
|
dest = j[tag].get<T>();
|
||||||
|
}
|
||||||
|
catch (const std::exception& e)
|
||||||
|
{
|
||||||
|
LOG(ERROR, LOG_TAG) << "failed to read tag: '" << tag << "': " << e.what() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void readTag(const json& j, const std::string& tag, T& dest, const T& def)
|
||||||
|
{
|
||||||
|
std::optional<T> val;
|
||||||
|
readTag(j, tag, val);
|
||||||
|
if (val.has_value())
|
||||||
|
dest = val.value();
|
||||||
|
else
|
||||||
|
dest = def;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void addTag(json& j, const std::string& tag, const T& source)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
j[tag] = source;
|
||||||
|
}
|
||||||
|
catch (const std::exception& e)
|
||||||
|
{
|
||||||
|
LOG(ERROR, LOG_TAG) << "failed to add tag: '" << tag << "': " << e.what() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void addTag(json& j, const std::string& tag, const std::optional<T>& source)
|
||||||
|
{
|
||||||
|
if (!source.has_value())
|
||||||
|
{
|
||||||
|
if (j.contains(tag))
|
||||||
|
j.erase(tag);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
addTag(j, tag, source.value());
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
Properties::Properties(const json& j)
|
||||||
|
{
|
||||||
|
fromJson(j);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
json Properties::toJson() const
|
||||||
|
{
|
||||||
|
json j;
|
||||||
|
if (playback_status.has_value())
|
||||||
|
addTag(j, "playbackStatus", std::optional<std::string>(to_string(playback_status.value())));
|
||||||
|
if (loop_status.has_value())
|
||||||
|
addTag(j, "loopStatus", std::optional<std::string>(to_string(loop_status.value())));
|
||||||
|
addTag(j, "rate", rate);
|
||||||
|
addTag(j, "shuffle", shuffle);
|
||||||
|
addTag(j, "volume", volume);
|
||||||
|
addTag(j, "position", position);
|
||||||
|
addTag(j, "minimumRate", minimum_rate);
|
||||||
|
addTag(j, "maximumRate", maximum_rate);
|
||||||
|
addTag(j, "canGoNext", can_go_next);
|
||||||
|
addTag(j, "canGoPrevious", can_go_previous);
|
||||||
|
addTag(j, "canPlay", can_play);
|
||||||
|
addTag(j, "canPause", can_pause);
|
||||||
|
addTag(j, "canSeek", can_seek);
|
||||||
|
addTag(j, "canControl", can_control);
|
||||||
|
if (metatags.has_value())
|
||||||
|
addTag(j, "metadata", metatags->toJson());
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Properties::fromJson(const json& j)
|
||||||
|
{
|
||||||
|
static std::set<std::string> rw_props = {"loopStatus", "shuffle", "volume", "rate"};
|
||||||
|
static std::set<std::string> ro_props = {"playbackStatus", "loopStatus", "shuffle", "volume", "position", "minimumRate", "maximumRate",
|
||||||
|
"canGoNext", "canGoPrevious", "canPlay", "canPause", "canSeek", "canControl", "metadata"};
|
||||||
|
for (const auto& element : j.items())
|
||||||
|
{
|
||||||
|
bool is_rw = (rw_props.find(element.key()) != rw_props.end());
|
||||||
|
bool is_ro = (ro_props.find(element.key()) != ro_props.end());
|
||||||
|
if (!is_rw && !is_ro)
|
||||||
|
LOG(WARNING, LOG_TAG) << "Property not supoorted: " << element.key() << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> opt;
|
||||||
|
|
||||||
|
readTag(j, "playbackStatus", opt);
|
||||||
|
if (opt.has_value())
|
||||||
|
playback_status = playback_status_from_string(opt.value());
|
||||||
|
else
|
||||||
|
playback_status = std::nullopt;
|
||||||
|
|
||||||
|
readTag(j, "loopStatus", opt);
|
||||||
|
if (opt.has_value())
|
||||||
|
loop_status = loop_status_from_string(opt.value());
|
||||||
|
else
|
||||||
|
loop_status = std::nullopt;
|
||||||
|
readTag(j, "rate", rate);
|
||||||
|
readTag(j, "shuffle", shuffle);
|
||||||
|
readTag(j, "volume", volume);
|
||||||
|
readTag(j, "position", position);
|
||||||
|
readTag(j, "minimumRate", minimum_rate);
|
||||||
|
readTag(j, "maximumRate", maximum_rate);
|
||||||
|
readTag(j, "canGoNext", can_go_next, false);
|
||||||
|
readTag(j, "canGoPrevious", can_go_previous, false);
|
||||||
|
readTag(j, "canPlay", can_play, false);
|
||||||
|
readTag(j, "canPause", can_pause, false);
|
||||||
|
readTag(j, "canSeek", can_seek, false);
|
||||||
|
readTag(j, "canControl", can_control, false);
|
||||||
|
|
||||||
|
if (j.contains("metadata"))
|
||||||
|
{
|
||||||
|
Metatags m;
|
||||||
|
m.fromJson(j["metadata"]);
|
||||||
|
metatags = m;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
metatags = std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Properties::operator==(const Properties& other) const
|
||||||
|
{
|
||||||
|
// expensive, but not called ofetn and less typing
|
||||||
|
return (toJson() == other.toJson());
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
// return (playback_status == other.playback_status &&
|
||||||
|
// loop_status == other.loop_status &&
|
||||||
|
// rate == other.rate &&
|
||||||
|
// shuffle == other.shuffle &&
|
||||||
|
// volume == other.volume &&
|
||||||
|
// position == other.position &&
|
||||||
|
// minimum_rate == other.minimum_rate &&
|
||||||
|
// maximum_rate == other.maximum_rate &&
|
||||||
|
// can_go_next == other.can_go_next &&
|
||||||
|
// can_go_previous == other.can_go_previous &&
|
||||||
|
// can_play == other.can_play &&
|
||||||
|
// can_pause == other.can_pause &&
|
||||||
|
// can_seek == other.can_seek &&
|
||||||
|
// can_control == other.can_control);
|
||||||
|
// clang-format on
|
||||||
|
}
|
193
server/streamreader/properties.hpp
Normal file
193
server/streamreader/properties.hpp
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
/***
|
||||||
|
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 PROPERTIES_HPP
|
||||||
|
#define PROPERTIES_HPP
|
||||||
|
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "common/aixlog.hpp"
|
||||||
|
#include "common/json.hpp"
|
||||||
|
#include "metatags.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
enum class PlaybackStatus
|
||||||
|
{
|
||||||
|
kPlaying = 0,
|
||||||
|
kPaused = 1,
|
||||||
|
kStopped = 2,
|
||||||
|
kUnknown = 3
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
enum class LoopStatus
|
||||||
|
{
|
||||||
|
kNone = 0,
|
||||||
|
kTrack = 1,
|
||||||
|
kPlaylist = 2,
|
||||||
|
kUnknown = 3
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
static std::string to_string(PlaybackStatus playback_status)
|
||||||
|
{
|
||||||
|
switch (playback_status)
|
||||||
|
{
|
||||||
|
case PlaybackStatus::kPlaying:
|
||||||
|
return "playing";
|
||||||
|
case PlaybackStatus::kPaused:
|
||||||
|
return "paused";
|
||||||
|
case PlaybackStatus::kStopped:
|
||||||
|
return "stopped";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static std::ostream& operator<<(std::ostream& os, PlaybackStatus playback_status)
|
||||||
|
{
|
||||||
|
os << to_string(playback_status);
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static PlaybackStatus playback_status_from_string(std::string& status)
|
||||||
|
{
|
||||||
|
if (status == "playing")
|
||||||
|
return PlaybackStatus::kPlaying;
|
||||||
|
else if (status == "paused")
|
||||||
|
return PlaybackStatus::kPaused;
|
||||||
|
else if (status == "stopped")
|
||||||
|
return PlaybackStatus::kStopped;
|
||||||
|
else
|
||||||
|
return PlaybackStatus::kUnknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static std::istream& operator>>(std::istream& is, PlaybackStatus& playback_status)
|
||||||
|
{
|
||||||
|
std::string status;
|
||||||
|
playback_status = PlaybackStatus::kUnknown;
|
||||||
|
if (is >> status)
|
||||||
|
playback_status = playback_status_from_string(status);
|
||||||
|
else
|
||||||
|
playback_status = PlaybackStatus::kUnknown;
|
||||||
|
return is;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static std::string to_string(LoopStatus loop_status)
|
||||||
|
{
|
||||||
|
switch (loop_status)
|
||||||
|
{
|
||||||
|
case LoopStatus::kNone:
|
||||||
|
return "none";
|
||||||
|
case LoopStatus::kTrack:
|
||||||
|
return "track";
|
||||||
|
case LoopStatus::kPlaylist:
|
||||||
|
return "playlist";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static std::ostream& operator<<(std::ostream& os, LoopStatus loop_status)
|
||||||
|
{
|
||||||
|
os << to_string(loop_status);
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static LoopStatus loop_status_from_string(std::string& status)
|
||||||
|
{
|
||||||
|
if (status == "none")
|
||||||
|
return LoopStatus::kNone;
|
||||||
|
else if (status == "track")
|
||||||
|
return LoopStatus::kTrack;
|
||||||
|
else if (status == "playlist")
|
||||||
|
return LoopStatus::kPlaylist;
|
||||||
|
else
|
||||||
|
return LoopStatus::kUnknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static std::istream& operator>>(std::istream& is, LoopStatus& loop_status)
|
||||||
|
{
|
||||||
|
std::string status;
|
||||||
|
if (is >> status)
|
||||||
|
loop_status = loop_status_from_string(status);
|
||||||
|
else
|
||||||
|
loop_status = LoopStatus::kUnknown;
|
||||||
|
|
||||||
|
return is;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Properties
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Properties() = default;
|
||||||
|
Properties(const json& j);
|
||||||
|
|
||||||
|
/// Meta data
|
||||||
|
std::optional<Metatags> metatags;
|
||||||
|
/// https://www.musicpd.org/doc/html/protocol.html#tags
|
||||||
|
/// The current playback status
|
||||||
|
std::optional<PlaybackStatus> playback_status;
|
||||||
|
/// The current loop / repeat status
|
||||||
|
std::optional<LoopStatus> loop_status;
|
||||||
|
/// The current playback rate
|
||||||
|
std::optional<float> rate;
|
||||||
|
/// A value of false indicates that playback is progressing linearly through a playlist, while true means playback is progressing through a playlist in some
|
||||||
|
/// other order.
|
||||||
|
std::optional<bool> shuffle;
|
||||||
|
/// The volume level between 0-100
|
||||||
|
std::optional<int> volume;
|
||||||
|
/// The current track position in seconds
|
||||||
|
std::optional<float> position;
|
||||||
|
/// The minimum value which the Rate property can take. Clients should not attempt to set the Rate property below this value
|
||||||
|
std::optional<float> minimum_rate;
|
||||||
|
/// The maximum value which the Rate property can take. Clients should not attempt to set the Rate property above this value
|
||||||
|
std::optional<float> maximum_rate;
|
||||||
|
/// Whether the client can call the Next method on this interface and expect the current track to change
|
||||||
|
bool can_go_next = false;
|
||||||
|
/// Whether the client can call the Previous method on this interface and expect the current track to change
|
||||||
|
bool can_go_previous = false;
|
||||||
|
/// Whether playback can be started using "play" or "playPause"
|
||||||
|
bool can_play = false;
|
||||||
|
/// Whether playback can be paused using "pause" or "playPause"
|
||||||
|
bool can_pause = false;
|
||||||
|
/// Whether the client can control the playback position using "seek" and "setPosition". This may be different for different tracks
|
||||||
|
bool can_seek = false;
|
||||||
|
/// Whether the media player may be controlled over this interface
|
||||||
|
bool can_control = false;
|
||||||
|
|
||||||
|
json toJson() const;
|
||||||
|
void fromJson(const json& j);
|
||||||
|
bool operator==(const Properties& other) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
|
@ -12,6 +12,8 @@ endif (ANDROID)
|
||||||
set(TEST_SOURCES
|
set(TEST_SOURCES
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/test_main.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/test_main.cpp
|
||||||
${CMAKE_SOURCE_DIR}/server/streamreader/control_error.cpp
|
${CMAKE_SOURCE_DIR}/server/streamreader/control_error.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/server/streamreader/properties.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/server/streamreader/metatags.cpp
|
||||||
${CMAKE_SOURCE_DIR}/server/streamreader/stream_uri.cpp)
|
${CMAKE_SOURCE_DIR}/server/streamreader/stream_uri.cpp)
|
||||||
|
|
||||||
add_executable(snapcast_test ${TEST_SOURCES})
|
add_executable(snapcast_test ${TEST_SOURCES})
|
||||||
|
|
|
@ -22,9 +22,9 @@
|
||||||
#include <regex>
|
#include <regex>
|
||||||
|
|
||||||
#include "common/aixlog.hpp"
|
#include "common/aixlog.hpp"
|
||||||
#include "common/properties.hpp"
|
|
||||||
#include "common/utils/string_utils.hpp"
|
#include "common/utils/string_utils.hpp"
|
||||||
#include "server/streamreader/control_error.hpp"
|
#include "server/streamreader/control_error.hpp"
|
||||||
|
#include "server/streamreader/properties.hpp"
|
||||||
#include "server/streamreader/stream_uri.hpp"
|
#include "server/streamreader/stream_uri.hpp"
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue