From b348bb3379e0261c5c9c4f4ff35d7346eceb76b0 Mon Sep 17 00:00:00 2001 From: badaix Date: Wed, 1 Sep 2021 21:12:29 +0200 Subject: [PATCH] Move properties and metatags into server dir --- common/metatags.hpp | 314 -------------------------- common/properties.hpp | 344 ----------------------------- server/CMakeLists.txt | 2 + server/Makefile | 2 +- server/streamreader/metatags.cpp | 214 ++++++++++++++++++ server/streamreader/metatags.hpp | 132 +++++++++++ server/streamreader/pcm_stream.hpp | 2 +- server/streamreader/properties.cpp | 179 +++++++++++++++ server/streamreader/properties.hpp | 193 ++++++++++++++++ test/CMakeLists.txt | 2 + test/test_main.cpp | 2 +- 11 files changed, 725 insertions(+), 661 deletions(-) delete mode 100644 common/metatags.hpp delete mode 100644 common/properties.hpp create mode 100644 server/streamreader/metatags.cpp create mode 100644 server/streamreader/metatags.hpp create mode 100644 server/streamreader/properties.cpp create mode 100644 server/streamreader/properties.hpp diff --git a/common/metatags.hpp b/common/metatags.hpp deleted file mode 100644 index 6f7145fa..00000000 --- a/common/metatags.hpp +++ /dev/null @@ -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 . -***/ - -#ifndef METATAGS_HPP -#define METATAGS_HPP - -#include -#include -#include - -#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 duration; - /// the artist name. Its meaning is not well-defined; see “composer” and “performer” for more specific tags. - std::optional> artist; - /// same as artist, but for sorting. This usually omits prefixes such as “The”. - std::optional> artist_sort; - /// the album name. - std::optional album; - /// same as album, but for sorting. - std::optional 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> album_artist; - /// same as albumartist, but for sorting. - std::optional> 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 name; - /// the song’s release date. This is usually a 4-digit year. - std::optional date; - /// the song’s original release date. - std::optional original_date; - /// the artist who performed the song. - std::optional performer; - /// the conductor who conducted the song. - std::optional 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 work; - /// “used if the sound belongs to a larger category of sounds/music” (from the IDv2.4.0 TIT1 description). - std::optional grouping; - /// the name of the label or publisher. - std::optional label; - /// the artist id in the MusicBrainz database. - std::optional musicbrainz_artist_id; - /// the album id in the MusicBrainz database. - std::optional musicbrainz_album_id; - /// the album artist id in the MusicBrainz database. - std::optional musicbrainz_album_artist_id; - /// the track id in the MusicBrainz database. - std::optional musicbrainz_track_id; - /// the release track id in the MusicBrainz database. - std::optional musicbrainz_release_track_id; - /// the work id in the MusicBrainz database. - std::optional 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 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 art_url; - /// The track lyrics - std::optional lyrics; - /// The speed of the music, in beats per minute - std::optional 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 auto_rating; - /// A (list of) freeform comment(s) - std::optional> comment; - /// The composer(s) of the track - std::optional> composer; - /// Date/Time: When the track was created. Usually only the year component will be useful - std::optional content_created; - /// Integer: The disc number on the album that this track is from - std::optional disc_number; - /// Date/Time: When the track was first played - std::optional first_used; - /// List of Strings: The genre(s) of the track - std::optional> genre; - /// Date/Time: When the track was last played - std::optional last_used; - /// List of Strings: The lyricist(s) of the track - std::optional> lyricist; - /// String: The track title - std::optional title; - /// Integer: The track number on the album disc - std::optional track_number; - /// URI: The location of the media file. - std::optional url; - /// Integer: The number of times the track has been played. - std::optional use_count; - /// Float: A user-specified rating. This should be in the range 0.0 to 1.0. - std::optional user_rating; - - /// Spotify artist id - std::optional spotify_artist_id; - /// Spotify track id - std::optional 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 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 - void readTag(const json& j, const std::string& tag, std::optional& dest) const - { - try - { - if (!j.contains(tag)) - dest = std::nullopt; - else - dest = j[tag].get(); - } - catch (const std::exception& e) - { - LOG(ERROR, LOG_TAG) << "failed to read tag: '" << tag << "': " << e.what() << '\n'; - } - } - - template - void addTag(json& j, const std::string& tag, const std::optional& 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 diff --git a/common/properties.hpp b/common/properties.hpp deleted file mode 100644 index e0e2346b..00000000 --- a/common/properties.hpp +++ /dev/null @@ -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 . -***/ - -#ifndef PROPERTIES_HPP -#define PROPERTIES_HPP - -#include -#include - -#include - -#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; - /// https://www.musicpd.org/doc/html/protocol.html#tags - /// The current playback status - std::optional playback_status; - /// The current loop / repeat status - std::optional loop_status; - /// The current playback rate - std::optional 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 shuffle; - /// The volume level between 0-100 - std::optional volume; - /// The current track position in seconds - std::optional position; - /// The minimum value which the Rate property can take. Clients should not attempt to set the Rate property below this value - std::optional 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 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(to_string(playback_status.value()))); - if (loop_status.has_value()) - addTag(j, "loopStatus", std::optional(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 rw_props = {"loopStatus", "shuffle", "volume", "rate"}; - static std::set 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 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 - void readTag(const json& j, const std::string& tag, std::optional& dest) const - { - try - { - if (!j.contains(tag)) - dest = std::nullopt; - else - dest = j[tag].get(); - } - catch (const std::exception& e) - { - LOG(ERROR, LOG_TAG) << "failed to read tag: '" << tag << "': " << e.what() << '\n'; - } - } - - template - void readTag(const json& j, const std::string& tag, T& dest, const T& def) const - { - std::optional val; - readTag(j, tag, val); - if (val.has_value()) - dest = val.value(); - else - dest = def; - } - - template - void addTag(json& j, const std::string& tag, const std::optional& source) const - { - if (!source.has_value()) - { - if (j.contains(tag)) - j.erase(tag); - } - else - addTag(j, tag, source.value()); - } - - template - 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 diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index e0ea658d..675f91f7 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -27,6 +27,8 @@ set(SERVER_SOURCES streamreader/librespot_stream.cpp streamreader/meta_stream.cpp streamreader/watchdog.cpp + streamreader/properties.cpp + streamreader/metatags.cpp streamreader/process_stream.cpp) set(SERVER_LIBRARIES diff --git a/server/Makefile b/server/Makefile index 24e719fc..98ff561d 100644 --- a/server/Makefile +++ b/server/Makefile @@ -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 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)) CXXFLAGS += -D$(TARGET) diff --git a/server/streamreader/metatags.cpp b/server/streamreader/metatags.cpp new file mode 100644 index 00000000..0da45250 --- /dev/null +++ b/server/streamreader/metatags.cpp @@ -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 . +***/ + +#include "metatags.hpp" + +static constexpr auto LOG_TAG = "Metatags"; + + +namespace +{ +template +void readTag(const json& j, const std::string& tag, std::optional& dest) +{ + try + { + if (!j.contains(tag)) + dest = std::nullopt; + else + dest = j[tag].get(); + } + catch (const std::exception& e) + { + LOG(ERROR, LOG_TAG) << "failed to read tag: '" << tag << "': " << e.what() << '\n'; + } +} + +template +void addTag(json& j, const std::string& tag, const std::optional& 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 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()); +} diff --git a/server/streamreader/metatags.hpp b/server/streamreader/metatags.hpp new file mode 100644 index 00000000..c2818ea4 --- /dev/null +++ b/server/streamreader/metatags.hpp @@ -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 . +***/ + +#ifndef METATAGS_HPP +#define METATAGS_HPP + +#include +#include +#include + +#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 duration; + /// the artist name. Its meaning is not well-defined; see “composer” and “performer” for more specific tags. + std::optional> artist; + /// same as artist, but for sorting. This usually omits prefixes such as “The”. + std::optional> artist_sort; + /// the album name. + std::optional album; + /// same as album, but for sorting. + std::optional 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> album_artist; + /// same as albumartist, but for sorting. + std::optional> 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 name; + /// the song’s release date. This is usually a 4-digit year. + std::optional date; + /// the song’s original release date. + std::optional original_date; + /// the artist who performed the song. + std::optional performer; + /// the conductor who conducted the song. + std::optional 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 work; + /// “used if the sound belongs to a larger category of sounds/music” (from the IDv2.4.0 TIT1 description). + std::optional grouping; + /// the name of the label or publisher. + std::optional label; + /// the artist id in the MusicBrainz database. + std::optional musicbrainz_artist_id; + /// the album id in the MusicBrainz database. + std::optional musicbrainz_album_id; + /// the album artist id in the MusicBrainz database. + std::optional musicbrainz_album_artist_id; + /// the track id in the MusicBrainz database. + std::optional musicbrainz_track_id; + /// the release track id in the MusicBrainz database. + std::optional musicbrainz_release_track_id; + /// the work id in the MusicBrainz database. + std::optional 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 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 art_url; + /// The track lyrics + std::optional lyrics; + /// The speed of the music, in beats per minute + std::optional 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 auto_rating; + /// A (list of) freeform comment(s) + std::optional> comment; + /// The composer(s) of the track + std::optional> composer; + /// Date/Time: When the track was created. Usually only the year component will be useful + std::optional content_created; + /// Integer: The disc number on the album that this track is from + std::optional disc_number; + /// Date/Time: When the track was first played + std::optional first_used; + /// List of Strings: The genre(s) of the track + std::optional> genre; + /// Date/Time: When the track was last played + std::optional last_used; + /// List of Strings: The lyricist(s) of the track + std::optional> lyricist; + /// String: The track title + std::optional title; + /// Integer: The track number on the album disc + std::optional track_number; + /// URI: The location of the media file. + std::optional url; + /// Integer: The number of times the track has been played. + std::optional use_count; + /// Float: A user-specified rating. This should be in the range 0.0 to 1.0. + std::optional user_rating; + + /// Spotify artist id + std::optional spotify_artist_id; + /// Spotify track id + std::optional spotify_track_id; + + json toJson() const; + void fromJson(const json& j); + bool operator==(const Metatags& other) const; +}; + + +#endif diff --git a/server/streamreader/pcm_stream.hpp b/server/streamreader/pcm_stream.hpp index 13aa47bc..ff8708e2 100644 --- a/server/streamreader/pcm_stream.hpp +++ b/server/streamreader/pcm_stream.hpp @@ -30,7 +30,6 @@ #include "common/error_code.hpp" #include "common/json.hpp" -#include "common/properties.hpp" #include "common/sample_format.hpp" #include "encoder/encoder.hpp" #include "jsonrpcpp.hpp" @@ -38,6 +37,7 @@ #include "server_settings.hpp" #include "stream_control.hpp" #include "stream_uri.hpp" +#include "properties.hpp" namespace bp = boost::process; diff --git a/server/streamreader/properties.cpp b/server/streamreader/properties.cpp new file mode 100644 index 00000000..5df68923 --- /dev/null +++ b/server/streamreader/properties.cpp @@ -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 . +***/ + +#include "properties.hpp" + +static constexpr auto LOG_TAG = "Properties"; + +namespace +{ +template +void readTag(const json& j, const std::string& tag, std::optional& dest) +{ + try + { + if (!j.contains(tag)) + dest = std::nullopt; + else + dest = j[tag].get(); + } + catch (const std::exception& e) + { + LOG(ERROR, LOG_TAG) << "failed to read tag: '" << tag << "': " << e.what() << '\n'; + } +} + +template +void readTag(const json& j, const std::string& tag, T& dest, const T& def) +{ + std::optional val; + readTag(j, tag, val); + if (val.has_value()) + dest = val.value(); + else + dest = def; +} + +template +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 +void addTag(json& j, const std::string& tag, const std::optional& 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(to_string(playback_status.value()))); + if (loop_status.has_value()) + addTag(j, "loopStatus", std::optional(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 rw_props = {"loopStatus", "shuffle", "volume", "rate"}; + static std::set 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 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 +} diff --git a/server/streamreader/properties.hpp b/server/streamreader/properties.hpp new file mode 100644 index 00000000..a633a7bd --- /dev/null +++ b/server/streamreader/properties.hpp @@ -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 . +***/ + +#ifndef PROPERTIES_HPP +#define PROPERTIES_HPP + +#include +#include + +#include + +#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; + /// https://www.musicpd.org/doc/html/protocol.html#tags + /// The current playback status + std::optional playback_status; + /// The current loop / repeat status + std::optional loop_status; + /// The current playback rate + std::optional 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 shuffle; + /// The volume level between 0-100 + std::optional volume; + /// The current track position in seconds + std::optional position; + /// The minimum value which the Rate property can take. Clients should not attempt to set the Rate property below this value + std::optional 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 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 diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d4f38561..b88185ee 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -12,6 +12,8 @@ endif (ANDROID) set(TEST_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/test_main.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) add_executable(snapcast_test ${TEST_SOURCES}) diff --git a/test/test_main.cpp b/test/test_main.cpp index 4675fef4..284fd32b 100644 --- a/test/test_main.cpp +++ b/test/test_main.cpp @@ -22,9 +22,9 @@ #include #include "common/aixlog.hpp" -#include "common/properties.hpp" #include "common/utils/string_utils.hpp" #include "server/streamreader/control_error.hpp" +#include "server/streamreader/properties.hpp" #include "server/streamreader/stream_uri.hpp" using namespace std;