strawberry-audio-player-win.../src/radios/radiomodel.cpp

338 lines
9.2 KiB
C++

/*
* Strawberry Music Player
* Copyright 2021, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QObject>
#include <QList>
#include <QVariant>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QMimeData>
#include <QPixmap>
#include <QPixmapCache>
#include <QRegularExpression>
#include "core/application.h"
#include "core/simpletreemodel.h"
#include "playlist/playlistmanager.h"
#include "covermanager/albumcoverloader.h"
#include "covermanager/albumcoverloaderresult.h"
#include "radiomodel.h"
#include "radioservices.h"
#include "radioservice.h"
#include "radiomimedata.h"
#include "radiochannel.h"
const int RadioModel::kTreeIconSize = 22;
RadioModel::RadioModel(Application *app, QObject *parent)
: SimpleTreeModel<RadioItem>(new RadioItem(this), parent),
app_(app) {
if (app_) {
QObject::connect(&*app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &RadioModel::AlbumCoverLoaded);
}
}
RadioModel::~RadioModel() {
delete root_;
}
Qt::ItemFlags RadioModel::flags(const QModelIndex &idx) const {
switch (IndexToItem(idx)->type) {
case RadioItem::Type_Service:
case RadioItem::Type_Channel:
return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled;
case RadioItem::Type_Root:
case RadioItem::Type_LoadingIndicator:
default:
return Qt::ItemIsEnabled;
}
}
QVariant RadioModel::data(const QModelIndex &idx, int role) const {
if (!idx.isValid()) return QVariant();
const RadioItem *item = IndexToItem(idx);
if (!item) return QVariant();
if (role == Qt::DecorationRole && item->type == RadioItem::Type_Channel) {
return const_cast<RadioModel*>(this)->ChannelIcon(idx);
}
return data(item, role);
}
QVariant RadioModel::data(const RadioItem *item, int role) const {
switch (role) {
case Qt::DecorationRole:
if (item->type == RadioItem::Type_Service) {
return Song::IconForSource(item->source);
}
break;
case Qt::DisplayRole:
return item->DisplayText();
break;
case Role_Type:
return item->type;
break;
case Role_SortText:
return item->SortText();
break;
case Role_Source:
return QVariant::fromValue(item->source);
break;
case Role_Homepage:{
RadioService *service = app_->radio_services()->ServiceBySource(item->source);
if (service) return service->Homepage();
break;
}
case Role_Donate:{
RadioService *service = app_->radio_services()->ServiceBySource(item->source);
if (service) return service->Donate();
break;
}
default:
return QVariant();
}
return QVariant();
}
QStringList RadioModel::mimeTypes() const {
return QStringList() << QStringLiteral("text/uri-list");
}
QMimeData *RadioModel::mimeData(const QModelIndexList &indexes) const {
if (indexes.isEmpty()) return nullptr;
RadioMimeData *data = new RadioMimeData;
QList<QUrl> urls;
for (const QModelIndex &idx : indexes) {
GetChildSongs(IndexToItem(idx), &urls, &data->songs);
}
data->setUrls(urls);
data->name_for_new_playlist_ = PlaylistManager::GetNameForNewPlaylist(data->songs);
return data;
}
void RadioModel::Reset() {
beginResetModel();
container_nodes_.clear();
items_.clear();
pending_art_.clear();
pending_cache_keys_.clear();
delete root_;
root_ = new RadioItem(this);
endResetModel();
}
void RadioModel::AddChannels(const RadioChannelList &channels) {
for (const RadioChannel &channel : channels) {
RadioItem *container = nullptr;
if (container_nodes_.contains(channel.source)) {
container = container_nodes_[channel.source];
}
else {
beginInsertRows(ItemToIndex(root_), static_cast<int>(root_->children.count()), static_cast<int>(root_->children.count()));
RadioItem *item = new RadioItem(RadioItem::Type_Service, root_);
item->source = channel.source;
item->display_text = Song::DescriptionForSource(channel.source);
item->sort_text = SortText(Song::TextForSource(channel.source));
container_nodes_.insert(channel.source, item);
endInsertRows();
container = item;
}
beginInsertRows(ItemToIndex(container), static_cast<int>(container->children.count()), static_cast<int>(container->children.count()));
RadioItem *item = new RadioItem(RadioItem::Type_Channel, container);
item->source = channel.source;
item->display_text = channel.name;
item->sort_text = SortText(Song::TextForSource(channel.source) + QStringLiteral(" - ") + channel.name);
item->channel = channel;
items_ << item;
endInsertRows();
}
}
bool RadioModel::IsPlayable(const QModelIndex &idx) const {
return idx.data(Role_Type) == RadioItem::Type_Channel;
}
bool RadioModel::CompareItems(const RadioItem *a, const RadioItem *b) const {
QVariant left(data(a, RadioModel::Role_SortText));
QVariant right(data(b, RadioModel::Role_SortText));
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
if (left.metaType().id() == QMetaType::Int)
#else
if (left.type() == QVariant::Int)
#endif
return left.toInt() < right.toInt();
else return left.toString() < right.toString();
}
void RadioModel::GetChildSongs(RadioItem *item, QList<QUrl> *urls, SongList *songs) const {
switch (item->type) {
case RadioItem::Type_Service:{
QList<RadioItem*> children = item->children;
std::sort(children.begin(), children.end(), std::bind(&RadioModel::CompareItems, this, std::placeholders::_1, std::placeholders::_2));
for (RadioItem *child : children) {
GetChildSongs(child, urls, songs);
}
break;
}
case RadioItem::Type_Channel:
if (!urls->contains(item->channel.url)) {
urls->append(item->channel.url);
songs->append(item->channel.ToSong());
}
break;
default:
break;
}
}
SongList RadioModel::GetChildSongs(const QModelIndexList &indexes) const {
QList<QUrl> urls;
SongList songs;
for (const QModelIndex &idx : indexes) {
GetChildSongs(IndexToItem(idx), &urls, &songs);
}
return songs;
}
SongList RadioModel::GetChildSongs(const QModelIndex &idx) const {
return GetChildSongs(QModelIndexList() << idx);
}
QString RadioModel::ChannelIconPixmapCacheKey(const QModelIndex &idx) const {
QStringList path;
QModelIndex idx_copy = idx;
while (idx_copy.isValid()) {
path.prepend(idx_copy.data().toString());
idx_copy = idx_copy.parent();
}
return path.join(QLatin1Char('/'));
}
QPixmap RadioModel::ServiceIcon(const QModelIndex &idx) const {
return Song::IconForSource(static_cast<Song::Source>(idx.data(Role_Source).toInt())).pixmap(kTreeIconSize, kTreeIconSize);
}
QPixmap RadioModel::ServiceIcon(RadioItem *item) const {
return Song::IconForSource(item->source).pixmap(kTreeIconSize, kTreeIconSize);
}
QPixmap RadioModel::ChannelIcon(const QModelIndex &idx) {
if (!idx.isValid()) return QPixmap();
RadioItem *item = IndexToItem(idx);
if (!item) return ServiceIcon(idx);
const QString cache_key = ChannelIconPixmapCacheKey(idx);
QPixmap cached_pixmap;
if (QPixmapCache::find(cache_key, &cached_pixmap)) {
return cached_pixmap;
}
if (pending_cache_keys_.contains(cache_key)) {
return ServiceIcon(idx);
}
SongList songs = GetChildSongs(idx);
if (!songs.isEmpty()) {
Song song = songs.first();
song.set_art_automatic(item->channel.thumbnail_url);
const quint64 id = app_->album_cover_loader()->LoadImageAsync(AlbumCoverLoaderOptions(AlbumCoverLoaderOptions::Option::ScaledImage | AlbumCoverLoaderOptions::Option::PadScaledImage, QSize(kTreeIconSize, kTreeIconSize)), song);
pending_art_[id] = ItemAndCacheKey(item, cache_key);
pending_cache_keys_.insert(cache_key);
}
return ServiceIcon(idx);
}
void RadioModel::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result) {
if (!pending_art_.contains(id)) return;
ItemAndCacheKey item_and_cache_key = pending_art_.take(id);
RadioItem *item = item_and_cache_key.first;
if (!item) return;
const QString &cache_key = item_and_cache_key.second;
pending_cache_keys_.remove(cache_key);
if (!result.success || result.image_scaled.isNull() || result.type == AlbumCoverLoaderResult::Type::Unset) {
QPixmapCache::insert(cache_key, ServiceIcon(item));
}
else {
QPixmapCache::insert(cache_key, QPixmap::fromImage(result.image_scaled));
}
const QModelIndex idx = ItemToIndex(item);
if (!idx.isValid()) return;
emit dataChanged(idx, idx);
}
QString RadioModel::SortText(QString text) {
if (text.isEmpty()) {
text = QStringLiteral(" unknown");
}
else {
text = text.toLower();
}
text = text.remove(QRegularExpression(QStringLiteral("[^\\w ]"), QRegularExpression::UseUnicodePropertiesOption));
return text;
}