Esempio n. 1
0
	void DataFilterUploader::UploadToAcc (const QByteArray& accId)
	{
		const auto acc = AccMgr_->GetAccount (accId);
		if (!acc)
		{
			qWarning () << Q_FUNC_INFO
					<< "no account for ID"
					<< accId;
			deleteLater ();
			return;
		}

		const auto& image = Entity_.Entity_.value<QImage> ();
		const auto& localFile = Entity_.Entity_.toUrl ().toLocalFile ();
		if (!image.isNull ())
		{
			auto tempFile = new QTemporaryFile { this };
			Entity_.Entity_.value<QImage> ().save (tempFile, "PNG", 0);
			UploadFileName_ = tempFile->fileName ();
		}
		else if (QFile::exists (localFile))
			UploadFileName_ = localFile;

		const auto dia = new UploadPhotosDialog { acc->GetQObject () };
		dia->LockFiles ();
		dia->SetFiles ({ { UploadFileName_, {} } });
		dia->open ();
		dia->setAttribute (Qt::WA_DeleteOnClose);

		new Util::SlotClosure<Util::DeleteLaterPolicy>
		{
			[this, dia, acc]
			{
				connect (acc->GetQObject (),
						SIGNAL (itemUploaded (UploadItem, QUrl)),
						this,
						SLOT (checkItemUploaded (UploadItem, QUrl)));

				const auto isu = qobject_cast<ISupportUploads*> (acc->GetQObject ());
				isu->UploadImages (dia->GetSelectedCollection (), dia->GetSelectedFiles ());
			},
			dia,
			SIGNAL (accepted ()),
			dia
		};

		connect (dia,
				SIGNAL (rejected ()),
				this,
				SLOT (deleteLater ()));
	}
	void NotificationsManager::handleLocationChanged (const QString& variant)
	{
		const auto entry = qobject_cast<ICLEntry*> (sender ());
		const auto acc = entry->GetParentAccount ();
		const auto isg = qobject_cast<ISupportGeolocation*> (acc->GetQObject ());
		if (!isg)
		{
			qWarning () << Q_FUNC_INFO
					<< "account"
					<< acc->GetQObject ()
					<< "does not implement ISupportGeolocation";
			return;
		}

		const auto& infoMap = isg->GetUserGeolocationInfo (sender (), variant);
		const auto& info = [&infoMap]
			{
				bool lonOk = false;
				bool latOk = false;

				GeolocationInfo info
				{
					true,
					infoMap ["lon"].toDouble (&lonOk),
					infoMap ["lat"].toDouble (&latOk),
					infoMap ["country"].toString (),
					infoMap ["locality"].toString ()
				};
				info.IsValid_ = lonOk && latOk;
				return info;
			} ();

		const auto& text = GetHRLocationText (entry, info);

		auto e = Util::MakeNotification ("LeechCraft", text, PInfo_);
		e.Mime_ += "+advanced";

		BuildNotification (e, entry, "LocationChangeEvent");
		e.Additional_ ["org.LC.AdvNotifications.EventType"] = AN::TypeIMEventLocationChange;

		e.Additional_ ["org.LC.AdvNotifications.FullText"] = text;
		e.Additional_ ["org.LC.AdvNotifications.ExtendedText"] = text;
		e.Additional_ ["org.LC.AdvNotifications.Count"] = 1;

		e.Additional_ [AN::Field::IMLocationLongitude] = info.Lon_;
		e.Additional_ [AN::Field::IMLocationLatitude] = info.Lat_;

		EntityMgr_->HandleEntity (e);
	}
Esempio n. 3
0
	void RadioManager::AddUrl (const QModelIndex& index, const QUrl& url, const QString& name)
	{
		WithSourceProv (index,
				[url, name] (Media::IRadioStationProvider *prov, const QModelIndex& srcIdx)
				{
					const auto radio = prov->GetRadioStation (srcIdx, {});
					if (!radio)
					{
						qWarning () << Q_FUNC_INFO
								<< "got a null radio station from provider";
						return;
					}

					auto modifiable = qobject_cast<Media::IModifiableRadioStation*> (radio->GetQObject ());
					if (!modifiable)
					{
						qWarning () << Q_FUNC_INFO
								<< radio->GetRadioName ()
								<< "is not modifiable";
						return;
					}

					modifiable->AddItem (url, name);
				});
	}
	void RecommendationsWidget::InitializeProviders ()
	{
		const auto& provs = Core::Instance ().GetProxy ()->GetPluginsManager ()->
				GetAllCastableTo<Media::IRecommendedArtists*> ();
		for (auto prov : provs)
		{
			const auto pending = prov->RequestRecommended (10);
			new Util::SlotClosure<Util::DeleteLaterPolicy>
			{
				[this, pending] { HandleInfos (pending->GetSimilar ()); },
				pending->GetQObject (),
				SIGNAL (ready ()),
				pending->GetQObject ()
			};
		}
	}
Esempio n. 5
0
	void RadioManager::RemoveUrl (const QModelIndex& index)
	{
		WithSourceProv (index,
				[] (Media::IRadioStationProvider *prov, const QModelIndex& index)
				{
					const auto radio = prov->GetRadioStation (index, {});
					if (!radio)
					{
						qWarning () << Q_FUNC_INFO
								<< "got a null radio station from provider";
						return;
					}

					auto modifiable = qobject_cast<Media::IModifiableRadioStation*> (radio->GetQObject ());
					if (!modifiable)
					{
						qWarning () << Q_FUNC_INFO
								<< radio->GetRadioName ()
								<< "is not modifiable";
						return;
					}

					modifiable->RemoveItem (index);
				});
	}
Esempio n. 6
0
	void RadioManager::RemoveUrl (const QModelIndex& index)
	{
		const auto item = StationsModel_->itemFromIndex (index);
		const auto root = GetRootItem (item);
		if (!Root2Prov_.contains (root))
		{
			qWarning () << Q_FUNC_INFO
					<< "unknown provider for index"
					<< index;
			return;
		}

		const auto radio = Root2Prov_ [root]->GetRadioStation (item, {});
		if (!radio)
		{
			qWarning () << Q_FUNC_INFO
					<< "got a null radio station from provider";
			return;
		}

		auto modifiable = qobject_cast<Media::IModifiableRadioStation*> (radio->GetQObject ());
		if (!modifiable)
		{
			qWarning () << Q_FUNC_INFO
					<< radio->GetRadioName ()
					<< "is not modifiable";
			return;
		}

		modifiable->RemoveItem (index);
	}
Esempio n. 7
0
	void CheckModel::SetMissingReleases (const QList<Media::ReleaseInfo>& releases,
			const Collection::Artist& artist)
	{
		qDebug () << Q_FUNC_INFO << artist.Name_ << releases.size ();

		const auto item = Artist2Item_.value (artist.ID_);
		if (!item)
		{
			qWarning () << Q_FUNC_INFO
					<< "no item for artist"
					<< artist.Name_;
			return;
		}

		Artist2Missings_ [artist.ID_] = releases;

		const auto model = Artist2Submodel_.value (artist.ID_);
		for (const auto& release : releases)
		{
			auto item = new QStandardItem;
			item->setData (release.Name_, ReleasesSubmodel::ReleaseName);
			item->setData (release.Year_, ReleasesSubmodel::ReleaseYear);
			item->setData (DefaultAlbumIcon_, ReleasesSubmodel::ReleaseArt);
			model->appendRow (item);

			const auto proxy = AAProv_->RequestAlbumArt ({ artist.Name_, release.Name_ });
			new Util::OneTimeRunner
			{
				[this, item, proxy] () -> void
				{
					proxy->GetQObject ()->deleteLater ();
					const auto& url = proxy->GetImageUrls ().value (0);
					if (url.isEmpty ())
						return;

					item->setData (url, ReleasesSubmodel::ReleaseArt);
				},
				proxy->GetQObject (),
				SIGNAL (urlsReady (Media::AlbumInfo, QList<QUrl>)),
				this
			};
		}

		item->setData (releases.size (), Role::MissingCount);
		item->setData (true, Role::IsChecked);
	}
Esempio n. 8
0
	void DataFilterUploader::UploadToAcc (const QByteArray& accId)
	{
		bool shouldCleanup = true;
		auto deleteGuard = std::shared_ptr<void> (nullptr,
				[this, shouldCleanup] (void*)
				{
					if (shouldCleanup)
						deleteLater ();
				});

		const auto acc = AccMgr_->GetAccount (accId);
		if (!acc)
		{
			qWarning () << Q_FUNC_INFO
					<< "no account for ID"
					<< accId;
			return;
		}

		const auto& image = Entity_.Entity_.value<QImage> ();
		const auto& localFile = Entity_.Entity_.toUrl ().toLocalFile ();
		if (!image.isNull ())
		{
			auto tempFile = new QTemporaryFile { this };
			Entity_.Entity_.value<QImage> ().save (tempFile, "PNG", 0);
			UploadFileName_ = tempFile->fileName ();
		}
		else if (QFile::exists (localFile))
			UploadFileName_ = localFile;

		UploadPhotosDialog dia { acc->GetQObject() };
		dia.LockFiles ();
		dia.SetFiles ({ { UploadFileName_, {} } });
		if (dia.exec () != QDialog::Accepted)
			return;

		connect (acc->GetQObject (),
				SIGNAL (itemUploaded (UploadItem, QUrl)),
				this,
				SLOT (checkItemUploaded (UploadItem, QUrl)));

		const auto isu = qobject_cast<ISupportUploads*> (acc->GetQObject ());
		isu->UploadImages (dia.GetSelectedCollection (), dia.GetSelectedFiles ());
		shouldCleanup = false;
	}
Esempio n. 9
0
	void Checker::rotateQueue ()
	{
		if (Artists_.isEmpty ())
		{
			HandleReady ();
			return;
		}

		Current_ = Artists_.takeFirst ();
		const auto pending = Provider_->GetDiscography (Current_.Name_);
		connect (pending->GetQObject (),
				SIGNAL (ready ()),
				this,
				SLOT (handleDiscoReady ()));
		connect (pending->GetQObject (),
				SIGNAL (error (QString)),
				this,
				SLOT (handleDiscoError ()));
	}
Esempio n. 10
0
	QList<Media::IPendingAudioSearch*> PreviewHandler::RequestPreview (const Media::AudioSearchRequest& req)
	{
		QList<Media::IPendingAudioSearch*> pendings;
		for (auto prov : Providers_)
		{
			auto pending = prov->Search (req);
			connect (pending->GetQObject (),
					SIGNAL (ready ()),
					this,
					SLOT (handlePendingReady ()));
			pendings << pending;
		}
		return pendings;
	}
Esempio n. 11
0
	void RecommendationsWidget::on_RecProvider__activated (int index)
	{
		if (index < 0 || index >= Providers_.size ())
			return;

		auto pending = Providers_.at (index)->RequestRecommended (10);
		connect (pending->GetQObject (),
				SIGNAL (ready ()),
				this,
				SLOT (handleGotRecs ()));

		auto scrob = qobject_cast<Media::IAudioScrobbler*> (ProvRoots_.at (index));
		XmlSettingsManager::Instance ()
				.setProperty ("LastUsedRecsProvider", scrob->GetServiceName ());
	}
Esempio n. 12
0
	void NotificationsManager::handleMUCInvitation (const QVariantMap& ident,
			const QString& inviter, const QString& reason)
	{
		const auto acc = qobject_cast<IAccount*> (sender ());
		if (!acc)
		{
			qWarning () << Q_FUNC_INFO
					<< sender ()
					<< "doesn't implement IAccount";
			return;
		}

		const auto& name = ident ["HumanReadableName"].toString ();

		const auto str = reason.isEmpty () ?
				tr ("You have been invited to %1 by %2.")
					.arg (name)
					.arg (inviter) :
				tr ("You have been invited to %1 by %2: %3")
					.arg (name)
					.arg (inviter)
					.arg (reason);

		auto e = Util::MakeNotification ("Azoth", str, PInfo_);
		e.Additional_ ["org.LC.AdvNotifications.SenderID"] = "org.LeechCraft.Azoth";
		e.Additional_ ["org.LC.AdvNotifications.EventCategory"] = AN::CatIM;
		e.Additional_ ["org.LC.AdvNotifications.VisualPath"] = QStringList (name);
		e.Additional_ ["org.LC.AdvNotifications.EventID"] =
				"org.LC.Plugins.Azoth.Invited/" + name + '/' + inviter;
		e.Additional_ ["org.LC.AdvNotifications.EventType"] = AN::TypeIMMUCInvite;
		e.Additional_ ["org.LC.AdvNotifications.FullText"] = str;
		e.Additional_ ["org.LC.AdvNotifications.Count"] = 1;
		e.Additional_ ["org.LC.Plugins.Azoth.Msg"] = reason;

		const auto& cancel = Util::MakeANCancel (e);

		const auto nh = new Util::NotificationActionHandler { e };
		nh->AddFunction (tr ("Join"), [this, acc, ident, cancel] ()
				{
					SuggestJoiningMUC (acc, ident);
					EntityMgr_->HandleEntity (cancel);
				});
		nh->AddDependentObject (acc->GetQObject ());

		EntityMgr_->HandleEntity (e);
	}
Esempio n. 13
0
	void NotificationsManager::handleAttentionDrawn (const QString& text, const QString&)
	{
		if (XmlSettingsManager::Instance ()
				.property ("IgnoreDrawAttentions").toBool ())
			return;

		const auto entry = qobject_cast<ICLEntry*> (sender ());
		if (!entry)
		{
			qWarning () << Q_FUNC_INFO
					<< sender ()
					<< "doesn't implement ICLEntry";
			return;
		}

		const auto& str = text.isEmpty () ?
				tr ("%1 requests your attention")
					.arg (entry->GetEntryName ()) :
				tr ("%1 requests your attention: %2")
					.arg (entry->GetEntryName ())
					.arg (text);

		auto e = Util::MakeNotification ("Azoth", str, PInfo_);
		BuildNotification (e, entry, "AttentionDrawnBy");
		e.Additional_ ["org.LC.AdvNotifications.DeltaCount"] = 1;
		e.Additional_ ["org.LC.AdvNotifications.EventType"] = AN::TypeIMAttention;
		e.Additional_ ["org.LC.AdvNotifications.ExtendedText"] = tr ("Attention requested");
		e.Additional_ ["org.LC.AdvNotifications.FullText"] = tr ("Attention requested by %1")
				.arg (entry->GetEntryName ());
		e.Additional_ ["org.LC.Plugins.Azoth.Msg"] = text;

		const auto nh = new Util::NotificationActionHandler { e, this };
		nh->AddFunction (tr ("Open chat"),
				[entry, this] { Core::Instance ().GetChatTabsManager ()->OpenChat (entry, true); });
		nh->AddDependentObject (entry->GetQObject ());

		EntityMgr_->HandleEntity (e);
	}
Esempio n. 14
0
	void NotificationsManager::handleAuthorizationRequested (QObject *entryObj, const QString& msg)
	{
		const auto& proxy = std::make_shared<Util::DefaultHookProxy> ();
		emit hookGotAuthRequest (proxy, entryObj, msg);
		if (proxy->IsCancelled ())
			return;

		const auto entry = qobject_cast<ICLEntry*> (entryObj);
		if (!entry)
		{
			qWarning () << Q_FUNC_INFO
					<< entryObj
					<< "doesn't implement ICLEntry";
			return;
		}

		const auto& str = msg.isEmpty () ?
				tr ("Subscription requested by %1.")
					.arg (entry->GetEntryName ()) :
				tr ("Subscription requested by %1: %2.")
					.arg (entry->GetEntryName ())
					.arg (msg);
		auto e = Util::MakeNotification ("Azoth", str, PInfo_);

		BuildNotification (e, entry, "AuthRequestFrom");
		e.Additional_ ["org.LC.AdvNotifications.EventType"] = AN::TypeIMSubscrRequest;
		e.Additional_ ["org.LC.AdvNotifications.FullText"] = str;
		e.Additional_ ["org.LC.AdvNotifications.Count"] = 1;
		e.Additional_ ["org.LC.Plugins.Azoth.Msg"] = msg;

		const auto nh = new Util::NotificationActionHandler (e, this);
		nh->AddFunction (tr ("Authorize"), [this, entry] () { AuthorizeEntry (entry); });
		nh->AddFunction (tr ("Deny"), [this, entry] () { DenyAuthForEntry (entry); });
		nh->AddFunction (tr ("View info"), [entry] () { entry->ShowInfo (); });
		nh->AddDependentObject (entry->GetQObject ());
		EntityMgr_->HandleEntity (e);
	}
	void TemplatesEditorWidget::loadTemplate ()
	{
		const auto currentType = Ui_.Editor_->GetCurrentEditorType ();
		const auto msgType = static_cast<MsgType> (Ui_.MessageType_->currentIndex ());

		Util::Visit (TemplatesMgr_->GetTemplate (currentType, msgType, nullptr).AsVariant (),
				[=] (const QString& tpl)
				{
					auto editor = Ui_.Editor_->GetCurrentEditor ();
					editor->SetContents (tpl, currentType);

					connect (editor->GetQObject (),
							SIGNAL (textChanged ()),
							this,
							SLOT (markAsDirty ()));
				},
				[=] (const auto& err)
				{
					QMessageBox::critical (this,
							"LeechCraft",
							tr ("Unable to load template: %1.")
								.arg (err.what ()));
				});
	}
Esempio n. 16
0
	void NotificationsManager::HandleMessage (IMessage *msg)
	{
		const bool showMsg = XmlSettingsManager::Instance ()
				.property ("ShowMsgInNotifications").toBool ();

		const auto other = qobject_cast<ICLEntry*> (msg->OtherPart ());
		const auto parentCL = qobject_cast<ICLEntry*> (msg->ParentCLEntry ());

		QString msgString;
		bool isHighlightMsg = false;
		switch (msg->GetMessageType ())
		{
		case IMessage::Type::ChatMessage:
			if (XmlSettingsManager::Instance ()
					.property ("NotifyAboutIncomingMessages").toBool ())
			{
				if (!showMsg)
					msgString = tr ("Incoming chat message from <em>%1</em>.")
							.arg (other->GetEntryName ());
				else
				{
					const auto& body = msg->GetEscapedBody ();
					const auto& notifMsg = body.size () > 50 ?
							body.left (50) + "..." :
							body;
					msgString = tr ("Incoming chat message from <em>%1</em>: <em>%2</em>")
							.arg (other->GetEntryName ())
							.arg (notifMsg);
				}
			}
			break;
		case IMessage::Type::MUCMessage:
		{
			isHighlightMsg = Core::Instance ().IsHighlightMessage (msg);
			if (isHighlightMsg && XmlSettingsManager::Instance ()
					.property ("NotifyAboutConferenceHighlights").toBool ())
			{
				if (!showMsg)
					msgString = tr ("Highlighted in conference <em>%1</em> by <em>%2</em>.")
							.arg (parentCL->GetEntryName ())
							.arg (other->GetEntryName ());
				else
				{
					const auto& body = msg->GetEscapedBody ();
					const auto& notifMsg = body.size () > 50 ?
							body.left (50) + "..." :
							body;
					msgString = tr ("Highlighted in conference <em>%1</em> by <em>%2</em>: <em>%3</em>")
							.arg (parentCL->GetEntryName ())
							.arg (other->GetEntryName ())
							.arg (notifMsg);
				}
			}
			break;
		}
		default:
			return;
		}

		auto e = Util::MakeNotification ("Azoth",
				msgString,
				PInfo_);

		if (msgString.isEmpty ())
			e.Mime_ += "+advanced";

		auto entry = msg->GetMessageType () == IMessage::Type::MUCMessage ?
				parentCL :
				other;
		BuildNotification (e, entry);

		const auto count = ++UnreadCounts_ [entry];
		if (msg->GetMessageType () == IMessage::Type::MUCMessage)
		{
			e.Additional_ ["org.LC.Plugins.Azoth.SubSourceID"] = other->GetEntryID ();
			e.Additional_ ["org.LC.AdvNotifications.EventType"] = isHighlightMsg ?
					AN::TypeIMMUCHighlight :
					AN::TypeIMMUCMsg;
			e.Additional_ ["NotificationPixmap"] = QVariant::fromValue (other->GetAvatar ());

			if (isHighlightMsg)
				e.Additional_ ["org.LC.AdvNotifications.FullText"] =
					tr ("%n message(s) from", 0, count) + ' ' + other->GetEntryName () +
							" <em>(" + parentCL->GetEntryName () + ")</em>";
			else
				e.Additional_ ["org.LC.AdvNotifications.FullText"] =
					tr ("%n message(s) in", 0, count) + ' ' + parentCL->GetEntryName ();
		}
		else
		{
			e.Additional_ ["org.LC.AdvNotifications.EventType"] = AN::TypeIMIncMsg;
			e.Additional_ ["org.LC.AdvNotifications.FullText"] =
				tr ("%n message(s) from", 0, count) +
						' ' + other->GetEntryName ();
		}

		e.Additional_ ["org.LC.AdvNotifications.Count"] = count;

		e.Additional_ ["org.LC.AdvNotifications.ExtendedText"] = tr ("%n message(s)", 0, count);
		e.Additional_ ["org.LC.Plugins.Azoth.Msg"] = msg->GetEscapedBody ();

		const auto nh = new Util::NotificationActionHandler { e, this };
		nh->AddFunction (tr ("Open chat"),
				[parentCL] { Core::Instance ().GetChatTabsManager ()->OpenChat (parentCL, true); });
		nh->AddDependentObject (parentCL->GetQObject ());

		EntityMgr_->HandleEntity (e);
	}
Esempio n. 17
0
	void ContactDropFilter::HandleContactsDropped (const QMimeData *data)
	{
		const auto thisEntry = GetEntry<ICLEntry> (EntryId_);
		const bool isMuc = thisEntry->GetEntryType () == ICLEntry::EntryType::MUC;

		auto entries = DndUtil::DecodeEntryObjs (data);
		entries.erase (std::remove_if (entries.begin (), entries.end (),
					[thisEntry] (QObject *entryObj)
					{
						return !CanEntryBeInvited (thisEntry,
								qobject_cast<ICLEntry*> (entryObj));
					}),
				entries.end ());

		if (entries.isEmpty ())
			return;

		QString text;
		if (entries.size () > 1)
			text = isMuc ?
					tr ("Enter reason to invite %n contact(s) to %1:", 0, entries.size ())
						.arg (thisEntry->GetEntryName ()) :
					tr ("Enter reason to invite %1 to %n conference(s):", 0, entries.size ())
						.arg (thisEntry->GetEntryName ());
		else
		{
			const auto muc = isMuc ?
					thisEntry :
					qobject_cast<ICLEntry*> (entries.first ());
			const auto entry = isMuc ?
					qobject_cast<ICLEntry*> (entries.first ()) :
					thisEntry;
			text = tr ("Enter reason to invite %1 to %2:")
					.arg (entry->GetEntryName ())
					.arg (muc->GetEntryName ());
		}

		bool ok = false;
		auto reason = QInputDialog::getText (nullptr,
				tr ("Invite to a MUC"),
				text,
				QLineEdit::Normal,
				{},
				&ok);
		if (!ok)
			return;

		if (isMuc)
		{
			const auto muc = qobject_cast<IMUCEntry*> (thisEntry->GetQObject ());

			for (const auto& entry : entries)
				muc->InviteToMUC (qobject_cast<ICLEntry*> (entry)->GetHumanReadableID (), reason);
		}
		else
		{
			const auto thisId = thisEntry->GetHumanReadableID ();

			for (const auto& mucEntryObj : entries)
			{
				const auto muc = qobject_cast<IMUCEntry*> (mucEntryObj);
				muc->InviteToMUC (thisId, reason);
			}
		}
	}
Esempio n. 18
0
	CheckModel::CheckModel (const Collection::Artists_t& artists,
			const ICoreProxy_ptr& proxy, const ILMPProxy_ptr& lmpProxy, QObject *parent)
	: RoleNamesMixin<QStandardItemModel> { parent }
	, AllArtists_ { artists }
	, Proxy_ { lmpProxy }
	, DefaultAlbumIcon_ { GetIcon (proxy, "media-optical", AASize * 2) }
	, DefaultArtistIcon_ { GetIcon (proxy, "view-media-artist", ArtistSize * 2) }
	, AAProv_ { proxy->GetPluginsManager ()->
				GetAllCastableTo<Media::IAlbumArtProvider*> ().value (0) }
	, BioProv_ { proxy->GetPluginsManager ()->
				GetAllCastableTo<Media::IArtistBioFetcher*> ().value (0) }
	{
		QHash<int, QByteArray> roleNames;
		roleNames [Role::ArtistId] = "artistId";
		roleNames [Role::ArtistName] = "artistName";
		roleNames [Role::ScheduledToCheck] = "scheduled";
		roleNames [Role::IsChecked] = "isChecked";
		roleNames [Role::ArtistImage] = "artistImageUrl";
		roleNames [Role::Releases] = "releases";
		roleNames [Role::MissingCount] = "missingCount";
		roleNames [Role::PresentCount] = "presentCount";
		setRoleNames (roleNames);

		for (const auto& artist : artists)
		{
			if (artist.Name_.contains (" vs. ") ||
					artist.Name_.contains (" with ") ||
					artist.Albums_.isEmpty ())
				continue;

			auto item = new QStandardItem { artist.Name_ };
			item->setData (artist.ID_, Role::ArtistId);
			item->setData (artist.Name_, Role::ArtistName);
			item->setData (true, Role::ScheduledToCheck);
			item->setData (false, Role::IsChecked);
			item->setData (DefaultArtistIcon_, Role::ArtistImage);
			item->setData (0, Role::MissingCount);
			item->setData (artist.Albums_.size (), Role::PresentCount);

			const auto submodel = new ReleasesSubmodel { this };
			item->setData (QVariant::fromValue<QObject*> (submodel), Role::Releases);

			appendRow (item);

			Artist2Submodel_ [artist.ID_] = submodel;
			Artist2Item_ [artist.ID_] = item;

			Scheduled_ << artist.ID_;

			const auto proxy = BioProv_->RequestArtistBio (artist.Name_, false);
			new Util::OneTimeRunner
			{
				[this, artist, item, proxy] () -> void
				{
					if (!Artist2Item_.contains (artist.ID_))
						return;

					const auto& url = proxy->GetArtistBio ().BasicInfo_.LargeImage_;
					item->setData (url, Role::ArtistImage);
				},
				proxy->GetQObject (),
				{
					SIGNAL (ready ()),
					SIGNAL (error ())
				},
				this
			};
		}
	}