/* * Cantata * * Copyright (c) 2011-2014 Craig Drummond * * ---- * * 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 2 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; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "config.h" #include "musiclibraryitemalbum.h" #include "musiclibraryitemartist.h" #include "musiclibraryitemsong.h" #include "musiclibraryitemroot.h" #include "musiclibraryitempodcast.h" #include "musiclibrarymodel.h" #include "dirviewmodel.h" #include "onlineservicesmodel.h" #include "onlineservice.h" #ifdef ENABLE_DEVICES_SUPPORT #include "onlinedevice.h" #endif #include "jamendoservice.h" #include "magnatuneservice.h" #include "soundcloudservice.h" #include "podcastservice.h" #include "playqueuemodel.h" #include "roles.h" #include "mpdparseutils.h" #include "localize.h" #include "plurals.h" #include "icons.h" #include "filejob.h" #include "utils.h" #include "stdactions.h" #include "actioncollection.h" #include "networkaccessmanager.h" #include "settings.h" #include "globalstatic.h" #include #include #include #include #include #if defined ENABLE_MODEL_TEST #include "modeltest.h" #endif QString OnlineServicesModel::constUdiPrefix("online-service://"); GLOBAL_STATIC(OnlineServicesModel, instance) OnlineServicesModel::OnlineServicesModel(QObject *parent) : MultiMusicModel(parent) , enabled(false) , dev(0) , podcast(0) { configureAction = ActionCollection::get()->createAction("configureonlineservice", i18n("Configure Service"), Icons::self()->configureIcon); refreshAction = ActionCollection::get()->createAction("refreshonlineservice", i18n("Refresh Service"), "view-refresh"); subscribeAction = ActionCollection::get()->createAction("subscribeonlineservice", i18n("Add Subscription"), "list-add"); unSubscribeAction = ActionCollection::get()->createAction("unsubscribeonlineservice", i18n("Remove Subscription"), "list-remove"); refreshSubscriptionAction = ActionCollection::get()->createAction("refreshsubscription", i18n("Refresh Subscription"), "view-refresh"); load(); #if defined ENABLE_MODEL_TEST new ModelTest(this, this); #endif } OnlineServicesModel::~OnlineServicesModel() { } QModelIndex OnlineServicesModel::serviceIndex(const OnlineService *srv) const { int r=row(const_cast(srv)); return -1==r ? QModelIndex() : createIndex(r, 0, (void *)srv); } bool OnlineServicesModel::hasChildren(const QModelIndex &index) const { return !index.isValid() || MusicLibraryItem::Type_Song!=static_cast(index.internalPointer())->itemType(); } bool OnlineServicesModel::canFetchMore(const QModelIndex &index) const { return index.isValid() && MusicLibraryItem::Type_Root==static_cast(index.internalPointer())->itemType() && !static_cast(index.internalPointer())->isLoaded() && !static_cast(index.internalPointer())->isSearchBased() && !static_cast(index.internalPointer())->isPodcasts(); } void OnlineServicesModel::fetchMore(const QModelIndex &index) { if (!index.isValid()) { return; } if (canFetchMore(index)) { static_cast(index.internalPointer())->reload(); } } QVariant OnlineServicesModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } MusicLibraryItem *item = static_cast(index.internalPointer()); switch (role) { case Qt::ToolTipRole: if (MusicLibraryItem::Type_Root==item->itemType() && 0!=item->childCount() && static_cast(item)->isSearchBased() && !static_cast(item)->currentSearchString().isEmpty()) { return MusicModel::data(index, role).toString()+"
"+i18n("Last Search:%1", static_cast(item)->currentSearchString()); } break; case Cantata::Role_SubText: switch(item->itemType()) { case MusicLibraryItem::Type_Root: { OnlineService *srv=static_cast(item); if (srv->isLoading()) { return static_cast(item)->statusMessage(); } if (srv->isSearching()) { return i18n("Searching..."); } if (!srv->isLoaded() && !srv->isSearchBased() && !srv->isPodcasts()) { return i18n("Not Loaded"); } if (0==item->childCount() && srv->isSearchBased()) { return i18n("Use search to locate tracks"); } if (srv->isSearchBased()) { return Plurals::tracks(item->childCount()); } if (srv->isPodcasts()) { return Plurals::podcasts(item->childCount()); } break; } case MusicLibraryItem::Type_Song: { if (MusicLibraryItem::Type_Podcast==item->parentItem()->itemType()) { if (static_cast(item)->downloadProgress()>=0) { return MultiMusicModel::data(index, role).toString()+QLatin1Char(' ')+ i18n("(Downloading: %1%)", static_cast(item)->downloadProgress()); } } break; } default: break; } break; case Cantata::Role_Actions: { QList actions; if (MusicLibraryItem::Type_Root==item->itemType()) { OnlineService *srv=static_cast(item); if (srv->canConfigure()) { actions << configureAction; } if (srv->canLoad()) { actions << refreshAction; } else if (srv->canSubscribe() && !srv->childItems().isEmpty()) { actions << refreshSubscriptionAction; } if (srv->isSearchBased() || srv->isLoaded()) { actions << StdActions::self()->searchAction; } if (srv->canSubscribe()) { actions << subscribeAction; } } else if (MusicLibraryItem::Type_Podcast==item->itemType()) { actions << refreshSubscriptionAction; } else { actions << StdActions::self()->replacePlayQueueAction << StdActions::self()->addToPlayQueueAction; } if (!actions.isEmpty()) { QVariant v; v.setValue >(actions); return v; } break; } case Qt::DecorationRole: if (MusicLibraryItem::Type_Song==item->itemType() && item->parentItem() && MusicLibraryItem::Type_Podcast==item->parentItem()->itemType()) { if (!static_cast(item)->localPath().isEmpty()) { return Icons::self()->downloadedPodcastEpisodeIcon; } } break; case Qt::FontRole: if ((MusicLibraryItem::Type_Song==item->itemType() && item->parentItem() && MusicLibraryItem::Type_Podcast==item->parentItem()->itemType() && !static_cast(item)->song().hasBeenPlayed()) || (MusicLibraryItem::Type_Podcast==item->itemType() && static_cast(item)->unplayedEpisodes()>0)) { QFont f; f.setBold(true); return f; } break; case Qt::DisplayRole: if (MusicLibraryItem::Type_Podcast==item->itemType() && static_cast(item)->unplayedEpisodes()) { return i18nc("podcast name (num unplayed episodes)", "%1 (%2)", item->data(), static_cast(item)->unplayedEpisodes()); } default: break; } return MultiMusicModel::data(index, role); } void OnlineServicesModel::clear() { QSet names; foreach (MusicLibraryItemRoot *col, collections) { names.insert(col->id()); } foreach (const QString &n, names) { removeService(n); } collections.clear(); } void OnlineServicesModel::setEnabled(bool e) { if (e==enabled) { return; } enabled=e; if (!enabled) { stop(); } } void OnlineServicesModel::stop() { foreach (MusicLibraryItemRoot *col, collections) { static_cast(col)->stopLoader(); } } OnlineService * OnlineServicesModel::service(const QString &name) { int idx=name.isEmpty() ? -1 : indexOf(name); if (idx>=0) { return static_cast(collections.at(idx)); } foreach (OnlineService *srv, hiddenServices) { if (srv->id()==name) { return srv; } } return 0; } void OnlineServicesModel::setBusy(const QString &id, bool b) { int before=busyServices.count(); if (b) { busyServices.insert(id); } else { busyServices.remove(id); } if (before!=busyServices.count()) { emit busy(busyServices.count()); } } Qt::ItemFlags OnlineServicesModel::flags(const QModelIndex &index) const { if (index.isValid()) { return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled; } return Qt::NoItemFlags; } OnlineService * OnlineServicesModel::addService(const QString &name, const QSet &hidden) { OnlineService *srv=0; if (!service(name)) { if (name==JamendoService::constName) { srv=new JamendoService(this); } else if (name==MagnatuneService::constName) { srv=new MagnatuneService(this); } else if (name==SoundCloudService::constName) { srv=new SoundCloudService(this); } else if (name==PodcastService::constName) { srv=podcast=new PodcastService(this); } if (srv) { srv->loadConfig(); if (hidden.contains(srv->id())) { hiddenServices.append(srv); } else { beginInsertRows(QModelIndex(), collections.count(), collections.count()); collections.append(srv); endInsertRows(); } connect(srv, SIGNAL(error(const QString &)), SIGNAL(error(const QString &))); } } return srv; } void OnlineServicesModel::removeService(const QString &name) { int idx=indexOf(name); if (idx<0) { return; } OnlineService *srv=static_cast(collections.at(idx)); if (srv) { beginRemoveRows(QModelIndex(), idx, idx); collections.takeAt(idx); endRemoveRows(); MultiMusicModel::updateGenres(); // Destroy will stop service, and delete it (via deleteLater()) srv->destroy(); } } void OnlineServicesModel::stateChanged(const QString &name, bool state) { if (!state) { int idx=indexOf(name); if (idx<0) { return; } MultiMusicModel::updateGenres(); } } void OnlineServicesModel::load() { QSet hidden=Settings::self()->hiddenOnlineProviders().toSet(); addService(JamendoService::constName, hidden); addService(MagnatuneService::constName, hidden); addService(SoundCloudService::constName, hidden); addService(PodcastService::constName, hidden); } void OnlineServicesModel::setSearch(const QString &serviceName, const QString &text) { OnlineService *srv=service(serviceName); if (srv) { srv->setSearch(text); } } QMimeData * OnlineServicesModel::mimeData(const QModelIndexList &indexes) const { QMimeData *mimeData=0; QStringList paths=filenames(indexes); if (!paths.isEmpty()) { mimeData=new QMimeData(); PlayQueueModel::encode(*mimeData, PlayQueueModel::constUriMimeType, paths); } return mimeData; } #ifdef ENABLE_DEVICES_SUPPORT Device * OnlineServicesModel::device(const QString &udi) { if (!dev) { dev=new OnlineDevice(); } dev->setData(udi.mid(constUdiPrefix.length())); return dev; } #endif bool OnlineServicesModel::subscribePodcast(const QUrl &url) { if (podcast && !podcast->subscribedToUrl(url)) { podcast->addUrl(url); return true; } return false; } void OnlineServicesModel::downloadPodcasts(MusicLibraryItemPodcast *pod, const QList &episodes) { podcast->downloadPodcasts(pod, episodes); } void OnlineServicesModel::deleteDownloadedPodcasts(MusicLibraryItemPodcast *pod, const QList &episodes) { podcast->deleteDownloadedPodcasts(pod, episodes); } bool OnlineServicesModel::isDownloading() const { return podcast->isDownloading(); } void OnlineServicesModel::cancelAll() { podcast->cancelAllJobs(); } void OnlineServicesModel::resetModel() { OnlineService *srv=service(PodcastService::constName); if (srv) { foreach (MusicLibraryItem *p, srv->childItems()) { if (MusicLibraryItem::Type_Podcast==p->itemType()) { MusicLibraryItemPodcast *pc=static_cast(p); pc->clearImage(); } } } ActionModel::resetModel(); } static const char * constExtensions[]={".jpg", ".png", 0}; static const char * constIdProperty="id"; static const char * constArtistProperty="artist"; static const char * constAlbumProperty="album"; static const char * constCacheProperty="cacheName"; static const char * constMaxSizeProperty="maxSize"; #define DBUG if (Covers::debugEnabled()) qWarning() << "OnlineServicesModel" << __FUNCTION__ static Covers::Image readCache(const QString &id, const QString &artist, const QString &album) { DBUG << id << artist << album; Covers::Image i; QString baseName=Utils::cacheDir(id.toLower(), false)+Covers::encodeName(album.isEmpty() ? artist : (artist+" - "+album)); for (int e=0; constExtensions[e]; ++e) { QString fileName=baseName+constExtensions[e]; if (QFile::exists(fileName)) { QImage img(fileName); if (!img.isNull()) { DBUG << "Read from cache" << fileName; i.img=img; i.fileName=fileName; break; } } } return i; } Covers::Image OnlineServicesModel::readImage(const Song &song) { DBUG << song.file << song.albumArtist() << song.album; QUrl u(song.file); Covers::Image img; QString id; if (u.host().contains(JamendoService::constName, Qt::CaseInsensitive)) { id=JamendoService::constName; } else if (u.host().contains(MagnatuneService::constName, Qt::CaseInsensitive)) { id=MagnatuneService::constName; } if (!id.isEmpty()) { img=readCache(id, song.albumArtist(), song.album); } return img; } QImage OnlineServicesModel::requestImage(const QString &id, const QString &artist, const QString &album, const QString &url, const QString cacheName, int maxSize) { DBUG << id << artist << album << url << cacheName << maxSize; if (cacheName.isEmpty()) { Covers::Image i=readCache(id, artist, album); if (!i.img.isNull()) { return i.img; } } else if (QFile::exists(cacheName)) { QImage img(cacheName); if (!img.isNull()) { DBUG << "Used cache filename"; return img; } } QString imageUrl=url; // Jamendo image URL is just the album ID! if (!imageUrl.isEmpty() && !imageUrl.startsWith("http:/") && imageUrl.length()<15 && id==JamendoService::constName) { imageUrl=JamendoService::imageUrl(imageUrl); DBUG << "Built jamendo url" << imageUrl; } if (!imageUrl.isEmpty()) { DBUG << "Download url"; // Need to download image... NetworkJob *j=NetworkAccessManager::self()->get(QUrl(imageUrl)); j->setProperty(constIdProperty, id); j->setProperty(constArtistProperty, artist); j->setProperty(constAlbumProperty, album); j->setProperty(constCacheProperty, cacheName); j->setProperty(constMaxSizeProperty, maxSize); connect(j, SIGNAL(finished()), this, SLOT(imageDownloaded())); } return QImage(); } void OnlineServicesModel::imageDownloaded() { NetworkJob *j=qobject_cast(sender()); if (!j) { return; } j->deleteLater(); QByteArray data=QNetworkReply::NoError==j->error() ? j->readAll() : QByteArray(); if (data.isEmpty()) { DBUG << j->url().toString() << "empty!"; return; } QString url=j->url().toString(); QImage img=QImage::fromData(data, Covers::imageFormat(data)); if (img.isNull()) { DBUG << url << "null image"; return; } bool png=Covers::isPng(data); QString id=j->property(constIdProperty).toString(); Song song; song.albumartist=song.artist=j->property(constArtistProperty).toString(); song.album=j->property(constAlbumProperty).toString(); DBUG << "Got image" << id << song.artist << song.album << png; OnlineService *srv=service(id); if (id==PodcastService::constName) { MusicLibraryItem *podcast=srv->childItem(song.artist); if (podcast && static_cast(podcast)->setCover(img)) { QModelIndex idx=index(podcast->row(), 0, index(row(srv), 0, QModelIndex())); emit dataChanged(idx, idx); } } else if (!song.album.isEmpty() && srv->useAlbumImages() && !srv->isPodcasts() && srv->id()==id) { MusicLibraryItemArtist *artistItem = srv->artist(song, false); if (artistItem) { MusicLibraryItemAlbum *albumItem = artistItem->album(song, false); if (albumItem && albumItem->saveToCache(img)) { QModelIndex idx=index(albumItem->row(), 0, index(artistItem->row(), 0, index(row(srv), 0, QModelIndex()))); emit dataChanged(idx, idx); } } } int maxSize=j->property(constMaxSizeProperty).toInt(); QString cacheName=j->property(constCacheProperty).toString(); QString fileName=(cacheName.isEmpty() ? Utils::cacheDir(id.toLower(), true)+Covers::encodeName(song.album.isEmpty() ? song.artist : (song.artist+" - "+song.album))+(png ? ".png" : ".jpg") : cacheName); if (!img.isNull() && (maxSize>0 || !cacheName.isEmpty())) { if (maxSize>32 && (img.width()>maxSize || img.height()>maxSize)) { img=img.scaled(maxSize, maxSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); } DBUG << "Saved scaled image to" << fileName << maxSize; img.save(fileName); if (!song.album.isEmpty()) { song.track=1; //song.setKey(); Covers::self()->emitCoverUpdated(song, img, fileName); } return; } QFile f(fileName); if (f.open(QIODevice::WriteOnly)) { DBUG << "Saved image to" << fileName; f.write(data); if (!song.album.isEmpty()) { song.track=1; //song.setKey(); Covers::self()->emitCoverUpdated(song, img, fileName); } } } void OnlineServicesModel::save() { QStringList disabled; foreach (OnlineService *i, hiddenServices) { disabled.append(i->id()); } disabled.sort(); Settings::self()->saveHiddenOnlineProviders(disabled); } QList OnlineServicesModel::getProviders() const { QList providers; foreach (OnlineService *i, hiddenServices) { providers.append(Provider(i->data(), static_cast(i)->icon(), static_cast(i)->id(), true, static_cast(i)->canConfigure())); } foreach (MusicLibraryItemRoot *i, collections) { providers.append(Provider(i->data(), static_cast(i)->icon(), static_cast(i)->id(), false, static_cast(i)->canConfigure())); } return providers; } void OnlineServicesModel::setHiddenProviders(const QSet &prov) { // bool added=false; bool changed=false; foreach (OnlineService *i, hiddenServices) { if (!prov.contains(i->id())) { beginInsertRows(QModelIndex(), collections.count(), collections.count()); collections.append(i); hiddenServices.removeAll(i); endInsertRows(); /*added=*/changed=true; } } foreach (MusicLibraryItemRoot *i, collections) { if (prov.contains(static_cast(i)->id())) { int r=row(i); if (r>=0) { beginRemoveRows(QModelIndex(), r, r); hiddenServices.append(static_cast(collections.takeAt(r))); endRemoveRows(); changed=true; } } } // if (added && collections.count()>1) { // emit needToSort(); // } updateGenres(); if (changed) { emit providersChanged(); } }