Compare commits

..

1 Commits

Author SHA1 Message Date
Jonas Kvinge
784c86aa80 Add network remote WIP 2025-12-29 00:41:07 +01:00
81 changed files with 5297 additions and 2852 deletions

View File

@@ -218,7 +218,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 GuiPrivate LinguistTools Test)
set(QT_OPTIONAL_COMPONENTS GuiPrivate LinguistTools Test Protobuf)
if(UNIX AND NOT APPLE)
list(APPEND QT_OPTIONAL_COMPONENTS DBus)
endif()
@@ -278,6 +278,7 @@ if(APPLE OR WIN32)
if(TARGET "qtsparkle-qt${QT_VERSION_MAJOR}::qtsparkle")
set(QTSPARKLE_FOUND ON)
endif()
pkg_check_modules(TINYSVCMDNS IMPORTED_TARGET tinysvcmdns)
endif()
if(UNIX AND NOT APPLE)
@@ -381,6 +382,18 @@ optional_component(DISCORD_RPC ON "Discord Rich Presence"
DEPENDS "RapidJSON" RapidJSON_FOUND
)
if(WIN32)
optional_component(NETWORKREMOTE ON "Network remote"
DEPENDS "Qt Protobuf" Qt${QT_VERSION_MAJOR}Protobuf_FOUND
DEPENDS "tinysvcmdns" TINYSVCMDNS_FOUND
)
else()
optional_component(NETWORKREMOTE ON "Network remote"
DEPENDS "Qt Protobuf" Qt${QT_VERSION_MAJOR}Protobuf_FOUND
)
endif()
if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ)
set(HAVE_CHROMAPRINT ON)
endif()
@@ -759,6 +772,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
@@ -834,12 +848,6 @@ set(SOURCES
src/device/devicestatefiltermodel.cpp
src/device/deviceviewcontainer.cpp
src/device/deviceview.cpp
src/artistbio/artistbioview.cpp
src/artistbio/artistbiofetcher.cpp
src/artistbio/artistbioprovider.cpp
src/artistbio/lastfmartistbio.cpp
src/artistbio/wikipediaartistbio.cpp
)
set(HEADERS
@@ -1063,6 +1071,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
@@ -1128,12 +1137,6 @@ set(HEADERS
src/device/devicestatefiltermodel.h
src/device/deviceviewcontainer.h
src/device/deviceview.h
src/artistbio/artistbiofetcher.h
src/artistbio/artistbioprovider.h
src/artistbio/artistbioview.h
src/artistbio/lastfmartistbio.h
src/artistbio/wikipediaartistbio.h
)
set(UI
@@ -1492,6 +1495,57 @@ 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)
@@ -1530,6 +1584,12 @@ if(HAVE_TRANSLATIONS)
endif()
endif()
if(HAVE_NETWORKREMOTE)
qt_add_protobuf(NetworkRemoteMessages
PROTO_FILES src/networkremote/networkremotemessages.proto
)
endif()
target_include_directories(strawberry_lib PUBLIC
${CMAKE_SOURCE_DIR}
${CMAKE_BINARY_DIR}
@@ -1562,6 +1622,7 @@ target_link_libraries(strawberry_lib PUBLIC
Qt${QT_VERSION_MAJOR}::Sql
$<$<BOOL:${HAVE_DBUS}>:Qt${QT_VERSION_MAJOR}::DBus>
$<$<BOOL:${HAVE_QPA_QPLATFORMNATIVEINTERFACE}>:Qt${QT_VERSION_MAJOR}::GuiPrivate>
$<$<BOOL:${HAVE_NETWORKREMOTE}>:Qt${QT_VERSION_MAJOR}::Protobuf>
ICU::uc
ICU::i18n
$<$<BOOL:${HAVE_STREAMTAGREADER}>:PkgConfig::LIBSPARSEHASH>
@@ -1581,6 +1642,7 @@ target_link_libraries(strawberry_lib PUBLIC
$<$<BOOL:${MSVC}>:WindowsApp>
KDAB::kdsingleapplication
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>
$<$<BOOL:${HAVE_NETWORKREMOTE}>:NetworkRemoteMessages>
)
if(APPLE)
@@ -1599,6 +1661,10 @@ if(APPLE)
endif()
endif()
if(WIN32 AND HAVE_NETWORKREMOTE)
target_link_libraries(strawberry_lib PUBLIC PkgConfig::TINYSVCMDNS)
endif()
target_link_libraries(strawberry PUBLIC strawberry_lib)
if(NOT APPLE)

View File

@@ -15,7 +15,6 @@
<file>schema/schema-21.sql</file>
<file>schema/device-schema.sql</file>
<file>style/strawberry.css</file>
<file>style/artistbio.css</file>
<file>style/smartplaylistsearchterm.css</file>
<file>html/oauthsuccess.html</file>
<file>pictures/strawberry.png</file>

View File

@@ -98,7 +98,6 @@
<file>icons/128x128/somafm.png</file>
<file>icons/128x128/radioparadise.png</file>
<file>icons/128x128/musicbrainz.png</file>
<file>icons/128x128/guitar.png</file>
<file>icons/64x64/albums.png</file>
<file>icons/64x64/alsa.png</file>
<file>icons/64x64/application-exit.png</file>
@@ -198,7 +197,6 @@
<file>icons/64x64/somafm.png</file>
<file>icons/64x64/radioparadise.png</file>
<file>icons/64x64/musicbrainz.png</file>
<file>icons/64x64/guitar.png</file>
<file>icons/48x48/albums.png</file>
<file>icons/48x48/alsa.png</file>
<file>icons/48x48/application-exit.png</file>
@@ -302,7 +300,6 @@
<file>icons/48x48/somafm.png</file>
<file>icons/48x48/radioparadise.png</file>
<file>icons/48x48/musicbrainz.png</file>
<file>icons/48x48/guitar.png</file>
<file>icons/32x32/albums.png</file>
<file>icons/32x32/alsa.png</file>
<file>icons/32x32/application-exit.png</file>
@@ -406,7 +403,6 @@
<file>icons/32x32/somafm.png</file>
<file>icons/32x32/radioparadise.png</file>
<file>icons/32x32/musicbrainz.png</file>
<file>icons/32x32/guitar.png</file>
<file>icons/22x22/albums.png</file>
<file>icons/22x22/alsa.png</file>
<file>icons/22x22/application-exit.png</file>
@@ -510,6 +506,5 @@
<file>icons/22x22/somafm.png</file>
<file>icons/22x22/radioparadise.png</file>
<file>icons/22x22/musicbrainz.png</file>
<file>icons/22x22/guitar.png</file>
</qresource>
</RCC>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

View File

@@ -1,7 +0,0 @@
QScrollArea {
background: qpalette(base);
}
QTextEdit {
border: 0px;
}

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
<?xml-stylesheet type="text/xsl" href="introspect.xsl"?>
<!DOCTYPE node SYSTEM "introspect.dtd">
<!--
This file is part of avahi.
avahi is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2 of the
License, or (at your option) any later version.
avahi 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 Lesser General Public
License along with avahi; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA.
-->
<node>
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg name="data" type="s" direction="out"/>
</method>
</interface>
<interface name="org.freedesktop.Avahi.EntryGroup">
<method name="Free"/>
<method name="Commit"/>
<method name="Reset"/>
<method name="GetState">
<arg name="state" type="i" direction="out"/>
</method>
<signal name="StateChanged">
<arg name="state" type="i"/>
<arg name="error" type="s"/>
</signal>
<method name="IsEmpty">
<arg name="empty" type="b" direction="out"/>
</method>
<method name="AddService">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="host" type="s" direction="in"/>
<arg name="port" type="q" direction="in"/>
<arg name="txt" type="aay" direction="in"/>
</method>
<method name="AddServiceSubtype">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="subtype" type="s" direction="in"/>
</method>
<method name="UpdateServiceTxt">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="txt" type="aay" direction="in"/>
</method>
<method name="AddAddress">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="address" type="s" direction="in"/>
</method>
<method name="AddRecord">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="clazz" type="q" direction="in"/>
<arg name="type" type="q" direction="in"/>
<arg name="ttl" type="u" direction="in"/>
<arg name="rdata" type="ay" direction="in"/>
</method>
</interface>
</node>

View File

@@ -0,0 +1,405 @@
<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
<?xml-stylesheet type="text/xsl" href="introspect.xsl"?>
<!DOCTYPE node SYSTEM "introspect.dtd">
<!--
This file is part of avahi.
avahi is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2 of the
License, or (at your option) any later version.
avahi 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 Lesser General Public
License along with avahi; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA.
-->
<node>
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg name="data" type="s" direction="out"/>
</method>
</interface>
<interface name="org.freedesktop.Avahi.Server">
<method name="GetVersionString">
<arg name="version" type="s" direction="out"/>
</method>
<method name="GetAPIVersion">
<arg name="version" type="u" direction="out"/>
</method>
<method name="GetHostName">
<arg name="name" type="s" direction="out"/>
</method>
<method name="SetHostName">
<arg name="name" type="s" direction="in"/>
</method>
<method name="GetHostNameFqdn">
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetDomainName">
<arg name="name" type="s" direction="out"/>
</method>
<method name="IsNSSSupportAvailable">
<arg name="yes" type="b" direction="out"/>
</method>
<method name="GetState">
<arg name="state" type="i" direction="out"/>
</method>
<signal name="StateChanged">
<arg name="state" type="i"/>
<arg name="error" type="s"/>
</signal>
<method name="GetLocalServiceCookie">
<arg name="cookie" type="u" direction="out"/>
</method>
<method name="GetAlternativeHostName">
<arg name="name" type="s" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetAlternativeServiceName">
<arg name="name" type="s" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetNetworkInterfaceNameByIndex">
<arg name="index" type="i" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetNetworkInterfaceIndexByName">
<arg name="name" type="s" direction="in"/>
<arg name="index" type="i" direction="out"/>
</method>
<method name="ResolveHostName">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="ResolveAddress">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="address" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="ResolveService">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="type" type="s" direction="out"/>
<arg name="domain" type="s" direction="out"/>
<arg name="host" type="s" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="port" type="q" direction="out"/>
<arg name="txt" type="aay" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="EntryGroupNew">
<arg name="path" type="o" direction="out"/>
</method>
<method name="DomainBrowserNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="btype" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceTypeBrowserNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceBrowserNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceResolverNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="HostNameResolverNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="AddressResolverNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="address" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="RecordBrowserNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="clazz" type="q" direction="in"/>
<arg name="type" type="q" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
</interface>
<interface name="org.freedesktop.Avahi.Server2">
<method name="GetVersionString">
<arg name="version" type="s" direction="out"/>
</method>
<method name="GetAPIVersion">
<arg name="version" type="u" direction="out"/>
</method>
<method name="GetHostName">
<arg name="name" type="s" direction="out"/>
</method>
<method name="SetHostName">
<arg name="name" type="s" direction="in"/>
</method>
<method name="GetHostNameFqdn">
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetDomainName">
<arg name="name" type="s" direction="out"/>
</method>
<method name="IsNSSSupportAvailable">
<arg name="yes" type="b" direction="out"/>
</method>
<method name="GetState">
<arg name="state" type="i" direction="out"/>
</method>
<signal name="StateChanged">
<arg name="state" type="i"/>
<arg name="error" type="s"/>
</signal>
<method name="GetLocalServiceCookie">
<arg name="cookie" type="u" direction="out"/>
</method>
<method name="GetAlternativeHostName">
<arg name="name" type="s" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetAlternativeServiceName">
<arg name="name" type="s" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetNetworkInterfaceNameByIndex">
<arg name="index" type="i" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetNetworkInterfaceIndexByName">
<arg name="name" type="s" direction="in"/>
<arg name="index" type="i" direction="out"/>
</method>
<method name="ResolveHostName">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="ResolveAddress">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="address" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="ResolveService">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="type" type="s" direction="out"/>
<arg name="domain" type="s" direction="out"/>
<arg name="host" type="s" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="port" type="q" direction="out"/>
<arg name="txt" type="aay" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="EntryGroupNew">
<arg name="path" type="o" direction="out"/>
</method>
<method name="DomainBrowserPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="btype" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceTypeBrowserPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceBrowserPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceResolverPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="HostNameResolverPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="AddressResolverPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="address" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="RecordBrowserPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="clazz" type="q" direction="in"/>
<arg name="type" type="q" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
</interface>
</node>

View File

@@ -1,126 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <QObject>
#include <QTimer>
#include <QUrl>
#include "artistbiofetcher.h"
#include "artistbioprovider.h"
#include "core/logging.h"
ArtistBioFetcher::ArtistBioFetcher(QObject *parent)
: QObject(parent),
timeout_duration_(kDefaultTimeoutDuration),
next_id_(1) {}
void ArtistBioFetcher::AddProvider(ArtistBioProvider *provider) {
providers_ << provider;
connect(provider, SIGNAL(ImageReady(int, QUrl)), SLOT(ImageReady(int, QUrl)), Qt::QueuedConnection);
connect(provider, SIGNAL(InfoReady(int, CollapsibleInfoPane::Data)), SLOT(InfoReady(int, CollapsibleInfoPane::Data)), Qt::QueuedConnection);
connect(provider, SIGNAL(Finished(int)), SLOT(ProviderFinished(int)), Qt::QueuedConnection);
}
ArtistBioFetcher::~ArtistBioFetcher() {
while (!providers_.isEmpty()) {
ArtistBioProvider *provider = providers_.takeFirst();
provider->deleteLater();
}
}
int ArtistBioFetcher::FetchInfo(const Song &metadata) {
const int id = next_id_++;
results_[id] = Result();
timeout_timers_[id] = new QTimer(this);
timeout_timers_[id]->setSingleShot(true);
timeout_timers_[id]->setInterval(timeout_duration_);
timeout_timers_[id]->start();
connect(timeout_timers_[id], &QTimer::timeout, [this, id]() { Timeout(id); });
for (ArtistBioProvider *provider : providers_) {
if (provider->is_enabled()) {
waiting_for_[id].append(provider);
provider->Start(id, metadata);
}
}
return id;
}
void ArtistBioFetcher::ImageReady(const int id, const QUrl &url) {
if (!results_.contains(id)) return;
results_[id].images_ << url;
}
void ArtistBioFetcher::InfoReady(const int id, const CollapsibleInfoPane::Data &data) {
if (!results_.contains(id)) return;
results_[id].info_ << data;
if (!waiting_for_.contains(id)) return;
Q_EMIT InfoResultReady(id, data);
}
void ArtistBioFetcher::ProviderFinished(const int id) {
if (!results_.contains(id)) return;
if (!waiting_for_.contains(id)) return;
ArtistBioProvider *provider = qobject_cast<ArtistBioProvider*>(sender());
if (!waiting_for_[id].contains(provider)) return;
waiting_for_[id].removeAll(provider);
if (waiting_for_[id].isEmpty()) {
Result result = results_.take(id);
Q_EMIT ResultReady(id, result);
waiting_for_.remove(id);
delete timeout_timers_.take(id);
}
}
void ArtistBioFetcher::Timeout(const int id) {
if (!results_.contains(id)) return;
if (!waiting_for_.contains(id)) return;
// Emit the results that we have already
Q_EMIT ResultReady(id, results_.take(id));
// Cancel any providers that we're still waiting for
for (ArtistBioProvider *provider : waiting_for_[id]) {
qLog(Info) << "Request timed out from info provider" << provider->name();
provider->Cancel(id);
}
waiting_for_.remove(id);
// Remove the timer
delete timeout_timers_.take(id);
}

View File

@@ -1,75 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef ARTISTBIOFETCHER_H
#define ARTISTBIOFETCHER_H
#include <QObject>
#include <QList>
#include <QMap>
#include <QUrl>
#include "widgets/collapsibleinfopane.h"
#include "core/song.h"
class QTimer;
class ArtistBioProvider;
class ArtistBioFetcher : public QObject {
Q_OBJECT
public:
explicit ArtistBioFetcher(QObject *parent = nullptr);
~ArtistBioFetcher() override;
struct Result {
QList<QUrl> images_;
QList<CollapsibleInfoPane::Data> info_;
};
static const int kDefaultTimeoutDuration = 25000;
void AddProvider(ArtistBioProvider *provider);
int FetchInfo(const Song &metadata);
QList<ArtistBioProvider*> providers() const { return providers_; }
Q_SIGNALS:
void InfoResultReady(int, CollapsibleInfoPane::Data);
void ResultReady(int, ArtistBioFetcher::Result);
private Q_SLOTS:
void ImageReady(const int id, const QUrl &url);
void InfoReady(const int id, const CollapsibleInfoPane::Data &data);
void ProviderFinished(const int id);
void Timeout(const int id);
private:
QList<ArtistBioProvider*> providers_;
QMap<int, Result> results_;
QMap<int, QList<ArtistBioProvider*>> waiting_for_;
QMap<int, QTimer*> timeout_timers_;
int timeout_duration_;
int next_id_;
};
#endif // ARTISTBIOFETCHER_H

View File

@@ -1,53 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef ARTISTBIOPROVIDER_H
#define ARTISTBIOPROVIDER_H
#include <QObject>
#include <QUrl>
#include "widgets/collapsibleinfopane.h"
#include "core/song.h"
class ArtistBioProvider : public QObject {
Q_OBJECT
public:
explicit ArtistBioProvider();
virtual void Start(const int id, const Song &song) = 0;
virtual void Cancel(const int) {}
virtual QString name() const;
bool is_enabled() const { return enabled_; }
void set_enabled(bool enabled) { enabled_ = enabled; }
Q_SIGNALS:
void ImageReady(int, QUrl);
void InfoReady(int, CollapsibleInfoPane::Data);
void Finished(int);
private:
bool enabled_;
};
#endif // ARTISTBIOPROVIDER_H

View File

@@ -1,287 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <QWidget>
#include <QFile>
#include <QScrollArea>
#include <QSettings>
#include <QSpacerItem>
#include <QTimer>
#include <QVBoxLayout>
#include <QShowEvent>
#include "core/song.h"
#include "core/networkaccessmanager.h"
#include "widgets/prettyimageview.h"
#include "widgets/widgetfadehelper.h"
#include "artistbiofetcher.h"
#include "lastfmartistbio.h"
#include "wikipediaartistbio.h"
#include "artistbioview.h"
const char *ArtistBioView::kSettingsGroup = "ArtistBio";
ArtistBioView::ArtistBioView(QWidget *parent)
: QWidget(parent),
network_(new NetworkAccessManager(this)),
fetcher_(new ArtistBioFetcher(this)),
current_request_id_(-1),
container_(new QVBoxLayout),
section_container_(nullptr),
fader_(new WidgetFadeHelper(this, 1000)),
dirty_(false) {
// Add the top-level scroll area
QScrollArea *scrollarea = new QScrollArea(this);
setLayout(new QVBoxLayout);
layout()->setContentsMargins(0, 0, 0, 0);
layout()->addWidget(scrollarea);
// Add a container widget to the scroll area
QWidget *container_widget = new QWidget;
container_widget->setLayout(container_);
container_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
container_widget->setBackgroundRole(QPalette::Base);
container_->setSizeConstraint(QLayout::SetMinAndMaxSize);
container_->setContentsMargins(0, 0, 0, 0);
container_->setSpacing(6);
scrollarea->setWidget(container_widget);
scrollarea->setWidgetResizable(true);
// Add a spacer to the bottom of the container
container_->addStretch();
// Set stylesheet
QFile stylesheet(QStringLiteral(":/style/artistbio.css"));
if (stylesheet.open(QIODevice::ReadOnly)) {
setStyleSheet(QString::fromLatin1(stylesheet.readAll()));
stylesheet.close();
}
fetcher_->AddProvider(new LastFMArtistBio);
fetcher_->AddProvider(new WikipediaArtistBio);
connect(fetcher_, SIGNAL(ResultReady(int, ArtistBioFetcher::Result)), SLOT(ResultReady(int, ArtistBioFetcher::Result)));
connect(fetcher_, SIGNAL(InfoResultReady(int, CollapsibleInfoPane::Data)), SLOT(InfoResultReady(int, CollapsibleInfoPane::Data)));
}
ArtistBioView::~ArtistBioView() {}
void ArtistBioView::showEvent(QShowEvent *e) {
if (dirty_) {
MaybeUpdate(queued_metadata_);
dirty_ = false;
}
QWidget::showEvent(e);
}
void ArtistBioView::ReloadSettings() {
for (CollapsibleInfoPane *pane : sections_) {
QWidget *contents = pane->data().contents_;
if (!contents) continue;
QMetaObject::invokeMethod(contents, "ReloadSettings");
}
}
bool ArtistBioView::NeedsUpdate(const Song &old_metadata, const Song &new_metadata) const {
if (new_metadata.artist().isEmpty()) return false;
return old_metadata.artist() != new_metadata.artist();
}
void ArtistBioView::InfoResultReady(const int id, const CollapsibleInfoPane::Data &_data) {
if (id != current_request_id_) return;
AddSection(new CollapsibleInfoPane(_data, this));
CollapseSections();
}
void ArtistBioView::ResultReady(const int id, const ArtistBioFetcher::Result &result) {
if (id != current_request_id_) return;
if (!result.images_.isEmpty()) {
// Image view goes at the top
PrettyImageView *image_view = new PrettyImageView(network_, this);
AddWidget(image_view);
for (const QUrl& url : result.images_) {
image_view->AddImage(url);
}
}
CollapseSections();
}
void ArtistBioView::Clear() {
fader_->StartFade();
qDeleteAll(widgets_);
widgets_.clear();
if (section_container_) {
container_->removeWidget(section_container_);
delete section_container_;
}
sections_.clear();
// Container for collapsible sections goes below
section_container_ = new QWidget;
section_container_->setLayout(new QVBoxLayout);
section_container_->layout()->setContentsMargins(0, 0, 0, 0);
section_container_->layout()->setSpacing(1);
section_container_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
container_->insertWidget(0, section_container_);
}
void ArtistBioView::AddSection(CollapsibleInfoPane *section) {
int i = 0;
for (; i < sections_.count(); ++i) {
if (section->data() < sections_[i]->data()) break;
}
ConnectWidget(section->data().contents_);
sections_.insert(i, section);
qobject_cast<QVBoxLayout*>(section_container_->layout())->insertWidget(i, section);
section->show();
}
void ArtistBioView::AddWidget(QWidget *widget) {
ConnectWidget(widget);
container_->insertWidget(container_->count() - 2, widget);
widgets_ << widget;
}
void ArtistBioView::SongChanged(const Song &metadata) {
if (isVisible()) {
MaybeUpdate(metadata);
dirty_ = false;
}
else {
queued_metadata_ = metadata;
dirty_ = true;
}
}
void ArtistBioView::SongFinished() { dirty_ = false; }
void ArtistBioView::MaybeUpdate(const Song &metadata) {
if (old_metadata_.is_valid()) {
if (!NeedsUpdate(old_metadata_, metadata)) {
return;
}
}
Update(metadata);
old_metadata_ = metadata;
}
void ArtistBioView::Update(const Song &metadata) {
current_request_id_ = fetcher_->FetchInfo(metadata);
// Do this after the new pane has been shown otherwise it'll just grab a black rectangle.
Clear();
QTimer::singleShot(0, fader_, SLOT(StartBlur()));
}
void ArtistBioView::CollapseSections() {
QSettings s;
s.beginGroup(kSettingsGroup);
// Sections are already sorted by type and relevance, so the algorithm we use to determine which ones to show by default is:
// * In the absence of any user preference, show the first (highest relevance section of each type and hide the rest)
// * If one or more sections in a type have been explicitly hidden/shown by the user before then hide all sections in that type and show only the ones that are explicitly shown.
QMultiMap<CollapsibleInfoPane::Data::Type, CollapsibleInfoPane*> types_;
QSet<CollapsibleInfoPane::Data::Type> has_user_preference_;
for (CollapsibleInfoPane *pane : sections_) {
const CollapsibleInfoPane::Data::Type type = pane->data().type_;
types_.insert(type, pane);
QVariant preference = s.value(pane->data().id_);
if (preference.isValid()) {
has_user_preference_.insert(type);
if (preference.toBool()) {
pane->Expand();
}
}
}
for (CollapsibleInfoPane::Data::Type type : types_.keys()) {
if (!has_user_preference_.contains(type)) {
// Expand the first one
types_.values(type).last()->Expand();
}
}
for (CollapsibleInfoPane *pane : sections_) {
connect(pane, SIGNAL(Toggled(bool)), SLOT(SectionToggled(bool)));
}
}
void ArtistBioView::SectionToggled(const bool value) {
CollapsibleInfoPane *pane = qobject_cast<CollapsibleInfoPane*>(sender());
if (!pane || !sections_.contains(pane)) return;
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue(pane->data().id_, value);
s.endGroup();
}
void ArtistBioView::ConnectWidget(QWidget *widget) {
const QMetaObject *m = widget->metaObject();
if (m->indexOfSignal("ShowSettingsDialog()") != -1) {
connect(widget, SIGNAL(ShowSettingsDialog()), SIGNAL(ShowSettingsDialog()));
}
}

View File

@@ -1,104 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef ARTISTBIOVIEW_H
#define ARTISTBIOVIEW_H
#include <QObject>
#include <QWidget>
#include <QList>
#include "core/song.h"
#include "widgets/collapsibleinfopane.h"
#include "widgets/widgetfadehelper.h"
#include "widgets/collapsibleinfopane.h"
#include "playlist/playlistitem.h"
#include "smartplaylists/playlistgenerator_fwd.h"
#include "artistbiofetcher.h"
class QNetworkAccessManager;
class QTimeLine;
class QVBoxLayout;
class QScrollArea;
class QShowEvent;
class PrettyImageView;
class CollapsibleInfoPane;
class WidgetFadeHelper;
class ArtistBioView : public QWidget {
Q_OBJECT
public:
explicit ArtistBioView(QWidget *parent = nullptr);
~ArtistBioView() override;
static const char *kSettingsGroup;
public Q_SLOTS:
void SongChanged(const Song& metadata);
void SongFinished();
virtual void ReloadSettings();
Q_SIGNALS:
void ShowSettingsDialog();
protected:
void showEvent(QShowEvent *e) override;
void Update(const Song &metadata);
void AddWidget(QWidget *widget);
void AddSection(CollapsibleInfoPane *section);
void Clear();
void CollapseSections();
bool NeedsUpdate(const Song& old_metadata, const Song &new_metadata) const;
protected Q_SLOTS:
void ResultReady(const int id, const ArtistBioFetcher::Result &result);
void InfoResultReady(const int id, const CollapsibleInfoPane::Data &data);
protected:
QNetworkAccessManager *network_;
ArtistBioFetcher *fetcher_;
int current_request_id_;
private:
void MaybeUpdate(const Song &metadata);
void ConnectWidget(QWidget *widget);
private Q_SLOTS:
void SectionToggled(const bool value);
private:
QVBoxLayout *container_;
QList<QWidget*> widgets_;
QWidget *section_container_;
QList<CollapsibleInfoPane*> sections_;
WidgetFadeHelper *fader_;
Song queued_metadata_;
Song old_metadata_;
bool dirty_;
};
#endif // ARTISTBIOVIEW_H

View File

@@ -1,209 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <algorithm>
#include <QtGlobal>
#include <QLocale>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QDateTime>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include "core/networkaccessmanager.h"
#include "core/song.h"
#include "core/logging.h"
#include "core/iconloader.h"
#include "lastfmartistbio.h"
#include "widgets/infotextview.h"
#include "scrobbler/scrobblingapi20.h"
#include "scrobbler/lastfmscrobbler.h"
LastFMArtistBio::LastFMArtistBio() : ArtistBioProvider(), network_(new NetworkAccessManager(this)) {}
LastFMArtistBio::~LastFMArtistBio() {
while (!replies_.isEmpty()) {
QNetworkReply *reply = replies_.takeFirst();
disconnect(reply, nullptr, this, nullptr);
reply->abort();
reply->deleteLater();
}
}
void LastFMArtistBio::Start(const int id, const Song &song) {
ParamList params = ParamList()
<< Param(QStringLiteral("api_key"), QString::fromLatin1(ScrobblingAPI20::kApiKey))
<< Param(QStringLiteral("lang"), QLocale().name().left(2).toLower())
<< Param(QStringLiteral("format"), QStringLiteral("json"))
<< Param(QStringLiteral("method"), QStringLiteral("artist.getinfo"))
<< Param(QStringLiteral("artist"), song.artist());
std::sort(params.begin(), params.end());
QUrlQuery url_query;
for (const Param &param : params) {
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second)));
}
QUrl url(QString::fromLatin1(LastFMScrobbler::kApiUrl));
url.setQuery(url_query);
QNetworkRequest req(url);
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
#else
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
#endif
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded"));
QNetworkReply *reply = network_->get(req);
replies_ << reply;
connect(reply, &QNetworkReply::finished, [=] { RequestFinished(reply, id); });
qLog(Debug) << "Sending request" << url_query.toString(QUrl::FullyDecoded);
}
QByteArray LastFMArtistBio::GetReplyData(QNetworkReply *reply) {
QByteArray data;
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
data = reply->readAll();
}
else {
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
// This is a network error, there is nothing more to do.
Error(QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
}
else {
QString error;
// See if there is Json data containing "error" and "message" - then use that instead.
data = reply->readAll();
QJsonParseError json_error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
int error_code = -1;
if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) {
QJsonObject json_obj = json_doc.object();
if (json_obj.contains(QLatin1String("error")) && json_obj.contains(QLatin1String("message"))) {
error_code = json_obj[QLatin1String("error")].toInt();
QString error_message = json_obj[QLatin1String("message")].toString();
error = QStringLiteral("%1 (%2)").arg(error_message).arg(error_code);
}
}
if (error.isEmpty()) {
if (reply->error() != QNetworkReply::NoError) {
error = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
else {
error = QStringLiteral("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
}
}
Error(error);
}
return QByteArray();
}
return data;
}
QJsonObject LastFMArtistBio::ExtractJsonObj(const QByteArray &data) {
if (data.isEmpty()) return QJsonObject();
QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
Error(QStringLiteral("Reply from server missing Json data."), data);
return QJsonObject();
}
if (json_doc.isEmpty()) {
Error(QStringLiteral("Received empty Json document."), json_doc);
return QJsonObject();
}
if (!json_doc.isObject()) {
Error(QStringLiteral("Json document is not an object."), json_doc);
return QJsonObject();
}
QJsonObject json_obj = json_doc.object();
if (json_obj.isEmpty()) {
Error(QStringLiteral("Received empty Json object."), json_doc);
return QJsonObject();
}
return json_obj;
}
void LastFMArtistBio::RequestFinished(QNetworkReply *reply, const int id) {
if (!replies_.contains(reply)) return;
replies_.removeAll(reply);
disconnect(reply, nullptr, this, nullptr);
reply->deleteLater();
QJsonObject json_obj = ExtractJsonObj(GetReplyData(reply));
QString title;
QString text;
if (!json_obj.isEmpty() && json_obj.contains(QLatin1String("artist")) && json_obj[QLatin1String("artist")].isObject()) {
json_obj = json_obj[QLatin1String("artist")].toObject();
if (json_obj.contains(QLatin1String("bio")) && json_obj[QLatin1String("bio")].isObject()) {
title = json_obj[QLatin1String("name")].toString();
QJsonObject obj_bio = json_obj[QLatin1String("bio")].toObject();
if (obj_bio.contains(QLatin1String("content"))) {
text = obj_bio[QLatin1String("content")].toString();
}
}
}
CollapsibleInfoPane::Data info_data;
info_data.id_ = title;
info_data.title_ = tr("Biography");
info_data.type_ = CollapsibleInfoPane::Data::Type_Biography;
info_data.icon_ = IconLoader::Load(QStringLiteral("scrobble"));
InfoTextView *editor = new InfoTextView;
editor->SetHtml(text);
info_data.contents_ = editor;
Q_EMIT InfoReady(id, info_data);
Q_EMIT Finished(id);
}
void LastFMArtistBio::Error(const QString &error, const QVariant &debug) {
qLog(Error) << error;
if (debug.isValid()) qLog(Debug) << debug;
}

View File

@@ -1,66 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef LASTFMARTISTBIO_H
#define LASTFMARTISTBIO_H
#include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QList>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include "core/song.h"
#include "artistbioprovider.h"
class NetworkAccessManager;
class QNetworkReply;
class LastFMArtistBio : public ArtistBioProvider {
Q_OBJECT
public:
explicit LastFMArtistBio();
~LastFMArtistBio();
void Start(const int id, const Song &song) override;
private:
using Param = QPair<QString, QString>;
using ParamList = QList<Param>;
QNetworkReply *CreateRequest(const ParamList &request_params);
QByteArray GetReplyData(QNetworkReply *reply);
QJsonObject ExtractJsonObj(const QByteArray &data);
void Error(const QString &error, const QVariant &debug = QVariant());
private Q_SLOTS:
void RequestFinished(QNetworkReply *reply, const int id);
private:
NetworkAccessManager *network_;
QList<QNetworkReply*> replies_;
};
#endif // LASTFMARTISTBIO_H

View File

@@ -1,319 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <QObject>
#include <QList>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QUrlQuery>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QNetworkReply>
#include "core/logging.h"
#include "core/networkaccessmanager.h"
#include "core/iconloader.h"
#include "core/latch.h"
#include "widgets/infotextview.h"
#include "wikipediaartistbio.h"
namespace {
constexpr char kApiUrl[] = "https://en.wikipedia.org/w/api.php";
constexpr int kMinimumImageSize = 400;
}
WikipediaArtistBio::WikipediaArtistBio() : ArtistBioProvider(), network_(new NetworkAccessManager(this)) {}
WikipediaArtistBio::~WikipediaArtistBio() {
while (!replies_.isEmpty()) {
QNetworkReply *reply = replies_.takeFirst();
disconnect(reply, nullptr, this, nullptr);
if (reply->isRunning()) reply->abort();
reply->deleteLater();
}
}
QNetworkReply *WikipediaArtistBio::CreateRequest(QList<Param> &params) {
params << Param(QLatin1String("format"), QLatin1String("json"));
params << Param(QLatin1String("action"), QLatin1String("query"));
QUrlQuery url_query;
for (const Param &param : params) {
url_query.addQueryItem(QString::fromLatin1(QUrl::toPercentEncoding(param.first)), QString::fromLatin1(QUrl::toPercentEncoding(param.second)));
}
QUrl url(QString::fromLatin1(kApiUrl));
url.setQuery(url_query);
QNetworkRequest req(url);
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
#else
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
#endif
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded"));
QNetworkReply *reply = network_->get(req);
connect(reply, &QNetworkReply::sslErrors, this, &WikipediaArtistBio::HandleSSLErrors);
replies_ << reply;
return reply;
}
QByteArray WikipediaArtistBio::GetReplyData(QNetworkReply *reply) {
QByteArray data;
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
data = reply->readAll();
}
else {
if (reply->error() == QNetworkReply::NoError) {
qLog(Error) << "Wikipedia artist biography error: Received HTTP code" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
else {
qLog(Error) << "Wikipedia artist biography error:" << reply->error() << reply->errorString();
}
}
return data;
}
QJsonObject WikipediaArtistBio::ExtractJsonObj(const QByteArray &data) {
if (data.isEmpty()) return QJsonObject();
QJsonParseError json_error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
if (json_error.error != QJsonParseError::NoError) {
qLog(Error) << "Wikipedia artist biography error: Failed to parse json data:" << json_error.errorString();
return QJsonObject();
}
if (json_doc.isEmpty()) {
qLog(Error) << "Wikipedia artist biography error: Received empty Json document.";
return QJsonObject();
}
if (!json_doc.isObject()) {
qLog(Error) << "Wikipedia artist biography error: Json document is not an object.";
return QJsonObject();
}
QJsonObject json_obj = json_doc.object();
if (json_obj.isEmpty()) {
qLog(Error) << "Wikipedia artist biography error: Received empty Json object.";
return QJsonObject();
}
return json_obj;
}
void WikipediaArtistBio::HandleSSLErrors(QList<QSslError>) {}
void WikipediaArtistBio::Start(const int id, const Song &metadata) {
if (metadata.artist().isEmpty()) {
Q_EMIT Finished(id);
return;
}
CountdownLatch *latch = new CountdownLatch;
connect(latch, &CountdownLatch::Done, [this, id, latch](){
latch->deleteLater();
Q_EMIT Finished(id);
});
GetImageTitles(id, metadata.artist(), latch);
//GetArticle(id, metadata.artist(), latch);
}
void WikipediaArtistBio::GetArticle(const int id, const QString &artist, CountdownLatch *latch) {
latch->Wait();
ParamList params = ParamList() << Param(QStringLiteral("titles"), artist)
<< Param(QStringLiteral("prop"), QStringLiteral("extracts"));
QNetworkReply *reply = CreateRequest(params);
connect(reply, &QNetworkReply::finished, [this, reply, id, latch]() { GetArticleReply(reply, id, latch); });
}
void WikipediaArtistBio::GetArticleReply(QNetworkReply *reply, const int id, CountdownLatch *latch) {
reply->deleteLater();
replies_.removeAll(reply);
QJsonObject json_obj = ExtractJsonObj(GetReplyData(reply));
QString title;
QString text;
if (!json_obj.isEmpty() && json_obj.contains(QLatin1String("query")) && json_obj[QLatin1String("query")].isObject()) {
json_obj = json_obj[QLatin1String("query")].toObject();
if (json_obj.contains(QLatin1String("pages")) && json_obj[QLatin1String("pages")].isObject()) {
QJsonObject value_pages = json_obj[QLatin1String("pages")].toObject();
for (const QJsonValue value_page : value_pages) {
if (!value_page.isObject()) continue;
QJsonObject obj_page = value_page.toObject();
if (!obj_page.contains(QLatin1String("title")) || !obj_page.contains(QLatin1String("extract"))) continue;
title = obj_page[QLatin1String("title")].toString();
text = obj_page[QLatin1String("extract")].toString();
}
}
}
CollapsibleInfoPane::Data info_data;
info_data.id_ = title;
info_data.title_ = tr("Biography");
info_data.type_ = CollapsibleInfoPane::Data::Type_Biography;
info_data.icon_ = IconLoader::Load(QStringLiteral("wikipedia"));
InfoTextView *editor = new InfoTextView;
editor->SetHtml(text);
info_data.contents_ = editor;
Q_EMIT InfoReady(id, info_data);
latch->CountDown();
}
void WikipediaArtistBio::GetImageTitles(const int id, const QString &artist, CountdownLatch *latch) {
latch->Wait();
ParamList params = ParamList() << Param(QStringLiteral("titles"), artist)
<< Param(QStringLiteral("prop"), QStringLiteral("images"))
<< Param(QStringLiteral("imlimit"), QString::number(25));
QNetworkReply *reply = CreateRequest(params);
connect(reply, &QNetworkReply::finished, [this, reply, id, latch]() { GetImageTitlesFinished(reply, id, latch); });
}
void WikipediaArtistBio::GetImageTitlesFinished(QNetworkReply *reply, const int id, CountdownLatch *latch) {
reply->deleteLater();
replies_.removeAll(reply);
QJsonObject json_obj = ExtractJsonObj(GetReplyData(reply));
QString title;
QStringList titles;
if (!json_obj.isEmpty() && json_obj.contains(QLatin1String("query")) && json_obj[QLatin1String("query")].isObject()) {
json_obj = json_obj[QLatin1String("query")].toObject();
if (json_obj.contains(QLatin1String("pages")) && json_obj[QLatin1String("pages")].isObject()) {
QJsonObject value_pages = json_obj[QLatin1String("pages")].toObject();
for (const QJsonValue value_page : value_pages) {
if (!value_page.isObject()) continue;
QJsonObject obj_page = value_page.toObject();
if (!obj_page.contains(QLatin1String("title")) || !obj_page.contains(QLatin1String("images")) || !obj_page[QLatin1String("images")].isArray()) continue;
title = obj_page[QLatin1String("title")].toString();
QJsonArray array_images = obj_page[QLatin1String("images")].toArray();
for (const QJsonValue value_image : array_images) {
if (!value_image.isObject()) continue;
QJsonObject obj_image = value_image.toObject();
if (!obj_image.contains(QLatin1String("title"))) continue;
QString filename = obj_image[QLatin1String("title")].toString();
if (filename.endsWith(QLatin1String(".jpg"), Qt::CaseInsensitive) || filename.endsWith(QLatin1String(".png"), Qt::CaseInsensitive)) {
titles << filename;
}
}
}
}
}
for (const QString &image_title : titles) {
GetImage(id, image_title, latch);
}
latch->CountDown();
}
void WikipediaArtistBio::GetImage(const int id, const QString &title, CountdownLatch *latch) {
latch->Wait();
ParamList params2 = ParamList() << Param(QStringLiteral("titles"), title)
<< Param(QStringLiteral("prop"), QStringLiteral("imageinfo"))
<< Param(QStringLiteral("iiprop"), QStringLiteral("url|size"));
QNetworkReply *reply = CreateRequest(params2);
connect(reply, &QNetworkReply::finished, [this, reply, id, latch]() { GetImageFinished(reply, id, latch); });
}
void WikipediaArtistBio::GetImageFinished(QNetworkReply *reply, const int id, CountdownLatch *latch) {
reply->deleteLater();
replies_.removeAll(reply);
QJsonObject json_obj = ExtractJsonObj(GetReplyData(reply));
if (!json_obj.isEmpty()) {
QList<QUrl> urls = ExtractImageUrls(json_obj);
for (const QUrl &url : urls) {
Q_EMIT ImageReady(id, url);
}
}
latch->CountDown();
}
QList<QUrl> WikipediaArtistBio::ExtractImageUrls(QJsonObject json_obj) {
QList<QUrl> urls;
if (json_obj.contains(QLatin1String("query")) && json_obj[QLatin1String("query")].isObject()) {
json_obj = json_obj[QLatin1String("query")].toObject();
if (json_obj.contains(QLatin1String("pages")) && json_obj[QLatin1String("pages")].isObject()) {
QJsonObject value_pages = json_obj[QLatin1String("pages")].toObject();
for (const QJsonValue value_page : value_pages) {
if (!value_page.isObject()) continue;
QJsonObject obj_page = value_page.toObject();
if (!obj_page.contains(QLatin1String("title")) || !obj_page.contains(QLatin1String("imageinfo")) || !obj_page[QLatin1String("imageinfo")].isArray()) continue;
QJsonArray array_images = obj_page[QLatin1String("imageinfo")].toArray();
for (const QJsonValue value_image : array_images) {
if (!value_image.isObject()) continue;
QJsonObject obj_image = value_image.toObject();
if (!obj_image.contains(QLatin1String("url")) || !obj_image.contains(QLatin1String("width")) || !obj_image.contains(QLatin1String("height"))) continue;
QUrl url(obj_image[QLatin1String("url")].toString());
const int width = obj_image[QLatin1String("width")].toInt();
const int height = obj_image[QLatin1String("height")].toInt();
if (!url.isValid() || width < kMinimumImageSize || height < kMinimumImageSize) continue;
urls << url;
}
}
}
}
return urls;
}

View File

@@ -1,69 +0,0 @@
/*
* Strawberry Music Player
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef WIKIPEDIAARTISTBIO_H
#define WIKIPEDIAARTISTBIO_H
#include <QList>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QSslError>
#include <QJsonObject>
#include "artistbioprovider.h"
class QNetworkReply;
class CountdownLatch;
class NetworkAccessManager;
class WikipediaArtistBio : public ArtistBioProvider {
Q_OBJECT
public:
explicit WikipediaArtistBio();
~WikipediaArtistBio();
void Start(const int id, const Song &song) override;
private:
typedef QPair<QString, QString> Param;
typedef QList<Param> ParamList;
QNetworkReply *CreateRequest(QList<Param> &params);
QByteArray GetReplyData(QNetworkReply *reply);
QJsonObject ExtractJsonObj(const QByteArray &data);
void GetArticle(const int id, const QString &artist, CountdownLatch* latch);
void GetImageTitles(const int id, const QString &artist, CountdownLatch* latch);
void GetImage(const int id, const QString &title, CountdownLatch *latch);
QList<QUrl> ExtractImageUrls(QJsonObject json_obj);
private Q_SLOTS:
void HandleSSLErrors(QList<QSslError> ssl_errors);
void GetArticleReply(QNetworkReply *reply, const int id, CountdownLatch *latch);
void GetImageTitlesFinished(QNetworkReply *reply, const int id, CountdownLatch *latch);
void GetImageFinished(QNetworkReply *reply, const int id, CountdownLatch *latch);
private:
NetworkAccessManager *network_;
QList<QNetworkReply*> replies_;
};
#endif // WIKIPEDIAARTISTBIO_H

114
src/avahi/avahi.cpp Normal file
View File

@@ -0,0 +1,114 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <QByteArray>
#include <QString>
#include <QDBusConnection>
#include <QDBusPendingReply>
#include <QDBusPendingCallWatcher>
#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<QDBusObjectPath> reply = server_interface.EntryGroupNew();
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, &Avahi::PublishInternalFinished);
}
void Avahi::PublishInternalFinished(QDBusPendingCallWatcher *watcher) {
const QDBusPendingReply<QDBusObjectPath> 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<QDBusObjectPath> 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<QDBusObjectPath> 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";
}
}

58
src/avahi/avahi.h Normal file
View File

@@ -0,0 +1,58 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef AVAHI_H
#define AVAHI_H
#include <QObject>
#include <QByteArray>
#include <QString>
#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

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
<!DOCTYPE node SYSTEM "introspect.dtd">
<!--
This file is part of avahi.
avahi is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2 of the
License, or (at your option) any later version.
avahi 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 Lesser General Public
License along with avahi; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA.
-->
<node>
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg name="data" type="s" direction="out"/>
</method>
</interface>
<interface name="org.freedesktop.Avahi.EntryGroup">
<method name="Free"/>
<method name="Commit"/>
<method name="Reset"/>
<method name="GetState">
<arg name="state" type="i" direction="out"/>
</method>
<signal name="StateChanged">
<arg name="state" type="i"/>
<arg name="error" type="s"/>
</signal>
<method name="IsEmpty">
<arg name="empty" type="b" direction="out"/>
</method>
<method name="AddService">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="host" type="s" direction="in"/>
<arg name="port" type="q" direction="in"/>
</method>
<method name="AddServiceSubtype">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="subtype" type="s" direction="in"/>
</method>
<method name="UpdateServiceTxt">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
</method>
<method name="AddAddress">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="address" type="s" direction="in"/>
</method>
<method name="AddRecord">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="clazz" type="q" direction="in"/>
<arg name="type" type="q" direction="in"/>
<arg name="ttl" type="u" direction="in"/>
<arg name="rdata" type="ay" direction="in"/>
</method>
</interface>
</node>

View File

@@ -0,0 +1,396 @@
<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
<!DOCTYPE node SYSTEM "introspect.dtd">
<!--
This file is part of avahi.
avahi is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2 of the
License, or (at your option) any later version.
avahi 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 Lesser General Public
License along with avahi; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA.
-->
<node>
<interface name="org.freedesktop.Avahi.Server">
<method name="GetVersionString">
<arg name="version" type="s" direction="out"/>
</method>
<method name="GetAPIVersion">
<arg name="version" type="u" direction="out"/>
</method>
<method name="GetHostName">
<arg name="name" type="s" direction="out"/>
</method>
<method name="SetHostName">
<arg name="name" type="s" direction="in"/>
</method>
<method name="GetHostNameFqdn">
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetDomainName">
<arg name="name" type="s" direction="out"/>
</method>
<method name="IsNSSSupportAvailable">
<arg name="yes" type="b" direction="out"/>
</method>
<method name="GetState">
<arg name="state" type="i" direction="out"/>
</method>
<signal name="StateChanged">
<arg name="state" type="i"/>
<arg name="error" type="s"/>
</signal>
<method name="GetLocalServiceCookie">
<arg name="cookie" type="u" direction="out"/>
</method>
<method name="GetAlternativeHostName">
<arg name="name" type="s" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetAlternativeServiceName">
<arg name="name" type="s" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetNetworkInterfaceNameByIndex">
<arg name="index" type="i" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetNetworkInterfaceIndexByName">
<arg name="name" type="s" direction="in"/>
<arg name="index" type="i" direction="out"/>
</method>
<method name="ResolveHostName">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="ResolveAddress">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="address" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="ResolveService">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="type" type="s" direction="out"/>
<arg name="domain" type="s" direction="out"/>
<arg name="host" type="s" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="port" type="q" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="EntryGroupNew">
<arg name="path" type="o" direction="out"/>
</method>
<method name="DomainBrowserNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="btype" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceTypeBrowserNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceBrowserNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceResolverNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="HostNameResolverNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="AddressResolverNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="address" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="RecordBrowserNew">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="clazz" type="q" direction="in"/>
<arg name="type" type="q" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
</interface>
<interface name="org.freedesktop.Avahi.Server2">
<method name="GetVersionString">
<arg name="version" type="s" direction="out"/>
</method>
<method name="GetAPIVersion">
<arg name="version" type="u" direction="out"/>
</method>
<method name="GetHostName">
<arg name="name" type="s" direction="out"/>
</method>
<method name="SetHostName">
<arg name="name" type="s" direction="in"/>
</method>
<method name="GetHostNameFqdn">
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetDomainName">
<arg name="name" type="s" direction="out"/>
</method>
<method name="IsNSSSupportAvailable">
<arg name="yes" type="b" direction="out"/>
</method>
<method name="GetState">
<arg name="state" type="i" direction="out"/>
</method>
<signal name="StateChanged">
<arg name="state" type="i"/>
<arg name="error" type="s"/>
</signal>
<method name="GetLocalServiceCookie">
<arg name="cookie" type="u" direction="out"/>
</method>
<method name="GetAlternativeHostName">
<arg name="name" type="s" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetAlternativeServiceName">
<arg name="name" type="s" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetNetworkInterfaceNameByIndex">
<arg name="index" type="i" direction="in"/>
<arg name="name" type="s" direction="out"/>
</method>
<method name="GetNetworkInterfaceIndexByName">
<arg name="name" type="s" direction="in"/>
<arg name="index" type="i" direction="out"/>
</method>
<method name="ResolveHostName">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="ResolveAddress">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="address" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="ResolveService">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="interface" type="i" direction="out"/>
<arg name="protocol" type="i" direction="out"/>
<arg name="name" type="s" direction="out"/>
<arg name="type" type="s" direction="out"/>
<arg name="domain" type="s" direction="out"/>
<arg name="host" type="s" direction="out"/>
<arg name="aprotocol" type="i" direction="out"/>
<arg name="address" type="s" direction="out"/>
<arg name="port" type="q" direction="out"/>
<arg name="flags" type="u" direction="out"/>
</method>
<method name="EntryGroupNew">
<arg name="path" type="o" direction="out"/>
</method>
<method name="DomainBrowserPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="btype" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceTypeBrowserPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceBrowserPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="ServiceResolverPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="type" type="s" direction="in"/>
<arg name="domain" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="HostNameResolverPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="aprotocol" type="i" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="AddressResolverPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="address" type="s" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
<method name="RecordBrowserPrepare">
<arg name="interface" type="i" direction="in"/>
<arg name="protocol" type="i" direction="in"/>
<arg name="name" type="s" direction="in"/>
<arg name="clazz" type="q" direction="in"/>
<arg name="type" type="q" direction="in"/>
<arg name="flags" type="u" direction="in"/>
<arg name="path" type="o" direction="out"/>
</method>
</interface>
</node>

View File

@@ -33,6 +33,7 @@
#cmakedefine HAVE_SPOTIFY
#cmakedefine HAVE_QOBUZ
#cmakedefine HAVE_DISCORD_RPC
#cmakedefine HAVE_NETWORKREMOTE
#cmakedefine HAVE_TAGLIB_DSFFILE
#cmakedefine HAVE_TAGLIB_DSDIFFFILE

View File

@@ -0,0 +1,36 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef NETWORKREMOTECONSTANTS_H
#define NETWORKREMOTECONSTANTS_H
#include <QStringList>
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

View File

@@ -0,0 +1,35 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#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

View File

@@ -110,6 +110,10 @@
# include "moodbar/moodbarloader.h"
#endif
#ifdef HAVE_NETWORKREMOTE
# include "networkremote/networkremote.h"
#endif
#include "radios/radioservices.h"
#include "radios/radiobackend.h"
@@ -216,6 +220,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()); })
{}
@@ -241,6 +252,9 @@ class ApplicationImpl {
#ifdef HAVE_MOODBAR
Lazy<MoodbarLoader> moodbar_loader_;
Lazy<MoodbarController> moodbar_controller_;
#endif
#ifdef HAVE_NETWORKREMOTE
Lazy<NetworkRemote> network_remote_;
#endif
Lazy<LastFMImport> lastfm_import_;
@@ -390,3 +404,6 @@ SharedPtr<LastFMImport> Application::lastfm_import() const { return p_->lastfm_i
SharedPtr<MoodbarController> Application::moodbar_controller() const { return p_->moodbar_controller_.ptr(); }
SharedPtr<MoodbarLoader> Application::moodbar_loader() const { return p_->moodbar_loader_.ptr(); }
#endif
#ifdef HAVE_NETWORKREMOTE
SharedPtr<NetworkRemote> Application::network_remote() const { return p_->network_remote_.ptr(); }
#endif

View File

@@ -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<MoodbarLoader> moodbar_loader() const;
#endif
#ifdef HAVE_NETWORKREMOTE
SharedPtr<NetworkRemote> network_remote() const;
#endif
SharedPtr<LastFMImport> lastfm_import() const;
void Exit();

24
src/core/bonjour.h Normal file
View File

@@ -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

57
src/core/bonjour.mm Normal file
View File

@@ -0,0 +1,57 @@
#include "bonjour.h"
#import <Foundation/NSNetServices.h>
#import <Foundation/NSString.h>
#include "core/logging.h"
#include "core/scoped_nsautorelease_pool.h"
@interface NetServicePublicationDelegate : NSObject <NSNetServiceDelegate> {}
- (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];
}
}

View File

@@ -1,39 +0,0 @@
/* This file is part of Clementine.
Copyright 2016, John Maguire <john.maguire@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <QMutexLocker>
#include "latch.h"
CountdownLatch::CountdownLatch() : count_(0) {}
void CountdownLatch::Wait() {
QMutexLocker l(&mutex_);
++count_;
}
void CountdownLatch::CountDown() {
QMutexLocker l(&mutex_);
Q_ASSERT(count_ > 0);
--count_;
if (count_ == 0) {
emit Done();
}
}

View File

@@ -1,39 +0,0 @@
/* This file is part of Clementine.
Copyright 2016, John Maguire <john.maguire@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#ifndef LATCH_H
#define LATCH_H
#include <QObject>
#include <QMutex>
class CountdownLatch : public QObject {
Q_OBJECT
public:
explicit CountdownLatch();
void Wait();
void CountDown();
Q_SIGNALS:
void Done();
private:
QMutex mutex_;
int count_;
};
#endif // LATCH_H

View File

@@ -205,7 +205,6 @@
#include "smartplaylists/smartplaylistsviewcontainer.h"
#include "organize/organizeerrordialog.h"
#include "artistbio/artistbioview.h"
#ifdef Q_OS_WIN32
# include "core/windows7thumbbar.h"
@@ -233,7 +232,6 @@
using std::make_unique;
using std::make_shared;
using namespace std::chrono_literals;
using namespace Qt::Literals::StringLiterals;
@@ -359,7 +357,6 @@ MainWindow::MainWindow(Application *app,
qobuz_view_(new StreamingTabsView(app->streaming_services()->ServiceBySource(Song::Source::Qobuz), app->albumcover_loader(), QLatin1String(QobuzSettings::kSettingsGroup), this)),
#endif
radio_view_(new RadioViewContainer(this)),
artistbio_view_(new ArtistBioView(this)),
lastfm_import_dialog_(new LastFMImportDialog(app_->lastfm_import(), this)),
collection_show_all_(nullptr),
collection_show_duplicates_(nullptr),
@@ -444,7 +441,6 @@ MainWindow::MainWindow(Application *app,
#ifdef HAVE_QOBUZ
ui_->tabs->AddTab(qobuz_view_, u"qobuz"_s, IconLoader::Load(u"qobuz"_s, true, 0, 32), tr("Qobuz"));
#endif
ui_->tabs->AddTab(artistbio_view_, QStringLiteral("artistbio"), IconLoader::Load(QStringLiteral("guitar")), tr("Artist biography"));
// Add the playing widget to the fancy tab widget
ui_->tabs->AddBottomWidget(ui_->widget_playing);
@@ -981,9 +977,9 @@ MainWindow::MainWindow(Application *app,
ui_->action_open_cd->setVisible(false);
#endif
connect(&*app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, artistbio_view_, &ArtistBioView::SongChanged);
connect(&*app_->player(), &Player::PlaylistFinished, artistbio_view_, &ArtistBioView::SongFinished);
connect(&*app_->player(), &Player::Stopped, artistbio_view_, &ArtistBioView::SongFinished);
#ifdef HAVE_NETWORKREMOTE
app_->network_remote();
#endif
// Load settings
qLog(Debug) << "Loading settings";
@@ -1240,16 +1236,6 @@ void MainWindow::ReloadSettings() {
album_cover_choice_controller_->search_cover_auto_action()->setChecked(s.value(MainWindowSettings::kSearchForCoverAuto, true).toBool());
s.endGroup();
s.beginGroup(BehaviourSettings::kSettingsGroup);
bool artistbio = s.value("artistbio", false).toBool();
s.endGroup();
if (artistbio) {
ui_->tabs->EnableTab(artistbio_view_);
}
else {
ui_->tabs->DisableTab(artistbio_view_);
}
#ifdef HAVE_SUBSONIC
s.beginGroup(SubsonicSettings::kSettingsGroup);
bool enable_subsonic = s.value(SubsonicSettings::kEnabled, false).toBool();

View File

@@ -97,7 +97,6 @@ class Windows7ThumbBar;
class AddStreamDialog;
class LastFMImportDialog;
class RadioViewContainer;
class ArtistBioView;
#ifdef HAVE_DISCORD_RPC
namespace discord {
@@ -359,8 +358,6 @@ class MainWindow : public QMainWindow, public PlatformInterface {
RadioViewContainer *radio_view_;
ArtistBioView *artistbio_view_;
LastFMImportDialog *lastfm_import_dialog_;
QAction *collection_show_all_;

View File

@@ -68,7 +68,6 @@
#include "smartplaylists/playlistgenerator_fwd.h"
#include "radios/radiochannel.h"
#include "widgets/collapsibleinfopane.h"
#ifdef HAVE_MTP
# include "device/mtpconnection.h"
@@ -148,8 +147,6 @@ void RegisterMetaTypes() {
qRegisterMetaType<RadioChannel>("RadioChannel");
qRegisterMetaType<RadioChannelList>("RadioChannelList");
qRegisterMetaType<CollapsibleInfoPane::Data>("CollapsibleInfoPane::Data");
#ifdef HAVE_MTP
qRegisterMetaType<MtpConnection*>("MtpConnection*");
#endif

View File

@@ -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);

View File

@@ -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;

86
src/core/tinysvcmdns.cpp Normal file
View File

@@ -0,0 +1,86 @@
extern "C" {
#include "mdnsd.h"
}
#include <QObject>
#include <QList>
#include <QString>
#include <QHostInfo>
#include <QNetworkInterface>
#include <QtEndian>
#include "tinysvcmdns.h"
#include "core/logging.h"
using namespace Qt::Literals::StringLiterals;
TinySVCMDNS::TinySVCMDNS(QObject *parent) : Zeroconf(parent) {
// Get all network interfaces
const QList<QNetworkInterface> 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)) {
qLog(Debug) << "Interface" << network_interface.humanReadableName();
uint32_t ipv4 = 0;
QString ipv6;
// Now check all network addresses for this device
QList<QNetworkAddressEntry> 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::CreateMdnsd(const uint32_t ipv4, const QString &ipv6) {
const QString host = QHostInfo::localHostName();
// Start the service
mdnsd *mdnsd = mdnsd_start();
// Set our hostname
const QString fullhostname = host + ".local"_L1;
mdnsd_set_hostname(mdnsd, fullhostname.toUtf8().constData(), ipv4);
// Add to the list
mdnsd_.append(mdnsd);
}
void TinySVCMDNS::PublishInternal(const QString &domain, const QString &type, const QByteArray &name, const quint16 port) {
// Some pointless text, so tinymDNS publishes the service correctly.
const char *txt[] = { "cat=nyan", nullptr };
for (mdnsd *mdnsd : mdnsd_) {
const QString fulltype = type + ".local"_L1;
mdnsd_register_svc(mdnsd, name.constData(), fulltype.toUtf8().constData(), port, nullptr, txt);
}
}

26
src/core/tinysvcmdns.h Normal file
View File

@@ -0,0 +1,26 @@
#ifndef TINYSVCMDNS_H
#define TINYSVCMDNS_H
#include <QList>
#include <QByteArray>
#include <QString>
#include "zeroconf.h"
struct mdnsd;
class TinySVCMDNS : public Zeroconf {
public:
explicit TinySVCMDNS(QObject *parent = nullptr);
virtual ~TinySVCMDNS();
protected:
virtual void PublishInternal(const QString &domain, const QString &type, const QByteArray &name, const quint16 port) override;
private:
void CreateMdnsd(const uint32_t ipv4, const QString &ipv6);
QList<mdnsd*> mdnsd_;
};
#endif // TINYSVCMDNS_H

69
src/core/zeroconf.cpp Normal file
View File

@@ -0,0 +1,69 @@
#include "config.h"
#include <QObject>
#include <QByteArray>
#include <QString>
#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);
}

28
src/core/zeroconf.h Normal file
View File

@@ -0,0 +1,28 @@
#ifndef ZEROCONF_H
#define ZEROCONF_H
#include <QObject>
#include <QByteArray>
#include <QString>
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

View File

@@ -0,0 +1,478 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <algorithm>
#include <QString>
#include <QUrl>
#include <QDir>
#include <QSettings>
#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> player,
const SharedPtr<PlaylistManager> playlist_manager,
const SharedPtr<AudioScrobbler> 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<BehaviourSettings::PlaylistAddBehaviour>(s.value(BehaviourSettings::kDoubleClickPlaylistAddMode, static_cast<int>(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<Song::FileType>(pb_song_metadata.filetype()));
return song;
}
void IncomingDataParser::Parse(const networkremote::Message &msg) {
close_connection_ = false;
NetworkRemoteClient *client = qobject_cast<NetworkRemoteClient*>(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<QUrl> 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<int> 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<QUrl> 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);
}
}

View File

@@ -0,0 +1,123 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef INCOMINGDATAPARSER_H
#define INCOMINGDATAPARSER_H
#include <QObject>
#include <QString>
#include <QStringList>
#include "constants/behavioursettings.h"
#include "core/player.h"
#include "networkremoteclient.h"
#include "networkremotemessages.qpb.h"
#include "playlist/playlistsequence.h"
class PlaylistManager;
class AudioScrobbler;
class IncomingDataParser : public QObject {
Q_OBJECT
public:
explicit IncomingDataParser(const SharedPtr<Player> player,
const SharedPtr<PlaylistManager> playlist_manager,
const SharedPtr<AudioScrobbler> 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<QUrl> &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<int> &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> player_;
const SharedPtr<PlaylistManager> playlist_manager_;
const SharedPtr<AudioScrobbler> 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

View File

@@ -0,0 +1,231 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <memory>
#include <QDataStream>
#include <QHostInfo>
#include <QNetworkProxy>
#include <QTcpSocket>
#include <QTcpServer>
#include <QSettings>
#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> database,
const SharedPtr<Player> player,
const SharedPtr<CollectionBackend> collection_backend,
const SharedPtr<PlaylistManager> playlist_manager,
const SharedPtr<PlaylistBackend> playlist_backend,
const SharedPtr<CurrentAlbumCoverLoader> current_albumcover_loader,
const SharedPtr<AudioScrobbler> 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<QTcpServer>();
server_ipv6_ = make_unique<QTcpServer>();
incoming_data_parser_ = make_unique<IncomingDataParser>(player_, playlist_manager_, scrobbler_);
outgoing_data_creator_ = make_unique<OutgoingDataCreator>(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<QTcpServer*>(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();
}
}

View File

@@ -0,0 +1,98 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef NETWORKREMOTE_H
#define NETWORKREMOTE_H
#include <QObject>
#include <QList>
#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> database,
const SharedPtr<Player> player,
const SharedPtr<CollectionBackend> collection_backend,
const SharedPtr<PlaylistManager> playlist_manager,
const SharedPtr<PlaylistBackend> playlist_backend,
const SharedPtr<CurrentAlbumCoverLoader> current_albumcover_loader,
const SharedPtr<AudioScrobbler> 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> database_;
const SharedPtr<Player> player_;
const SharedPtr<CollectionBackend> collection_backend_;
const SharedPtr<PlaylistManager> playlist_manager_;
const SharedPtr<PlaylistBackend> playlist_backend_;
const SharedPtr<CurrentAlbumCoverLoader> current_albumcover_loader_;
const SharedPtr<AudioScrobbler> scrobbler_;
ScopedPtr<QTcpServer> server_;
ScopedPtr<QTcpServer> server_ipv6_;
ScopedPtr<IncomingDataParser> incoming_data_parser_;
ScopedPtr<OutgoingDataCreator> outgoing_data_creator_;
bool enabled_;
quint16 port_;
bool allow_public_access_;
bool signals_connected_;
QList<NetworkRemoteClient*> clients_;
void StopServer();
void CreateRemoteClient(QTcpSocket *client_socket);
bool IpIsPrivate(const QHostAddress &address);
};
#endif // NETWORKREMOTE_H

View File

@@ -0,0 +1,223 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2013, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <QDataStream>
#include <QTcpSocket>
#include <QProtobufSerializer>
#include <QSettings>
#include "constants/networkremotesettingsconstants.h"
#include "core/logging.h"
#include "networkremote.h"
#include "networkremoteclient.h"
#include "networkremotemessages.qpb.h"
using namespace Qt::Literals::StringLiterals;
using namespace NetworkRemoteSettingsConstants;
NetworkRemoteClient::NetworkRemoteClient(const SharedPtr<Player> player,
const SharedPtr<CollectionBackend> collection_backend,
const SharedPtr<PlaylistManager> 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<qint32>(expected_length_) - buffer_.size()));
// Did we get everything?
if (buffer_.size() == static_cast<qint32>(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<qint32>(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(); }

View File

@@ -0,0 +1,89 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2013, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef NETWORKREMOTECLIENT_H
#define NETWORKREMOTECLIENT_H
#include <QObject>
#include <QByteArray>
#include <QString>
#include <QStringList>
#include <QAbstractSocket>
#include "networkremotemessages.qpb.h"
#include "songsender.h"
class QTcpSocket;
class NetworkRemoteClient : public QObject {
Q_OBJECT
public:
explicit NetworkRemoteClient(const SharedPtr<Player> player,
const SharedPtr<CollectionBackend> collection_backend,
const SharedPtr<PlaylistManager> 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> 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

View File

@@ -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;
}

View File

@@ -0,0 +1,638 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <cmath>
#include <QCoreApplication>
#include <QDir>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QBuffer>
#include <QTimer>
#include <QStandardPaths>
#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> database,
const SharedPtr<Player> player,
const SharedPtr<PlaylistManager> playlist_manager,
const SharedPtr<PlaylistBackend> 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<NetworkRemoteClient*> *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<PlaylistBackend::Playlist> 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<Playlist*> playlists = playlist_manager_->GetAllPlaylists();
QList<networkremote::Playlist> 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<networkremote::SongMetadata::FileType>(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<networkremote::SongMetadata> 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<int>(std::floor(static_cast<double>(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<QFileInfo> 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);
}

View File

@@ -0,0 +1,120 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef OUTGOINGDATACREATOR_H
#define OUTGOINGDATACREATOR_H
#include <QObject>
#include <QList>
#include <QMap>
#include <QQueue>
#include <QString>
#include <QStringList>
#include <QImage>
#include <QTcpSocket>
#include <QTimer>
#include "includes/shared_ptr.h"
#include "engine/enginebase.h"
#include "playlist/playlistsequence.h"
#include "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> database,
const SharedPtr<Player> player,
const SharedPtr<PlaylistManager> playlist_manager,
const SharedPtr<PlaylistBackend> playlist_backend,
QObject *parent = nullptr);
~OutgoingDataCreator();
void SetClients(QList<NetworkRemoteClient*> *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> database_;
const SharedPtr<Player> player_;
const SharedPtr<PlaylistManager> playlist_manager_;
const SharedPtr<PlaylistBackend> playlist_backend_;
QList<NetworkRemoteClient*> *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

View File

@@ -0,0 +1,442 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "songsender.h"
#include <QImage>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QSettings>
#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> player,
const SharedPtr<CollectionBackend> collection_backend,
const SharedPtr<PlaylistManager> 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<TranscoderPreset> 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<int>(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<quint32>(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<int>(songs.indexOf(song)) + 1, static_cast<int>(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<int> 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<int>(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<int>(songs.indexOf(song)) + 1, static_cast<int>(songs.count()));
download_queue_.append(item);
}
}

View File

@@ -0,0 +1,96 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, Andreas Muttscheller <asfa194@gmail.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef SONGSENDER_H
#define SONGSENDER_H
#include <QObject>
#include <QMap>
#include <QQueue>
#include <QString>
#include <QUrl>
#include "includes/shared_ptr.h"
#include "core/song.h"
#include "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> player,
const SharedPtr<CollectionBackend> collection_backend,
const SharedPtr<PlaylistManager> 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> player_;
const SharedPtr<CollectionBackend> collection_backend_;
const SharedPtr<PlaylistManager> playlist_manager_;
NetworkRemoteClient *client_;
TranscoderPreset transcoder_preset_;
Transcoder *transcoder_;
bool transcode_lossless_files_;
QQueue<DownloadItem> download_queue_;
QMap<QString, QString> 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

View File

@@ -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;
@@ -185,9 +186,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);
@@ -203,6 +204,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) {
@@ -614,3 +617,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);
}

View File

@@ -95,7 +95,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 &playlist_name, const QString &filename, const PlaylistSettings::PathType path_type) override;
// Display a file dialog to let user choose a file before saving the file
@@ -144,6 +144,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();

View File

@@ -77,7 +77,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 &playlist_name, const QString &filename, const PlaylistSettings::PathType path_type) = 0;
virtual void Rename(const int id, const QString &new_name) = 0;

View File

@@ -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<int> &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);

View File

@@ -165,7 +165,6 @@ void BehaviourSettingsPage::Load() {
ui_->checkbox_resumeplayback->setChecked(s.value(kResumePlayback, false).toBool());
ui_->checkbox_playingwidget->setChecked(s.value(kPlayingWidget, true).toBool());
ui_->checkbox_artistbio->setChecked(s.value("artistbio", false).toBool());
#ifndef Q_OS_MACOS
const StartupBehaviour startup_behaviour = static_cast<StartupBehaviour>(s.value(kStartupBehaviour, static_cast<int>(StartupBehaviour::Remember)).toInt());
@@ -233,12 +232,9 @@ void BehaviourSettingsPage::Save() {
#if defined(HAVE_DBUS) && !defined(Q_OS_MACOS)
s.setValue(kTaskbarProgress, ui_->checkbox_taskbar_progress->isChecked());
#endif
s.setValue(kResumePlayback, ui_->checkbox_resumeplayback->isChecked());
s.setValue(kPlayingWidget, ui_->checkbox_playingwidget->isChecked());
s.setValue("artistbio", ui_->checkbox_artistbio->isChecked());
StartupBehaviour startup_behaviour = StartupBehaviour::Remember;
if (ui_->radiobutton_remember->isChecked()) startup_behaviour = StartupBehaviour::Remember;
if (ui_->radiobutton_show->isChecked()) startup_behaviour = StartupBehaviour::Show;

View File

@@ -65,13 +65,6 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkbox_artistbio">
<property name="text">
<string>Artist biography</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupbox_startup">
<property name="title">

View File

@@ -0,0 +1,157 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "networkremotesettingspage.h"
#include <algorithm>
#include <QString>
#include <QUrl>
#include <QFile>
#include <QHostInfo>
#include <QNetworkInterface>
#include <QDesktopServices>
#include <QSettings>
#include <QRandomGenerator>
#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<TranscoderPreset> 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<TranscoderPreset>().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<QHostAddress> 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<TranscoderPreset>();
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<TranscoderPreset>();
TranscoderOptionsDialog dialog(preset.filetype_, this);
dialog.set_settings_postfix(QLatin1String(kTranscoderSettingPostfix));
dialog.exec();
}

View File

@@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,8 +19,28 @@
*
*/
#include "artistbioprovider.h"
#ifndef NETWORKREMOTESETTINGSPAGE_H
#define NETWORKREMOTESETTINGSPAGE_H
ArtistBioProvider::ArtistBioProvider() : enabled_(true) {}
#include "settingspage.h"
QString ArtistBioProvider::name() const { return QString::fromLatin1(metaObject()->className()); }
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

View File

@@ -0,0 +1,298 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NetworkRemoteSettingsPage</class>
<widget class="QWidget" name="NetworkRemoteSettingsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>421</width>
<height>664</height>
</rect>
</property>
<property name="windowTitle">
<string>Network Remote</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="enabled">
<property name="text">
<string>Enabled</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupbox_use_remote_container">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>Settings</string>
</property>
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_port">
<property name="minimumSize">
<size>
<width>171</width>
<height>0</height>
</size>
</property>
<property name="layoutDirection">
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string>Port</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="spinbox_port">
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>8080</number>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="checkbox_allow_public_access">
<property name="toolTip">
<string>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</string>
</property>
<property name="text">
<string>Allow public access</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="checkbox_use_auth_code">
<property name="toolTip">
<string>A client can connect only, if the correct code was entered.</string>
</property>
<property name="text">
<string>Require authentication code</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="spinbox_auth_code">
<property name="enabled">
<bool>false</bool>
</property>
<property name="suffix">
<string/>
</property>
<property name="maximum">
<number>99999</number>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_ip_address_description">
<property name="toolTip">
<string>Enter this IP in the App to connect to Clementine.</string>
</property>
<property name="text">
<string>Your IP address:</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QLabel" name="label_ip_address">
<property name="text">
<string notr="true">127.0.0.1</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="allow_downloads">
<property name="toolTip">
<string>Allow a client to download music from this computer.</string>
</property>
<property name="text">
<string>Allow downloads</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QGroupBox" name="download_settings_container">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>Download settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QCheckBox" name="convert_lossless">
<property name="toolTip">
<string>Convert lossless audiofiles before sending them to the remote.</string>
</property>
<property name="text">
<string>Convert lossless files</string>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="format_container">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>Audio format</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<widget class="QPushButton" name="options">
<property name="text">
<string>Options...</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QComboBox" name="format"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_files_root_folder">
<property name="toolTip">
<string>Root folder that will be browsable from the network remote</string>
</property>
<property name="text">
<string>Files root folder</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="FileChooserWidget" name="files_root_folder" native="true">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="files_music_extensions"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_music_extensions">
<property name="toolTip">
<string>comma-separated list of the allowed extensions that will be visible from the network remote (ex: m3u,mp3,flac,ogg,wav)</string>
</property>
<property name="text">
<string>Music extensions remotely visible</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="spacer_bottom">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>98</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>FileChooserWidget</class>
<extends>QWidget</extends>
<header location="global">widgets/filechooserwidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>checkbox_use_auth_code</sender>
<signal>toggled(bool)</signal>
<receiver>spinbox_auth_code</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>137</x>
<y>124</y>
</hint>
<hint type="destinationlabel">
<x>351</x>
<y>125</y>
</hint>
</hints>
</connection>
<connection>
<sender>enabled</sender>
<signal>toggled(bool)</signal>
<receiver>groupbox_use_remote_container</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>59</x>
<y>22</y>
</hint>
<hint type="destinationlabel">
<x>57</x>
<y>43</y>
</hint>
</hints>
</connection>
<connection>
<sender>allow_downloads</sender>
<signal>toggled(bool)</signal>
<receiver>download_settings_container</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>196</x>
<y>160</y>
</hint>
<hint type="destinationlabel">
<x>117</x>
<y>205</y>
</hint>
</hints>
</connection>
<connection>
<sender>convert_lossless</sender>
<signal>toggled(bool)</signal>
<receiver>format_container</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>218</x>
<y>212</y>
</hint>
<hint type="destinationlabel">
<x>218</x>
<y>262</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -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> player,
AddPage(Page::Qobuz, new QobuzSettingsPage(this, streaming_services->Service<QobuzService>(), 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_);

View File

@@ -93,6 +93,7 @@ class SettingsDialog : public QDialog {
Tidal,
Qobuz,
Spotify,
NetworkRemote
};
enum Role {

View File

@@ -20,6 +20,8 @@
#include <QByteArray>
#include <QString>
#include <QCryptographicHash>
#include <QFile>
#include <QIODevice>
#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

View File

@@ -23,6 +23,7 @@
#include <QByteArray>
#include <QString>
#include <QCryptographicHash>
#include <QFile>
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

View File

@@ -1,174 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <QWidget>
#include <QApplication>
#include <QString>
#include <QIcon>
#include <QPainter>
#include <QPalette>
#include <QColor>
#include <QStyle>
#include <QFont>
#include <QPropertyAnimation>
#include <QStyleOption>
#include <QEnterEvent>
#include <QPaintEvent>
#include <QMouseEvent>
#include <QEvent>
#include "collapsibleinfoheader.h"
const int CollapsibleInfoHeader::kHeight = 20;
const int CollapsibleInfoHeader::kIconSize = 16;
CollapsibleInfoHeader::CollapsibleInfoHeader(QWidget* parent)
: QWidget(parent),
expanded_(false),
hovering_(false),
animation_(new QPropertyAnimation(this, "opacity", this)),
opacity_(0.0) {
setMinimumHeight(kHeight);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
setCursor(QCursor(Qt::PointingHandCursor));
}
void CollapsibleInfoHeader::SetTitle(const QString &title) {
title_ = title;
update();
}
void CollapsibleInfoHeader::SetIcon(const QIcon &icon) {
icon_ = icon;
update();
}
void CollapsibleInfoHeader::SetExpanded(const bool expanded) {
expanded_ = expanded;
emit ExpandedToggled(expanded);
if (expanded)
emit Expanded();
else
emit Collapsed();
}
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void CollapsibleInfoHeader::enterEvent(QEnterEvent*) {
#else
void CollapsibleInfoHeader::enterEvent(QEvent*) {
#endif
hovering_ = true;
if (!expanded_) {
animation_->stop();
animation_->setEndValue(1.0);
animation_->setDuration(80);
animation_->start();
}
}
void CollapsibleInfoHeader::leaveEvent(QEvent*) {
hovering_ = false;
if (!expanded_) {
animation_->stop();
animation_->setEndValue(0.0);
animation_->setDuration(160);
animation_->start();
}
}
void CollapsibleInfoHeader::set_opacity(const float opacity) {
opacity_ = opacity;
update();
}
void CollapsibleInfoHeader::paintEvent(QPaintEvent*) {
QPainter p(this);
QColor active_text_color(palette().color(QPalette::Active, QPalette::HighlightedText));
QColor inactive_text_color(palette().color(QPalette::Active, QPalette::Text));
QColor text_color;
if (expanded_) {
text_color = active_text_color;
}
else {
p.setOpacity(0.4 + opacity_ * 0.6);
text_color = QColor(active_text_color.red() * opacity_ + inactive_text_color.red() * (1.0 - opacity_), active_text_color.green() * opacity_ + inactive_text_color.green() * (1.0 - opacity_), active_text_color.blue() * opacity_ + inactive_text_color.blue() * (1.0 - opacity_));
}
QRect indicator_rect(0, 0, height(), height());
QRect icon_rect(height() + 2, (kHeight - kIconSize) / 2, kIconSize, kIconSize);
QRect text_rect(rect());
text_rect.setLeft(icon_rect.right() + 4);
// Draw the background
QColor highlight(palette().color(QPalette::Active, QPalette::Highlight));
const QColor bg_color_1(highlight.lighter(120));
const QColor bg_color_2(highlight.darker(120));
const QColor bg_border(palette().color(QPalette::Dark));
QLinearGradient bg_brush(rect().topLeft(), rect().bottomLeft());
bg_brush.setColorAt(0.0, bg_color_1);
bg_brush.setColorAt(0.5, bg_color_1);
bg_brush.setColorAt(0.5, bg_color_2);
bg_brush.setColorAt(1.0, bg_color_2);
p.setPen(Qt::NoPen);
p.fillRect(rect(), bg_brush);
p.setPen(bg_border);
p.drawLine(rect().topLeft(), rect().topRight());
p.drawLine(rect().bottomLeft(), rect().bottomRight());
// Draw the expand/collapse indicator
QStyleOption opt;
opt.initFrom(this);
opt.rect = indicator_rect;
opt.state |= QStyle::State_Children;
if (expanded_) opt.state |= QStyle::State_Open;
if (hovering_) opt.state |= QStyle::State_Active;
// Have to use the application's style here because using the widget's style
// will trigger QStyleSheetStyle's recursion guard (I don't know why).
QApplication::style()->drawPrimitive(QStyle::PE_IndicatorBranch, &opt, &p, this);
// Draw the icon
p.drawPixmap(icon_rect, icon_.pixmap(kIconSize));
// Draw the title text
QFont bold_font(font());
bold_font.setBold(true);
p.setFont(bold_font);
p.setPen(text_color);
p.drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, title_);
}
void CollapsibleInfoHeader::mouseReleaseEvent(QMouseEvent*) {
SetExpanded(!expanded_);
}

View File

@@ -1,82 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef COLLAPSIBLEINFOHEADER_H
#define COLLAPSIBLEINFOHEADER_H
#include <QWidget>
#include <QString>
#include <QIcon>
class QPropertyAnimation;
class QEnterEvent;
class QEvent;
class QPaintEvent;
class QMouseEvent;
class CollapsibleInfoHeader : public QWidget {
Q_OBJECT
Q_PROPERTY(float opacity READ opacity WRITE set_opacity)
public:
CollapsibleInfoHeader(QWidget *parent = nullptr);
static const int kHeight;
static const int kIconSize;
bool expanded() const { return expanded_; }
bool hovering() const { return hovering_; }
const QString &title() const { return title_; }
const QIcon &icon() const { return icon_; }
float opacity() const { return opacity_; }
void set_opacity(const float opacity);
public Q_SLOTS:
void SetExpanded(const bool expanded);
void SetTitle(const QString &title);
void SetIcon(const QIcon &icon);
Q_SIGNALS:
void Expanded();
void Collapsed();
void ExpandedToggled(const bool expanded);
protected:
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
void enterEvent(QEnterEvent*) override;
#else
void enterEvent(QEvent*) override;
#endif
void leaveEvent(QEvent*) override;
void paintEvent(QPaintEvent*) override;
void mouseReleaseEvent(QMouseEvent*) override;
private:
bool expanded_;
bool hovering_;
QString title_;
QIcon icon_;
QPropertyAnimation *animation_;
float opacity_;
};
#endif // COLLAPSIBLEINFOHEADER_H

View File

@@ -1,64 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "collapsibleinfoheader.h"
#include "collapsibleinfopane.h"
#include <QVBoxLayout>
CollapsibleInfoPane::CollapsibleInfoPane(const Data &data, QWidget *parent)
: QWidget(parent), data_(data), header_(new CollapsibleInfoHeader(this)) {
QVBoxLayout* layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(3);
layout->setSizeConstraint(QLayout::SetMinAndMaxSize);
setLayout(layout);
layout->addWidget(header_);
layout->addWidget(data.contents_);
data.contents_->hide();
header_->SetTitle(data.title_);
header_->SetIcon(data.icon_);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
connect(header_, SIGNAL(ExpandedToggled(bool)), SLOT(ExpandedToggled(bool)));
connect(header_, SIGNAL(ExpandedToggled(bool)), SIGNAL(Toggled(bool)));
}
void CollapsibleInfoPane::Collapse() { header_->SetExpanded(false); }
void CollapsibleInfoPane::Expand() { header_->SetExpanded(true); }
void CollapsibleInfoPane::ExpandedToggled(bool expanded) {
data_.contents_->setVisible(expanded);
}
bool CollapsibleInfoPane::Data::operator<(const CollapsibleInfoPane::Data &other) const {
const int my_score = (TypeCount - type_) * 1000 + relevance_;
const int other_score = (TypeCount - other.type_) * 1000 + other.relevance_;
return my_score > other_score;
}

View File

@@ -1,72 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef COLLAPSIBLEINFOPANE_H
#define COLLAPSIBLEINFOPANE_H
#include <QIcon>
#include <QWidget>
class CollapsibleInfoHeader;
class CollapsibleInfoPane : public QWidget {
Q_OBJECT
public:
struct Data {
explicit Data() : type_(Type_Biography), relevance_(0) {}
bool operator<(const Data& other) const;
enum Type {
Type_Biography,
TypeCount
};
QString id_;
QString title_;
QIcon icon_;
Type type_;
int relevance_;
QWidget *contents_;
QObject *content_object_;
};
CollapsibleInfoPane(const Data &data, QWidget* parent = nullptr);
const Data &data() const { return data_; }
public Q_SLOTS:
void Expand();
void Collapse();
Q_SIGNALS:
void Toggled(const bool expanded);
private Q_SLOTS:
void ExpandedToggled(const bool expanded);
private:
Data data_;
CollapsibleInfoHeader *header_;
};
#endif // COLLAPSIBLEINFOPANE_H

View File

@@ -0,0 +1,116 @@
#include <QFileDialog>
#include <QFileInfo>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#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);
}
}
}

View File

@@ -0,0 +1,40 @@
#ifndef FILECHOOSERWIDGET_H
#define FILECHOOSERWIDGET_H
#include <QWidget>
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

View File

@@ -1,89 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <QWidget>
#include <QApplication>
#include <QMenu>
#include <QWheelEvent>
#include <QRegularExpression>
#include <QtDebug>
#include "core/logging.h"
#include "infotextview.h"
InfoTextView::InfoTextView(QWidget *parent) : QTextBrowser(parent), last_width_(-1), recursion_filter_(false) {
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setOpenExternalLinks(true);
}
void InfoTextView::resizeEvent(QResizeEvent *e) {
const int w = qMax(100, width());
if (w == last_width_) return;
last_width_ = w;
document()->setTextWidth(w);
setMinimumHeight(document()->size().height());
QTextBrowser::resizeEvent(e);
}
QSize InfoTextView::sizeHint() const { return minimumSize(); }
void InfoTextView::wheelEvent(QWheelEvent *e) { e->ignore(); }
void InfoTextView::SetHtml(const QString &html) {
QString copy(html.trimmed());
// Simplify newlines
copy.replace(QRegularExpression(QStringLiteral("\\r\\n?")), QStringLiteral("\n"));
// Convert two or more newlines to <p>, convert single newlines to <br>
copy.replace(QRegularExpression(QStringLiteral("([^>])([\\t ]*\\n){2,}")), QStringLiteral("\\1<p>"));
copy.replace(QRegularExpression(QStringLiteral("([^>])[\\t ]*\\n")), QStringLiteral("\\1<br>"));
// Strip any newlines from the end
copy.replace(QRegularExpression(QStringLiteral("((<\\s*br\\s*/?\\s*>)|(<\\s*/?\\s*p\\s*/?\\s*>))+$")), QLatin1String(""));
setHtml(copy);
}
// Prevents QTextDocument from trying to load remote images before they are ready.
QVariant InfoTextView::loadResource(int type, const QUrl &name) {
if (recursion_filter_) {
recursion_filter_ = false;
return QVariant();
}
recursion_filter_ = true;
if (type == QTextDocument::ImageResource && name.scheme() == QLatin1String("http")) {
if (document()->resource(type, name).isNull()) {
return QVariant();
}
}
return QTextBrowser::loadResource(type, name);
}

View File

@@ -1,52 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef INFOTEXTVIEW_H
#define INFOTEXTVIEW_H
#include <QTextBrowser>
#include <QString>
#include <QUrl>
class QResizeEvent;
class QWheelEvent;
class InfoTextView : public QTextBrowser {
Q_OBJECT
public:
explicit InfoTextView(QWidget *parent = nullptr);
QSize sizeHint() const override;
public Q_SLOTS:
void SetHtml(const QString &html);
protected:
void resizeEvent(QResizeEvent *e) override;
void wheelEvent(QWheelEvent *e) override;
QVariant loadResource(int type, const QUrl &name) override;
private:
int last_width_;
bool recursion_filter_;
};
#endif // INFOTEXTVIEW_H

View File

@@ -1,257 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <QApplication>
#include <QWindow>
#include <QScreen>
#include <QtConcurrentRun>
#include <QFuture>
#include <QString>
#include <QImage>
#include <QPixmap>
#include <QPainter>
#include <QFileInfo>
#include <QDir>
#include <QFileDialog>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QSettings>
#include <QLabel>
#include <QMenu>
#include <QScrollArea>
#include <QContextMenuEvent>
#include "core/logging.h"
#include "core/networkaccessmanager.h"
#include "core/iconloader.h"
#include "prettyimage.h"
const int PrettyImage::kTotalHeight = 200;
const int PrettyImage::kReflectionHeight = 40;
const int PrettyImage::kImageHeight = PrettyImage::kTotalHeight - PrettyImage::kReflectionHeight;
const int PrettyImage::kMaxImageWidth = 300;
const char *PrettyImage::kSettingsGroup = "PrettyImageView";
PrettyImage::PrettyImage(const QUrl &url, QNetworkAccessManager *network, QWidget *parent)
: QWidget(parent),
network_(network),
state_(State_WaitingForLazyLoad),
url_(url),
menu_(nullptr) {
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
LazyLoad();
}
void PrettyImage::LazyLoad() {
if (state_ != State_WaitingForLazyLoad) return;
// Start fetching the image
QNetworkReply *reply = network_->get(QNetworkRequest(url_));
state_ = State_Fetching;
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { ImageFetched(reply); });
}
QSize PrettyImage::image_size() const {
if (state_ != State_Finished) return QSize(kImageHeight * 1.6, kImageHeight);
QSize ret = image_.size();
ret.scale(kMaxImageWidth, kImageHeight, Qt::KeepAspectRatio);
return ret;
}
QSize PrettyImage::sizeHint() const {
return QSize(image_size().width(), kTotalHeight);
}
void PrettyImage::ImageFetched(QNetworkReply *reply) {
reply->deleteLater();
QImage image = QImage::fromData(reply->readAll());
if (image.isNull()) {
qLog(Debug) << "Image failed to load" << reply->request().url() << reply->error();
deleteLater();
}
else {
state_ = State_CreatingThumbnail;
image_ = image;
(void)QtConcurrent::run([=]{ ImageScaled(image_.scaled(image_size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); });
}
}
void PrettyImage::ImageScaled(QImage image) {
thumbnail_ = QPixmap::fromImage(image);
state_ = State_Finished;
updateGeometry();
update();
emit Loaded();
}
void PrettyImage::paintEvent(QPaintEvent*) {
// Draw at the bottom of our area
QRect image_rect(QPoint(0, 0), image_size());
image_rect.moveBottom(kImageHeight);
QPainter p(this);
// Draw the main image
DrawThumbnail(&p, image_rect);
// Draw the reflection
// Figure out where to draw it
QRect reflection_rect(image_rect);
reflection_rect.moveTop(image_rect.bottom());
// Create the reflected pixmap
QImage reflection(reflection_rect.size(), QImage::Format_ARGB32_Premultiplied);
reflection.fill(palette().color(QPalette::Base).rgba());
QPainter reflection_painter(&reflection);
// Set up the transformation
QTransform transform;
transform.scale(1.0, -1.0);
transform.translate(0.0, -reflection_rect.height());
reflection_painter.setTransform(transform);
QRect fade_rect(reflection.rect().bottomLeft() - QPoint(0, kReflectionHeight), reflection.rect().bottomRight());
// Draw the reflection into the buffer
DrawThumbnail(&reflection_painter, reflection.rect());
// Make it fade out towards the bottom
QLinearGradient fade_gradient(fade_rect.topLeft(), fade_rect.bottomLeft());
fade_gradient.setColorAt(0.0, QColor(0, 0, 0, 0));
fade_gradient.setColorAt(1.0, QColor(0, 0, 0, 128));
reflection_painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
reflection_painter.fillRect(fade_rect, fade_gradient);
reflection_painter.end();
// Draw the reflection on the image
p.drawImage(reflection_rect, reflection);
}
void PrettyImage::DrawThumbnail(QPainter *p, const QRect &rect) {
switch (state_) {
case State_WaitingForLazyLoad:
case State_Fetching:
case State_CreatingThumbnail:
p->setPen(palette().color(QPalette::Disabled, QPalette::Text));
p->drawText(rect, Qt::AlignHCenter | Qt::AlignBottom, tr("Loading..."));
break;
case State_Finished:
p->drawPixmap(rect, thumbnail_);
break;
}
}
void PrettyImage::contextMenuEvent(QContextMenuEvent *e) {
if (e->pos().y() >= kImageHeight) return;
if (!menu_) {
menu_ = new QMenu(this);
menu_->addAction(IconLoader::Load(QStringLiteral("zoom-in")), tr("Show fullsize..."), this, &PrettyImage::ShowFullsize);
menu_->addAction(IconLoader::Load(QStringLiteral("document-save")), tr("Save image") + QLatin1String("..."), this, &PrettyImage::SaveAs);
}
menu_->popup(e->globalPos());
}
void PrettyImage::ShowFullsize() {
// Create the window
QScrollArea *pwindow = new QScrollArea;
pwindow->setAttribute(Qt::WA_DeleteOnClose, true);
pwindow->setWindowTitle(tr("%1 image viewer").arg(QLatin1String("Strawberry")));
// Work out how large to make the window, based on the size of the screen
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
QScreen *screen = QWidget::screen();
#else
QScreen *screen = (window() && window()->windowHandle() ? window()->windowHandle()->screen() : QGuiApplication::primaryScreen());
#endif
if (screen) {
QRect desktop_rect(screen->availableGeometry());
QSize window_size(qMin(desktop_rect.width() - 20, image_.width()), qMin(desktop_rect.height() - 20, image_.height()));
pwindow->resize(window_size);
}
// Create the label that displays the image
QLabel *label = new QLabel(pwindow);
label->setPixmap(QPixmap::fromImage(image_));
// Show the label in the window
pwindow->setWidget(label);
pwindow->setFrameShape(QFrame::NoFrame);
pwindow->show();
}
void PrettyImage::SaveAs() {
QString filename = QFileInfo(url_.path()).fileName();
if (filename.isEmpty()) filename = QLatin1String("artwork.jpg");
QSettings s;
s.beginGroup(kSettingsGroup);
QString last_save_dir = s.value("last_save_dir", QDir::homePath()).toString();
QString path = last_save_dir.isEmpty() ? QDir::homePath() : last_save_dir;
QFileInfo path_info(path);
if (path_info.isDir()) {
path += QLatin1Char('/') + filename;
}
else {
path = path_info.path() + QLatin1Char('/') + filename;
}
filename = QFileDialog::getSaveFileName(this, tr("Save image"), path);
if (filename.isEmpty()) return;
image_.save(filename);
s.setValue("last_save_dir", last_save_dir);
s.endGroup();
}

View File

@@ -1,92 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef PRETTYIMAGE_H
#define PRETTYIMAGE_H
#include <QWidget>
#include <QString>
#include <QUrl>
#include <QImage>
#include <QPixmap>
#include <QPainter>
class QMenu;
class QNetworkAccessManager;
class QNetworkReply;
class QContextMenuEvent;
class QPaintEvent;
class PrettyImage : public QWidget {
Q_OBJECT
public:
PrettyImage(const QUrl &url, QNetworkAccessManager *network, QWidget *parent = nullptr);
static const int kTotalHeight;
static const int kReflectionHeight;
static const int kImageHeight;
static const int kMaxImageWidth;
static const char *kSettingsGroup;
QSize sizeHint() const override;
QSize image_size() const;
signals:
void Loaded();
public slots:
void LazyLoad();
void SaveAs();
void ShowFullsize();
protected:
void contextMenuEvent(QContextMenuEvent*) override;
void paintEvent(QPaintEvent*) override;
private slots:
void ImageFetched(QNetworkReply *reply);
void ImageScaled(QImage image);
private:
enum State {
State_WaitingForLazyLoad,
State_Fetching,
State_CreatingThumbnail,
State_Finished,
};
void DrawThumbnail(QPainter *p, const QRect &rect);
private:
QNetworkAccessManager *network_;
State state_;
QUrl url_;
QImage image_;
QPixmap thumbnail_;
QMenu *menu_;
QString last_save_dir_;
};
#endif // PRETTYIMAGE_H

View File

@@ -1,189 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <QObject>
#include <QUrl>
#include <QNetworkAccessManager>
#include <QPropertyAnimation>
#include <QAbstractSlider>
#include <QHBoxLayout>
#include <QScrollArea>
#include <QScrollBar>
#include <QTimer>
#include <QMouseEvent>
#include <QResizeEvent>
#include <QWheelEvent>
#include "core/networkaccessmanager.h"
#include "prettyimage.h"
#include "prettyimageview.h"
PrettyImageView::PrettyImageView(QNetworkAccessManager *network, QWidget* parent)
: QScrollArea(parent),
network_(network),
container_(new QWidget(this)),
layout_(new QHBoxLayout(container_)),
current_index_(-1),
scroll_animation_(new QPropertyAnimation(horizontalScrollBar(), "value", this)),
recursion_filter_(false) {
setWidget(container_);
setWidgetResizable(true);
setMinimumHeight(PrettyImage::kTotalHeight + 10);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
setFrameShape(QFrame::NoFrame);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scroll_animation_->setDuration(250);
scroll_animation_->setEasingCurve(QEasingCurve::InOutCubic);
connect(horizontalScrollBar(), SIGNAL(sliderReleased()), SLOT(ScrollBarReleased()));
connect(horizontalScrollBar(), SIGNAL(actionTriggered(int)), SLOT(ScrollBarAction(int)));
layout_->setSizeConstraint(QLayout::SetMinAndMaxSize);
layout_->setContentsMargins(6, 6, 6, 6);
layout_->setSpacing(6);
layout_->addSpacing(200);
layout_->addSpacing(200);
container_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
}
bool PrettyImageView::eventFilter(QObject *obj, QEvent *event) {
// Work around infinite recursion in QScrollArea resizes.
if (recursion_filter_) {
return false;
}
recursion_filter_ = true;
bool ret = QScrollArea::eventFilter(obj, event);
recursion_filter_ = false;
return ret;
}
void PrettyImageView::AddImage(const QUrl &url) {
PrettyImage *image = new PrettyImage(url, network_, container_);
connect(image, SIGNAL(destroyed()), SLOT(ScrollToCurrent()));
connect(image, SIGNAL(Loaded()), SLOT(ScrollToCurrent()));
layout_->insertWidget(layout_->count() - 1, image);
if (current_index_ == -1) ScrollTo(0);
}
void PrettyImageView::mouseReleaseEvent(QMouseEvent *e) {
// Find the image that was clicked on
QWidget *widget = container_->childAt(container_->mapFrom(this, e->pos()));
if (!widget) return;
// Get the index of that image
const int index = layout_->indexOf(widget) - 1;
if (index == -1) return;
if (index == current_index_) {
// Show the image fullsize
PrettyImage* pretty_image = qobject_cast<PrettyImage*>(widget);
if (pretty_image) {
pretty_image->ShowFullsize();
}
}
else {
// Scroll to the image
ScrollTo(index);
}
}
void PrettyImageView::ScrollTo(const int index, const bool smooth) {
current_index_ = qBound(0, index, layout_->count() - 3);
const int layout_index = current_index_ + 1;
const QWidget *target_widget = layout_->itemAt(layout_index)->widget();
if (!target_widget) return;
const int current_x = horizontalScrollBar()->value();
const int target_x = target_widget->geometry().center().x() - width() / 2;
if (current_x == target_x) return;
if (smooth) {
scroll_animation_->setStartValue(current_x);
scroll_animation_->setEndValue(target_x);
scroll_animation_->start();
}
else {
scroll_animation_->stop();
horizontalScrollBar()->setValue(target_x);
}
}
void PrettyImageView::ScrollToCurrent() { ScrollTo(current_index_); }
void PrettyImageView::ScrollBarReleased() {
// Find the nearest widget to where the scroll bar was released
const int current_x = horizontalScrollBar()->value() + width() / 2;
int layout_index = 1;
for (; layout_index < layout_->count() - 1; ++layout_index) {
const QWidget *widget = layout_->itemAt(layout_index)->widget();
if (widget && widget->geometry().right() > current_x) {
break;
}
}
ScrollTo(layout_index - 1);
}
void PrettyImageView::ScrollBarAction(const int action) {
switch (action) {
case QAbstractSlider::SliderSingleStepAdd:
case QAbstractSlider::SliderPageStepAdd:
ScrollTo(current_index_ + 1);
break;
case QAbstractSlider::SliderSingleStepSub:
case QAbstractSlider::SliderPageStepSub:
ScrollTo(current_index_ - 1);
break;
}
}
void PrettyImageView::resizeEvent(QResizeEvent *e) {
QScrollArea::resizeEvent(e);
ScrollTo(current_index_, false);
}
void PrettyImageView::wheelEvent(QWheelEvent *e) {
const int d = e->angleDelta().x() > 0 ? -1 : 1;
ScrollTo(current_index_ + d, true);
}

View File

@@ -1,74 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef PRETTYIMAGEVIEW_H
#define PRETTYIMAGEVIEW_H
#include <QScrollArea>
#include <QMap>
#include <QUrl>
class QNetworkAccessManager;
class QNetworkReply;
class QMenu;
class QHBoxLayout;
class QPropertyAnimation;
class QTimeLine;
class QMouseEvent;
class QResizeEvent;
class QWheelEvent;
class PrettyImageView : public QScrollArea {
Q_OBJECT
public:
PrettyImageView(QNetworkAccessManager *network, QWidget *parent = nullptr);
static const char* kSettingsGroup;
public Q_SLOTS:
void AddImage(const QUrl& url);
protected:
void mouseReleaseEvent(QMouseEvent*) override;
void resizeEvent(QResizeEvent *e) override;
void wheelEvent(QWheelEvent *e) override;
private Q_SLOTS:
void ScrollBarReleased();
void ScrollBarAction(const int action);
void ScrollTo(const int index, const bool smooth = true);
void ScrollToCurrent();
private:
bool eventFilter(QObject*, QEvent*) override;
QNetworkAccessManager *network_;
QWidget *container_;
QHBoxLayout *layout_;
int current_index_;
QPropertyAnimation *scroll_animation_;
bool recursion_filter_;
};
#endif // PRETTYIMAGEVIEW_H

View File

@@ -1,187 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <QObject>
#include <QWidget>
#include <QImage>
#include <QPixmap>
#include <QPainter>
#include <QTimeLine>
#include <QResizeEvent>
#include <QEvent>
#include "widgetfadehelper.h"
#include "core/qt_blurimage.h"
const int WidgetFadeHelper::kLoadingPadding = 9;
const int WidgetFadeHelper::kLoadingBorderRadius = 10;
WidgetFadeHelper::WidgetFadeHelper(QWidget *parent, const int msec)
: QWidget(parent),
parent_(parent),
blur_timeline_(new QTimeLine(msec, this)),
fade_timeline_(new QTimeLine(msec, this)) {
parent->installEventFilter(this);
connect(blur_timeline_, SIGNAL(valueChanged(qreal)), SLOT(update()));
connect(fade_timeline_, SIGNAL(valueChanged(qreal)), SLOT(update()));
connect(fade_timeline_, SIGNAL(finished()), SLOT(FadeFinished()));
hide();
}
bool WidgetFadeHelper::eventFilter(QObject *obj, QEvent *event) {
// We're only interested in our parent's resize events
if (obj != parent_ || event->type() != QEvent::Resize) return false;
// Don't care if we're hidden
if (!isVisible()) return false;
QResizeEvent *re = static_cast<QResizeEvent*>(event);
if (re->oldSize() == re->size()) {
// Ignore phoney resize events
return false;
}
// Get a new capture of the parent
hide();
CaptureParent();
show();
return false;
}
void WidgetFadeHelper::StartBlur() {
CaptureParent();
// Cover the parent
raise();
show();
// Start the timeline
blur_timeline_->stop();
blur_timeline_->start();
setAttribute(Qt::WA_TransparentForMouseEvents, false);
}
void WidgetFadeHelper::CaptureParent() {
// Take a "screenshot" of the window
original_pixmap_ = parent_->grab();
QImage original_image = original_pixmap_.toImage();
// Blur it
QImage blurred(original_image.size(), QImage::Format_ARGB32_Premultiplied);
blurred.fill(Qt::transparent);
QPainter blur_painter(&blurred);
blur_painter.save();
qt_blurImage(&blur_painter, original_image, 10.0, true, false);
blur_painter.restore();
// Draw some loading text over the top
QFont loading_font(font());
loading_font.setBold(true);
QFontMetrics loading_font_metrics(loading_font);
const QString loading_text = tr("Loading...");
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
const QSize loading_size(kLoadingPadding * 2 + loading_font_metrics.horizontalAdvance(loading_text), kLoadingPadding * 2 + loading_font_metrics.height());
#else
const QSize loading_size(kLoadingPadding * 2 + loading_font_metrics.width(loading_text), kLoadingPadding * 2 + loading_font_metrics.height());
#endif
const QRect loading_rect((blurred.width() - loading_size.width()) / 2, 100, loading_size.width(), loading_size.height());
blur_painter.setRenderHint(QPainter::Antialiasing);
blur_painter.translate(0.5, 0.5);
blur_painter.setPen(QColor(200, 200, 200, 255));
blur_painter.setBrush(QColor(200, 200, 200, 192));
blur_painter.drawRoundedRect(loading_rect, kLoadingBorderRadius, kLoadingBorderRadius);
blur_painter.setPen(palette().brush(QPalette::Text).color());
blur_painter.setFont(loading_font);
blur_painter.drawText(loading_rect.translated(-1, -1), Qt::AlignCenter, loading_text);
blur_painter.translate(-0.5, -0.5);
blur_painter.end();
blurred_pixmap_ = QPixmap::fromImage(blurred);
resize(parent_->size());
}
void WidgetFadeHelper::StartFade() {
if (blur_timeline_->state() == QTimeLine::Running) {
// Blur timeline is still running, so we need render the current state
// into a new pixmap.
QPixmap pixmap(original_pixmap_);
QPainter painter(&pixmap);
painter.setOpacity(blur_timeline_->currentValue());
painter.drawPixmap(0, 0, blurred_pixmap_);
painter.end();
blurred_pixmap_ = pixmap;
}
blur_timeline_->stop();
original_pixmap_ = QPixmap();
// Start the timeline
fade_timeline_->stop();
fade_timeline_->start();
setAttribute(Qt::WA_TransparentForMouseEvents, true);
}
void WidgetFadeHelper::paintEvent(QPaintEvent *event) {
Q_UNUSED(event)
QPainter p(this);
if (fade_timeline_->state() != QTimeLine::Running) {
// We're fading in the blur
p.drawPixmap(0, 0, original_pixmap_);
p.setOpacity(blur_timeline_->currentValue());
}
else {
// Fading out the blur into the new image
p.setOpacity(1.0 - fade_timeline_->currentValue());
}
p.drawPixmap(0, 0, blurred_pixmap_);
}
void WidgetFadeHelper::FadeFinished() {
hide();
original_pixmap_ = QPixmap();
blurred_pixmap_ = QPixmap();
}

View File

@@ -1,63 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef WIDGETFADEHELPER_H
#define WIDGETFADEHELPER_H
#include <QWidget>
#include <QPixmap>
class QTimeLine;
class QPaintEvent;
class QEvent;
class WidgetFadeHelper : public QWidget {
Q_OBJECT
public:
WidgetFadeHelper(QWidget *parent, const int msec = 500);
public Q_SLOTS:
void StartBlur();
void StartFade();
protected:
void paintEvent(QPaintEvent *event) override;
bool eventFilter(QObject *obj, QEvent *event) override;
private Q_SLOTS:
void FadeFinished();
private:
void CaptureParent();
private:
static const int kLoadingPadding;
static const int kLoadingBorderRadius;
QWidget *parent_;
QTimeLine *blur_timeline_;
QTimeLine *fade_timeline_;
QPixmap original_pixmap_;
QPixmap blurred_pixmap_;
};
#endif // WIDGETFADEHELPER_H