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 + QLatin1Char('/') + socketName
+ QLatin1String("-lockfile"); + QLatin1String("-lockfile");
lockFile.setFileName(lockName); lockFile.setFileName(lockName);
lockFileCreated = !lockFile.exists();
lockFile.open(QIODevice::ReadWrite); lockFile.open(QIODevice::ReadWrite);
} }
@ -212,5 +213,6 @@ void QtLocalPeer::receiveConnection()
QtLocalPeer::~QtLocalPeer () QtLocalPeer::~QtLocalPeer ()
{ {
lockFile.remove(); if (lockFileCreated)
lockFile.remove();
} }

View File

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

View File

@ -210,6 +210,29 @@ IntReply *AudioProvider::removeFromLibrary(int aid, int oid)
return reply; 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) AudioItemListReply *AudioProvider::getAudiosByIds(const QString &ids)
{ {
Q_D(AudioProvider); Q_D(AudioProvider);

View File

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

View File

@ -122,6 +122,16 @@ void ReplyPrivate::_q_network_reply_error(QNetworkReply::NetworkError code)
emit q->resultReady(response); 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) QVariant MessageListHandler::operator()(const QVariant &response)
{ {

View File

@ -50,15 +50,16 @@ public:
void _q_reply_finished(); void _q_reply_finished();
void _q_network_reply_error(QNetworkReply::NetworkError); 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 { struct MessageListHandler {
MessageListHandler(int clientId) : clientId(clientId) {} MessageListHandler(int clientId) : clientId(clientId) {}
QVariant operator()(const QVariant &response); QVariant operator()(const QVariant &response);
int clientId; int clientId;
}; };
} //namespace Vreen } //namespace Vreen

View File

@ -21,6 +21,13 @@ if (APPLE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --stdlib=libc++") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --stdlib=libc++")
endif () 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) if (UNIX AND NOT APPLE)
set(LINUX 1) set(LINUX 1)
endif (UNIX AND NOT APPLE) endif (UNIX AND NOT APPLE)

View File

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

View File

@ -59,6 +59,13 @@
</extract> </extract>
<invalidIndicator value="Verifique se o nome do seu arquivo e sua"/> <invalidIndicator value="Verifique se o nome do seu arquivo e sua"/>
</provider> </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}"> <provider name="loudson.gs" title="" charset="utf-8" url="http://www.loudson.gs/{a}/{artist}/{album}/{title}">
<urlFormat replace=" _@,;&amp;\/&quot;" with="-"/> <urlFormat replace=" _@,;&amp;\/&quot;" with="-"/>
<urlFormat replace="." 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 // Try to send 5 seconds of audio in advance to initially fill Clementine's
// buffer. // buffer.
g_object_set(G_OBJECT(tcpsink_), "ts-offset", qint64(-5 * kNsecPerSec), // Commented for now as otherwise the seek will take too long.
nullptr); //g_object_set(G_OBJECT(tcpsink_), "ts-offset", qint64(-5 * kNsecPerSec),
// nullptr);
// We know the time of each buffer // We know the time of each buffer
g_object_set(G_OBJECT(appsrc_), "format", GST_FORMAT_TIME, nullptr); 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()); SetPlaybackSettings(message.set_playback_settings_request());
} else if (message.has_browse_toplist_request()) { } else if (message.has_browse_toplist_request()) {
BrowseToplist(message.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) { 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; return 0;
} }
@ -840,8 +845,16 @@ void SpotifyClient::StartPlayback(const pb::spotify::PlaybackRequest& req) {
} }
void SpotifyClient::Seek(qint64 offset_bytes) { void SpotifyClient::Seek(qint64 offset_bytes) {
// TODO if (sp_session_player_seek(session_, offset_bytes) != SP_ERROR_OK) {
qLog(Error) << "TODO seeking"; 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) { void SpotifyClient::TryPlaybackAgain(const PendingPlaybackRequest& req) {
@ -1017,6 +1030,10 @@ void SpotifyClient::BrowseToplist(
pending_toplist_browses_[browse] = req; 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 SpotifyClient::ToplistBrowseComplete(sp_toplistbrowse* result,
void* userdata) { void* userdata) {
SpotifyClient* me = reinterpret_cast<SpotifyClient*>(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); pb::spotify::LoginResponse_Error error_code);
void SendPlaybackError(const QString& error); void SendPlaybackError(const QString& error);
void SendSearchResponse(sp_search* result); void SendSearchResponse(sp_search* result);
void SendSeekCompleted();
// Spotify session callbacks. // Spotify session callbacks.
static void SP_CALLCONV LoggedInCallback(sp_session* session, sp_error error); 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 BrowseAlbum(const QString& uri);
void BrowseToplist(const pb::spotify::BrowseToplistRequest& req); void BrowseToplist(const pb::spotify::BrowseToplistRequest& req);
void SetPlaybackSettings(const pb::spotify::PlaybackSettings& req); void SetPlaybackSettings(const pb::spotify::PlaybackSettings& req);
void SetPaused(const pb::spotify::PauseRequest& req);
void SendPlaylistList(); void SendPlaylistList();

View File

@ -173,6 +173,9 @@ message SeekRequest {
optional int64 offset_bytes = 1; optional int64 offset_bytes = 1;
} }
message SeekCompleted {
}
enum Bitrate { enum Bitrate {
Bitrate96k = 1; Bitrate96k = 1;
Bitrate160k = 2; Bitrate160k = 2;
@ -184,7 +187,11 @@ message PlaybackSettings {
optional bool volume_normalisation = 2 [default = false]; optional bool volume_normalisation = 2 [default = false];
} }
// NEXT_ID: 21 message PauseRequest {
optional bool paused = 1 [default = false];
}
// NEXT_ID: 23
message Message { message Message {
// Not currently used // Not currently used
optional int32 id = 18; optional int32 id = 18;
@ -208,4 +215,6 @@ message Message {
optional PlaybackSettings set_playback_settings_request = 17; optional PlaybackSettings set_playback_settings_request = 17;
optional BrowseToplistRequest browse_toplist_request = 19; optional BrowseToplistRequest browse_toplist_request = 19;
optional BrowseToplistResponse browse_toplist_response = 20; 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) if(HAVE_DBUS)
file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/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 # MPRIS DBUS interfaces
qt4_add_dbus_adaptor(SOURCES qt4_add_dbus_adaptor(SOURCES
dbus/org.freedesktop.MediaPlayer.player.xml dbus/org.freedesktop.MediaPlayer.player.xml
@ -964,6 +959,10 @@ if(HAVE_DBUS)
# DeviceKit DBUS interfaces # DeviceKit DBUS interfaces
if(HAVE_DEVICEKIT) 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 qt4_add_dbus_interface(SOURCES
dbus/org.freedesktop.UDisks.xml dbus/org.freedesktop.UDisks.xml
dbus/udisks) 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 // roof pixmaps don't depend on size() so we do in the ctor
m_bg = parent->palette().color(QPalette::Background); 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()) / double dr = double(m_bg.red() - fg.red()) /
(NUM_ROOFS - 1); //-1 because we start loop below at 0 (NUM_ROOFS - 1); //-1 because we start loop below at 0
@ -69,7 +69,9 @@ void BarAnalyzer::init() {
canvas_.fill(palette().color(QPalette::Background)); canvas_.fill(palette().color(QPalette::Background));
QPainter p(&m_pixBarGradient); 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) { ++x) {
for (int y = x; y > 0; --y) { for (int y = x; y > 0; --y) {
const double fraction = (double)y / height(); const double fraction = (double)y / height();

View File

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

View File

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

View File

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

View File

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

View File

@ -66,6 +66,7 @@ GstEnginePipeline::GstEnginePipeline(GstEngine* engine)
buffering_(false), buffering_(false),
mono_playback_(false), mono_playback_(false),
end_offset_nanosec_(-1), end_offset_nanosec_(-1),
spotify_offset_(0),
next_beginning_offset_nanosec_(-1), next_beginning_offset_nanosec_(-1),
next_end_offset_nanosec_(-1), next_end_offset_nanosec_(-1),
ignore_next_seek_(false), ignore_next_seek_(false),
@ -93,6 +94,10 @@ GstEnginePipeline::GstEnginePipeline(GstEngine* engine)
} }
for (int i = 0; i < kEqBandCount; ++i) eq_band_gains_ << 0; 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, 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. // Tell spotify to start sending data to us.
InternetModel::Service<SpotifyService>()->server()->StartPlaybackLater( InternetModel::Service<SpotifyService>()->server()->StartPlaybackLater(
url.toString(), port); url.toString(), port);
spotify_offset_ = 0;
} else { } else {
new_bin = engine_->CreateElement("uridecodebin"); new_bin = engine_->CreateElement("uridecodebin");
g_object_set(G_OBJECT(new_bin), "uri", url.toEncoded().constData(), 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")) { instance->url().host().contains("grooveshark")) {
// Grooveshark streaming servers will answer with a 400 error 'Bad request' // Grooveshark streaming servers will answer with a 400 error 'Bad request'
// if we don't specify 'Range' field in HTTP header. // 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. // keep this grooveshark specific.
GstStructure* headers; GstStructure* headers;
headers = gst_structure_new("extra-headers", "Range", G_TYPE_STRING, headers = gst_structure_new("extra-headers", "Range", G_TYPE_STRING,
@ -848,17 +854,6 @@ void GstEnginePipeline::SourceSetupCallback(GstURIDecodeBin* bin,
gst_structure_free(headers); 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")) { if (g_object_class_find_property(G_OBJECT_GET_CLASS(element), "user-agent")) {
QString user_agent = QString user_agent =
QString("%1 %2").arg(QCoreApplication::applicationName(), QString("%1 %2").arg(QCoreApplication::applicationName(),
@ -899,6 +894,10 @@ qint64 GstEnginePipeline::position() const {
gint64 value = 0; gint64 value = 0;
gst_element_query_position(pipeline_, GST_FORMAT_TIME, &value); gst_element_query_position(pipeline_, GST_FORMAT_TIME, &value);
if (url_.scheme() == "spotify") {
value += spotify_offset_;
}
return value; return value;
} }
@ -919,6 +918,23 @@ GstState GstEnginePipeline::state() const {
} }
QFuture<GstStateChangeReturn> GstEnginePipeline::SetState(GstState state) { 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>( return ConcurrentRun::Run<GstStateChangeReturn, GstElement*, GstState>(
&set_state_threadpool_, &gst_element_set_state, pipeline_, state); &set_state_threadpool_, &gst_element_set_state, pipeline_, state);
} }
@ -929,6 +945,18 @@ bool GstEnginePipeline::Seek(qint64 nanosec) {
return true; 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_) { if (!pipeline_is_connected_ || !pipeline_is_initialised_) {
pending_seek_nanosec_ = nanosec; pending_seek_nanosec_ = nanosec;
return true; return true;
@ -939,6 +967,15 @@ bool GstEnginePipeline::Seek(qint64 nanosec) {
GST_SEEK_FLAG_FLUSH, 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) { void GstEnginePipeline::SetEqualizerEnabled(bool enabled) {
eq_enabled_ = enabled; eq_enabled_ = enabled;
UpdateEqualizer(); UpdateEqualizer();

View File

@ -163,6 +163,7 @@ signals:
private slots: private slots:
void FaderTimelineFinished(); void FaderTimelineFinished();
void SpotifySeekCompleted();
private: private:
static const int kGstStateTimeoutNanosecs; static const int kGstStateTimeoutNanosecs;
@ -228,6 +229,13 @@ signals:
// past this position. // past this position.
qint64 end_offset_nanosec_; 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 // 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 // carry on without reloading the file if the sections carry on from each
// other. // other.

View File

@ -236,8 +236,28 @@ void GlobalSearchModel::GetChildResults(
GetChildResults(itemFromIndex(index), results, visited); GetChildResults(itemFromIndex(index), results, visited);
} }
} else { } else {
// No - it's a song, add its result // No - maybe it's a song, add its result if valid
results->append(item->data(Role_Result).value<SearchProvider::Result>()); 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 <qjson/parser.h>
#include <QTimer> #include <QTimer>
@ -550,7 +552,7 @@ bool SeafileService::CheckReply(QNetworkReply** reply, int tries) {
seconds_to_wait = seconds_to_wait =
((*reply)->rawHeader("X-Throttle-Wait-Seconds").toInt() + 1) * 1000; ((*reply)->rawHeader("X-Throttle-Wait-Seconds").toInt() + 1) * 1000;
} else { } else {
seconds_to_wait = pow(tries, 2) * 1000; seconds_to_wait = std::pow(tries, 2) * 1000;
} }
QTimer timer; QTimer timer;

View File

@ -495,10 +495,14 @@ Song SoundCloudService::ExtractSong(const QVariantMap& result_song) {
QVariant cover = result_song["artwork_url"]; QVariant cover = result_song["artwork_url"];
if (cover.isValid()) { 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 // 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 // deal well with https URL. Anyway, we don't need a secure connection to
// get a cover image. // get a cover image.
QUrl cover_url = cover.toUrl();
cover_url.setScheme("http"); cover_url.setScheme("http");
song.set_art_automatic(cover_url.toEncoded()); 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()); emit AlbumBrowseResults(message.browse_album_response());
} else if (message.has_browse_toplist_response()) { } else if (message.has_browse_toplist_response()) {
emit ToplistBrowseResults(message.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); 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, void SetPlaybackSettings(pb::spotify::Bitrate bitrate,
bool volume_normalisation); bool volume_normalisation);
void LoadToplist(); void LoadToplist();
void SetPaused(const bool paused);
int server_port() const; int server_port() const;
@ -71,6 +72,7 @@ signals:
void SyncPlaylistProgress(const pb::spotify::SyncPlaylistProgress& progress); void SyncPlaylistProgress(const pb::spotify::SyncPlaylistProgress& progress);
void AlbumBrowseResults(const pb::spotify::BrowseAlbumResponse& response); void AlbumBrowseResults(const pb::spotify::BrowseAlbumResponse& response);
void ToplistBrowseResults(const pb::spotify::BrowseToplistResponse& response); void ToplistBrowseResults(const pb::spotify::BrowseToplistResponse& response);
void SeekCompleted();
protected: protected:
void MessageArrived(const pb::spotify::Message& message); 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); song->set_filesize(0);
} }
PlaylistItem::Options SpotifyService::playlistitem_options() const {
return PlaylistItem::PauseDisabled | PlaylistItem::SeekDisabled;
}
QWidget* SpotifyService::HeaderWidget() const { QWidget* SpotifyService::HeaderWidget() const {
if (IsLoggedIn()) return search_box_; if (IsLoggedIn()) return search_box_;
return nullptr; return nullptr;
@ -697,6 +693,16 @@ void SpotifyService::LoadImage(const QString& id) {
server_->LoadImage(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( void SpotifyService::SyncPlaylistProgress(
const pb::spotify::SyncPlaylistProgress& progress) { const pb::spotify::SyncPlaylistProgress& progress) {
qLog(Debug) << "Sync progress:" << progress.sync_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 ShowContextMenu(const QPoint& global_pos);
void ItemDoubleClicked(QStandardItem* item); void ItemDoubleClicked(QStandardItem* item);
void DropMimeData(const QMimeData* data, const QModelIndex& index); void DropMimeData(const QMimeData* data, const QModelIndex& index);
PlaylistItem::Options playlistitem_options() const;
QWidget* HeaderWidget() const; QWidget* HeaderWidget() const;
void Logout(); void Logout();
void Login(const QString& username, const QString& password); void Login(const QString& username, const QString& password);
Q_INVOKABLE void LoadImage(const QString& id); 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; SpotifyServer* server() const;

View File

@ -374,8 +374,18 @@ void SubsonicLibraryScanner::OnGetAlbumListFinished(QNetworkReply* reply,
reader.readNextStartElement(); reader.readNextStartElement();
Q_ASSERT(reader.name() == "subsonic-response"); Q_ASSERT(reader.name() == "subsonic-response");
if (reader.attributes().value("status") != "ok") { if (reader.attributes().value("status") != "ok") {
// TODO: error handling reader.readNextStartElement();
return; 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; int albums_added = 0;

View File

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

View File

@ -25,44 +25,40 @@
#include "core/taskmanager.h" #include "core/taskmanager.h"
VkMusicCache::VkMusicCache(Application* app, VkService* service) VkMusicCache::VkMusicCache(Application* app, VkService* service)
: QObject(service), : QObject(service),
app_(app), app_(app),
service_(service), service_(service),
current_cashing_index(0), current_song_index(0),
is_downloading(false), is_downloading(false),
is_aborted(false), is_aborted(false),
task_id(0), task_id(0),
file_(NULL), file_(NULL),
network_manager_(new QNetworkAccessManager), network_manager_(new QNetworkAccessManager),
reply_(NULL) { reply_(NULL) {}
}
QUrl VkMusicCache::Get(const QUrl& url) { QUrl VkMusicCache::Get(const QUrl& url) {
QString cached_filename = CachedFilename(url);
QUrl result; QUrl result;
if (InCache(cached_filename)) { if (InCache(url)) {
QString cached_filename = CachedFilename(url);
qLog(Info) << "Use cashed file" << cached_filename; qLog(Info) << "Use cashed file" << cached_filename;
result = QUrl::fromLocalFile(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; return result;
} }
void VkMusicCache::ForceCache(const QUrl& url) { void VkMusicCache::AddToCache(const QUrl& url, const QUrl& media_url,
AddToQueue(CachedFilename(url), service_->GetSongPlayUrl(url)); bool force) {
AddToQueue(CachedFilename(url), media_url);
if (!force) {
current_song_index = queue_.size();
}
} }
void VkMusicCache::BreakCurrentCaching() { void VkMusicCache::BreakCurrentCaching() {
if (current_cashing_index > 0) { if (current_song_index > 0) {
// Current song in queue // Current song in queue
queue_.removeAt(current_cashing_index - 1); queue_.removeAt(current_song_index - 1);
} else if (current_cashing_index == 0) { } else if (current_song_index == 0) {
// Current song is downloading // Current song is downloading
if (reply_) { if (reply_) {
reply_->abort(); reply_->abort();
@ -75,7 +71,8 @@ void VkMusicCache::BreakCurrentCaching() {
* Queue operations * Queue operations
*/ */
void VkMusicCache::AddToQueue(const QString& filename, const QUrl& download_url) { void VkMusicCache::AddToQueue(const QString& filename,
const QUrl& download_url) {
DownloadItem item; DownloadItem item;
item.filename = filename; item.filename = filename;
item.url = download_url; item.url = download_url;
@ -93,11 +90,12 @@ void VkMusicCache::DownloadNext() {
} else { } else {
current_download = queue_.first(); current_download = queue_.first();
queue_.pop_front(); queue_.pop_front();
current_cashing_index--; current_song_index--;
// Check file path and file existance first // Check file path and file existance first
if (QFile::exists(current_download.filename)) { 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; return;
} }
@ -117,14 +115,15 @@ void VkMusicCache::DownloadNext() {
// Start downloading // Start downloading
is_aborted = false; is_aborted = false;
is_downloading = true; is_downloading = true;
task_id = app_->task_manager()-> task_id = app_->task_manager()->StartTask(
StartTask(tr("Caching %1") tr("Caching %1").arg(QFileInfo(current_download.filename).baseName()));
.arg(QFileInfo(current_download.filename).baseName()));
reply_ = network_manager_->get(QNetworkRequest(current_download.url)); reply_ = network_manager_->get(QNetworkRequest(current_download.url));
connect(reply_, SIGNAL(finished()), SLOT(Downloaded())); connect(reply_, SIGNAL(finished()), SLOT(Downloaded()));
connect(reply_, SIGNAL(readyRead()), SLOT(DownloadReadyToRead())); connect(reply_, SIGNAL(readyRead()), SLOT(DownloadReadyToRead()));
connect(reply_, SIGNAL(downloadProgress(qint64, qint64)), SLOT(DownloadProgress(qint64, qint64))); connect(reply_, SIGNAL(downloadProgress(qint64, qint64)),
qLog(Info)<< "Start cashing" << current_download.filename << "from" << current_download.url; 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(); QString path = service_->cacheDir();
if (file_->size() > 0) { if (file_->size() > 0) {
QDir(path).mkpath(QFileInfo(current_download.filename).path()); QDir(path).mkpath(QFileInfo(current_download.filename).path());
if (file_->copy(current_download.filename)) { if (file_->copy(current_download.filename)) {
qLog(Info) << "Cached" << current_download.filename; qLog(Info) << "Cached" << current_download.filename;
} else { } else {
qLog(Error) << "Unable to save" << current_download.filename qLog(Error) << "Unable to save" << current_download.filename << ":"
<< ":" << file_->errorString(); << file_->errorString();
} }
} else { } else {
qLog(Error) << "File" << current_download.filename << "is empty"; qLog(Error) << "File" << current_download.filename << "is empty";
@ -181,12 +180,8 @@ void VkMusicCache::Downloaded() {
* Utils * Utils
*/ */
bool VkMusicCache::InCache(const QString& filename) {
return QFile::exists(filename);
}
bool VkMusicCache::InCache(const QUrl& url) { bool VkMusicCache::InCache(const QUrl& url) {
return InCache(CachedFilename(url)); return QFile::exists(CachedFilename(url));
} }
QString VkMusicCache::CachedFilename(const QUrl& 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("%artist", args[2]);
cache_filename.replace("%title", args[3]); cache_filename.replace("%title", args[3]);
} else { } 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."; << "use id as file name for cache.";
cache_filename = args[1]; cache_filename = args[1];
} }
@ -209,5 +205,5 @@ QString VkMusicCache::CachedFilename(const QUrl& url) {
return ""; return "";
} }
// TODO(Vk): Maybe use extenstion from link? Seems it's always mp3. // 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 { class VkMusicCache : public QObject {
Q_OBJECT Q_OBJECT
public: public:
explicit VkMusicCache(Application* app, VkService* service); explicit VkMusicCache(Application* app, VkService* service);
~VkMusicCache() {} ~VkMusicCache() {}
// Return file path if file in cache otherwise // Return file path if file in cache otherwise
// return internet url and add song to caching queue // return internet url and add song to caching queue
QUrl Get(const QUrl& url); QUrl Get(const QUrl& url);
void ForceCache(const QUrl& url); void AddToCache(const QUrl& url, const QUrl& media_url, bool force = false);
void BreakCurrentCaching(); void BreakCurrentCaching();
bool InCache(const QUrl& url); bool InCache(const QUrl& url);
private slots: private slots:
bool InCache(const QString& filename);
void AddToQueue(const QString& filename, const QUrl& download_url); void AddToQueue(const QString& filename, const QUrl& download_url);
void DownloadNext(); void DownloadNext();
void DownloadProgress(qint64 bytesReceived, qint64 bytesTotal); void DownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
void DownloadReadyToRead(); void DownloadReadyToRead();
void Downloaded(); void Downloaded();
private: private:
struct DownloadItem { struct DownloadItem {
QString filename; QString filename;
QUrl url; QUrl url;
bool operator ==(const DownloadItem& rhv) { bool operator==(const DownloadItem& rhv) {
return filename == rhv.filename; return filename == rhv.filename;
} }
}; };
@ -63,9 +62,10 @@ private:
Application* app_; Application* app_;
VkService* service_; VkService* service_;
QList<DownloadItem> queue_; QList<DownloadItem> queue_;
// Contain index of current song in queue, need for removing if song was skipped. // Contain index of current song in queue, need for removing if song was
// Is zero if song downloading now, and less that zero if current song not caching or cached. // skipped. It's zero if song downloading now, and less that zero
int current_cashing_index; // if current song not caching or cached.
int current_song_index;
DownloadItem current_download; DownloadItem current_download;
bool is_downloading; bool is_downloading;
bool is_aborted; bool is_aborted;

View File

@ -312,7 +312,7 @@ void VkService::EnsureMenuCreated() {
add_song_to_cache_ = context_menu_->addAction(QIcon(":vk/download.png"), add_song_to_cache_ = context_menu_->addAction(QIcon(":vk/download.png"),
tr("Add song to cache"), this, tr("Add song to cache"), this,
SLOT(AddToCache())); SLOT(AddSelectedToCache()));
copy_share_url_ = context_menu_->addAction( copy_share_url_ = context_menu_->addAction(
QIcon(":vk/link.png"), tr("Copy share url to clipboard"), this, 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>(); current.data(InternetModel::Role_SongMetadata).value<Song>();
is_in_mymusic = is_my_music_item || is_in_mymusic = is_my_music_item ||
ExtractIds(selected_song_.url()).owner_id == UserID(); 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); update_item_->setVisible(is_updatable);
@ -443,7 +443,7 @@ QList<QAction*> VkService::playlistitem_actions(const Song& song) {
copy_share_url_->setVisible(true); copy_share_url_->setVisible(true);
actions << copy_share_url_; actions << copy_share_url_;
if (!cache()->InCache(selected_song_.url())) { if (!cache_->InCache(selected_song_.url())) {
add_song_to_cache_->setVisible(true); add_song_to_cache_->setVisible(true);
actions << add_song_to_cache_; actions << add_song_to_cache_;
} }
@ -835,7 +835,7 @@ void VkService::AddToMyMusic() {
} }
void VkService::AddToMyMusicCurrent() { void VkService::AddToMyMusicCurrent() {
if (isLoveAddToMyMusic()) { if (isLoveAddToMyMusic() && current_song_.is_valid()) {
selected_song_ = current_song_; selected_song_ = current_song_;
AddToMyMusic(); AddToMyMusic();
} }
@ -852,8 +852,10 @@ void VkService::RemoveFromMyMusic() {
} }
} }
void VkService::AddToCache() { void VkService::AddSelectedToCache() {
url_handler_->ForceAddToCache(selected_song_.url()); QUrl selected_song_media_url =
GetAudioItemFromUrl(selected_song_.url()).url();
cache_->AddToCache(selected_song_.url(), selected_song_media_url, true);
} }
void VkService::CopyShareUrl() { void VkService::CopyShareUrl() {
@ -999,12 +1001,12 @@ SongList VkService::FromAudioList(const Vreen::AudioItemList& list) {
* Url handling * Url handling
*/ */
QUrl VkService::GetSongPlayUrl(const QUrl& url, bool is_playing) { Vreen::AudioItem VkService::GetAudioItemFromUrl(const QUrl& url) {
QStringList tokens = url.path().split('/'); QStringList tokens = url.path().split('/');
if (tokens.count() < 2) { if (tokens.count() < 2) {
qLog(Error) << "Wrong song url" << url; qLog(Error) << "Wrong song url" << url;
return QUrl(); return Vreen::AudioItem();
} }
QString song_id = tokens[1]; QString song_id = tokens[1];
@ -1016,17 +1018,35 @@ QUrl VkService::GetSongPlayUrl(const QUrl& url, bool is_playing) {
bool success = WaitForReply(song_request); bool success = WaitForReply(song_request);
if (success && !song_request->result().isEmpty()) { if (success && !song_request->result().isEmpty()) {
Vreen::AudioItem song = song_request->result()[0]; return song_request->result()[0];
if (is_playing) {
current_song_ = FromAudioItem(song);
current_song_.set_url(url);
}
return song.url();
} }
} }
qLog(Info) << "Unresolved url by id" << song_id; 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) { UrlHandler::LoadResult VkService::GetGroupNextSongUrl(const QUrl& url) {
@ -1054,7 +1074,7 @@ UrlHandler::LoadResult VkService::GetGroupNextSongUrl(const QUrl& url) {
if (success && !song_request->result().isEmpty()) { if (success && !song_request->result().isEmpty()) {
Vreen::AudioItem song = song_request->result()[0]; Vreen::AudioItem song = song_request->result()[0];
current_group_url_ = url; current_group_url_ = url;
current_song_ = FromAudioItem(song); SongStarting(FromAudioItem(song));
emit StreamMetadataFound(url, current_song_); emit StreamMetadataFound(url, current_song_);
return UrlHandler::LoadResult(url, UrlHandler::LoadResult::TrackAvailable, return UrlHandler::LoadResult(url, UrlHandler::LoadResult::TrackAvailable,
song.url(), current_song_.length_nanosec()); song.url(), current_song_.length_nanosec());
@ -1065,8 +1085,48 @@ UrlHandler::LoadResult VkService::GetGroupNextSongUrl(const QUrl& url) {
return UrlHandler::LoadResult(); 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(); cacheFilename_ = s.value("cache_filename", kDefCacheFilename).toString();
love_is_add_to_mymusic_ = s.value("love_is_add_to_my_music", false).toBool(); 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(); 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) { void VkService::ClearStandardItem(QStandardItem* item) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -464,7 +464,7 @@ QVariant LibraryModel::AlbumIcon(const QModelIndex& index) {
} }
// Try to load it from the disk cache // 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) { if (cache) {
QImage cached_pixmap; QImage cached_pixmap;
if (cached_pixmap.load(cache.get(), "XPM")) { 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 // 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) { if (!cached_img) {
QNetworkCacheMetaData item_metadata; QNetworkCacheMetaData item_metadata;
item_metadata.setSaveToDisk(true); item_metadata.setSaveToDisk(true);
@ -1073,6 +1073,10 @@ QString LibraryModel::SortTextForArtist(QString artist) {
if (artist.startsWith("the ")) { if (artist.startsWith("the ")) {
artist = artist.right(artist.length() - 4) + ", 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; return artist;

View File

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

View File

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

View File

@ -19,6 +19,7 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <QNetworkReply> #include <QNetworkReply>
#include <QStringList>
#include <qjson/parser.h> #include <qjson/parser.h>
@ -45,7 +46,7 @@ void AcoustidClient::Start(int id, const QString& fingerprint,
QList<Param> parameters; QList<Param> parameters;
parameters << Param("format", "json") << Param("client", kClientId) parameters << Param("format", "json") << Param("client", kClientId)
<< Param("duration", QString::number(duration_msec / kMsecPerSec)) << Param("duration", QString::number(duration_msec / kMsecPerSec))
<< Param("meta", "recordingids") << Param("meta", "recordingids+sources")
<< Param("fingerprint", fingerprint); << Param("fingerprint", fingerprint);
QUrl url(kUrl); QUrl url(kUrl);
@ -67,13 +68,29 @@ void AcoustidClient::CancelAll() {
requests_.clear(); 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(); reply->deleteLater();
requests_.remove(id); requests_.remove(request_id);
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() !=
200) { 200) {
emit Finished(id, QString()); emit Finished(request_id, QStringList());
return; return;
} }
@ -81,16 +98,26 @@ void AcoustidClient::RequestFinished(QNetworkReply* reply, int id) {
bool ok = false; bool ok = false;
QVariantMap result = parser.parse(reply, &ok).toMap(); QVariantMap result = parser.parse(reply, &ok).toMap();
if (!ok) { if (!ok) {
emit Finished(id, QString()); emit Finished(request_id, QStringList());
return; return;
} }
QString status = result["status"].toString(); QString status = result["status"].toString();
if (status != "ok") { if (status != "ok") {
emit Finished(id, QString()); emit Finished(request_id, QStringList());
return; 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(); QVariantList results = result["results"].toList();
// List of <id, nb of sources> pairs
QList<IdSource> id_source_list;
for (const QVariant& v : results) { for (const QVariant& v : results) {
QVariantMap r = v.toMap(); QVariantMap r = v.toMap();
if (r.contains("recordings")) { if (r.contains("recordings")) {
@ -98,12 +125,18 @@ void AcoustidClient::RequestFinished(QNetworkReply* reply, int id) {
for (const QVariant& recording : recordings) { for (const QVariant& recording : recordings) {
QVariantMap o = recording.toMap(); QVariantMap o = recording.toMap();
if (o.contains("id")) { if (o.contains("id")) {
emit Finished(id, o["id"].toString()); id_source_list << IdSource(o["id"].toString(), o["sources"].toInt());
return;
} }
} }
} }
} }
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(); void CancelAll();
signals: signals:
void Finished(int id, const QString& mbid); void Finished(int id, const QStringList& mbid_list);
private slots: private slots:
void RequestFinished(QNetworkReply* reply, int id); void RequestFinished(QNetworkReply* reply, int id);

View File

@ -33,6 +33,7 @@ const char* MusicBrainzClient::kDiscUrl =
"https://musicbrainz.org/ws/2/discid/"; "https://musicbrainz.org/ws/2/discid/";
const char* MusicBrainzClient::kDateRegex = "^[12]\\d{3}"; const char* MusicBrainzClient::kDateRegex = "^[12]\\d{3}";
const int MusicBrainzClient::kDefaultTimeout = 5000; // msec const int MusicBrainzClient::kDefaultTimeout = 5000; // msec
const int MusicBrainzClient::kMaxRequestPerTrack = 3;
MusicBrainzClient::MusicBrainzClient(QObject* parent, MusicBrainzClient::MusicBrainzClient(QObject* parent,
QNetworkAccessManager* network) QNetworkAccessManager* network)
@ -40,22 +41,30 @@ MusicBrainzClient::MusicBrainzClient(QObject* parent,
network_(network ? network : new NetworkAccessManager(this)), network_(network ? network : new NetworkAccessManager(this)),
timeouts_(new NetworkTimeouts(kDefaultTimeout, 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; typedef QPair<QString, QString> Param;
QList<Param> parameters; int request_number = 0;
parameters << Param("inc", "artists+releases+media"); for (const QString& mbid : mbid_list) {
QList<Param> parameters;
parameters << Param("inc", "artists+releases+media");
QUrl url(kTrackUrl + mbid); QUrl url(kTrackUrl + mbid);
url.setQueryItems(parameters); url.setQueryItems(parameters);
QNetworkRequest req(url); QNetworkRequest req(url);
QNetworkReply* reply = network_->get(req); QNetworkReply* reply = network_->get(req);
NewClosure(reply, SIGNAL(finished()), this, NewClosure(reply, SIGNAL(finished()), this,
SLOT(RequestFinished(QNetworkReply*, int)), reply, id); SLOT(RequestFinished(QNetworkReply*, int, int)),
requests_[id] = reply; 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) { 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(); reply->deleteLater();
requests_.remove(id);
ResultList ret;
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != const int nb_removed = requests_.remove(id, reply);
200) { if (nb_removed != 1) {
emit Finished(id, ret); qLog(Error) << "Error: unknown reply received:" << nb_removed <<
return; "requests removed, while only one was supposed to be removed";
} }
QXmlStreamReader reader(reply); if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() ==
while (!reader.atEnd()) { 200) {
if (reader.readNext() == QXmlStreamReader::StartElement && QXmlStreamReader reader(reply);
reader.name() == "recording") { ResultList res;
ResultList tracks = ParseTrack(&reader); while (!reader.atEnd()) {
for (const Result& track : tracks) { if (reader.readNext() == QXmlStreamReader::StartElement &&
if (!track.title_.isEmpty()) { reader.name() == "recording") {
ret << track; 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, bool MusicBrainzClient::MediumHasDiscid(const QString& discid,
@ -327,8 +353,22 @@ MusicBrainzClient::Release MusicBrainzClient::ParseRelease(
} }
MusicBrainzClient::ResultList MusicBrainzClient::UniqueResults( MusicBrainzClient::ResultList MusicBrainzClient::UniqueResults(
const ResultList& results) { const ResultList& results, UniqueResultsSortOption opt) {
ResultList ret = QSet<Result>::fromList(results).toList();
qSort(ret); 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; return ret;
} }

View File

@ -19,7 +19,7 @@
#define MUSICBRAINZCLIENT_H #define MUSICBRAINZCLIENT_H
#include <QHash> #include <QHash>
#include <QMap> #include <QMultiMap>
#include <QObject> #include <QObject>
#include <QXmlStreamReader> #include <QXmlStreamReader>
@ -78,7 +78,7 @@ class MusicBrainzClient : public QObject {
// Starts a request and returns immediately. Finished() will be emitted // Starts a request and returns immediately. Finished() will be emitted
// later with the same ID. // later with the same ID.
void Start(int id, const QString& mbid); void Start(int id, const QStringList& mbid);
void StartDiscIdRequest(const QString& discid); void StartDiscIdRequest(const QString& discid);
// Cancels the request with the given ID. Finished() will never be emitted // Cancels the request with the given ID. Finished() will never be emitted
@ -97,10 +97,18 @@ signals:
const MusicBrainzClient::ResultList& result); const MusicBrainzClient::ResultList& result);
private slots: 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); void DiscIdRequestFinished(const QString& discid, QNetworkReply* reply);
private: private:
// Used as parameter for UniqueResults
enum UniqueResultsSortOption {
SortResults = 0,
KeepOriginalOrder
};
struct Release { struct Release {
Release() : track_(0), year_(0) {} Release() : track_(0), year_(0) {}
@ -117,23 +125,40 @@ signals:
int year_; 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 bool MediumHasDiscid(const QString& discid, QXmlStreamReader* reader);
static ResultList ParseMedium(QXmlStreamReader* reader); static ResultList ParseMedium(QXmlStreamReader* reader);
static Result ParseTrackFromDisc(QXmlStreamReader* reader); static Result ParseTrackFromDisc(QXmlStreamReader* reader);
static ResultList ParseTrack(QXmlStreamReader* reader); static ResultList ParseTrack(QXmlStreamReader* reader);
static void ParseArtist(QXmlStreamReader* reader, QString* artist); static void ParseArtist(QXmlStreamReader* reader, QString* artist);
static Release ParseRelease(QXmlStreamReader* reader); static Release ParseRelease(QXmlStreamReader* reader);
static ResultList UniqueResults(const ResultList& results); static ResultList UniqueResults(const ResultList& results,
UniqueResultsSortOption opt = SortResults);
private: private:
static const char* kTrackUrl; static const char* kTrackUrl;
static const char* kDiscUrl; static const char* kDiscUrl;
static const char* kDateRegex; static const char* kDateRegex;
static const int kDefaultTimeout; static const int kDefaultTimeout;
static const int kMaxRequestPerTrack;
QNetworkAccessManager* network_; QNetworkAccessManager* network_;
NetworkTimeouts* timeouts_; 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) { inline uint qHash(const MusicBrainzClient::Result& result) {

View File

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

View File

@ -47,7 +47,7 @@ signals:
private slots: private slots:
void FingerprintFound(int index); 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); void TagsFetched(int index, const MusicBrainzClient::ResultList& result);
private: private:

View File

@ -89,6 +89,10 @@ const QRgb Playlist::kDynamicHistoryColor = qRgb(0x80, 0x80, 0x80);
const char* Playlist::kSettingsGroup = "Playlist"; 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::kUndoStackSize = 20;
const int Playlist::kUndoItemLimit = 500; const int Playlist::kUndoItemLimit = 500;
@ -1097,9 +1101,8 @@ void Playlist::InsertInternetItems(const InternetModel* model,
switch (item.data(InternetModel::Role_PlayBehaviour).toInt()) { switch (item.data(InternetModel::Role_PlayBehaviour).toInt()) {
case InternetModel::PlayBehaviour_SingleItem: case InternetModel::PlayBehaviour_SingleItem:
playlist_items << shared_ptr<PlaylistItem>(new InternetPlaylistItem( playlist_items << shared_ptr<PlaylistItem>(new InternetPlaylistItem(
model->ServiceForIndex(item), model->ServiceForIndex(item),
item.data(InternetModel::Role_SongMetadata) item.data(InternetModel::Role_SongMetadata).value<Song>()));
.value<Song>()));
break; break;
case InternetModel::PlayBehaviour_UseSongLoader: case InternetModel::PlayBehaviour_UseSongLoader:
@ -1122,7 +1125,7 @@ void Playlist::InsertInternetItems(InternetService* service,
PlaylistItemList playlist_items; PlaylistItemList playlist_items;
for (const Song& song : songs) { for (const Song& song : songs) {
playlist_items << shared_ptr<PlaylistItem>( playlist_items << shared_ptr<PlaylistItem>(
new InternetPlaylistItem(service, song)); new InternetPlaylistItem(service, song));
} }
InsertItems(playlist_items, pos, play_now, enqueue); 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; 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(); const PlaylistItem* item = old_items[idx.row()].get();
changePersistentIndex( changePersistentIndex(idx,
idx, index(new_rows[item], idx.column(), idx.parent())); index(new_rows[item], idx.column(), idx.parent()));
} }
layoutChanged(); layoutChanged();
@ -1914,8 +1917,10 @@ void Playlist::ReshuffleIndices() {
std::random_shuffle(shuffled_album_keys.begin(), std::random_shuffle(shuffled_album_keys.begin(),
shuffled_album_keys.end()); shuffled_album_keys.end());
// If the user is currently playing a song, force its album to be first. // If the user is currently playing a song, force its album to be first
if (current_virtual_index_ != -1) { // 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 QString key = items_[current_row()]->Metadata().AlbumKey();
const int pos = shuffled_album_keys.indexOf(key); const int pos = shuffled_album_keys.indexOf(key);
if (pos >= 1) { if (pos >= 1) {
@ -1984,6 +1989,7 @@ void Playlist::TracksDequeued() {
emit dataChanged(index, index); emit dataChanged(index, index);
} }
temp_dequeue_change_indexes_.clear(); temp_dequeue_change_indexes_.clear();
emit QueueChanged();
} }
void Playlist::TracksEnqueued(const QModelIndex&, int begin, int end) { void Playlist::TracksEnqueued(const QModelIndex&, int begin, int end) {
@ -2103,6 +2109,21 @@ void Playlist::RemoveDuplicateSongs() {
removeRows(rows_to_remove); 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) { bool Playlist::ApplyValidityOnCurrentSong(const QUrl& url, bool valid) {
PlaylistItemPtr current = current_item(); PlaylistItemPtr current = current_item();

View File

@ -134,6 +134,12 @@ class Playlist : public QAbstractListModel {
LastFM_Queued, // Track added to the queue for scrobbling 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* kCddaMimeType;
static const char* kRowsMimetype; static const char* kRowsMimetype;
static const char* kPlayNowMimetype; static const char* kPlayNowMimetype;
@ -146,6 +152,10 @@ class Playlist : public QAbstractListModel {
static const char* kSettingsGroup; static const char* kSettingsGroup;
static const char* kPathType;
static const char* kWriteMetadata;
static const char* kQuickChangeMenu;
static const int kUndoStackSize; static const int kUndoStackSize;
static const int kUndoItemLimit; static const int kUndoItemLimit;
@ -304,6 +314,7 @@ class Playlist : public QAbstractListModel {
void Clear(); void Clear();
void RemoveDuplicateSongs(); void RemoveDuplicateSongs();
void RemoveUnavailableSongs();
void Shuffle(); void Shuffle();
void ShuffleModeChanged(PlaylistSequence::ShuffleMode mode); void ShuffleModeChanged(PlaylistSequence::ShuffleMode mode);
@ -333,6 +344,10 @@ signals:
void LoadTracksError(const QString& message); void LoadTracksError(const QString& message);
// Signals that the queue has changed, meaning that the remaining queued
// items should update their position.
void QueueChanged();
private: private:
void SetCurrentIsPaused(bool paused); void SetCurrentIsPaused(bool paused);
void UpdateScrobblePoint(); void UpdateScrobblePoint();

View File

@ -51,6 +51,13 @@ PlaylistContainer::PlaylistContainer(QWidget* parent)
filter_timer_(new QTimer(this)) { filter_timer_(new QTimer(this)) {
ui_->setupUi(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_ = new QLabel(ui_->playlist);
no_matches_label_->setAlignment(Qt::AlignTop | Qt::AlignHCenter); no_matches_label_->setAlignment(Qt::AlignTop | Qt::AlignHCenter);
no_matches_label_->setAttribute(Qt::WA_TransparentForMouseEvents); no_matches_label_->setAttribute(Qt::WA_TransparentForMouseEvents);
@ -75,6 +82,8 @@ PlaylistContainer::PlaylistContainer(QWidget* parent)
settings_.beginGroup(kSettingsGroup); settings_.beginGroup(kSettingsGroup);
ReloadSettings();
// Tab bar // Tab bar
ui_->tab_bar->setExpanding(false); ui_->tab_bar->setExpanding(false);
ui_->tab_bar->setMovable(true); ui_->tab_bar->setMovable(true);
@ -101,6 +110,28 @@ PlaylistContainer::PlaylistContainer(QWidget* parent)
PlaylistContainer::~PlaylistContainer() { delete ui_; } 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; } PlaylistView* PlaylistContainer::view() const { return ui_->playlist; }
void PlaylistContainer::SetActions(QAction* new_playlist, void PlaylistContainer::SetActions(QAction* new_playlist,
@ -361,9 +392,16 @@ void PlaylistContainer::UpdateNoMatchesLabel() {
QString text; QString text;
if (has_rows && !has_results) { if (has_rows && !has_results) {
text = if (ui_->filter->text().trimmed().compare(
tr("No matches found. Clear the search box to show the whole playlist " "the answer to life the universe "
"again."); "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()) { if (!text.isEmpty()) {
@ -432,3 +470,7 @@ bool PlaylistContainer::eventFilter(QObject* objectWatched, QEvent* event) {
} }
return QWidget::eventFilter(objectWatched, 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 <QWidget>
#include <QSettings> #include <QSettings>
#include "playlist.h"
class Ui_PlaylistContainer; class Ui_PlaylistContainer;
class LineEditInterface; class LineEditInterface;
@ -46,6 +48,8 @@ class PlaylistContainer : public QWidget {
QAction* previous_playlist); QAction* previous_playlist);
void SetManager(PlaylistManager* manager); void SetManager(PlaylistManager* manager);
void ReloadSettings();
PlaylistView* view() const; PlaylistView* view() const;
bool eventFilter(QObject* objectWatched, QEvent* event); bool eventFilter(QObject* objectWatched, QEvent* event);
@ -90,6 +94,8 @@ signals:
void UpdateNoMatchesLabel(); void UpdateNoMatchesLabel();
void PathSettingChanged(int index);
private: private:
void UpdateActiveIcon(const QIcon& icon); void UpdateActiveIcon(const QIcon& icon);
void RepositionNoMatchesLabel(bool force = false); void RepositionNoMatchesLabel(bool force = false);

View File

@ -24,7 +24,16 @@
<property name="spacing"> <property name="spacing">
<number>0</number> <number>0</number>
</property> </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> <number>0</number>
</property> </property>
<item> <item>
@ -36,7 +45,16 @@
<property name="spacing"> <property name="spacing">
<number>0</number> <number>0</number>
</property> </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> <number>0</number>
</property> </property>
<item> <item>
@ -104,6 +122,36 @@
</property> </property>
</widget> </widget>
</item> </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> <item>
<widget class="Line" name="line_2"> <widget class="Line" name="line_2">
<property name="orientation"> <property name="orientation">

View File

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

View File

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

View File

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

View File

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

View File

@ -20,6 +20,7 @@
#include "library/librarybackend.h" #include "library/librarybackend.h"
#include "library/libraryquery.h" #include "library/libraryquery.h"
#include "library/sqlrow.h" #include "library/sqlrow.h"
#include "playlist/playlist.h"
#include <QUrl> #include <QUrl>
@ -46,8 +47,10 @@ void ParserBase::LoadSong(const QString& filename_or_url, qint64 beginning,
} }
} }
// Convert native separators for Windows paths // Clementine always wants / separators internally. Using
filename = QDir::fromNativeSeparators(filename); // 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 // Make the path absolute
if (!QDir::isAbsolutePath(filename)) { if (!QDir::isAbsolutePath(filename)) {
@ -87,11 +90,19 @@ QString ParserBase::URLOrRelativeFilename(const QUrl& url,
const QDir& dir) const { const QDir& dir) const {
if (url.scheme() != "file") return url.toString(); 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(); const QString filename = url.toLocalFile();
if (QDir::isAbsolutePath(filename)) {
if (path != Playlist::Path_Absolute && QDir::isAbsolutePath(filename)) {
const QString relative = dir.relativeFilePath(filename); const QString relative = dir.relativeFilePath(filename);
if (!relative.contains("..")) return relative; if (!relative.startsWith("../") || path == Playlist::Path_Relative)
return relative;
} }
return filename; return filename;
} }

View File

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