Merge branch 'master' into gstreamer-1.2

Conflicts:
	src/moodbar/moodbarloader.cpp
This commit is contained in:
David Sansome 2014-09-21 19:39:27 +10:00
commit a2408f7c0e
139 changed files with 24352 additions and 17765 deletions

View File

@ -107,6 +107,7 @@ QtLocalPeer::QtLocalPeer(QObject* parent, const QString &appId)
+ QLatin1Char('/') + socketName
+ QLatin1String("-lockfile");
lockFile.setFileName(lockName);
lockFileCreated = !lockFile.exists();
lockFile.open(QIODevice::ReadWrite);
}
@ -212,5 +213,6 @@ void QtLocalPeer::receiveConnection()
QtLocalPeer::~QtLocalPeer ()
{
lockFile.remove();
if (lockFileCreated)
lockFile.remove();
}

View File

@ -81,4 +81,5 @@ protected:
private:
static const char* ack;
bool lockFileCreated;
};

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

@ -21,6 +21,13 @@ if (APPLE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --stdlib=libc++")
endif ()
find_program(CCACHE_EXECUTABLE NAMES ccache)
if (CCACHE_EXECUTABLE)
message(STATUS "ccache found: will be used for compilation and linkage")
SET_PROPERTY(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ${CCACHE_EXECUTABLE})
SET_PROPERTY(GLOBAL PROPERTY RULE_LAUNCH_LINK ${CCACHE_EXECUTABLE})
endif ()
if (UNIX AND NOT APPLE)
set(LINUX 1)
endif (UNIX AND NOT APPLE)

View File

@ -340,7 +340,6 @@
<file>providers/soundcloud.png</file>
<file>providers/subsonic-32.png</file>
<file>providers/subsonic.png</file>
<file>providers/ubuntuone.png</file>
<file>providers/wikipedia.png</file>
<file>rainbowdash.png</file>
<file>sample.mood</file>

View File

@ -59,6 +59,13 @@
</extract>
<invalidIndicator value="Verifique se o nome do seu arquivo e sua"/>
</provider>
<provider name="lololyrics.com" title="" charset="utf-8" url="http://api.lololyrics.com/0.5/getLyric?artist={artist}&amp;track={title}">
<urlFormat replace="_@,;&amp;\/&quot;#" with="_"/>
<extract>
<item tag="&lt;response&gt;"/>
</extract>
<invalidIndicator value="ERROR"/>
</provider>
<provider name="loudson.gs" title="" charset="utf-8" url="http://www.loudson.gs/{a}/{artist}/{album}/{title}">
<urlFormat replace=" _@,;&amp;\/&quot;" with="-"/>
<urlFormat replace="." with=""/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -81,8 +81,9 @@ bool MediaPipeline::Init(int sample_rate, int channels) {
// Try to send 5 seconds of audio in advance to initially fill Clementine's
// buffer.
g_object_set(G_OBJECT(tcpsink_), "ts-offset", qint64(-5 * kNsecPerSec),
nullptr);
// Commented for now as otherwise the seek will take too long.
//g_object_set(G_OBJECT(tcpsink_), "ts-offset", qint64(-5 * kNsecPerSec),
// nullptr);
// We know the time of each buffer
g_object_set(G_OBJECT(appsrc_), "format", GST_FORMAT_TIME, nullptr);

View File

@ -292,6 +292,8 @@ void SpotifyClient::MessageArrived(const pb::spotify::Message& message) {
SetPlaybackSettings(message.set_playback_settings_request());
} else if (message.has_browse_toplist_request()) {
BrowseToplist(message.browse_toplist_request());
} else if (message.has_pause_request()) {
SetPaused(message.pause_request());
}
}
@ -682,6 +684,9 @@ int SpotifyClient::MusicDeliveryCallback(sp_session* session,
}
if (num_frames == 0) {
// According to libspotify documentation, this occurs when a discontinuity
// has occurred (such as after a seek). Maybe should clear buffers here as
// well? (in addition of clearing buffers in gstenginepipeline.cpp)
return 0;
}
@ -840,8 +845,16 @@ void SpotifyClient::StartPlayback(const pb::spotify::PlaybackRequest& req) {
}
void SpotifyClient::Seek(qint64 offset_bytes) {
// TODO
qLog(Error) << "TODO seeking";
if (sp_session_player_seek(session_, offset_bytes) != SP_ERROR_OK) {
qLog(Error) << "Seek error";
return;
}
pb::spotify::Message message;
pb::spotify::SeekCompleted* response = message.mutable_seek_completed();
Q_UNUSED(response);
SendMessage(message);
}
void SpotifyClient::TryPlaybackAgain(const PendingPlaybackRequest& req) {
@ -1017,6 +1030,10 @@ void SpotifyClient::BrowseToplist(
pending_toplist_browses_[browse] = req;
}
void SpotifyClient::SetPaused(const pb::spotify::PauseRequest& req) {
sp_session_player_play(session_, !req.paused());
}
void SpotifyClient::ToplistBrowseComplete(sp_toplistbrowse* result,
void* userdata) {
SpotifyClient* me = reinterpret_cast<SpotifyClient*>(userdata);

View File

@ -59,6 +59,7 @@ class SpotifyClient : public AbstractMessageHandler<pb::spotify::Message> {
pb::spotify::LoginResponse_Error error_code);
void SendPlaybackError(const QString& error);
void SendSearchResponse(sp_search* result);
void SendSeekCompleted();
// Spotify session callbacks.
static void SP_CALLCONV LoggedInCallback(sp_session* session, sp_error error);
@ -128,6 +129,7 @@ class SpotifyClient : public AbstractMessageHandler<pb::spotify::Message> {
void BrowseAlbum(const QString& uri);
void BrowseToplist(const pb::spotify::BrowseToplistRequest& req);
void SetPlaybackSettings(const pb::spotify::PlaybackSettings& req);
void SetPaused(const pb::spotify::PauseRequest& req);
void SendPlaylistList();

View File

@ -173,6 +173,9 @@ message SeekRequest {
optional int64 offset_bytes = 1;
}
message SeekCompleted {
}
enum Bitrate {
Bitrate96k = 1;
Bitrate160k = 2;
@ -184,7 +187,11 @@ message PlaybackSettings {
optional bool volume_normalisation = 2 [default = false];
}
// NEXT_ID: 21
message PauseRequest {
optional bool paused = 1 [default = false];
}
// NEXT_ID: 23
message Message {
// Not currently used
optional int32 id = 18;
@ -208,4 +215,6 @@ message Message {
optional PlaybackSettings set_playback_settings_request = 17;
optional BrowseToplistRequest browse_toplist_request = 19;
optional BrowseToplistResponse browse_toplist_response = 20;
optional PauseRequest pause_request = 21;
optional SeekCompleted seek_completed = 22;
}

View File

@ -892,11 +892,6 @@ optional_source(LINUX SOURCES widgets/osd_x11.cpp)
if(HAVE_DBUS)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/dbus)
# Hack to get it to generate interfaces without namespaces - required
# because otherwise org::freedesktop::UDisks and
# org::freedesktop::UDisks::Device conflict.
list(APPEND QT_DBUSXML2CPP_EXECUTABLE -N)
# MPRIS DBUS interfaces
qt4_add_dbus_adaptor(SOURCES
dbus/org.freedesktop.MediaPlayer.player.xml
@ -964,6 +959,10 @@ if(HAVE_DBUS)
# DeviceKit DBUS interfaces
if(HAVE_DEVICEKIT)
set_source_files_properties(dbus/org.freedesktop.UDisks.xml
PROPERTIES NO_NAMESPACE dbus/udisks)
set_source_files_properties(dbus/org.freedesktop.UDisks.Device.xml
PROPERTIES NO_NAMESPACE dbus/udisksdevice)
qt4_add_dbus_interface(SOURCES
dbus/org.freedesktop.UDisks.xml
dbus/udisks)

View File

@ -25,7 +25,7 @@ BarAnalyzer::BarAnalyzer(QWidget* parent) : Analyzer::Base(parent, 8) {
// roof pixmaps don't depend on size() so we do in the ctor
m_bg = parent->palette().color(QPalette::Background);
QColor fg(0xff, 0x50, 0x70);
QColor fg(parent->palette().color(QPalette::Highlight).lighter(150));
double dr = double(m_bg.red() - fg.red()) /
(NUM_ROOFS - 1); //-1 because we start loop below at 0
@ -69,7 +69,9 @@ void BarAnalyzer::init() {
canvas_.fill(palette().color(QPalette::Background));
QPainter p(&m_pixBarGradient);
for (int x = 0, r = 0x40, g = 0x30, b = 0xff, r2 = 255 - r; x < height();
QColor rgb(palette().color(QPalette::Highlight));
for (int x = 0, r = rgb.red(), g = rgb.green(), b = rgb.blue(), r2 = 255 - r; x < height();
++x) {
for (int y = x; y > 0; --y) {
const double fraction = (double)y / height();

View File

@ -39,7 +39,7 @@ RainbowDashAnalyzer::RainbowDashAnalyzer(QWidget* parent)
available_rainbow_width_(0),
px_per_frame_(0),
x_offset_(0),
background_brush_(QColor(0x38, 0x88, 0x00)) {
background_brush_(QColor(0x0f, 0x43, 0x73)) {
memset(history_, 0, sizeof(history_));
for (int i = 0; i < kRainbowBands; ++i) {

View File

@ -38,12 +38,12 @@ class RainbowDashAnalyzer : public Analyzer::Base {
void resizeEvent(QResizeEvent* e);
private:
static const int kDashHeight = 30;
static const int kDashWidth = 54;
static const int kDashHeight = 33;
static const int kDashWidth = 53;
static const int kRainbowHeight = 16;
static const int kDashFrameCount = 1;
static const int kDashFrameCount = 16;
static const int kRainbowOverlap = 15;
static const int kSleepingDashHeight = 30;
static const int kSleepingDashHeight = 33;
static const int kHistorySize = 128;
static const int kRainbowBands = 6;

View File

@ -31,7 +31,6 @@
#endif
#include "config.h"
#include "core/concurrentrun.h"
#include "core/logging.h"
#include "core/player.h"
#include "core/signalchecker.h"

View File

@ -148,7 +148,6 @@ class GstEngine : public Engine::Base, public BufferConsumer {
typedef QList<PluginDetails> PluginDetailsList;
static void SetEnv(const char* key, const QString& value);
PluginDetailsList GetPluginList(const QString& classname) const;
void StartFadeout();

View File

@ -66,6 +66,7 @@ GstEnginePipeline::GstEnginePipeline(GstEngine* engine)
buffering_(false),
mono_playback_(false),
end_offset_nanosec_(-1),
spotify_offset_(0),
next_beginning_offset_nanosec_(-1),
next_end_offset_nanosec_(-1),
ignore_next_seek_(false),
@ -93,6 +94,10 @@ GstEnginePipeline::GstEnginePipeline(GstEngine* engine)
}
for (int i = 0; i < kEqBandCount; ++i) eq_band_gains_ << 0;
// Spotify hack
connect(InternetModel::Service<SpotifyService>()->server(), SIGNAL(SeekCompleted()),
SLOT(SpotifySeekCompleted()));
}
void GstEnginePipeline::set_output_device(const QString& sink,
@ -168,6 +173,7 @@ bool GstEnginePipeline::ReplaceDecodeBin(const QUrl& url) {
// Tell spotify to start sending data to us.
InternetModel::Service<SpotifyService>()->server()->StartPlaybackLater(
url.toString(), port);
spotify_offset_ = 0;
} else {
new_bin = engine_->CreateElement("uridecodebin");
g_object_set(G_OBJECT(new_bin), "uri", url.toEncoded().constData(),
@ -839,7 +845,7 @@ void GstEnginePipeline::SourceSetupCallback(GstURIDecodeBin* bin,
instance->url().host().contains("grooveshark")) {
// Grooveshark streaming servers will answer with a 400 error 'Bad request'
// if we don't specify 'Range' field in HTTP header.
// Maybe it could be usefull in some other cases, but for now, I prefer to
// Maybe it could be useful in some other cases, but for now, I prefer to
// keep this grooveshark specific.
GstStructure* headers;
headers = gst_structure_new("extra-headers", "Range", G_TYPE_STRING,
@ -848,17 +854,6 @@ void GstEnginePipeline::SourceSetupCallback(GstURIDecodeBin* bin,
gst_structure_free(headers);
}
if (g_object_class_find_property(G_OBJECT_GET_CLASS(element),
"extra-headers") &&
instance->url().host().contains("files.one.ubuntu.com")) {
GstStructure* headers;
headers =
gst_structure_new("extra-headers", "Authorization", G_TYPE_STRING,
instance->url().fragment().toAscii().data(), nullptr);
g_object_set(element, "extra-headers", headers, nullptr);
gst_structure_free(headers);
}
if (g_object_class_find_property(G_OBJECT_GET_CLASS(element), "user-agent")) {
QString user_agent =
QString("%1 %2").arg(QCoreApplication::applicationName(),
@ -899,6 +894,10 @@ qint64 GstEnginePipeline::position() const {
gint64 value = 0;
gst_element_query_position(pipeline_, GST_FORMAT_TIME, &value);
if (url_.scheme() == "spotify") {
value += spotify_offset_;
}
return value;
}
@ -919,6 +918,23 @@ GstState GstEnginePipeline::state() const {
}
QFuture<GstStateChangeReturn> GstEnginePipeline::SetState(GstState state) {
if (url_.scheme() == "spotify" && !buffering_) {
const GstState current_state = this->state();
if (state == GST_STATE_PAUSED && current_state == GST_STATE_PLAYING) {
SpotifyService* spotify = InternetModel::Service<SpotifyService>();
// Need to schedule this in the spotify service's thread
QMetaObject::invokeMethod(spotify, "SetPaused", Qt::QueuedConnection,
Q_ARG(bool, true));
} else if (state == GST_STATE_PLAYING && current_state == GST_STATE_PAUSED) {
SpotifyService* spotify = InternetModel::Service<SpotifyService>();
// Need to schedule this in the spotify service's thread
QMetaObject::invokeMethod(spotify, "SetPaused", Qt::QueuedConnection,
Q_ARG(bool, false));
}
}
return ConcurrentRun::Run<GstStateChangeReturn, GstElement*, GstState>(
&set_state_threadpool_, &gst_element_set_state, pipeline_, state);
}
@ -929,6 +945,18 @@ bool GstEnginePipeline::Seek(qint64 nanosec) {
return true;
}
if (url_.scheme() == "spotify" && !buffering_) {
SpotifyService* spotify = InternetModel::Service<SpotifyService>();
// Need to schedule this in the spotify service's thread
QMetaObject::invokeMethod(spotify, "Seek", Qt::QueuedConnection,
Q_ARG(int, nanosec / kNsecPerMsec));
// Need to reset spotify_offset_ to get the real pipeline position, as it is
// used in position()
spotify_offset_ = 0;
spotify_offset_ = nanosec - position() ;
return true;
}
if (!pipeline_is_connected_ || !pipeline_is_initialised_) {
pending_seek_nanosec_ = nanosec;
return true;
@ -939,6 +967,15 @@ bool GstEnginePipeline::Seek(qint64 nanosec) {
GST_SEEK_FLAG_FLUSH, nanosec);
}
void GstEnginePipeline::SpotifySeekCompleted() {
qLog(Debug) << "Spotify Seek completed";
// FIXME: we should clear buffers to start playing data from seek point right
// now (currently there is small delay) but I didn't managed to tell gstreamer
// to do this without breaking the streaming completely...
// Funny thing to notice: for me the delay varies when changing buffer size,
// but a larger buffer doesn't necessary increase the delay.
}
void GstEnginePipeline::SetEqualizerEnabled(bool enabled) {
eq_enabled_ = enabled;
UpdateEqualizer();

View File

@ -163,6 +163,7 @@ signals:
private slots:
void FaderTimelineFinished();
void SpotifySeekCompleted();
private:
static const int kGstStateTimeoutNanosecs;
@ -228,6 +229,13 @@ signals:
// past this position.
qint64 end_offset_nanosec_;
// Another Spotify hack...
// Used in position(). We need this because when seeking Spotify tracks, we
// don't actually seek the pipeline, but ask libspotify to send us data with
// a seek offset instead. So querying the pipeline to get track's position
// wouldn't make sense.
qint64 spotify_offset_;
// We store the beginning and end for the preloading song too, so we can just
// carry on without reloading the file if the sections carry on from each
// other.

View File

@ -236,8 +236,28 @@ void GlobalSearchModel::GetChildResults(
GetChildResults(itemFromIndex(index), results, visited);
}
} else {
// No - it's a song, add its result
results->append(item->data(Role_Result).value<SearchProvider::Result>());
// No - maybe it's a song, add its result if valid
QVariant result = item->data(Role_Result);
if (result.isValid()) {
results->append(result.value<SearchProvider::Result>());
} else {
// Maybe it's a provider then?
bool is_provider;
const int sort_index = item->data(Role_ProviderIndex).toInt(&is_provider);
if (is_provider) {
// Go through all the items (through the proxy to keep them ordered) and
// add the ones belonging to this provider to our list
for (int i = 0; i < proxy_->rowCount(invisibleRootItem()->index()); ++i) {
QModelIndex child_index =
proxy_->index(i, 0, invisibleRootItem()->index());
const QStandardItem* child_item =
itemFromIndex(proxy_->mapToSource(child_index));
if (child_item->data(Role_ProviderIndex).toInt() == sort_index) {
GetChildResults(child_item, results, visited);
}
}
}
}
}
}

View File

@ -1,4 +1,6 @@
#include "seafileservice.h"
#include "seafileservice.h"
#include <cmath>
#include <qjson/parser.h>
#include <QTimer>
@ -550,7 +552,7 @@ bool SeafileService::CheckReply(QNetworkReply** reply, int tries) {
seconds_to_wait =
((*reply)->rawHeader("X-Throttle-Wait-Seconds").toInt() + 1) * 1000;
} else {
seconds_to_wait = pow(tries, 2) * 1000;
seconds_to_wait = std::pow(tries, 2) * 1000;
}
QTimer timer;

View File

@ -495,10 +495,14 @@ Song SoundCloudService::ExtractSong(const QVariantMap& result_song) {
QVariant cover = result_song["artwork_url"];
if (cover.isValid()) {
// Increase cover size.
// See https://developers.soundcloud.com/docs/api/reference#artwork_url
QString big_cover = cover.toString().replace("large", "t500x500");
QUrl cover_url(big_cover, QUrl::StrictMode);
// SoundCloud covers URL are https, but our cover loader doesn't seem to
// deal well with https URL. Anyway, we don't need a secure connection to
// get a cover image.
QUrl cover_url = cover.toUrl();
cover_url.setScheme("http");
song.set_art_automatic(cover_url.toEncoded());
}

View File

@ -154,6 +154,8 @@ void SpotifyServer::MessageArrived(const pb::spotify::Message& message) {
emit AlbumBrowseResults(message.browse_album_response());
} else if (message.has_browse_toplist_response()) {
emit ToplistBrowseResults(message.browse_toplist_response());
} else if (message.has_seek_completed()) {
emit SeekCompleted();
}
}
@ -265,3 +267,10 @@ void SpotifyServer::LoadToplist() {
SendOrQueueMessage(message);
}
void SpotifyServer::SetPaused(const bool paused) {
pb::spotify::Message message;
pb::spotify::PauseRequest* req = message.mutable_pause_request();
req->set_paused(paused);
SendOrQueueMessage(message);
}

View File

@ -50,6 +50,7 @@ class SpotifyServer : public AbstractMessageHandler<pb::spotify::Message> {
void SetPlaybackSettings(pb::spotify::Bitrate bitrate,
bool volume_normalisation);
void LoadToplist();
void SetPaused(const bool paused);
int server_port() const;
@ -71,6 +72,7 @@ signals:
void SyncPlaylistProgress(const pb::spotify::SyncPlaylistProgress& progress);
void AlbumBrowseResults(const pb::spotify::BrowseAlbumResponse& response);
void ToplistBrowseResults(const pb::spotify::BrowseToplistResponse& response);
void SeekCompleted();
protected:
void MessageArrived(const pb::spotify::Message& message);

View File

@ -528,10 +528,6 @@ void SpotifyService::SongFromProtobuf(const pb::spotify::Track& track,
song->set_filesize(0);
}
PlaylistItem::Options SpotifyService::playlistitem_options() const {
return PlaylistItem::PauseDisabled | PlaylistItem::SeekDisabled;
}
QWidget* SpotifyService::HeaderWidget() const {
if (IsLoggedIn()) return search_box_;
return nullptr;
@ -697,6 +693,16 @@ void SpotifyService::LoadImage(const QString& id) {
server_->LoadImage(id);
}
void SpotifyService::SetPaused(const bool paused) {
EnsureServerCreated();
server_->SetPaused(paused);
}
void SpotifyService::Seek(const int offset /* in msec */) {
EnsureServerCreated();
server_->Seek(offset);
}
void SpotifyService::SyncPlaylistProgress(
const pb::spotify::SyncPlaylistProgress& progress) {
qLog(Debug) << "Sync progress:" << progress.sync_progress();

View File

@ -53,12 +53,13 @@ class SpotifyService : public InternetService {
void ShowContextMenu(const QPoint& global_pos);
void ItemDoubleClicked(QStandardItem* item);
void DropMimeData(const QMimeData* data, const QModelIndex& index);
PlaylistItem::Options playlistitem_options() const;
QWidget* HeaderWidget() const;
void Logout();
void Login(const QString& username, const QString& password);
Q_INVOKABLE void LoadImage(const QString& id);
Q_INVOKABLE void SetPaused(const bool paused);
Q_INVOKABLE void Seek(const int offset /* in msec */);
SpotifyServer* server() const;

View File

@ -374,8 +374,18 @@ void SubsonicLibraryScanner::OnGetAlbumListFinished(QNetworkReply* reply,
reader.readNextStartElement();
Q_ASSERT(reader.name() == "subsonic-response");
if (reader.attributes().value("status") != "ok") {
// TODO: error handling
return;
reader.readNextStartElement();
int error = reader.attributes().value("code").toString().toInt();
// Compatibility with Ampache :
// When there is no data, Ampache returns NotFound
// whereas Subsonic returns empty albumList2 tag
switch (error) {
case SubsonicService::ApiError_NotFound:
break;
default:
return;
}
}
int albums_added = 0;

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

@ -202,6 +202,8 @@ void SetGstreamerEnvironment() {
SetEnv("GIO_EXTRA_MODULES",
QCoreApplication::applicationDirPath() + "/../PlugIns/gio-modules");
#endif
SetEnv("PULSE_PROP_media.role", "music");
}
void ParseAProto() {

View File

@ -356,7 +356,7 @@ QPixmap MoodbarProxyStyle::MoodbarPixmap(const ColorVector& colors,
// Draw the outer bit
p.setPen(QPen(palette.brush(QPalette::Active, QPalette::Background),
kMarginSize, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
// First: a rectangle around the slier
// First: a rectangle around the slider
p.drawRect(rect.adjusted(1, 1, -2, -2));
// Then, thicker border on left and right, because of the margins.
p.setPen(QPen(palette.brush(QPalette::Active, QPalette::Background),

View File

@ -19,6 +19,7 @@
#include <QCoreApplication>
#include <QNetworkReply>
#include <QStringList>
#include <qjson/parser.h>
@ -45,7 +46,7 @@ void AcoustidClient::Start(int id, const QString& fingerprint,
QList<Param> parameters;
parameters << Param("format", "json") << Param("client", kClientId)
<< Param("duration", QString::number(duration_msec / kMsecPerSec))
<< Param("meta", "recordingids")
<< Param("meta", "recordingids+sources")
<< Param("fingerprint", fingerprint);
QUrl url(kUrl);
@ -67,13 +68,29 @@ void AcoustidClient::CancelAll() {
requests_.clear();
}
void AcoustidClient::RequestFinished(QNetworkReply* reply, int id) {
namespace {
// Struct used when extracting results in RequestFinished
struct IdSource {
IdSource(const QString& id, int source)
: id_(id), nb_sources_(source) {}
bool operator<(const IdSource& other) const {
// We want the items with more sources to be at the beginning of the list
return nb_sources_ > other.nb_sources_;
}
QString id_;
int nb_sources_;
};
}
void AcoustidClient::RequestFinished(QNetworkReply* reply, int request_id) {
reply->deleteLater();
requests_.remove(id);
requests_.remove(request_id);
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() !=
200) {
emit Finished(id, QString());
emit Finished(request_id, QStringList());
return;
}
@ -81,16 +98,26 @@ void AcoustidClient::RequestFinished(QNetworkReply* reply, int id) {
bool ok = false;
QVariantMap result = parser.parse(reply, &ok).toMap();
if (!ok) {
emit Finished(id, QString());
emit Finished(request_id, QStringList());
return;
}
QString status = result["status"].toString();
if (status != "ok") {
emit Finished(id, QString());
emit Finished(request_id, QStringList());
return;
}
// Get the results:
// -in a first step, gather ids and their corresponding number of sources
// -then sort results by number of sources (the results are originally
// unsorted but results with more sources are likely to be more accurate)
// -keep only the ids, as sources where useful only to sort the results
QVariantList results = result["results"].toList();
// List of <id, nb of sources> pairs
QList<IdSource> id_source_list;
for (const QVariant& v : results) {
QVariantMap r = v.toMap();
if (r.contains("recordings")) {
@ -98,12 +125,18 @@ void AcoustidClient::RequestFinished(QNetworkReply* reply, int id) {
for (const QVariant& recording : recordings) {
QVariantMap o = recording.toMap();
if (o.contains("id")) {
emit Finished(id, o["id"].toString());
return;
id_source_list << IdSource(o["id"].toString(), o["sources"].toInt());
}
}
}
}
emit Finished(id, QString());
qStableSort(id_source_list);
QList<QString> id_list;
for (const IdSource& is : id_source_list) {
id_list << is.id_;
}
emit Finished(request_id, id_list);
}

View File

@ -56,7 +56,7 @@ class AcoustidClient : public QObject {
void CancelAll();
signals:
void Finished(int id, const QString& mbid);
void Finished(int id, const QStringList& mbid_list);
private slots:
void RequestFinished(QNetworkReply* reply, int id);

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)
@ -40,22 +41,30 @@ MusicBrainzClient::MusicBrainzClient(QObject* parent,
network_(network ? network : new NetworkAccessManager(this)),
timeouts_(new NetworkTimeouts(kDefaultTimeout, this)) {}
void MusicBrainzClient::Start(int id, const QString& mbid) {
void MusicBrainzClient::Start(int id, const QStringList& mbid_list) {
typedef QPair<QString, QString> Param;
QList<Param> parameters;
parameters << Param("inc", "artists+releases+media");
int request_number = 0;
for (const QString& mbid : mbid_list) {
QList<Param> parameters;
parameters << Param("inc", "artists+releases+media");
QUrl url(kTrackUrl + mbid);
url.setQueryItems(parameters);
QNetworkRequest req(url);
QUrl url(kTrackUrl + mbid);
url.setQueryItems(parameters);
QNetworkRequest req(url);
QNetworkReply* reply = network_->get(req);
NewClosure(reply, SIGNAL(finished()), this,
SLOT(RequestFinished(QNetworkReply*, int)), reply, id);
requests_[id] = reply;
QNetworkReply* reply = network_->get(req);
NewClosure(reply, SIGNAL(finished()), this,
SLOT(RequestFinished(QNetworkReply*, int, int)),
reply, id, request_number++);
requests_.insert(id, reply);
timeouts_->AddReply(reply);
timeouts_->AddReply(reply);
if (request_number >= kMaxRequestPerTrack) {
break;
}
}
}
void MusicBrainzClient::StartDiscIdRequest(const QString& discid) {
@ -138,34 +147,51 @@ 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) {
void MusicBrainzClient::RequestFinished(QNetworkReply* reply, int id, int request_number) {
reply->deleteLater();
requests_.remove(id);
ResultList ret;
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() !=
200) {
emit Finished(id, ret);
return;
const int nb_removed = requests_.remove(id, reply);
if (nb_removed != 1) {
qLog(Error) << "Error: unknown reply received:" << nb_removed <<
"requests removed, while only one was supposed to be removed";
}
QXmlStreamReader reader(reply);
while (!reader.atEnd()) {
if (reader.readNext() == QXmlStreamReader::StartElement &&
reader.name() == "recording") {
ResultList tracks = ParseTrack(&reader);
for (const Result& track : tracks) {
if (!track.title_.isEmpty()) {
ret << track;
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() ==
200) {
QXmlStreamReader reader(reply);
ResultList res;
while (!reader.atEnd()) {
if (reader.readNext() == QXmlStreamReader::StartElement &&
reader.name() == "recording") {
ResultList tracks = ParseTrack(&reader);
for (const Result& track : tracks) {
if (!track.title_.isEmpty()) {
res << track;
}
}
}
}
pending_results_[id] << PendingResults(request_number, res);
} else {
qLog(Error) << "Error:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() <<
"http status code received";
qLog(Error) << reply->readAll();
}
emit Finished(id, UniqueResults(ret));
// No more pending requests for this id: emit the results we have.
if (!requests_.contains(id)) {
// Merge the results we have
ResultList ret;
QList<PendingResults> result_list_list = pending_results_.take(id);
qSort(result_list_list);
for (const PendingResults& result_list : result_list_list) {
ret << result_list.results_;
}
emit Finished(id, UniqueResults(ret, KeepOriginalOrder));
}
}
bool MusicBrainzClient::MediumHasDiscid(const QString& discid,
@ -327,8 +353,22 @@ MusicBrainzClient::Release MusicBrainzClient::ParseRelease(
}
MusicBrainzClient::ResultList MusicBrainzClient::UniqueResults(
const ResultList& results) {
ResultList ret = QSet<Result>::fromList(results).toList();
qSort(ret);
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

@ -19,7 +19,7 @@
#define MUSICBRAINZCLIENT_H
#include <QHash>
#include <QMap>
#include <QMultiMap>
#include <QObject>
#include <QXmlStreamReader>
@ -78,7 +78,7 @@ class MusicBrainzClient : public QObject {
// Starts a request and returns immediately. Finished() will be emitted
// later with the same ID.
void Start(int id, const QString& mbid);
void Start(int id, const QStringList& mbid);
void StartDiscIdRequest(const QString& discid);
// Cancels the request with the given ID. Finished() will never be emitted
@ -97,10 +97,18 @@ signals:
const MusicBrainzClient::ResultList& result);
private slots:
void RequestFinished(QNetworkReply* reply, int id);
// id identifies the track, and request_number means it's the
// 'request_number'th request for this track
void RequestFinished(QNetworkReply* reply, int id, int request_number);
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) {}
@ -117,23 +125,40 @@ signals:
int year_;
};
struct PendingResults {
PendingResults(int sort_id, const ResultList& results)
: sort_id_(sort_id), results_(results) {}
bool operator<(const PendingResults& other) const {
return sort_id_ < other.sort_id_;
}
int sort_id_;
ResultList results_;
};
static bool MediumHasDiscid(const QString& discid, QXmlStreamReader* reader);
static ResultList ParseMedium(QXmlStreamReader* reader);
static Result ParseTrackFromDisc(QXmlStreamReader* reader);
static ResultList ParseTrack(QXmlStreamReader* reader);
static void ParseArtist(QXmlStreamReader* reader, QString* artist);
static Release ParseRelease(QXmlStreamReader* reader);
static ResultList UniqueResults(const 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_;
QMap<int, QNetworkReply*> requests_;
QMultiMap<int, QNetworkReply*> requests_;
// Results we received so far, kept here until all the replies are finished
QMap<int, QList<PendingResults>> pending_results_;
};
inline uint qHash(const MusicBrainzClient::Result& result) {

View File

@ -32,8 +32,8 @@ TagFetcher::TagFetcher(QObject* parent)
fingerprint_watcher_(nullptr),
acoustid_client_(new AcoustidClient(this)),
musicbrainz_client_(new MusicBrainzClient(this)) {
connect(acoustid_client_, SIGNAL(Finished(int, QString)),
SLOT(PuidFound(int, QString)));
connect(acoustid_client_, SIGNAL(Finished(int, QStringList)),
SLOT(PuidsFound(int, QStringList)));
connect(musicbrainz_client_,
SIGNAL(Finished(int, MusicBrainzClient::ResultList)),
SLOT(TagsFetched(int, MusicBrainzClient::ResultList)));
@ -92,20 +92,20 @@ void TagFetcher::FingerprintFound(int index) {
song.length_nanosec() / kNsecPerMsec);
}
void TagFetcher::PuidFound(int index, const QString& puid) {
void TagFetcher::PuidsFound(int index, const QStringList& puid_list) {
if (index >= songs_.count()) {
return;
}
const Song& song = songs_[index];
if (puid.isEmpty()) {
if (puid_list.isEmpty()) {
emit ResultAvailable(song, SongList());
return;
}
emit Progress(song, tr("Downloading metadata"));
musicbrainz_client_->Start(index, puid);
musicbrainz_client_->Start(index, puid_list);
}
void TagFetcher::TagsFetched(int index,

View File

@ -47,7 +47,7 @@ signals:
private slots:
void FingerprintFound(int index);
void PuidFound(int index, const QString& puid);
void PuidsFound(int index, const QStringList& puid_list);
void TagsFetched(int index, const MusicBrainzClient::ResultList& result);
private:

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) {
@ -1984,6 +1989,7 @@ void Playlist::TracksDequeued() {
emit dataChanged(index, index);
}
temp_dequeue_change_indexes_.clear();
emit QueueChanged();
}
void Playlist::TracksEnqueued(const QModelIndex&, int begin, int end) {
@ -2103,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);
@ -333,6 +344,10 @@ signals:
void LoadTracksError(const QString& message);
// Signals that the queue has changed, meaning that the remaining queued
// items should update their position.
void QueueChanged();
private:
void SetCurrentIsPaused(bool paused);
void UpdateScrobblePoint();

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,9 +392,16 @@ void PlaylistContainer::UpdateNoMatchesLabel() {
QString text;
if (has_rows && !has_results) {
text =
tr("No matches found. Clear the search box to show the whole playlist "
"again.");
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.");
}
}
if (!text.isEmpty()) {
@ -432,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

@ -256,6 +256,7 @@ void PlaylistView::SetPlaylist(Playlist* playlist) {
disconnect(playlist_, SIGNAL(DynamicModeChanged(bool)), this,
SLOT(DynamicModeChanged(bool)));
disconnect(playlist_, SIGNAL(destroyed()), this, SLOT(PlaylistDestroyed()));
disconnect(playlist_, SIGNAL(QueueChanged()), this, SLOT(update()));
disconnect(dynamic_controls_, SIGNAL(Expand()), playlist_,
SLOT(ExpandDynamicPlaylist()));
@ -273,11 +274,12 @@ void PlaylistView::SetPlaylist(Playlist* playlist) {
read_only_settings_ = false;
connect(playlist_, SIGNAL(RestoreFinished()), SLOT(JumpToLastPlayedTrack()));
connect(playlist_, SIGNAL(CurrentSongChanged(Song)), SLOT(MaybeAutoscroll()));
connect(playlist_, SIGNAL(DynamicModeChanged(bool)),
SLOT(DynamicModeChanged(bool)));
connect(playlist_, SIGNAL(destroyed()), SLOT(PlaylistDestroyed()));
connect(playlist_, SIGNAL(QueueChanged()), SLOT(update()));
connect(dynamic_controls_, SIGNAL(Expand()), playlist_,
SLOT(ExpandDynamicPlaylist()));
connect(dynamic_controls_, SIGNAL(Repopulate()), playlist_,
@ -727,7 +729,7 @@ void PlaylistView::leaveEvent(QEvent* e) {
}
void PlaylistView::RatingHoverIn(const QModelIndex& index, const QPoint& pos) {
if (!(editTriggers() & QAbstractItemView::SelectedClicked)) {
if (editTriggers() & QAbstractItemView::NoEditTriggers) {
return;
}
@ -750,7 +752,7 @@ void PlaylistView::RatingHoverIn(const QModelIndex& index, const QPoint& pos) {
}
void PlaylistView::RatingHoverOut() {
if (!(editTriggers() & QAbstractItemView::SelectedClicked)) {
if (editTriggers() & QAbstractItemView::NoEditTriggers) {
return;
}
@ -771,7 +773,7 @@ void PlaylistView::RatingHoverOut() {
}
void PlaylistView::mousePressEvent(QMouseEvent* event) {
if (!(editTriggers() & QAbstractItemView::SelectedClicked)) {
if (editTriggers() & QAbstractItemView::NoEditTriggers) {
QTreeView::mousePressEvent(event);
return;
}
@ -1109,6 +1111,11 @@ void PlaylistView::ReloadSettings() {
emit BackgroundPropertyChanged();
force_background_redraw_ = true;
}
if(!s.value("click_edit_inline", true).toBool())
setEditTriggers(editTriggers() & ~QAbstractItemView::SelectedClicked);
else
setEditTriggers(editTriggers() | QAbstractItemView::SelectedClicked);
}
void PlaylistView::SaveSettings() {

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

Some files were not shown because too many files have changed in this diff Show More