#include "radiobackend.h" #include #include #include #include #include #include #include #include namespace { constexpr int kRecentLimit = 20; QString appConfigDir() { return QDir::homePath() + QStringLiteral("/.config/bearwave"); } } RadioBackend::RadioBackend(QObject *parent) : QObject(parent) { m_radioBrowser = new RadioBrowser(this); m_player = new BearPlayer(this); setupConnections(); loadFavorites(); loadState(); } QList RadioBackend::stations() const { QList list; for (RadioStation *s : m_filteredStations) { list.append(s); } return list; } QList RadioBackend::favoriteStations() const { QList list; for (RadioStation *s : m_favorites) { list.append(s); } return list; } void RadioBackend::setupConnections() { connect(m_radioBrowser, &RadioBrowser::stationsLoaded, this, &RadioBackend::onStationsLoaded); connect(m_radioBrowser, &RadioBrowser::error, this, [this](const QString &error) { qWarning() << "RadioBrowser error:" << error; setLastError(error); setLoading(false); }); connect(m_player, &BearPlayer::currentStationChanged, this, [this](const QString &name) { if (!name.isEmpty()) { m_lastStationName = name; emit resumeStateChanged(); saveState(); } else { m_currentStationUuid.clear(); m_currentStationUrl.clear(); emit currentStationChanged(); } }); connect(m_player, &BearPlayer::volumeChanged, this, [this]() { saveState(); }); } void RadioBackend::onStationsLoaded(const QList &stations) { qDeleteAll(m_stations); m_stations.clear(); m_filteredStations.clear(); m_stations = stations; for (RadioStation *s : m_stations) { s->setParent(this); for (RadioStation *f : m_favorites) { if ((!s->uuid().isEmpty() && s->uuid() == f->uuid()) || (s->uuid().isEmpty() && s->urlResolved() == f->urlResolved())) { s->setIsFavorite(true); break; } } } std::sort(m_stations.begin(), m_stations.end(), [](RadioStation *a, RadioStation *b) { return QString::localeAwareCompare(a->name(), b->name()) < 0; }); rebuildFilteredStations(false); setLastError(QString()); setLoading(false); qDebug() << "Loaded" << stations.size() << "stations"; } void RadioBackend::loadGermanStations() { setLoading(true); m_radioBrowser->getGermanStations(); } void RadioBackend::loadDutchStations() { setLoading(true); m_radioBrowser->getDutchStations(); } void RadioBackend::loadTopStations() { setLoading(true); m_radioBrowser->getTopStations(100); } void RadioBackend::loadWorldStations() { setLoading(true); m_radioBrowser->getWorldStations(200); } void RadioBackend::loadByTag(const QString &tag) { if (tag.trimmed().isEmpty()) { return; } setLoading(true); m_radioBrowser->getByTag(tag.trimmed()); } void RadioBackend::loadByCountryCode(const QString &countryCode) { if (countryCode.trimmed().isEmpty()) { return; } setLoading(true); m_radioBrowser->getByCountry(countryCode.trimmed().toUpper()); } void RadioBackend::searchStations(const QString &query) { if (query.isEmpty()) { return; } setLoading(true); m_radioBrowser->search(query); } void RadioBackend::playStation(int index) { if (!stationForVisibleIndex(index)) { return; } m_currentFromFavorites = false; m_currentIndex = index; playCurrentSelection(); } void RadioBackend::playFavoriteStation(int index) { if (index < 0 || index >= m_favorites.size()) { return; } m_currentFromFavorites = true; m_currentIndex = index; playCurrentSelection(); } void RadioBackend::playNextStation() { const QList list = currentList(); if (list.isEmpty()) { return; } if (m_currentIndex < 0) { m_currentIndex = 0; } else if (m_currentIndex < list.size() - 1) { ++m_currentIndex; } playCurrentSelection(); } void RadioBackend::playPreviousStation() { const QList list = currentList(); if (list.isEmpty()) { return; } if (m_currentIndex < 0) { m_currentIndex = 0; } else if (m_currentIndex > 0) { --m_currentIndex; } playCurrentSelection(); } bool RadioBackend::hasNextStation() const { const QList list = currentList(); if (list.isEmpty()) { return false; } if (m_currentIndex < 0) { return true; } return m_currentIndex < list.size() - 1; } bool RadioBackend::hasPreviousStation() const { const QList list = currentList(); if (list.isEmpty()) { return false; } return m_currentIndex > 0; } void RadioBackend::toggleFavorite(int index) { if (index < 0 || index >= m_stations.size()) { return; } RadioStation *station = m_stations[index]; for (int i = 0; i < m_favorites.size(); ++i) { if ((!station->uuid().isEmpty() && m_favorites[i]->uuid() == station->uuid()) || (station->uuid().isEmpty() && m_favorites[i]->urlResolved() == station->urlResolved())) { m_favorites.removeAt(i); station->setIsFavorite(false); saveFavorites(); emit favoritesChanged(); emit stationsChanged(); return; } } RadioStation *copy = new RadioStation(this); copy->setUuid(station->uuid()); copy->setName(station->name()); copy->setUrl(station->url()); copy->setUrlResolved(station->urlResolved()); copy->setHomepage(station->homepage()); copy->setFavicon(station->favicon()); copy->setCountry(station->country()); copy->setTags(station->tags()); copy->setCodec(station->codec()); copy->setBitrate(station->bitrate()); copy->setVotes(station->votes()); copy->setIsOnline(station->isOnline()); copy->setIsFavorite(true); m_favorites.append(copy); std::sort(m_favorites.begin(), m_favorites.end(), [](RadioStation *a, RadioStation *b) { return QString::localeAwareCompare(a->name(), b->name()) < 0; }); station->setIsFavorite(true); saveFavorites(); emit favoritesChanged(); emit stationsChanged(); emit listsChanged(); } void RadioBackend::toggleFavoriteById(const QString &uuid, const QString &urlResolved) { int stationIndex = -1; for (int i = 0; i < m_stations.size(); ++i) { if ((!uuid.isEmpty() && m_stations[i]->uuid() == uuid) || (uuid.isEmpty() && !urlResolved.isEmpty() && m_stations[i]->urlResolved() == urlResolved)) { stationIndex = i; break; } } if (stationIndex >= 0) { toggleFavorite(stationIndex); return; } for (int i = 0; i < m_favorites.size(); ++i) { if ((!uuid.isEmpty() && m_favorites[i]->uuid() == uuid) || (uuid.isEmpty() && !urlResolved.isEmpty() && m_favorites[i]->urlResolved() == urlResolved)) { m_favorites.removeAt(i); for (RadioStation *s : m_stations) { if ((!uuid.isEmpty() && s->uuid() == uuid) || (uuid.isEmpty() && !urlResolved.isEmpty() && s->urlResolved() == urlResolved)) { s->setIsFavorite(false); } } saveFavorites(); emit favoritesChanged(); emit stationsChanged(); emit listsChanged(); return; } } } void RadioBackend::addManualStation(const QString &name, const QString &url, const QString &country) { if (name.trimmed().isEmpty() || url.trimmed().isEmpty()) { return; } RadioStation *station = new RadioStation(this); station->setName(name.trimmed()); station->setUrl(url.trimmed()); station->setUrlResolved(url.trimmed()); station->setCountry(country.trimmed().isEmpty() ? tr("Manual") : country.trimmed()); station->setCodec(QStringLiteral("unknown")); station->setBitrate(0); station->setVotes(0); station->setIsOnline(true); m_stations.prepend(station); rebuildFilteredStations(false); } void RadioBackend::sortStations(const QString &mode) { if (mode == QStringLiteral("name")) { std::sort(m_stations.begin(), m_stations.end(), [](RadioStation *a, RadioStation *b) { return QString::localeAwareCompare(a->name(), b->name()) < 0; }); } else if (mode == QStringLiteral("bitrate")) { std::sort(m_stations.begin(), m_stations.end(), [](RadioStation *a, RadioStation *b) { return a->bitrate() > b->bitrate(); }); } else if (mode == QStringLiteral("votes")) { std::sort(m_stations.begin(), m_stations.end(), [](RadioStation *a, RadioStation *b) { return a->votes() > b->votes(); }); } rebuildFilteredStations(false); } QVariantList RadioBackend::getRecentStations() const { return m_recentStations; } QVariantList RadioBackend::recentStations() const { QVariantList updatedList; for (const QVariant &entry : m_recentStations) { QVariantMap station = entry.toMap(); const QString uuid = station.value(QStringLiteral("uuid")).toString(); const QString urlResolved = station.value(QStringLiteral("urlResolved")).toString(); bool isFav = false; for (const RadioStation *fav : m_favorites) { if ((!uuid.isEmpty() && fav->uuid() == uuid) || (uuid.isEmpty() && !urlResolved.isEmpty() && fav->urlResolved() == urlResolved)) { isFav = true; break; } } station.insert(QStringLiteral("isFavorite"), isFav); updatedList.append(station); } return updatedList; } QVariantList RadioBackend::getFavoriteStations() const { QVariantList result; for (RadioStation *station : m_favorites) { result.push_back(toVariantMap(station)); } return result; } bool RadioBackend::playRecentByUuid(const QString &uuid, const QString &urlResolved) { for (RadioStation *station : m_stations) { if (matchesStation(station, uuid, urlResolved)) { const QString url = station->urlResolved().isEmpty() ? station->url() : station->urlResolved(); m_lastStationName = station->name(); m_lastStationUrl = url; emit resumeStateChanged(); recordRecentStation(toVariantMap(station)); saveState(); m_currentStationUuid = station->uuid(); m_currentStationUrl = url; emit currentStationChanged(); m_player->playUrl(url, station->name()); return true; } } for (const QVariant &entry : m_recentStations) { const QVariantMap station = entry.toMap(); if ((!uuid.isEmpty() && station.value(QStringLiteral("uuid")).toString() == uuid) || (uuid.isEmpty() && !urlResolved.isEmpty() && station.value(QStringLiteral("urlResolved")).toString() == urlResolved)) { const QString url = station.value(QStringLiteral("urlResolved")).toString(); if (url.isEmpty()) { return false; } m_lastStationName = station.value(QStringLiteral("name")).toString(); m_lastStationUrl = url; emit resumeStateChanged(); recordRecentStation(station); saveState(); m_currentStationUuid = station.value(QStringLiteral("uuid")).toString(); m_currentStationUrl = url; emit currentStationChanged(); m_player->playUrl(url, m_lastStationName); return true; } } return false; } bool RadioBackend::playFavoriteByUuid(const QString &uuid, const QString &urlResolved) { for (int i = 0; i < m_favorites.size(); ++i) { if (matchesStation(m_favorites[i], uuid, urlResolved)) { playFavoriteStation(i); return true; } } return false; } void RadioBackend::resumeLastStation() { if (m_lastStationUrl.isEmpty()) { return; } QVariantMap station; station.insert(QStringLiteral("name"), m_lastStationName.isEmpty() ? tr("Last played") : m_lastStationName); station.insert(QStringLiteral("urlResolved"), m_lastStationUrl); recordRecentStation(station); saveState(); QString resolvedUuid; for (const RadioStation *s : m_stations) { if (s->urlResolved() == m_lastStationUrl || s->url() == m_lastStationUrl) { resolvedUuid = s->uuid(); break; } } if (resolvedUuid.isEmpty()) { for (const RadioStation *s : m_favorites) { if (s->urlResolved() == m_lastStationUrl || s->url() == m_lastStationUrl) { resolvedUuid = s->uuid(); break; } } } m_currentStationUuid = resolvedUuid; m_currentStationUrl = m_lastStationUrl; emit currentStationChanged(); m_player->playUrl(m_lastStationUrl, m_lastStationName.isEmpty() ? tr("Last played") : m_lastStationName); } void RadioBackend::loadFavorites() { const QString configDir = appConfigDir(); QDir().mkpath(configDir); QFile file(configDir + "/favorites.json"); if (!file.open(QIODevice::ReadOnly)) { return; } const QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); if (!doc.isArray()) { return; } for (const QJsonValue &v : doc.array()) { if (!v.isObject()) { continue; } QJsonObject o = v.toObject(); RadioStation *s = new RadioStation(this); s->setUuid(o.value("uuid").toString()); s->setName(o.value("name").toString()); s->setUrl(o.value("url").toString()); s->setUrlResolved(o.value("urlResolved").toString()); s->setCountry(o.value("country").toString()); s->setIsFavorite(true); m_favorites.append(s); } std::sort(m_favorites.begin(), m_favorites.end(), [](RadioStation *a, RadioStation *b) { return QString::localeAwareCompare(a->name(), b->name()) < 0; }); } void RadioBackend::saveFavorites() const { const QString configDir = appConfigDir(); QDir().mkpath(configDir); QFile file(configDir + "/favorites.json"); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { return; } QJsonArray arr; for (RadioStation *s : m_favorites) { QJsonObject o; o.insert("uuid", s->uuid()); o.insert("name", s->name()); o.insert("url", s->url()); o.insert("urlResolved", s->urlResolved()); o.insert("country", s->country()); arr.append(o); } file.write(QJsonDocument(arr).toJson(QJsonDocument::Indented)); } void RadioBackend::loadState() { const QString configDir = appConfigDir(); QDir().mkpath(configDir); QFile file(configDir + "/state.json"); if (!file.open(QIODevice::ReadOnly)) { return; } const QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); if (!doc.isObject()) { return; } const QJsonObject obj = doc.object(); m_lastStationName = obj.value("lastStationName").toString(); m_lastStationUrl = obj.value("lastStationUrl").toString(); const double volume = obj.value("volume").toDouble(0.5); const QJsonArray recentArray = obj.value("recentStations").toArray(); m_recentStations.clear(); for (const QJsonValue &value : recentArray) { if (!value.isObject()) { continue; } const QVariantMap recentStation = value.toObject().toVariantMap(); if (recentStation.value(QStringLiteral("urlResolved")).toString().isEmpty()) { continue; } m_recentStations.append(recentStation); if (m_recentStations.size() >= kRecentLimit) { break; } } m_player->setVolume(volume); emit resumeStateChanged(); } void RadioBackend::saveState() const { const QString configDir = appConfigDir(); QDir().mkpath(configDir); QFile file(configDir + "/state.json"); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { return; } QJsonObject obj; obj.insert("lastStationName", m_lastStationName); obj.insert("lastStationUrl", m_lastStationUrl); obj.insert("volume", m_player->volume()); obj.insert("recentStations", QJsonArray::fromVariantList(m_recentStations)); file.write(QJsonDocument(obj).toJson(QJsonDocument::Indented)); } void RadioBackend::setLoading(bool loading) { if (m_loading == loading) { return; } m_loading = loading; emit loadingChanged(); } void RadioBackend::setLastError(const QString &error) { if (m_lastError == error) { return; } m_lastError = error; emit lastErrorChanged(); } QList RadioBackend::currentList() const { return m_currentFromFavorites ? m_favorites : m_filteredStations; } RadioStation *RadioBackend::stationForVisibleIndex(int index) const { if (index < 0 || index >= m_filteredStations.size()) { return nullptr; } return m_filteredStations[index]; } void RadioBackend::setFilterQuery(const QString &query) { if (m_filterQuery == query && !m_filteredStations.isEmpty() && !m_stations.isEmpty()) { return; } m_filterQuery = query; rebuildFilteredStations(); } void RadioBackend::rebuildFilteredStations(bool emitFilterSignal) { m_filteredStations.clear(); if (m_filterQuery.trimmed().isEmpty()) { m_filteredStations = m_stations; } else { const QString lowerQuery = m_filterQuery.toLower(); for (RadioStation *s : m_stations) { if (s->name().toLower().contains(lowerQuery) || s->tags().toLower().contains(lowerQuery) || s->country().toLower().contains(lowerQuery)) { m_filteredStations.append(s); } } } if (emitFilterSignal) { emit filterQueryChanged(); } emit stationsChanged(); emit listsChanged(); } void RadioBackend::recordRecentStation(const QVariantMap &stationData) { const QString url = stationData.value(QStringLiteral("urlResolved")).toString(); if (url.isEmpty()) { return; } QVariantMap normalizedStation = stationData; if (!normalizedStation.contains(QStringLiteral("name"))) { normalizedStation.insert(QStringLiteral("name"), QString()); } for (int i = 0; i < m_recentStations.size(); ++i) { const QVariantMap existing = m_recentStations[i].toMap(); const QString existingUuid = existing.value(QStringLiteral("uuid")).toString(); const QString newUuid = normalizedStation.value(QStringLiteral("uuid")).toString(); if ((!newUuid.isEmpty() && existingUuid == newUuid) || (newUuid.isEmpty() && existing.value(QStringLiteral("urlResolved")).toString() == url)) { m_recentStations.removeAt(i); break; } } m_recentStations.prepend(normalizedStation); while (m_recentStations.size() > kRecentLimit) { m_recentStations.removeLast(); } emit listsChanged(); } void RadioBackend::playCurrentSelection() { const QList list = currentList(); if (list.isEmpty() || m_currentIndex < 0 || m_currentIndex >= list.size()) { return; } RadioStation *station = list[m_currentIndex]; QString url = station->urlResolved().isEmpty() ? station->url() : station->urlResolved(); m_lastStationName = station->name(); m_lastStationUrl = url; emit resumeStateChanged(); recordRecentStation(toVariantMap(station)); saveState(); m_currentStationUuid = station->uuid(); m_currentStationUrl = url; emit currentStationChanged(); m_player->playUrl(url, station->name()); } QVariantMap RadioBackend::toVariantMap(const RadioStation *station) { QVariantMap map; if (!station) { return map; } map.insert(QStringLiteral("uuid"), station->uuid()); map.insert(QStringLiteral("name"), station->name()); map.insert(QStringLiteral("country"), station->country()); map.insert(QStringLiteral("bitrate"), station->bitrate()); map.insert(QStringLiteral("isFavorite"), station->isFavorite()); map.insert(QStringLiteral("favicon"), station->favicon()); map.insert(QStringLiteral("urlResolved"), station->urlResolved()); return map; } bool RadioBackend::matchesStation(const RadioStation *station, const QString &uuid, const QString &urlResolved) { if (!station) { return false; } if (!uuid.isEmpty() && station->uuid() == uuid) { return true; } if (uuid.isEmpty() && !urlResolved.isEmpty() && station->urlResolved() == urlResolved) { return true; } return false; }