Clementine-audio-player-Mac.../src/internet/vk/vkservice.cpp

1531 lines
45 KiB
C++

/* This file is part of Clementine.
Copyright 2014, Vlad Maltsev <shedwardx@gmail.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, Ivan Leontiev <leont.ivan@gmail.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "vkservice.h"
#include <qmath.h>
#include <QApplication>
#include <QByteArray>
#include <QClipboard>
#include <QDir>
#include <QEventLoop>
#include <QFile>
#include <QMenu>
#include <QMessageBox>
#include <QSettings>
#include <QTimer>
#include <boost/scoped_ptr.hpp>
#include "core/application.h"
#include "core/closure.h"
#include "core/logging.h"
#include "core/mergedproxymodel.h"
#include "core/player.h"
#include "core/timeconstants.h"
#include "core/utilities.h"
#include "ui/iconloader.h"
#include "widgets/didyoumean.h"
#include "globalsearch/globalsearch.h"
#include "internet/core/internetmodel.h"
#include "internet/core/internetplaylistitem.h"
#include "internet/core/searchboxwidget.h"
#include "vreen/audio.h"
#include "vreen/contact.h"
#include "vreen/roster.h"
#include "globalsearch/vksearchprovider.h"
#include "vkmusiccache.h"
#include "vksearchdialog.h"
const char* VkService::kServiceName = "Vk.com";
const char* VkService::kSettingGroup = "Vk.com";
const char* VkService::kUrlScheme = "vk";
const char* VkService::kDefCacheFilename = "%artist - %title";
const int VkService::kMaxVkSongList = 6000;
const int VkService::kMaxVkWallPostList = 100;
const int VkService::kMaxVkSongCount = 300;
const int VkService::kSearchDelayMsec = 400;
QString VkService::DefaultCacheDir() {
return QDir::toNativeSeparators(
Utilities::GetConfigPath(Utilities::Path_CacheRoot) + "/vkcache");
}
uint SearchID::last_id_ = 0;
/***
* Little functions
*/
inline static void RemoveLastRow(QStandardItem* item,
VkService::ItemType type) {
QStandardItem* last_item = item->child(item->rowCount() - 1);
if (last_item->data(InternetModel::Role_Type).toInt() == type) {
item->removeRow(item->rowCount() - 1);
} else {
qLog(Error) << "Tryed to remove row" << last_item->text() << "of type"
<< last_item->data(InternetModel::Role_Type).toInt()
<< "instead of" << type << "from" << item->text();
}
}
struct SongId {
SongId() : audio_id(0), owner_id(0) {}
int audio_id;
int owner_id;
};
static SongId ExtractIds(const QUrl& url) {
SongId res;
QString song_ids = url.path().section('/', 1, 1);
res.owner_id = song_ids.section('_', 0, 0).toInt();
res.audio_id = song_ids.section('_', 1, 1).toInt();
if (res.owner_id && res.audio_id && url.scheme() == "vk" &&
url.host() == "song") {
return res;
} else {
qLog(Error) << "Wromg song url" << url;
return SongId();
}
}
static Song SongFromUrl(const QUrl& url) {
Song result;
if (url.scheme() == "vk" && url.host() == "song") {
QStringList ids = url.path().split('/');
if (ids.size() == 4) {
result.set_artist(ids[2]);
result.set_title(ids[3]);
result.set_valid(true);
} else {
qLog(Error) << "Wrong song url" << url;
result.set_valid(false);
}
result.set_url(url);
} else {
qLog(Error) << "Wrong song url" << url;
result.set_valid(false);
}
return result;
}
MusicOwner::MusicOwner(const QUrl& group_url) {
QStringList tokens = group_url.path().split('/');
if (group_url.scheme() == "vk" && group_url.host() == "group" &&
tokens.size() == 5) {
id_ = -tokens[1].toInt();
songs_count_ = tokens[2].toInt();
screen_name_ = tokens[3];
name_ = tokens[4].replace('_', '/');
} else {
qLog(Error) << "Wrong group url" << group_url;
}
}
Song MusicOwner::toOwnerRadio() const {
Song song;
song.set_title(QObject::tr("%1 (%2 songs)").arg(name_).arg(songs_count_));
song.set_url(QUrl(QString("vk://group/%1/%2/%3/%4")
.arg(-id_)
.arg(songs_count_)
.arg(screen_name_)
.arg(QString(name_).replace('/', '_'))));
song.set_artist(" " + QObject::tr("Community Radio"));
song.set_valid(true);
return song;
}
QDataStream& operator<<(QDataStream& stream, const MusicOwner& val) {
stream << val.id_;
stream << val.name_;
stream << val.songs_count_;
stream << val.screen_name_;
return stream;
}
QDataStream& operator>>(QDataStream& stream, MusicOwner& var) {
stream >> var.id_;
stream >> var.name_;
stream >> var.songs_count_;
stream >> var.screen_name_;
return stream;
}
QDebug operator<<(QDebug d, const MusicOwner& owner) {
d << "MusicOwner(" << owner.id_ << "," << owner.name_ << ","
<< owner.songs_count_ << "," << owner.screen_name_ << ")";
return d;
}
MusicOwnerList MusicOwner::parseMusicOwnerList(const QVariant& request_result) {
auto list = request_result.toList();
MusicOwnerList result;
for (const auto& item : list) {
auto map = item.toMap();
MusicOwner owner;
owner.songs_count_ = map.value("songs_count").toInt();
owner.id_ = map.value("id").toInt();
owner.name_ = map.value("name").toString();
owner.screen_name_ = map.value("screen_name").toString();
owner.photo_ = map.value("photo").toUrl();
result.append(owner);
}
return result;
}
VkService::VkService(Application* app, InternetModel* parent)
: InternetService(kServiceName, app, parent, parent),
root_item_(NULL),
recommendations_item_(NULL),
my_music_item_(NULL),
my_albums_item_(NULL),
search_result_item_(NULL),
context_menu_(NULL),
update_item_(NULL),
find_this_artist_(NULL),
add_to_my_music_(NULL),
remove_from_my_music_(NULL),
add_song_to_cache_(NULL),
copy_share_url_(NULL),
add_to_bookmarks_(NULL),
remove_from_bookmarks_(NULL),
find_owner_(NULL),
search_box_(new SearchBoxWidget(this)),
vk_search_dialog_(new VkSearchDialog(this)),
client_(new Vreen::Client),
connection_(new VkConnection(this)),
url_handler_(new VkUrlHandler(this, this)),
audio_provider_(new Vreen::AudioProvider(client_.get())),
cache_(new VkMusicCache(app_, this)),
last_search_id_(0),
search_delay_(new QTimer(this)) {
QSettings s;
s.beginGroup(kSettingGroup);
/* Init connection */
client_->setTrackMessages(false);
client_->setInvisible(true);
client_->setConnection(connection_.get());
ReloadSettings();
if (HasAccount()) {
Login();
}
connect(client_.get(), SIGNAL(connectionStateChanged(Vreen::Client::State)),
SLOT(ChangeConnectionState(Vreen::Client::State)));
connect(client_.get(), SIGNAL(error(Vreen::Client::Error)),
SLOT(Error(Vreen::Client::Error)));
/* Init interface */
VkSearchProvider* search_provider = new VkSearchProvider(app_, this);
search_provider->Init(this);
app_->global_search()->AddProvider(search_provider);
search_delay_->setInterval(kSearchDelayMsec);
search_delay_->setSingleShot(true);
connect(search_delay_, SIGNAL(timeout()), SLOT(DoLocalSearch()));
connect(search_box_, SIGNAL(TextChanged(QString)), SLOT(FindSongs(QString)));
connect(this, SIGNAL(SongSearchResult(SearchID, SongList)),
SLOT(SearchResultLoaded(SearchID, SongList)));
app_->player()->RegisterUrlHandler(url_handler_);
}
VkService::~VkService() {}
/***
* Interface
*/
QStandardItem* VkService::CreateRootItem() {
root_item_ = new QStandardItem(QIcon(":providers/vk.png"), kServiceName);
root_item_->setData(true, InternetModel::Role_CanLazyLoad);
return root_item_;
}
void VkService::LazyPopulate(QStandardItem* parent) {
switch (parent->data(InternetModel::Role_Type).toInt()) {
case InternetModel::Type_Service:
UpdateRoot();
break;
case Type_Recommendations:
UpdateRecommendations();
break;
case Type_AlbumList:
UpdateAlbumList(parent);
break;
case Type_Music:
UpdateMusic(parent);
break;
case Type_Album:
UpdateAlbumSongs(parent);
break;
case Type_Wall:
UpdateWallSongs(parent);
break;
default:
break;
}
}
void VkService::EnsureMenuCreated() {
if (!context_menu_) {
context_menu_ = new QMenu;
context_menu_->addActions(GetPlaylistActions());
context_menu_->addSeparator();
add_to_bookmarks_ =
context_menu_->addAction(QIcon(":vk/add.png"), tr("Add to bookmarks"),
this, SLOT(AddSelectedToBookmarks()));
remove_from_bookmarks_ = context_menu_->addAction(
QIcon(":vk/remove.png"), tr("Remove from bookmarks"), this,
SLOT(RemoveFromBookmark()));
context_menu_->addSeparator();
find_this_artist_ =
context_menu_->addAction(QIcon(":vk/find.png"), tr("Find this artist"),
this, SLOT(FindThisArtist()));
add_to_my_music_ =
context_menu_->addAction(QIcon(":vk/add.png"), tr("Add to My Music"),
this, SLOT(AddToMyMusic()));
remove_from_my_music_ = context_menu_->addAction(
QIcon(":vk/remove.png"), tr("Remove from My Music"), this,
SLOT(RemoveFromMyMusic()));
add_song_to_cache_ = context_menu_->addAction(QIcon(":vk/download.png"),
tr("Add song to cache"), this,
SLOT(AddSelectedToCache()));
copy_share_url_ = context_menu_->addAction(
QIcon(":vk/link.png"), tr("Copy share url to clipboard"), this,
SLOT(CopyShareUrl()));
find_owner_ = context_menu_->addAction(QIcon(":vk/find.png"),
tr("Add user/group to bookmarks"),
this, SLOT(ShowSearchDialog()));
update_item_ =
context_menu_->addAction(IconLoader::Load("view-refresh"), tr("Update"),
this, SLOT(UpdateItem()));
context_menu_->addSeparator();
context_menu_->addAction(IconLoader::Load("configure"),
tr("Configure Vk.com..."), this,
SLOT(ShowConfig()));
}
}
void VkService::ShowContextMenu(const QPoint& global_pos) {
EnsureMenuCreated();
QModelIndex current(model()->current_index());
QStandardItem* current_item = model()->itemFromIndex(current);
const int item_type = current.data(InternetModel::Role_Type).toInt();
const int parent_type =
current.parent().data(InternetModel::Role_Type).toInt();
const bool is_playable = model()->IsPlayable(current);
const bool is_my_music_item = current_item == my_music_item_ ||
current_item->parent() == my_music_item_;
const bool is_track = item_type == InternetModel::Type_Track;
const bool is_bookmark = item_type == Type_Bookmark;
const bool is_updatable =
item_type != Type_Bookmark && item_type != Type_Loading &&
item_type != Type_More && item_type != Type_Search &&
parent_type != Type_Search;
const bool is_update_enable =
// To prevent call LazyPopulate twice when we try to Update not populated
// item
!current.data(InternetModel::Role_CanLazyLoad).toBool() &&
!current.parent().data(InternetModel::Role_CanLazyLoad).toBool() &&
// disable update action until all of item's children have finished
// updating
!isItemBusy(model()->itemFromIndex(current));
bool is_in_mymusic = false;
bool is_cached = false;
if (is_track) {
selected_song_ =
current.data(InternetModel::Role_SongMetadata).value<Song>();
is_in_mymusic = is_my_music_item ||
ExtractIds(selected_song_.url()).owner_id == UserID();
is_cached = cache_->InCache(selected_song_.url());
}
update_item_->setVisible(is_updatable);
update_item_->setEnabled(is_update_enable);
find_this_artist_->setVisible(is_track);
add_song_to_cache_->setVisible(is_track && !is_cached);
add_to_my_music_->setVisible(is_track && !is_in_mymusic);
remove_from_my_music_->setVisible(is_track && is_in_mymusic);
copy_share_url_->setVisible(is_track);
remove_from_bookmarks_->setVisible(is_bookmark);
add_to_bookmarks_->setVisible(false);
GetAppendToPlaylistAction()->setEnabled(is_playable);
GetReplacePlaylistAction()->setEnabled(is_playable);
GetOpenInNewPlaylistAction()->setEnabled(is_playable);
context_menu_->popup(global_pos);
}
void VkService::ItemDoubleClicked(QStandardItem* item) {
switch (item->data(InternetModel::Role_Type).toInt()) {
case Type_More:
switch (item->parent()->data(InternetModel::Role_Type).toInt()) {
case Type_Recommendations:
MoreRecommendations();
break;
case Type_Search:
FindMore();
break;
case Type_Wall:
MoreWallSongs(item);
break;
default:
qLog(Warning) << "Wrong parent for More item:"
<< item->parent()->text();
}
break;
default:
qLog(Warning) << "Wrong item for double click with type:"
<< item->data(InternetModel::Role_Type);
}
}
QList<QAction*> VkService::playlistitem_actions(const Song& song) {
EnsureMenuCreated();
QList<QAction*> actions;
if (song.url().host() == "song") {
selected_song_ = song;
} else if (song.url().host() == "group") {
add_to_bookmarks_->setVisible(true);
actions << add_to_bookmarks_;
if (song.url() == current_group_url_) {
// Selected now playing group.
selected_song_ = current_song_;
} else {
// If selected not playing group, return only "Add to bookmarks" action.
selected_song_ = song;
return actions;
}
}
// Adding songs actions.
find_this_artist_->setVisible(true);
actions << find_this_artist_;
if (ExtractIds(selected_song_.url()).owner_id != UserID()) {
add_to_my_music_->setVisible(true);
actions << add_to_my_music_;
} else {
remove_from_my_music_->setVisible(true);
actions << remove_from_my_music_;
}
copy_share_url_->setVisible(true);
actions << copy_share_url_;
if (!cache_->InCache(selected_song_.url())) {
add_song_to_cache_->setVisible(true);
actions << add_song_to_cache_;
}
return actions;
}
void VkService::ShowConfig() {
app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Vk);
}
void VkService::UpdateRoot() {
ClearStandardItem(root_item_);
if (HasAccount()) {
CreateAndAppendRow(root_item_, Type_Recommendations);
AppendMusic(root_item_, true);
AppendAlbumList(root_item_, true);
LoadBookmarks();
} else {
ShowConfig();
}
}
QWidget* VkService::HeaderWidget() const {
if (HasAccount()) {
return search_box_;
} else {
return NULL;
}
}
QStandardItem* VkService::CreateAndAppendRow(QStandardItem* parent,
VkService::ItemType type) {
QStandardItem* item = NULL;
switch (type) {
case Type_Loading:
item = new QStandardItem(tr("Loading..."));
break;
case Type_More:
item = new QStandardItem(tr("More"));
item->setData(InternetModel::PlayBehaviour_DoubleClickAction,
InternetModel::Role_PlayBehaviour);
break;
case Type_Recommendations:
item = new QStandardItem(QIcon(":vk/recommends.png"),
tr("My Recommendations"));
item->setData(true, InternetModel::Role_CanLazyLoad);
item->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour);
recommendations_item_ = item;
break;
case Type_Search:
item = new QStandardItem(QIcon(":vk/find.png"), tr("Search"));
item->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour);
search_result_item_ = item;
break;
case Type_Bookmark:
qLog(Error) << "Use AppendBookmark(const MusicOwner &owner)"
<< "for creating Bookmark item instead.";
break;
case Type_Album:
qLog(Error) << "Use AppendAlbum(const Vreen::AudioAlbumItem &album)"
<< "for creating Album item instead.";
break;
default:
qLog(Error) << "Invalid type for creating row: " << type;
break;
}
item->setData(type, InternetModel::Role_Type);
parent->appendRow(item);
return item;
}
/***
* Connection
*/
void VkService::Login() { client_->connectToHost(); }
void VkService::Logout() {
if (connection_) {
client_->disconnectFromHost();
connection_->clear();
}
}
bool VkService::HasAccount() const { return connection_->hasAccount(); }
int VkService::UserID() const { return connection_->uid(); }
void VkService::ChangeConnectionState(Vreen::Client::State state) {
qLog(Debug) << "Connection state changed to" << state;
switch (state) {
case Vreen::Client::StateOnline:
emit LoginSuccess(true);
UpdateRoot();
break;
case Vreen::Client::StateInvalid:
case Vreen::Client::StateOffline:
emit LoginSuccess(false);
UpdateRoot();
break;
case Vreen::Client::StateConnecting:
break;
default:
qLog(Error) << "Wrong connection state " << state;
break;
}
}
void VkService::RequestUserProfile() {
QVariantMap args;
args.insert("users_ids", "0");
Vreen::Reply* reply = client_->request("users.get", args);
connect(reply, SIGNAL(resultReady(QVariant)), this,
SLOT(UserProfileRecived(QVariant)), Qt::UniqueConnection);
}
void VkService::UserProfileRecived(const QVariant& result) {
auto list = result.toList();
if (!list.isEmpty()) {
auto profile = list[0].toMap();
QString name = profile.value("first_name").toString() + " " +
profile.value("last_name").toString();
emit NameUpdated(name);
} else {
qLog(Debug) << "Fetching user profile failed" << result;
}
}
void VkService::Error(Vreen::Client::Error error) {
QString msg;
switch (error) {
case Vreen::Client::ErrorApplicationDisabled:
msg = "Application disabled";
break;
case Vreen::Client::ErrorIncorrectSignature:
msg = "Incorrect signature";
break;
case Vreen::Client::ErrorAuthorizationFailed:
msg = "Authorization failed";
emit LoginSuccess(false);
break;
case Vreen::Client::ErrorToManyRequests:
msg = "Too many requests";
break;
case Vreen::Client::ErrorPermissionDenied:
msg = "Permission denied";
break;
case Vreen::Client::ErrorCaptchaNeeded:
msg = "Captcha needed";
QMessageBox::critical(NULL, tr("Error"),
tr("Captcha is needed.\n"
"Try to login into Vk.com with your browser,"
"to fix this problem."),
QMessageBox::Close);
break;
case Vreen::Client::ErrorMissingOrInvalidParameter:
msg = "Missing or invalid parameter";
break;
case Vreen::Client::ErrorNetworkReply:
msg = "Network reply";
break;
default:
msg = "Unknown error";
break;
}
qLog(Error) << "Client error: " << error << msg;
}
/***
* My Music
*/
void VkService::UpdateMusic(QStandardItem* item) {
if (item) {
MusicOwner owner = item->data(Role_MusicOwnerMetadata).value<MusicOwner>();
LoadAndAppendSongList(item, owner.id());
}
}
/***
* Recommendation
*/
void VkService::UpdateRecommendations() {
ClearStandardItem(recommendations_item_);
CreateAndAppendRow(recommendations_item_, Type_Loading);
auto my_audio =
audio_provider_->getRecommendationsForUser(0, kMaxVkSongCount, 0);
NewClosure(my_audio, SIGNAL(resultReady(QVariant)), this,
SLOT(RecommendationsLoaded(Vreen::AudioItemListReply*)), my_audio);
}
void VkService::MoreRecommendations() {
RemoveLastRow(recommendations_item_, Type_More);
CreateAndAppendRow(recommendations_item_, Type_Loading);
auto my_audio = audio_provider_->getRecommendationsForUser(
0, kMaxVkSongCount, recommendations_item_->rowCount() - 1);
NewClosure(my_audio, SIGNAL(resultReady(QVariant)), this,
SLOT(RecommendationsLoaded(Vreen::AudioItemListReply*)), my_audio);
}
void VkService::RecommendationsLoaded(Vreen::AudioItemListReply* reply) {
SongList songs = FromAudioList(reply->result());
RemoveLastRow(recommendations_item_, Type_Loading);
AppendSongs(recommendations_item_, songs);
if (songs.count() > 0) {
CreateAndAppendRow(recommendations_item_, Type_More);
}
}
/***
* Bookmarks
*/
void VkService::AddSelectedToBookmarks() {
QUrl group_url;
if (selected_song_.url().scheme() == "vk" &&
selected_song_.url().host() == "song") {
// Selected song is song of now playing group, so group url in
// current_group_url_
group_url = current_group_url_;
} else {
// Otherwise selectet group radio in playlist
group_url = selected_song_.url();
}
AppendBookmark(MusicOwner(group_url));
SaveBookmarks();
}
void VkService::RemoveFromBookmark() {
QModelIndex current(model()->current_index());
root_item_->removeRow(current.row());
SaveBookmarks();
}
void VkService::SaveBookmarks() {
QSettings s;
s.beginGroup(kSettingGroup);
s.beginWriteArray("bookmarks");
int index = 0;
for (int i = 0; i < root_item_->rowCount(); ++i) {
auto item = root_item_->child(i);
if (item->data(InternetModel::Role_Type).toInt() == Type_Bookmark) {
MusicOwner owner =
item->data(Role_MusicOwnerMetadata).value<MusicOwner>();
s.setArrayIndex(index);
qLog(Info) << "Save" << index << ":" << owner;
s.setValue("owner", QVariant::fromValue(owner));
++index;
}
}
s.endArray();
}
void VkService::LoadBookmarks() {
QSettings s;
s.beginGroup(kSettingGroup);
int max = s.beginReadArray("bookmarks");
for (int i = 0; i < max; ++i) {
s.setArrayIndex(i);
MusicOwner owner = s.value("owner").value<MusicOwner>();
qLog(Info) << "Load" << i << ":" << owner;
AppendBookmark(owner);
}
s.endArray();
}
QStandardItem* VkService::AppendBookmark(const MusicOwner& owner) {
QIcon icon;
if (owner.id() > 0) {
icon = QIcon(":vk/user.png");
} else {
icon = QIcon(":vk/group.png");
}
QStandardItem* item = new QStandardItem(icon, owner.name());
item->setData(QVariant::fromValue(owner), Role_MusicOwnerMetadata);
item->setData(Type_Bookmark, InternetModel::Role_Type);
AppendWall(item);
AppendMusic(item);
AppendAlbumList(item);
root_item_->appendRow(item);
return item;
}
void VkService::UpdateItem() {
QModelIndex current(model()->current_index());
QStandardItem* item = current.data(InternetModel::Role_Type).toInt() ==
InternetModel::Type_Track
? model()->itemFromIndex(current.parent())
: model()->itemFromIndex(current);
LazyPopulate(item);
}
void VkService::UpdateAlbumList(QStandardItem* item) {
MusicOwner owner = item->data(Role_MusicOwnerMetadata).value<MusicOwner>();
ClearStandardItem(item);
CreateAndAppendRow(item, Type_Loading);
LoadAlbums(item, owner);
}
/***
* Albums
*/
void VkService::LoadAlbums(QStandardItem* parent, const MusicOwner& owner) {
auto albums_request = audio_provider_->getAlbums(owner.id());
NewClosure(
albums_request, SIGNAL(resultReady(QVariant)), this,
SLOT(AlbumListReceived(QStandardItem*, Vreen::AudioAlbumItemListReply*)),
parent, albums_request);
}
QStandardItem* VkService::AppendAlbum(QStandardItem* parent,
const Vreen::AudioAlbumItem& album) {
QStandardItem* item =
new QStandardItem(QIcon(":vk/playlist.png"), album.title());
item->setData(QVariant::fromValue(album), Role_AlbumMetadata);
item->setData(Type_Album, InternetModel::Role_Type);
item->setData(true, InternetModel::Role_CanLazyLoad);
item->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour);
parent->appendRow(item);
return item;
}
QStandardItem* VkService::AppendAlbumList(QStandardItem* parent, bool myself) {
MusicOwner owner;
QStandardItem* item;
if (myself) {
item = new QStandardItem(QIcon(":vk/discography.png"), tr("My Albums"));
// TODO(Ivan Leontiev): Do this better. We have incomplete MusicOwner
// instance for logged in user.
owner.setId(UserID());
my_albums_item_ = item;
} else {
owner = parent->data(Role_MusicOwnerMetadata).value<MusicOwner>();
item = new QStandardItem(QIcon(":vk/discography.png"), tr("Albums"));
}
item->setData(QVariant::fromValue(owner), Role_MusicOwnerMetadata);
item->setData(Type_AlbumList, InternetModel::Role_Type);
item->setData(true, InternetModel::Role_CanLazyLoad);
parent->appendRow(item);
return item;
}
void VkService::AlbumListReceived(QStandardItem* parent,
Vreen::AudioAlbumItemListReply* reply) {
Vreen::AudioAlbumItemList albums = reply->result();
RemoveLastRow(parent, Type_Loading);
for (const auto& album : albums) {
AppendAlbum(parent, album);
}
}
void VkService::UpdateAlbumSongs(QStandardItem* item) {
Vreen::AudioAlbumItem album =
item->data(Role_AlbumMetadata).value<Vreen::AudioAlbumItem>();
LoadAndAppendSongList(item, album.ownerId(), album.id());
}
/***
* Wall
*/
QStandardItem* VkService::AppendWall(QStandardItem* parent) {
QStandardItem* item =
new QStandardItem(QIcon(":vk/playlist.png"), tr("Wall"));
MusicOwner owner = parent->data(Role_MusicOwnerMetadata).value<MusicOwner>();
item->setData(QVariant::fromValue(owner), Role_MusicOwnerMetadata);
item->setData(Type_Wall, InternetModel::Role_Type);
item->setData(true, InternetModel::Role_CanLazyLoad);
item->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour);
parent->appendRow(item);
return item;
}
QStandardItem* VkService::AppendMusic(QStandardItem* parent, bool myself) {
MusicOwner owner;
QStandardItem* item;
if (myself) {
item = new QStandardItem(QIcon(":vk/my_music.png"), tr("My Music"));
// TODO(Ivan Leontiev): Do this better. We have incomplete MusicOwner
// instance for logged in user.
owner.setId(UserID());
my_music_item_ = item;
} else {
item = new QStandardItem(QIcon(":vk/playlist.png"), tr("Music"));
owner = parent->data(Role_MusicOwnerMetadata).value<MusicOwner>();
}
item->setData(QVariant::fromValue(owner), Role_MusicOwnerMetadata);
item->setData(Type_Music, InternetModel::Role_Type);
item->setData(true, InternetModel::Role_CanLazyLoad);
item->setData(InternetModel::PlayBehaviour_MultipleItems,
InternetModel::Role_PlayBehaviour);
parent->appendRow(item);
return item;
}
void VkService::UpdateWallSongs(QStandardItem* item) {
MusicOwner owner = item->data(Role_MusicOwnerMetadata).value<MusicOwner>();
ClearStandardItem(item);
LoadAndAppendWallSongList(item, owner);
}
void VkService::MoreWallSongs(QStandardItem* item) {
QStandardItem* parent = item->parent();
MusicOwner owner = parent->data(Role_MusicOwnerMetadata).value<MusicOwner>();
int offset = item->data(Role_MoreMetadata).value<int>();
RemoveLastRow(parent, Type_More);
LoadAndAppendWallSongList(parent, owner, offset);
}
void VkService::WallPostsLoaded(QStandardItem* item, Vreen::Reply* reply,
int offset) {
auto response = reply->response().toMap();
int count = response.value("count").toInt();
SongList songs = FromAudioList(handleWallPosts(response.value("items")));
RemoveLastRow(item, Type_Loading);
AppendSongs(item, songs);
if (count > offset) {
auto m = CreateAndAppendRow(item, Type_More);
m->setData(offset, Role_MoreMetadata);
}
}
void VkService::LoadAndAppendWallSongList(QStandardItem* item,
const MusicOwner& owner, int offset) {
if (item) {
CreateAndAppendRow(item, Type_Loading);
QVariantMap args;
QString vk_script =
"var a = API.wall.get({"
" \"owner_id\": Args.q,"
" \"count\": Args.count,"
" \"offset\": Args.offset"
"});"
"return {\"count\": a.count, \"items\": a.items@.attachments};";
args.insert("v", "5.25");
args.insert("q", owner.id());
args.insert("offset", offset);
args.insert("count", kMaxVkWallPostList);
args.insert("code", vk_script);
auto reply = client_->request("execute", args);
NewClosure(reply, SIGNAL(resultReady(QVariant)), this,
SLOT(WallPostsLoaded(QStandardItem*, Vreen::Reply*, int)), item,
reply, offset + kMaxVkWallPostList);
}
}
/***
* Features
*/
void VkService::FindThisArtist() {
search_box_->SetText(selected_song_.artist());
}
void VkService::AddToMyMusic() {
SongId id = ExtractIds(selected_song_.url());
auto reply = audio_provider_->addToLibrary(id.audio_id, id.owner_id);
NewClosure(reply, SIGNAL(resultReady(QVariant)), this,
SLOT(UpdateMusic(QStandardItem*)), my_music_item_);
}
void VkService::AddToMyMusicCurrent() {
if (isLoveAddToMyMusic() && current_song_.is_valid()) {
selected_song_ = current_song_;
AddToMyMusic();
}
}
void VkService::RemoveFromMyMusic() {
SongId id = ExtractIds(selected_song_.url());
if (id.owner_id == UserID()) {
auto reply = audio_provider_->removeFromLibrary(id.audio_id, id.owner_id);
NewClosure(reply, SIGNAL(resultReady(QVariant)), this,
SLOT(UpdateMusic(QStandardItem*)), my_music_item_);
} else {
qLog(Error) << "Tried to delete song that not owned by user (" << UserID()
<< selected_song_.url();
}
}
void VkService::AddSelectedToCache() {
QUrl selected_song_media_url =
GetAudioItemFromUrl(selected_song_.url()).url();
cache_->AddToCache(selected_song_.url(), selected_song_media_url, true);
}
void VkService::CopyShareUrl() {
QByteArray share_url("http://vk.com/audio?q=");
share_url += QUrl::toPercentEncoding(
QString(selected_song_.artist() + " " + selected_song_.title()));
QApplication::clipboard()->setText(share_url);
}
/***
* Search
*/
void VkService::DoLocalSearch() {
ClearStandardItem(search_result_item_);
CreateAndAppendRow(search_result_item_, Type_Loading);
SearchID id(SearchID::LocalSearch);
last_search_id_ = id.id();
SongSearch(id, last_query_);
}
void VkService::FindSongs(const QString& query) {
last_query_ = query;
if (query.isEmpty()) {
search_delay_->stop();
root_item_->removeRow(search_result_item_->row());
search_result_item_ = NULL;
last_search_id_ = 0;
return;
}
search_delay_->start();
if (!search_result_item_) {
CreateAndAppendRow(root_item_, Type_Search);
}
}
void VkService::FindMore() {
RemoveLastRow(search_result_item_, Type_More);
CreateAndAppendRow(search_result_item_, Type_Loading);
SearchID id(SearchID::MoreLocalSearch);
last_search_id_ = id.id();
SongSearch(id, last_query_, kMaxVkSongCount,
search_result_item_->rowCount() - 1);
}
void VkService::SearchResultLoaded(const SearchID& id, const SongList& songs) {
if (!search_result_item_) {
return; // Result received when search is already over.
}
if (id.id() == last_search_id_) {
if (id.type() == SearchID::LocalSearch) {
ClearStandardItem(search_result_item_);
} else if (id.type() == SearchID::MoreLocalSearch) {
RemoveLastRow(search_result_item_, Type_Loading);
} else {
return; // Others request types ignored.
}
if (!songs.isEmpty()) {
AppendSongs(search_result_item_, songs);
CreateAndAppendRow(search_result_item_, Type_More);
}
// If new search, scroll to search results.
if (id.type() == SearchID::LocalSearch) {
QModelIndex index =
model()->merged_model()->mapFromSource(search_result_item_->index());
ScrollToIndex(index);
}
}
}
/***
* Load song list methods
*/
void VkService::LoadAndAppendSongList(QStandardItem* item, int uid,
int album_id) {
if (item) {
ClearStandardItem(item);
CreateAndAppendRow(item, Type_Loading);
auto audioreq =
audio_provider_->getContactAudio(uid, kMaxVkSongList, 0, album_id);
NewClosure(
audioreq, SIGNAL(resultReady(QVariant)), this,
SLOT(AppendLoadedSongs(QStandardItem*, Vreen::AudioItemListReply*)),
item, audioreq);
}
}
void VkService::AppendLoadedSongs(QStandardItem* item,
Vreen::AudioItemListReply* reply) {
SongList songs = FromAudioList(reply->result());
if (item) {
ClearStandardItem(item);
if (songs.count() > 0) {
AppendSongs(item, songs);
return;
}
} else {
qLog(Warning) << "Item for request not exist";
}
item->appendRow(new QStandardItem(
tr("Connection trouble "
"or audio is disabled by owner")));
}
static QString SanitiseCharacters(QString str) {
// Remove all leading and trailing unicode symbols
// that some users love to add to title and artist.
str = str.remove(QRegExp("^[^\\w]*"));
str = str.remove(QRegExp("[^])\\w]*$"));
return str;
}
Song VkService::FromAudioItem(const Vreen::AudioItem& item) {
Song song;
song.set_title(SanitiseCharacters(item.title()));
song.set_artist(SanitiseCharacters(item.artist()));
song.set_length_nanosec(qFloor(item.duration()) * kNsecPerSec);
QString url = QString("vk://song/%1_%2/%3/%4")
.arg(item.ownerId())
.arg(item.id())
.arg(item.artist().replace('/', '_'))
.arg(item.title().replace('/', '_'));
song.set_url(QUrl(url));
song.set_valid(true);
return song;
}
SongList VkService::FromAudioList(const Vreen::AudioItemList& list) {
SongList song_list;
for (const Vreen::AudioItem& item : list) {
song_list.append(FromAudioItem(item));
}
return song_list;
}
/***
* Url handling
*/
Vreen::AudioItem VkService::GetAudioItemFromUrl(const QUrl& url) {
QStringList tokens = url.path().split('/');
if (tokens.count() < 2) {
qLog(Error) << "Wrong song url" << url;
return Vreen::AudioItem();
}
QString song_id = tokens[1];
if (HasAccount()) {
Vreen::AudioItemListReply* song_request =
audio_provider_->getAudiosByIds(song_id);
emit StopWaiting(); // Stop all previous requests.
bool success = WaitForReply(song_request);
if (success && !song_request->result().isEmpty()) {
return song_request->result()[0];
}
}
qLog(Info) << "Unresolved url by id" << song_id;
return Vreen::AudioItem();
}
UrlHandler::LoadResult VkService::GetSongResult(const QUrl& url) {
// Try get from cache
QUrl media_url = cache_->Get(url);
if (media_url.isValid()) {
SongStarting(url);
return UrlHandler::LoadResult(url, UrlHandler::LoadResult::TrackAvailable,
media_url);
}
// Otherwise get fresh link
auto audio_item = GetAudioItemFromUrl(url);
media_url = audio_item.url();
if (media_url.isValid()) {
Song song = FromAudioItem(audio_item);
SongStarting(song);
if (cachingEnabled_) {
cache_->AddToCache(url, media_url);
}
return UrlHandler::LoadResult(url, UrlHandler::LoadResult::TrackAvailable,
media_url, song.length_nanosec());
}
return UrlHandler::LoadResult(url);
}
UrlHandler::LoadResult VkService::GetGroupNextSongUrl(const QUrl& url) {
QStringList tokens = url.path().split('/');
if (tokens.count() < 3) {
qLog(Error) << "Wrong url" << url;
return UrlHandler::LoadResult(url);
}
int gid = tokens[1].toInt();
int songs_count = tokens[2].toInt();
if (songs_count > kMaxVkSongList) {
songs_count = kMaxVkSongList;
}
if (HasAccount()) {
// Getting one random song from groups playlist.
Vreen::AudioItemListReply* song_request =
audio_provider_->getContactAudio(-gid, 1, qrand() % songs_count);
emit StopWaiting(); // Stop all previous requests.
bool success = WaitForReply(song_request);
if (success && !song_request->result().isEmpty()) {
Vreen::AudioItem song = song_request->result()[0];
current_group_url_ = url;
SongStarting(FromAudioItem(song));
emit StreamMetadataFound(url, current_song_);
return UrlHandler::LoadResult(url, UrlHandler::LoadResult::TrackAvailable,
song.url(), current_song_.length_nanosec());
}
}
qLog(Info) << "Unresolved group url" << url;
return UrlHandler::LoadResult(url);
}
/***
* Song playing
*/
void VkService::SongStarting(const QUrl& url) {
SongStarting(SongFromUrl(url));
}
void VkService::SongStarting(const Song& song) {
current_song_ = song;
if (isBroadcasting() && HasAccount()) {
auto id = ExtractIds(song.url());
auto reply =
audio_provider_->setBroadcast(id.audio_id, id.owner_id, IdList());
NewClosure(reply, SIGNAL(resultReady(QVariant)), this,
SLOT(BroadcastChangeReceived(Vreen::IntReply*)), reply);
connect(app_->player(), SIGNAL(Stopped()), this, SLOT(SongStopped()),
Qt::UniqueConnection);
qLog(Debug) << "Broadcasting" << song.artist() << "-" << song.title();
}
}
void VkService::SongSkipped() {
current_song_.set_valid(false);
cache_->BreakCurrentCaching();
}
void VkService::SongStopped() {
current_song_.set_valid(false);
if (isBroadcasting() && HasAccount()) {
auto reply = audio_provider_->resetBroadcast(IdList());
NewClosure(reply, SIGNAL(resultReady(QVariant)), this,
SLOT(BroadcastChangeReceived(Vreen::IntReply*)), reply);
disconnect(app_->player(), SIGNAL(Stopped()), this, SLOT(SongStopped()));
qLog(Debug) << "End of broadcasting";
}
}
void VkService::BroadcastChangeReceived(Vreen::IntReply* reply) {
qLog(Debug) << "Broadcast changed for " << reply->result();
}
/***
* Search
*/
void VkService::SongSearch(SearchID id, const QString& query, int count,
int offset) {
auto reply = audio_provider_->searchAudio(
query, count, offset, false, Vreen::AudioProvider::SortByPopularity);
NewClosure(reply, SIGNAL(resultReady(QVariant)), this,
SLOT(SongSearchReceived(SearchID, Vreen::AudioItemListReply*)), id,
reply);
}
void VkService::SongSearchReceived(const SearchID& id,
Vreen::AudioItemListReply* reply) {
SongList songs = FromAudioList(reply->result());
emit SongSearchResult(id, songs);
}
void VkService::GroupSearch(SearchID id, const QString& query) {
QVariantMap args;
args.insert("q", query);
// This is using of 'execute' method that execute
// this VKScript method on vk server:
/*
var groups = API.groups.search({"q": Args.q});
if (groups.length == 0) {
return [];
}
var i = 1;
var res = [];
while (i < groups.length - 1) {
i = i + 1;
var grp = groups[i];
var songs = API.audio.getCount({oid: -grp.gid});
if ( songs > 1 &&
(grp.is_closed == 0 || grp.is_member == 1))
{
res = res + [{"songs_count" : songs,
"id" : -grp.gid,
"name" : grp.name,
"screen_name" : grp.screen_name,
"photo": grp.photo}];
}
}
return res;
*/
//
// I leave it here in case if my vk app disappear or smth.
auto reply = client_->request("execute.searchMusicGroup", args);
NewClosure(reply, SIGNAL(resultReady(QVariant)), this,
SLOT(GroupSearchReceived(SearchID, Vreen::Reply*)), id, reply);
}
void VkService::GroupSearchReceived(const SearchID& id, Vreen::Reply* reply) {
QVariant groups = reply->response();
emit GroupSearchResult(id, MusicOwner::parseMusicOwnerList(groups));
}
/***
* Vk search user or group.
*/
void VkService::ShowSearchDialog() {
if (vk_search_dialog_->exec() == QDialog::Accepted) {
AppendBookmark(vk_search_dialog_->found());
SaveBookmarks();
}
}
void VkService::FindUserOrGroup(const QString& q) {
QVariantMap args;
args.insert("q", q);
// This is using of 'execute' method that execute
// this VKScript method on vk server:
/*
var q = Args.q;
if (q + "" == ""){
return [];
}
var results_count = 0;
var res = [];
// Search groups
var groups = API.groups.search({"q": q});
var i = 0;
while (i < groups.length - 1 && results_count <= 5) {
i = i + 1;
var grp = groups[i];
var songs = API.audio.getCount({oid: -grp.gid});
// Add only accessible groups with songs
if ( songs > 1 &&
(grp.is_closed == 0 || grp.is_member == 1))
{
results_count = results_count + 1;
res = res + [{"songs_count" : songs,
"id" : -grp.gid,
"name" : grp.name,
"screen_name" : grp.screen_name,
"photo": grp.photo}];
}
}
// Search peoples
var peoples = API.users.search({"q": q,
"count":10,
"fields":"screen_name,photo"});
var i = 0;
while (i < peoples.length - 1 && results_count <= 7) {
i = i + 1;
var user = peoples[i];
var songs = API.audio.getCount({"oid": user.uid});
// Add groups only with songs
if (songs > 1) {
results_count = results_count + 1;
res = res + [{"songs_count" : songs,
"id" : user.uid,
"name" : user.first_name + " " + user.last_name,
"screen_name" : user.screen_name,
"phone" : user.photo}];
}
}
return res;
*/
// I leave it here just in case if my vk app will disappear or smth.
auto reply = client_->request("execute.searchMusicOwner", args);
NewClosure(reply, SIGNAL(resultReady(QVariant)), this,
SLOT(UserOrGroupReceived(SearchID, Vreen::Reply*)),
SearchID(SearchID::UserOrGroup), reply);
}
void VkService::UserOrGroupReceived(const SearchID& id, Vreen::Reply* reply) {
QVariant owners = reply->response();
emit UserOrGroupSearchResult(id, MusicOwner::parseMusicOwnerList(owners));
}
/***
* Utils
*/
int VkService::TypeOfItem(const QStandardItem* item) {
return item->data(InternetModel::Role_Type).toInt();
}
bool VkService::isItemBusy(const QStandardItem* item) {
const QStandardItem* cur_item =
TypeOfItem(item) == InternetModel::Type_Track ? item->parent() : item;
int r_count = cur_item->rowCount();
bool flag = false;
if (r_count) {
if (TypeOfItem(cur_item->child(r_count - 1)) == Type_Loading) return true;
int t = TypeOfItem(cur_item);
if (cur_item == root_item_ || t == Type_Bookmark || t == Type_AlbumList) {
for (int i = 0; i < r_count; i++) {
flag |= isItemBusy(cur_item->child(i));
}
}
}
return flag;
}
void VkService::AppendSongs(QStandardItem* parent, const SongList& songs) {
for (const auto& song : songs) {
parent->appendRow(CreateSongItem(song));
}
}
void VkService::ReloadSettings() {
QSettings s;
s.beginGroup(kSettingGroup);
maxGlobalSearch_ = s.value("max_global_search", kMaxVkSongCount).toInt();
cachingEnabled_ = s.value("cache_enabled", false).toBool();
cacheDir_ = s.value("cache_dir", DefaultCacheDir()).toString();
cacheFilename_ = s.value("cache_filename", kDefCacheFilename).toString();
love_is_add_to_mymusic_ = s.value("love_is_add_to_my_music", false).toBool();
groups_in_global_search_ = s.value("groups_in_global_search", false).toBool();
if (!s.contains("enable_broadcast")) {
// Need to update premissions
Logout();
}
enable_broadcast_ = s.value("enable_broadcast", false).toBool();
}
void VkService::ClearStandardItem(QStandardItem* item) {
if (item && item->hasChildren()) {
item->removeRows(0, item->rowCount());
}
}
bool VkService::WaitForReply(Vreen::Reply* reply) {
QEventLoop event_loop;
QTimer timeout_timer;
connect(this, SIGNAL(StopWaiting()), &timeout_timer, SLOT(stop()));
connect(&timeout_timer, SIGNAL(timeout()), &event_loop, SLOT(quit()));
connect(reply, SIGNAL(resultReady(QVariant)), &event_loop, SLOT(quit()));
timeout_timer.start(10000);
event_loop.exec();
if (!timeout_timer.isActive()) {
qLog(Error) << "Vk.com request timeout";
return false;
}
timeout_timer.stop();
return true;
}
Vreen::AudioItemList VkService::handleWallPosts(const QVariant& response) {
Vreen::AudioItemList items;
auto list = response.toList();
for (const auto& i : list) {
auto attachments = i.toList();
for (const auto& j : attachments) {
auto item = j.toMap();
if (item.value("type") == "audio") {
auto map = item.value("audio").toMap();
Vreen::AudioItem audio;
audio.setId(map.value("id").toInt());
audio.setOwnerId(map.value("owner_id").toInt());
audio.setArtist(map.value("artist").toString());
audio.setTitle(map.value("title").toString());
audio.setDuration(map.value("duration").toReal());
audio.setAlbumId(map.value("album").toInt());
audio.setLyricsId(map.value("lyrics_id").toInt());
audio.setUrl(map.value("url").toUrl());
items.append(audio);
}
}
}
return items;
}