diff --git a/CMakeLists.txt b/CMakeLists.txt index 204d9570b6..61bb6beb26 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -215,7 +215,7 @@ set(QT_VERSION_MAJOR 6) set(QT_MIN_VERSION 6.4.0) set(QT_DEFAULT_MAJOR_VERSION ${QT_VERSION_MAJOR}) set(QT_COMPONENTS Core Concurrent Gui Widgets Network Sql) -set(QT_OPTIONAL_COMPONENTS LinguistTools Test) +set(QT_OPTIONAL_COMPONENTS LinguistTools Test Protobuf) if(UNIX AND NOT APPLE) list(APPEND QT_OPTIONAL_COMPONENTS DBus) endif() @@ -358,6 +358,10 @@ optional_component(EBUR128 ON "EBU R 128 loudness normalization" DEPENDS "libebur128" LIBEBUR128_FOUND ) +optional_component(NETWORKREMOTE ON "Network remote" + DEPENDS "Qt Protobuf" Qt${QT_VERSION_MAJOR}Protobuf_FOUND +) + if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ) set(HAVE_CHROMAPRINT ON) endif() @@ -731,6 +735,7 @@ set(SOURCES src/widgets/loginstatewidget.cpp src/widgets/ratingwidget.cpp src/widgets/resizabletextedit.cpp + src/widgets/filechooserwidget.cpp src/osd/osdbase.cpp src/osd/osdpretty.cpp @@ -1027,6 +1032,7 @@ set(HEADERS src/widgets/ratingwidget.h src/widgets/forcescrollperpixel.h src/widgets/resizabletextedit.h + src/widgets/filechooserwidget.h src/osd/osdbase.h src/osd/osdpretty.h @@ -1441,11 +1447,63 @@ optional_source(HAVE_QOBUZ src/settings/qobuzsettingspage.ui ) +if(HAVE_NETWORKREMOTE) + optional_source(HAVE_NETWORKREMOTE + SOURCES + src/core/zeroconf.cpp + src/networkremote/incomingdataparser.cpp + src/networkremote/networkremote.cpp + src/networkremote/outgoingdatacreator.cpp + src/networkremote/networkremoteclient.cpp + src/networkremote/songsender.cpp + src/settings/networkremotesettingspage.cpp + HEADERS + src/networkremote/networkremote.h + src/networkremote/incomingdataparser.h + src/networkremote/outgoingdatacreator.h + src/networkremote/networkremoteclient.h + src/networkremote/songsender.h + src/settings/networkremotesettingspage.h + UI + src/settings/networkremotesettingspage.ui + ) + if(UNIX AND NOT APPLE) + get_target_property(QT_DBUSXML2CPP_EXECUTABLE Qt${QT_VERSION_MAJOR}::qdbusxml2cpp LOCATION) + add_custom_command( + OUTPUT + ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahiserver.cpp + ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahiserver.h + COMMAND ${QT_DBUSXML2CPP_EXECUTABLE} + ${CMAKE_SOURCE_DIR}/src/avahi/org.freedesktop.Avahi.Server.xml + -p ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahiserver + -i includes/dbus_metatypes.h + DEPENDS src/avahi/org.freedesktop.Avahi.Server.xml + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + add_custom_command( + OUTPUT + ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahientrygroup.cpp + ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahientrygroup.h + COMMAND ${QT_DBUSXML2CPP_EXECUTABLE} + ${CMAKE_SOURCE_DIR}/src/avahi/org.freedesktop.Avahi.EntryGroup.xml + -p ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahientrygroup + -i includes/dbus_metatypes.h + DEPENDS src/avahi/org.freedesktop.Avahi.EntryGroup.xml + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + list(APPEND SOURCES src/avahi/avahi.cpp ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahientrygroup.cpp ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahiserver.cpp) + list(APPEND HEADERS src/avahi/avahi.h ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahientrygroup.h ${CMAKE_CURRENT_BINARY_DIR}/avahi/avahiserver.h) + endif() + optional_source(APPLE SOURCES src/core/bonjour.mm HEADERS src/core/bonjour.h) + optional_source(WIN32 SOURCES src/core/tinysvcmdns.cpp HEADERS src/core/tinysvcmdns.h) +endif() + qt_wrap_cpp(SOURCES ${HEADERS}) qt_wrap_ui(SOURCES ${UI}) qt_add_resources(SOURCES data/data.qrc data/icons.qrc) -add_library(strawberry_lib STATIC ${SOURCES}) +add_library(strawberry_lib STATIC ${SOURCES} + src/constants/networkremoteconstants.h) target_sources(strawberry PRIVATE src/main.cpp) @@ -1474,6 +1532,13 @@ if(HAVE_TRANSLATIONS) endif() endif() +if(HAVE_NETWORKREMOTE) + qt_add_protobuf(NetworkRemoteMessages + GENERATE_PACKAGE_SUBFOLDERS + PROTO_FILES src/networkremote/networkremotemessages.proto + ) +endif() + target_include_directories(strawberry_lib PUBLIC ${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR} @@ -1508,6 +1573,7 @@ target_link_libraries(strawberry_lib PUBLIC Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Sql $<$:Qt${QT_VERSION_MAJOR}::DBus> + $<$:Qt${QT_VERSION_MAJOR}::Protobuf> ICU::uc ICU::i18n $<$:ALSA::ALSA> @@ -1526,6 +1592,7 @@ target_link_libraries(strawberry_lib PUBLIC $<$:dsound dwmapi getopt-win::getopt> $<$:WindowsApp> ${SINGLEAPPLICATION_LIBRARIES} + $<$:NetworkRemoteMessages> ) if(APPLE) diff --git a/org.freedesktop.Avahi.EntryGroup.xml b/org.freedesktop.Avahi.EntryGroup.xml new file mode 100644 index 0000000000..43fd63c571 --- /dev/null +++ b/org.freedesktop.Avahi.EntryGroup.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.freedesktop.Avahi.Server.xml b/org.freedesktop.Avahi.Server.xml new file mode 100644 index 0000000000..d119aeb6be --- /dev/null +++ b/org.freedesktop.Avahi.Server.xml @@ -0,0 +1,405 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/avahi/avahi.cpp b/src/avahi/avahi.cpp new file mode 100644 index 0000000000..76219cb238 --- /dev/null +++ b/src/avahi/avahi.cpp @@ -0,0 +1,114 @@ +/* + * Strawberry Music Player + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include +#include +#include +#include + +#include "core/logging.h" + +#include "avahi.h" +#include "avahi/avahiserver.h" +#include "avahi/avahientrygroup.h" + +using namespace Qt::StringLiterals; + +Avahi::Avahi(QObject *parent) : Zeroconf(parent), port_(0), entry_group_interface_(nullptr) {} + +void Avahi::PublishInternal(const QString &domain, const QString &type, const QByteArray &name, quint16 port) { + + domain_ = domain; + type_ = type; + name_ = name; + port_ = port; + + OrgFreedesktopAvahiServerInterface server_interface(u"org.freedesktop.Avahi"_s, u"/"_s, QDBusConnection::systemBus()); + QDBusPendingReply reply = server_interface.EntryGroupNew(); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &Avahi::PublishInternalFinished); + +} + +void Avahi::PublishInternalFinished(QDBusPendingCallWatcher *watcher) { + + const QDBusPendingReply path_reply = watcher->reply(); + + watcher->deleteLater(); + + if (path_reply.isError()) { + qLog(Error) << "Failed to create Avahi entry group:" << path_reply.error(); + qLog(Info) << "This might be because 'disable-user-service-publishing'" << "is set to 'yes' in avahi-daemon.conf"; + return; + } + + AddService(path_reply.reply().path()); + +} + +void Avahi::AddService(const QString &path) { + + entry_group_interface_ = new OrgFreedesktopAvahiEntryGroupInterface(u"org.freedesktop.Avahi"_s, path, QDBusConnection::systemBus()); + QDBusPendingReply<> reply = entry_group_interface_->AddService(-1, -1, 0, QString::fromUtf8(name_.constData(), name_.size()), type_, domain_, QString(), port_); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &Avahi::AddServiceFinished); + +} + +void Avahi::AddServiceFinished(QDBusPendingCallWatcher *watcher) { + + const QDBusPendingReply path_reply = watcher->reply(); + + watcher->deleteLater(); + + if (path_reply.isError()) { + qLog(Error) << "Failed to add Avahi service:" << path_reply.error(); + return; + } + + Commit(); + +} + +void Avahi::Commit() { + + QDBusPendingReply<> reply = entry_group_interface_->Commit(); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &Avahi::CommitFinished); + +} + +void Avahi::CommitFinished(QDBusPendingCallWatcher *watcher) { + + const QDBusPendingReply path_reply = watcher->reply(); + + watcher->deleteLater(); + + entry_group_interface_->deleteLater(); + entry_group_interface_ = nullptr; + + if (path_reply.isError()) { + qLog(Debug) << "Commit error:" << path_reply.error(); + } + else { + qLog(Debug) << "Remote interface published on Avahi"; + } + +} diff --git a/src/avahi/avahi.h b/src/avahi/avahi.h new file mode 100644 index 0000000000..95e714741e --- /dev/null +++ b/src/avahi/avahi.h @@ -0,0 +1,58 @@ +/* + * Strawberry Music Player + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef AVAHI_H +#define AVAHI_H + +#include +#include +#include + +#include "core/zeroconf.h" + +class QDBusPendingCallWatcher; +class OrgFreedesktopAvahiEntryGroupInterface; + +class Avahi : public Zeroconf { + Q_OBJECT + +public: + explicit Avahi(QObject *parent = nullptr); + + private: + void AddService(const QString &path); + void Commit(); + + private Q_SLOTS: + void PublishInternalFinished(QDBusPendingCallWatcher *watcher); + void AddServiceFinished(QDBusPendingCallWatcher *watcher); + void CommitFinished(QDBusPendingCallWatcher *watcher); + + protected: + virtual void PublishInternal(const QString &domain, const QString &type, const QByteArray &name, quint16 port) override; + + private: + QString domain_; + QString type_; + QByteArray name_; + quint16 port_; + OrgFreedesktopAvahiEntryGroupInterface *entry_group_interface_; +}; + +#endif // AVAHI_H diff --git a/src/avahi/org.freedesktop.Avahi.EntryGroup.xml b/src/avahi/org.freedesktop.Avahi.EntryGroup.xml new file mode 100644 index 0000000000..4e80b8adf1 --- /dev/null +++ b/src/avahi/org.freedesktop.Avahi.EntryGroup.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/avahi/org.freedesktop.Avahi.Server.xml b/src/avahi/org.freedesktop.Avahi.Server.xml new file mode 100644 index 0000000000..65fd9dd8cb --- /dev/null +++ b/src/avahi/org.freedesktop.Avahi.Server.xml @@ -0,0 +1,396 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/config.h.in b/src/config.h.in index 4b5371acd6..50477196b1 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -30,6 +30,7 @@ #cmakedefine HAVE_TIDAL #cmakedefine HAVE_SPOTIFY #cmakedefine HAVE_QOBUZ +#cmakedefine HAVE_NETWORKREMOTE #cmakedefine HAVE_TAGLIB_DSFFILE #cmakedefine HAVE_TAGLIB_DSDIFFFILE diff --git a/src/constants/networkremoteconstants.h b/src/constants/networkremoteconstants.h new file mode 100644 index 0000000000..30be7b6a9d --- /dev/null +++ b/src/constants/networkremoteconstants.h @@ -0,0 +1,36 @@ +/* +* Strawberry Music Player +* Copyright 2024, Jonas Kvinge +* +* Strawberry is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* Strawberry is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with Strawberry. If not, see . +* +*/ + +#ifndef NETWORKREMOTECONSTANTS_H +#define NETWORKREMOTECONSTANTS_H + +#include + +using namespace Qt::Literals::StringLiterals; + +namespace NetworkRemoteConstants { + +const QStringList kDefaultMusicExtensionsAllowedRemotely = { u"aac"_s, u"alac"_s, u"flac"_s, u"m3u"_s, u"m4a"_s, u"mp3"_s, u"ogg"_s, u"wav"_s, u"wmv"_s }; +constexpr quint16 kDefaultServerPort = 5500; +constexpr char kTranscoderSettingPostfix[] = "/NetworkRemote"; +constexpr quint32 kFileChunkSize = 100000; + +} // namespace NetworkRemoteConstants + +#endif // NETWORKREMOTECONSTANTS_H diff --git a/src/constants/networkremotesettingsconstants.h b/src/constants/networkremotesettingsconstants.h new file mode 100644 index 0000000000..fbea8c0514 --- /dev/null +++ b/src/constants/networkremotesettingsconstants.h @@ -0,0 +1,35 @@ +/* +* Strawberry Music Player +* Copyright 2024, Jonas Kvinge +* +* Strawberry is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* Strawberry is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with Strawberry. If not, see . +* +*/ + +#ifndef NETWORKREMOTESETTINGSCONSTANTS_H +#define NETWORKREMOTESETTINGSCONSTANTS_H + +namespace NetworkRemoteSettingsConstants { + +constexpr char kSettingsGroup[] = "NetworkRemote"; +constexpr char kEnabled[] = "enabled"; +constexpr char kPort[] = "port"; +constexpr char kAllowPublicAccess[] = "allow_public_access"; +constexpr char kUseAuthCode[] = "use_authcode"; +constexpr char kAuthCode[] = "authcode"; +constexpr char kFilesRootFolder[] = "files_root_folder"; + +} // namespace NetworkRemoteSettingsConstants + +#endif // NETWORKREMOTESETTINGSCONSTANTS_H diff --git a/src/core/application.cpp b/src/core/application.cpp index a0325d5a95..58acb47728 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -109,6 +109,10 @@ # include "moodbar/moodbarloader.h" #endif +#ifdef HAVE_NETWORKREMOTE +# include "networkremote/networkremote.h" +#endif + #include "radios/radioservices.h" #include "radios/radiobackend.h" @@ -215,6 +219,13 @@ class ApplicationImpl { #ifdef HAVE_MOODBAR moodbar_loader_([app]() { return new MoodbarLoader(app); }), moodbar_controller_([app]() { return new MoodbarController(app->player(), app->moodbar_loader()); }), +#endif +#ifdef HAVE_NETWORKREMOTE + network_remote_([app]() { + NetworkRemote *networkremote = new NetworkRemote(app->database(), app->player(), app->collection_backend(), app->playlist_manager(), app->playlist_backend(), app->current_albumcover_loader(), app->scrobbler()); + app->MoveToNewThread(networkremote); + return networkremote; + }), #endif lastfm_import_([app]() { return new LastFMImport(app->network()); }) {} @@ -240,6 +251,9 @@ class ApplicationImpl { #ifdef HAVE_MOODBAR Lazy moodbar_loader_; Lazy moodbar_controller_; +#endif +#ifdef HAVE_NETWORKREMOTE + Lazy network_remote_; #endif Lazy lastfm_import_; @@ -389,3 +403,6 @@ SharedPtr Application::lastfm_import() const { return p_->lastfm_i SharedPtr Application::moodbar_controller() const { return p_->moodbar_controller_.ptr(); } SharedPtr Application::moodbar_loader() const { return p_->moodbar_loader_.ptr(); } #endif +#ifdef HAVE_NETWORKREMOTE +SharedPtr Application::network_remote() const { return p_->network_remote_.ptr(); } +#endif diff --git a/src/core/application.h b/src/core/application.h index 38185fa730..a7e2ab749d 100644 --- a/src/core/application.h +++ b/src/core/application.h @@ -63,6 +63,9 @@ class RadioServices; class MoodbarController; class MoodbarLoader; #endif +#ifdef HAVE_NETWORKREMOTE +class NetworkRemote; +#endif class Application : public QObject { Q_OBJECT @@ -103,6 +106,10 @@ class Application : public QObject { SharedPtr moodbar_loader() const; #endif +#ifdef HAVE_NETWORKREMOTE + SharedPtr network_remote() const; +#endif + SharedPtr lastfm_import() const; void Exit(); diff --git a/src/core/bonjour.h b/src/core/bonjour.h new file mode 100644 index 0000000000..522bff9d6c --- /dev/null +++ b/src/core/bonjour.h @@ -0,0 +1,24 @@ +#ifndef BONJOUR_H +#define BONJOUR_H + +#include "zeroconf.h" + +#ifdef __OBJC__ +@class NetServicePublicationDelegate; +#else +class NetServicePublicationDelegate; +#endif // __OBJC__ + +class Bonjour : public Zeroconf { + public: + explicit Bonjour(); + virtual ~Bonjour(); + + protected: + virtual void PublishInternal(const QString &domain, const QString &type, const QByteArray &name, const quint16 port); + + private: + NetServicePublicationDelegate *delegate_; +}; + +#endif // BONJOUR_H diff --git a/src/core/bonjour.mm b/src/core/bonjour.mm new file mode 100644 index 0000000000..b8f407e05e --- /dev/null +++ b/src/core/bonjour.mm @@ -0,0 +1,57 @@ +#include "bonjour.h" + +#import +#import + +#include "core/logging.h" +#include "core/scoped_nsautorelease_pool.h" + +@interface NetServicePublicationDelegate : NSObject {} + +- (void)netServiceWillPublish:(NSNetService*)netService; +- (void)netService:(NSNetService*)netService didNotPublish:(NSDictionary*)errorDict; +- (void)netServiceDidStop:(NSNetService*)netService; + +@end + +@implementation NetServicePublicationDelegate + +- (void)netServiceWillPublish:(NSNetService*)netService { + qLog(Debug) << "Publishing:" << [[netService name] UTF8String]; +} + +- (void)netService:(NSNetService*)netServie didNotPublish:(NSDictionary*)errorDict { + qLog(Debug) << "Failed to publish remote service with Bonjour"; + NSLog(@"%@", errorDict); +} + +- (void)netServiceDidStop:(NSNetService*)netService { + qLog(Debug) << "Unpublished:" << [[netService name] UTF8String]; +} + +@end + +namespace { + +NSString* NSStringFromQString(const QString& s) { + return [[NSString alloc] initWithUTF8String:s.toUtf8().constData()]; +} +} + +Bonjour::Bonjour() : delegate_([[NetServicePublicationDelegate alloc] init]) {} + +Bonjour::~Bonjour() { [delegate_ release]; } + +void Bonjour::PublishInternal(const QString& domain, const QString& type, const QByteArray& name, const quint16 port) { + ScopedNSAutoreleasePool pool; + NSNetService* service = + [[NSNetService alloc] initWithDomain:NSStringFromQString(domain) + type:NSStringFromQString(type) + name:[NSString stringWithUTF8String:name.constData()] + port:port]; + if (service) { + [service setDelegate:delegate_]; + [service publish]; + } + +} diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index c2ecdb93e0..6666a30674 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -946,6 +946,10 @@ MainWindow::MainWindow(Application *app, SharedPtr tray_icon, OS ui_->action_open_cd->setVisible(false); #endif +#ifdef HAVE_NETWORKREMOTE + app_->network_remote(); +#endif + // Load settings qLog(Debug) << "Loading settings"; settings_.beginGroup(MainWindowSettings::kSettingsGroup); diff --git a/src/core/mimedata.cpp b/src/core/mimedata.cpp index 7247ffba39..e5155d00e9 100644 --- a/src/core/mimedata.cpp +++ b/src/core/mimedata.cpp @@ -25,14 +25,15 @@ #include "mimedata.h" -MimeData::MimeData(const bool clear, const bool play_now, const bool enqueue, const bool enqueue_next_now, const bool open_in_new_playlist, QObject *parent) +MimeData::MimeData(const bool clear, const bool play_now, const bool enqueue, const bool enqueue_next_now, const bool open_in_new_playlist, const int playlist_id, QObject *parent) : override_user_settings_(false), clear_first_(clear), play_now_(play_now), enqueue_now_(enqueue), enqueue_next_now_(enqueue_next_now), open_in_new_playlist_(open_in_new_playlist), - from_doubleclick_(false) { + from_doubleclick_(false), + playlist_id_(playlist_id) { Q_UNUSED(parent); diff --git a/src/core/mimedata.h b/src/core/mimedata.h index 6002b44843..6ee4ba28e6 100644 --- a/src/core/mimedata.h +++ b/src/core/mimedata.h @@ -29,7 +29,7 @@ class MimeData : public QMimeData { Q_OBJECT public: - explicit MimeData(const bool clear = false, const bool play_now = false, const bool enqueue = false, const bool enqueue_next_now = false, const bool open_in_new_playlist = false, QObject *parent = nullptr); + explicit MimeData(const bool clear = false, const bool play_now = false, const bool enqueue = false, const bool enqueue_next_now = false, const bool open_in_new_playlist = false, const int playlist_id = -1, QObject *parent = nullptr); // If this is set then MainWindow will not touch any of the other flags. bool override_user_settings_; @@ -57,6 +57,9 @@ class MimeData : public QMimeData { // The MainWindow will set the above flags to 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 generic "Playlist" string if the 'name_for_new_playlist_' attribute is empty. QString get_name_for_new_playlist() const; diff --git a/src/core/tinysvcmdns.cpp b/src/core/tinysvcmdns.cpp new file mode 100644 index 0000000000..1cb24ae477 --- /dev/null +++ b/src/core/tinysvcmdns.cpp @@ -0,0 +1,79 @@ +#include "tinysvcmdns.h" + +extern "C" { +#include "mdnsd.h" +} + +#include +#include +#include + +#include "core/logging.h" + +void TinySVCMDNS::CreateMdnsd(const uint32_t ipv4, const QString &ipv6) { + + const QString host = QHostInfo::localHostName(); + + // Start the service + mdnsd *mdnsd = mdnsd_start_bind(ipv4); + + // Set our hostname + mdnsd_set_hostname(mdnsd, QString(host + ".local").toUtf8().constData(), ipv4); + + // Add to the list + mdnsd_.append(mdnsd); + +} + +TinySVCMDNS::TinySVCMDNS() { + + // Get all network interfaces + const QList network_interfaces = QNetworkInterface::allInterfaces(); + for (const QNetworkInterface &network_interface : network_interfaces) { + // Only use up and non loopback interfaces + if (network_interface.flags().testFlag(network_interface.IsUp) && !network_interface.flags().testFlag(network_interface.IsLoopBack)) { + uint32_t ipv4 = 0; + QString ipv6; + + qLog(Debug) << "Interface" << network_interface.humanReadableName(); + + // Now check all network addresses for this device + QList network_address_entries = network_interface.addressEntries(); + + for (QNetworkAddressEntry network_address_entry : network_address_entries) { + QHostAddress host_address = network_address_entry.ip(); + if (host_address.protocol() == QAbstractSocket::IPv4Protocol) { + ipv4 = qToBigEndian(host_address.toIPv4Address()); + qLog(Debug) << " ipv4:" << host_address.toString(); + } + else if (host_address.protocol() == QAbstractSocket::IPv6Protocol) { + ipv6 = host_address.toString(); + qLog(Debug) << " ipv6:" << host_address.toString(); + } + } + + // Now start the service + CreateMdnsd(ipv4, ipv6); + } + } + +} + +TinySVCMDNS::~TinySVCMDNS() { + + for (mdnsd *mdnsd : std::as_const(mdnsd_)) { + mdnsd_stop(mdnsd); + } + +} + +void TinySVCMDNS::PublishInternal(const QString &domain, const QString &type, const QByteArray &name, quint16 port) { + + // Some pointless text, so tinymDNS publishes the service correctly. + const char *txt[] = { "cat=nyan", nullptr }; + + for (mdnsd *mdnsd : mdnsd_) { + mdnsd_register_svc(mdnsd, name.constData(), QString(type + ".local").toUtf8().constData(), port, nullptr, txt); + } + +} diff --git a/src/core/tinysvcmdns.h b/src/core/tinysvcmdns.h new file mode 100644 index 0000000000..7d4c819f96 --- /dev/null +++ b/src/core/tinysvcmdns.h @@ -0,0 +1,25 @@ +#ifndef TINYSVCMDNS_H +#define TINYSVCMDNS_H + +#include +#include +#include + +#include "zeroconf.h" + +struct mdnsd; + +class TinySVCMDNS : public Zeroconf { + public: + explicit TinySVCMDNS(); + virtual ~TinySVCMDNS(); + + protected: + virtual void PublishInternal(const QString &domain, const QString &type, const QByteArray &name, quint16 port); + + private: + void CreateMdnsd(uint32_t ipv4, QString ipv6); + QList mdnsd_; +}; + +#endif // TINYSVCMDNS_H diff --git a/src/core/zeroconf.cpp b/src/core/zeroconf.cpp new file mode 100644 index 0000000000..cdeb89ac2f --- /dev/null +++ b/src/core/zeroconf.cpp @@ -0,0 +1,69 @@ +#include "config.h" + +#include +#include +#include + +#ifdef HAVE_DBUS +# include "avahi/avahi.h" +#endif + +#ifdef Q_OS_DARWIN +# include "bonjour.h" +#endif + +#ifdef Q_OS_WIN32 +# include "tinysvcmdns.h" +#endif + +#include "zeroconf.h" + +Zeroconf *Zeroconf::sInstance = nullptr; + +Zeroconf::Zeroconf(QObject *parent) : QObject(parent) {} + +Zeroconf::~Zeroconf() = default; + +Zeroconf *Zeroconf::GetZeroconf() { + + if (!sInstance) { +#ifdef HAVE_DBUS + sInstance = new Avahi; +#endif // HAVE_DBUS + +#ifdef Q_OS_DARWIN + sInstance = new Bonjour; +#endif + +#ifdef Q_OS_WIN32 + sInstance = new TinySVCMDNS; +#endif + } + + return sInstance; + +} + +QByteArray Zeroconf::TruncateName(const QString &name) { + + QByteArray truncated_utf8; + for (const QChar c : name) { + if (truncated_utf8.size() + 1 >= 63) { + break; + } + truncated_utf8 += c.toLatin1(); + } + + // NULL-terminate the string. + truncated_utf8.append('\0'); + + return truncated_utf8; + +} + +void Zeroconf::Publish(const QString &domain, const QString &type, const QString &name, quint16 port) { + + const QByteArray truncated_name = TruncateName(name); + PublishInternal(domain, type, truncated_name, port); + +} diff --git a/src/core/zeroconf.h b/src/core/zeroconf.h new file mode 100644 index 0000000000..3950d42d24 --- /dev/null +++ b/src/core/zeroconf.h @@ -0,0 +1,28 @@ +#ifndef ZEROCONF_H +#define ZEROCONF_H + +#include +#include +#include + +class Zeroconf : public QObject { + + public: + explicit Zeroconf(QObject *parent); + virtual ~Zeroconf(); + + void Publish(const QString &domain, const QString &type, const QString &name, quint16 port); + + static Zeroconf *GetZeroconf(); + + // Truncate a QString to 63 bytes of UTF-8. + static QByteArray TruncateName(const QString &name); + + protected: + virtual void PublishInternal(const QString &domain, const QString &type, const QByteArray &name, quint16 port) = 0; + + private: + static Zeroconf *sInstance; +}; + +#endif // ZEROCONF_H diff --git a/src/networkremote/incomingdataparser.cpp b/src/networkremote/incomingdataparser.cpp new file mode 100644 index 0000000000..10edb751fe --- /dev/null +++ b/src/networkremote/incomingdataparser.cpp @@ -0,0 +1,478 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2012, Andreas Muttscheller + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include + +#include +#include +#include +#include + +#include "core/logging.h" +#include "core/mimedata.h" +#include "constants/timeconstants.h" +#include "engine/enginebase.h" +#include "playlist/playlist.h" +#include "playlist/playlistmanager.h" +#include "playlist/playlistsequence.h" +#include "incomingdataparser.h" +#include "scrobbler/audioscrobbler.h" +#include "constants/mainwindowsettings.h" + +using namespace Qt::Literals::StringLiterals; + +IncomingDataParser::IncomingDataParser(const SharedPtr player, + const SharedPtr playlist_manager, + const SharedPtr scrobbler, + QObject *parent) + : QObject(parent), + player_(player), + playlist_manager_(playlist_manager), + scrobbler_(scrobbler), + close_connection_(false), + doubleclick_playlist_addmode_(BehaviourSettings::PlaylistAddBehaviour::Enqueue) { + + ReloadSettings(); + + QObject::connect(this, &IncomingDataParser::Play, &*player_, &Player::PlayHelper); + QObject::connect(this, &IncomingDataParser::PlayPause, &*player_, &Player::PlayPauseHelper); + QObject::connect(this, &IncomingDataParser::Pause, &*player_, &Player::Pause); + QObject::connect(this, &IncomingDataParser::Stop, &*player_, &Player::Stop); + QObject::connect(this, &IncomingDataParser::StopAfterCurrent, &*player_, &Player::StopAfterCurrent); + QObject::connect(this, &IncomingDataParser::Next, &*player_, &Player::Next); + QObject::connect(this, &IncomingDataParser::Previous, &*player_, &Player::Previous); + QObject::connect(this, &IncomingDataParser::SetVolume, &*player_, &Player::SetVolume); + QObject::connect(this, &IncomingDataParser::PlayAt, &*player_, &Player::PlayAt); + QObject::connect(this, &IncomingDataParser::SeekTo, &*player_, &Player::SeekTo); + + QObject::connect(this, &IncomingDataParser::Enqueue, &*playlist_manager_, &PlaylistManager::Enqueue); + QObject::connect(this, &IncomingDataParser::SetActivePlaylist, &*playlist_manager_, &PlaylistManager::SetActivePlaylist); + QObject::connect(this, &IncomingDataParser::ShuffleCurrent, &*playlist_manager_, &PlaylistManager::ShuffleCurrent); + QObject::connect(this, &IncomingDataParser::InsertUrls, &*playlist_manager_, &PlaylistManager::InsertUrls); + QObject::connect(this, &IncomingDataParser::InsertSongs, &*playlist_manager_, &PlaylistManager::InsertSongs); + QObject::connect(this, &IncomingDataParser::RemoveSongs, &*playlist_manager_, &PlaylistManager::RemoveItemsWithoutUndo); + QObject::connect(this, &IncomingDataParser::New, &*playlist_manager_, &PlaylistManager::New); + QObject::connect(this, &IncomingDataParser::Open, &*playlist_manager_, &PlaylistManager::Open); + QObject::connect(this, &IncomingDataParser::Close, &*playlist_manager_, &PlaylistManager::Close); + QObject::connect(this, &IncomingDataParser::Clear, &*playlist_manager_, &PlaylistManager::Clear); + QObject::connect(this, &IncomingDataParser::Rename, &*playlist_manager_, &PlaylistManager::Rename); + QObject::connect(this, &IncomingDataParser::Favorite, &*playlist_manager_, &PlaylistManager::Favorite); + + QObject::connect(this, &IncomingDataParser::SetRepeatMode, &*playlist_manager_->sequence(), &PlaylistSequence::SetRepeatMode); + QObject::connect(this, &IncomingDataParser::SetShuffleMode, &*playlist_manager_->sequence(), &PlaylistSequence::SetShuffleMode); + + QObject::connect(this, &IncomingDataParser::RateCurrentSong, &*playlist_manager_, &PlaylistManager::RateCurrentSong); + + QObject::connect(this, &IncomingDataParser::Love, &*scrobbler_, &AudioScrobbler::Love); + +} + +IncomingDataParser::~IncomingDataParser() = default; + +void IncomingDataParser::ReloadSettings() { + + QSettings s; + s.beginGroup(MainWindowSettings::kSettingsGroup); + doubleclick_playlist_addmode_ = static_cast(s.value(BehaviourSettings::kDoubleClickPlaylistAddMode, static_cast(BehaviourSettings::PlaylistAddBehaviour::Enqueue)).toInt()); + s.endGroup(); + +} + +bool IncomingDataParser::close_connection() const { return close_connection_; } + +void IncomingDataParser::SetRemoteRootFiles(const QString &files_root_folder) { + files_root_folder_ = files_root_folder; +} + +Song IncomingDataParser::SongFromPbSongMetadata(const networkremote::SongMetadata &pb_song_metadata) const { + + Song song; + song.Init(pb_song_metadata.title(), pb_song_metadata.artist(), pb_song_metadata.album(), pb_song_metadata.length() * kNsecPerSec); + song.set_albumartist(pb_song_metadata.albumartist()); + song.set_genre(pb_song_metadata.genre()); + song.set_year(pb_song_metadata.prettyYear().toInt()); + song.set_track(pb_song_metadata.track()); + song.set_disc(pb_song_metadata.disc()); + song.set_url(QUrl(pb_song_metadata.url())); + song.set_filesize(pb_song_metadata.fileSize()); + song.set_rating(pb_song_metadata.rating()); + song.set_basefilename(pb_song_metadata.filename()); + song.set_art_automatic(QUrl(pb_song_metadata.artAutomatic())); + song.set_art_manual(QUrl(pb_song_metadata.artManual())); + song.set_filetype(static_cast(pb_song_metadata.filetype())); + + return song; + +} + +void IncomingDataParser::Parse(const networkremote::Message &msg) { + + close_connection_ = false; + NetworkRemoteClient *client = qobject_cast(sender()); + + switch (msg.type()) { + case networkremote::MsgTypeGadget::MsgType::CONNECT: + ClientConnect(msg, client); + break; + case networkremote::MsgTypeGadget::MsgType::DISCONNECT: + close_connection_ = true; + break; + case networkremote::MsgTypeGadget::MsgType::GET_COLLECTION: + Q_EMIT SendCollection(client); + break; + case networkremote::MsgTypeGadget::MsgType::GET_PLAYLISTS: + ParseSendPlaylists(msg); + break; + case networkremote::MsgTypeGadget::MsgType::GET_PLAYLIST_SONGS: + ParseGetPlaylistSongs(msg); + break; + case networkremote::MsgTypeGadget::MsgType::SET_VOLUME: + Q_EMIT SetVolume(msg.requestSetVolume().volume()); + break; + case networkremote::MsgTypeGadget::MsgType::PLAY: + Q_EMIT Play(); + break; + case networkremote::MsgTypeGadget::MsgType::PLAYPAUSE: + Q_EMIT PlayPause(); + break; + case networkremote::MsgTypeGadget::MsgType::PAUSE: + Q_EMIT Pause(); + break; + case networkremote::MsgTypeGadget::MsgType::STOP: + Q_EMIT Stop(); + break; + case networkremote::MsgTypeGadget::MsgType::STOP_AFTER: + Q_EMIT StopAfterCurrent(); + break; + case networkremote::MsgTypeGadget::MsgType::NEXT: + Q_EMIT Next(); + break; + case networkremote::MsgTypeGadget::MsgType::PREVIOUS: + Q_EMIT Previous(); + break; + case networkremote::MsgTypeGadget::MsgType::CHANGE_SONG: + ParseChangeSong(msg); + break; + case networkremote::MsgTypeGadget::MsgType::SHUFFLE_PLAYLIST: + Q_EMIT ShuffleCurrent(); + break; + case networkremote::MsgTypeGadget::MsgType::REPEAT: + ParseSetRepeatMode(msg.repeat()); + break; + case networkremote::MsgTypeGadget::MsgType::SHUFFLE: + ParseSetShuffleMode(msg.shuffle()); + break; + case networkremote::MsgTypeGadget::MsgType::SET_TRACK_POSITION: + Q_EMIT SeekTo(msg.requestSetTrackPosition().position()); + break; + case networkremote::MsgTypeGadget::MsgType::PLAYLIST_INSERT_URLS: + ParseInsertUrls(msg); + break; + case networkremote::MsgTypeGadget::MsgType::REMOVE_PLAYLIST_SONGS: + ParseRemoveSongs(msg); + break; + case networkremote::MsgTypeGadget::MsgType::OPEN_PLAYLIST: + ParseOpenPlaylist(msg); + break; + case networkremote::MsgTypeGadget::MsgType::CLOSE_PLAYLIST: + ParseClosePlaylist(msg); + break; + case networkremote::MsgTypeGadget::MsgType::UPDATE_PLAYLIST: + ParseUpdatePlaylist(msg); + break; + case networkremote::MsgTypeGadget::MsgType::LOVE: + Q_EMIT Love(); + break; + case networkremote::MsgTypeGadget::MsgType::GET_LYRICS: + Q_EMIT GetLyrics(); + break; + case networkremote::MsgTypeGadget::MsgType::DOWNLOAD_SONGS: + client->song_sender()->SendSongs(msg.requestDownloadSongs()); + break; + case networkremote::MsgTypeGadget::MsgType::SONG_OFFER_RESPONSE: + client->song_sender()->ResponseSongOffer(msg.responseSongOffer().accepted()); + break; + case networkremote::MsgTypeGadget::MsgType::RATE_SONG: + ParseRateSong(msg); + break; + case networkremote::MsgTypeGadget::MsgType::REQUEST_FILES: + Q_EMIT SendListFiles(msg.requestListFiles().relativePath(), client); + break; + case networkremote::MsgTypeGadget::MsgType::APPEND_FILES: + ParseAppendFilesToPlaylist(msg); + break; + + default: + break; + } + +} + +void IncomingDataParser::ClientConnect(const networkremote::Message &msg, NetworkRemoteClient *client) { + + Q_EMIT SendInfo(); + + if (!client->isDownloader()) { + if (!msg.requestConnect().hasSendPlaylistSongs() || msg.requestConnect().sendPlaylistSongs()) { + Q_EMIT SendFirstData(true); + } + else { + Q_EMIT SendFirstData(false); + } + } + +} + +void IncomingDataParser::ParseGetPlaylistSongs(const networkremote::Message &msg) { + Q_EMIT SendPlaylistSongs(msg.requestPlaylistSongs().playlistId()); +} + +void IncomingDataParser::ParseChangeSong(const networkremote::Message &msg) { + + // Get the first entry and check if there is a song + const networkremote::RequestChangeSong &request = msg.requestChangeSong(); + + // Check if we need to change the playlist + if (request.playlistId() != playlist_manager_->active_id()) { + Q_EMIT SetActivePlaylist(request.playlistId()); + } + + switch (doubleclick_playlist_addmode_) { + case BehaviourSettings::PlaylistAddBehaviour::Play:{ + Q_EMIT PlayAt(request.songIndex(), false, 0, EngineBase::TrackChangeType::Manual, Playlist::AutoScroll::Maybe, false, false); + break; + } + case BehaviourSettings::PlaylistAddBehaviour::Enqueue:{ + Q_EMIT Enqueue(request.playlistId(), request.songIndex()); + if (player_->GetState() != EngineBase::State::Playing) { + Q_EMIT PlayAt(request.songIndex(), false, 0, EngineBase::TrackChangeType::Manual, Playlist::AutoScroll::Maybe, false, false); + } + break; + } + } + +} + +void IncomingDataParser::ParseSetRepeatMode(const networkremote::Repeat &repeat) { + + switch (repeat.repeatMode()) { + case networkremote::RepeatModeGadget::RepeatMode::RepeatMode_Off: + Q_EMIT SetRepeatMode(PlaylistSequence::RepeatMode::Off); + break; + case networkremote::RepeatModeGadget::RepeatMode::RepeatMode_Track: + Q_EMIT SetRepeatMode(PlaylistSequence::RepeatMode::Track); + break; + case networkremote::RepeatModeGadget::RepeatMode::RepeatMode_Album: + Q_EMIT SetRepeatMode(PlaylistSequence::RepeatMode::Album); + break; + case networkremote::RepeatModeGadget::RepeatMode::RepeatMode_Playlist: + Q_EMIT SetRepeatMode(PlaylistSequence::RepeatMode::Playlist); + break; + default: + break; + } + +} + +void IncomingDataParser::ParseSetShuffleMode(const networkremote::Shuffle &shuffle) { + + switch (shuffle.shuffleMode()) { + case networkremote::ShuffleModeGadget::ShuffleMode::ShuffleMode_Off: + Q_EMIT SetShuffleMode(PlaylistSequence::ShuffleMode::Off); + break; + case networkremote::ShuffleModeGadget::ShuffleMode::ShuffleMode_All: + Q_EMIT SetShuffleMode(PlaylistSequence::ShuffleMode::All); + break; + case networkremote::ShuffleModeGadget::ShuffleMode::ShuffleMode_InsideAlbum: + Q_EMIT SetShuffleMode(PlaylistSequence::ShuffleMode::InsideAlbum); + break; + case networkremote::ShuffleModeGadget::ShuffleMode::ShuffleMode_Albums: + Q_EMIT SetShuffleMode(PlaylistSequence::ShuffleMode::Albums); + break; + default: + break; + } + +} + +void IncomingDataParser::ParseInsertUrls(const networkremote::Message &msg) { + + const networkremote::RequestInsertUrls &request = msg.requestInsertUrls(); + int playlist_id = request.playlistId(); + + // Insert plain urls without metadata + if (!request.urls().empty()) { + QList urls; + for (auto it = request.urls().begin(); it != request.urls().end(); ++it) { + const QString s = *it; + urls << QUrl(s); + } + + if (request.hasNewPlaylistName()) { + playlist_id = playlist_manager_->New(request.newPlaylistName()); + } + + // Insert the urls + Q_EMIT InsertUrls(playlist_id, urls, request.position(), request.playNow(), request.enqueue()); + } + + // Add songs with metadata if present + if (!request.songs().empty()) { + SongList songs; + for (int i = 0; i < request.songs().size(); i++) { + songs << SongFromPbSongMetadata(request.songs().at(i)); + } + + // Create a new playlist if required and not already done above by InsertUrls + if (request.hasNewPlaylistName() && playlist_id == request.playlistId()) { + playlist_id = playlist_manager_->New(request.newPlaylistName()); + } + + Q_EMIT InsertSongs(request.playlistId(), songs, request.position(), request.playNow(), request.enqueue()); + } + +} + +void IncomingDataParser::ParseRemoveSongs(const networkremote::Message &msg) { + + const networkremote::RequestRemoveSongs &request = msg.requestRemoveSongs(); + + QList songs; + songs.reserve(request.songs().size()); + for (int i = 0; i < request.songs().size(); i++) { + songs.append(request.songs().at(i)); + } + + Q_EMIT RemoveSongs(request.playlistId(), songs); + +} + +void IncomingDataParser::ParseSendPlaylists(const networkremote::Message &msg) { + + if (!msg.hasRequestPlaylistSongs() || !msg.requestPlaylists().includeClosed()) { + Q_EMIT SendAllActivePlaylists(); + } + else { + Q_EMIT SendAllPlaylists(); + } + +} + +void IncomingDataParser::ParseOpenPlaylist(const networkremote::Message &msg) { + Q_EMIT Open(msg.requestOpenPlaylist().playlistId()); +} + +void IncomingDataParser::ParseClosePlaylist(const networkremote::Message &msg) { + Q_EMIT Close(msg.requestClosePlaylist().playlistId()); +} + +void IncomingDataParser::ParseUpdatePlaylist(const networkremote::Message &msg) { + + const networkremote::RequestUpdatePlaylist &req_update = msg.requestUpdatePlaylist(); + if (req_update.hasCreateNewPlaylist() && req_update.createNewPlaylist()) { + Q_EMIT New(req_update.hasNewPlaylistName() ? req_update.newPlaylistName() : u"New Playlist"_s); + return; + } + if (req_update.hasClearPlaylist() && req_update.clearPlaylist()) { + Q_EMIT Clear(req_update.playlistId()); + return; + } + if (req_update.hasNewPlaylistName() && !req_update.newPlaylistName().isEmpty()) { + Q_EMIT Rename(req_update.playlistId(), req_update.newPlaylistName()); + } + if (req_update.hasFavorite()) { + Q_EMIT Favorite(req_update.playlistId(), req_update.favorite()); + } + +} + +void IncomingDataParser::ParseRateSong(const networkremote::Message &msg) { + + Q_EMIT RateCurrentSong(msg.requestRateSong().rating()); + +} + +void IncomingDataParser::ParseAppendFilesToPlaylist(const networkremote::Message &msg) { + + if (files_root_folder_.isEmpty()) { + 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 networkremote::RequestAppendFiles &req_append = msg.requestAppendFiles(); + QString relative_path = req_append.relativePath(); + if (relative_path.startsWith("/"_L1)) 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..."; + return; + } + else if (!fi_folder.isDir()) { + qLog(Warning) << "Remote relative path " << relative_path << " is not a directory..."; + return; + } + else if (root_dir.relativeFilePath(fi_folder.absoluteFilePath()).startsWith("../"_L1)) { + qLog(Warning) << "Remote relative path " << relative_path << " should not be accessed..."; + return; + } + + QList urls; + QDir dir(fi_folder.absoluteFilePath()); + for (const auto &file : req_append.files()) { + QFileInfo fi(dir, file); + if (fi.exists()) urls << QUrl::fromLocalFile(fi.canonicalFilePath()); + } + if (!urls.isEmpty()) { + MimeData *data = new MimeData; + data->setUrls(urls); + if (req_append.hasPlayNow()) { + data->play_now_ = req_append.playNow(); + } + if (req_append.hasClearFirst()) { + data->clear_first_ = req_append.clearFirst(); + } + if (req_append.hasNewPlaylistName()) { + QString playlist_name = req_append.newPlaylistName(); + if (!playlist_name.isEmpty()) { + data->open_in_new_playlist_ = true; + data->name_for_new_playlist_ = playlist_name; + } + } + else if (req_append.hasPlaylistId()) { + // If playing we will drop the files in another playlist + if (player_->GetState() == EngineBase::State::Playing) { + data->playlist_id_ = req_append.playlistId(); + } + else { + // As we may play the song, we change the current playlist + Q_EMIT SetCurrentPlaylist(req_append.playlistId()); + } + } + Q_EMIT AddToPlaylistSignal(data); + } + +} diff --git a/src/networkremote/incomingdataparser.h b/src/networkremote/incomingdataparser.h new file mode 100644 index 0000000000..369e1d8c18 --- /dev/null +++ b/src/networkremote/incomingdataparser.h @@ -0,0 +1,123 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2012, Andreas Muttscheller + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef INCOMINGDATAPARSER_H +#define INCOMINGDATAPARSER_H + +#include +#include +#include + +#include "constants/behavioursettings.h" +#include "core/player.h" +#include "networkremoteclient.h" +#include "networkremote/networkremotemessages.qpb.h" +#include "playlist/playlistsequence.h" + +class PlaylistManager; +class AudioScrobbler; + +class IncomingDataParser : public QObject { + Q_OBJECT + + public: + explicit IncomingDataParser(const SharedPtr player, + const SharedPtr playlist_manager, + const SharedPtr scrobbler, + QObject *parent = nullptr); + + ~IncomingDataParser(); + + bool close_connection() const; + + void SetRemoteRootFiles(const QString &files_root_folder); + + public Q_SLOTS: + void Parse(const networkremote::Message &msg); + void ReloadSettings(); + + Q_SIGNALS: + void SendInfo(); + void SendFirstData(const bool send_playlist_songs); + void SendAllPlaylists(); + void SendAllActivePlaylists(); + void SendPlaylistSongs(const int id); + void New(const QString &name, const SongList &songs = SongList(), const QString &special_type = QString()); + void Open(const int id); + void Clear(const int id); + void Close(const int id); + void Rename(const int id, const QString &new_playlist_name); + void Favorite(const int id, const bool favorite); + void GetLyrics(); + void Love(); + + void Play(); + void PlayPause(); + void Pause(); + void Stop(const bool stop_after = false); + void StopAfterCurrent(); + void Next(); + void Previous(); + void SetVolume(const uint volume); + void PlayAt(const int index, const bool pause, const quint64 offset_nanosec, EngineBase::TrackChangeFlags change, const Playlist::AutoScroll autoscroll, const bool reshuffle, const bool force_inform); + void Enqueue(const int id, const int i); + void SetActivePlaylist(const int id); + void ShuffleCurrent(); + void SetRepeatMode(const PlaylistSequence::RepeatMode repeat_mode); + void SetShuffleMode(const PlaylistSequence::ShuffleMode shuffle_mode); + void InsertUrls(const int id, const QList &urls, const int pos = -1, const bool play_now = false, const bool enqueue = false); + void InsertSongs(const int id, const SongList &songs, const int pos, const bool play_now, const bool enqueue); + void RemoveSongs(const int id, const QList &indices); + void SeekTo(const quint64 seconds); + void SendCollection(NetworkRemoteClient *client); + void RateCurrentSong(const float rating); + + void SendListFiles(const QString &path, NetworkRemoteClient *client); + void AddToPlaylistSignal(QMimeData *data); + void SetCurrentPlaylist(const int id); + + private: + const SharedPtr player_; + const SharedPtr playlist_manager_; + const SharedPtr scrobbler_; + + bool close_connection_; + BehaviourSettings::PlaylistAddBehaviour doubleclick_playlist_addmode_; + QString files_root_folder_; + + void ClientConnect(const networkremote::Message &msg, NetworkRemoteClient *client); + Song SongFromPbSongMetadata(const networkremote::SongMetadata &pb_song_metadata) const; + + void ParseGetPlaylistSongs(const networkremote::Message &msg); + void ParseChangeSong(const networkremote::Message &msg); + void ParseSetRepeatMode(const networkremote::Repeat &repeat); + void ParseSetShuffleMode(const networkremote::Shuffle &shuffle); + void ParseInsertUrls(const networkremote::Message &msg); + void ParseRemoveSongs(const networkremote::Message &msg); + void ParseSendPlaylists(const networkremote::Message &msg); + void ParseOpenPlaylist(const networkremote::Message &msg); + void ParseClosePlaylist(const networkremote::Message &msg); + void ParseUpdatePlaylist(const networkremote::Message &msg); + void ParseRateSong(const networkremote::Message &msg); + void ParseAppendFilesToPlaylist(const networkremote::Message &msg); +}; + +#endif // INCOMINGDATAPARSER_H diff --git a/src/networkremote/networkremote.cpp b/src/networkremote/networkremote.cpp new file mode 100644 index 0000000000..8027348a02 --- /dev/null +++ b/src/networkremote/networkremote.cpp @@ -0,0 +1,231 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2012, Andreas Muttscheller + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include + +#include +#include +#include +#include +#include +#include + +#include "constants/networkremotesettingsconstants.h" +#include "constants/networkremoteconstants.h" +#include "core/logging.h" +#include "core/zeroconf.h" +#include "playlist/playlistmanager.h" +#include "covermanager/currentalbumcoverloader.h" +#include "networkremote.h" +#include "incomingdataparser.h" +#include "outgoingdatacreator.h" + +using namespace Qt::Literals::StringLiterals; +using namespace NetworkRemoteSettingsConstants; +using namespace NetworkRemoteConstants; +using std::make_unique; + +NetworkRemote::NetworkRemote(const SharedPtr database, + const SharedPtr player, + const SharedPtr collection_backend, + const SharedPtr playlist_manager, + const SharedPtr playlist_backend, + const SharedPtr current_albumcover_loader, + const SharedPtr scrobbler, + QObject *parent) + : QObject(parent), + database_(database), + player_(player), + collection_backend_(collection_backend), + playlist_manager_(playlist_manager), + playlist_backend_(playlist_backend), + current_albumcover_loader_(current_albumcover_loader), + scrobbler_(scrobbler), + enabled_(false), + port_(0), + allow_public_access_(true), + signals_connected_(false) { + + setObjectName("NetworkRemote"); + + ReloadSettings(); + +} + +NetworkRemote::~NetworkRemote() { StopServer(); } + +void NetworkRemote::ReloadSettings() { + + QSettings s; + s.beginGroup(kSettingsGroup); + enabled_ = s.value(kEnabled, false).toBool(); + port_ = s.value("port", kDefaultServerPort).toInt(); + allow_public_access_ = s.value(kAllowPublicAccess, false).toBool(); + s.endGroup(); + + SetupServer(); + StopServer(); + StartServer(); + +} + +void NetworkRemote::SetupServer() { + + server_ = make_unique(); + server_ipv6_ = make_unique(); + incoming_data_parser_ = make_unique(player_, playlist_manager_, scrobbler_); + outgoing_data_creator_ = make_unique(database_, player_, playlist_manager_, playlist_backend_); + + outgoing_data_creator_->SetClients(&clients_); + + QObject::connect(&*current_albumcover_loader_, &CurrentAlbumCoverLoader::AlbumCoverLoaded, &*outgoing_data_creator_, &OutgoingDataCreator::CurrentSongChanged); + + QObject::connect(&*server_, &QTcpServer::newConnection, this, &NetworkRemote::AcceptConnection); + QObject::connect(&*server_ipv6_, &QTcpServer::newConnection, this, &NetworkRemote::AcceptConnection); + + QObject::connect(&*incoming_data_parser_, &IncomingDataParser::AddToPlaylistSignal, this, &NetworkRemote::AddToPlaylistSignal); + QObject::connect(&*incoming_data_parser_, &IncomingDataParser::SetCurrentPlaylist, this, &NetworkRemote::SetCurrentPlaylist); + +} + +void NetworkRemote::StartServer() { + + if (!enabled_) { + qLog(Info) << "Network Remote deactivated"; + return; + } + + qLog(Info) << "Starting network remote"; + + server_->setProxy(QNetworkProxy::NoProxy); + server_ipv6_->setProxy(QNetworkProxy::NoProxy); + + server_->listen(QHostAddress::Any, port_); + server_ipv6_->listen(QHostAddress::AnyIPv6, port_); + + qLog(Info) << "Listening on port " << port_; + + if (Zeroconf::GetZeroconf()) { + QString name = QLatin1String("Strawberry on %1").arg(QHostInfo::localHostName()); + Zeroconf::GetZeroconf()->Publish(u"local"_s, u"_strawberry._tcp"_s, name, port_); + } + +} + +void NetworkRemote::StopServer() { + + if (server_->isListening()) { + outgoing_data_creator_->DisconnectAllClients(); + server_->close(); + server_ipv6_->close(); + qDeleteAll(clients_); + clients_.clear(); + } + +} + +void NetworkRemote::AcceptConnection() { + + if (!signals_connected_) { + signals_connected_ = true; + + QObject::connect(&*incoming_data_parser_, &IncomingDataParser::SendInfo, &*outgoing_data_creator_, &OutgoingDataCreator::SendInfo); + QObject::connect(&*incoming_data_parser_, &IncomingDataParser::SendFirstData, &*outgoing_data_creator_, &OutgoingDataCreator::SendFirstData); + QObject::connect(&*incoming_data_parser_, &IncomingDataParser::SendAllPlaylists, &*outgoing_data_creator_, &OutgoingDataCreator::SendAllPlaylists); + QObject::connect(&*incoming_data_parser_, &IncomingDataParser::SendAllActivePlaylists, &*outgoing_data_creator_, &OutgoingDataCreator::SendAllActivePlaylists); + QObject::connect(&*incoming_data_parser_, &IncomingDataParser::SendPlaylistSongs, &*outgoing_data_creator_, &OutgoingDataCreator::SendPlaylistSongs); + + QObject::connect(&*playlist_manager_, &PlaylistManager::ActiveChanged, &*outgoing_data_creator_, &OutgoingDataCreator::ActiveChanged); + QObject::connect(&*playlist_manager_, &PlaylistManager::PlaylistChanged, &*outgoing_data_creator_, &OutgoingDataCreator::PlaylistChanged); + QObject::connect(&*playlist_manager_, &PlaylistManager::PlaylistAdded, &*outgoing_data_creator_, &OutgoingDataCreator::PlaylistAdded); + QObject::connect(&*playlist_manager_, &PlaylistManager::PlaylistRenamed, &*outgoing_data_creator_, &OutgoingDataCreator::PlaylistRenamed); + QObject::connect(&*playlist_manager_, &PlaylistManager::PlaylistClosed, &*outgoing_data_creator_, &OutgoingDataCreator::PlaylistClosed); + QObject::connect(&*playlist_manager_, &PlaylistManager::PlaylistDeleted, &*outgoing_data_creator_, &OutgoingDataCreator::PlaylistDeleted); + + QObject::connect(&*player_, &Player::VolumeChanged, &*outgoing_data_creator_, &OutgoingDataCreator::VolumeChanged); + QObject::connect(&*player_->engine(), &EngineBase::StateChanged, &*outgoing_data_creator_, &OutgoingDataCreator::StateChanged); + + QObject::connect(&*playlist_manager_->sequence(), &PlaylistSequence::RepeatModeChanged, &*outgoing_data_creator_, &OutgoingDataCreator::SendRepeatMode); + QObject::connect(&*playlist_manager_->sequence(), &PlaylistSequence::ShuffleModeChanged, &*outgoing_data_creator_, &OutgoingDataCreator::SendShuffleMode); + + QObject::connect(&*incoming_data_parser_, &IncomingDataParser::SendCollection, &*outgoing_data_creator_, &OutgoingDataCreator::SendCollection); + QObject::connect(&*incoming_data_parser_, &IncomingDataParser::SendListFiles, &*outgoing_data_creator_, &OutgoingDataCreator::SendListFiles); + } + + QTcpServer *server = qobject_cast(sender()); + QTcpSocket *client_socket = server->nextPendingConnection(); + + if (!allow_public_access_ && !IpIsPrivate(client_socket->peerAddress())) { + qLog(Warning) << "Got connection from public IP address" << client_socket->peerAddress().toString(); + client_socket->close(); + client_socket->deleteLater(); + } + else { + CreateRemoteClient(client_socket); + } + +} + +bool NetworkRemote::IpIsPrivate(const QHostAddress &address) { + + return + // Localhost v4 + address.isInSubnet(QHostAddress::parseSubnet(u"127.0.0.0/8"_s)) || + // Link Local v4 + address.isInSubnet(QHostAddress::parseSubnet(u"169.254.1.0/16"_s)) || + // Link Local v6 + address.isInSubnet(QHostAddress::parseSubnet(u"::1/128"_s)) || + address.isInSubnet(QHostAddress::parseSubnet(u"fe80::/10"_s)) || + // Private v4 range + address.isInSubnet(QHostAddress::parseSubnet(u"192.168.0.0/16"_s)) || + address.isInSubnet(QHostAddress::parseSubnet(u"172.16.0.0/12"_s)) || + address.isInSubnet(QHostAddress::parseSubnet(u"10.0.0.0/8"_s)) || + // Private v4 range translated to v6 + address.isInSubnet(QHostAddress::parseSubnet(u"::ffff:192.168.0.0/112"_s)) || + address.isInSubnet(QHostAddress::parseSubnet(u"::ffff:172.16.0.0/108"_s)) || + address.isInSubnet(QHostAddress::parseSubnet(u"::ffff:10.0.0.0/104"_s)) || + // Private v6 range + address.isInSubnet(QHostAddress::parseSubnet(u"fc00::/7"_s)); + +} + +void NetworkRemote::CreateRemoteClient(QTcpSocket *client_socket) { + + if (client_socket) { + + NetworkRemoteClient *client = new NetworkRemoteClient(player_, collection_backend_, playlist_manager_, 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 + QObject::connect(client, &NetworkRemoteClient::Parse, &*incoming_data_parser_, &IncomingDataParser::Parse); + + client->IncomingData(); + + } + +} diff --git a/src/networkremote/networkremote.h b/src/networkremote/networkremote.h new file mode 100644 index 0000000000..a4069de393 --- /dev/null +++ b/src/networkremote/networkremote.h @@ -0,0 +1,98 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2012, Andreas Muttscheller + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef NETWORKREMOTE_H +#define NETWORKREMOTE_H + +#include +#include + +#include "includes/shared_ptr.h" +#include "includes/scoped_ptr.h" + +class QMimeData; +class QHostAddress; +class QTcpServer; +class QTcpSocket; + +class Database; +class Player; +class CollectionBackend; +class PlaylistManager; +class PlaylistBackend; +class CurrentAlbumCoverLoader; +class AudioScrobbler; +class IncomingDataParser; +class OutgoingDataCreator; +class NetworkRemoteClient; + +class NetworkRemote : public QObject { + Q_OBJECT + + public: + explicit NetworkRemote(const SharedPtr database, + const SharedPtr player, + const SharedPtr collection_backend, + const SharedPtr playlist_manager, + const SharedPtr playlist_backend, + const SharedPtr current_albumcover_loader, + const SharedPtr scrobbler, + QObject *parent = nullptr); + + ~NetworkRemote(); + + Q_SIGNALS: + void AddToPlaylistSignal(QMimeData *data); + void SetCurrentPlaylist(const int id); + + public Q_SLOTS: + void SetupServer(); + void StartServer(); + void ReloadSettings(); + void AcceptConnection(); + + private: + const SharedPtr database_; + const SharedPtr player_; + const SharedPtr collection_backend_; + const SharedPtr playlist_manager_; + const SharedPtr playlist_backend_; + const SharedPtr current_albumcover_loader_; + const SharedPtr scrobbler_; + + ScopedPtr server_; + ScopedPtr server_ipv6_; + ScopedPtr incoming_data_parser_; + ScopedPtr outgoing_data_creator_; + + bool enabled_; + quint16 port_; + bool allow_public_access_; + bool signals_connected_; + + QList clients_; + + void StopServer(); + void CreateRemoteClient(QTcpSocket *client_socket); + bool IpIsPrivate(const QHostAddress &address); +}; + +#endif // NETWORKREMOTE_H diff --git a/src/networkremote/networkremoteclient.cpp b/src/networkremote/networkremoteclient.cpp new file mode 100644 index 0000000000..e131a4a081 --- /dev/null +++ b/src/networkremote/networkremoteclient.cpp @@ -0,0 +1,223 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2013, Andreas Muttscheller + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include +#include +#include + +#include "constants/networkremotesettingsconstants.h" +#include "core/logging.h" +#include "networkremote.h" +#include "networkremoteclient.h" +#include "networkremote/networkremotemessages.qpb.h" + +using namespace Qt::Literals::StringLiterals; +using namespace NetworkRemoteSettingsConstants; + +NetworkRemoteClient::NetworkRemoteClient(const SharedPtr player, + const SharedPtr collection_backend, + const SharedPtr playlist_manager, + QTcpSocket *socket, + QObject *parent) + : QObject(parent), + player_(player), + socket_(socket), + downloader_(false), + reading_protobuf_(false), + expected_length_(0), + song_sender_(new SongSender(player, collection_backend, playlist_manager, this)) { + + QObject::connect(socket, &QTcpSocket::readyRead, this, &NetworkRemoteClient::ReadyRead); + QObject::connect(socket, &QTcpSocket::channelReadyRead, this, &NetworkRemoteClient::ReadyRead); + + QSettings s; + s.beginGroup(kSettingsGroup); + use_auth_code_ = s.value(kUseAuthCode, false).toBool(); + auth_code_ = s.value(kAuthCode, 0).toInt(); + files_root_folder_ = s.value(kFilesRootFolder, ""_L1).toString(); + s.endGroup(); + + authenticated_ = !use_auth_code_; + +} + +NetworkRemoteClient::~NetworkRemoteClient() { + + socket_->close(); + if (socket_->state() == QAbstractSocket::ConnectedState) { + socket_->waitForDisconnected(2000); + } + + song_sender_->deleteLater(); + socket_->deleteLater(); + +} + +void NetworkRemoteClient::setDownloader(const bool downloader) { downloader_ = downloader; } + +void NetworkRemoteClient::ReadyRead() { + + IncomingData(); + +} + +void NetworkRemoteClient::IncomingData() { + + while (socket_->bytesAvailable()) { + if (!reading_protobuf_) { + // If we have less than 4 byte, we cannot read the length. Wait for more data + if (socket_->bytesAvailable() < 4) { + break; + } + // Read the length of the next message + QDataStream s(socket_); + s >> expected_length_; + + // Receiving more than 128 MB is very unlikely + // Flush the data and disconnect the client + if (expected_length_ > 134217728) { + qLog(Debug) << "Received invalid data, disconnect client"; + qLog(Debug) << "expected_length_ =" << expected_length_; + socket_->close(); + return; + } + + reading_protobuf_ = true; + } + + // Read some of the message + buffer_.append(socket_->read(static_cast(expected_length_) - buffer_.size())); + + // Did we get everything? + if (buffer_.size() == static_cast(expected_length_)) { + + ParseMessage(buffer_); + + // Clear the buffer + buffer_.clear(); + reading_protobuf_ = false; + } + } + +} + +void NetworkRemoteClient::ParseMessage(const QByteArray &data) { + + QProtobufSerializer serializer; + networkremote::Message msg; + if (!serializer.deserialize(&msg, data)) { + qLog(Info) << "Couldn't parse data:" << serializer.lastErrorString(); + return; + } + + if (msg.type() == networkremote::MsgTypeGadget::MsgType::CONNECT && use_auth_code_) { + if (msg.requestConnect().authCode() != auth_code_) { + DisconnectClient(networkremote::ReasonDisconnectGadget::ReasonDisconnect::Wrong_Auth_Code); + return; + } + else { + authenticated_ = true; + } + } + + if (msg.type() == networkremote::MsgTypeGadget::MsgType::CONNECT) { + setDownloader(msg.requestConnect().hasDownloader() && msg.requestConnect().downloader()); + qLog(Debug) << "Downloader" << downloader_; + } + + // Check if downloads are allowed + if (msg.type() == networkremote::MsgTypeGadget::MsgType::DOWNLOAD_SONGS && !allow_downloads_) { + DisconnectClient(networkremote::ReasonDisconnectGadget::ReasonDisconnect::Download_Forbidden); + return; + } + + if (msg.type() == networkremote::MsgTypeGadget::MsgType::DISCONNECT) { + socket_->abort(); + qLog(Debug) << "Client disconnected"; + return; + } + + // Check if the client has sent the correct auth code + if (!authenticated_) { + DisconnectClient(networkremote::ReasonDisconnectGadget::ReasonDisconnect::Not_Authenticated); + return; + } + + // Now parse the other data + Q_EMIT Parse(msg); + +} + +void NetworkRemoteClient::DisconnectClient(const networkremote::ReasonDisconnectGadget::ReasonDisconnect reason) { + + networkremote::Message msg; + msg.setType(networkremote::MsgTypeGadget::MsgType::DISCONNECT); + + networkremote::ResponseDisconnect response_disconnect; + response_disconnect.setReasonDisconnect(reason); + msg.setResponseDisconnect(response_disconnect); + SendDataToClient(&msg); + + // Just close the connection. The next time the outgoing data creator sends a keep alive, the client will be deleted + socket_->close(); + +} + +// Sends data to client without check if authenticated +void NetworkRemoteClient::SendDataToClient(networkremote::Message *msg) { + + //msg->setVersion(msg); + + if (socket_->state() == QTcpSocket::ConnectedState) { + // Serialize the message + QProtobufSerializer serializer; + const QByteArray data = serializer.serialize(msg); + + // Write the length of the data first + QDataStream s(socket_); + s << static_cast(data.length()); + if (downloader_) { + // Don't use QDataSteam for large files + socket_->write(data.data(), data.length()); + } + else { + s.writeRawData(data.data(), data.length()); + } + + // Do NOT flush data here! If the client is already disconnected, it causes a SIGPIPE termination!!! + } + else { + qDebug() << "Closed"; + socket_->close(); + } + +} + +void NetworkRemoteClient::SendData(networkremote::Message *msg) { + + if (authenticated_) { + SendDataToClient(msg); + } + +} + +QAbstractSocket::SocketState NetworkRemoteClient::State() const { return socket_->state(); } diff --git a/src/networkremote/networkremoteclient.h b/src/networkremote/networkremoteclient.h new file mode 100644 index 0000000000..931f6299b8 --- /dev/null +++ b/src/networkremote/networkremoteclient.h @@ -0,0 +1,89 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2013, Andreas Muttscheller + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef NETWORKREMOTECLIENT_H +#define NETWORKREMOTECLIENT_H + +#include +#include +#include +#include +#include + +#include "networkremote/networkremotemessages.qpb.h" +#include "songsender.h" + +class QTcpSocket; + +class NetworkRemoteClient : public QObject { + Q_OBJECT + + public: + explicit NetworkRemoteClient(const SharedPtr player, + const SharedPtr collection_backend, + const SharedPtr playlist_manager, + QTcpSocket *client, + QObject *parent = nullptr); + + ~NetworkRemoteClient(); + + void SendData(networkremote::Message *msg); + QAbstractSocket::SocketState State() const; + void setDownloader(const bool downloader); + bool isDownloader() const { return downloader_; } + void DisconnectClient(const networkremote::ReasonDisconnectGadget::ReasonDisconnect reason); + + SongSender *song_sender() const { 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_; } + + public Q_SLOTS: + void ReadyRead(); + void IncomingData(); + + Q_SIGNALS: + void Parse(const networkremote::Message &msg); + + private: + void ParseMessage(const QByteArray &data); + void SendDataToClient(networkremote::Message *msg); + + private: + const SharedPtr player_; + QTcpSocket *socket_; + + bool use_auth_code_; + int auth_code_; + bool authenticated_; + bool allow_downloads_; + bool downloader_; + + bool reading_protobuf_; + quint32 expected_length_; + QByteArray buffer_; + SongSender *song_sender_; + + QString files_root_folder_; + QStringList files_music_extensions_; +}; + +#endif // NETWORKREMOTECLIENT_H diff --git a/src/networkremote/networkremotemessages.proto b/src/networkremote/networkremotemessages.proto new file mode 100644 index 0000000000..a12a964344 --- /dev/null +++ b/src/networkremote/networkremotemessages.proto @@ -0,0 +1,422 @@ +syntax = "proto2"; + +package networkremote; + +enum MsgType { + + UNKNOWN = 0; + + CONNECT = 1; + DISCONNECT = 2; + + INFO = 3; + KEEP_ALIVE = 4; + + GET_COLLECTION = 5; + + GET_PLAYLISTS = 6; + GET_PLAYLIST_SONGS = 7; + SEND_PLAYLISTS = 8; + SEND_PLAYLIST_SONGS = 10; + + OPEN_PLAYLIST = 11; + CLOSE_PLAYLIST = 12; + UPDATE_PLAYLIST = 13; + REMOVE_PLAYLIST_SONGS = 14; + PLAYLIST_INSERT_URLS = 15; + + CHANGE_SONG = 21; + SET_VOLUME = 22; + SET_TRACK_POSITION = 23; + GET_LYRICS = 24; + DOWNLOAD_SONGS = 25; + SONG_OFFER_RESPONSE = 26; + SONG_OFFER_FILE_CHUNK = 27; + CURRENT_METAINFO = 28; + ENGINE_STATE_CHANGED = 29; + UPDATE_TRACK_POSITION = 30; + ACTIVE_PLAYLIST_CHANGED = 31; + FIRST_DATA_SENT_COMPLETE = 32; + LYRICS = 33; + DOWNLOAD_QUEUE_EMPTY = 34; + COLLECTION_CHUNK = 35; + DOWNLOAD_TOTAL_SIZE = 36; + TRANSCODING_FILES = 37; + + PLAYPAUSE = 101; + PLAY = 102; + PAUSE = 103; + STOP = 104; + STOP_AFTER = 105; + NEXT = 106; + PREVIOUS = 107; + SHUFFLE_PLAYLIST = 108; + + REPEAT = 111; + SHUFFLE = 112; + + LIST_FILES = 121; + REQUEST_FILES = 122; + APPEND_FILES = 123; + + LOVE = 131; + RATE_SONG = 132; + +} + +enum EngineState { + EngineState_Empty = 0; + EngineState_Idle = 1; + EngineState_Playing = 2; + EngineState_Paused = 3; +} + +message SongMetadata { + + enum Source { + Source_Unknown = 0; + Source_LocalFile = 1; + Source_Collection = 2; + Source_CDDA = 3; + Source_Device = 4; + Source_Stream = 5; + Source_Tidal = 6; + Source_Subsonic = 7; + Source_Qobuz = 8; + Source_SomaFM = 9; + Source_RadioParadise = 10; + Source_Spotify = 11; + } + + enum FileType { + FileType_Unknown = 0; + FileType_WAV = 1; + FileType_FLAC = 2; + FileType_WavPack = 3; + FileType_OggFlac = 4; + FileType_OggVorbis = 5; + FileType_OggOpus = 6; + FileType_OggSpeex = 7; + FileType_MPEG = 8; + FileType_MP4 = 9; + FileType_ASF = 10; + FileType_AIFF = 11; + FileType_MPC = 12; + FileType_TrueAudio = 13; + FileType_DSF = 14; + FileType_DSDIFF = 15; + FileType_PCM = 16; + FileType_APE = 17; + FileType_MOD = 18; + FileType_S3M = 19; + FileType_XM = 20; + FileType_IT = 21; + FileType_SPC = 22; + FileType_VGM = 23; + FileType_CDDA = 90; + FileType_Stream = 91; + } + + optional int32 song_id = 1; + optional int32 index = 2; + optional string title = 3; + optional string album = 4; + optional string artist = 5; + optional string albumartist = 6; + optional int32 track = 7; + optional int32 disc = 8; + optional string pretty_year = 9; + optional string genre = 10; + optional uint32 playcount = 11; + optional string pretty_length = 12; + optional bytes art = 13; + optional int64 length = 14; + optional bool is_local = 15; + optional Source source = 22; + optional FileType filetype = 23; + optional string filename = 16; + optional int64 file_size = 17; + optional float rating = 18; + optional string url = 19; + optional string art_automatic = 20; + optional string art_manual = 21; + +} + +message Playlist { + optional int32 playlist_id = 1; + optional string name = 2; + optional int32 item_count = 3; + optional bool active = 4; + optional bool closed = 5; + optional bool favorite = 6; +} + +enum RepeatMode { + RepeatMode_Off = 0; + RepeatMode_Track = 1; + RepeatMode_Album = 2; + RepeatMode_Playlist = 3; + RepeatMode_OneByOne = 4; + RepeatMode_Intro = 5; +} + +enum ShuffleMode { + ShuffleMode_Off = 0; + ShuffleMode_All = 1; + ShuffleMode_InsideAlbum = 2; + ShuffleMode_Albums = 3; +} + +message RequestPlaylists { + optional bool include_closed = 1; +} + +message RequestPlaylistSongs { + optional int32 playlist_id = 1; +} + +message RequestChangeSong { + optional int32 playlist_id = 1; + optional int32 song_index = 2; +} + +message RequestSetVolume { + optional uint32 volume = 1; +} + +message Repeat { + optional RepeatMode repeat_mode = 1; +} + +message Shuffle { + optional ShuffleMode shuffle_mode = 1; +} + +message ResponseInfo { + optional string version = 1; + optional EngineState state = 2; + optional bool allow_downloads = 3; + repeated string files_music_extensions = 4; +} + +message ResponseCurrentMetadata { + optional SongMetadata song_metadata = 1; +} + +message ResponsePlaylists { + repeated Playlist playlist = 1; + optional bool include_closed = 2; +} + +message ResponsePlaylistSongs { + optional Playlist requested_playlist = 1; + repeated SongMetadata songs = 2; +} + +message ResponseEngineStateChanged { + optional EngineState state = 1; +} + +message ResponseUpdateTrackPosition { + optional int32 position = 1; +} + +message RequestConnect { + optional int32 auth_code = 1; + optional bool send_playlist_songs = 2; + optional bool downloader = 3; +} + +enum ReasonDisconnect { + Server_Shutdown = 1; + Wrong_Auth_Code = 2; + Not_Authenticated = 3; + Download_Forbidden = 4; +} + +message ResponseDisconnect { + optional ReasonDisconnect reason_disconnect = 1; +} + +message ResponseActiveChanged { + optional int32 playlist_id = 1; +} + +message RequestSetTrackPosition { + optional int32 position = 1; +} + +message RequestInsertUrls { + + 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]; + repeated SongMetadata songs = 6; + optional string new_playlist_name = 7; + +} + +message RequestRemoveSongs { + optional int32 playlist_id = 1; + repeated int32 songs = 2; +} + +message RequestOpenPlaylist { + optional int32 playlist_id = 1; +} + +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 ResponseLyrics { + repeated Lyric lyrics = 1; +} +message Lyric { + optional string song_id = 1; + optional string title = 2; + optional string content = 3; +} + +enum DownloadItem { + CurrentItem = 1; + ItemAlbum = 2; + APlaylist = 3; + Urls = 4; +} + +message RequestDownloadSongs { + optional DownloadItem download_item = 1; + optional int32 playlist_id = 2; + repeated string urls = 3; + repeated int32 songs_ids = 4; + optional string relative_path = 5; +} + +message ResponseSongFileChunk { + optional int32 chunk_number = 1; + optional int32 chunk_count = 2; + optional int32 file_number = 3; + optional int32 file_count = 4; + optional SongMetadata song_metadata = 6; + optional bytes data = 7; + optional int64 size = 8; + optional bytes file_hash = 9; +} + +message ResponseCollectionChunk { + optional int32 chunk_number = 1; + optional int32 chunk_count = 2; + optional bytes data = 3; + optional int64 size = 4; + optional bytes file_hash = 5; +} + +message ResponseSongOffer { + optional bool accepted = 1; +} + +message RequestRateSong { + optional float rating = 1; +} + +message ResponseDownloadTotalSize { + optional int64 total_size = 1; + optional int64 file_count = 2; +} + +message ResponseTranscoderStatus { + optional int32 processed = 1; + optional int32 total = 2; +} + +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 { + optional int32 playlist_id = 1; + 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 Message { + + optional int32 version = 1 [default = 21]; + optional MsgType type = 2 + [default = UNKNOWN]; + + optional RequestConnect request_connect = 21; + optional RequestPlaylists request_playlists = 27; + optional RequestPlaylistSongs request_playlist_songs = 10; + optional RequestChangeSong request_change_song = 11; + optional RequestSetVolume request_set_volume = 12; + optional RequestSetTrackPosition request_set_track_position = 23; + optional RequestInsertUrls request_insert_urls = 25; + 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 RequestListFiles request_list_files = 50; + optional RequestAppendFiles request_append_files = 51; + + optional Repeat repeat = 13; + optional Shuffle shuffle = 14; + + optional ResponseInfo response_info = 15; + optional ResponseCurrentMetadata response_current_metadata = 16; + optional ResponsePlaylists response_playlists = 17; + optional ResponsePlaylistSongs response_playlist_songs = 18; + optional ResponseEngineStateChanged response_engine_state_changed = 19; + optional ResponseUpdateTrackPosition response_update_track_position = 20; + optional ResponseDisconnect response_disconnect = 22; + optional ResponseActiveChanged response_active_changed = 24; + optional ResponseLyrics response_lyrics = 30; + optional ResponseSongFileChunk response_song_file_chunk = 32; + optional ResponseSongOffer response_song_offer = 33; + optional ResponseCollectionChunk response_collection_chunk = 34; + optional ResponseDownloadTotalSize response_download_total_size = 36; + optional ResponseTranscoderStatus response_transcoder_status = 39; + optional ResponseListFiles response_list_files = 52; + +} diff --git a/src/networkremote/outgoingdatacreator.cpp b/src/networkremote/outgoingdatacreator.cpp new file mode 100644 index 0000000000..e591f30579 --- /dev/null +++ b/src/networkremote/outgoingdatacreator.cpp @@ -0,0 +1,638 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2012, Andreas Muttscheller + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "includes/shared_ptr.h" +#include "constants/timeconstants.h" +#include "utilities/randutils.h" +#include "core/player.h" +#include "core/database.h" +#include "core/sqlquery.h" +#include "core/logging.h" +#include "utilities/cryptutils.h" +#include "collection/collectionbackend.h" +#include "playlist/playlistmanager.h" +#include "playlist/playlistbackend.h" +#include "networkremote.h" +#include "networkremoteclient.h" +#include "outgoingdatacreator.h" + +using namespace std::chrono_literals; +using namespace Qt::Literals::StringLiterals; + +namespace { +constexpr quint32 kFileChunkSize = 100000; +} + +OutgoingDataCreator::OutgoingDataCreator(const SharedPtr database, + const SharedPtr player, + const SharedPtr playlist_manager, + const SharedPtr playlist_backend, + QObject *parent) + : QObject(parent), + database_(database), + player_(player), + playlist_manager_(playlist_manager), + playlist_backend_(playlist_backend), + keep_alive_timer_(new QTimer(this)), + keep_alive_timeout_(10000) { + + QObject::connect(keep_alive_timer_, &QTimer::timeout, this, &OutgoingDataCreator::SendKeepAlive); + +} + +OutgoingDataCreator::~OutgoingDataCreator() = default; + +void OutgoingDataCreator::SetClients(QList *clients) { + + clients_ = clients; + // After we got some clients, start the keep alive timer + // Default: every 10 seconds + keep_alive_timer_->start(keep_alive_timeout_); + + // Create the song position timer + track_position_timer_ = new QTimer(this); + QObject::connect(track_position_timer_, &QTimer::timeout, this, &OutgoingDataCreator::UpdateTrackPosition); + +} + +void OutgoingDataCreator::SendDataToClients(networkremote::Message *msg) { + + if (clients_->empty()) { + return; + } + + for (NetworkRemoteClient *client : std::as_const(*clients_)) { + // Do not send data to downloaders + if (client->isDownloader()) { + if (client->State() != QTcpSocket::ConnectedState) { + clients_->removeAt(clients_->indexOf(client)); + delete client; + } + continue; + } + + // Check if the client is still active + if (client->State() == QTcpSocket::ConnectedState) { + client->SendData(msg); + } + else { + clients_->removeAt(clients_->indexOf(client)); + delete client; + } + } + +} + +void OutgoingDataCreator::SendInfo() { + + networkremote::Message msg; + msg.setType(networkremote::MsgTypeGadget::MsgType::INFO); + networkremote::ResponseInfo info; + info.setVersion(QLatin1String("%1 %2").arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion())); + info.setFilesMusicExtensions(files_music_extensions_); + info.setAllowDownloads(allow_downloads_); + info.setState(GetEngineState()); + msg.setResponseInfo(info); + SendDataToClients(&msg); + +} + +void OutgoingDataCreator::SendKeepAlive() { + + networkremote::Message msg; + msg.setType(networkremote::MsgTypeGadget::MsgType::KEEP_ALIVE); + SendDataToClients(&msg); + +} + +networkremote::EngineStateGadget::EngineState OutgoingDataCreator::GetEngineState() { + + switch (player_->GetState()) { + case EngineBase::State::Idle: + return networkremote::EngineStateGadget::EngineState::EngineState_Idle; + break; + case EngineBase::State::Error: + case EngineBase::State::Empty: + return networkremote::EngineStateGadget::EngineState::EngineState_Empty; + break; + case EngineBase::State::Playing: + return networkremote::EngineStateGadget::EngineState::EngineState_Playing; + break; + case EngineBase::State::Paused: + return networkremote::EngineStateGadget::EngineState::EngineState_Paused; + break; + } + + return networkremote::EngineStateGadget::EngineState::EngineState_Empty; + +} + +void OutgoingDataCreator::SendAllPlaylists() { + + // Get all Playlists + const int active_playlist = playlist_manager_->active_id(); + + // Create message + networkremote::Message msg; + msg.setType(networkremote::MsgTypeGadget::MsgType::SEND_PLAYLISTS); + + networkremote::ResponsePlaylists playlists = msg.responsePlaylists(); + playlists.setIncludeClosed(true); + + // Get all playlists, even ones that are hidden in the UI. + const QList all_playlists = playlist_backend_->GetAllPlaylists(); + for (const PlaylistBackend::Playlist &p : all_playlists) { + const bool playlist_open = playlist_manager_->IsPlaylistOpen(p.id); + const int item_count = playlist_open ? playlist_manager_->playlist(p.id)->rowCount() : 0; + + // Create a new playlist + networkremote::Playlist playlist;// = playlists.playlist(); + playlist.setPlaylistId(p.id); + playlist.setName(p.name); + playlist.setActive((p.id == active_playlist)); + playlist.setItemCount(item_count); + playlist.setClosed(!playlist_open); + playlist.setFavorite(p.favorite); + } + + SendDataToClients(&msg); + +} + +void OutgoingDataCreator::SendAllActivePlaylists() { + + const int active_playlist = playlist_manager_->active_id(); + + const QList playlists = playlist_manager_->GetAllPlaylists(); + QList pb_playlists; + pb_playlists.reserve(playlists.count()); + for (Playlist *p : playlists) { + networkremote::Playlist pb_playlist; + pb_playlist.setPlaylistId(p->id()); + pb_playlist.setName(playlist_manager_->GetPlaylistName(p->id())); + pb_playlist.setActive(p->id() == active_playlist); + pb_playlist.setItemCount(p->rowCount()); + pb_playlist.setClosed(false); + pb_playlist.setFavorite(p->is_favorite()); + pb_playlists << pb_playlist; + } + + networkremote::ResponsePlaylists response_playlists; + response_playlists.setPlaylist(pb_playlists); + networkremote::Message msg; + msg.setType(networkremote::MsgTypeGadget::MsgType::SEND_PLAYLISTS); + msg.setResponsePlaylists(response_playlists); + + SendDataToClients(&msg); + +} + +void OutgoingDataCreator::ActiveChanged(Playlist *playlist) { + + SendPlaylistSongs(playlist->id()); + + networkremote::Message msg; + msg.setType(networkremote::MsgTypeGadget::MsgType::ACTIVE_PLAYLIST_CHANGED); + networkremote::ResponseActiveChanged response_active_changed; + response_active_changed.setPlaylistId(playlist->id()); + msg.setResponseActiveChanged(response_active_changed); + SendDataToClients(&msg); + +} + +void OutgoingDataCreator::PlaylistAdded(const int id, const QString &name, const bool favorite) { + + Q_UNUSED(id) + Q_UNUSED(name) + Q_UNUSED(favorite) + + SendAllActivePlaylists(); + +} + +void OutgoingDataCreator::PlaylistDeleted(const int id) { + + Q_UNUSED(id) + + SendAllActivePlaylists(); + +} + +void OutgoingDataCreator::PlaylistClosed(const int id) { + + Q_UNUSED(id) + + SendAllActivePlaylists(); + +} + +void OutgoingDataCreator::PlaylistRenamed(const int id, const QString &new_name) { + + Q_UNUSED(id) + Q_UNUSED(new_name) + + SendAllActivePlaylists(); + +} + +void OutgoingDataCreator::SendFirstData(const bool send_playlist_songs) { + + CurrentSongChanged(current_song_, albumcoverloader_result_); + + VolumeChanged(player_->GetVolume()); + + if (!track_position_timer_->isActive() && player_->engine()->state() == EngineBase::State::Playing) { + track_position_timer_->start(1s); + } + + UpdateTrackPosition(); + + SendAllActivePlaylists(); + + if (send_playlist_songs) { + SendPlaylistSongs(playlist_manager_->active_id()); + } + + SendShuffleMode(playlist_manager_->sequence()->shuffle_mode()); + SendRepeatMode(playlist_manager_->sequence()->repeat_mode()); + + networkremote::Message msg; + msg.setType(networkremote::MsgTypeGadget::MsgType::FIRST_DATA_SENT_COMPLETE); + SendDataToClients(&msg); + +} + +void OutgoingDataCreator::CurrentSongChanged(const Song &song, const AlbumCoverLoaderResult &result) { + + albumcoverloader_result_ = result; + current_song_ = song; + current_image_ = result.album_cover.image; + + SendSongMetadata(); + +} + +void OutgoingDataCreator::SendSongMetadata() { + + networkremote::Message msg; + msg.setType(networkremote::MsgTypeGadget::MsgType::CURRENT_METAINFO); + const networkremote::SongMetadata pb_song_metadata = PbSongMetadataFromSong(playlist_manager_->active()->current_row(), current_song_, current_image_); + networkremote::ResponseCurrentMetadata response_current_metadata; + response_current_metadata.setSongMetadata(pb_song_metadata); + msg.setResponseCurrentMetadata(response_current_metadata); + SendDataToClients(&msg); + +} + +networkremote::SongMetadata OutgoingDataCreator::PbSongMetadataFromSong(const int index, const Song &song, const QImage &image_cover_art) { + + if (!song.is_valid()) { + return networkremote::SongMetadata(); + } + + networkremote::SongMetadata pb_song_metadata; + pb_song_metadata.setSongId(song.id()); + pb_song_metadata.setIndex(index); + pb_song_metadata.setTitle(song.PrettyTitle()); + pb_song_metadata.setArtist(song.artist()); + pb_song_metadata.setAlbum(song.album()); + pb_song_metadata.setAlbumartist(song.albumartist()); + pb_song_metadata.setLength(song.length_nanosec() / kNsecPerSec); + pb_song_metadata.setPrettyLength(song.PrettyLength()); + pb_song_metadata.setGenre(song.genre()); + pb_song_metadata.setPrettyYear(song.PrettyYear()); + pb_song_metadata.setTrack(song.track()); + pb_song_metadata.setDisc(song.disc()); + pb_song_metadata.setPlaycount(song.playcount()); + pb_song_metadata.setIsLocal(song.url().isLocalFile()); + pb_song_metadata.setFilename(song.basefilename()); + pb_song_metadata.setFileSize(song.filesize()); + pb_song_metadata.setRating(song.rating()); + pb_song_metadata.setUrl(song.url().toString()); + pb_song_metadata.setArtAutomatic(song.art_automatic().toString()); + pb_song_metadata.setArtManual(song.art_manual().toString()); + pb_song_metadata.setFiletype(static_cast(song.filetype())); + + if (!image_cover_art.isNull()) { + QImage image_cover_art_small; + if (image_cover_art.width() > 1000 || image_cover_art.height() > 1000) { + image_cover_art_small = image_cover_art.scaled(1000, 1000, Qt::KeepAspectRatio); + } + else { + image_cover_art_small = image_cover_art; + } + + QByteArray data; + QBuffer buffer(&data); + if (buffer.open(QIODevice::WriteOnly)) { + image_cover_art_small.save(&buffer, "JPG"); + buffer.close(); + } + + pb_song_metadata.setArt(data); + } + + return pb_song_metadata; + +} + +void OutgoingDataCreator::VolumeChanged(const uint volume) { + + networkremote::Message msg; + msg.setType(networkremote::MsgTypeGadget::MsgType::SET_VOLUME); + networkremote::RequestSetVolume request_set_volume; + request_set_volume.setVolume(volume); + msg.setRequestSetVolume(request_set_volume); + SendDataToClients(&msg); + +} + +void OutgoingDataCreator::SendPlaylistSongs(const int playlist_id) { + + Playlist *playlist = playlist_manager_->playlist(playlist_id); + if (!playlist) { + qLog(Error) << "Could not find playlist with ID" << playlist_id; + return; + } + + networkremote::Message msg; + msg.setType(networkremote::MsgTypeGadget::MsgType::SEND_PLAYLIST_SONGS); + + networkremote::Playlist pb_playlist; + pb_playlist.setPlaylistId(playlist_id); + networkremote::ResponsePlaylistSongs pb_response_playlist_songs; + pb_response_playlist_songs.setRequestedPlaylist(pb_playlist); + + const SongList songs = playlist->GetAllSongs(); + QList pb_song_metadatas; + pb_song_metadatas.reserve(songs.count()); + for (const Song &song : songs) { + pb_song_metadatas << PbSongMetadataFromSong(songs.indexOf(song), song); + } + + pb_response_playlist_songs.setSongs(pb_song_metadatas); + msg.setResponsePlaylistSongs(pb_response_playlist_songs); + + SendDataToClients(&msg); + +} + +void OutgoingDataCreator::PlaylistChanged(Playlist *playlist) { + SendPlaylistSongs(playlist->id()); +} + +void OutgoingDataCreator::StateChanged(const EngineBase::State state) { + + if (state == last_state_) { + return; + } + last_state_ = state; + + networkremote::Message msg; + + switch (state) { + case EngineBase::State::Playing: + msg.setType(networkremote::MsgTypeGadget::MsgType::PLAY); + track_position_timer_->start(1s); + break; + case EngineBase::State::Paused: + msg.setType(networkremote::MsgTypeGadget::MsgType::PAUSE); + track_position_timer_->stop(); + break; + case EngineBase::State::Empty: + msg.setType(networkremote::MsgTypeGadget::MsgType::STOP); // Empty is called when player stopped + track_position_timer_->stop(); + break; + default: + msg.setType(networkremote::MsgTypeGadget::MsgType::STOP); + track_position_timer_->stop(); + break; + }; + + SendDataToClients(&msg); + +} + +void OutgoingDataCreator::SendRepeatMode(const PlaylistSequence::RepeatMode mode) { + + networkremote::Repeat repeat; + + switch (mode) { + case PlaylistSequence::RepeatMode::Off: + repeat.setRepeatMode(networkremote::RepeatModeGadget::RepeatMode::RepeatMode_Off); + break; + case PlaylistSequence::RepeatMode::Track: + repeat.setRepeatMode(networkremote::RepeatModeGadget::RepeatMode::RepeatMode_Track); + break; + case PlaylistSequence::RepeatMode::Album: + repeat.setRepeatMode(networkremote::RepeatModeGadget::RepeatMode::RepeatMode_Album); + break; + case PlaylistSequence::RepeatMode::Playlist: + repeat.setRepeatMode(networkremote::RepeatModeGadget::RepeatMode::RepeatMode_Playlist); + break; + case PlaylistSequence::RepeatMode::OneByOne: + repeat.setRepeatMode(networkremote::RepeatModeGadget::RepeatMode::RepeatMode_OneByOne); + break; + case PlaylistSequence::RepeatMode::Intro: + repeat.setRepeatMode(networkremote::RepeatModeGadget::RepeatMode::RepeatMode_Intro); + break; + } + + networkremote::Message msg; + msg.setType(networkremote::MsgTypeGadget::MsgType::REPEAT); + msg.setRepeat(repeat); + + SendDataToClients(&msg); + +} + +void OutgoingDataCreator::SendShuffleMode(const PlaylistSequence::ShuffleMode mode) { + + networkremote::Shuffle shuffle; + + switch (mode) { + case PlaylistSequence::ShuffleMode::Off: + shuffle.setShuffleMode(networkremote::ShuffleModeGadget::ShuffleMode::ShuffleMode_Off); + break; + case PlaylistSequence::ShuffleMode::All: + shuffle.setShuffleMode(networkremote::ShuffleModeGadget::ShuffleMode::ShuffleMode_All); + break; + case PlaylistSequence::ShuffleMode::InsideAlbum: + shuffle.setShuffleMode(networkremote::ShuffleModeGadget::ShuffleMode::ShuffleMode_InsideAlbum); + break; + case PlaylistSequence::ShuffleMode::Albums: + shuffle.setShuffleMode(networkremote::ShuffleModeGadget::ShuffleMode::ShuffleMode_Albums); + break; + } + + networkremote::Message msg; + msg.setType(networkremote::MsgTypeGadget::MsgType::SHUFFLE); + msg.setShuffle(shuffle); + + SendDataToClients(&msg); + +} + +void OutgoingDataCreator::UpdateTrackPosition() { + + const qint64 position_nanosec = player_->engine()->position_nanosec(); + int position = static_cast(std::floor(static_cast(position_nanosec) / kNsecPerSec + 0.5)); + + if (position_nanosec > current_song_.length_nanosec()) { + position = last_track_position_; + } + + last_track_position_ = position; + + networkremote::Message msg; + msg.setType(networkremote::MsgTypeGadget::MsgType::UPDATE_TRACK_POSITION); + networkremote::ResponseUpdateTrackPosition reponse_update_track_position; + reponse_update_track_position.setPosition(position); + msg.setResponseUpdateTrackPosition(reponse_update_track_position); + + SendDataToClients(&msg); + +} + +void OutgoingDataCreator::DisconnectAllClients() { + + networkremote::Message msg; + msg.setType(networkremote::MsgTypeGadget::MsgType::DISCONNECT); + networkremote::ResponseDisconnect reponse_disconnect; + reponse_disconnect.setReasonDisconnect(networkremote::ReasonDisconnectGadget::ReasonDisconnect::Server_Shutdown); + msg.setResponseDisconnect(reponse_disconnect); + SendDataToClients(&msg); + +} + +void OutgoingDataCreator::SendCollection(NetworkRemoteClient *client) { + + const QString temp_database_filename = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + u'/' + Utilities::GetRandomStringWithChars(20); + + Database::AttachedDatabase adb(temp_database_filename, ""_L1, true); + QSqlDatabase db(database_->Connect()); + + database_->AttachDatabaseOnDbConnection(u"songs_export"_s, adb, db); + + SqlQuery q(db); + q.prepare(u"CREATE TABLE songs_export.songs AS SELECT * FROM songs WHERE unavailable = 0"_s); + + if (!q.exec()) { + database_->ReportErrors(q); + return; + } + + database_->DetachDatabase(u"songs_export"_s); + + QFile file(temp_database_filename); + const QByteArray sha1 = Utilities::Sha1File(file).toHex(); + qLog(Debug) << "Collection SHA1" << sha1; + + if (!file.open(QIODevice::ReadOnly)) { + qLog(Error) << "Could not open file" << temp_database_filename; + } + + const int chunk_count = qRound((file.size() / kFileChunkSize) + 0.5); + int chunk_number = 0; + while (!file.atEnd()) { + ++chunk_number; + const QByteArray data = file.read(kFileChunkSize); + networkremote::ResponseCollectionChunk chunk; + chunk.setChunkNumber(chunk_number); + chunk.setChunkCount(chunk_count); + chunk.setSize(file.size()); + chunk.setData(data); + chunk.setFileHash(sha1); + networkremote::Message msg; + msg.setType(networkremote::MsgTypeGadget::MsgType::COLLECTION_CHUNK); + msg.setResponseCollectionChunk(chunk); + client->SendData(&msg); + } + + file.remove(); + file.close(); + +} + +void OutgoingDataCreator::SendListFiles(QString relative_path, NetworkRemoteClient *client) { + + networkremote::Message msg; + msg.setType(networkremote::MsgTypeGadget::MsgType::LIST_FILES); + networkremote::ResponseListFiles files; + + if (files_root_folder_.isEmpty()) { + files.setError(networkremote::ResponseListFiles::Error::ROOT_DIR_NOT_SET); + SendDataToClients(&msg); + return; + } + + QDir root_dir(files_root_folder_); + if (!root_dir.exists()) { + files.setError(networkremote::ResponseListFiles::Error::ROOT_DIR_NOT_SET); + } + else if (relative_path.startsWith(".."_L1) || relative_path.startsWith("./.."_L1)) { + files.setError(networkremote::ResponseListFiles::Error::DIR_NOT_ACCESSIBLE); + } + else { + if (relative_path.startsWith("/"_L1)) relative_path.remove(0, 1); + + QFileInfo fi_folder(root_dir, relative_path); + if (!fi_folder.exists()) { + files.setError(networkremote::ResponseListFiles::Error::DIR_NOT_EXIST); + } + else if (!fi_folder.isDir()) { + files.setError(networkremote::ResponseListFiles::Error::DIR_NOT_EXIST); + } + else if (root_dir.relativeFilePath(fi_folder.absoluteFilePath()).startsWith("../"_L1)) { + files.setError(networkremote::ResponseListFiles::Error::DIR_NOT_ACCESSIBLE); + } + else { + files.setRelativePath(root_dir.relativeFilePath(fi_folder.absoluteFilePath())); + QDir dir(fi_folder.absoluteFilePath()); + dir.setFilter(QDir::NoDotAndDotDot | QDir::AllEntries); + dir.setSorting(QDir::Name | QDir::DirsFirst); + + const QList fis = dir.entryInfoList(); + for (const QFileInfo &fi : fis) { + if (fi.isDir() || files_music_extensions_.contains(fi.suffix())) { + networkremote::FileMetadata pb_file;// = files->addFiles(); + pb_file.setIsDir(fi.isDir()); + pb_file.setFilename(fi.fileName()); + } + } + } + } + + msg.setResponseListFiles(files); + + client->SendData(&msg); + +} diff --git a/src/networkremote/outgoingdatacreator.h b/src/networkremote/outgoingdatacreator.h new file mode 100644 index 0000000000..1639d12fbf --- /dev/null +++ b/src/networkremote/outgoingdatacreator.h @@ -0,0 +1,120 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2012, Andreas Muttscheller + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef OUTGOINGDATACREATOR_H +#define OUTGOINGDATACREATOR_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "includes/shared_ptr.h" +#include "engine/enginebase.h" +#include "playlist/playlistsequence.h" +#include "networkremote/networkremotemessages.qpb.h" +#include "covermanager/albumcoverloaderresult.h" + +class Database; +class Player; +class PlaylistManager; +class PlaylistBackend; +class Playlist; +class NetworkRemoteClient; + +class OutgoingDataCreator : public QObject { + Q_OBJECT + + public: + explicit OutgoingDataCreator(const SharedPtr database, + const SharedPtr player, + const SharedPtr playlist_manager, + const SharedPtr playlist_backend, + QObject *parent = nullptr); + + ~OutgoingDataCreator(); + + 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 networkremote::SongMetadata PbSongMetadataFromSong(const int index, const Song &song, const QImage &image_cover_art = QImage()); + + public Q_SLOTS: + void SendInfo(); + void SendKeepAlive(); + void SendAllPlaylists(); + void SendAllActivePlaylists(); + void SendFirstData(const bool send_playlist_songs); + void SendPlaylistSongs(const int id); + void PlaylistChanged(Playlist *playlist); + void VolumeChanged(const uint volume); + void PlaylistAdded(const int id, const QString &name, bool favorite); + void PlaylistDeleted(const int id); + void PlaylistClosed(const int id); + void PlaylistRenamed(const int id, const QString &new_name); + void ActiveChanged(Playlist *playlist); + void CurrentSongChanged(const Song &song, const AlbumCoverLoaderResult &result); + void SendSongMetadata(); + void StateChanged(const EngineBase::State state); + void SendRepeatMode(const PlaylistSequence::RepeatMode mode); + void SendShuffleMode(const PlaylistSequence::ShuffleMode mode); + void UpdateTrackPosition(); + void DisconnectAllClients(); + void SendCollection(NetworkRemoteClient *client); + void SendListFiles(QString relative_path, NetworkRemoteClient *client); + + private: + void SendDataToClients(networkremote::Message *msg); + networkremote::EngineStateGadget::EngineState GetEngineState(); + + private: + const SharedPtr database_; + const SharedPtr player_; + const SharedPtr playlist_manager_; + const SharedPtr playlist_backend_; + QList *clients_; + Song current_song_; + AlbumCoverLoaderResult albumcoverloader_result_; + QImage current_image_; + EngineBase::State last_state_; + QTimer *keep_alive_timer_; + QTimer *track_position_timer_; + int keep_alive_timeout_; + int last_track_position_; + QString files_root_folder_; + QStringList files_music_extensions_; + bool allow_downloads_; +}; + +#endif // OUTGOINGDATACREATOR_H diff --git a/src/networkremote/songsender.cpp b/src/networkremote/songsender.cpp new file mode 100644 index 0000000000..e0c2505e46 --- /dev/null +++ b/src/networkremote/songsender.cpp @@ -0,0 +1,442 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2012, Andreas Muttscheller + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "songsender.h" + +#include +#include +#include +#include +#include + +#include "includes/shared_ptr.h" +#include "constants/networkremotesettingsconstants.h" +#include "constants/networkremoteconstants.h" +#include "core/logging.h" +#include "core/player.h" +#include "collection/collectionbackend.h" +#include "playlist/playlistmanager.h" +#include "playlist/playlist.h" +#include "networkremote.h" +#include "outgoingdatacreator.h" +#include "networkremoteclient.h" +#include "utilities/randutils.h" +#include "utilities/cryptutils.h" + +using namespace Qt::Literals::StringLiterals; +using namespace NetworkRemoteSettingsConstants; +using namespace NetworkRemoteConstants; + +SongSender::SongSender(const SharedPtr player, + const SharedPtr collection_backend, + const SharedPtr playlist_manager, + NetworkRemoteClient *client, + QObject *parent) + : QObject(parent), + player_(player), + collection_backend_(collection_backend), + playlist_manager_(playlist_manager), + client_(client), + transcoder_(new Transcoder(this, QLatin1String(kTranscoderSettingPostfix))) { + + QSettings s; + s.beginGroup(kSettingsGroup); + + transcode_lossless_files_ = s.value("convert_lossless", false).toBool(); + + // Load preset + QString last_output_format = s.value("last_output_format", u"audio/x-vorbis"_s).toString(); + QList presets = transcoder_->GetAllPresets(); + for (int i = 0; i < presets.count(); ++i) { + if (last_output_format == presets.at(i).codec_mimetype_) { + transcoder_preset_ = presets.at(i); + break; + } + } + + qLog(Debug) << "Transcoder preset" << transcoder_preset_.codec_mimetype_; + + QObject::connect(transcoder_, &Transcoder::JobComplete, this, &SongSender::TranscodeJobComplete); + QObject::connect(transcoder_, &Transcoder::AllJobsComplete, this, &SongSender::StartTransfer); + + total_transcode_ = 0; + +} + +SongSender::~SongSender() { + + QObject::disconnect(transcoder_, &Transcoder::JobComplete, this, &SongSender::TranscodeJobComplete); + QObject::disconnect(transcoder_, &Transcoder::AllJobsComplete, this, &SongSender::StartTransfer); + + transcoder_->Cancel(); + +} + +void SongSender::SendSongs(const networkremote::RequestDownloadSongs &request) { + + Song current_song; + if (player_->GetCurrentItem()) { + current_song = player_->GetCurrentItem()->Metadata(); + } + + switch (request.downloadItem()) { + case networkremote::DownloadItemGadget::DownloadItem::CurrentItem:{ + if (current_song.is_valid()) { + const DownloadItem item(current_song, 1, 1); + download_queue_.append(item); + } + break; + } + case networkremote::DownloadItemGadget::DownloadItem::ItemAlbum: + if (current_song.is_valid()) { + SendAlbum(current_song); + } + break; + case networkremote::DownloadItemGadget::DownloadItem::APlaylist: + SendPlaylist(request); + break; + case networkremote::DownloadItemGadget::DownloadItem::Urls: + SendUrls(request); + break; + default: + break; + } + + if (transcode_lossless_files_) { + TranscodeLosslessFiles(); + } + else { + StartTransfer(); + } + +} + +void SongSender::TranscodeLosslessFiles() { + + for (const DownloadItem &item : std::as_const(download_queue_)) { + // Check only lossless files + if (!item.song_.IsFileLossless()) continue; + + // Add the file to the transcoder + const QString local_file = item.song_.url().toLocalFile(); + + qLog(Debug) << "Transcoding" << local_file; + + transcoder_->AddJob(local_file, transcoder_preset_, Utilities::GetRandomStringWithCharsAndNumbers(20)); + + total_transcode_++; + } + + if (total_transcode_ > 0) { + transcoder_->Start(); + SendTranscoderStatus(); + } + else { + StartTransfer(); + } + +} + +void SongSender::TranscodeJobComplete(const QString &input, const QString &output, const bool success) { + + qLog(Debug) << input << "transcoded to" << output << success; + + // If it wasn't successful send original file + if (success) { + transcoder_map_.insert(input, output); + } + + SendTranscoderStatus(); + +} + +void SongSender::SendTranscoderStatus() { + + // Send a message to the remote that we are converting files + networkremote::Message msg; + msg.setType(networkremote::MsgTypeGadget::MsgType::TRANSCODING_FILES); + + networkremote::ResponseTranscoderStatus status = msg.responseTranscoderStatus(); + status.setProcessed(static_cast(transcoder_map_.count())); + status.setTotal(total_transcode_); + + client_->SendData(&msg); + +} + +void SongSender::StartTransfer() { + + total_transcode_ = 0; + + // Send total file size & file count + SendTotalFileSize(); + + // Send first file + OfferNextSong(); + +} + +void SongSender::SendTotalFileSize() { + + networkremote::Message msg; + msg.setType(networkremote::MsgTypeGadget::MsgType::DOWNLOAD_TOTAL_SIZE); + + networkremote::ResponseDownloadTotalSize response = msg.responseDownloadTotalSize(); + + response.setFileCount(download_queue_.size()); + + qint64 total = 0; + for (const DownloadItem &item : std::as_const(download_queue_)) { + QString local_file = item.song_.url().toLocalFile(); + const bool is_transcoded = transcoder_map_.contains(local_file); + + if (is_transcoded) { + local_file = transcoder_map_.value(local_file); + } + + total += QFileInfo(local_file).size(); + + } + + response.setTotalSize(total); + + client_->SendData(&msg); + +} + +void SongSender::OfferNextSong() { + + networkremote::Message msg; + + if (download_queue_.isEmpty()) { + msg.setType(networkremote::MsgTypeGadget::MsgType::DOWNLOAD_QUEUE_EMPTY); + } + else { + // Get the item and send the single song + const DownloadItem item = download_queue_.head(); + + msg.setType(networkremote::MsgTypeGadget::MsgType::SONG_OFFER_FILE_CHUNK); + networkremote::ResponseSongFileChunk chunk = msg.responseSongFileChunk(); + + // Open the file + QFile file(item.song_.url().toLocalFile()); + + // Song offer is chunk no 0 + chunk.setChunkCount(0); + chunk.setChunkNumber(0); + chunk.setFileCount(item.song_count_); + chunk.setFileNumber(item.song_number_); + chunk.setSize(file.size()); + chunk.setSongMetadata(OutgoingDataCreator::PbSongMetadataFromSong(-1, item.song_)); + msg.setResponseSongFileChunk(chunk); + } + + client_->SendData(&msg); + +} + +void SongSender::ResponseSongOffer(const bool accepted) { + + if (download_queue_.isEmpty()) return; + + // Get the item and send the single song + DownloadItem item = download_queue_.dequeue(); + if (accepted) SendSingleSong(item); + + // And offer the next song + OfferNextSong(); + +} + +void SongSender::SendSingleSong(const DownloadItem &download_item) { + + if (!download_item.song_.url().isLocalFile()) return; + + QString local_file = download_item.song_.url().toLocalFile(); + bool is_transcoded = transcoder_map_.contains(local_file); + + if (is_transcoded) { + local_file = transcoder_map_.take(local_file); + } + + // Open the file + QFile file(local_file); + + // Get sha1 for file + QByteArray sha1 = Utilities::Sha1File(file).toHex(); + qLog(Debug) << "sha1 for file" << local_file << "=" << sha1; + + file.open(QIODevice::ReadOnly); + + QByteArray data; + networkremote::Message msg; + networkremote::ResponseSongFileChunk chunk = msg.responseSongFileChunk(); + msg.setType(networkremote::MsgTypeGadget::MsgType::SONG_OFFER_FILE_CHUNK); + + // Calculate the number of chunks + int chunk_count = qRound((static_cast(file.size()) / kFileChunkSize) + 0.5); + int chunk_number = 1; + + while (!file.atEnd()) { + // Read file chunk + data = file.read(kFileChunkSize); + + // Set chunk data + chunk.setChunkCount(chunk_count); + chunk.setChunkNumber(chunk_number); + chunk.setFileCount(download_item.song_count_); + chunk.setFileNumber(download_item.song_number_); + chunk.setSize(file.size()); + chunk.setData(data); + chunk.setFileHash(sha1); + + // On the first chunk send the metadata, so the client knows what file it receives. + if (chunk_number == 1) { + const int i = playlist_manager_->active()->current_row(); + networkremote::SongMetadata song_metadata = OutgoingDataCreator::PbSongMetadataFromSong(i, download_item.song_); + + // If the file was transcoded, we have to change the filename and filesize + if (is_transcoded) { + song_metadata.setFileSize(file.size()); + QString basefilename = download_item.song_.basefilename(); + QFileInfo info(basefilename); + basefilename.replace(u'.' + info.suffix(), u'.' + transcoder_preset_.extension_); + song_metadata.setFilename(basefilename); + } + } + + // Send data directly to the client + client_->SendData(&msg); + + // Clear working data + chunk = networkremote::ResponseSongFileChunk(); + data.clear(); + + chunk_number++; + } + + // If the file was transcoded, delete the temporary one + if (is_transcoded) { + file.remove(); + } + else { + file.close(); + } + +} + +void SongSender::SendAlbum(const Song &album_song) { + + if (!album_song.url().isLocalFile()) return; + + const SongList songs = collection_backend_->GetSongsByAlbum(album_song.album()); + + for (const Song &song : songs) { + const DownloadItem item(song, static_cast(songs.indexOf(song)) + 1, static_cast(songs.size())); + download_queue_.append(item); + } + +} + +void SongSender::SendPlaylist(const networkremote::RequestDownloadSongs &request) { + + const int playlist_id = request.playlistId(); + Playlist *playlist = playlist_manager_->playlist(playlist_id); + if (!playlist) { + qLog(Info) << "Could not find playlist with id = " << playlist_id; + return; + } + const SongList song_list = playlist->GetAllSongs(); + + QList requested_ids; + requested_ids.reserve(request.songsIds().count()); + for (auto song_id : request.songsIds()) { + requested_ids << song_id; + } + + // Count the local songs + int count = 0; + for (const Song &song : song_list) { + if (song.url().isLocalFile() && (requested_ids.isEmpty() || requested_ids.contains(song.id()))) { + ++count; + } + } + + for (const Song &song : song_list) { + if (song.url().isLocalFile() && (requested_ids.isEmpty() || requested_ids.contains(song.id()))) { + DownloadItem item(song, static_cast(song_list.indexOf(song)) + 1, count); + download_queue_.append(item); + } + } + +} + +void SongSender::SendUrls(const networkremote::RequestDownloadSongs &request) { + + SongList songs; + + // First gather all valid songs + if (!request.relativePath().isEmpty()) { + // 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.relativePath(); + if (!root_dir.exists() || relative_path.startsWith(".."_L1) || relative_path.startsWith("./.."_L1)) + return; + + if (relative_path.startsWith(u'/')) relative_path.remove(0, 1); + + QFileInfo fi_folder(root_dir, relative_path); + if (!fi_folder.exists() || !fi_folder.isDir() || root_dir.relativeFilePath(fi_folder.absoluteFilePath()).startsWith(u"../"_s)) { + return; + } + + QDir dir(fi_folder.absoluteFilePath()); + const QStringList &files_music_extensions = client_->files_music_extensions(); + for (const QString &s : request.urls()) { + QFileInfo fi(dir, s); + 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); + songs.append(song); + } + } + } + else { + for (const QString &url_str : request.urls()) { + const QUrl url(url_str); + Song song = collection_backend_->GetSongByUrl(url); + if (song.is_valid() && song.url().isLocalFile()) { + songs.append(song); + } + } + } + + for (const Song &song : songs) { + DownloadItem item(song, static_cast(songs.indexOf(song)) + 1, static_cast(songs.count())); + download_queue_.append(item); + } + +} diff --git a/src/networkremote/songsender.h b/src/networkremote/songsender.h new file mode 100644 index 0000000000..60b32f880e --- /dev/null +++ b/src/networkremote/songsender.h @@ -0,0 +1,96 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2012, Andreas Muttscheller + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef SONGSENDER_H +#define SONGSENDER_H + +#include +#include +#include +#include +#include + +#include "includes/shared_ptr.h" +#include "core/song.h" +#include "networkremote/networkremotemessages.qpb.h" +#include "transcoder/transcoder.h" + +class Player; +class CollectionBackend; +class PlaylistManager; +class NetworkRemoteClient; +class Transcoder; + +class DownloadItem { + public: + explicit DownloadItem(const Song &song, const int song_number, const int song_count) + : song_(song), song_number_(song_number), song_count_(song_count) {} + + Song song_; + int song_number_; + int song_count_; +}; + +class SongSender : public QObject { + Q_OBJECT + + public: + explicit SongSender(const SharedPtr player, + const SharedPtr collection_backend, + const SharedPtr playlist_manager, + NetworkRemoteClient *client, + QObject *parent = nullptr); + + ~SongSender(); + + public Q_SLOTS: + void SendSongs(const networkremote::RequestDownloadSongs &request); + void ResponseSongOffer(bool accepted); + + private Q_SLOTS: + void TranscodeJobComplete(const QString &input, const QString &output, const bool success); + void StartTransfer(); + + private: + const SharedPtr player_; + const SharedPtr collection_backend_; + const SharedPtr playlist_manager_; + NetworkRemoteClient *client_; + + TranscoderPreset transcoder_preset_; + Transcoder *transcoder_; + bool transcode_lossless_files_; + + QQueue download_queue_; + QMap transcoder_map_; + int total_transcode_; + + void SendSingleSong(const DownloadItem &download_item); + void SendAlbum(const Song &song); + void SendPlaylist(const networkremote::RequestDownloadSongs &request); + void SendUrls(const networkremote::RequestDownloadSongs &request); + void OfferNextSong(); + void SendTotalFileSize(); + void TranscodeLosslessFiles(); + void SendTranscoderStatus(); +}; + +#endif // SONGSENDER_H diff --git a/src/playlist/playlistmanager.cpp b/src/playlist/playlistmanager.cpp index 6e2c3912cc..6166289966 100644 --- a/src/playlist/playlistmanager.cpp +++ b/src/playlist/playlistmanager.cpp @@ -57,6 +57,7 @@ #include "playlistview.h" #include "playlistsaveoptionsdialog.h" #include "playlistparsers/playlistparser.h" +#include "queue/queue.h" #include "dialogs/saveplaylistsdialog.h" using namespace Qt::Literals::StringLiterals; @@ -184,9 +185,9 @@ Playlist *PlaylistManager::AddPlaylist(const int id, const QString &name, const } -void PlaylistManager::New(const QString &name, const SongList &songs, const QString &special_type) { +int PlaylistManager::New(const QString &name, const SongList &songs, const QString &special_type) { - if (name.isNull()) return; + if (name.isNull()) return -1; int id = playlist_backend_->CreatePlaylist(name, special_type); @@ -202,6 +203,8 @@ void PlaylistManager::New(const QString &name, const SongList &songs, const QStr Rename(id, QStringLiteral("%1 %2").arg(name).arg(id)); } + return id; + } void PlaylistManager::Load(const QString &filename) { @@ -622,3 +625,22 @@ void PlaylistManager::SaveAllPlaylists() { } } + +void PlaylistManager::Clear(const int id) { + + if (playlists_.count() <= 1 || !playlists_.contains(id)) return; + playlists_[id].p->Clear(); + +} + +void PlaylistManager::Enqueue(const int id, const int i) { + + QModelIndexList dummyIndexList; + + Q_ASSERT(playlists_.contains(id)); + + dummyIndexList.append(playlist(id)->index(i, 0)); + playlist(id)->queue()->ToggleTracks(dummyIndexList); + +} + diff --git a/src/playlist/playlistmanager.h b/src/playlist/playlistmanager.h index 872b238d56..139795716b 100644 --- a/src/playlist/playlistmanager.h +++ b/src/playlist/playlistmanager.h @@ -97,7 +97,7 @@ class PlaylistManager : public PlaylistManagerInterface { PlaylistContainer *playlist_container() const override { return playlist_container_; } public Q_SLOTS: - void New(const QString &name, const SongList &songs = SongList(), const QString &special_type = QString()) override; + int New(const QString &name, const SongList &songs = SongList(), const QString &special_type = QString()) override; void Load(const QString &filename) override; void Save(const int id, const QString &filename, const PlaylistSettings::PathType path_type) override; // Display a file dialog to let user choose a file before saving the file @@ -146,6 +146,9 @@ class PlaylistManager : public PlaylistManagerInterface { void SetActivePaused() override; void SetActiveStopped() override; + void Clear(const int id); + void Enqueue(const int id, const int i); + private Q_SLOTS: void OneOfPlaylistsChanged(); void UpdateSummaryText(); diff --git a/src/playlist/playlistmanagerinterface.h b/src/playlist/playlistmanagerinterface.h index 92badfbe3e..d699141338 100644 --- a/src/playlist/playlistmanagerinterface.h +++ b/src/playlist/playlistmanagerinterface.h @@ -79,7 +79,7 @@ class PlaylistManagerInterface : public QObject { virtual void PlaySmartPlaylist(PlaylistGeneratorPtr generator, const bool as_new, const bool clear) = 0; public Q_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(const int id, const QString &filename, const PlaylistSettings::PathType path_type) = 0; virtual void Rename(const int id, const QString &new_name) = 0; diff --git a/src/queue/queue.h b/src/queue/queue.h index ca8b498854..8c2dafa36c 100644 --- a/src/queue/queue.h +++ b/src/queue/queue.h @@ -51,7 +51,6 @@ class Queue : public QAbstractProxyModel { // Modify the queue int TakeNext(); - void ToggleTracks(const QModelIndexList &source_indexes); void InsertFirst(const QModelIndexList &source_indexes); void Clear(); void Move(const QList &proxy_rows, int pos); @@ -79,6 +78,7 @@ class Queue : public QAbstractProxyModel { public Q_SLOTS: void UpdateSummaryText(); + void ToggleTracks(const QModelIndexList &source_indexes); Q_SIGNALS: void TotalLengthChanged(const quint64 length); diff --git a/src/settings/networkremotesettingspage.cpp b/src/settings/networkremotesettingspage.cpp new file mode 100644 index 0000000000..3683d8562b --- /dev/null +++ b/src/settings/networkremotesettingspage.cpp @@ -0,0 +1,157 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "networkremotesettingspage.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "constants/networkremotesettingsconstants.h" +#include "constants/networkremoteconstants.h" +#include "core/iconloader.h" +#include "networkremote/networkremote.h" +#include "transcoder/transcoder.h" +#include "transcoder/transcoderoptionsdialog.h" +#include "settingsdialog.h" +#include "ui_networkremotesettingspage.h" + +using namespace Qt::Literals::StringLiterals; +using namespace NetworkRemoteSettingsConstants; +using namespace NetworkRemoteConstants; + +namespace { + +static bool ComparePresetsByName(const TranscoderPreset &left, const TranscoderPreset &right) { + return left.name_ < right.name_; +} + +} // namespace + +NetworkRemoteSettingsPage::NetworkRemoteSettingsPage(SettingsDialog *dialog) + : SettingsPage(dialog), + ui_(new Ui_NetworkRemoteSettingsPage) { + + ui_->setupUi(this); + + setWindowIcon(IconLoader::Load(u"ipodtouchicon"_s)); + + QObject::connect(ui_->options, &QPushButton::clicked, this, &NetworkRemoteSettingsPage::Options); + + QList presets = Transcoder::GetAllPresets(); + std::sort(presets.begin(), presets.end(), ComparePresetsByName); + for (const TranscoderPreset &preset : std::as_const(presets)) { + ui_->format->addItem(QStringLiteral("%1 (.%2)").arg(preset.name_, preset.extension_), QVariant::fromValue(preset)); + } + +} + +NetworkRemoteSettingsPage::~NetworkRemoteSettingsPage() { delete ui_; } + +void NetworkRemoteSettingsPage::Load() { + + QSettings s; + + s.beginGroup(kSettingsGroup); + + ui_->enabled->setChecked(s.value(kEnabled).toBool()); + ui_->spinbox_port->setValue(s.value(kPort, kDefaultServerPort).toInt()); + ui_->checkbox_allow_public_access->setChecked(s.value(kAllowPublicAccess, false).toBool()); + + ui_->checkbox_use_auth_code->setChecked(s.value(kUseAuthCode, false).toBool()); + ui_->spinbox_auth_code->setValue(s.value(kAuthCode, QRandomGenerator::global()->bounded(100000)).toInt()); + + ui_->allow_downloads->setChecked(s.value("allow_downloads", false).toBool()); + ui_->convert_lossless->setChecked(s.value("convert_lossless", false).toBool()); + + QString last_output_format = s.value("last_output_format", u"audio/x-vorbis"_s).toString(); + for (int i = 0; i < ui_->format->count(); ++i) { + if (last_output_format == ui_->format->itemData(i).value().codec_mimetype_) { + ui_->format->setCurrentIndex(i); + break; + } + } + + ui_->files_root_folder->SetPath(s.value("files_root_folder").toString()); + ui_->files_music_extensions->setText(s.value("files_music_extensions", kDefaultMusicExtensionsAllowedRemotely).toStringList().join(u',')); + + s.endGroup(); + + // Get local IP addresses + QString ip_addresses; + QList addresses = QNetworkInterface::allAddresses(); + for (const QHostAddress &address : addresses) { + // TODO: Add IPv6 support to tinysvcmdns + if (address.protocol() == QAbstractSocket::IPv4Protocol && !address.isInSubnet(QHostAddress::parseSubnet(u"127.0.0.1/8"_s))) { + if (!ip_addresses.isEmpty()) { + ip_addresses.append(u", "_s); + } + ip_addresses.append(address.toString()); + } + } + ui_->label_ip_address->setText(ip_addresses); + +} + +void NetworkRemoteSettingsPage::Save() { + + QSettings s; + + s.beginGroup(kSettingsGroup); + s.setValue(kEnabled, ui_->enabled->isChecked()); + s.setValue(kPort, ui_->spinbox_port->value()); + s.setValue(kAllowPublicAccess, ui_->checkbox_allow_public_access->isChecked()); + s.setValue(kUseAuthCode, ui_->checkbox_use_auth_code->isChecked()); + s.setValue(kAuthCode, ui_->spinbox_auth_code->value()); + + TranscoderPreset preset = ui_->format->itemData(ui_->format->currentIndex()).value(); + s.setValue("last_output_format", preset.codec_mimetype_); + + s.setValue(kFilesRootFolder, ui_->files_root_folder->Path()); + + QStringList files_music_extensions; + for (const QString &extension : ui_->files_music_extensions->text().split(u',')) { + 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(); + +} + +void NetworkRemoteSettingsPage::Options() { + + TranscoderPreset preset = ui_->format->itemData(ui_->format->currentIndex()).value(); + + TranscoderOptionsDialog dialog(preset.filetype_, this); + dialog.set_settings_postfix(QLatin1String(kTranscoderSettingPostfix)); + dialog.exec(); + +} diff --git a/src/settings/networkremotesettingspage.h b/src/settings/networkremotesettingspage.h new file mode 100644 index 0000000000..75be2067ef --- /dev/null +++ b/src/settings/networkremotesettingspage.h @@ -0,0 +1,46 @@ +/* + * Strawberry Music Player + * This file was part of Clementine. + * Copyright 2010, David Sansome + * Copyright 2024, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef NETWORKREMOTESETTINGSPAGE_H +#define NETWORKREMOTESETTINGSPAGE_H + +#include "settingspage.h" + +class Ui_NetworkRemoteSettingsPage; + +class NetworkRemoteSettingsPage : public SettingsPage { + Q_OBJECT + + public: + explicit NetworkRemoteSettingsPage(SettingsDialog *dialog); + ~NetworkRemoteSettingsPage(); + + void Load(); + void Save(); + + private Q_SLOTS: + void Options(); + + private: + Ui_NetworkRemoteSettingsPage *ui_; +}; + +#endif // NETWORKREMOTESETTINGSPAGE_H diff --git a/src/settings/networkremotesettingspage.ui b/src/settings/networkremotesettingspage.ui new file mode 100644 index 0000000000..2c70df7505 --- /dev/null +++ b/src/settings/networkremotesettingspage.ui @@ -0,0 +1,298 @@ + + + NetworkRemoteSettingsPage + + + + 0 + 0 + 421 + 664 + + + + Network Remote + + + + + + Enabled + + + + + + + false + + + Settings + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + + 171 + 0 + + + + Qt::LeftToRight + + + Port + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + 65535 + + + 8080 + + + + + + + Only accept connections from clients within the ip ranges: +10.x.x.x +172.16.0.0 - 172.31.255.255 +192.168.x.x + + + Allow public access + + + + + + + A client can connect only, if the correct code was entered. + + + Require authentication code + + + + + + + false + + + + + + 99999 + + + + + + + Enter this IP in the App to connect to Clementine. + + + Your IP address: + + + + + + + 127.0.0.1 + + + + + + + Allow a client to download music from this computer. + + + Allow downloads + + + + + + + false + + + Download settings + + + + + + Convert lossless audiofiles before sending them to the remote. + + + Convert lossless files + + + + + + + false + + + Audio format + + + + + + Options... + + + + + + + + + + + + + + + + Root folder that will be browsable from the network remote + + + Files root folder + + + + + + + + 100 + 0 + + + + + + + + + + + comma-separated list of the allowed extensions that will be visible from the network remote (ex: m3u,mp3,flac,ogg,wav) + + + Music extensions remotely visible + + + + + + + + + + Qt::Vertical + + + + 20 + 98 + + + + + + + + + FileChooserWidget + QWidget +
widgets/filechooserwidget.h
+ 1 +
+
+ + + + checkbox_use_auth_code + toggled(bool) + spinbox_auth_code + setEnabled(bool) + + + 137 + 124 + + + 351 + 125 + + + + + enabled + toggled(bool) + groupbox_use_remote_container + setEnabled(bool) + + + 59 + 22 + + + 57 + 43 + + + + + allow_downloads + toggled(bool) + download_settings_container + setEnabled(bool) + + + 196 + 160 + + + 117 + 205 + + + + + convert_lossless + toggled(bool) + format_container + setEnabled(bool) + + + 218 + 212 + + + 218 + 262 + + + + +
diff --git a/src/settings/settingsdialog.cpp b/src/settings/settingsdialog.cpp index 33f277cb26..16e851f649 100644 --- a/src/settings/settingsdialog.cpp +++ b/src/settings/settingsdialog.cpp @@ -90,6 +90,9 @@ # include "qobuz/qobuzservice.h" # include "qobuzsettingspage.h" #endif +#ifdef HAVE_NETWORKREMOTE +# include "networkremotesettingspage.h" +#endif #include "ui_settingsdialog.h" @@ -161,6 +164,10 @@ SettingsDialog::SettingsDialog(const SharedPtr player, AddPage(Page::Qobuz, new QobuzSettingsPage(this, streaming_services->Service(), this), streaming); #endif +#ifdef HAVE_NETWORKREMOTE + AddPage(Page::NetworkRemote, new NetworkRemoteSettingsPage(this)); +#endif + // List box QObject::connect(ui_->list, &QTreeWidget::currentItemChanged, this, &SettingsDialog::CurrentItemChanged); ui_->list->setCurrentItem(pages_[Page::Behaviour].item_); diff --git a/src/settings/settingsdialog.h b/src/settings/settingsdialog.h index 8723ca193c..d444d34952 100644 --- a/src/settings/settingsdialog.h +++ b/src/settings/settingsdialog.h @@ -93,6 +93,7 @@ class SettingsDialog : public QDialog { Tidal, Qobuz, Spotify, + NetworkRemote }; enum Role { diff --git a/src/utilities/cryptutils.cpp b/src/utilities/cryptutils.cpp index 98b1b9e2fc..bb05d1e2f6 100644 --- a/src/utilities/cryptutils.cpp +++ b/src/utilities/cryptutils.cpp @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include "cryptutils.h" @@ -62,4 +64,17 @@ QByteArray HmacSha1(const QByteArray &key, const QByteArray &data) { return Hmac(key, data, QCryptographicHash::Sha1); } +QByteArray Sha1File(QFile &file) { + + file.open(QIODevice::ReadOnly); + QCryptographicHash hash(QCryptographicHash::Sha1); + while (!file.atEnd()) { + hash.addData(file.read(1000000)); + } + file.close(); + + return hash.result(); + +} + } // namespace Utilities diff --git a/src/utilities/cryptutils.h b/src/utilities/cryptutils.h index 9f77ed0d67..d06dfa71b7 100644 --- a/src/utilities/cryptutils.h +++ b/src/utilities/cryptutils.h @@ -23,6 +23,7 @@ #include #include #include +#include namespace Utilities { @@ -30,6 +31,7 @@ QByteArray Hmac(const QByteArray &key, const QByteArray &data, const QCryptograp QByteArray HmacMd5(const QByteArray &key, const QByteArray &data); QByteArray HmacSha256(const QByteArray &key, const QByteArray &data); QByteArray HmacSha1(const QByteArray &key, const QByteArray &data); +QByteArray Sha1File(QFile &file); } // namespace Utilities diff --git a/src/widgets/filechooserwidget.cpp b/src/widgets/filechooserwidget.cpp new file mode 100644 index 0000000000..bebb5038d7 --- /dev/null +++ b/src/widgets/filechooserwidget.cpp @@ -0,0 +1,116 @@ +#include +#include +#include +#include +#include +#include + +#include "filechooserwidget.h" + +using namespace Qt::Literals::StringLiterals; + +FileChooserWidget::FileChooserWidget(QWidget *parent) + : QWidget(parent), + layout_(new QHBoxLayout(this)), + path_edit_(new QLineEdit(this)), + mode_(Mode::Directory) { + + Init(); + +} + +FileChooserWidget::FileChooserWidget(const Mode mode, const QString &initial_path, QWidget* parent) + : QWidget(parent), + layout_(new QHBoxLayout(this)), + path_edit_(new QLineEdit(this)), + mode_(mode) { + + Init(initial_path); + +} + +FileChooserWidget::FileChooserWidget(const Mode mode, const QString &label, const QString &initial_path, QWidget* parent) + : QWidget(parent), + layout_(new QHBoxLayout(this)), + path_edit_(new QLineEdit(this)), + mode_(mode) { + + layout_->addWidget(new QLabel(label, this)); + + Init(initial_path); + +} + +void FileChooserWidget::SetFileFilter(const QString &file_filter) { + file_filter_ = file_filter; +} + +void FileChooserWidget::SetPath(const QString &path) { + + QFileInfo fi(path); + if (fi.exists()) { + path_edit_->setText(path); + open_dir_path_ = fi.absolutePath(); + } + +} + +QString FileChooserWidget::Path() const { + + QString path(path_edit_->text()); + QFileInfo fi(path); + if (!fi.exists()) return QString(); + if (mode_ == Mode::File) { + if (!fi.isFile()) return QString(); + } + else { + if (!fi.isDir()) return QString(); + } + + return path; + +} + +void FileChooserWidget::Init(const QString &initial_path) { + + QFileInfo fi(initial_path); + if (fi.exists()) { + path_edit_->setText(initial_path); + open_dir_path_ = fi.absolutePath(); + } + layout_->addWidget(path_edit_); + + QPushButton* changePath = new QPushButton(QLatin1String("..."), this); + connect(changePath, &QAbstractButton::clicked, this, &FileChooserWidget::ChooseFile); + changePath->setFixedWidth(2 * changePath->fontMetrics().horizontalAdvance(" ... "_L1)); + + layout_->addWidget(changePath); + layout_->setContentsMargins(2, 0, 2, 0); + + setFocusProxy(path_edit_); + +} + +void FileChooserWidget::ChooseFile() { + + QString new_path; + + if (mode_ == Mode::File) { + new_path = QFileDialog::getOpenFileName(this, tr("Select a file"), open_dir_path_, file_filter_); + } + else { + new_path = QFileDialog::getExistingDirectory(this, tr("Select a directory"), open_dir_path_); + } + + if (!new_path.isEmpty()) { + QFileInfo fi(new_path); + open_dir_path_ = fi.absolutePath(); + if (mode_ == Mode::File) { + path_edit_->setText(fi.absoluteFilePath()); + } + else { + path_edit_->setText(fi.absoluteFilePath() + u"/"_s); + } + } + +} diff --git a/src/widgets/filechooserwidget.h b/src/widgets/filechooserwidget.h new file mode 100644 index 0000000000..84243df59f --- /dev/null +++ b/src/widgets/filechooserwidget.h @@ -0,0 +1,40 @@ +#ifndef FILECHOOSERWIDGET_H +#define FILECHOOSERWIDGET_H + +#include + +class QLineEdit; +class QHBoxLayout; + +class FileChooserWidget : public QWidget { + Q_OBJECT + + public: + enum class Mode { File, Directory }; + + public: + explicit FileChooserWidget(QWidget *parent = nullptr); + explicit FileChooserWidget(const Mode mode, const QString& initial_path = QString(), QWidget *parent = nullptr); + explicit FileChooserWidget(const Mode mode, const QString& label, const QString &initial_path = QString(), QWidget *parent = nullptr); + ~FileChooserWidget() = default; + + void SetFileFilter(const QString &file_filter); + + void SetPath(const QString &path); + QString Path() const; + + public Q_SLOTS: + void ChooseFile(); + + private: + void Init(const QString &initial_path = QString()); + + private: + QHBoxLayout *layout_; + QLineEdit *path_edit_; + const Mode mode_; + QString file_filter_; + QString open_dir_path_; +}; + +#endif // FILECHOOSERWIDGET_H