Merge pull request #1 from clementine-player/master

upstream update
This commit is contained in:
Max 2014-09-04 06:27:46 +06:00
commit f4e0e03236
99 changed files with 16677 additions and 13929 deletions

View File

@ -210,6 +210,29 @@ IntReply *AudioProvider::removeFromLibrary(int aid, int oid)
return reply;
}
IdListReply *AudioProvider::setBroadcast(int aid, int oid, const IdList &targetIds)
{
Q_D(AudioProvider);
QVariantMap args;
args.insert("audio", QString("%1_%2").arg(oid).arg(aid));
args.insert("target_ids", join(targetIds));
auto reply = d->client->request<IdListReply>("audio.setBroadcast", args, ReplyPrivate::handleIdList);
return reply;
}
IdListReply *AudioProvider::resetBroadcast(const IdList &targetIds)
{
Q_D(AudioProvider);
QVariantMap args;
args.insert("audio","");
args.insert("target_ids", join(targetIds));
auto reply = d->client->request<IdListReply>("audio.setBroadcast", args, ReplyPrivate::handleIdList);
return reply;
}
AudioItemListReply *AudioProvider::getAudiosByIds(const QString &ids)
{
Q_D(AudioProvider);

View File

@ -26,6 +26,7 @@
#define VK_AUDIO_H
#include <QAbstractListModel>
#include "vk_global.h"
#include "audioitem.h"
#include "abstractlistmodel.h"
#include "reply.h"
@ -35,6 +36,7 @@ namespace Vreen {
class Client;
typedef ReplyBase<AudioItemList> AudioItemListReply;
typedef ReplyBase<AudioAlbumItemList> AudioAlbumItemListReply;
typedef ReplyBase<QList<int>> IdListReply;
class AudioProviderPrivate;
class VK_SHARED_EXPORT AudioProvider : public QObject
@ -60,6 +62,8 @@ public:
IntReply *getCount(int oid = 0);
IntReply *addToLibrary(int aid, int oid, int gid = 0);
IntReply *removeFromLibrary(int aid, int oid);
IdListReply *setBroadcast(int aid, int oid, const IdList& targetIds);
IdListReply *resetBroadcast(const IdList& targetIds);
protected:
QScopedPointer<AudioProviderPrivate> d_ptr;
};

View File

@ -122,6 +122,16 @@ void ReplyPrivate::_q_network_reply_error(QNetworkReply::NetworkError code)
emit q->resultReady(response);
}
QVariant ReplyPrivate::handleIdList(const QVariant &response)
{
IdList ids;
auto list = response.toList();
foreach (auto item, list) {
ids.append(item.toInt());
}
return QVariant::fromValue(ids);
}
QVariant MessageListHandler::operator()(const QVariant &response)
{

View File

@ -50,15 +50,16 @@ public:
void _q_reply_finished();
void _q_network_reply_error(QNetworkReply::NetworkError);
static QVariant handleInt(const QVariant &response) { return response.toInt(); }
static QVariant handleInt(const QVariant &response) { return response.toInt(); }
static QVariant handleIdList(const QVariant& response);
};
struct MessageListHandler {
MessageListHandler(int clientId) : clientId(clientId) {}
QVariant operator()(const QVariant &response);
MessageListHandler(int clientId) : clientId(clientId) {}
QVariant operator()(const QVariant &response);
int clientId;
int clientId;
};
} //namespace Vreen

View File

@ -30,32 +30,31 @@
static const QUrl kVkOAuthEndpoint("https://oauth.vk.com/authorize");
static const QUrl kVkOAuthTokenEndpoint("https://oauth.vk.com/access_token");
static const QUrl kApiUrl("https://api.vk.com/method/");
static const char *kScopeNames[] = { "notify", "friends", "photos", "audio",
"video", "docs", "notes", "pages", "status", "offers", "questions", "wall",
"groups", "messages", "notifications", "stats", "ads", "offline" };
static const char* kScopeNames[] = {
"notify", "friends", "photos", "audio", "video", "docs",
"notes", "pages", "status", "offers", "questions", "wall",
"groups", "messages", "notifications", "stats", "ads", "offline"};
static const QString kAppID = "3421812";
static const QString kAppSecret = "cY7KMyX46Fq3nscZlbdo";
static const VkConnection::Scopes kScopes =
VkConnection::Offline |
VkConnection::Audio |
VkConnection::Friends |
VkConnection::Groups;
VkConnection::Offline | VkConnection::Audio | VkConnection::Friends |
VkConnection::Groups | VkConnection::Status;
static const char* kSettingsGroup = "Vk.com/oauth";
VkConnection::VkConnection(QObject* parent)
: Connection(parent),
state_(Vreen::Client::StateOffline),
expires_in_(0),
uid_(0) {
: Connection(parent),
state_(Vreen::Client::StateOffline),
expires_in_(0),
uid_(0) {
loadToken();
}
VkConnection::~VkConnection() {
}
VkConnection::~VkConnection() {}
void VkConnection::connectToHost(const QString& login, const QString& password) {
void VkConnection::connectToHost(const QString& login,
const QString& password) {
Q_UNUSED(login)
Q_UNUSED(password)
if (hasAccount()) {
@ -84,16 +83,17 @@ void VkConnection::clear() {
}
bool VkConnection::hasAccount() {
return !access_token_.isNull()
&& (expires_in_ > static_cast<time_t>(QDateTime::currentDateTime().toTime_t()));
return !access_token_.isNull() &&
(expires_in_ >
static_cast<time_t>(QDateTime::currentDateTime().toTime_t()));
}
QNetworkRequest VkConnection::makeRequest(const QString& method, const QVariantMap& args) {
QNetworkRequest VkConnection::makeRequest(const QString& method,
const QVariantMap& args) {
QUrl url = kApiUrl;
url.setPath(url.path() % QLatin1Literal("/") % method);
for (auto it = args.constBegin(); it != args.constEnd(); ++it) {
url.addQueryItem(it.key(),
it.value().toString());
url.addQueryItem(it.key(), it.value().toString());
}
url.addEncodedQueryItem("access_token", access_token_);
return QNetworkRequest(url);
@ -118,9 +118,9 @@ void VkConnection::requestAccessToken() {
qLog(Debug) << "Try to login to Vk.com" << url;
NewClosure(server, SIGNAL(Finished()),
this, SLOT(codeRecived(LocalRedirectServer*, QUrl)),
server, server->url());
NewClosure(server, SIGNAL(Finished()), this,
SLOT(codeRecived(LocalRedirectServer*, QUrl)), server,
server->url());
QDesktopServices::openUrl(url);
}

View File

@ -25,44 +25,40 @@
#include "core/taskmanager.h"
VkMusicCache::VkMusicCache(Application* app, VkService* service)
: QObject(service),
app_(app),
service_(service),
current_cashing_index(0),
is_downloading(false),
is_aborted(false),
task_id(0),
file_(NULL),
network_manager_(new QNetworkAccessManager),
reply_(NULL) {
}
: QObject(service),
app_(app),
service_(service),
current_song_index(0),
is_downloading(false),
is_aborted(false),
task_id(0),
file_(NULL),
network_manager_(new QNetworkAccessManager),
reply_(NULL) {}
QUrl VkMusicCache::Get(const QUrl& url) {
QString cached_filename = CachedFilename(url);
QUrl result;
if (InCache(cached_filename)) {
if (InCache(url)) {
QString cached_filename = CachedFilename(url);
qLog(Info) << "Use cashed file" << cached_filename;
result = QUrl::fromLocalFile(cached_filename);
} else {
result = service_->GetSongPlayUrl(url, false);
if (service_->isCachingEnabled()) {
AddToQueue(cached_filename, result);
current_cashing_index = queue_.size();
}
}
return result;
}
void VkMusicCache::ForceCache(const QUrl& url) {
AddToQueue(CachedFilename(url), service_->GetSongPlayUrl(url));
void VkMusicCache::AddToCache(const QUrl& url, const QUrl& media_url,
bool force) {
AddToQueue(CachedFilename(url), media_url);
if (!force) {
current_song_index = queue_.size();
}
}
void VkMusicCache::BreakCurrentCaching() {
if (current_cashing_index > 0) {
if (current_song_index > 0) {
// Current song in queue
queue_.removeAt(current_cashing_index - 1);
} else if (current_cashing_index == 0) {
queue_.removeAt(current_song_index - 1);
} else if (current_song_index == 0) {
// Current song is downloading
if (reply_) {
reply_->abort();
@ -75,7 +71,8 @@ void VkMusicCache::BreakCurrentCaching() {
* Queue operations
*/
void VkMusicCache::AddToQueue(const QString& filename, const QUrl& download_url) {
void VkMusicCache::AddToQueue(const QString& filename,
const QUrl& download_url) {
DownloadItem item;
item.filename = filename;
item.url = download_url;
@ -93,11 +90,12 @@ void VkMusicCache::DownloadNext() {
} else {
current_download = queue_.first();
queue_.pop_front();
current_cashing_index--;
current_song_index--;
// Check file path and file existance first
if (QFile::exists(current_download.filename)) {
qLog(Warning) << "Tried to overwrite already cached file" << current_download.filename;
qLog(Warning) << "Tried to overwrite already cached file"
<< current_download.filename;
return;
}
@ -117,14 +115,15 @@ void VkMusicCache::DownloadNext() {
// Start downloading
is_aborted = false;
is_downloading = true;
task_id = app_->task_manager()->
StartTask(tr("Caching %1")
.arg(QFileInfo(current_download.filename).baseName()));
task_id = app_->task_manager()->StartTask(
tr("Caching %1").arg(QFileInfo(current_download.filename).baseName()));
reply_ = network_manager_->get(QNetworkRequest(current_download.url));
connect(reply_, SIGNAL(finished()), SLOT(Downloaded()));
connect(reply_, SIGNAL(readyRead()), SLOT(DownloadReadyToRead()));
connect(reply_, SIGNAL(downloadProgress(qint64, qint64)), SLOT(DownloadProgress(qint64, qint64)));
qLog(Info)<< "Start cashing" << current_download.filename << "from" << current_download.url;
connect(reply_, SIGNAL(downloadProgress(qint64, qint64)),
SLOT(DownloadProgress(qint64, qint64)));
qLog(Info) << "Start cashing" << current_download.filename << "from"
<< current_download.url;
}
}
@ -154,13 +153,13 @@ void VkMusicCache::Downloaded() {
QString path = service_->cacheDir();
if (file_->size() > 0) {
if (file_->size() > 0) {
QDir(path).mkpath(QFileInfo(current_download.filename).path());
if (file_->copy(current_download.filename)) {
qLog(Info) << "Cached" << current_download.filename;
} else {
qLog(Error) << "Unable to save" << current_download.filename
<< ":" << file_->errorString();
qLog(Error) << "Unable to save" << current_download.filename << ":"
<< file_->errorString();
}
} else {
qLog(Error) << "File" << current_download.filename << "is empty";
@ -181,12 +180,8 @@ void VkMusicCache::Downloaded() {
* Utils
*/
bool VkMusicCache::InCache(const QString& filename) {
return QFile::exists(filename);
}
bool VkMusicCache::InCache(const QUrl& url) {
return InCache(CachedFilename(url));
return QFile::exists(CachedFilename(url));
}
QString VkMusicCache::CachedFilename(const QUrl& url) {
@ -198,7 +193,8 @@ QString VkMusicCache::CachedFilename(const QUrl& url) {
cache_filename.replace("%artist", args[2]);
cache_filename.replace("%title", args[3]);
} else {
qLog(Warning) << "Song url with args" << args << "does not contain artist and title"
qLog(Warning) << "Song url with args" << args
<< "does not contain artist and title"
<< "use id as file name for cache.";
cache_filename = args[1];
}
@ -209,5 +205,5 @@ QString VkMusicCache::CachedFilename(const QUrl& url) {
return "";
}
// TODO(Vk): Maybe use extenstion from link? Seems it's always mp3.
return cache_dir+QDir::separator()+cache_filename+".mp3";
return cache_dir + QDir::separator() + cache_filename + ".mp3";
}

View File

@ -30,30 +30,29 @@ class Application;
class VkMusicCache : public QObject {
Q_OBJECT
public:
public:
explicit VkMusicCache(Application* app, VkService* service);
~VkMusicCache() {}
// Return file path if file in cache otherwise
// return internet url and add song to caching queue
QUrl Get(const QUrl& url);
void ForceCache(const QUrl& url);
void AddToCache(const QUrl& url, const QUrl& media_url, bool force = false);
void BreakCurrentCaching();
bool InCache(const QUrl& url);
private slots:
bool InCache(const QString& filename);
private slots:
void AddToQueue(const QString& filename, const QUrl& download_url);
void DownloadNext();
void DownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
void DownloadReadyToRead();
void Downloaded();
private:
private:
struct DownloadItem {
QString filename;
QUrl url;
bool operator ==(const DownloadItem& rhv) {
bool operator==(const DownloadItem& rhv) {
return filename == rhv.filename;
}
};
@ -63,9 +62,10 @@ private:
Application* app_;
VkService* service_;
QList<DownloadItem> queue_;
// Contain index of current song in queue, need for removing if song was skipped.
// Is zero if song downloading now, and less that zero if current song not caching or cached.
int current_cashing_index;
// Contain index of current song in queue, need for removing if song was
// skipped. It's zero if song downloading now, and less that zero
// if current song not caching or cached.
int current_song_index;
DownloadItem current_download;
bool is_downloading;
bool is_aborted;

View File

@ -312,7 +312,7 @@ void VkService::EnsureMenuCreated() {
add_song_to_cache_ = context_menu_->addAction(QIcon(":vk/download.png"),
tr("Add song to cache"), this,
SLOT(AddToCache()));
SLOT(AddSelectedToCache()));
copy_share_url_ = context_menu_->addAction(
QIcon(":vk/link.png"), tr("Copy share url to clipboard"), this,
@ -367,7 +367,7 @@ void VkService::ShowContextMenu(const QPoint& global_pos) {
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());
is_cached = cache_->InCache(selected_song_.url());
}
update_item_->setVisible(is_updatable);
@ -443,7 +443,7 @@ QList<QAction*> VkService::playlistitem_actions(const Song& song) {
copy_share_url_->setVisible(true);
actions << copy_share_url_;
if (!cache()->InCache(selected_song_.url())) {
if (!cache_->InCache(selected_song_.url())) {
add_song_to_cache_->setVisible(true);
actions << add_song_to_cache_;
}
@ -835,7 +835,7 @@ void VkService::AddToMyMusic() {
}
void VkService::AddToMyMusicCurrent() {
if (isLoveAddToMyMusic()) {
if (isLoveAddToMyMusic() && current_song_.is_valid()) {
selected_song_ = current_song_;
AddToMyMusic();
}
@ -852,8 +852,10 @@ void VkService::RemoveFromMyMusic() {
}
}
void VkService::AddToCache() {
url_handler_->ForceAddToCache(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() {
@ -999,12 +1001,12 @@ SongList VkService::FromAudioList(const Vreen::AudioItemList& list) {
* Url handling
*/
QUrl VkService::GetSongPlayUrl(const QUrl& url, bool is_playing) {
Vreen::AudioItem VkService::GetAudioItemFromUrl(const QUrl& url) {
QStringList tokens = url.path().split('/');
if (tokens.count() < 2) {
qLog(Error) << "Wrong song url" << url;
return QUrl();
return Vreen::AudioItem();
}
QString song_id = tokens[1];
@ -1016,17 +1018,35 @@ QUrl VkService::GetSongPlayUrl(const QUrl& url, bool is_playing) {
bool success = WaitForReply(song_request);
if (success && !song_request->result().isEmpty()) {
Vreen::AudioItem song = song_request->result()[0];
if (is_playing) {
current_song_ = FromAudioItem(song);
current_song_.set_url(url);
}
return song.url();
return song_request->result()[0];
}
}
qLog(Info) << "Unresolved url by id" << song_id;
return QUrl();
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);
cache_->AddToCache(url, media_url);
return UrlHandler::LoadResult(url, UrlHandler::LoadResult::TrackAvailable,
media_url, song.length_nanosec());
}
return UrlHandler::LoadResult();
}
UrlHandler::LoadResult VkService::GetGroupNextSongUrl(const QUrl& url) {
@ -1054,7 +1074,7 @@ UrlHandler::LoadResult VkService::GetGroupNextSongUrl(const QUrl& url) {
if (success && !song_request->result().isEmpty()) {
Vreen::AudioItem song = song_request->result()[0];
current_group_url_ = url;
current_song_ = FromAudioItem(song);
SongStarting(FromAudioItem(song));
emit StreamMetadataFound(url, current_song_);
return UrlHandler::LoadResult(url, UrlHandler::LoadResult::TrackAvailable,
song.url(), current_song_.length_nanosec());
@ -1065,8 +1085,48 @@ UrlHandler::LoadResult VkService::GetGroupNextSongUrl(const QUrl& url) {
return UrlHandler::LoadResult();
}
void VkService::SetCurrentSongFromUrl(const QUrl& url) {
current_song_ = SongFromUrl(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();
}
/***
@ -1234,6 +1294,12 @@ void VkService::ReloadSettings() {
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) {

View File

@ -44,11 +44,8 @@ class VkSearchDialog;
* using in bookmarks.
*/
class MusicOwner {
public:
MusicOwner() :
songs_count_(0),
id_(0)
{}
public:
MusicOwner() : songs_count_(0), id_(0) {}
explicit MusicOwner(const QUrl& group_url);
Song toOwnerRadio() const;
@ -58,7 +55,7 @@ public:
int song_count() const { return songs_count_; }
static QList<MusicOwner> parseMusicOwnerList(const QVariant& request_result);
private:
private:
friend QDataStream& operator<<(QDataStream& stream, const MusicOwner& val);
friend QDataStream& operator>>(QDataStream& stream, MusicOwner& val);
friend QDebug operator<<(QDebug d, const MusicOwner& owner);
@ -66,7 +63,8 @@ private:
int songs_count_;
int id_; // if id > 0 is user otherwise id group
QString name_;
// name used in url http://vk.com/<screen_name> for example: http://vk.com/shedward
// name used in url http://vk.com/<screen_name> for example:
// http://vk.com/shedward
QString screen_name_;
QUrl photo_;
};
@ -84,20 +82,13 @@ QDebug operator<<(QDebug d, const MusicOwner& owner);
* how to react to the received request or quickly skip unwanted.
*/
struct SearchID {
enum Type {
GlobalSearch,
LocalSearch,
MoreLocalSearch,
UserOrGroup
};
enum Type { GlobalSearch, LocalSearch, MoreLocalSearch, UserOrGroup };
explicit SearchID(Type type)
: type_(type) {
id_= last_id_++;
}
explicit SearchID(Type type) : type_(type) { id_ = last_id_++; }
int id() const { return id_; }
Type type() const { return type_; }
private:
private:
static uint last_id_;
int id_;
Type type_;
@ -109,7 +100,7 @@ private:
class VkService : public InternetService {
Q_OBJECT
public:
public:
explicit VkService(Application* app, InternetModel* parent);
~VkService();
@ -133,8 +124,12 @@ public:
Type_Search
};
enum Role { Role_MusicOwnerMetadata = InternetModel::RoleCount,
Role_AlbumMetadata };
enum Role {
Role_MusicOwnerMetadata = InternetModel::RoleCount,
Role_AlbumMetadata
};
Application* app() const { return app_; }
/* InternetService interface */
QStandardItem* CreateRootItem();
@ -155,13 +150,16 @@ public:
bool WaitForReply(Vreen::Reply* reply);
/* Music */
VkMusicCache* cache() const { return cache_; }
void SetCurrentSongFromUrl(const QUrl& url); // Used if song taked from cache.
QUrl GetSongPlayUrl(const QUrl& url, bool is_playing = true);
void SongStarting(const Song& song);
void SongStarting(const QUrl& url); // Used if song taked from cache.
void SongSkipped();
UrlHandler::LoadResult GetSongResult(const QUrl& url);
Vreen::AudioItem GetAudioItemFromUrl(const QUrl& url);
// Return random song result from group playlist.
UrlHandler::LoadResult GetGroupNextSongUrl(const QUrl& url);
void SongSearch(SearchID id, const QString& query, int count = 50, int offset = 0);
void SongSearch(SearchID id, const QString& query, int count = 50,
int offset = 0);
void GroupSearch(SearchID id, const QString& query);
/* Settings */
@ -169,6 +167,7 @@ public:
int maxGlobalSearch() const { return maxGlobalSearch_; }
bool isCachingEnabled() const { return cachingEnabled_; }
bool isGroupsInGlobalSearch() const { return groups_in_global_search_; }
bool isBroadcasting() const { return enable_broadcast_; }
QString cacheDir() const { return cacheDir_; }
QString cacheFilename() const { return cacheFilename_; }
bool isLoveAddToMyMusic() const { return love_is_add_to_mymusic_; }
@ -179,15 +178,16 @@ signals:
void LoginSuccess(bool success);
void SongSearchResult(const SearchID& id, const SongList& songs);
void GroupSearchResult(const SearchID& id, const MusicOwnerList& groups);
void UserOrGroupSearchResult(const SearchID& id, const MusicOwnerList& owners);
void UserOrGroupSearchResult(const SearchID& id,
const MusicOwnerList& owners);
void StopWaiting();
public slots:
public slots:
void UpdateRoot();
void ShowConfig();
void FindUserOrGroup(const QString& q);
private slots:
private slots:
/* Interface */
void UpdateItem();
@ -197,6 +197,7 @@ private slots:
void Error(Vreen::Client::Error error);
/* Music */
void SongStopped();
void UpdateMyMusic();
void UpdateBookmarkSongs(QStandardItem* item);
void UpdateAlbumSongs(QStandardItem* item);
@ -208,7 +209,7 @@ private slots:
void AddToMyMusic();
void AddToMyMusicCurrent();
void RemoveFromMyMusic();
void AddToCache();
void AddSelectedToCache();
void CopyShareUrl();
void ShowSearchDialog();
@ -219,14 +220,16 @@ private slots:
void GroupSearchReceived(const SearchID& id, Vreen::Reply* reply);
void UserOrGroupReceived(const SearchID& id, Vreen::Reply* reply);
void AlbumListReceived(Vreen::AudioAlbumItemListReply* reply);
void BroadcastChangeReceived(Vreen::IntReply* reply);
void AppendLoadedSongs(QStandardItem* item, Vreen::AudioItemListReply* reply);
void RecommendationsLoaded(Vreen::AudioItemListReply* reply);
void SearchResultLoaded(const SearchID& id, const SongList& songs);
private:
private:
/* Interface */
QStandardItem* CreateAndAppendRow(QStandardItem* parent, VkService::ItemType type);
QStandardItem* CreateAndAppendRow(QStandardItem* parent,
VkService::ItemType type);
void ClearStandardItem(QStandardItem* item);
QStandardItem* GetBookmarkItemById(int id);
void EnsureMenuCreated();
@ -279,7 +282,7 @@ private:
uint last_search_id_;
QString last_query_;
Song selected_song_; // Store for context menu actions.
Song current_song_; // Store for actions with now playing song.
Song current_song_; // Store for actions with now playing song.
// Store current group url for actions with it.
QUrl current_group_url_;
@ -288,6 +291,7 @@ private:
bool cachingEnabled_;
bool love_is_add_to_mymusic_;
bool groups_in_global_search_;
bool enable_broadcast_;
QString cacheDir_;
QString cacheFilename_;
};

View File

@ -25,32 +25,29 @@
#include "core/logging.h"
#include "internet/vkservice.h"
VkSettingsPage::VkSettingsPage(SettingsDialog *parent)
: SettingsPage(parent),
ui_(new Ui::VkSettingsPage),
service_(dialog()->app()->internet_model()->Service<VkService>()) {
VkSettingsPage::VkSettingsPage(SettingsDialog* parent)
: SettingsPage(parent),
ui_(new Ui::VkSettingsPage),
service_(dialog()->app()->internet_model()->Service<VkService>()) {
ui_->setupUi(this);
connect(service_, SIGNAL(LoginSuccess(bool)),
SLOT(LoginSuccess(bool)));
connect(ui_->choose_path, SIGNAL(clicked()),
SLOT(CacheDirBrowse()));
connect(ui_->reset, SIGNAL(clicked()),
SLOT(ResetCasheFilenames()));
connect(service_, SIGNAL(LoginSuccess(bool)), SLOT(LoginSuccess(bool)));
connect(ui_->choose_path, SIGNAL(clicked()), SLOT(CacheDirBrowse()));
connect(ui_->reset, SIGNAL(clicked()), SLOT(ResetCasheFilenames()));
}
VkSettingsPage::~VkSettingsPage() {
delete ui_;
}
VkSettingsPage::~VkSettingsPage() { delete ui_; }
void VkSettingsPage::Load() {
service_->ReloadSettings();
ui_->maxGlobalSearch->setValue(service_->maxGlobalSearch());
ui_->max_global_search->setValue(service_->maxGlobalSearch());
ui_->enable_caching->setChecked(service_->isCachingEnabled());
ui_->cache_dir->setText(service_->cacheDir());
ui_->cache_filename->setText(service_->cacheFilename());
ui_->love_button_is_add_to_mymusic->setChecked(service_->isLoveAddToMyMusic());
ui_->love_button_is_add_to_mymusic->setChecked(
service_->isLoveAddToMyMusic());
ui_->groups_in_global_search->setChecked(service_->isGroupsInGlobalSearch());
ui_->enable_broadcast->setChecked(service_->isBroadcasting());
if (service_->HasAccount()) {
LogoutWidgets();
@ -63,12 +60,15 @@ void VkSettingsPage::Save() {
QSettings s;
s.beginGroup(VkService::kSettingGroup);
s.setValue("max_global_search", ui_->maxGlobalSearch->value());
s.setValue("max_global_search", ui_->max_global_search->value());
s.setValue("cache_enabled", ui_->enable_caching->isChecked());
s.setValue("cache_dir", ui_->cache_dir->text());
s.setValue("cache_filename", ui_->cache_filename->text());
s.setValue("love_is_add_to_my_music", ui_->love_button_is_add_to_mymusic->isChecked());
s.setValue("groups_in_global_search", ui_->groups_in_global_search->isChecked());
s.setValue("love_is_add_to_my_music",
ui_->love_button_is_add_to_mymusic->isChecked());
s.setValue("groups_in_global_search",
ui_->groups_in_global_search->isChecked());
s.setValue("enable_broadcast", ui_->enable_broadcast->isChecked());
service_->ReloadSettings();
}
@ -94,7 +94,7 @@ void VkSettingsPage::Logout() {
void VkSettingsPage::CacheDirBrowse() {
QString directory = QFileDialog::getExistingDirectory(
this, tr("Choose Vk.com cache directory"), ui_->cache_dir->text());
this, tr("Choose Vk.com cache directory"), ui_->cache_dir->text());
if (directory.isEmpty()) {
return;
}
@ -111,10 +111,9 @@ void VkSettingsPage::LoginWidgets() {
ui_->name->setText("");
ui_->login_button->setEnabled(true);
connect(ui_->login_button, SIGNAL(clicked()),
SLOT(Login()), Qt::UniqueConnection);
disconnect(ui_->login_button, SIGNAL(clicked()),
this, SLOT(Logout()));
connect(ui_->login_button, SIGNAL(clicked()), SLOT(Login()),
Qt::UniqueConnection);
disconnect(ui_->login_button, SIGNAL(clicked()), this, SLOT(Logout()));
}
void VkSettingsPage::LogoutWidgets() {
@ -122,12 +121,11 @@ void VkSettingsPage::LogoutWidgets() {
ui_->name->setText(tr("Loading..."));
ui_->login_button->setEnabled(true);
connect(service_, SIGNAL(NameUpdated(QString)),
ui_->name, SLOT(setText(QString)), Qt::UniqueConnection);
connect(service_, SIGNAL(NameUpdated(QString)), ui_->name,
SLOT(setText(QString)), Qt::UniqueConnection);
service_->RequestUserProfile();
connect(ui_->login_button, SIGNAL(clicked()),
SLOT(Logout()), Qt::UniqueConnection);
disconnect(ui_->login_button, SIGNAL(clicked()),
this, SLOT(Login()));
connect(ui_->login_button, SIGNAL(clicked()), SLOT(Logout()),
Qt::UniqueConnection);
disconnect(ui_->login_button, SIGNAL(clicked()), this, SLOT(Login()));
}

View File

@ -64,12 +64,12 @@
<string>Max global search results</string>
</property>
<property name="buddy">
<cstring>maxGlobalSearch</cstring>
<cstring>max_global_search</cstring>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="maxGlobalSearch">
<widget class="QSpinBox" name="max_global_search">
<property name="minimum">
<number>50</number>
</property>
@ -110,6 +110,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="enable_broadcast">
<property name="text">
<string>Show playing song on your page</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@ -19,45 +19,39 @@
#include "core/application.h"
#include "core/logging.h"
#include "core/player.h"
#include "vkservice.h"
#include "vkmusiccache.h"
VkUrlHandler::VkUrlHandler(VkService* service, QObject* parent)
: UrlHandler(parent),
service_(service) {
}
: UrlHandler(parent), service_(service) {}
UrlHandler::LoadResult VkUrlHandler::StartLoading(const QUrl& url) {
QStringList args = url.path().split("/");
LoadResult result;
if (args.size() < 2) {
qLog(Error) << "Invalid Vk.com URL: " << url
<< "Url format should be vk://<source>/<id>."
<< "For example vk://song/61145020_166946521/Daughtry/Gone Too Soon";
qLog(Error)
<< "Invalid Vk.com URL: " << url
<< "Url format should be vk://<source>/<id>."
<< "For example vk://song/61145020_166946521/Daughtry/Gone Too Soon";
} else {
QString action = url.host();
if (action == "song") {
service_->SetCurrentSongFromUrl(url);
result = LoadResult(url, LoadResult::TrackAvailable, service_->cache()->Get(url));
result = service_->GetSongResult(url);
} else if (action == "group") {
result = service_->GetGroupNextSongUrl(url);
} else {
qLog(Error) << "Invalid vk.com url action:" << action;
}
}
return result;
}
void VkUrlHandler::TrackSkipped() {
service_->cache()->BreakCurrentCaching();
}
void VkUrlHandler::ForceAddToCache(const QUrl& url) {
service_->cache()->ForceCache(url);
}
void VkUrlHandler::TrackSkipped() { service_->SongSkipped(); }
UrlHandler::LoadResult VkUrlHandler::LoadNext(const QUrl& url) {
if (url.host() == "group") {

View File

@ -28,16 +28,15 @@ class VkMusicCache;
class VkUrlHandler : public UrlHandler {
Q_OBJECT
public:
public:
VkUrlHandler(VkService* service, QObject* parent);
QString scheme() const { return "vk"; }
QIcon icon() const { return QIcon(":providers/vk.png"); }
LoadResult StartLoading(const QUrl& url);
void TrackSkipped();
void ForceAddToCache(const QUrl& url);
LoadResult LoadNext(const QUrl& url);
private:
private:
VkService* service_;
};

View File

@ -464,7 +464,7 @@ QVariant LibraryModel::AlbumIcon(const QModelIndex& index) {
}
// Try to load it from the disk cache
std::unique_ptr<QIODevice> cache (icon_cache_->data(QUrl(cache_key)));
std::unique_ptr<QIODevice> cache(icon_cache_->data(QUrl(cache_key)));
if (cache) {
QImage cached_pixmap;
if (cached_pixmap.load(cache.get(), "XPM")) {
@ -508,7 +508,7 @@ void LibraryModel::AlbumArtLoaded(quint64 id, const QImage& image) {
}
// if not already in the disk cache
std::unique_ptr<QIODevice> cached_img (icon_cache_->data(QUrl(cache_key)));
std::unique_ptr<QIODevice> cached_img(icon_cache_->data(QUrl(cache_key)));
if (!cached_img) {
QNetworkCacheMetaData item_metadata;
item_metadata.setSaveToDisk(true);
@ -1073,6 +1073,10 @@ QString LibraryModel::SortTextForArtist(QString artist) {
if (artist.startsWith("the ")) {
artist = artist.right(artist.length() - 4) + ", the";
} else if (artist.startsWith("a ")) {
artist = artist.right(artist.length() - 2) + ", a";
} else if (artist.startsWith("an ")) {
artist = artist.right(artist.length() - 3) + ", an";
}
return artist;

View File

@ -156,6 +156,10 @@ void MoodbarLoader::RequestFinished(MoodbarPipeline* request, const QUrl& url) {
metadata.setUrl(url);
QIODevice* cache_file = cache_->prepare(metadata);
if (!cache_file) {
qLog(Warning) << "Error writing to moodbar cache";
return;
}
cache_file->write(request->data());
cache_->insert(cache_file);

View File

@ -33,6 +33,7 @@ const char* MusicBrainzClient::kDiscUrl =
"https://musicbrainz.org/ws/2/discid/";
const char* MusicBrainzClient::kDateRegex = "^[12]\\d{3}";
const int MusicBrainzClient::kDefaultTimeout = 5000; // msec
const int MusicBrainzClient::kMaxRequestPerTrack = 3;
MusicBrainzClient::MusicBrainzClient(QObject* parent,
QNetworkAccessManager* network)
@ -59,6 +60,10 @@ void MusicBrainzClient::Start(int id, const QStringList& mbid_list) {
requests_.insert(id, reply);
timeouts_->AddReply(reply);
if (request_number >= kMaxRequestPerTrack) {
break;
}
}
}
@ -142,7 +147,7 @@ void MusicBrainzClient::DiscIdRequestFinished(const QString& discid,
}
}
emit Finished(artist, album, UniqueResults(ret));
emit Finished(artist, album, UniqueResults(ret, SortResults));
}
void MusicBrainzClient::RequestFinished(QNetworkReply* reply, int id, int request_number) {
@ -154,7 +159,6 @@ void MusicBrainzClient::RequestFinished(QNetworkReply* reply, int id, int reques
"requests removed, while only one was supposed to be removed";
}
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() ==
200) {
QXmlStreamReader reader(reply);
@ -170,8 +174,11 @@ void MusicBrainzClient::RequestFinished(QNetworkReply* reply, int id, int reques
}
}
}
qSort(res);
pending_results_[id] << PendingResults(request_number, res);
} else {
qLog(Error) << "Error:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() <<
"http status code received";
qLog(Error) << reply->readAll();
}
// No more pending requests for this id: emit the results we have.
@ -183,7 +190,7 @@ void MusicBrainzClient::RequestFinished(QNetworkReply* reply, int id, int reques
for (const PendingResults& result_list : result_list_list) {
ret << result_list.results_;
}
emit Finished(id, UniqueResults(ret));
emit Finished(id, UniqueResults(ret, KeepOriginalOrder));
}
}
@ -345,8 +352,23 @@ MusicBrainzClient::Release MusicBrainzClient::ParseRelease(
return ret;
}
MusicBrainzClient::ResultList& MusicBrainzClient::UniqueResults(ResultList& results) {
qStableSort(results);
results.erase(std::unique(results.begin(), results.end()), results.end());
return results;
MusicBrainzClient::ResultList MusicBrainzClient::UniqueResults(
const ResultList& results, UniqueResultsSortOption opt) {
ResultList ret;
if (opt == SortResults) {
ret = QSet<Result>::fromList(results).toList();
qSort(ret);
} else { // KeepOriginalOrder
// Qt doesn't provide a ordered set (QSet "stores values in an unspecified
// order" according to Qt documentation).
// We might use std::set instead, but it's probably faster to use ResultList
// directly to avoid converting from one structure to another.
for (const Result& res : results) {
if (!ret.contains(res)) {
ret << res;
}
}
}
return ret;
}

View File

@ -103,6 +103,12 @@ signals:
void DiscIdRequestFinished(const QString& discid, QNetworkReply* reply);
private:
// Used as parameter for UniqueResults
enum UniqueResultsSortOption {
SortResults = 0,
KeepOriginalOrder
};
struct Release {
Release() : track_(0), year_(0) {}
@ -137,14 +143,16 @@ signals:
static ResultList ParseTrack(QXmlStreamReader* reader);
static void ParseArtist(QXmlStreamReader* reader, QString* artist);
static Release ParseRelease(QXmlStreamReader* reader);
// Remove duplicate from the list. Returns a reference to the input parameter
static ResultList& UniqueResults(ResultList& results);
static ResultList UniqueResults(const ResultList& results,
UniqueResultsSortOption opt = SortResults);
private:
static const char* kTrackUrl;
static const char* kDiscUrl;
static const char* kDateRegex;
static const int kDefaultTimeout;
static const int kMaxRequestPerTrack;
QNetworkAccessManager* network_;
NetworkTimeouts* timeouts_;

View File

@ -89,6 +89,10 @@ const QRgb Playlist::kDynamicHistoryColor = qRgb(0x80, 0x80, 0x80);
const char* Playlist::kSettingsGroup = "Playlist";
const char* Playlist::kPathType = "path_type";
const char* Playlist::kWriteMetadata = "write_metadata";
const char* Playlist::kQuickChangeMenu = "quick_change_menu";
const int Playlist::kUndoStackSize = 20;
const int Playlist::kUndoItemLimit = 500;
@ -1097,9 +1101,8 @@ void Playlist::InsertInternetItems(const InternetModel* model,
switch (item.data(InternetModel::Role_PlayBehaviour).toInt()) {
case InternetModel::PlayBehaviour_SingleItem:
playlist_items << shared_ptr<PlaylistItem>(new InternetPlaylistItem(
model->ServiceForIndex(item),
item.data(InternetModel::Role_SongMetadata)
.value<Song>()));
model->ServiceForIndex(item),
item.data(InternetModel::Role_SongMetadata).value<Song>()));
break;
case InternetModel::PlayBehaviour_UseSongLoader:
@ -1122,7 +1125,7 @@ void Playlist::InsertInternetItems(InternetService* service,
PlaylistItemList playlist_items;
for (const Song& song : songs) {
playlist_items << shared_ptr<PlaylistItem>(
new InternetPlaylistItem(service, song));
new InternetPlaylistItem(service, song));
}
InsertItems(playlist_items, pos, play_now, enqueue);
@ -1402,10 +1405,10 @@ void Playlist::ReOrderWithoutUndo(const PlaylistItemList& new_items) {
new_rows[new_items[i].get()] = i;
}
for (const QModelIndex& idx: persistentIndexList()) {
for (const QModelIndex& idx : persistentIndexList()) {
const PlaylistItem* item = old_items[idx.row()].get();
changePersistentIndex(
idx, index(new_rows[item], idx.column(), idx.parent()));
changePersistentIndex(idx,
index(new_rows[item], idx.column(), idx.parent()));
}
layoutChanged();
@ -1914,8 +1917,10 @@ void Playlist::ReshuffleIndices() {
std::random_shuffle(shuffled_album_keys.begin(),
shuffled_album_keys.end());
// If the user is currently playing a song, force its album to be first.
if (current_virtual_index_ != -1) {
// If the user is currently playing a song, force its album to be first
// Or if the song was not playing but it was selected, force its album
// to be first.
if (current_virtual_index_ != -1 || current_row() != -1) {
const QString key = items_[current_row()]->Metadata().AlbumKey();
const int pos = shuffled_album_keys.indexOf(key);
if (pos >= 1) {
@ -2104,6 +2109,21 @@ void Playlist::RemoveDuplicateSongs() {
removeRows(rows_to_remove);
}
void Playlist::RemoveUnavailableSongs() {
QList<int> rows_to_remove;
for (int row = 0; row < items_.count(); ++row) {
PlaylistItemPtr item = items_[row];
const Song& song = item->Metadata();
// check only local files
if (song.url().isLocalFile() && !QFile::exists(song.url().toLocalFile())) {
rows_to_remove.append(row);
}
}
removeRows(rows_to_remove);
}
bool Playlist::ApplyValidityOnCurrentSong(const QUrl& url, bool valid) {
PlaylistItemPtr current = current_item();

View File

@ -134,6 +134,12 @@ class Playlist : public QAbstractListModel {
LastFM_Queued, // Track added to the queue for scrobbling
};
enum Path {
Path_Automatic = 0, // Automatically select path type
Path_Absolute, // Always use absolute paths
Path_Relative, // Always use relative paths
};
static const char* kCddaMimeType;
static const char* kRowsMimetype;
static const char* kPlayNowMimetype;
@ -146,6 +152,10 @@ class Playlist : public QAbstractListModel {
static const char* kSettingsGroup;
static const char* kPathType;
static const char* kWriteMetadata;
static const char* kQuickChangeMenu;
static const int kUndoStackSize;
static const int kUndoItemLimit;
@ -304,6 +314,7 @@ class Playlist : public QAbstractListModel {
void Clear();
void RemoveDuplicateSongs();
void RemoveUnavailableSongs();
void Shuffle();
void ShuffleModeChanged(PlaylistSequence::ShuffleMode mode);

View File

@ -51,6 +51,13 @@ PlaylistContainer::PlaylistContainer(QWidget* parent)
filter_timer_(new QTimer(this)) {
ui_->setupUi(this);
ui_->file_path_box->addItem(tr("Automatic"));
ui_->file_path_box->addItem(tr("Absolute"));
ui_->file_path_box->addItem(tr("Relative"));
connect(ui_->file_path_box, SIGNAL(currentIndexChanged(int)),
SLOT(PathSettingChanged(int)));
no_matches_label_ = new QLabel(ui_->playlist);
no_matches_label_->setAlignment(Qt::AlignTop | Qt::AlignHCenter);
no_matches_label_->setAttribute(Qt::WA_TransparentForMouseEvents);
@ -75,6 +82,8 @@ PlaylistContainer::PlaylistContainer(QWidget* parent)
settings_.beginGroup(kSettingsGroup);
ReloadSettings();
// Tab bar
ui_->tab_bar->setExpanding(false);
ui_->tab_bar->setMovable(true);
@ -101,6 +110,28 @@ PlaylistContainer::PlaylistContainer(QWidget* parent)
PlaylistContainer::~PlaylistContainer() { delete ui_; }
void PlaylistContainer::ReloadSettings() {
bool show_menu = settings_.value(Playlist::kQuickChangeMenu, false).toBool();
ui_->line->setVisible(show_menu);
ui_->file_path_label->setVisible(show_menu);
ui_->file_path_box->setVisible(show_menu);
int value =
settings_.value(Playlist::kPathType, Playlist::Path_Automatic).toInt();
Playlist::Path path = static_cast<Playlist::Path>(value);
switch (path) {
case Playlist::Path_Automatic:
ui_->file_path_box->setCurrentIndex(0);
break;
case Playlist::Path_Absolute:
ui_->file_path_box->setCurrentIndex(1);
break;
case Playlist::Path_Relative:
ui_->file_path_box->setCurrentIndex(2);
break;
}
}
PlaylistView* PlaylistContainer::view() const { return ui_->playlist; }
void PlaylistContainer::SetActions(QAction* new_playlist,
@ -361,14 +392,15 @@ void PlaylistContainer::UpdateNoMatchesLabel() {
QString text;
if (has_rows && !has_results) {
if (ui_->filter->text().trimmed().compare("the answer to life the universe "
"and everything",
Qt::CaseInsensitive) == 0) {
if (ui_->filter->text().trimmed().compare(
"the answer to life the universe "
"and everything",
Qt::CaseInsensitive) == 0) {
text = "42";
} else {
text =
tr("No matches found. Clear the search box to show the whole playlist "
"again.");
text = tr(
"No matches found. Clear the search box to show the whole playlist "
"again.");
}
}
@ -438,3 +470,7 @@ bool PlaylistContainer::eventFilter(QObject* objectWatched, QEvent* event) {
}
return QWidget::eventFilter(objectWatched, event);
}
void PlaylistContainer::PathSettingChanged(int index) {
settings_.setValue(Playlist::kPathType, index);
}

View File

@ -21,6 +21,8 @@
#include <QWidget>
#include <QSettings>
#include "playlist.h"
class Ui_PlaylistContainer;
class LineEditInterface;
@ -46,6 +48,8 @@ class PlaylistContainer : public QWidget {
QAction* previous_playlist);
void SetManager(PlaylistManager* manager);
void ReloadSettings();
PlaylistView* view() const;
bool eventFilter(QObject* objectWatched, QEvent* event);
@ -90,6 +94,8 @@ signals:
void UpdateNoMatchesLabel();
void PathSettingChanged(int index);
private:
void UpdateActiveIcon(const QIcon& icon);
void RepositionNoMatchesLabel(bool force = false);

View File

@ -24,7 +24,16 @@
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
@ -36,7 +45,16 @@
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
@ -104,6 +122,36 @@
</property>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="file_path_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>File Paths:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="file_path_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">

View File

@ -336,6 +336,10 @@ void PlaylistManager::RemoveDuplicatesCurrent() {
current()->RemoveDuplicateSongs();
}
void PlaylistManager::RemoveUnavailableCurrent() {
current()->RemoveUnavailableSongs();
}
void PlaylistManager::SetActivePlaying() { active()->Playing(); }
void PlaylistManager::SetActivePaused() { active()->Paused(); }

View File

@ -95,6 +95,7 @@ class PlaylistManagerInterface : public QObject {
virtual void ClearCurrent() = 0;
virtual void ShuffleCurrent() = 0;
virtual void RemoveDuplicatesCurrent() = 0;
virtual void RemoveUnavailableCurrent() = 0;
virtual void SetActivePlaying() = 0;
virtual void SetActivePaused() = 0;
virtual void SetActiveStopped() = 0;
@ -204,6 +205,7 @@ class PlaylistManager : public PlaylistManagerInterface {
void ClearCurrent();
void ShuffleCurrent();
void RemoveDuplicatesCurrent();
void RemoveUnavailableCurrent();
void SetActiveStreamMetadata(const QUrl& url, const Song& song);
// Rate current song using 0.0 - 1.0 scale.
void RateCurrentSong(double rating);

View File

@ -19,6 +19,8 @@
#include "core/logging.h"
#include "core/timeconstants.h"
#include "playlist/playlist.h"
#include <QBuffer>
#include <QtDebug>
@ -92,7 +94,7 @@ bool M3UParser::ParseMetadata(const QString& line,
metadata->length = length * kNsecPerSec;
QString track_info = info.section(',', 1);
QStringList list = track_info.split('-');
QStringList list = track_info.split(" - ");
if (list.size() <= 1) {
metadata->title = track_info;
return true;
@ -105,15 +107,23 @@ bool M3UParser::ParseMetadata(const QString& line,
void M3UParser::Save(const SongList& songs, QIODevice* device,
const QDir& dir) const {
device->write("#EXTM3U\n");
QSettings s;
s.beginGroup(Playlist::kSettingsGroup);
bool writeMetadata = s.value(Playlist::kWriteMetadata, true).toBool();
s.endGroup();
for (const Song& song : songs) {
if (song.url().isEmpty()) {
continue;
}
QString meta = QString("#EXTINF:%1,%2 - %3\n")
.arg(song.length_nanosec() / kNsecPerSec)
.arg(song.artist())
.arg(song.title());
device->write(meta.toUtf8());
if (writeMetadata) {
QString meta = QString("#EXTINF:%1,%2 - %3\n")
.arg(song.length_nanosec() / kNsecPerSec)
.arg(song.artist())
.arg(song.title());
device->write(meta.toUtf8());
}
device->write(URLOrRelativeFilename(song.url(), dir).toUtf8());
device->write("\n");
}

View File

@ -20,6 +20,7 @@
#include "library/librarybackend.h"
#include "library/libraryquery.h"
#include "library/sqlrow.h"
#include "playlist/playlist.h"
#include <QUrl>
@ -46,8 +47,10 @@ void ParserBase::LoadSong(const QString& filename_or_url, qint64 beginning,
}
}
// Convert native separators for Windows paths
filename = QDir::fromNativeSeparators(filename);
// Clementine always wants / separators internally. Using
// QDir::fromNativeSeparators() only works on the same platform the playlist
// was created on/for, using replace() lets playlists work on any platform.
filename = filename.replace('\\', '/');
// Make the path absolute
if (!QDir::isAbsolutePath(filename)) {
@ -87,11 +90,19 @@ QString ParserBase::URLOrRelativeFilename(const QUrl& url,
const QDir& dir) const {
if (url.scheme() != "file") return url.toString();
QSettings s;
s.beginGroup(Playlist::kSettingsGroup);
int p = s.value(Playlist::kPathType, Playlist::Path_Automatic).toInt();
const Playlist::Path path = static_cast<Playlist::Path>(p);
s.endGroup();
const QString filename = url.toLocalFile();
if (QDir::isAbsolutePath(filename)) {
if (path != Playlist::Path_Absolute && QDir::isAbsolutePath(filename)) {
const QString relative = dir.relativeFilePath(filename);
if (!relative.contains("..")) return relative;
if (!relative.startsWith("../") || path == Playlist::Path_Relative)
return relative;
}
return filename;
}

View File

@ -19,6 +19,8 @@
#include "core/timeconstants.h"
#include "core/utilities.h"
#include "playlist/playlist.h"
#include <QDomDocument>
#include <QFile>
#include <QIODevice>
@ -111,53 +113,55 @@ void XSPFParser::Save(const SongList& songs, QIODevice* device,
writer.writeAttribute("version", "1");
writer.writeDefaultNamespace("http://xspf.org/ns/0/");
QSettings s;
s.beginGroup(Playlist::kSettingsGroup);
bool writeMetadata = s.value(Playlist::kWriteMetadata, true).toBool();
s.endGroup();
StreamElement tracklist("trackList", &writer);
for (const Song& song : songs) {
QString filename_or_url;
if (song.url().scheme() == "file") {
// Make the filename relative to the directory we're saving the playlist.
filename_or_url = dir.relativeFilePath(
QFileInfo(song.url().toLocalFile()).absoluteFilePath());
} else {
filename_or_url = song.url().toEncoded();
}
QString filename_or_url = URLOrRelativeFilename(song.url(), dir).toUtf8();
StreamElement track("track", &writer);
writer.writeTextElement("location", filename_or_url);
writer.writeTextElement("title", song.title());
if (!song.artist().isEmpty()) {
writer.writeTextElement("creator", song.artist());
}
if (!song.album().isEmpty()) {
writer.writeTextElement("album", song.album());
}
if (song.length_nanosec() != -1) {
writer.writeTextElement(
"duration", QString::number(song.length_nanosec() / kNsecPerMsec));
}
QString art =
song.art_manual().isEmpty() ? song.art_automatic() : song.art_manual();
// Ignore images that are in our resource bundle.
if (!art.startsWith(":") && !art.isEmpty()) {
QString art_filename;
if (!art.contains("://")) {
art_filename = art;
} else if (QUrl(art).scheme() == "file") {
art_filename = QUrl(art).toLocalFile();
if (writeMetadata) {
writer.writeTextElement("title", song.title());
if (!song.artist().isEmpty()) {
writer.writeTextElement("creator", song.artist());
}
if (!song.album().isEmpty()) {
writer.writeTextElement("album", song.album());
}
if (song.length_nanosec() != -1) {
writer.writeTextElement(
"duration", QString::number(song.length_nanosec() / kNsecPerMsec));
}
if (!art_filename.isEmpty()) {
// Make this filename relative to the directory we're saving the
// playlist.
art_filename = dir.relativeFilePath(
QFileInfo(art_filename).absoluteFilePath());
} else {
// Just use whatever URL was in the Song.
art_filename = art;
}
QString art = song.art_manual().isEmpty() ? song.art_automatic()
: song.art_manual();
// Ignore images that are in our resource bundle.
if (!art.startsWith(":") && !art.isEmpty()) {
QString art_filename;
if (!art.contains("://")) {
art_filename = art;
} else if (QUrl(art).scheme() == "file") {
art_filename = QUrl(art).toLocalFile();
}
writer.writeTextElement("image", art_filename);
if (!art_filename.isEmpty() && !(art_filename == "(embedded)")) {
// Make this filename relative to the directory we're saving the
// playlist.
QUrl url = QUrl(art_filename);
url.setScheme("file"); // Need to explicitly set this.
art_filename = URLOrRelativeFilename(url, dir).toUtf8();
} else {
// Just use whatever URL was in the Song.
art_filename = art;
}
writer.writeTextElement("image", art_filename);
}
}
}
writer.writeEndDocument();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -133,6 +133,24 @@ void BehaviourSettingsPage::Load() {
s.value("greyoutdeleted", false).toBool());
ui_->b_click_edit_inline_->setChecked(
s.value("click_edit_inline", true).toBool());
Playlist::Path path = Playlist::Path(
s.value(Playlist::kPathType, Playlist::Path_Automatic).toInt());
switch (path) {
case Playlist::Path_Automatic:
ui_->b_automatic_path->setChecked(true);
break;
case Playlist::Path_Absolute:
ui_->b_absolute_path->setChecked(true);
break;
case Playlist::Path_Relative:
ui_->b_relative_path->setChecked(true);
break;
}
ui_->b_write_metadata->setChecked(
s.value(Playlist::kWriteMetadata, true).toBool());
ui_->b_quickchange_menu->setChecked(
s.value(Playlist::kQuickChangeMenu, false).toBool());
s.endGroup();
s.beginGroup(PlaylistTabBar::kSettingsGroup);
@ -162,6 +180,15 @@ void BehaviourSettingsPage::Save() {
MainWindow::PlayBehaviour menu_playmode = MainWindow::PlayBehaviour(
ui_->menu_playmode->itemData(ui_->menu_playmode->currentIndex()).toInt());
Playlist::Path path = Playlist::Path_Automatic;
if (ui_->b_automatic_path->isChecked()) {
path = Playlist::Path_Automatic;
} else if (ui_->b_absolute_path->isChecked()) {
path = Playlist::Path_Absolute;
} else if (ui_->b_relative_path->isChecked()) {
path = Playlist::Path_Relative;
}
s.beginGroup(MainWindow::kSettingsGroup);
s.setValue("showtray", ui_->b_show_tray_icon_->isChecked());
s.setValue("keeprunning", ui_->b_keep_running_->isChecked());
@ -182,6 +209,9 @@ void BehaviourSettingsPage::Save() {
s.beginGroup(Playlist::kSettingsGroup);
s.setValue("greyoutdeleted", ui_->b_grey_out_deleted_->isChecked());
s.setValue("click_edit_inline", ui_->b_click_edit_inline_->isChecked());
s.setValue(Playlist::kPathType, static_cast<int>(path));
s.setValue(Playlist::kWriteMetadata, ui_->b_write_metadata->isChecked());
s.setValue(Playlist::kQuickChangeMenu, ui_->b_quickchange_menu->isChecked());
s.endGroup();
s.beginGroup(PlaylistTabBar::kSettingsGroup);

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>516</width>
<height>561</height>
<height>753</height>
</rect>
</property>
<property name="windowTitle">
@ -235,6 +235,72 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_9">
<property name="title">
<string>When saving a playlist, file paths should be</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QRadioButton" name="b_automatic_path">
<property name="text">
<string>Automatic</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="b_absolute_path">
<property name="text">
<string>Absolute</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="b_relative_path">
<property name="text">
<string>Relative</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_7">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="b_quickchange_menu">
<property name="text">
<string>Show quick change menu</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="b_write_metadata">
<property name="text">
<string>Write metadata</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">

View File

@ -161,8 +161,8 @@ const char* MainWindow::kSettingsGroup = "MainWindow";
const char* MainWindow::kAllFilesFilterSpec = QT_TR_NOOP("All Files (*)");
namespace {
const int kTrackSliderUpdateTimeMs = 40;
const int kTrackPositionUpdateTimeMs = 1000;
const int kTrackSliderUpdateTimeMs = 40;
const int kTrackPositionUpdateTimeMs = 1000;
}
MainWindow::MainWindow(Application* app, SystemTrayIcon* tray_icon, OSD* osd,
@ -368,6 +368,8 @@ MainWindow::MainWindow(Application* app, SystemTrayIcon* tray_icon, OSD* osd,
app_->playlist_manager(), SLOT(ClearCurrent()));
connect(ui_->action_remove_duplicates, SIGNAL(triggered()),
app_->playlist_manager(), SLOT(RemoveDuplicatesCurrent()));
connect(ui_->action_remove_unavailable, SIGNAL(triggered()),
app_->playlist_manager(), SLOT(RemoveUnavailableCurrent()));
connect(ui_->action_remove_from_playlist, SIGNAL(triggered()),
SLOT(PlaylistRemoveCurrent()));
connect(ui_->action_edit_track, SIGNAL(triggered()), SLOT(EditTracks()));
@ -626,6 +628,7 @@ MainWindow::MainWindow(Application* app, SystemTrayIcon* tray_icon, OSD* osd,
playlist_menu_->addAction(ui_->action_clear_playlist);
playlist_menu_->addAction(ui_->action_shuffle);
playlist_menu_->addAction(ui_->action_remove_duplicates);
playlist_menu_->addAction(ui_->action_remove_unavailable);
#ifdef Q_OS_DARWIN
ui_->action_shuffle->setShortcut(QKeySequence());
@ -976,6 +979,7 @@ void MainWindow::ReloadAllSettings() {
library_view_->ReloadSettings();
song_info_view_->ReloadSettings();
app_->player()->engine()->ReloadSettings();
ui_->playlist->ReloadSettings();
ui_->playlist->view()->ReloadSettings();
app_->internet_model()->ReloadSettings();
#ifdef HAVE_WIIMOTEDEV

View File

@ -444,6 +444,7 @@
<addaction name="action_clear_playlist"/>
<addaction name="action_shuffle"/>
<addaction name="action_remove_duplicates"/>
<addaction name="action_remove_unavailable"/>
</widget>
<widget class="QMenu" name="menu_help">
<property name="title">
@ -849,6 +850,11 @@
<string>Rip audio CD...</string>
</property>
</action>
<action name="action_remove_unavailable">
<property name="text">
<string>Remove unavailable tracks from playlist</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>

View File

@ -26,15 +26,14 @@
#include <QMetaType>
#include <QSignalSpy>
#include <QString>
#include <QStringList>
#include "mock_networkaccessmanager.h"
#include "gtest/gtest.h"
#include "test_utils.h"
namespace {
typedef QList<MusicBrainzClient::Result> ResultList;
Q_DECLARE_METATYPE(ResultList);
};
class MusicBrainzClientTest : public ::testing::Test {
protected:
@ -193,7 +192,7 @@ TEST_F(MusicBrainzClientTest, ParseTrack) {
// Start the request and get a result.
// The mbid argument doesn't matter in the test.
const int sent_id = 0;
musicbrainz_client.Start(sent_id, "fooMbid");
musicbrainz_client.Start(sent_id, QStringList() << "fooMbid");
discid_reply->Done();
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
EXPECT_EQ(1, spy.count());
@ -236,7 +235,7 @@ TEST_F(MusicBrainzClientTest, ParseTrackWithMultipleReleases) {
// Start the request and get a result.
// The mbid argument doesn't matter in the test.
const int sent_id = 0;
musicbrainz_client.Start(sent_id, "fooMbid");
musicbrainz_client.Start(sent_id, QStringList() << "fooMbid");
discid_reply->Done();
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
EXPECT_EQ(1, spy.count());