Merge remote-tracking branch 'upstream/master' into qt5

This commit is contained in:
Chocobozzz 2018-02-01 09:50:42 +01:00
commit 70f68b1926
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
130 changed files with 12552 additions and 11915 deletions

1
.gitignore vendored
View File

@ -45,3 +45,4 @@ dist/windows/Python27.zip
*.swp *.swp
src/translations/translations.pot src/translations/translations.pot
*.DS_Store *.DS_Store
.vscode

40
.travis.yml Normal file
View File

@ -0,0 +1,40 @@
language: cpp
addons:
apt:
packages:
- cmake
- gettext
- libboost-dev
- libboost-serialization-dev
- libcdio-dev
- libchromaprint-dev
- libcrypto++-dev
- libechonest-dev
- libfftw3-dev
- libglew1.5-dev
- libgpod-dev
- libgstreamer1.0-dev
- libgstreamer-plugins-base1.0-dev
- liblastfm-dev
- libmtp-dev
- libplist-dev
- libprotobuf-dev
- libpulse-dev
- libqca2-dev
- libqca2-plugin-ossl
- libqjson-dev
- libqt4-dev
- libqt4-opengl-dev
- libqtwebkit-dev
- libsparsehash-dev
- libsqlite3-dev
- libtag1-dev
- libusbmuxd-dev
- protobuf-compiler
- qt4-dev-tools
script:
- cd bin
- cmake ..
- make

View File

@ -26,6 +26,8 @@ set(DISABLE_MILKDROP_PRESETS OFF)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++98") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++98")
pkg_check_modules(GLEW glew)
if(DISABLE_NATIVE_PRESETS) if(DISABLE_NATIVE_PRESETS)
ADD_DEFINITIONS(-DDISABLE_NATIVE_PRESETS) ADD_DEFINITIONS(-DDISABLE_NATIVE_PRESETS)
endif(DISABLE_NATIVE_PRESETS) endif(DISABLE_NATIVE_PRESETS)

View File

@ -22,6 +22,6 @@ FIND_PACKAGE(X11)
INCLUDE_DIRECTORIES(${projectM_SOURCE_DIR} ${X11_INCLUDE_DIR}) INCLUDE_DIRECTORIES(${projectM_SOURCE_DIR} ${X11_INCLUDE_DIR})
ADD_LIBRARY(Renderer STATIC ${Renderer_SOURCES}) ADD_LIBRARY(Renderer STATIC ${Renderer_SOURCES})
SET_TARGET_PROPERTIES(Renderer PROPERTIES VERSION 2.00 SOVERSION 2) SET_TARGET_PROPERTIES(Renderer PROPERTIES VERSION 2.00 SOVERSION 2)
TARGET_LINK_LIBRARIES(Renderer ${MATH_LIBRARIES} projectM) TARGET_LINK_LIBRARIES(Renderer ${MATH_LIBRARIES} ${GLEW_LIBRARIES} projectM)
set_target_properties(Renderer PROPERTIES COMPILE_FLAGS "-Wno-parentheses") set_target_properties(Renderer PROPERTIES COMPILE_FLAGS "-Wno-parentheses")

View File

@ -1,7 +1,7 @@
Clementine Clementine
========== ==========
Clementine is a modern music player and library organizer for Windows, Linux and Mac OS X. Clementine is a modern music player and library organizer for Windows, Linux and macOS.
- Website: http://www.clementine-player.org/ - Website: http://www.clementine-player.org/
- Github: https://github.com/clementine-player/Clementine - Github: https://github.com/clementine-player/Clementine

View File

@ -471,6 +471,7 @@
<file>schema/schema-4.sql</file> <file>schema/schema-4.sql</file>
<file>schema/schema-5.sql</file> <file>schema/schema-5.sql</file>
<file>schema/schema-50.sql</file> <file>schema/schema-50.sql</file>
<file>schema/schema-51.sql</file>
<file>schema/schema-6.sql</file> <file>schema/schema-6.sql</file>
<file>schema/schema-7.sql</file> <file>schema/schema-7.sql</file>
<file>schema/schema-8.sql</file> <file>schema/schema-8.sql</file>

View File

@ -71,7 +71,7 @@
</exclude> </exclude>
<invalidIndicator value="Couldn't find that page."/> <invalidIndicator value="Couldn't find that page."/>
</provider> </provider>
<provider name="letras.mus.br" title="" charset="utf-8" url="http://letras.terra.com.br/winamp.php?musica={title}&amp;artista={artist}"> <provider name="letras.mus.br" title="" charset="utf-8" url="https://www.letras.mus.br/winamp.php?musica={title}&amp;artista={artist}">
<urlFormat replace="_@,;&amp;\/&quot;" with="_"/> <urlFormat replace="_@,;&amp;\/&quot;" with="_"/>
<urlFormat replace=" " with="+"/> <urlFormat replace=" " with="+"/>
<extract> <extract>
@ -105,7 +105,7 @@
<urlFormat replace=" _@;\&quot;" with="_"/> <urlFormat replace=" _@;\&quot;" with="_"/>
<urlFormat replace="?" with="%3F"/> <urlFormat replace="?" with="%3F"/>
<extract> <extract>
<item begin="&lt;div class='lyricbox'&gt;" end="&lt;!--"/> <item begin="&lt;div class='lyricbox'&gt;" end="&lt;div class='lyricsbreak'"/>
</extract> </extract>
<exclude> <exclude>
<item tag="&lt;div class='rtMatcher'&gt;"/> <item tag="&lt;div class='rtMatcher'&gt;"/>

15
data/schema/schema-51.sql Normal file
View File

@ -0,0 +1,15 @@
DELETE FROM %allsongstables_fts;
DROP TABLE %allsongstables_fts;
CREATE VIRTUAL TABLE %allsongstables_fts USING fts3( ftstitle, ftsalbum, ftsartist, ftsalbumartist,
ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment, ftsyear,
tokenize=unicode
);
INSERT INTO %allsongstables_fts ( ROWID, ftstitle, ftsalbum, ftsartist, ftsalbumartist,
ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment, ftsyear)
SELECT ROWID, title, album, artist, albumartist, composer, performer, grouping, genre, comment, year
FROM %allsongstables;
UPDATE schema_version SET version=51;

View File

@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Type=Application Type=Application
Name=Clementine Name=Clementine
Name[hi]=क्लेमेंटैन्
Name[sr]=Клементина Name[sr]=Клементина
Name[sr@ijekavian]=Клементина Name[sr@ijekavian]=Клементина
Name[sr@ijekavianlatin]=Klementina Name[sr@ijekavianlatin]=Klementina
@ -9,6 +10,7 @@ Name[sr@latin]=Klementina
GenericName=Clementine Music Player GenericName=Clementine Music Player
GenericName[ca]=Reproductor de música Clementine GenericName[ca]=Reproductor de música Clementine
GenericName[es]=Reproductor de música Clementine GenericName[es]=Reproductor de música Clementine
GenericName[hi]=क्लेमेंटैन् संगीत वादक
GenericName[pl]=Odtwarzacz muzyki Clementine GenericName[pl]=Odtwarzacz muzyki Clementine
GenericName[pt]=Reprodutor de músicas Clementine GenericName[pt]=Reprodutor de músicas Clementine
GenericName[sl]=Predvajalnik glasbe Clementine GenericName[sl]=Predvajalnik glasbe Clementine
@ -56,6 +58,7 @@ Name[fr]=Lecture
Name[ga]=Seinn Name[ga]=Seinn
Name[gl]=Reproducir Name[gl]=Reproducir
Name[he]=נגינה Name[he]=נגינה
Name[hi]=गाना बजाएं
Name[hr]=Pokreni reprodukciju Name[hr]=Pokreni reprodukciju
Name[hu]=Lejátszás Name[hu]=Lejátszás
Name[it]=Riproduci Name[it]=Riproduci
@ -102,6 +105,7 @@ Name[fi]=Keskeytä
Name[ga]=Cuir ar sos Name[ga]=Cuir ar sos
Name[gl]=Pausa Name[gl]=Pausa
Name[he]=השהייה Name[he]=השהייה
Name[hi]=गाना रोकें
Name[hr]=Pauza Name[hr]=Pauza
Name[hu]=Szünet Name[hu]=Szünet
Name[it]=Pausa Name[it]=Pausa
@ -148,6 +152,7 @@ Name[fi]=Pysäytä
Name[ga]=Stad Name[ga]=Stad
Name[gl]=Deter Name[gl]=Deter
Name[he]=הפסקה Name[he]=הפסקה
Name[hi]=गाना बंद करे
Name[hr]=Zaustavi reprodukciju Name[hr]=Zaustavi reprodukciju
Name[hu]=Leállít Name[hu]=Leállít
Name[it]=Ferma Name[it]=Ferma
@ -197,6 +202,7 @@ Name[fr]=Arrêter la lecture après cette piste
Name[ga]=Stad i ndiaidh an rian seo Name[ga]=Stad i ndiaidh an rian seo
Name[gl]=Deter a reprodución despois da pista actual Name[gl]=Deter a reprodución despois da pista actual
Name[he]=הפסקה אחרי רצועה זו Name[he]=הפסקה אחרי רצועה זו
Name[hi]=इस गाने के बाद बंद करे
Name[hr]=Zaustavi reprodukciju nakon ove pjesme Name[hr]=Zaustavi reprodukciju nakon ove pjesme
Name[hu]=Leállítás az aktuális szám után Name[hu]=Leállítás az aktuális szám után
Name[it]=Ferma dopo questa traccia Name[it]=Ferma dopo questa traccia
@ -215,8 +221,6 @@ Name[ru]=Остановить после этого трека
Name[sk]=Zastaviť po tejto skladbe Name[sk]=Zastaviť po tejto skladbe
Name[sl]=Zaustavi po tej skladbi Name[sl]=Zaustavi po tej skladbi
Name[sr]=Заустави после ове нумере Name[sr]=Заустави после ове нумере
Name[sr@ijekavian]=
Name[sr@ijekavianlatin]=
Name[sr@latin]=Zaustavi posle ove numere Name[sr@latin]=Zaustavi posle ove numere
Name[sv]=Stoppa efter detta spår Name[sv]=Stoppa efter detta spår
Name[tr]=Bu parçadan sonra durdur Name[tr]=Bu parçadan sonra durdur
@ -245,6 +249,7 @@ Name[fr]=Précédent
Name[ga]=Roimhe Name[ga]=Roimhe
Name[gl]=Anterior Name[gl]=Anterior
Name[he]=הקודם Name[he]=הקודם
Name[hi]=पिछला गाना
Name[hr]=Prijašnje Name[hr]=Prijašnje
Name[hu]=Előző Name[hu]=Előző
Name[it]=Precedente Name[it]=Precedente
@ -292,6 +297,7 @@ Name[fr]=Suivant
Name[ga]=Ar aghaidh Name[ga]=Ar aghaidh
Name[gl]=Próximo Name[gl]=Próximo
Name[he]=הבא Name[he]=הבא
Name[hi]=अगला गाना
Name[hr]=Sljedeće Name[hr]=Sljedeće
Name[hu]=Következő Name[hu]=Következő
Name[id]=Berikut Name[id]=Berikut

1
dist/macdeploy.py vendored
View File

@ -72,6 +72,7 @@ GSTREAMER_PLUGINS = [
'libgstvorbis.so', 'libgstvorbis.so',
'libgstwavpack.so', 'libgstwavpack.so',
'libgstwavparse.so', 'libgstwavparse.so',
'libgstxingmux.so',
# HTTP src support # HTTP src support
'libgstsouphttpsrc.so', 'libgstsouphttpsrc.so',

View File

@ -35,6 +35,7 @@ add_library(libclementine-common STATIC
) )
target_link_libraries(libclementine-common target_link_libraries(libclementine-common
${QT_LIBRARIES}
${TAGLIB_LIBRARIES} ${TAGLIB_LIBRARIES}
${CMAKE_THREAD_LIBS_INIT} ${CMAKE_THREAD_LIBS_INIT}
) )

View File

@ -67,7 +67,8 @@ const char* CommandlineOptions::kHelpText =
" --quiet %29\n" " --quiet %29\n"
" --verbose %30\n" " --verbose %30\n"
" --log-levels <levels> %31\n" " --log-levels <levels> %31\n"
" --version %32\n"; " --version %32\n"
" -x, --delete-current %33\n";
const char* CommandlineOptions::kVersionText = "Clementine %1"; const char* CommandlineOptions::kVersionText = "Clementine %1";
@ -81,6 +82,7 @@ CommandlineOptions::CommandlineOptions(int argc, char** argv)
seek_to_(-1), seek_to_(-1),
seek_by_(0), seek_by_(0),
play_track_at_(-1), play_track_at_(-1),
delete_current_track_(false),
show_osd_(false), show_osd_(false),
toggle_pretty_osd_(false), toggle_pretty_osd_(false),
log_levels_(logging::kDefaultLogLevels) { log_levels_(logging::kDefaultLogLevels) {
@ -135,12 +137,13 @@ bool CommandlineOptions::Parse() {
{"verbose", no_argument, 0, Verbose}, {"verbose", no_argument, 0, Verbose},
{"log-levels", required_argument, 0, LogLevels}, {"log-levels", required_argument, 0, LogLevels},
{"version", no_argument, 0, Version}, {"version", no_argument, 0, Version},
{"delete-current", no_argument, 0, 'x'},
{0, 0, 0, 0}}; {0, 0, 0, 0}};
// Parse the arguments // Parse the arguments
bool ok = false; bool ok = false;
forever { forever {
int c = getopt_long(argc_, argv_, "hptusqrfv:c:alk:oyg:", kOptions, nullptr); int c = getopt_long(argc_, argv_, "xhptusqrfv:c:alk:oyg:", kOptions, nullptr);
// End of the options // End of the options
if (c == -1) break; if (c == -1) break;
@ -158,8 +161,8 @@ bool CommandlineOptions::Parse() {
.arg(tr("Skip backwards in playlist"), .arg(tr("Skip backwards in playlist"),
tr("Skip forwards in playlist"), tr("Skip forwards in playlist"),
tr("Set the volume to <value> percent"), tr("Set the volume to <value> percent"),
tr("Increase the volume by 4%"), tr("Increase the volume by 4 percent"),
tr("Decrease the volume by 4%"), tr("Decrease the volume by 4 percent"),
tr("Increase the volume by <value> percent"), tr("Increase the volume by <value> percent"),
tr("Decrease the volume by <value> percent")) tr("Decrease the volume by <value> percent"))
.arg(tr("Seek the currently playing track to an absolute " .arg(tr("Seek the currently playing track to an absolute "
@ -179,7 +182,8 @@ bool CommandlineOptions::Parse() {
tr("Equivalent to --log-levels *:1"), tr("Equivalent to --log-levels *:1"),
tr("Equivalent to --log-levels *:3"), tr("Equivalent to --log-levels *:3"),
tr("Comma separated list of class:level, level is 0-3")) tr("Comma separated list of class:level, level is 0-3"))
.arg(tr("Print out version information")); .arg(tr("Print out version information"),
tr("Delete the currently playing song"));
std::cout << translated_help_text.toLocal8Bit().constData(); std::cout << translated_help_text.toLocal8Bit().constData();
return false; return false;
@ -280,6 +284,10 @@ bool CommandlineOptions::Parse() {
if (!ok) play_track_at_ = -1; if (!ok) play_track_at_ = -1;
break; break;
case 'x':
delete_current_track_ = true;
break;
case '?': case '?':
default: default:
return false; return false;
@ -303,7 +311,8 @@ bool CommandlineOptions::is_empty() const {
return player_action_ == Player_None && set_volume_ == -1 && return player_action_ == Player_None && set_volume_ == -1 &&
volume_modifier_ == 0 && seek_to_ == -1 && seek_by_ == 0 && volume_modifier_ == 0 && seek_to_ == -1 && seek_by_ == 0 &&
play_track_at_ == -1 && show_osd_ == false && play_track_at_ == -1 && show_osd_ == false &&
toggle_pretty_osd_ == false && urls_.isEmpty(); toggle_pretty_osd_ == false && urls_.isEmpty() &&
delete_current_track_ == false;
} }
bool CommandlineOptions::contains_play_options() const { bool CommandlineOptions::contains_play_options() const {
@ -338,7 +347,8 @@ QString CommandlineOptions::tr(const char* source_text) {
QDataStream& operator<<(QDataStream& s, const CommandlineOptions& a) { QDataStream& operator<<(QDataStream& s, const CommandlineOptions& a) {
s << qint32(a.player_action_) << qint32(a.url_list_action_) << a.set_volume_ s << qint32(a.player_action_) << qint32(a.url_list_action_) << a.set_volume_
<< a.volume_modifier_ << a.seek_to_ << a.seek_by_ << a.play_track_at_ << a.volume_modifier_ << a.seek_to_ << a.seek_by_ << a.play_track_at_
<< a.show_osd_ << a.urls_ << a.log_levels_ << a.toggle_pretty_osd_; << a.show_osd_ << a.urls_ << a.log_levels_ << a.toggle_pretty_osd_
<< a.delete_current_track_;
return s; return s;
} }
@ -348,7 +358,8 @@ QDataStream& operator>>(QDataStream& s, CommandlineOptions& a) {
quint32 url_list_action = 0; quint32 url_list_action = 0;
s >> player_action >> url_list_action >> a.set_volume_ >> s >> player_action >> url_list_action >> a.set_volume_ >>
a.volume_modifier_ >> a.seek_to_ >> a.seek_by_ >> a.play_track_at_ >> a.volume_modifier_ >> a.seek_to_ >> a.seek_by_ >> a.play_track_at_ >>
a.show_osd_ >> a.urls_ >> a.log_levels_ >> a.toggle_pretty_osd_; a.show_osd_ >> a.urls_ >> a.log_levels_ >> a.toggle_pretty_osd_ >>
a.delete_current_track_;
a.player_action_ = CommandlineOptions::PlayerAction(player_action); a.player_action_ = CommandlineOptions::PlayerAction(player_action);
a.url_list_action_ = CommandlineOptions::UrlListAction(url_list_action); a.url_list_action_ = CommandlineOptions::UrlListAction(url_list_action);

View File

@ -70,6 +70,7 @@ class CommandlineOptions {
int seek_to() const { return seek_to_; } int seek_to() const { return seek_to_; }
int seek_by() const { return seek_by_; } int seek_by() const { return seek_by_; }
int play_track_at() const { return play_track_at_; } int play_track_at() const { return play_track_at_; }
bool delete_current_track() const { return delete_current_track_; }
bool show_osd() const { return show_osd_; } bool show_osd() const { return show_osd_; }
bool toggle_pretty_osd() const { return toggle_pretty_osd_; } bool toggle_pretty_osd() const { return toggle_pretty_osd_; }
QList<QUrl> urls() const { return urls_; } QList<QUrl> urls() const { return urls_; }
@ -113,6 +114,7 @@ class CommandlineOptions {
int seek_to_; int seek_to_;
int seek_by_; int seek_by_;
int play_track_at_; int play_track_at_;
bool delete_current_track_;
bool show_osd_; bool show_osd_;
bool toggle_pretty_osd_; bool toggle_pretty_osd_;
QString language_; QString language_;

View File

@ -47,7 +47,7 @@
#include <QVariant> #include <QVariant>
const char* Database::kDatabaseFilename = "clementine.db"; const char* Database::kDatabaseFilename = "clementine.db";
const int Database::kSchemaVersion = 50; const int Database::kSchemaVersion = 51;
const char* Database::kMagicAllSongsTables = "%allsongstables"; const char* Database::kMagicAllSongsTables = "%allsongstables";
int Database::sNextConnectionId = 1; int Database::sNextConnectionId = 1;

View File

@ -57,6 +57,15 @@ void DeleteFiles::Start(const SongList& songs) {
thread_->start(); thread_->start();
} }
void DeleteFiles::Start(const QUrl& url) {
SongList songs;
Song song;
song.set_url(url);
songs << song;
Start(songs);
}
void DeleteFiles::Start(const QStringList& filenames) { void DeleteFiles::Start(const QStringList& filenames) {
SongList songs; SongList songs;
for (const QString& filename : filenames) { for (const QString& filename : filenames) {

View File

@ -40,6 +40,7 @@ class DeleteFiles : public QObject {
void Start(const SongList& songs); void Start(const SongList& songs);
void Start(const QStringList& filenames); void Start(const QStringList& filenames);
void Start(const QUrl& url);
signals: signals:
void Finished(const SongList& songs_with_errors); void Finished(const SongList& songs_with_errors);

View File

@ -22,6 +22,7 @@
#include "metatypes.h" #include "metatypes.h"
#include <QFileInfo>
#include <QMetaType> #include <QMetaType>
#include <QNetworkCookie> #include <QNetworkCookie>
@ -110,6 +111,7 @@ void RegisterMetaTypes() {
qRegisterMetaType<Subdirectory>("Subdirectory"); qRegisterMetaType<Subdirectory>("Subdirectory");
qRegisterMetaType<QList<QUrl>>("QList<QUrl>"); qRegisterMetaType<QList<QUrl>>("QList<QUrl>");
qRegisterMetaType<QAbstractSocket::SocketState>(); qRegisterMetaType<QAbstractSocket::SocketState>();
qRegisterMetaType<QFileInfo>("QFileInfo");
#ifdef HAVE_DBUS #ifdef HAVE_DBUS
qDBusRegisterMetaType<QImage>(); qDBusRegisterMetaType<QImage>();

View File

@ -320,7 +320,7 @@ void Mpris2::SetShuffle(bool enable) {
QVariantMap Mpris2::Metadata() const { return last_metadata_; } QVariantMap Mpris2::Metadata() const { return last_metadata_; }
QString Mpris2::current_track_id() const { QString Mpris2::current_track_id() const {
return QString("/org/mpris/MediaPlayer2/Track/%1") return QString("/org/clementineplayer/Clementine/Track/%1")
.arg(QString::number(app_->playlist_manager()->active()->current_row())); .arg(QString::number(app_->playlist_manager()->active()->current_row()));
} }
@ -495,7 +495,7 @@ namespace {
QDBusObjectPath MakePlaylistPath(int id) { QDBusObjectPath MakePlaylistPath(int id) {
return QDBusObjectPath( return QDBusObjectPath(
QString("/org/mpris/MediaPlayer2/Playlists/%1").arg(id)); QString("/org/clementineplayer/clementine/PlaylistId/%1").arg(id));
} }
} }

View File

@ -194,6 +194,13 @@ void Organise::ProcessSomeFiles() {
if (!destination_->CopyToStorage(job)) { if (!destination_->CopyToStorage(job)) {
files_with_errors_ << task.song_info_.song_.basefilename(); files_with_errors_ << task.song_info_.song_.basefilename();
} else { } else {
if (job.remove_original_) {
// Notify other aspects of system that song has been invalidated
QString root = destination_->LocalPath();
QFileInfo new_file = QFileInfo(
root + "/" + task.song_info_.new_filename_);
emit SongPathChanged(song, new_file);
}
if (job.mark_as_listened_) { if (job.mark_as_listened_) {
emit FileCopied(job.metadata_.id()); emit FileCopied(job.metadata_.id());
} }

View File

@ -24,6 +24,7 @@
#include <memory> #include <memory>
#include <QFileInfo>
#include <QBasicTimer> #include <QBasicTimer>
#include <QObject> #include <QObject>
#include <QTemporaryFile> #include <QTemporaryFile>
@ -60,6 +61,7 @@ class Organise : public QObject {
signals: signals:
void Finished(const QStringList& files_with_errors); void Finished(const QStringList& files_with_errors);
void FileCopied(int database_id); void FileCopied(int database_id);
void SongPathChanged(const Song& song, const QFileInfo& new_file);
protected: protected:
void timerEvent(QTimerEvent* e); void timerEvent(QTimerEvent* e);

View File

@ -613,7 +613,16 @@ void Player::InvalidSongRequested(const QUrl& url) {
emit SongChangeRequestProcessed(url, false); emit SongChangeRequestProcessed(url, false);
// ... and now when our listeners have completed their processing of the // ... and now when our listeners have completed their processing of the
// current item we can change the current item by skipping to the next song // current item we can change the current item by skipping to the next song
NextItem(Engine::Auto);
QSettings s;
s.beginGroup(kSettingsGroup);
bool stop_playback = s.value("stop_play_if_fail", 0).toBool();
s.endGroup();
if (!stop_playback) {
NextItem(Engine::Auto);
}
} }
void Player::RegisterUrlHandler(UrlHandler* handler) { void Player::RegisterUrlHandler(UrlHandler* handler) {

View File

@ -130,7 +130,8 @@ const QStringList Song::kFtsColumns = QStringList() << "ftstitle"
<< "ftsperformer" << "ftsperformer"
<< "ftsgrouping" << "ftsgrouping"
<< "ftsgenre" << "ftsgenre"
<< "ftscomment"; << "ftscomment"
<< "ftsyear";
const QString Song::kFtsColumnSpec = Song::kFtsColumns.join(", "); const QString Song::kFtsColumnSpec = Song::kFtsColumns.join(", ");
const QString Song::kFtsBindSpec = const QString Song::kFtsBindSpec =
@ -407,7 +408,7 @@ QString Song::TextForFiletype(FileType type) {
case Song::Type_Asf: case Song::Type_Asf:
return QObject::tr("Windows Media audio"); return QObject::tr("Windows Media audio");
case Song::Type_Flac: case Song::Type_Flac:
return QObject::tr("Flac"); return QObject::tr("FLAC");
case Song::Type_Mp4: case Song::Type_Mp4:
return QObject::tr("MP4 AAC"); return QObject::tr("MP4 AAC");
case Song::Type_Mpc: case Song::Type_Mpc:
@ -415,7 +416,7 @@ QString Song::TextForFiletype(FileType type) {
case Song::Type_Mpeg: case Song::Type_Mpeg:
return QObject::tr("MP3"); // Not technically correct return QObject::tr("MP3"); // Not technically correct
case Song::Type_OggFlac: case Song::Type_OggFlac:
return QObject::tr("Ogg Flac"); return QObject::tr("Ogg FLAC");
case Song::Type_OggSpeex: case Song::Type_OggSpeex:
return QObject::tr("Ogg Speex"); return QObject::tr("Ogg Speex");
case Song::Type_OggVorbis: case Song::Type_OggVorbis:
@ -996,6 +997,7 @@ void Song::BindToFtsQuery(QSqlQuery* query) const {
query->bindValue(":ftsgrouping", d->grouping_); query->bindValue(":ftsgrouping", d->grouping_);
query->bindValue(":ftsgenre", d->genre_); query->bindValue(":ftsgenre", d->genre_);
query->bindValue(":ftscomment", d->comment_); query->bindValue(":ftscomment", d->comment_);
query->bindValue(":ftsyear", d->year_);
} }
#ifdef HAVE_LIBLASTFM #ifdef HAVE_LIBLASTFM

View File

@ -113,13 +113,18 @@ QString PrettyTimeNanosec(qint64 nanoseconds) {
} }
QString WordyTime(quint64 seconds) { QString WordyTime(quint64 seconds) {
quint64 days = seconds / (60 * 60 * 24); quint64 days = seconds / (kSecsPerDay);
quint64 remaining_hours = (seconds - days * kSecsPerDay) / (60 * 60);
// TODO(David Sansome): Make the plural rules translatable // TODO(David Sansome): Make the plural rules translatable
QStringList parts; QStringList parts;
if (days) parts << (days == 1 ? tr("1 day") : tr("%1 days").arg(days)); if (days) parts << (days == 1 ? tr("1 day") : tr("%1 days").arg(days));
parts << PrettyTime(seconds - days * 60 * 60 * 24);
// Since PrettyTime does not return the hour if it is 0, we need to add it
// explicitly for durations longer than 1 day.
parts << (days && !remaining_hours ? QString("0:") : QString()) +
PrettyTime(seconds - days * kSecsPerDay);
return parts.join(" "); return parts.join(" ");
} }

View File

@ -122,7 +122,7 @@ QVariantMap DeviceKitLister::DeviceHardwareInfo(const QString& id) {
if (!device_data_.contains(id)) return ret; if (!device_data_.contains(id)) return ret;
const DeviceData& data = device_data_[id]; const DeviceData& data = device_data_[id];
ret[QT_TR_NOOP("DBus path")] = data.dbus_path; ret[QT_TR_NOOP("D-Bus path")] = data.dbus_path;
ret[QT_TR_NOOP("Serial number")] = data.drive_serial; ret[QT_TR_NOOP("Serial number")] = data.drive_serial;
ret[QT_TR_NOOP("Mount points")] = data.device_mount_paths.join(", "); ret[QT_TR_NOOP("Mount points")] = data.device_mount_paths.join(", ");
ret[QT_TR_NOOP("Device")] = data.device_file; ret[QT_TR_NOOP("Device")] = data.device_file;

View File

@ -73,7 +73,7 @@ QVariantMap Udisks2Lister::DeviceHardwareInfo(const QString& id) {
QVariantMap result; QVariantMap result;
const auto& data = device_data_[id]; const auto& data = device_data_[id];
result[QT_TR_NOOP("DBus path")] = data.dbus_path; result[QT_TR_NOOP("D-Bus path")] = data.dbus_path;
result[QT_TR_NOOP("Serial number")] = data.serial; result[QT_TR_NOOP("Serial number")] = data.serial;
result[QT_TR_NOOP("Mount points")] = data.mount_paths.join(", "); result[QT_TR_NOOP("Mount points")] = data.mount_paths.join(", ");
result[QT_TR_NOOP("Partition label")] = data.label; result[QT_TR_NOOP("Partition label")] = data.label;

View File

@ -692,6 +692,18 @@ void GstEngine::HandlePipelineError(int pipeline_id, const QString& message,
qLog(Warning) << "Gstreamer error:" << message; qLog(Warning) << "Gstreamer error:" << message;
// try to reload the URL in case of a drop of the connection
if (domain == GST_RESOURCE_ERROR && error_code == GST_RESOURCE_ERROR_SEEK) {
if (Load(url_, 0, false, 0, 0)) {
current_pipeline_->SetState(GST_STATE_PLAYING);
return;
}
qLog(Warning) << "Attempt to reload " << url_ << " failed";
}
current_pipeline_.reset(); current_pipeline_.reset();
BufferingFinished(); BufferingFinished();

View File

@ -36,9 +36,11 @@
#include "core/logging.h" #include "core/logging.h"
#include "core/mimedata.h" #include "core/mimedata.h"
#include "core/timeconstants.h" #include "core/timeconstants.h"
#include "internet/core/internetsongmimedata.h"
#include "library/libraryfilterwidget.h" #include "library/libraryfilterwidget.h"
#include "library/librarymodel.h" #include "library/librarymodel.h"
#include "library/groupbydialog.h" #include "library/groupbydialog.h"
#include "playlist/songmimedata.h"
using std::placeholders::_1; using std::placeholders::_1;
using std::placeholders::_2; using std::placeholders::_2;
@ -446,34 +448,38 @@ bool GlobalSearchView::SearchKeyEvent(QKeyEvent* event) {
} }
bool GlobalSearchView::ResultsContextMenuEvent(QContextMenuEvent* event) { bool GlobalSearchView::ResultsContextMenuEvent(QContextMenuEvent* event) {
if (!context_menu_) { context_menu_ = new QMenu(this);
context_menu_ = new QMenu(this); context_actions_ << context_menu_->addAction(
context_actions_ << context_menu_->addAction( IconLoader::Load("media-playback-start", IconLoader::Base),
IconLoader::Load("media-playback-start", IconLoader::Base), tr("Append to current playlist"), this, SLOT(AddSelectedToPlaylist()));
tr("Append to current playlist"), this, context_actions_ << context_menu_->addAction(
SLOT(AddSelectedToPlaylist())); IconLoader::Load("media-playback-start", IconLoader::Base),
context_actions_ << context_menu_->addAction( tr("Replace current playlist"), this, SLOT(LoadSelected()));
IconLoader::Load("media-playback-start", IconLoader::Base), context_actions_ << context_menu_->addAction(
tr("Replace current playlist"), this, IconLoader::Load("document-new", IconLoader::Base),
SLOT(LoadSelected())); tr("Open in new playlist"), this, SLOT(OpenSelectedInNewPlaylist()));
context_actions_ << context_menu_->addAction(
IconLoader::Load("document-new", IconLoader::Base),
tr("Open in new playlist"), this,
SLOT(OpenSelectedInNewPlaylist()));
context_menu_->addSeparator(); context_menu_->addSeparator();
context_actions_ << context_menu_->addAction( context_actions_ << context_menu_->addAction(
IconLoader::Load("go-next", IconLoader::Base), tr("Queue track"), IconLoader::Load("go-next", IconLoader::Base), tr("Queue track"), this,
this, SLOT(AddSelectedToPlaylistEnqueue())); SLOT(AddSelectedToPlaylistEnqueue()));
context_menu_->addSeparator(); context_menu_->addSeparator();
context_menu_->addMenu(tr("Group by"))
->addActions(group_by_actions_->actions()); if (ui_->results->selectionModel() &&
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base), ui_->results->selectionModel()->selectedRows().length() == 1) {
tr("Configure global search..."), this, context_actions_ << context_menu_->addAction(
SLOT(OpenSettingsDialog())); IconLoader::Load("system-search", IconLoader::Base),
tr("Search for this"), this, SLOT(SearchForThis()));
} }
context_menu_->addSeparator();
context_menu_->addMenu(tr("Group by"))
->addActions(group_by_actions_->actions());
context_menu_->addAction(IconLoader::Load("configure", IconLoader::Base),
tr("Configure global search..."), this,
SLOT(OpenSettingsDialog()));
const bool enable_context_actions = const bool enable_context_actions =
ui_->results->selectionModel() && ui_->results->selectionModel() &&
ui_->results->selectionModel()->hasSelection(); ui_->results->selectionModel()->hasSelection();
@ -515,6 +521,11 @@ void GlobalSearchView::OpenSelectedInNewPlaylist() {
emit AddToPlaylist(data); emit AddToPlaylist(data);
} }
void GlobalSearchView::SearchForThis() {
StartSearch(
ui_->results->selectionModel()->selectedRows().first().data().toString());
}
void GlobalSearchView::showEvent(QShowEvent* e) { void GlobalSearchView::showEvent(QShowEvent* e) {
if (show_suggestions_) { if (show_suggestions_) {
UpdateSuggestions(); UpdateSuggestions();
@ -560,9 +571,12 @@ void GlobalSearchView::GroupByClicked(QAction* action) {
} }
void GlobalSearchView::SetGroupBy(const LibraryModel::Grouping& g) { void GlobalSearchView::SetGroupBy(const LibraryModel::Grouping& g) {
// Clear requests: changing "group by" on the models will cause all the items to be removed/added // Clear requests: changing "group by" on the models will cause all the items
// again, so all the QModelIndex here will become invalid. New requests will be created for those // to be removed/added
// songs when they will be displayed again anyway (when GlobalSearchItemDelegate::paint will call // again, so all the QModelIndex here will become invalid. New requests will
// be created for those
// songs when they will be displayed again anyway (when
// GlobalSearchItemDelegate::paint will call
// LazyLoadArt) // LazyLoadArt)
art_requests_.clear(); art_requests_.clear();
// Update the models // Update the models

View File

@ -21,6 +21,7 @@
#include "searchprovider.h" #include "searchprovider.h"
#include "library/librarymodel.h" #include "library/librarymodel.h"
#include "ui/settingsdialog.h" #include "ui/settingsdialog.h"
#include "playlist/playlistmanager.h"
#include <QWidget> #include <QWidget>
@ -82,6 +83,8 @@ signals:
void OpenSelectedInNewPlaylist(); void OpenSelectedInNewPlaylist();
void AddSelectedToPlaylistEnqueue(); void AddSelectedToPlaylistEnqueue();
void SearchForThis();
void GroupByClicked(QAction* action); void GroupByClicked(QAction* action);
void SetGroupBy(const LibraryModel::Grouping& grouping); void SetGroupBy(const LibraryModel::Grouping& grouping);

View File

@ -32,8 +32,12 @@
#include "lastfmservice.h" #include "lastfmservice.h"
#include <QCryptographicHash>
#include <QDesktopServices>
#include <QMenu> #include <QMenu>
#include <QMessageBox>
#include <QSettings> #include <QSettings>
#include <QUrlQuery>
#ifdef HAVE_LIBLASTFM1 #ifdef HAVE_LIBLASTFM1
#include <lastfm5/RadioStation.h> #include <lastfm5/RadioStation.h>
@ -42,14 +46,16 @@
#endif #endif
#include "lastfmcompat.h" #include "lastfmcompat.h"
#include "internet/core/internetmodel.h"
#include "internet/core/internetplaylistitem.h"
#include "core/application.h" #include "core/application.h"
#include "core/closure.h" #include "core/closure.h"
#include "core/logging.h" #include "core/logging.h"
#include "core/network.h"
#include "core/player.h" #include "core/player.h"
#include "core/song.h" #include "core/song.h"
#include "core/taskmanager.h" #include "core/taskmanager.h"
#include "internet/core/internetmodel.h"
#include "internet/core/internetplaylistitem.h"
#include "internet/core/localredirectserver.h"
#include "covers/coverproviders.h" #include "covers/coverproviders.h"
#include "covers/lastfmcoverprovider.h" #include "covers/lastfmcoverprovider.h"
#include "ui/iconloader.h" #include "ui/iconloader.h"
@ -75,7 +81,8 @@ LastFMService::LastFMService(Application* app, QObject* parent)
already_scrobbled_(false), already_scrobbled_(false),
scrobbling_enabled_(false), scrobbling_enabled_(false),
connection_problems_(false), connection_problems_(false),
app_(app) { app_(app),
network_(new NetworkAccessManager) {
#ifdef HAVE_LIBLASTFM1 #ifdef HAVE_LIBLASTFM1
lastfm::ws::setScheme(lastfm::ws::Https); lastfm::ws::setScheme(lastfm::ws::Https);
#endif #endif
@ -123,37 +130,53 @@ bool LastFMService::IsSubscriber() const {
return settings.value("Subscriber", false).toBool(); return settings.value("Subscriber", false).toBool();
} }
void LastFMService::GetToken() { namespace {
QMap<QString, QString> params; QByteArray SignApiRequest(QList<QPair<QString, QString>> params) {
params["method"] = "auth.getToken"; qSort(params);
QNetworkReply* reply = lastfm::ws::post(params); QString to_sign;
NewClosure(reply, SIGNAL(finished()), this, for (const auto& p : params) {
SLOT(GetTokenReplyFinished(QNetworkReply*)), reply); to_sign += p.first;
} to_sign += p.second;
void LastFMService::GetTokenReplyFinished(QNetworkReply* reply) {
reply->deleteLater();
// Parse the reply
lastfm::XmlQuery lfm(lastfm::compat::EmptyXmlQuery());
if (lastfm::compat::ParseQuery(reply->readAll(), &lfm)) {
QString token = lfm["token"].text();
emit TokenReceived(true, token);
} else {
emit TokenReceived(false, lfm["error"].text().trimmed());
} }
to_sign += LastFMService::kSecret;
return QCryptographicHash::hash(to_sign.toUtf8(), QCryptographicHash::Md5).toHex();
} }
} // namespace
void LastFMService::Authenticate(const QString& token) { void LastFMService::Authenticate() {
QMap<QString, QString> params; QUrl url("https://www.last.fm/api/auth/");
params["method"] = "auth.getSession"; QUrlQuery url_query;
params["token"] = token; url_query.addQueryItem("api_key", kApiKey);
QNetworkReply* reply = lastfm::ws::post(params); LocalRedirectServer* server = new LocalRedirectServer(this);
NewClosure(reply, SIGNAL(finished()), this, server->Listen();
SLOT(AuthenticateReplyFinished(QNetworkReply*)), reply);
// If we need more detailed error reporting, handle error(NetworkError) signal url_query.addQueryItem("cb", server->url().toString());
NewClosure(server, SIGNAL(Finished()), [this, server]() {
server->deleteLater();
const QUrl& url = server->request_url();
QString token = QUrlQuery(url).queryItemValue("token");
QUrl session_url("https://ws.audioscrobbler.com/2.0/");
QUrlQuery session_url_query;
session_url_query.addQueryItem("api_key", kApiKey);
session_url_query.addQueryItem("method", "auth.getSession");
session_url_query.addQueryItem("token", token);
session_url_query.addQueryItem("api_sig", SignApiRequest(session_url_query.queryItems()));
session_url.setQuery(session_url_query);
QNetworkReply* reply = network_->get(QNetworkRequest(session_url));
NewClosure(reply, SIGNAL(finished()), this, SLOT(AuthenticateReplyFinished(QNetworkReply*)), reply);
});
if (!QDesktopServices::openUrl(url)) {
QMessageBox box(QMessageBox::NoIcon, tr("Last.fm Authentication"), tr("Please open this URL in your browser: <a href=\"%1\">%1</a>").arg(url.toString()), QMessageBox::Ok);
box.setTextFormat(Qt::RichText);
qLog(Debug) << "Last.fm authentication URL: " << url.toString();
box.exec();
}
} }
void LastFMService::AuthenticateReplyFinished(QNetworkReply* reply) { void LastFMService::AuthenticateReplyFinished(QNetworkReply* reply) {
@ -174,14 +197,14 @@ void LastFMService::AuthenticateReplyFinished(QNetworkReply* reply) {
settings.setValue("Session", lastfm::ws::SessionKey); settings.setValue("Session", lastfm::ws::SessionKey);
settings.setValue("Subscriber", is_subscriber); settings.setValue("Subscriber", is_subscriber);
} else { } else {
emit AuthenticationComplete(false, lfm["error"].text().trimmed()); emit AuthenticationComplete(false);
return; return;
} }
// Invalidate the scrobbler - it will get recreated later // Invalidate the scrobbler - it will get recreated later
scrobbler_.reset(nullptr); scrobbler_.reset(nullptr);
emit AuthenticationComplete(true, QString()); emit AuthenticationComplete(true);
} }
void LastFMService::SignOut() { void LastFMService::SignOut() {

View File

@ -38,8 +38,8 @@ uint qHash(const lastfm::Track& track);
class Application; class Application;
class LastFMUrlHandler; class LastFMUrlHandler;
class NetworkAccessManager;
class QAction; class QAction;
class QNetworkAccessManager;
class Song; class Song;
class LastFMService : public Scrobbler { class LastFMService : public Scrobbler {
@ -69,8 +69,7 @@ class LastFMService : public Scrobbler {
bool PreferAlbumArtist() const { return prefer_albumartist_; } bool PreferAlbumArtist() const { return prefer_albumartist_; }
bool HasConnectionProblems() const { return connection_problems_; } bool HasConnectionProblems() const { return connection_problems_; }
void GetToken(); void Authenticate();
void Authenticate(const QString& token);
void SignOut(); void SignOut();
void UpdateSubscriberStatus(); void UpdateSubscriberStatus();
@ -83,8 +82,7 @@ class LastFMService : public Scrobbler {
void ToggleScrobbling(); void ToggleScrobbling();
signals: signals:
void TokenReceived(bool success, const QString& token); void AuthenticationComplete(bool success);
void AuthenticationComplete(bool success, const QString& error_message);
void ScrobblingEnabledChanged(bool value); void ScrobblingEnabledChanged(bool value);
void ButtonVisibilityChanged(bool value); void ButtonVisibilityChanged(bool value);
void ScrobbleButtonVisibilityChanged(bool value); void ScrobbleButtonVisibilityChanged(bool value);
@ -97,7 +95,6 @@ signals:
void SavedItemsChanged(); void SavedItemsChanged();
private slots: private slots:
void GetTokenReplyFinished(QNetworkReply* reply);
void AuthenticateReplyFinished(QNetworkReply* reply); void AuthenticateReplyFinished(QNetworkReply* reply);
void UpdateSubscriberStatusFinished(QNetworkReply* reply); void UpdateSubscriberStatusFinished(QNetworkReply* reply);
@ -129,6 +126,7 @@ signals:
bool connection_problems_; bool connection_problems_;
Application* app_; Application* app_;
std::unique_ptr<NetworkAccessManager> network_;
}; };
#endif // INTERNET_LASTFM_LASTFMSERVICE_H_ #endif // INTERNET_LASTFM_LASTFMSERVICE_H_

View File

@ -43,10 +43,8 @@ LastFMSettingsPage::LastFMSettingsPage(SettingsDialog* dialog)
// Icons // Icons
setWindowIcon(IconLoader::Load("lastfm", IconLoader::Provider)); setWindowIcon(IconLoader::Load("lastfm", IconLoader::Provider));
connect(service_, SIGNAL(TokenReceived(bool,QString)), connect(service_, SIGNAL(AuthenticationComplete(bool)),
SLOT(TokenReceived(bool,QString))); SLOT(AuthenticationComplete(bool)));
connect(service_, SIGNAL(AuthenticationComplete(bool, QString)),
SLOT(AuthenticationComplete(bool, QString)));
connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(Logout())); connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(Logout()));
connect(ui_->login_state, SIGNAL(LoginClicked()), SLOT(Login())); connect(ui_->login_state, SIGNAL(LoginClicked()), SLOT(Login()));
connect(ui_->login, SIGNAL(clicked()), SLOT(Login())); connect(ui_->login, SIGNAL(clicked()), SLOT(Login()));
@ -62,26 +60,10 @@ void LastFMSettingsPage::Login() {
waiting_for_auth_ = true; waiting_for_auth_ = true;
ui_->login_state->SetLoggedIn(LoginStateWidget::LoginInProgress); ui_->login_state->SetLoggedIn(LoginStateWidget::LoginInProgress);
service_->GetToken(); service_->Authenticate();
} }
void LastFMSettingsPage::TokenReceived(bool success, const QString &token) { void LastFMSettingsPage::AuthenticationComplete(bool success) {
if (!success) {
QMessageBox::warning(this, tr("Last.fm authentication failed"), token);
return;
}
QString url = QString(LastFMService::kAuthLoginUrl).arg(LastFMService::kApiKey, token);
QDesktopServices::openUrl(QUrl(url));
QMessageBox::information(this, tr("Last.fm authentication"),
tr("Click Ok once you authenticated Clementine in your last.fm account."));
service_->Authenticate(token);
}
void LastFMSettingsPage::AuthenticationComplete(bool success,
const QString& message) {
if (!waiting_for_auth_) return; // Wasn't us that was waiting for auth if (!waiting_for_auth_) return; // Wasn't us that was waiting for auth
waiting_for_auth_ = false; waiting_for_auth_ = false;
@ -90,10 +72,7 @@ void LastFMSettingsPage::AuthenticationComplete(bool success,
// Save settings // Save settings
Save(); Save();
} else { } else {
QString dialog_text = tr("Your Last.fm credentials were incorrect"); QString dialog_text = tr("Could not log in to Last.fm. Please try again.");
if (!message.isEmpty()) {
dialog_text = message;
}
QMessageBox::warning(this, tr("Authentication failed"), dialog_text); QMessageBox::warning(this, tr("Authentication failed"), dialog_text);
} }

View File

@ -38,8 +38,7 @@ class LastFMSettingsPage : public SettingsPage {
private slots: private slots:
void Login(); void Login();
void TokenReceived(bool success, const QString& token); void AuthenticationComplete(bool success);
void AuthenticationComplete(bool success, const QString& error_message);
void Logout(); void Logout();
private: private:

View File

@ -229,9 +229,16 @@ void PodcastParser::ParseItem(QXmlStreamReader* reader, Podcast* ret) const {
} }
} else if (name == "enclosure") { } else if (name == "enclosure") {
const QString type = reader->attributes().value("type").toString(); const QString type = reader->attributes().value("type").toString();
const QUrl url = QUrl::fromEncoded(reader->attributes().value("url").toString().toLatin1());
if (type.startsWith("audio/") || type.startsWith("x-audio/")) { if (type.startsWith("audio/") || type.startsWith("x-audio/")) {
episode.set_url(QUrl::fromEncoded( episode.set_url(url);
reader->attributes().value("url").toString().toLatin1())); }
// If the URL doesn't have a type, see if it's one of the obvious types
else if (type.isEmpty() && (
url.path().endsWith(".mp3", Qt::CaseInsensitive) ||
url.path().endsWith(".m4a", Qt::CaseInsensitive) ||
url.path().endsWith(".wav", Qt::CaseInsensitive))) {
episode.set_url(url);
} }
Utilities::ConsumeCurrentElement(reader); Utilities::ConsumeCurrentElement(reader);
} else if (name == "author" && lower_namespace == kItunesNamespace) { } else if (name == "author" && lower_namespace == kItunesNamespace) {

View File

@ -67,7 +67,7 @@ PodcastService::PodcastService(Application* app, InternetModel* parent)
proxy_(new PodcastSortProxyModel(this)), proxy_(new PodcastSortProxyModel(this)),
context_menu_(nullptr), context_menu_(nullptr),
root_(nullptr), root_(nullptr),
organise_dialog_(new OrganiseDialog(app_->task_manager(), nullptr)) { organise_dialog_(new OrganiseDialog(app_->task_manager())) {
icon_loader_->SetModel(model_); icon_loader_->SetModel(model_);
proxy_->setSourceModel(model_); proxy_->setSourceModel(model_);
proxy_->setDynamicSortFilter(true); proxy_->setDynamicSortFilter(true);
@ -128,7 +128,7 @@ bool PodcastSortProxyModel::lessThan(const QModelIndex& left,
} }
QStandardItem* PodcastService::CreateRootItem() { QStandardItem* PodcastService::CreateRootItem() {
root_ = new QStandardItem(IconLoader::Load("podcast", IconLoader::Provider), root_ = new QStandardItem(IconLoader::Load("podcast", IconLoader::Provider),
tr("Podcasts")); tr("Podcasts"));
root_->setData(true, InternetModel::Role_CanLazyLoad); root_->setData(true, InternetModel::Role_CanLazyLoad);
return root_; return root_;
@ -414,7 +414,7 @@ QStandardItem* PodcastService::CreatePodcastEpisodeItem(
void PodcastService::ShowContextMenu(const QPoint& global_pos) { void PodcastService::ShowContextMenu(const QPoint& global_pos) {
if (!context_menu_) { if (!context_menu_) {
context_menu_ = new QMenu; context_menu_ = new QMenu;
context_menu_->addAction(IconLoader::Load("list-add", IconLoader::Base), context_menu_->addAction(IconLoader::Load("list-add", IconLoader::Base),
tr("Add podcast..."), this, SLOT(AddPodcast())); tr("Add podcast..."), this, SLOT(AddPodcast()));
context_menu_->addAction(IconLoader::Load("view-refresh", IconLoader::Base), context_menu_->addAction(IconLoader::Load("view-refresh", IconLoader::Base),
tr("Update all podcasts"), app_->podcast_updater(), tr("Update all podcasts"), app_->podcast_updater(),
@ -425,23 +425,23 @@ void PodcastService::ShowContextMenu(const QPoint& global_pos) {
context_menu_->addSeparator(); context_menu_->addSeparator();
update_selected_action_ = context_menu_->addAction( update_selected_action_ = context_menu_->addAction(
IconLoader::Load("view-refresh", IconLoader::Base), IconLoader::Load("view-refresh", IconLoader::Base),
tr("Update this podcast"), this, SLOT(UpdateSelectedPodcast())); tr("Update this podcast"), this, SLOT(UpdateSelectedPodcast()));
download_selected_action_ = download_selected_action_ =
context_menu_->addAction(IconLoader::Load("download", IconLoader::Base), context_menu_->addAction(IconLoader::Load("download", IconLoader::Base),
"", this, SLOT(DownloadSelectedEpisode())); "", this, SLOT(DownloadSelectedEpisode()));
delete_downloaded_action_ = context_menu_->addAction( delete_downloaded_action_ = context_menu_->addAction(
IconLoader::Load("edit-delete", IconLoader::Base), IconLoader::Load("edit-delete", IconLoader::Base),
tr("Delete downloaded data"), this, SLOT(DeleteDownloadedData())); tr("Delete downloaded data"), this, SLOT(DeleteDownloadedData()));
copy_to_device_ = context_menu_->addAction( copy_to_device_ = context_menu_->addAction(
IconLoader::Load("multimedia-player-ipod-mini-blue", IconLoader::Base), IconLoader::Load("multimedia-player-ipod-mini-blue", IconLoader::Base),
tr("Copy to device..."), this, SLOT(CopyToDevice())); tr("Copy to device..."), this, SLOT(CopyToDevice()));
cancel_download_ = context_menu_->addAction(IconLoader::Load("cancel", cancel_download_ = context_menu_->addAction(IconLoader::Load("cancel",
IconLoader::Base), IconLoader::Base),
tr("Cancel download"), this, tr("Cancel download"), this,
SLOT(CancelDownload())); SLOT(CancelDownload()));
remove_selected_action_ = context_menu_->addAction( remove_selected_action_ = context_menu_->addAction(
IconLoader::Load("list-remove", IconLoader::Base), tr("Unsubscribe"), IconLoader::Load("list-remove", IconLoader::Base), tr("Unsubscribe"),
this, SLOT(RemoveSelectedPodcast())); this, SLOT(RemoveSelectedPodcast()));
context_menu_->addSeparator(); context_menu_->addSeparator();

View File

@ -42,6 +42,11 @@
#ifdef HAVE_CRYPTOPP #ifdef HAVE_CRYPTOPP
#include <cryptopp/pkcspad.h> #include <cryptopp/pkcspad.h>
#include <cryptopp/rsa.h> #include <cryptopp/rsa.h>
// Compatibility with cryptocpp >= 6.0.0
namespace CryptoPP {
typedef unsigned char byte;
}
#endif // HAVE_CRYPTOPP #endif // HAVE_CRYPTOPP
const char* SpotifyBlobDownloader::kSignatureSuffix = ".sha512"; const char* SpotifyBlobDownloader::kSignatureSuffix = ".sha512";
@ -189,7 +194,7 @@ bool SpotifyBlobDownloader::CheckSignature(
try { try {
CryptoPP::ByteQueue bytes; CryptoPP::ByteQueue bytes;
bytes.Put(reinterpret_cast<const byte*>(public_key_data.constData()), bytes.Put(reinterpret_cast<const CryptoPP::byte*>(public_key_data.constData()),
public_key_data.size()); public_key_data.size());
bytes.MessageEnd(); bytes.MessageEnd();
@ -204,9 +209,9 @@ bool SpotifyBlobDownloader::CheckSignature(
actual_filename.remove(kSignatureSuffix); actual_filename.remove(kSignatureSuffix);
const bool result = verifier.VerifyMessage( const bool result = verifier.VerifyMessage(
reinterpret_cast<const byte*>(file_data[actual_filename].constData()), reinterpret_cast<const CryptoPP::byte*>(file_data[actual_filename].constData()),
file_data[actual_filename].size(), file_data[actual_filename].size(),
reinterpret_cast<const byte*>( reinterpret_cast<const CryptoPP::byte*>(
file_data[signature_filename].constData()), file_data[signature_filename].constData()),
file_data[signature_filename].size()); file_data[signature_filename].size());
qLog(Debug) << "Verifying" << actual_filename << "against" qLog(Debug) << "Verifying" << actual_filename << "against"

View File

@ -220,6 +220,11 @@ void SubsonicDynamicPlaylist::GetAlbum(SubsonicService* service,
length *= kNsecPerSec; length *= kNsecPerSec;
song.set_length_nanosec(length); song.set_length_nanosec(length);
QUrl url = QUrl(QString("subsonic://%1").arg(id)); QUrl url = QUrl(QString("subsonic://%1").arg(id));
QUrl cover_url = service->BuildRequestUrl("getCoverArt");
QUrlQuery cover_url_query(cover_url.query());
cover_url_query.addQueryItem("id", id);
cover_url.setQuery(cover_url_query);
song.set_art_automatic(cover_url.toEncoded());
song.set_url(url); song.set_url(url);
song.set_filesize(reader.attributes().value("size").toString().toInt()); song.set_filesize(reader.attributes().value("size").toString().toInt());
QFileInfo fi(reader.attributes().value("path").toString()); QFileInfo fi(reader.attributes().value("path").toString());

View File

@ -542,6 +542,11 @@ void SubsonicLibraryScanner::OnGetAlbumFinished(QNetworkReply* reply) {
length *= kNsecPerSec; length *= kNsecPerSec;
song.set_length_nanosec(length); song.set_length_nanosec(length);
QUrl url = QUrl(QString("subsonic://%1").arg(id)); QUrl url = QUrl(QString("subsonic://%1").arg(id));
QUrl cover_url = service_->BuildRequestUrl("getCoverArt");
QUrlQuery cover_url_query(url.query());
cover_url_query.addQueryItem("id", id);
cover_url.setQuery(cover_url_query);
song.set_art_automatic(cover_url.toEncoded());
song.set_url(url); song.set_url(url);
song.set_filesize(reader.attributes().value("size").toString().toInt()); song.set_filesize(reader.attributes().value("size").toString().toInt());
// We need to set these to satisfy the database constraints // We need to set these to satisfy the database constraints

View File

@ -290,6 +290,16 @@ SongList LibraryBackend::FindSongsInDirectory(int id) {
return ret; return ret;
} }
void LibraryBackend::SongPathChanged(const Song& song,
const QFileInfo& new_file) {
// Take a song and update its path
Song updated_song = song;
updated_song.InitFromFilePartial(new_file.absoluteFilePath());
SongList updated_songs;
updated_songs << updated_song;
AddOrUpdateSongs(updated_songs);
}
void LibraryBackend::AddOrUpdateSubdirs(const SubdirectoryList& subdirs) { void LibraryBackend::AddOrUpdateSubdirs(const SubdirectoryList& subdirs) {
QMutexLocker l(db_->Mutex()); QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect()); QSqlDatabase db(db_->Connect());

View File

@ -21,6 +21,7 @@
#include <QObject> #include <QObject>
#include <QSet> #include <QSet>
#include <QUrl> #include <QUrl>
#include <QFileInfo>
#include "directory.h" #include "directory.h"
#include "libraryquery.h" #include "libraryquery.h"
@ -218,6 +219,8 @@ class LibraryBackend : public LibraryBackendInterface {
void ResetStatistics(int id); void ResetStatistics(int id);
void UpdateSongRating(int id, float rating); void UpdateSongRating(int id, float rating);
void UpdateSongsRating(const QList<int>& id_list, float rating); void UpdateSongsRating(const QList<int>& id_list, float rating);
// Tells the library model that a song path has changed
void SongPathChanged(const Song& song, const QFileInfo& new_file);
signals: signals:
void DirectoryDiscovered(const Directory& dir, void DirectoryDiscovered(const Directory& dir,

View File

@ -109,7 +109,7 @@ LibraryModel::LibraryModel(LibraryBackend* backend, Application* app,
QIcon nocover = IconLoader::Load("nocover", IconLoader::Other); QIcon nocover = IconLoader::Load("nocover", IconLoader::Other);
no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled( no_cover_icon_ = nocover.pixmap(nocover.availableSizes().last()).scaled(
kPrettyCoverSize, kPrettyCoverSize, kPrettyCoverSize, kPrettyCoverSize,
Qt::KeepAspectRatio, Qt::KeepAspectRatio,
Qt::SmoothTransformation); Qt::SmoothTransformation);
@ -657,6 +657,9 @@ QVariant LibraryModel::data(const LibraryItem* item, int role) const {
case Role_SortText: case Role_SortText:
return item->SortText(); return item->SortText();
case Role_DisplayText:
return item->DisplayText();
} }
return QVariant(); return QVariant();
} }

View File

@ -64,6 +64,7 @@ class LibraryModel : public SimpleTreeModel<LibraryItem> {
Role_Type = Qt::UserRole + 1, Role_Type = Qt::UserRole + 1,
Role_ContainerType, Role_ContainerType,
Role_SortText, Role_SortText,
Role_DisplayText,
Role_Key, Role_Key,
Role_Artist, Role_Artist,
Role_IsDivider, Role_IsDivider,

View File

@ -103,7 +103,7 @@ void LibraryItemDelegate::paint(QPainter* painter,
// Draw the line under the item // Draw the line under the item
QColor line_color = opt.palette.color(QPalette::Text); QColor line_color = opt.palette.color(QPalette::Text);
QLinearGradient grad_color(opt.rect.bottomLeft(), opt.rect.bottomRight()); QLinearGradient grad_color(opt.rect.bottomLeft(), opt.rect.bottomRight());
const double fade_start_end = (opt.rect.width()/3.0)/opt.rect.width(); const double fade_start_end = (opt.rect.width() / 3.0) / opt.rect.width();
line_color.setAlphaF(0.0); line_color.setAlphaF(0.0);
grad_color.setColorAt(0, line_color); grad_color.setColorAt(0, line_color);
line_color.setAlphaF(0.5); line_color.setAlphaF(0.5);
@ -175,7 +175,7 @@ LibraryView::LibraryView(QWidget* parent)
total_song_count_(-1), total_song_count_(-1),
context_menu_(nullptr), context_menu_(nullptr),
is_in_keyboard_search_(false) { is_in_keyboard_search_(false) {
QIcon nocover = IconLoader::Load("nocover", IconLoader::Other); QIcon nocover = IconLoader::Load("nocover", IconLoader::Other);
nomusic_ = nocover.pixmap(nocover.availableSizes().last()); nomusic_ = nocover.pixmap(nocover.availableSizes().last());
setItemDelegate(new LibraryItemDelegate(this)); setItemDelegate(new LibraryItemDelegate(this));
setAttribute(Qt::WA_MacShowFocusRect, false); setAttribute(Qt::WA_MacShowFocusRect, false);
@ -193,15 +193,17 @@ LibraryView::~LibraryView() {}
void LibraryView::SaveFocus() { void LibraryView::SaveFocus() {
QModelIndex current = currentIndex(); QModelIndex current = currentIndex();
QVariant type = model()->data(current, LibraryModel::Role_Type); QVariant type = model()->data(current, LibraryModel::Role_Type);
if (!type.isValid() || !(type.toInt() == LibraryItem::Type_Song || if (!type.isValid() ||
type.toInt() == LibraryItem::Type_Container || !(type.toInt() == LibraryItem::Type_Song ||
type.toInt() == LibraryItem::Type_Divider)) { type.toInt() == LibraryItem::Type_Container ||
type.toInt() == LibraryItem::Type_Divider)) {
return; return;
} }
last_selected_path_.clear(); last_selected_path_.clear();
last_selected_song_ = Song(); last_selected_song_ = Song();
last_selected_container_ = QString(); last_selected_container_ = QString();
last_selected_text_ = QString();
switch (type.toInt()) { switch (type.toInt()) {
case LibraryItem::Type_Song: { case LibraryItem::Type_Song: {
@ -210,6 +212,7 @@ void LibraryView::SaveFocus() {
SongList songs = app_->library_model()->GetChildSongs(index); SongList songs = app_->library_model()->GetChildSongs(index);
if (!songs.isEmpty()) { if (!songs.isEmpty()) {
last_selected_song_ = songs.last(); last_selected_song_ = songs.last();
last_selected_text_ = songs.last().title();
} }
break; break;
} }
@ -217,8 +220,9 @@ void LibraryView::SaveFocus() {
case LibraryItem::Type_Container: case LibraryItem::Type_Container:
case LibraryItem::Type_Divider: { case LibraryItem::Type_Divider: {
QString text = QString text =
model()->data(current, LibraryModel::Role_SortText).toString(); model()->data(current, LibraryModel::Role_Key).toString();
last_selected_container_ = text; last_selected_container_ = text;
last_selected_text_ = model()->data(current, LibraryModel::Role_DisplayText).toString();
break; break;
} }
@ -232,8 +236,9 @@ void LibraryView::SaveFocus() {
void LibraryView::SaveContainerPath(const QModelIndex& child) { void LibraryView::SaveContainerPath(const QModelIndex& child) {
QModelIndex current = model()->parent(child); QModelIndex current = model()->parent(child);
QVariant type = model()->data(current, LibraryModel::Role_Type); QVariant type = model()->data(current, LibraryModel::Role_Type);
if (!type.isValid() || !(type.toInt() == LibraryItem::Type_Container || if (!type.isValid() ||
type.toInt() == LibraryItem::Type_Divider)) { !(type.toInt() == LibraryItem::Type_Container ||
type.toInt() == LibraryItem::Type_Divider)) {
return; return;
} }
@ -276,7 +281,7 @@ bool LibraryView::RestoreLevelFocus(const QModelIndex& parent) {
case LibraryItem::Type_Container: case LibraryItem::Type_Container:
case LibraryItem::Type_Divider: { case LibraryItem::Type_Divider: {
QString text = QString text =
model()->data(current, LibraryModel::Role_SortText).toString(); model()->data(current, LibraryModel::Role_Key).toString();
if (!last_selected_container_.isEmpty() && if (!last_selected_container_.isEmpty() &&
last_selected_container_ == text) { last_selected_container_ == text) {
emit expand(current); emit expand(current);
@ -380,46 +385,48 @@ void LibraryView::contextMenuEvent(QContextMenuEvent* e) {
IconLoader::Load("media-playback-start", IconLoader::Base), IconLoader::Load("media-playback-start", IconLoader::Base),
tr("Replace current playlist"), this, SLOT(Load())); tr("Replace current playlist"), this, SLOT(Load()));
open_in_new_playlist_ = context_menu_->addAction( open_in_new_playlist_ = context_menu_->addAction(
IconLoader::Load("document-new", IconLoader::Base), IconLoader::Load("document-new", IconLoader::Base),
tr("Open in new playlist"), this, SLOT(OpenInNewPlaylist())); tr("Open in new playlist"), this, SLOT(OpenInNewPlaylist()));
context_menu_->addSeparator(); context_menu_->addSeparator();
add_to_playlist_enqueue_ = add_to_playlist_enqueue_ = context_menu_->addAction(
context_menu_->addAction(IconLoader::Load("go-next", IconLoader::Base), IconLoader::Load("go-next", IconLoader::Base), tr("Queue track"), this,
tr("Queue track"), this, SLOT(AddToPlaylistEnqueue()));
SLOT(AddToPlaylistEnqueue())); context_menu_->addSeparator();
search_for_this_ = context_menu_->addAction(
IconLoader::Load("system-search", IconLoader::Base),
tr("Search for this"), this, SLOT(SearchForThis()));
context_menu_->addSeparator(); context_menu_->addSeparator();
new_smart_playlist_ = context_menu_->addAction( new_smart_playlist_ = context_menu_->addAction(
IconLoader::Load("document-new", IconLoader::Base), IconLoader::Load("document-new", IconLoader::Base),
tr("New smart playlist..."), this, SLOT(NewSmartPlaylist())); tr("New smart playlist..."), this, SLOT(NewSmartPlaylist()));
edit_smart_playlist_ = context_menu_->addAction( edit_smart_playlist_ = context_menu_->addAction(
IconLoader::Load("edit-rename", IconLoader::Base), IconLoader::Load("edit-rename", IconLoader::Base),
tr("Edit smart playlist..."), this, SLOT(EditSmartPlaylist())); tr("Edit smart playlist..."), this, SLOT(EditSmartPlaylist()));
delete_smart_playlist_ = context_menu_->addAction( delete_smart_playlist_ = context_menu_->addAction(
IconLoader::Load("edit-delete", IconLoader::Base), IconLoader::Load("edit-delete", IconLoader::Base),
tr("Delete smart playlist"), this, SLOT(DeleteSmartPlaylist())); tr("Delete smart playlist"), this, SLOT(DeleteSmartPlaylist()));
context_menu_->addSeparator(); context_menu_->addSeparator();
organise_ = context_menu_->addAction(IconLoader::Load("edit-copy", IconLoader::Base), organise_ = context_menu_->addAction(
tr("Organise files..."), this, IconLoader::Load("edit-copy", IconLoader::Base),
SLOT(Organise())); tr("Organise files..."), this, SLOT(Organise()));
copy_to_device_ = context_menu_->addAction( copy_to_device_ = context_menu_->addAction(
IconLoader::Load("multimedia-player-ipod-mini-blue", IconLoader::Base), IconLoader::Load("multimedia-player-ipod-mini-blue", IconLoader::Base),
tr("Copy to device..."), this, SLOT(CopyToDevice())); tr("Copy to device..."), this, SLOT(CopyToDevice()));
delete_ = context_menu_->addAction(IconLoader::Load("edit-delete", IconLoader::Base), delete_ = context_menu_->addAction(
tr("Delete from disk..."), this, IconLoader::Load("edit-delete", IconLoader::Base),
SLOT(Delete())); tr("Delete from disk..."), this, SLOT(Delete()));
context_menu_->addSeparator(); context_menu_->addSeparator();
edit_track_ = context_menu_->addAction(IconLoader::Load("edit-rename", IconLoader::Base), edit_track_ = context_menu_->addAction(
tr("Edit track information..."), IconLoader::Load("edit-rename", IconLoader::Base),
this, SLOT(EditTracks())); tr("Edit track information..."), this, SLOT(EditTracks()));
edit_tracks_ = context_menu_->addAction(IconLoader::Load("edit-rename", IconLoader::Base), edit_tracks_ = context_menu_->addAction(
tr("Edit tracks information..."), IconLoader::Load("edit-rename", IconLoader::Base),
this, SLOT(EditTracks())); tr("Edit tracks information..."), this, SLOT(EditTracks()));
show_in_browser_ = context_menu_->addAction( show_in_browser_ = context_menu_->addAction(
IconLoader::Load("document-open-folder", IconLoader::Base), IconLoader::Load("document-open-folder", IconLoader::Base),
tr("Show in file browser..."), this, SLOT(ShowInBrowser())); tr("Show in file browser..."), this, SLOT(ShowInBrowser()));
context_menu_->addSeparator(); context_menu_->addSeparator();
@ -458,6 +465,8 @@ void LibraryView::contextMenuEvent(QContextMenuEvent* e) {
int regular_elements = 0; int regular_elements = 0;
// number of editable non smart playlists selected // number of editable non smart playlists selected
int regular_editable = 0; int regular_editable = 0;
// number of container elements selected
int container_elements = 0;
for (const QModelIndex& index : selected_indexes) { for (const QModelIndex& index : selected_indexes) {
int type = int type =
@ -467,6 +476,10 @@ void LibraryView::contextMenuEvent(QContextMenuEvent* e) {
smart_playlists++; smart_playlists++;
} else if (type == LibraryItem::Type_PlaylistContainer) { } else if (type == LibraryItem::Type_PlaylistContainer) {
smart_playlists_header++; smart_playlists_header++;
} else if (type == LibraryItem::Type_Container) {
container_elements++;
// To preserve expected behavior, since a container is "regular"
regular_elements++;
} else { } else {
regular_elements++; regular_elements++;
} }
@ -487,6 +500,10 @@ void LibraryView::contextMenuEvent(QContextMenuEvent* e) {
songs_selected == smart_playlists + smart_playlists_header; songs_selected == smart_playlists + smart_playlists_header;
const bool only_smart_playlist_selected = const bool only_smart_playlist_selected =
smart_playlists == 1 && songs_selected == 1; smart_playlists == 1 && songs_selected == 1;
const bool one_regular_song_only =
regular_elements_only && container_elements == 0 && regular_elements == 1;
const bool one_container_only =
container_elements == 1 && songs_selected == 1;
// in all modes // in all modes
load_->setEnabled(songs_selected); load_->setEnabled(songs_selected);
@ -509,6 +526,9 @@ void LibraryView::contextMenuEvent(QContextMenuEvent* e) {
show_in_various_->setVisible(regular_elements_only); show_in_various_->setVisible(regular_elements_only);
no_show_in_various_->setVisible(regular_elements_only); no_show_in_various_->setVisible(regular_elements_only);
// only when a single container or one song is selected exclusively
search_for_this_->setVisible(one_container_only || one_regular_song_only);
// only when all selected items are editable // only when all selected items are editable
organise_->setEnabled(regular_elements == regular_editable); organise_->setEnabled(regular_elements == regular_editable);
copy_to_device_->setEnabled(regular_elements == regular_editable); copy_to_device_->setEnabled(regular_elements == regular_editable);
@ -617,6 +637,7 @@ void LibraryView::scrollTo(const QModelIndex& index, ScrollHint hint) {
QTreeView::scrollTo(index, hint); QTreeView::scrollTo(index, hint);
} }
// get selected songs
SongList LibraryView::GetSelectedSongs() const { SongList LibraryView::GetSelectedSongs() const {
QModelIndexList selected_indexes = QModelIndexList selected_indexes =
qobject_cast<QSortFilterProxyModel*>(model()) qobject_cast<QSortFilterProxyModel*>(model())
@ -627,7 +648,8 @@ SongList LibraryView::GetSelectedSongs() const {
void LibraryView::Organise() { void LibraryView::Organise() {
if (!organise_dialog_) if (!organise_dialog_)
organise_dialog_.reset(new OrganiseDialog(app_->task_manager())); organise_dialog_.reset(new OrganiseDialog(app_->task_manager(),
app_->library_backend()));
organise_dialog_->SetDestinationModel( organise_dialog_->SetDestinationModel(
app_->library_model()->directory_model()); app_->library_model()->directory_model());
@ -675,6 +697,10 @@ void LibraryView::EditTracks() {
void LibraryView::CopyToDevice() { void LibraryView::CopyToDevice() {
if (!organise_dialog_) if (!organise_dialog_)
// Don't notify song has been replaced if copying to device, so
// don't associate the organise dialog with the library backend.
// Could improve this behavior if the device has a separate set of saved
// playlists that are somehow in sync with the library.
organise_dialog_.reset(new OrganiseDialog(app_->task_manager())); organise_dialog_.reset(new OrganiseDialog(app_->task_manager()));
organise_dialog_->SetDestinationModel( organise_dialog_->SetDestinationModel(
@ -709,6 +735,13 @@ void LibraryView::FilterReturnPressed() {
emit doubleClicked(currentIndex()); emit doubleClicked(currentIndex());
} }
void LibraryView::SearchForThis() {
SaveFocus();
if (!last_selected_text_.isEmpty()) {
filter_->ShowInLibrary(last_selected_text_.simplified());
}
}
void LibraryView::NewSmartPlaylist() { void LibraryView::NewSmartPlaylist() {
Wizard* wizard = new Wizard(app_, app_->library_backend(), this); Wizard* wizard = new Wizard(app_, app_->library_backend(), this);
wizard->setAttribute(Qt::WA_DeleteOnClose); wizard->setAttribute(Qt::WA_DeleteOnClose);

View File

@ -101,6 +101,8 @@ signals:
void ShowInVarious(); void ShowInVarious();
void NoShowInVarious(); void NoShowInVarious();
void SearchForThis();
void NewSmartPlaylist(); void NewSmartPlaylist();
void EditSmartPlaylist(); void EditSmartPlaylist();
void DeleteSmartPlaylist(); void DeleteSmartPlaylist();
@ -139,6 +141,8 @@ signals:
QAction* show_in_various_; QAction* show_in_various_;
QAction* no_show_in_various_; QAction* no_show_in_various_;
QAction* search_for_this_;
QAction* new_smart_playlist_; QAction* new_smart_playlist_;
QAction* edit_smart_playlist_; QAction* edit_smart_playlist_;
QAction* delete_smart_playlist_; QAction* delete_smart_playlist_;
@ -151,6 +155,7 @@ signals:
// Save focus // Save focus
Song last_selected_song_; Song last_selected_song_;
QString last_selected_container_; QString last_selected_container_;
QString last_selected_text_;
QSet<QString> last_selected_path_; QSet<QString> last_selected_path_;
}; };

View File

@ -247,7 +247,7 @@ int main(int argc, char* argv[]) {
if (QSysInfo::MacintoshVersion > QSysInfo::MV_10_8) { if (QSysInfo::MacintoshVersion > QSysInfo::MV_10_8) {
// Work around 10.9 issue. // Work around 10.9 issue.
// https://bugreports.qt-project.org/browse/QTBUG-32789 // https://bugreports.qt.io/browse/QTBUG-32789
QFont::insertSubstitution(".Lucida Grande UI", "Lucida Grande"); QFont::insertSubstitution(".Lucida Grande UI", "Lucida Grande");
} }
#endif #endif
@ -292,7 +292,10 @@ int main(int argc, char* argv[]) {
qLog(Info) qLog(Info)
<< "Clementine is already running - activating existing window"; << "Clementine is already running - activating existing window";
} }
if (a.sendMessage(options.Serialize(), 5000)) {
QByteArray serializedOptions = options.Serialize();
if (a.sendMessage(serializedOptions, 5000)) {
qLog(Info) << "Options found, sent message to running instance";
return 0; return 0;
} }
// Couldn't send the message so start anyway // Couldn't send the message so start anyway

View File

@ -19,6 +19,7 @@
#include "playlistmanager.h" #include "playlistmanager.h"
#include "ui_playlistcontainer.h" #include "ui_playlistcontainer.h"
#include "core/logging.h" #include "core/logging.h"
#include "core/appearance.h"
#include "playlistparsers/playlistparser.h" #include "playlistparsers/playlistparser.h"
#include "ui/iconloader.h" #include "ui/iconloader.h"
@ -71,6 +72,11 @@ PlaylistContainer::PlaylistContainer(QWidget* parent)
// Remove QFrame border // Remove QFrame border
ui_->toolbar->setStyleSheet("QFrame { border: 0px; }"); ui_->toolbar->setStyleSheet("QFrame { border: 0px; }");
QSettings settings;
settings.beginGroup(Appearance::kSettingsGroup);
bool hide_toolbar = settings.value("b_hide_filter_toolbar", false).toBool();
ui_->toolbar->setVisible(!hide_toolbar);
// Make it bold // Make it bold
QFont no_matches_font = no_matches_label_->font(); QFont no_matches_font = no_matches_label_->font();
no_matches_font.setBold(true); no_matches_font.setBold(true);
@ -452,3 +458,10 @@ bool PlaylistContainer::eventFilter(QObject* objectWatched, QEvent* event) {
} }
return QWidget::eventFilter(objectWatched, event); return QWidget::eventFilter(objectWatched, event);
} }
void PlaylistContainer::ReloadSettings() {
QSettings settings;
settings.beginGroup(Appearance::kSettingsGroup);
bool hide_toolbar = settings.value("b_hide_filter_toolbar", false).toBool();
ui_->toolbar->setVisible(!hide_toolbar);
}

View File

@ -61,6 +61,9 @@ signals:
// QWidget // QWidget
void resizeEvent(QResizeEvent*); void resizeEvent(QResizeEvent*);
public slots:
void ReloadSettings();
private slots: private slots:
void NewPlaylist(); void NewPlaylist();
void LoadPlaylist(); void LoadPlaylist();

View File

@ -617,9 +617,6 @@ void PlaylistView::keyPressEvent(QKeyEvent* event) {
event->key() == Qt::Key_Space) { event->key() == Qt::Key_Space) {
emit PlayPause(); emit PlayPause();
event->accept(); event->accept();
} else if (event->key() == Qt::Key_Up) {
app_->player()->SeekTo(0);
event->accept();
} else if (event->key() == Qt::Key_Left) { } else if (event->key() == Qt::Key_Left) {
emit SeekBackward(); emit SeekBackward();
event->accept(); event->accept();

View File

@ -71,7 +71,7 @@
</size> </size>
</property> </property>
<property name="shortcut"> <property name="shortcut">
<string>Ctrl+Up</string> <string>Ctrl+</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -90,7 +90,7 @@
</size> </size>
</property> </property>
<property name="shortcut"> <property name="shortcut">
<string>Ctrl+Down</string> <string>Ctrl+</string>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -11,7 +11,7 @@
</rect> </rect>
</property> </property>
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed"> <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch> <horstretch>0</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>

View File

@ -230,7 +230,7 @@ QList<TranscoderPreset> Transcoder::GetAllPresets() {
TranscoderPreset Transcoder::PresetForFileType(Song::FileType type) { TranscoderPreset Transcoder::PresetForFileType(Song::FileType type) {
switch (type) { switch (type) {
case Song::Type_Flac: case Song::Type_Flac:
return TranscoderPreset(type, tr("Flac"), "flac", "audio/x-flac"); return TranscoderPreset(type, tr("FLAC"), "flac", "audio/x-flac");
case Song::Type_Mp4: case Song::Type_Mp4:
return TranscoderPreset(type, tr("M4A AAC"), "mp4", return TranscoderPreset(type, tr("M4A AAC"), "mp4",
"audio/mpeg, mpegversion=(int)4", "audio/mp4"); "audio/mpeg, mpegversion=(int)4", "audio/mp4");

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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