From 9714b0632d11ec99422b15c2d1e846ba482b9914 Mon Sep 17 00:00:00 2001 From: Matthieu Bruel Date: Fri, 27 Nov 2020 13:05:53 +0100 Subject: [PATCH] All changes for ClemRemote v1.0 (in one go) --- .../remotecontrolmessages.proto | 113 ++++++++++-- src/CMakeLists.txt | 2 + src/core/application.cpp | 2 + src/core/application.h | 1 + src/core/mimedata.h | 7 +- src/internet/internetradio/savedradio.cpp | 20 +- src/internet/internetradio/savedradio.h | 11 +- src/networkremote/incomingdataparser.cpp | 172 +++++++++++++++--- src/networkremote/incomingdataparser.h | 16 ++ src/networkremote/networkremote.cpp | 20 ++ src/networkremote/networkremote.h | 5 + src/networkremote/outgoingdatacreator.cpp | 103 +++++++++-- src/networkremote/outgoingdatacreator.h | 16 +- src/networkremote/remoteclient.cpp | 20 +- src/networkremote/remoteclient.h | 12 +- src/networkremote/songsender.cpp | 58 +++++- src/networkremote/songsender.h | 2 +- src/playlist/playlistmanager.cpp | 15 +- src/playlist/playlistmanager.h | 9 +- src/ui/addstreamdialog.cpp | 11 +- src/ui/addstreamdialog.h | 2 + src/ui/addstreamdialog.ui | 22 ++- src/ui/mainwindow.cpp | 15 +- src/ui/networkremotesettingspage.cpp | 18 ++ src/ui/networkremotesettingspage.ui | 47 ++++- 25 files changed, 606 insertions(+), 113 deletions(-) diff --git a/ext/libclementine-remote/remotecontrolmessages.proto b/ext/libclementine-remote/remotecontrolmessages.proto index 4dd5ca9ce..4cb1e6524 100644 --- a/ext/libclementine-remote/remotecontrolmessages.proto +++ b/ext/libclementine-remote/remotecontrolmessages.proto @@ -15,7 +15,7 @@ limitations under the License. */ -// Note: this file is licensed under the Apache License instead of GPL, so +// Note: this file is licensed under the Apache License instead of GPL, so // 3rd party applications or libraries can use another license besides GPL. syntax = "proto2"; @@ -36,6 +36,7 @@ enum MsgType { REMOVE_SONGS = 9; OPEN_PLAYLIST = 10; CLOSE_PLAYLIST = 11; + UPDATE_PLAYLIST = 60; GET_LYRICS = 14; DOWNLOAD_SONGS = 15; SONG_OFFER_RESPONSE = 16; @@ -46,6 +47,10 @@ enum MsgType { GET_LIBRARY = 18; RATE_SONG = 19; GLOBAL_SEARCH = 100; + REQUEST_SAVED_RADIOS = 110; + // access Files from remote control + REQUEST_FILES = 200; + APPEND_FILES = 201; // Messages send by both DISCONNECT = 2; @@ -79,6 +84,8 @@ enum MsgType { GLOBAL_SEARCH_RESULT = 54; TRANSCODING_FILES = 55; GLOBAL_SEARCH_STATUS = 56; + // access Files from remote control + LIST_FILES = 202; } // Valid Engine states @@ -113,8 +120,8 @@ message SongMetadata { STREAM = 99; } - optional int32 id = 1; // unique id of the song - optional int32 index = 2; // Index of the current row of the active playlist + optional int32 id = 1; // unique id of the song + optional int32 index = 2; // Index of the current row of the active playlist optional string title = 3; optional string album = 4; optional string artist = 5; @@ -130,7 +137,7 @@ message SongMetadata { optional bool is_local = 15; optional string filename = 16; optional int32 file_size = 17; - optional float rating = 18; // 0 (0 stars) to 1 (5 stars) + optional float rating = 18; // 0 (0 stars) to 1 (5 stars) optional string url = 19; optional string art_automatic = 20; optional string art_manual = 21; @@ -144,6 +151,7 @@ message Playlist { optional int32 item_count = 3; optional bool active = 4; optional bool closed = 5; + optional bool favorite = 6; } // Valid Repeatmodes @@ -200,6 +208,11 @@ message Shuffle { message ResponseClementineInfo { optional string version = 1; optional EngineState state = 2; + + optional bool allow_downloads = 3; + + // allowed extensions for REQUEST_FILES and LIST_FILES + repeated string files_music_extensions = 4; } // The current song played @@ -210,12 +223,13 @@ message ResponseCurrentMetadata { // The playlists in clementine message ResponsePlaylists { repeated Playlist playlist = 1; + optional bool include_closed = 2; } // A list of songs in a playlist message ResponsePlaylistSongs { optional Playlist requested_playlist = 1; - + // The songs that are in the playlist repeated SongMetadata songs = 2; } @@ -226,7 +240,7 @@ message ResponseEngineStateChanged { } // Sends the current position of the track -message ResponseUpdateTrackPosition { +message ResponseUpdateTrackPosition { optional int32 position = 1; } @@ -262,10 +276,12 @@ message RequestInsertUrls { // In which playlist should the urls be inserted? optional int32 playlist_id = 1; repeated string urls = 2; - optional int32 position = 3 [default=-1]; - optional bool play_now = 4 [default=false]; - optional bool enqueue = 5 [default=false]; + optional int32 position = 3 [default = -1]; + optional bool play_now = 4 [default = false]; + optional bool enqueue = 5 [default = false]; repeated SongMetadata songs = 6; + // if we wish to create a new playlist + optional string new_playlist_name = 7; } // Client want to change track @@ -283,6 +299,13 @@ message RequestOpenPlaylist { message RequestClosePlaylist { optional int32 playlist_id = 1; } +message RequestUpdatePlaylist { + optional int32 playlist_id = 1; + optional string new_playlist_name = 2; + optional bool favorite = 3; + optional bool create_new_playlist = 4; + optional bool clear_playlist = 5; +} // Message containing lyrics message ResponseLyrics { @@ -305,6 +328,13 @@ message RequestDownloadSongs { optional DownloadItem download_item = 1; optional int32 playlist_id = 2; repeated string urls = 3; + + // within a Playlist, download only requested songs + repeated int32 songs_ids = 4; + + // download from the FileSystem remotely + // using the defined root directory and the urls (filenames) + optional string relative_path = 5; } message ResponseSongFileChunk { @@ -312,7 +342,7 @@ message ResponseSongFileChunk { optional int32 chunk_count = 2; optional int32 file_number = 3; optional int32 file_count = 4; - optional SongMetadata song_metadata = 6; // only sent with first chunk! + optional SongMetadata song_metadata = 6; // only sent with first chunk! optional bytes data = 7; optional int32 size = 8; optional bytes file_hash = 9; @@ -327,11 +357,11 @@ message ResponseLibraryChunk { } message ResponseSongOffer { - optional bool accepted = 1; // true = client wants to download item + optional bool accepted = 1; // true = client wants to download item } message RequestRateSong { - optional float rating = 1; // 0 to 1 + optional float rating = 1; // 0 to 1 } message ResponseDownloadTotalSize { @@ -367,10 +397,56 @@ message ResponseGlobalSearchStatus { optional GlobalSearchStatus status = 3; } +// access the FileSystem remotely from a defined root directory +message RequestListFiles { + optional string relative_path = 1; +} + +message FileMetadata { + optional string filename = 1; + optional bool is_dir = 2; +} + +message ResponseListFiles { + enum Error { + NONE = 0; + ROOT_DIR_NOT_SET = 1; + DIR_NOT_ACCESSIBLE = 2; + DIR_NOT_EXIST = 3; + UNKNOWN = 4; + } + optional string relative_path = 1; + repeated FileMetadata files = 2; + optional Error error = 3; +} + +message RequestAppendFiles { + // where to append the files + optional int32 playlist_id = 1; + // or we create a new playlist + optional string new_playlist_name = 2; + + optional string relative_path = 3; + repeated string files = 4; + + optional bool play_now = 5; + optional bool clear_first = 6; +} + +message Stream { + optional string name = 1; + optional string url = 2; + optional string url_logo = 3; +} +message ResponseSavedRadios { + repeated Stream streams = 1; +} + // The message itself message Message { - optional int32 version = 1 [default=21]; - optional MsgType type = 2 [default=UNKNOWN]; // What data is in the message? + optional int32 version = 1 [default = 21]; + optional MsgType type = 2 + [default = UNKNOWN]; // What data is in the message? optional RequestConnect request_connect = 21; optional RequestPlaylists request_playlists = 27; @@ -382,13 +458,16 @@ message Message { optional RequestRemoveSongs request_remove_songs = 26; optional RequestOpenPlaylist request_open_playlist = 28; optional RequestClosePlaylist request_close_playlist = 29; + optional RequestUpdatePlaylist request_update_playlist = 53; optional RequestDownloadSongs request_download_songs = 31; optional RequestRateSong request_rate_song = 35; optional RequestGlobalSearch request_global_search = 37; - + optional RequestListFiles request_list_files = 50; + optional RequestAppendFiles request_append_files = 51; + optional Repeat repeat = 13; optional Shuffle shuffle = 14; - + optional ResponseClementineInfo response_clementine_info = 15; optional ResponseCurrentMetadata response_current_metadata = 16; optional ResponsePlaylists response_playlists = 17; @@ -405,4 +484,6 @@ message Message { optional ResponseGlobalSearch response_global_search = 38; optional ResponseTranscoderStatus response_transcoder_status = 39; optional ResponseGlobalSearchStatus response_global_search_status = 40; + optional ResponseListFiles response_list_files = 52; + optional ResponseSavedRadios response_saved_radios = 54; } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0bbc775ba..186a9e60e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -339,6 +339,7 @@ set(SOURCES ui/edittagdialog.cpp ui/lovedialog.cpp ui/equalizer.cpp + ui/filechooserwidget.cpp ui/flowlayout.cpp ui/globalshortcutgrabber.cpp ui/globalshortcutssettingspage.cpp @@ -625,6 +626,7 @@ set(HEADERS ui/console.h ui/coverfromurldialog.h ui/edittagdialog.h + ui/filechooserwidget.h ui/lovedialog.h ui/equalizer.h ui/globalshortcutgrabber.h diff --git a/src/core/application.cpp b/src/core/application.cpp index 1d6a0367c..8c64c348d 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -69,6 +69,8 @@ bool Application::kIsPortable = false; const char* Application::kLegacyPortableDataDir = "data"; const char* Application::kDefaultPortableDataDir = "clementine-data"; const char* Application::kPortableDataDir = nullptr; +const QStringList Application::kDefaultMusicExtensionsAllowedRemotely = { + "aac", "alac", "flac", "m3u", "m4a", "mp3", "ogg", "wav", "wmv"}; class ApplicationImpl { public: diff --git a/src/core/application.h b/src/core/application.h index 38eba66f0..6b5663228 100644 --- a/src/core/application.h +++ b/src/core/application.h @@ -65,6 +65,7 @@ class Application : public QObject { static const char* kPortableDataDir; static const char* kLegacyPortableDataDir; static const char* kDefaultPortableDataDir; + static const QStringList kDefaultMusicExtensionsAllowedRemotely; static bool IsPortable() { return kIsPortable; } explicit Application(QObject* parent = nullptr); diff --git a/src/core/mimedata.h b/src/core/mimedata.h index 3d2c2aeab..ca1fa5c64 100644 --- a/src/core/mimedata.h +++ b/src/core/mimedata.h @@ -36,7 +36,8 @@ class MimeData : public QMimeData { enqueue_next_now_(enqueue_next_now), open_in_new_playlist_(open_in_new_playlist), name_for_new_playlist_(QString()), - from_doubleclick_(false) {} + from_doubleclick_(false), + playlist_id(-1) {} // If this is set then MainWindow will not touch any of the other flags. bool override_user_settings_; @@ -69,6 +70,10 @@ class MimeData : public QMimeData { // the defaults set by the user. bool from_doubleclick_; + // The Network Remote can use this MimeData to drop songs on another + // playlist than the one currently opened on the server + int playlist_id; + // Returns a pretty name for a playlist containing songs described by this // MimeData // object. By pretty name we mean the value of 'name_for_new_playlist_' or diff --git a/src/internet/internetradio/savedradio.cpp b/src/internet/internetradio/savedradio.cpp index 279e51875..1d7037f02 100644 --- a/src/internet/internetradio/savedradio.cpp +++ b/src/internet/internetradio/savedradio.cpp @@ -79,7 +79,8 @@ void SavedRadio::LoadStreams() { for (int i = 0; i < count; ++i) { s.setArrayIndex(i); streams_ << Stream(QUrl(s.value("url").toString()), - s.value("name").toString()); + s.value("name").toString(), + QUrl(s.value("url_logo").toString())); } s.endArray(); } @@ -94,6 +95,7 @@ void SavedRadio::SaveStreams() { s.setArrayIndex(i); s.setValue("url", streams_[i].url_); s.setValue("name", streams_[i].name_); + s.setValue("url_logo", streams_[i].url_logo_); } s.endArray(); @@ -148,15 +150,18 @@ void SavedRadio::Edit() { edit_dialog_->set_save_visible(false); } - edit_dialog_->set_name(context_item->text()); - edit_dialog_->set_url(context_item->data(InternetModel::Role_Url).toUrl()); - if (edit_dialog_->exec() == QDialog::Rejected) return; - int i = streams_.indexOf( Stream(QUrl(context_item->data(InternetModel::Role_Url).toUrl()))); Stream* stream = &streams_[i]; + + edit_dialog_->set_name(context_item->text()); + edit_dialog_->set_url(context_item->data(InternetModel::Role_Url).toUrl()); + edit_dialog_->set_url_logo(stream->url_logo_); + if (edit_dialog_->exec() == QDialog::Rejected) return; + stream->name_ = edit_dialog_->name(); stream->url_ = edit_dialog_->url(); + stream->url_logo_ = edit_dialog_->url_logo(); context_item->setText(stream->name_); context_item->setData(stream->url_, InternetModel::Role_Url); @@ -173,10 +178,11 @@ void SavedRadio::AddStreamToList(const Stream& stream, QStandardItem* parent) { parent->appendRow(s); } -void SavedRadio::Add(const QUrl& url, const QString& name) { +void SavedRadio::Add(const QUrl& url, const QString& name, + const QUrl& url_logo) { if (streams_.contains(Stream(url))) return; - Stream stream(url, name); + Stream stream(url, name, url_logo); streams_ << stream; if (!root_->data(InternetModel::Role_CanLazyLoad).toBool()) { diff --git a/src/internet/internetradio/savedradio.h b/src/internet/internetradio/savedradio.h index 9ff193f9d..c275da64f 100644 --- a/src/internet/internetradio/savedradio.h +++ b/src/internet/internetradio/savedradio.h @@ -41,14 +41,16 @@ class SavedRadio : public InternetService { }; struct Stream { - explicit Stream(const QUrl& url, const QString& name = QString()) - : url_(url), name_(name) {} + explicit Stream(const QUrl& url, const QString& name = QString(), + const QUrl& url_logo = QUrl()) + : url_(url), name_(name), url_logo_(url_logo) {} // For QList::contains bool operator==(const Stream& other) const { return url_ == other.url_; } QUrl url_; QString name_; + QUrl url_logo_; }; typedef QList StreamList; @@ -60,9 +62,10 @@ class SavedRadio : public InternetService { void ShowContextMenu(const QPoint& global_pos); - void Add(const QUrl& url, const QString& name = QString()); + void Add(const QUrl& url, const QString& name = QString(), + const QUrl& url_logo = QUrl()); - StreamList Streams() const { return streams_; } + const StreamList& Streams() const { return streams_; } signals: void ShowAddStreamDialog(); diff --git a/src/networkremote/incomingdataparser.cpp b/src/networkremote/incomingdataparser.cpp index 883b65683..18a67ef0f 100644 --- a/src/networkremote/incomingdataparser.cpp +++ b/src/networkremote/incomingdataparser.cpp @@ -17,9 +17,11 @@ #include "incomingdataparser.h" +#include #include #include "core/logging.h" +#include "core/mimedata.h" #include "core/timeconstants.h" #include "engines/enginebase.h" #include "internet/core/internetmodel.h" @@ -36,47 +38,55 @@ IncomingDataParser::IncomingDataParser(Application* app) : app_(app) { ReloadSettings(); connect(app_, SIGNAL(SettingsChanged()), SLOT(ReloadSettings())); + Player* player = app_->player(); + PlaylistManager* playlist_manager = app_->playlist_manager(); + // Connect all the signals // due the player is in a different thread, we cannot access these functions // directly - connect(this, SIGNAL(Play()), app_->player(), SLOT(Play())); - connect(this, SIGNAL(PlayPause()), app_->player(), SLOT(PlayPause())); - connect(this, SIGNAL(Pause()), app_->player(), SLOT(Pause())); - connect(this, SIGNAL(Stop()), app_->player(), SLOT(Stop())); - connect(this, SIGNAL(StopAfterCurrent()), app_->player(), - SLOT(StopAfterCurrent())); - connect(this, SIGNAL(Next()), app_->player(), SLOT(Next())); - connect(this, SIGNAL(Previous()), app_->player(), SLOT(Previous())); - connect(this, SIGNAL(SetVolume(int)), app_->player(), SLOT(SetVolume(int))); - connect(this, SIGNAL(PlayAt(int, Engine::TrackChangeFlags, bool)), - app_->player(), SLOT(PlayAt(int, Engine::TrackChangeFlags, bool))); - connect(this, SIGNAL(SeekTo(int)), app_->player(), SLOT(SeekTo(int))); - connect(this, SIGNAL(Enque(int, int)), app_->playlist_manager(), + connect(this, SIGNAL(Play()), player, SLOT(Play())); + connect(this, SIGNAL(PlayPause()), player, SLOT(PlayPause())); + connect(this, SIGNAL(Pause()), player, SLOT(Pause())); + connect(this, SIGNAL(Stop()), player, SLOT(Stop())); + connect(this, SIGNAL(StopAfterCurrent()), player, SLOT(StopAfterCurrent())); + connect(this, SIGNAL(Next()), player, SLOT(Next())); + connect(this, SIGNAL(Previous()), player, SLOT(Previous())); + connect(this, SIGNAL(SetVolume(int)), player, SLOT(SetVolume(int))); + connect(this, SIGNAL(PlayAt(int, Engine::TrackChangeFlags, bool)), player, + SLOT(PlayAt(int, Engine::TrackChangeFlags, bool))); + connect(this, SIGNAL(SeekTo(int)), player, SLOT(SeekTo(int))); + connect(this, SIGNAL(Enque(int, int)), playlist_manager, SLOT(Enque(int, int))); - connect(this, SIGNAL(SetActivePlaylist(int)), app_->playlist_manager(), + connect(this, SIGNAL(SetActivePlaylist(int)), playlist_manager, SLOT(SetActivePlaylist(int))); - connect(this, SIGNAL(ShuffleCurrent()), app_->playlist_manager(), + connect(this, SIGNAL(ShuffleCurrent()), playlist_manager, SLOT(ShuffleCurrent())); connect(this, SIGNAL(SetRepeatMode(PlaylistSequence::RepeatMode)), - app_->playlist_manager()->sequence(), + playlist_manager->sequence(), SLOT(SetRepeatMode(PlaylistSequence::RepeatMode))); connect(this, SIGNAL(SetShuffleMode(PlaylistSequence::ShuffleMode)), - app_->playlist_manager()->sequence(), + playlist_manager->sequence(), SLOT(SetShuffleMode(PlaylistSequence::ShuffleMode))); connect(this, SIGNAL(InsertUrls(int, const QList&, int, bool, bool)), - app_->playlist_manager(), + playlist_manager, SLOT(InsertUrls(int, const QList&, int, bool, bool))); connect(this, SIGNAL(InsertSongs(int, const SongList&, int, bool, bool)), - app_->playlist_manager(), + playlist_manager, SLOT(InsertSongs(int, const SongList&, int, bool, bool))); - connect(this, SIGNAL(RemoveSongs(int, const QList&)), - app_->playlist_manager(), + connect(this, SIGNAL(RemoveSongs(int, const QList&)), playlist_manager, SLOT(RemoveItemsWithoutUndo(int, const QList&))); - connect(this, SIGNAL(Open(int)), app_->playlist_manager(), SLOT(Open(int))); - connect(this, SIGNAL(Close(int)), app_->playlist_manager(), SLOT(Close(int))); + connect(this, SIGNAL(New(const QString&)), playlist_manager, + SLOT(New(const QString&))); + connect(this, SIGNAL(Open(int)), playlist_manager, SLOT(Open(int))); + connect(this, SIGNAL(Close(int)), playlist_manager, SLOT(Close(int))); + connect(this, SIGNAL(Clear(int)), playlist_manager, SLOT(Clear(int))); + connect(this, SIGNAL(Rename(int, const QString&)), playlist_manager, + SLOT(Rename(int, const QString&))); + connect(this, SIGNAL(Favorite(int, bool)), playlist_manager, + SLOT(Favorite(int, bool))); - connect(this, SIGNAL(RateCurrentSong(double)), app_->playlist_manager(), + connect(this, SIGNAL(RateCurrentSong(double)), playlist_manager, SLOT(RateCurrentSong(double))); #ifdef HAVE_LIBLASTFM @@ -99,7 +109,6 @@ bool IncomingDataParser::close_connection() { return close_connection_; } void IncomingDataParser::Parse(const pb::remote::Message& msg) { close_connection_ = false; - RemoteClient* client = qobject_cast(sender()); // Now check what's to do @@ -167,6 +176,9 @@ void IncomingDataParser::Parse(const pb::remote::Message& msg) { case pb::remote::CLOSE_PLAYLIST: ClosePlaylist(msg); break; + case pb::remote::UPDATE_PLAYLIST: + UpdatePlaylist(msg); + break; case pb::remote::LOVE: emit Love(); break; @@ -192,6 +204,18 @@ void IncomingDataParser::Parse(const pb::remote::Message& msg) { case pb::remote::GLOBAL_SEARCH: GlobalSearch(client, msg); break; + case pb::remote::REQUEST_FILES: + emit SendListFiles( + QString::fromStdString(msg.request_list_files().relative_path()), + client); + break; + case pb::remote::APPEND_FILES: + AppendFilesToPlaylist(msg); + break; + case pb::remote::REQUEST_SAVED_RADIOS: + emit SendSavedRadios(client); + break; + default: break; } @@ -267,6 +291,7 @@ void IncomingDataParser::SetShuffleMode(const pb::remote::Shuffle& shuffle) { void IncomingDataParser::InsertUrls(const pb::remote::Message& msg) { const pb::remote::RequestInsertUrls& request = msg.request_insert_urls(); + int playlist_id = request.playlist_id(); // Insert plain urls without metadata if (!request.urls().empty()) { @@ -276,9 +301,13 @@ void IncomingDataParser::InsertUrls(const pb::remote::Message& msg) { urls << QUrl(QStringFromStdString(s)); } + if (request.has_new_playlist_name()) + playlist_id = + app_->playlist_manager()->New(request.new_playlist_name().c_str()); + // Insert the urls - emit InsertUrls(request.playlist_id(), urls, request.position(), - request.play_now(), request.enqueue()); + emit InsertUrls(playlist_id, urls, request.position(), request.play_now(), + request.enqueue()); } // Add songs with metadata if present @@ -287,6 +316,13 @@ void IncomingDataParser::InsertUrls(const pb::remote::Message& msg) { for (int i = 0; i < request.songs().size(); i++) { songs << CreateSongFromProtobuf(request.songs(i)); } + + // create a new playlist if required and not already done above by + // InsertUrls + if (request.has_new_playlist_name() && playlist_id == request.playlist_id()) + playlist_id = + app_->playlist_manager()->New(request.new_playlist_name().c_str()); + emit InsertSongs(request.playlist_id(), songs, request.position(), request.play_now(), request.enqueue()); } @@ -338,6 +374,28 @@ void IncomingDataParser::ClosePlaylist(const pb::remote::Message& msg) { emit Close(msg.request_close_playlist().playlist_id()); } +void IncomingDataParser::UpdatePlaylist(const pb::remote::Message& msg) { + const pb::remote::RequestUpdatePlaylist& req_update = + msg.request_update_playlist(); + if (req_update.has_create_new_playlist() && + req_update.create_new_playlist()) { + emit New(req_update.has_new_playlist_name() + ? req_update.new_playlist_name().c_str() + : "New Playlist"); + return; + } + if (req_update.has_clear_playlist() && req_update.clear_playlist()) { + emit Clear(req_update.playlist_id()); + return; + } + if (req_update.has_new_playlist_name() && + req_update.new_playlist_name().size()) + emit Rename(req_update.playlist_id(), + req_update.new_playlist_name().c_str()); + if (req_update.has_favorite()) + emit Favorite(req_update.playlist_id(), req_update.favorite()); +} + void IncomingDataParser::RateSong(const pb::remote::Message& msg) { double rating = (double)msg.request_rate_song().rating(); emit RateCurrentSong(rating); @@ -370,3 +428,63 @@ Song IncomingDataParser::CreateSongFromProtobuf( return song; } + +void IncomingDataParser::AppendFilesToPlaylist(const pb::remote::Message& msg) { + if (files_root_folder_.isEmpty()) { // should never happen... + qLog(Warning) << "Remote root dir is not set although receiving " + "APPEND_FILES request..."; + return; + } + QDir root_dir(files_root_folder_); + if (!root_dir.exists()) { + qLog(Warning) << "Remote root dir doesn't exist..."; + return; + } + + const pb::remote::RequestAppendFiles& req_append = msg.request_append_files(); + QString relative_path = QString::fromStdString(req_append.relative_path()); + if (relative_path.startsWith("/")) relative_path.remove(0, 1); + + QFileInfo fi_folder(root_dir, relative_path); + if (!fi_folder.exists()) + qLog(Warning) << "Remote relative path " << relative_path + << " doesn't exist..."; + else if (!fi_folder.isDir()) + qLog(Warning) << "Remote relative path " << relative_path + << " is not a directory..."; + else if (root_dir.relativeFilePath(fi_folder.absoluteFilePath()) + .startsWith("../")) + qLog(Warning) << "Remote relative path " << relative_path + << " should not be accessed..."; + else { + QList urls; + QDir dir(fi_folder.absoluteFilePath()); + for (const auto& file : req_append.files()) { + QFileInfo fi(dir, file.c_str()); + if (fi.exists()) urls << QUrl::fromLocalFile(fi.canonicalFilePath()); + } + if (urls.size()) { + MimeData* data = new MimeData; + data->setUrls(urls); + if (req_append.has_play_now()) data->play_now_ = req_append.play_now(); + if (req_append.has_clear_first()) + data->clear_first_ = req_append.clear_first(); + if (req_append.has_new_playlist_name()) { + QString playlist_name = + QString::fromStdString(req_append.new_playlist_name()); + if (!playlist_name.isEmpty()) { + data->open_in_new_playlist_ = true; + data->name_for_new_playlist_ = playlist_name; + } + } else if (req_append.has_playlist_id()) { + // if playing we will drop the files in another playlist + if (app_->player()->GetState() == Engine::Playing) + data->playlist_id = req_append.playlist_id(); + else + // as me may play the song, we change the current playlist + emit SetCurrentPlaylist(req_append.playlist_id()); + } + emit AddToPlaylistSignal(data); + } + } +} diff --git a/src/networkremote/incomingdataparser.h b/src/networkremote/incomingdataparser.h index 97b7d39b4..9c46dcf72 100644 --- a/src/networkremote/incomingdataparser.h +++ b/src/networkremote/incomingdataparser.h @@ -15,6 +15,10 @@ class IncomingDataParser : public QObject { bool close_connection(); + void SetRemoteRootFiles(const QString& files_root_folder) { + files_root_folder_ = files_root_folder; + } + public slots: void Parse(const pb::remote::Message& msg); void ReloadSettings(); @@ -25,8 +29,12 @@ class IncomingDataParser : public QObject { void SendAllPlaylists(); void SendAllActivePlaylists(); void SendPlaylistSongs(int id); + void New(const QString& new_playlist_name); void Open(int id); + void Clear(int id); void Close(int id); + void Rename(int id, const QString& new_playlist_name); + void Favorite(int id, bool favorite); void GetLyrics(); void Love(); void Ban(); @@ -56,10 +64,16 @@ class IncomingDataParser : public QObject { void DoGlobalSearch(QString, RemoteClient*); + void SendSavedRadios(RemoteClient* client); + void SendListFiles(QString, RemoteClient*); + void AddToPlaylistSignal(QMimeData* data); + void SetCurrentPlaylist(int id); + private: Application* app_; bool close_connection_; MainWindow::PlaylistAddBehaviour doubleclick_playlist_addmode_; + QString files_root_folder_; void GetPlaylistSongs(const pb::remote::Message& msg); void ChangeSong(const pb::remote::Message& msg); @@ -71,8 +85,10 @@ class IncomingDataParser : public QObject { void SendPlaylists(const pb::remote::Message& msg); void OpenPlaylist(const pb::remote::Message& msg); void ClosePlaylist(const pb::remote::Message& msg); + void UpdatePlaylist(const pb::remote::Message& msg); void RateSong(const pb::remote::Message& msg); void GlobalSearch(RemoteClient* client, const pb::remote::Message& msg); + void AppendFilesToPlaylist(const pb::remote::Message& msg); Song CreateSongFromProtobuf(const pb::remote::SongMetadata& pb); }; diff --git a/src/networkremote/networkremote.cpp b/src/networkremote/networkremote.cpp index 90b2ef3a8..e376404a2 100644 --- a/src/networkremote/networkremote.cpp +++ b/src/networkremote/networkremote.cpp @@ -72,6 +72,11 @@ void NetworkRemote::SetupServer() { SLOT(AcceptConnection())); connect(server_ipv6_.get(), SIGNAL(newConnection()), this, SLOT(AcceptConnection())); + + connect(incoming_data_parser_.get(), SIGNAL(AddToPlaylistSignal(QMimeData*)), + SIGNAL(AddToPlaylistSignal(QMimeData*))); + connect(incoming_data_parser_.get(), SIGNAL(SetCurrentPlaylist(int)), + SIGNAL(SetCurrentPlaylist(int))); } void NetworkRemote::StartServer() { @@ -171,6 +176,13 @@ void NetworkRemote::AcceptConnection() { SIGNAL(DoGlobalSearch(QString, RemoteClient*)), outgoing_data_creator_.get(), SLOT(DoGlobalSearch(QString, RemoteClient*))); + + connect(incoming_data_parser_.get(), + SIGNAL(SendListFiles(QString, RemoteClient*)), + outgoing_data_creator_.get(), + SLOT(SendListFiles(QString, RemoteClient*))); + connect(incoming_data_parser_.get(), SIGNAL(SendSavedRadios(RemoteClient*)), + outgoing_data_creator_.get(), SLOT(SendSavedRadios(RemoteClient*))); } QTcpServer* server = qobject_cast(sender()); @@ -213,6 +225,14 @@ void NetworkRemote::CreateRemoteClient(QTcpSocket* client_socket) { RemoteClient* client = new RemoteClient(app_, client_socket); clients_.push_back(client); + // Update the Remote Root Files for the latest Client + outgoing_data_creator_->SetMusicExtensions( + client->files_music_extensions()); + outgoing_data_creator_->SetRemoteRootFiles(client->files_root_folder()); + incoming_data_parser_->SetRemoteRootFiles(client->files_root_folder()); + // update OutgoingDataCreator with latest allow_downloads setting + outgoing_data_creator_->SetAllowDownloads(client->allow_downloads()); + // Connect the signal to parse data connect(client, SIGNAL(Parse(pb::remote::Message)), incoming_data_parser_.get(), SLOT(Parse(pb::remote::Message))); diff --git a/src/networkremote/networkremote.h b/src/networkremote/networkremote.h index 5cbd009cc..e01a9a41f 100644 --- a/src/networkremote/networkremote.h +++ b/src/networkremote/networkremote.h @@ -13,6 +13,7 @@ class QImage; class QTcpServer; class QTcpSocket; class RemoteClient; +class QMimeData; class NetworkRemote : public QObject { Q_OBJECT @@ -24,6 +25,10 @@ class NetworkRemote : public QObject { explicit NetworkRemote(Application* app, QObject* parent = nullptr); ~NetworkRemote(); + signals: + void AddToPlaylistSignal(QMimeData* data); + void SetCurrentPlaylist(int id); + public slots: void SetupServer(); void StartServer(); diff --git a/src/networkremote/outgoingdatacreator.cpp b/src/networkremote/outgoingdatacreator.cpp index 23b8fc4cc..b408d96fa 100644 --- a/src/networkremote/outgoingdatacreator.cpp +++ b/src/networkremote/outgoingdatacreator.cpp @@ -17,6 +17,7 @@ #include "outgoingdatacreator.h" +#include #include #include #include @@ -26,6 +27,8 @@ #include "core/timeconstants.h" #include "core/utilities.h" #include "globalsearch/librarysearchprovider.h" +#include "internet/core/internetmodel.h" +#include "internet/internetradio/savedradio.h" #include "library/librarybackend.h" #include "networkremote.h" #include "ui/iconloader.h" @@ -180,10 +183,15 @@ void OutgoingDataCreator::SendClementineInfo() { msg.mutable_response_clementine_info(); SetEngineState(info); + // allowed extensions for REQUEST_FILES and LIST_FILES + for (const QString& ext : files_music_extensions_) + *info->add_files_music_extensions() = ext.toStdString(); + QString version = QString("%1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion()); info->set_version(version.toLatin1()); + info->set_allow_downloads(allow_downloads_); SendDataToClients(&msg); } @@ -208,20 +216,22 @@ void OutgoingDataCreator::SetEngineState( void OutgoingDataCreator::SendAllPlaylists() { // Get all Playlists - QList app_playlists = app_->playlist_manager()->GetAllPlaylists(); - int active_playlist = app_->playlist_manager()->active_id(); + PlaylistManager* playlist_manager = app_->playlist_manager(); + int active_playlist = playlist_manager->active_id(); // Create message pb::remote::Message msg; msg.set_type(pb::remote::PLAYLISTS); pb::remote::ResponsePlaylists* playlists = msg.mutable_response_playlists(); + playlists->set_include_closed(true); // Get all playlists, even ones that are hidden in the UI. for (const PlaylistBackend::Playlist& p : app_->playlist_backend()->GetAllPlaylists()) { - bool playlist_open = app_->playlist_manager()->IsPlaylistOpen(p.id); - int item_count = playlist_open ? app_playlists.at(p.id)->rowCount() : 0; + bool playlist_open = playlist_manager->IsPlaylistOpen(p.id); + int item_count = + playlist_open ? playlist_manager->playlist(p.id)->rowCount() : 0; // Create a new playlist pb::remote::Playlist* playlist = playlists->add_playlist(); @@ -230,6 +240,7 @@ void OutgoingDataCreator::SendAllPlaylists() { playlist->set_active((p.id == active_playlist)); playlist->set_item_count(item_count); playlist->set_closed(!playlist_open); + playlist->set_favorite(p.favorite); } SendDataToClients(&msg); @@ -259,6 +270,7 @@ void OutgoingDataCreator::SendAllActivePlaylists() { playlist->set_active((p->id() == active_playlist)); playlist->set_item_count(p->rowCount()); playlist->set_closed(false); + playlist->set_favorite(p->is_favorite()); } SendDataToClients(&msg); @@ -289,8 +301,9 @@ void OutgoingDataCreator::PlaylistRenamed(int id, const QString& new_name) { } void OutgoingDataCreator::SendFirstData(bool send_playlist_songs) { + Player* player = app_->player(); // First Send the current song - PlaylistItemPtr item = app_->player()->GetCurrentItem(); + PlaylistItemPtr item = player->GetCurrentItem(); if (!item) { qLog(Info) << "No current item found!"; } @@ -298,11 +311,11 @@ void OutgoingDataCreator::SendFirstData(bool send_playlist_songs) { CurrentSongChanged(current_song_, current_uri_, current_image_); // then the current volume - VolumeChanged(app_->player()->GetVolume()); + VolumeChanged(player->GetVolume()); // Check if we need to start the track position timer if (!track_position_timer_->isActive() && - app_->player()->engine()->state() == Engine::Playing) { + player->engine()->state() == Engine::Playing) { track_position_timer_->start(1000); } @@ -546,11 +559,11 @@ void OutgoingDataCreator::UpdateTrackPosition() { pb::remote::Message msg; msg.set_type(pb::remote::UPDATE_TRACK_POSITION); - int position = std::floor( - float(app_->player()->engine()->position_nanosec()) / kNsecPerSec + 0.5); + qint64 position_nanosec = app_->player()->engine()->position_nanosec(); + int position = static_cast( + std::floor(static_cast(position_nanosec) / kNsecPerSec + 0.5)); - if (app_->player()->engine()->position_nanosec() > - current_song_.length_nanosec()) + if (position_nanosec > current_song_.length_nanosec()) position = last_track_position_; msg.mutable_response_update_track_position()->set_position(position); @@ -744,3 +757,71 @@ void OutgoingDataCreator::SearchFinished(int id) { qLog(Debug) << "SearchFinished" << req.id_ << req.query_; } + +void OutgoingDataCreator::SendListFiles(QString relative_path, + RemoteClient* client) { + pb::remote::Message msg; + msg.set_type(pb::remote::LIST_FILES); + pb::remote::ResponseListFiles* files = msg.mutable_response_list_files(); + // Security checks + if (files_root_folder_.isEmpty()) { + files->set_error(pb::remote::ResponseListFiles::ROOT_DIR_NOT_SET); + SendDataToClients(&msg); + return; + } + + QDir root_dir(files_root_folder_); + if (!root_dir.exists()) + files->set_error(pb::remote::ResponseListFiles::ROOT_DIR_NOT_SET); + else if (relative_path.startsWith("..") || relative_path.startsWith("./..")) + files->set_error(pb::remote::ResponseListFiles::DIR_NOT_ACCESSIBLE); + else { + if (relative_path.startsWith("/")) relative_path.remove(0, 1); + + QFileInfo fi_folder(root_dir, relative_path); + if (!fi_folder.exists()) + files->set_error(pb::remote::ResponseListFiles::DIR_NOT_EXIST); + else if (!fi_folder.isDir()) + files->set_error(pb::remote::ResponseListFiles::DIR_NOT_EXIST); + else if (root_dir.relativeFilePath(fi_folder.absoluteFilePath()) + .startsWith("../")) + files->set_error(pb::remote::ResponseListFiles::DIR_NOT_ACCESSIBLE); + else { + files->set_relative_path( + root_dir.relativeFilePath(fi_folder.absoluteFilePath()) + .toStdString()); + QDir dir(fi_folder.absoluteFilePath()); + dir.setFilter(QDir::NoDotAndDotDot | QDir::AllEntries); + dir.setSorting(QDir::Name | QDir::DirsFirst); + + for (const QFileInfo& fi : dir.entryInfoList()) { + if (fi.isDir() || files_music_extensions_.contains(fi.suffix())) { + pb::remote::FileMetadata* pb_file = files->add_files(); + pb_file->set_is_dir(fi.isDir()); + pb_file->set_filename(fi.fileName().toStdString()); + } + } + } + } + client->SendData(&msg); +} + +void OutgoingDataCreator::SendSavedRadios(RemoteClient* client) { + pb::remote::Message msg; + msg.set_type(pb::remote::REQUEST_SAVED_RADIOS); + + SavedRadio* radio_service = static_cast( + InternetModel::ServiceByName(SavedRadio::kServiceName)); + if (radio_service) { + pb::remote::ResponseSavedRadios* radios = + msg.mutable_response_saved_radios(); + for (const auto& stream : radio_service->Streams()) { + pb::remote::Stream* pb_stream = radios->add_streams(); + pb_stream->set_name(stream.name_.toStdString()); + pb_stream->set_url(stream.url_.toString().toStdString()); + if (!stream.url_logo_.isEmpty()) + pb_stream->set_url_logo(stream.url_logo_.toString().toStdString()); + } + } + client->SendData(&msg); +} diff --git a/src/networkremote/outgoingdatacreator.h b/src/networkremote/outgoingdatacreator.h index a261e21fb..c50d95c5f 100644 --- a/src/networkremote/outgoingdatacreator.h +++ b/src/networkremote/outgoingdatacreator.h @@ -47,7 +47,15 @@ class OutgoingDataCreator : public QObject { static const quint32 kFileChunkSize; void SetClients(QList* clients); - + void SetRemoteRootFiles(const QString& files_root_folder) { + files_root_folder_ = files_root_folder; + } + void SetMusicExtensions(const QStringList& files_music_extensions) { + files_music_extensions_ = files_music_extensions; + } + void SetAllowDownloads(bool allow_downloads) { + allow_downloads_ = allow_downloads; + } static void CreateSong(const Song& song, const QImage& art, const int index, pb::remote::SongMetadata* song_metadata); @@ -83,6 +91,9 @@ class OutgoingDataCreator : public QObject { void ResultsAvailable(int id, const SearchProvider::ResultList& results); void SearchFinished(int id); + void SendListFiles(QString relative_path, RemoteClient* client); + void SendSavedRadios(RemoteClient* client); + private: Application* app_; QList* clients_; @@ -95,6 +106,9 @@ class OutgoingDataCreator : public QObject { int keep_alive_timeout_; int last_track_position_; bool aww_; + QString files_root_folder_; + QStringList files_music_extensions_; + bool allow_downloads_; std::unique_ptr ultimate_reader_; QMap results_; diff --git a/src/networkremote/remoteclient.cpp b/src/networkremote/remoteclient.cpp index 5f9f3628a..e9bf6298f 100644 --- a/src/networkremote/remoteclient.cpp +++ b/src/networkremote/remoteclient.cpp @@ -28,9 +28,6 @@ RemoteClient::RemoteClient(Application* app, QTcpSocket* client) downloader_(false), client_(client), song_sender_(new SongSender(app, this)) { - // Open the buffer - buffer_.setData(QByteArray()); - buffer_.open(QIODevice::ReadWrite); reading_protobuf_ = false; // Connect to the slot IncomingData when receiving data @@ -43,7 +40,11 @@ RemoteClient::RemoteClient(Application* app, QTcpSocket* client) use_auth_code_ = s.value("use_auth_code", false).toBool(); auth_code_ = s.value("auth_code", 0).toInt(); allow_downloads_ = s.value("allow_downloads", false).toBool(); - + files_root_folder_ = s.value("files_root_folder", "").toString(); + files_music_extensions_ = + s.value("files_music_extensions", + Application::kDefaultMusicExtensionsAllowedRemotely) + .toStringList(); s.endGroup(); // If we don't use an auth code, we don't need to authenticate the client. @@ -86,17 +87,16 @@ void RemoteClient::IncomingData() { } // Read some of the message - buffer_.write(client_->read(expected_length_ - buffer_.size())); + buffer_.append( + client_->read(static_cast(expected_length_) - buffer_.size())); // Did we get everything? - if (buffer_.size() == expected_length_) { + if (buffer_.size() == static_cast(expected_length_)) { // Parse the message - ParseMessage(buffer_.data()); + ParseMessage(buffer_); // Clear the buffer - buffer_.close(); - buffer_.setData(QByteArray()); - buffer_.open(QIODevice::ReadWrite); + buffer_.clear(); reading_protobuf_ = false; } } diff --git a/src/networkremote/remoteclient.h b/src/networkremote/remoteclient.h index a35e8bec4..11a878842 100644 --- a/src/networkremote/remoteclient.h +++ b/src/networkremote/remoteclient.h @@ -1,8 +1,6 @@ #ifndef REMOTECLIENT_H #define REMOTECLIENT_H -#include -#include #include #include "core/application.h" @@ -23,6 +21,11 @@ class RemoteClient : public QObject { void DisconnectClient(pb::remote::ReasonDisconnect reason); SongSender* song_sender() { return song_sender_; } + const QString& files_root_folder() const { return files_root_folder_; } + const QStringList& files_music_extensions() const { + return files_music_extensions_; + } + bool allow_downloads() const { return allow_downloads_; } private slots: void IncomingData(); @@ -47,8 +50,11 @@ class RemoteClient : public QObject { QTcpSocket* client_; bool reading_protobuf_; quint32 expected_length_; - QBuffer buffer_; + QByteArray buffer_; SongSender* song_sender_; + + QString files_root_folder_; + QStringList files_music_extensions_; }; #endif // REMOTECLIENT_H diff --git a/src/networkremote/songsender.cpp b/src/networkremote/songsender.cpp index e06b2933b..3e78e2ca3 100644 --- a/src/networkremote/songsender.cpp +++ b/src/networkremote/songsender.cpp @@ -17,6 +17,7 @@ #include "songsender.h" +#include #include #include "core/application.h" @@ -87,7 +88,7 @@ void SongSender::SendSongs(const pb::remote::RequestDownloadSongs& request) { } break; case pb::remote::APlaylist: - SendPlaylist(request.playlist_id()); + SendPlaylist(request); break; case pb::remote::Urls: SendUrls(request); @@ -321,7 +322,8 @@ void SongSender::SendAlbum(const Song& song) { } } -void SongSender::SendPlaylist(int playlist_id) { +void SongSender::SendPlaylist(const pb::remote::RequestDownloadSongs& request) { + int playlist_id = request.playlist_id(); Playlist* playlist = app_->playlist_manager()->playlist(playlist_id); if (!playlist) { qLog(Info) << "Could not find playlist with id = " << playlist_id; @@ -329,17 +331,22 @@ void SongSender::SendPlaylist(int playlist_id) { } SongList song_list = playlist->GetAllSongs(); + QList requested_ids; + for (auto song_id : request.songs_ids()) requested_ids << song_id; + // Count the local songs int count = 0; for (Song s : song_list) { - if (s.url().scheme() == "file") { + if (s.url().scheme() == "file" && + (requested_ids.isEmpty() || requested_ids.contains(s.id()))) { count++; } } for (Song s : song_list) { // Only local files! - if (s.url().scheme() == "file") { + if (s.url().scheme() == "file" && + (requested_ids.isEmpty() || requested_ids.contains(s.id()))) { DownloadItem item(s, song_list.indexOf(s) + 1, count); download_queue_.append(item); } @@ -350,14 +357,45 @@ void SongSender::SendUrls(const pb::remote::RequestDownloadSongs& request) { SongList song_list; // First gather all valid songs - for (auto it = request.urls().begin(); it != request.urls().end(); ++it) { - std::string s = *it; - QUrl url = QUrl(QStringFromStdString(s)); + if (request.has_relative_path()) { + // Security checks, cf OutgoingDataCreator::SendListFiles + const QString& files_root_folder = client_->files_root_folder(); + if (files_root_folder.isEmpty()) return; + QDir root_dir(files_root_folder); + QString relative_path(request.relative_path().c_str()); + if (!root_dir.exists() || relative_path.startsWith("..") || + relative_path.startsWith("./..")) + return; + if (relative_path.startsWith("/")) relative_path.remove(0, 1); - Song song = app_->library_backend()->GetSongByUrl(url); + QFileInfo fi_folder(root_dir, relative_path); + if (!fi_folder.exists() || !fi_folder.isDir() || + root_dir.relativeFilePath(fi_folder.absoluteFilePath()) + .startsWith("../")) + return; - if (song.is_valid() && song.url().scheme() == "file") { - song_list.append(song); + QDir dir(fi_folder.absoluteFilePath()); + const QStringList& files_music_extensions = + client_->files_music_extensions(); + for (const std::string& s : request.urls()) { + QFileInfo fi(dir, s.c_str()); + if (fi.exists() && fi.isFile() && + files_music_extensions.contains(fi.suffix())) { + Song song; + song.set_basefilename(fi.fileName()); + song.set_filesize(fi.size()); + song.set_url(QUrl::fromLocalFile(fi.absoluteFilePath())); + song.set_valid(true); + song_list.append(song); + } + } + } else { + for (const std::string& s : request.urls()) { + QUrl url = QUrl(QStringFromStdString(s)); + Song song = app_->library_backend()->GetSongByUrl(url); + if (song.is_valid() && song.url().scheme() == "file") { + song_list.append(song); + } } } diff --git a/src/networkremote/songsender.h b/src/networkremote/songsender.h index 4ea059501..a48261340 100644 --- a/src/networkremote/songsender.h +++ b/src/networkremote/songsender.h @@ -52,7 +52,7 @@ class SongSender : public QObject { void SendSingleSong(DownloadItem download_item); void SendAlbum(const Song& song); - void SendPlaylist(int playlist_id); + void SendPlaylist(const pb::remote::RequestDownloadSongs& request); void SendUrls(const pb::remote::RequestDownloadSongs& request); void OfferNextSong(); void SendTotalFileSize(); diff --git a/src/playlist/playlistmanager.cpp b/src/playlist/playlistmanager.cpp index 9f59e2326..4733b7dec 100644 --- a/src/playlist/playlistmanager.cpp +++ b/src/playlist/playlistmanager.cpp @@ -140,9 +140,9 @@ Playlist* PlaylistManager::AddPlaylist(int id, const QString& name, return ret; } -void PlaylistManager::New(const QString& name, const SongList& songs, - const QString& special_type) { - if (name.isNull()) return; +int PlaylistManager::New(const QString& name, const SongList& songs, + const QString& special_type) { + if (name.isNull()) return -1; int id = playlist_backend_->CreatePlaylist(name, special_type); @@ -157,6 +157,8 @@ void PlaylistManager::New(const QString& name, const SongList& songs, if (name == tr("Playlist")) { Rename(id, QString("%1 %2").arg(name).arg(id)); } + + return id; } void PlaylistManager::Load(const QString& filename) { @@ -295,6 +297,11 @@ void PlaylistManager::Favorite(int id, bool favorite) { emit PlaylistFavorited(id, favorite); } +void PlaylistManager::Clear(int id) { + if (playlists_.count() <= 1 || !playlists_.contains(id)) return; + playlists_[id].p->Clear(); +} + bool PlaylistManager::Close(int id) { // Won't allow removing the last playlist if (playlists_.count() <= 1 || !playlists_.contains(id)) return false; @@ -490,6 +497,7 @@ void PlaylistManager::InsertUrls(int id, const QList& urls, int pos, bool play_now, bool enqueue) { Q_ASSERT(playlists_.contains(id)); + if (play_now && active_ != id) SetActivePlaylist(id); playlists_[id].p->InsertUrls(urls, pos, play_now, enqueue); } @@ -497,6 +505,7 @@ void PlaylistManager::InsertSongs(int id, const SongList& songs, int pos, bool play_now, bool enqueue) { Q_ASSERT(playlists_.contains(id)); + if (play_now && active_ != id) SetActivePlaylist(id); playlists_[id].p->InsertSongs(songs, pos, play_now, enqueue); } diff --git a/src/playlist/playlistmanager.h b/src/playlist/playlistmanager.h index c7fc795af..7ea0e46e7 100644 --- a/src/playlist/playlistmanager.h +++ b/src/playlist/playlistmanager.h @@ -73,8 +73,8 @@ class PlaylistManagerInterface : public QObject { virtual PlaylistContainer* playlist_container() const = 0; public slots: - virtual void New(const QString& name, const SongList& songs = SongList(), - const QString& special_type = QString()) = 0; + virtual int New(const QString& name, const SongList& songs = SongList(), + const QString& special_type = QString()) = 0; virtual void Load(const QString& filename) = 0; virtual void Save(int id, const QString& filename, Playlist::Path path_type) = 0; @@ -181,8 +181,8 @@ class PlaylistManager : public PlaylistManagerInterface { PlaylistContainer* playlist_container() const { return playlist_container_; } public slots: - void New(const QString& name, const SongList& songs = SongList(), - const QString& special_type = QString()); + int New(const QString& name, const SongList& songs = SongList(), + const QString& special_type = QString()); void Load(const QString& filename); void Save(int id, const QString& filename, Playlist::Path path_type); // Display a file dialog to let user choose a file before saving the file @@ -190,6 +190,7 @@ class PlaylistManager : public PlaylistManagerInterface { void Rename(int id, const QString& new_name); void Favorite(int id, bool favorite); void Delete(int id); + void Clear(int id); bool Close(int id); void Open(int id); void ChangePlaylistOrder(const QList& ids); diff --git a/src/ui/addstreamdialog.cpp b/src/ui/addstreamdialog.cpp index c71ffccee..6aaa1a66c 100644 --- a/src/ui/addstreamdialog.cpp +++ b/src/ui/addstreamdialog.cpp @@ -41,30 +41,33 @@ AddStreamDialog::AddStreamDialog(QWidget* parent) ui_->save->setChecked(s.value("save", true).toBool()); ui_->url->setText(s.value("url").toString()); ui_->name->setText(s.value("name").toString()); + ui_->url_logo->setText(s.value("url_logo").toString()); } AddStreamDialog::~AddStreamDialog() { delete ui_; } QUrl AddStreamDialog::url() const { return QUrl(ui_->url->text()); } - QString AddStreamDialog::name() const { return ui_->name->text(); } +QUrl AddStreamDialog::url_logo() const { return QUrl(ui_->url_logo->text()); } void AddStreamDialog::set_name(const QString& name) { ui_->name->setText(name); } - void AddStreamDialog::set_url(const QUrl& url) { ui_->url->setText(url.toString()); } +void AddStreamDialog::set_url_logo(const QUrl& url) { + ui_->url_logo->setText(url.toString()); +} void AddStreamDialog::set_save_visible(bool visible) { ui_->save->setVisible(visible); - if (!visible) ui_->name_container->setEnabled(true); + if (!visible) ui_->details_container->setEnabled(true); } void AddStreamDialog::accept() { if (ui_->save->isChecked() && saved_radio_) { - saved_radio_->Add(url(), name()); + saved_radio_->Add(url(), name(), url_logo()); } // Save settings diff --git a/src/ui/addstreamdialog.h b/src/ui/addstreamdialog.h index f076d36f0..a48acc8f1 100644 --- a/src/ui/addstreamdialog.h +++ b/src/ui/addstreamdialog.h @@ -33,6 +33,7 @@ class AddStreamDialog : public QDialog { void set_url(const QUrl& url); void set_name(const QString& name); + void set_url_logo(const QUrl& url); void set_save_visible(bool visible); void set_add_on_accept(SavedRadio* saved_radio) { saved_radio_ = saved_radio; @@ -40,6 +41,7 @@ class AddStreamDialog : public QDialog { QUrl url() const; QString name() const; + QUrl url_logo() const; void accept(); diff --git a/src/ui/addstreamdialog.ui b/src/ui/addstreamdialog.ui index 87c3036b1..fe50b7f52 100644 --- a/src/ui/addstreamdialog.ui +++ b/src/ui/addstreamdialog.ui @@ -7,7 +7,7 @@ 0 0 400 - 168 + 220 @@ -39,21 +39,31 @@ - - + + 0 - + Give it a name: - + + + + + URL of its Logo: + + + + + + @@ -127,7 +137,7 @@ save toggled(bool) - name_container + details_container setEnabled(bool) diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp index a10088151..91c5942a6 100644 --- a/src/ui/mainwindow.cpp +++ b/src/ui/mainwindow.cpp @@ -796,6 +796,12 @@ MainWindow::MainWindow(Application* app, SystemTrayIcon* tray_icon, OSD* osd, connect(InternetModel::Service(), SIGNAL(ShowAddStreamDialog()), SLOT(AddStream())); + // Network Remote + connect(app->network_remote(), SIGNAL(AddToPlaylistSignal(QMimeData*)), + SLOT(AddToPlaylist(QMimeData*))); + connect(app->network_remote(), SIGNAL(SetCurrentPlaylist(int)), + app_->playlist_manager(), SLOT(SetCurrentPlaylist(int))); + #ifdef Q_OS_DARWIN mac::SetApplicationHandler(this); #endif @@ -1669,6 +1675,7 @@ void MainWindow::ApplyPlayBehaviour(MainWindow::PlayBehaviour b, void MainWindow::AddToPlaylist(QMimeData* data) { if (!data) return; + Playlist* playlist = nullptr; if (MimeData* mime_data = qobject_cast(data)) { // Should we replace the flags with the user's preference? if (mime_data->override_user_settings_) { @@ -1684,10 +1691,14 @@ void MainWindow::AddToPlaylist(QMimeData* data) { if (mime_data->open_in_new_playlist_) { app_->playlist_manager()->New(mime_data->get_name_for_new_playlist()); } + // or shall we drop the songs in another playlist? + else if (mime_data->playlist_id != -1) + playlist = app_->playlist_manager()->playlist(mime_data->playlist_id); } - app_->playlist_manager()->current()->dropMimeData(data, Qt::CopyAction, -1, 0, - QModelIndex()); + if (!playlist) playlist = app_->playlist_manager()->current(); + + playlist->dropMimeData(data, Qt::CopyAction, -1, 0, QModelIndex()); delete data; } diff --git a/src/ui/networkremotesettingspage.cpp b/src/ui/networkremotesettingspage.cpp index fafab44da..c1eb385c1 100644 --- a/src/ui/networkremotesettingspage.cpp +++ b/src/ui/networkremotesettingspage.cpp @@ -103,6 +103,13 @@ void NetworkRemoteSettingsPage::Load() { } } + ui_->files_root_folder->SetPath(s.value("files_root_folder", "").toString()); + ui_->files_music_extensions->setText( + s.value("files_music_extensions", + Application::kDefaultMusicExtensionsAllowedRemotely) + .toStringList() + .join(",")); + s.endGroup(); // Get local ip addresses @@ -147,6 +154,17 @@ void NetworkRemoteSettingsPage::Save() { .value(); s.setValue("last_output_format", preset.codec_mimetype_); + s.setValue("files_root_folder", ui_->files_root_folder->Path()); + + QStringList files_music_extensions; + for (const QString& extension : + ui_->files_music_extensions->text().split(",")) { + QString ext = extension.trimmed(); + if (ext.size() > 0 && ext.size() < 8) // no empty string, less than 8 char + files_music_extensions << ext; + } + s.setValue("files_music_extensions", files_music_extensions); + s.endGroup(); if (NetworkRemoteHelper::Instance()) { diff --git a/src/ui/networkremotesettingspage.ui b/src/ui/networkremotesettingspage.ui index 2f9e1cada..393140806 100644 --- a/src/ui/networkremotesettingspage.ui +++ b/src/ui/networkremotesettingspage.ui @@ -101,7 +101,7 @@ - + Enter this IP in the App to connect to Clementine. @@ -111,7 +111,7 @@ - + 127.0.0.1 @@ -128,7 +128,7 @@ - + false @@ -172,6 +172,39 @@ + + + + Root folder that will be browsable from the network remote + + + Files root folder + + + + + + + + 100 + 0 + + + + + + + + + + + coma separated list of the allowed extensions that will be visible from the network remote (ex: m3u,mp3,flac,ogg,wav) + + + Music extensions remotely visible + + + @@ -259,6 +292,14 @@ + + + FileChooserWidget + QWidget +
ui/filechooserwidget.h
+ 1 +
+