// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 #pragma once #include "lms_artwork.h" #include "queue_lms_cli.h" #include #include #include #include namespace queue_audio { struct PlaylistEntry { std::string id, title, artist, album, url; std::string artworkUrl, remote; double duration = 0, liveEdge = -1; unsigned startOffsetMs = 0; std::string plainTitle() const { if (url.compare(0, 10, "podcast://")) return title; // Native Podcast scanUrl can store getCurrentTitle's player formatting // back into TITLE. Remove only the exact suffix made from these tags. const auto suffix = (!artist.empty() ? " by " + artist : std::string{}) + (!album.empty() ? " from " + album : std::string{}); if (!suffix.empty() && title.size() >= suffix.size() && title.compare(title.size() - suffix.size(), suffix.size(), suffix) == 0) return title.substr(0, title.size() - suffix.size()); return title; } bool finiteRemote() const { return std::isfinite(duration) && duration > 0 && liveEdge < 0; } std::string artwork(const std::string &host) const { return bridge::lmsArtwork(host, id, artworkUrl, bridge::remoteArtworkTrack(id, remote, url)); } }; struct PlaylistSnapshot { unsigned index = 0, timeMs = 0; std::string mode; double duration = 0; std::vector entries; std::string revision; unsigned shuffle = 0, repeat = 0; }; // Read-only metadata snapshot. LMS alone determines stream order; the relay // has no occurrence matching, playlist edit inference or mirroring policy. class PlaylistReader { public: PlaylistSnapshot parse(const std::string &wire) { PlaylistSnapshot out; std::istringstream input(wire); std::string field; PlaylistEntry *entry = nullptr; unsigned count = 0; while (input >> field) { field = decodeLms(field); auto colon = field.find(':'); if (colon == std::string::npos) continue; auto key = field.substr(0, colon), value = field.substr(colon + 1); if (key == "mode" && !entry) out.mode = value; else if (key == "playlist_cur_index") out.index = value == "-1" ? 0 : std::stoul(value); else if (key == "playlist_tracks") count = std::stoul(value); else if (key == "playlist_timestamp") out.revision = value; else if (key == "playlist shuffle" || key == "playlist repeat") { const auto policy = std::stoul(value); if (policy > 2) throw std::runtime_error("playlist-policy-invalid"); (key == "playlist shuffle" ? out.shuffle : out.repeat) = unsigned(policy); } else if (key == "duration" && !entry) out.duration = std::stod(value); else if (key == "time") out.timeMs = unsigned(std::stod(value) * 1000); else if (key == "playlist index" || key == "index") { if (std::stoul(value) != out.entries.size()) throw std::runtime_error("playlist-incomplete"); out.entries.emplace_back(); entry = &out.entries.back(); } else if (entry) { if (key == "id") entry->id = value; else if (key == "title") entry->title = value; else if (key == "artist") entry->artist = value; else if (key == "album") entry->album = value; else if (key == "url") entry->url = value; else if (key == "artwork_url") entry->artworkUrl = value; else if (key == "duration") entry->duration = std::stod(value); else if (key == "live_edge") entry->liveEdge = std::stod(value); else if (key == "remote") entry->remote = value; } } if (count > 60000 || out.entries.size() != count || (!count && out.index) || (count && out.index >= count)) throw std::runtime_error("playlist-incomplete"); return out; } PlaylistSnapshot read(const std::string &host, const std::string &mac, unsigned port = 9090) { std::string reply; if (!controlLms(host, mac, "status 0 60000 tags:alKudV", port, &reply)) throw std::runtime_error("playlist-cli-unavailable"); return parse(reply); } }; } // namespace queue_audio