Compare commits

..

12 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
ef0065a49c MainWindow: Save tab mode immediately when changed
The sidebar mode (tabs on top, large sidebar, etc.) was only being saved
when the application exits normally. This caused the setting to be lost
if the application crashed or was force-closed, reverting to the default
"Large Sidebar" mode on next startup.

This fix connects the FancyTabWidget::ModeChanged signal to immediately
save the tab_mode setting when the user changes it via the context menu.

Fixes issue where sidebar keeps periodically reverting to Large mode.

Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2026-01-03 15:34:55 +00:00
copilot-swe-agent[bot]
28fb5a863c Initial plan 2026-01-03 15:12:21 +00:00
Jonas Kvinge
e8d9e1172f FileViewTreeModel: Add const 2026-01-03 16:09:56 +01:00
Alexopus
aac8d4e68b Add file tree view 2026-01-03 15:11:56 +01:00
dependabot[bot]
0e28e800b3 Bump vmactions/freebsd-vm from 1.3.4 to 1.3.5
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.3.4 to 1.3.5.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.3.4...v1.3.5)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-version: 1.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-02 17:33:02 +01:00
Jonas Kvinge
cf84bc29ab CI: Manually codesign 2026-01-01 01:51:10 +01:00
Jonas Kvinge
afc3effc9d CI: Switch macOS dependencies repo 2025-12-30 20:01:34 +01:00
Jonas Kvinge
370bebff5f CollectionView: Fix Enter/Return behavior to respect double-click settings
Fixes #1691
2025-12-30 19:08:52 +01:00
Jonas Kvinge
db410cc257 MainWindow: Remove unused declaration 2025-12-29 22:14:08 +01:00
Jonas Kvinge
20a9946e51 Song: Prefer filenames with "front" or "cover" for art automatic
Fixes #1745
2025-12-29 21:16:06 +01:00
dependabot[bot]
b6c8ff19af Bump vmactions/freebsd-vm from 1.3.2 to 1.3.4
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.3.2 to 1.3.4.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.3.2...v1.3.4)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-version: 1.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 18:18:55 +01:00
dependabot[bot]
80d058af10 Bump vmactions/openbsd-vm from 1.2.9 to 1.3.1
Bumps [vmactions/openbsd-vm](https://github.com/vmactions/openbsd-vm) from 1.2.9 to 1.3.1.
- [Release notes](https://github.com/vmactions/openbsd-vm/releases)
- [Commits](https://github.com/vmactions/openbsd-vm/compare/v1.2.9...v1.3.1)

---
updated-dependencies:
- dependency-name: vmactions/openbsd-vm
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 17:23:16 +01:00
58 changed files with 1075 additions and 5371 deletions

View File

@@ -747,7 +747,7 @@ jobs:
df -h
- name: Build FreeBSD
id: build-freebsd
uses: vmactions/freebsd-vm@v1.3.2
uses: vmactions/freebsd-vm@v1.3.5
with:
usesh: true
mem: 8192
@@ -772,7 +772,7 @@ jobs:
submodules: recursive
- name: Build OpenBSD
id: build-openbsd
uses: vmactions/openbsd-vm@v1.2.9
uses: vmactions/openbsd-vm@v1.3.1
with:
usesh: true
mem: 4096
@@ -845,7 +845,7 @@ jobs:
p12-password: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }}
- name: Download macOS dependencies
run: curl -f -O -L https://github.com/strawberrymusicplayer/strawberry-macos-dependencies$(test "${{env.arch}}" = "x86_64" && echo "-intel" || echo "")/releases/latest/download/strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
run: curl -f -O -L https://github.com/strawberrymusicplayer/strawberry-macos-dependencies/releases/latest/download/strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
- name: Extract macOS dependencies
run: sudo tar -C / -xf strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
@@ -898,7 +898,7 @@ jobs:
- name: Manually Codesign
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15-intel'
working-directory: build
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib,libbrotlicommon.1.dylib,libbrotlienc.1.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib,libbrotlicommon.1.dylib,libbrotlienc.1.dylib,libbrotlidec.1.dylib,libsoup-3.0.0.dylib,libnghttp2.14.dylib,libpsl.5.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
- name: Manually Codesign
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15'

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 Protobuf)
set(QT_OPTIONAL_COMPONENTS GuiPrivate LinguistTools Test)
if(UNIX AND NOT APPLE)
list(APPEND QT_OPTIONAL_COMPONENTS DBus)
endif()
@@ -278,7 +278,6 @@ 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)
@@ -382,18 +381,6 @@ 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()
@@ -772,7 +759,6 @@ 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
@@ -837,6 +823,8 @@ set(SOURCES
src/fileview/fileview.cpp
src/fileview/fileviewlist.cpp
src/fileview/fileviewtree.cpp
src/fileview/fileviewtreemodel.cpp
src/device/devicemanager.cpp
src/device/devicelister.cpp
@@ -1071,7 +1059,6 @@ 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
@@ -1127,6 +1114,8 @@ set(HEADERS
src/fileview/fileview.h
src/fileview/fileviewlist.h
src/fileview/fileviewtree.h
src/fileview/fileviewtreemodel.h
src/device/devicemanager.h
src/device/devicelister.h
@@ -1495,57 +1484,6 @@ 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)
@@ -1584,12 +1522,6 @@ 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}
@@ -1622,7 +1554,6 @@ 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>
@@ -1642,7 +1573,6 @@ target_link_libraries(strawberry_lib PUBLIC
$<$<BOOL:${MSVC}>:WindowsApp>
KDAB::kdsingleapplication
$<$<BOOL:${HAVE_DISCORD_RPC}>:discord-rpc>
$<$<BOOL:${HAVE_NETWORKREMOTE}>:NetworkRemoteMessages>
)
if(APPLE)
@@ -1661,10 +1591,6 @@ 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

@@ -1,101 +0,0 @@
<?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

@@ -1,405 +0,0 @@
<?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,114 +0,0 @@
/*
* 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";
}
}

View File

@@ -1,58 +0,0 @@
/*
* 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

@@ -1,98 +0,0 @@
<?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

@@ -1,396 +0,0 @@
<?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

@@ -383,7 +383,7 @@ void CollectionView::keyPressEvent(QKeyEvent *e) {
case Qt::Key_Enter:
case Qt::Key_Return:
if (currentIndex().isValid()) {
AddToPlaylist();
Q_EMIT doubleClicked(currentIndex());
}
e->accept();
break;

View File

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

View File

@@ -1,36 +0,0 @@
/*
* 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

@@ -1,35 +0,0 @@
/*
* 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,10 +110,6 @@
# include "moodbar/moodbarloader.h"
#endif
#ifdef HAVE_NETWORKREMOTE
# include "networkremote/networkremote.h"
#endif
#include "radios/radioservices.h"
#include "radios/radiobackend.h"
@@ -220,13 +216,6 @@ 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()); })
{}
@@ -252,9 +241,6 @@ 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_;
@@ -404,6 +390,3 @@ 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,9 +63,6 @@ class RadioServices;
class MoodbarController;
class MoodbarLoader;
#endif
#ifdef HAVE_NETWORKREMOTE
class NetworkRemote;
#endif
class Application : public QObject {
Q_OBJECT
@@ -106,10 +103,6 @@ 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();

View File

@@ -1,24 +0,0 @@
#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

View File

@@ -1,57 +0,0 @@
#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

@@ -447,6 +447,14 @@ MainWindow::MainWindow(Application *app,
ui_->tabs->SetBackgroundPixmap(QPixmap(u":/pictures/sidebar-background.png"_s));
ui_->tabs->LoadSettings(QLatin1String(MainWindowSettings::kSettingsGroup));
// Save tab mode immediately when changed to avoid losing the setting
QObject::connect(ui_->tabs, &FancyTabWidget::ModeChanged, this, [this](FancyTabWidget::Mode mode) {
Settings s;
s.beginGroup(MainWindowSettings::kSettingsGroup);
s.setValue("tab_mode", static_cast<int>(mode));
s.endGroup();
});
track_position_timer_->setInterval(kTrackPositionUpdateTimeMs);
QObject::connect(track_position_timer_, &QTimer::timeout, this, &MainWindow::UpdateTrackPosition);
track_slider_timer_->setInterval(kTrackSliderUpdateTimeMs);
@@ -977,10 +985,6 @@ MainWindow::MainWindow(Application *app,
ui_->action_open_cd->setVisible(false);
#endif
#ifdef HAVE_NETWORKREMOTE
app_->network_remote();
#endif
// Load settings
qLog(Debug) << "Loading settings";
Settings settings;

View File

@@ -43,7 +43,6 @@
#include <QString>
#include <QUrl>
#include <QImage>
#include <QPixmap>
#include <QTimer>
#include <QtEvents>
@@ -290,9 +289,6 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void CheckFullRescanRevisions();
// creates the icon by painting the full one depending on the current position
QPixmap CreateOverlayedIcon(const int position, const int scrobble_point);
void GetCoverAutomatically();
void SetToggleScrobblingIcon(const bool value);

View File

@@ -25,15 +25,14 @@
#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, const int playlist_id, 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, 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),
playlist_id_(playlist_id) {
from_doubleclick_(false) {
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, const int playlist_id = -1, 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, QObject *parent = nullptr);
// If this is set then MainWindow will not touch any of the other flags.
bool override_user_settings_;
@@ -57,9 +57,6 @@ 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;

View File

@@ -1668,12 +1668,24 @@ void Song::InitArtManual() {
void Song::InitArtAutomatic() {
if (d->art_automatic_.isEmpty() && d->source_ == Source::LocalFile && d->url_.isLocalFile()) {
// Pick the first image file in the album directory.
QFileInfo file(d->url_.toLocalFile());
QDir dir(file.path());
QStringList files = dir.entryList(QStringList() << u"*.jpg"_s << u"*.png"_s << u"*.gif"_s << u"*.jpeg"_s, QDir::Files|QDir::Readable, QDir::Name);
if (files.count() > 0) {
d->art_automatic_ = QUrl::fromLocalFile(file.path() + QDir::separator() + files.first());
const QFileInfo fileinfo(d->url_.toLocalFile());
const QDir dir(fileinfo.path());
const QStringList cover_files = dir.entryList(QStringList() << u"*.jpg"_s << u"*.png"_s << u"*.gif"_s << u"*.jpeg"_s, QDir::Files|QDir::Readable, QDir::Name);
QString best_cover_file;
for (const QString &cover_file : cover_files) {
if (cover_file.contains("back"_L1, Qt::CaseInsensitive)) {
continue;
}
if (cover_file.contains("front"_L1, Qt::CaseInsensitive) || cover_file.startsWith("cover"_L1, Qt::CaseInsensitive)) {
best_cover_file = cover_file;
break;
}
if (best_cover_file.isEmpty()) {
best_cover_file = cover_file;
}
}
if (!best_cover_file.isEmpty()) {
d->art_automatic_ = QUrl::fromLocalFile(fileinfo.path() + QDir::separator() + best_cover_file);
}
}

View File

@@ -1,86 +0,0 @@
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);
}
}

View File

@@ -1,26 +0,0 @@
#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

View File

@@ -1,69 +0,0 @@
#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);
}

View File

@@ -1,28 +0,0 @@
#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

@@ -29,13 +29,17 @@
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QSettings>
#include <QStandardPaths>
#include <QMessageBox>
#include <QScrollBar>
#include <QLineEdit>
#include <QToolButton>
#include <QFileDialog>
#include <QSpacerItem>
#include <QtEvents>
#include "constants/appearancesettings.h"
#include "constants/filefilterconstants.h"
#include "includes/shared_ptr.h"
#include "core/deletefiles.h"
#include "core/filesystemmusicstorage.h"
@@ -45,10 +49,11 @@
#include "dialogs/deleteconfirmationdialog.h"
#include "fileview.h"
#include "fileviewlist.h"
#include "fileviewtree.h"
#include "fileviewtreemodel.h"
#include "fileviewtreeitem.h"
#include "ui_fileview.h"
#include "organize/organizeerrordialog.h"
#include "constants/appearancesettings.h"
#include "constants/filefilterconstants.h"
using std::make_unique;
using namespace Qt::Literals::StringLiterals;
@@ -57,9 +62,12 @@ FileView::FileView(QWidget *parent)
: QWidget(parent),
ui_(new Ui_FileView),
model_(nullptr),
tree_model_(nullptr),
undo_stack_(new QUndoStack(this)),
task_manager_(nullptr),
storage_(new FilesystemMusicStorage(Song::Source::LocalFile, u"/"_s)) {
storage_(new FilesystemMusicStorage(Song::Source::LocalFile, u"/"_s)),
tree_view_active_(false),
view_mode_spacer_(nullptr) {
ui_->setupUi(this);
@@ -68,12 +76,14 @@ FileView::FileView(QWidget *parent)
ui_->forward->setIcon(IconLoader::Load(u"go-next"_s));
ui_->home->setIcon(IconLoader::Load(u"go-home"_s));
ui_->up->setIcon(IconLoader::Load(u"go-up"_s));
ui_->toggle_view->setIcon(IconLoader::Load(u"view-choose"_s));
QObject::connect(ui_->back, &QToolButton::clicked, undo_stack_, &QUndoStack::undo);
QObject::connect(ui_->forward, &QToolButton::clicked, undo_stack_, &QUndoStack::redo);
QObject::connect(ui_->home, &QToolButton::clicked, this, &FileView::FileHome);
QObject::connect(ui_->up, &QToolButton::clicked, this, &FileView::FileUp);
QObject::connect(ui_->path, &QLineEdit::textChanged, this, &FileView::ChangeFilePath);
QObject::connect(ui_->toggle_view, &QToolButton::clicked, this, &FileView::ToggleViewMode);
QObject::connect(undo_stack_, &QUndoStack::canUndoChanged, ui_->back, &FileView::setEnabled);
QObject::connect(undo_stack_, &QUndoStack::canRedoChanged, ui_->forward, &FileView::setEnabled);
@@ -87,6 +97,22 @@ FileView::FileView(QWidget *parent)
QObject::connect(ui_->list, &FileViewList::Delete, this, &FileView::Delete);
QObject::connect(ui_->list, &FileViewList::EditTags, this, &FileView::EditTags);
// Connect tree view signals
QObject::connect(ui_->tree, &FileViewTree::AddToPlaylist, this, &FileView::AddToPlaylist);
QObject::connect(ui_->tree, &FileViewTree::CopyToCollection, this, &FileView::CopyToCollection);
QObject::connect(ui_->tree, &FileViewTree::MoveToCollection, this, &FileView::MoveToCollection);
QObject::connect(ui_->tree, &FileViewTree::CopyToDevice, this, &FileView::CopyToDevice);
QObject::connect(ui_->tree, &FileViewTree::Delete, this, &FileView::Delete);
QObject::connect(ui_->tree, &FileViewTree::EditTags, this, &FileView::EditTags);
QObject::connect(ui_->tree, &FileViewTree::activated, this, &FileView::ItemActivated);
QObject::connect(ui_->tree, &FileViewTree::doubleClicked, this, &FileView::ItemDoubleClick);
// Setup tree root management buttons
ui_->add_tree_root->setIcon(IconLoader::Load(u"folder-new"_s));
ui_->remove_tree_root->setIcon(IconLoader::Load(u"list-remove"_s));
QObject::connect(ui_->add_tree_root, &QToolButton::clicked, this, &FileView::AddRootButtonClicked);
QObject::connect(ui_->remove_tree_root, &QToolButton::clicked, this, &FileView::RemoveRootButtonClicked);
QString filter = QLatin1String(kFileFilter);
filter_list_ << filter.split(u' ');
@@ -109,6 +135,19 @@ void FileView::ReloadSettings() {
ui_->forward->setIconSize(QSize(iconsize, iconsize));
ui_->home->setIconSize(QSize(iconsize, iconsize));
ui_->up->setIconSize(QSize(iconsize, iconsize));
ui_->toggle_view->setIconSize(QSize(iconsize, iconsize));
ui_->add_tree_root->setIconSize(QSize(iconsize, iconsize));
ui_->remove_tree_root->setIconSize(QSize(iconsize, iconsize));
// Load tree root paths setting
Settings file_settings;
file_settings.beginGroup(u"FileView"_s);
tree_root_paths_ = file_settings.value(u"tree_root_paths"_s, QStandardPaths::standardLocations(QStandardPaths::StandardLocation::MusicLocation)).toStringList();
tree_view_active_ = file_settings.value(u"tree_view_active"_s, false).toBool();
file_settings.endGroup();
// Set initial view mode
UpdateViewModeUI();
}
@@ -180,24 +219,46 @@ void FileView::ChangeFilePathWithoutUndo(const QString &new_path) {
}
void FileView::ItemActivated(const QModelIndex &idx) {
if (model_->isDir(idx))
// Only handle activation for list view (not tree view)
if (!tree_view_active_ && model_->isDir(idx)) {
ChangeFilePath(model_->filePath(idx));
}
}
void FileView::ItemDoubleClick(const QModelIndex &idx) {
if (model_->isDir(idx)) {
return;
QString file_path;
bool is_file = false;
// Handle tree view with virtual roots
if (tree_view_active_ && tree_model_) {
QVariant type_var = tree_model_->data(idx, FileViewTreeModel::Role_Type);
if (type_var.isValid()) {
FileViewTreeItem::Type item_type = type_var.value<FileViewTreeItem::Type>();
// Only handle files, ignore directories and virtual roots
if (item_type == FileViewTreeItem::Type::File) {
file_path = tree_model_->data(idx, FileViewTreeModel::Role_FilePath).toString();
is_file = true;
}
}
}
// Handle list view with filesystem model
else if (!tree_view_active_ && model_) {
if (!model_->isDir(idx)) {
file_path = model_->filePath(idx);
is_file = true;
}
}
QString file_path = model_->filePath(idx);
// Add file to playlist if it's a valid file
if (is_file && !file_path.isEmpty()) {
MimeData *mimedata = new MimeData;
mimedata->from_doubleclick_ = true;
mimedata->setUrls(QList<QUrl>() << QUrl::fromLocalFile(file_path));
mimedata->name_for_new_playlist_ = file_path;
MimeData *mimedata = new MimeData;
mimedata->from_doubleclick_ = true;
mimedata->setUrls(QList<QUrl>() << QUrl::fromLocalFile(file_path));
mimedata->name_for_new_playlist_ = file_path;
Q_EMIT AddToPlaylist(mimedata);
Q_EMIT AddToPlaylist(mimedata);
}
}
@@ -272,12 +333,156 @@ void FileView::showEvent(QShowEvent *e) {
model_->setNameFilterDisables(false);
ui_->list->setModel(model_);
// Create tree model
tree_model_ = new FileViewTreeModel(this);
tree_model_->SetNameFilters(filter_list_);
SetupTreeView();
ChangeFilePathWithoutUndo(QDir::homePath());
if (!lazy_set_path_.isEmpty()) ChangeFilePathWithoutUndo(lazy_set_path_);
}
void FileView::SetupTreeView() {
// Use the new tree model with virtual roots
ui_->tree->setModel(tree_model_);
// Set the root paths in the model
tree_model_->SetRootPaths(tree_root_paths_);
// No need to set root index - the model handles virtual roots
}
void FileView::ToggleViewMode() {
tree_view_active_ = !tree_view_active_;
UpdateViewModeUI();
// Save the preference
Settings s;
s.beginGroup(u"FileView"_s);
s.setValue(u"tree_view_active"_s, tree_view_active_);
s.endGroup();
}
void FileView::UpdateViewModeUI() {
if (tree_view_active_) {
ui_->view_stack->setCurrentWidget(ui_->tree_page);
// Hide navigation controls in tree view mode
ui_->back->setVisible(false);
ui_->forward->setVisible(false);
ui_->up->setVisible(false);
ui_->home->setVisible(false);
ui_->path->setVisible(false);
// Show tree root management buttons
ui_->add_tree_root->setVisible(true);
ui_->remove_tree_root->setVisible(true);
// Insert spacer in tree view if not already present
if (!view_mode_spacer_) {
view_mode_spacer_ = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum);
ui_->horizontalLayout->insertSpacerItem(ui_->horizontalLayout->indexOf(ui_->toggle_view), view_mode_spacer_);
}
}
else {
ui_->view_stack->setCurrentWidget(ui_->list_page);
// Show navigation controls in list view mode
ui_->back->setVisible(true);
ui_->forward->setVisible(true);
ui_->up->setVisible(true);
ui_->home->setVisible(true);
ui_->path->setVisible(true);
// Hide tree root management buttons in list view
ui_->add_tree_root->setVisible(false);
ui_->remove_tree_root->setVisible(false);
// Remove spacer in list view
if (view_mode_spacer_) {
ui_->horizontalLayout->removeItem(view_mode_spacer_);
delete view_mode_spacer_;
view_mode_spacer_ = nullptr;
}
}
}
void FileView::AddTreeRootPath(const QString &path) {
if (!tree_root_paths_.contains(path)) {
tree_root_paths_.append(path);
SaveTreeRootPaths();
// Refresh the tree view to show the new root
if (tree_model_) {
SetupTreeView();
}
}
}
void FileView::RemoveTreeRootPath(const QString &path) {
tree_root_paths_.removeAll(path);
SaveTreeRootPaths();
// Refresh the tree view
if (tree_model_) {
SetupTreeView();
}
}
void FileView::SaveTreeRootPaths() {
Settings s;
s.beginGroup(u"FileView"_s);
s.setValue(u"tree_root_paths"_s, tree_root_paths_);
s.endGroup();
}
void FileView::AddRootButtonClicked() {
const QString dir = QFileDialog::getExistingDirectory(this, tr("Select folder to add as tree root"), tree_root_paths_.isEmpty() ? QDir::homePath() : tree_root_paths_.first(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
if (!dir.isEmpty()) {
AddTreeRootPath(dir);
}
}
void FileView::RemoveRootButtonClicked() {
// Get currently selected item in tree
QModelIndex current = ui_->tree->currentIndex();
if (!current.isValid()) return;
QString path;
// Get the file path from the appropriate model
if (tree_model_) {
path = tree_model_->data(current, FileViewTreeModel::Role_FilePath).toString();
}
if (path.isEmpty()) return;
const QString clean_path = QDir::cleanPath(path);
// Check if this path or any parent is a configured root
for (const QString &root : std::as_const(tree_root_paths_)) {
const QString clean_root = QDir::cleanPath(root);
if (clean_path == clean_root || clean_path.startsWith(clean_root + QDir::separator())) {
RemoveTreeRootPath(root);
return;
}
}
}
void FileView::keyPressEvent(QKeyEvent *e) {
switch (e->key()) {

View File

@@ -40,10 +40,12 @@ class QFileIconProvider;
class QUndoStack;
class QKeyEvent;
class QShowEvent;
class QSpacerItem;
class MusicStorage;
class TaskManager;
class Ui_FileView;
class FileViewTreeModel;
class FileView : public QWidget {
Q_OBJECT
@@ -76,12 +78,22 @@ class FileView : public QWidget {
void ChangeFilePath(const QString &new_path);
void ItemActivated(const QModelIndex &idx);
void ItemDoubleClick(const QModelIndex &idx);
void ToggleViewMode();
void Delete(const QStringList &filenames);
void DeleteFinished(const SongList &songs_with_errors);
public Q_SLOTS:
void AddTreeRootPath(const QString &path);
void RemoveTreeRootPath(const QString &path);
private:
void ChangeFilePathWithoutUndo(const QString &new_path);
void SetupTreeView();
void SaveTreeRootPaths();
void AddRootButtonClicked();
void RemoveRootButtonClicked();
void UpdateViewModeUI();
private:
class UndoCommand : public QUndoCommand {
@@ -110,16 +122,21 @@ class FileView : public QWidget {
Ui_FileView *ui_;
QFileSystemModel *model_;
FileViewTreeModel *tree_model_;
QUndoStack *undo_stack_;
SharedPtr<TaskManager> task_manager_;
SharedPtr<MusicStorage> storage_;
QString lazy_set_path_;
QStringList tree_root_paths_;
QStringList filter_list_;
ScopedPtr<QFileIconProvider> file_icon_provider_;
bool tree_view_active_;
QSpacerItem *view_mode_spacer_;
};
#endif // FILEVIEW_H

View File

@@ -95,28 +95,143 @@
<item>
<widget class="QLineEdit" name="path"/>
</item>
<item>
<widget class="QToolButton" name="add_tree_root">
<property name="toolTip">
<string>Add root directory</string>
</property>
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="remove_tree_root">
<property name="toolTip">
<string>Remove selected root directory</string>
</property>
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="toggle_view">
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
<property name="toolTip">
<string>Toggle between list and tree view</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="FileViewList" name="list">
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragOnly</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
<widget class="QStackedWidget" name="view_stack">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="list_page">
<layout class="QVBoxLayout" name="list_layout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="FileViewList" name="list">
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragOnly</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tree_page">
<layout class="QVBoxLayout" name="tree_layout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="FileViewTree" name="tree">
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragOnly</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
@@ -127,6 +242,11 @@
<extends>QListView</extends>
<header>fileview/fileviewlist.h</header>
</customwidget>
<customwidget>
<class>FileViewTree</class>
<extends>QTreeView</extends>
<header>fileview/fileviewtree.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>

View File

@@ -99,7 +99,7 @@ MimeData *FileViewList::MimeDataFromSelection() const {
const QStringList filenames = FilenamesFromSelection();
// if just one folder selected - use its path as the new playlist's name
// If just one folder selected - use its path as the new playlist's name
if (filenames.size() == 1 && QFileInfo(filenames.first()).isDir()) {
if (filenames.first().length() > 20) {
mimedata->name_for_new_playlist_ = QDir(filenames.first()).dirName();
@@ -108,7 +108,7 @@ MimeData *FileViewList::MimeDataFromSelection() const {
mimedata->name_for_new_playlist_ = filenames.first();
}
}
// otherwise, use the current root path
// Otherwise, use the current root path
else {
QString path = qobject_cast<QFileSystemModel*>(model())->rootPath();
if (path.length() > 20) {
@@ -196,11 +196,11 @@ void FileViewList::mousePressEvent(QMouseEvent *e) {
case Qt::XButton2:
Q_EMIT Forward();
break;
// enqueue to playlist with middleClick
// Enqueue to playlist with middleClick
case Qt::MiddleButton:{
QListView::mousePressEvent(e);
// we need to update the menu selection
// We need to update the menu selection
menu_selection_ = selectionModel()->selection();
MimeData *mimedata = new MimeData;

View File

@@ -0,0 +1,205 @@
/*
* Strawberry Music Player
* Copyright 2025, 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 <utility>
#include <QWidget>
#include <QAbstractItemModel>
#include <QFileInfo>
#include <QDir>
#include <QMenu>
#include <QUrl>
#include <QCollator>
#include <QtEvents>
#include "core/iconloader.h"
#include "core/mimedata.h"
#include "utilities/filemanagerutils.h"
#include "fileviewtree.h"
#include "fileviewtreemodel.h"
using namespace Qt::Literals::StringLiterals;
FileViewTree::FileViewTree(QWidget *parent)
: QTreeView(parent),
menu_(new QMenu(this)) {
menu_->addAction(IconLoader::Load(u"media-playback-start"_s), tr("Append to current playlist"), this, &FileViewTree::AddToPlaylistSlot);
menu_->addAction(IconLoader::Load(u"media-playback-start"_s), tr("Replace current playlist"), this, &FileViewTree::LoadSlot);
menu_->addAction(IconLoader::Load(u"document-new"_s), tr("Open in new playlist"), this, &FileViewTree::OpenInNewPlaylistSlot);
menu_->addSeparator();
menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Copy to collection..."), this, &FileViewTree::CopyToCollectionSlot);
menu_->addAction(IconLoader::Load(u"go-jump"_s), tr("Move to collection..."), this, &FileViewTree::MoveToCollectionSlot);
menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &FileViewTree::CopyToDeviceSlot);
menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &FileViewTree::DeleteSlot);
menu_->addSeparator();
menu_->addAction(IconLoader::Load(u"edit-rename"_s), tr("Edit track information..."), this, &FileViewTree::EditTagsSlot);
menu_->addAction(IconLoader::Load(u"document-open-folder"_s), tr("Show in file browser..."), this, &FileViewTree::ShowInBrowser);
setAttribute(Qt::WA_MacShowFocusRect, false);
setHeaderHidden(true);
setUniformRowHeights(true);
}
void FileViewTree::contextMenuEvent(QContextMenuEvent *e) {
menu_selection_ = selectionModel()->selection();
menu_->popup(e->globalPos());
e->accept();
}
QStringList FileViewTree::FilenamesFromSelection() const {
QStringList filenames;
const QModelIndexList indexes = menu_selection_.indexes();
FileViewTreeModel *tree_model = qobject_cast<FileViewTreeModel*>(model());
if (tree_model) {
for (const QModelIndex &index : indexes) {
if (index.column() == 0) {
QString path = tree_model->data(index, FileViewTreeModel::Role_FilePath).toString();
if (!path.isEmpty()) {
filenames << path;
}
}
}
}
QCollator collator;
collator.setNumericMode(true);
std::sort(filenames.begin(), filenames.end(), collator);
return filenames;
}
QList<QUrl> FileViewTree::UrlListFromSelection() const {
QList<QUrl> urls;
const QStringList filenames = FilenamesFromSelection();
urls.reserve(filenames.count());
for (const QString &filename : std::as_const(filenames)) {
urls << QUrl::fromLocalFile(filename);
}
return urls;
}
MimeData *FileViewTree::MimeDataFromSelection() const {
MimeData *mimedata = new MimeData;
mimedata->setUrls(UrlListFromSelection());
const QStringList filenames = FilenamesFromSelection();
// if just one folder selected - use its path as the new playlist's name
if (filenames.size() == 1 && QFileInfo(filenames.first()).isDir()) {
if (filenames.first().length() > 20) {
mimedata->name_for_new_playlist_ = QDir(filenames.first()).dirName();
}
else {
mimedata->name_for_new_playlist_ = filenames.first();
}
}
// otherwise, use "Files" as default
else {
mimedata->name_for_new_playlist_ = tr("Files");
}
return mimedata;
}
void FileViewTree::LoadSlot() {
MimeData *mimedata = MimeDataFromSelection();
mimedata->clear_first_ = true;
Q_EMIT AddToPlaylist(mimedata);
}
void FileViewTree::AddToPlaylistSlot() {
Q_EMIT AddToPlaylist(MimeDataFromSelection());
}
void FileViewTree::OpenInNewPlaylistSlot() {
MimeData *mimedata = MimeDataFromSelection();
mimedata->open_in_new_playlist_ = true;
Q_EMIT AddToPlaylist(mimedata);
}
void FileViewTree::CopyToCollectionSlot() {
Q_EMIT CopyToCollection(UrlListFromSelection());
}
void FileViewTree::MoveToCollectionSlot() {
Q_EMIT MoveToCollection(UrlListFromSelection());
}
void FileViewTree::CopyToDeviceSlot() {
Q_EMIT CopyToDevice(UrlListFromSelection());
}
void FileViewTree::DeleteSlot() {
Q_EMIT Delete(FilenamesFromSelection());
}
void FileViewTree::EditTagsSlot() {
Q_EMIT EditTags(UrlListFromSelection());
}
void FileViewTree::mousePressEvent(QMouseEvent *e) {
switch (e->button()) {
// Enqueue to playlist with middleClick
case Qt::MiddleButton:{
QTreeView::mousePressEvent(e);
// We need to update the menu selection
QItemSelectionModel *selection_model = selectionModel();
if (!selection_model) {
e->ignore();
return;
}
menu_selection_ = selection_model->selection();
MimeData *mimedata = new MimeData;
mimedata->setUrls(UrlListFromSelection());
mimedata->enqueue_now_ = true;
Q_EMIT AddToPlaylist(mimedata);
break;
}
default:
QTreeView::mousePressEvent(e);
break;
}
}
void FileViewTree::ShowInBrowser() {
Utilities::OpenInFileBrowser(UrlListFromSelection());
}

View File

@@ -0,0 +1,78 @@
/*
* Strawberry Music Player
* Copyright 2025, 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 FILEVIEWTREE_H
#define FILEVIEWTREE_H
#include <QObject>
#include <QTreeView>
#include <QList>
#include <QUrl>
#include <QString>
#include <QStringList>
class QWidget;
class QMimeData;
class QMenu;
class QMouseEvent;
class QContextMenuEvent;
class MimeData;
class FileViewTree : public QTreeView {
Q_OBJECT
public:
explicit FileViewTree(QWidget *parent = nullptr);
void mousePressEvent(QMouseEvent *e) override;
Q_SIGNALS:
void AddToPlaylist(QMimeData *data);
void CopyToCollection(const QList<QUrl> &urls);
void MoveToCollection(const QList<QUrl> &urls);
void CopyToDevice(const QList<QUrl> &urls);
void Delete(const QStringList &filenames);
void EditTags(const QList<QUrl> &urls);
protected:
void contextMenuEvent(QContextMenuEvent *e) override;
private:
QStringList FilenamesFromSelection() const;
QList<QUrl> UrlListFromSelection() const;
MimeData *MimeDataFromSelection() const;
private Q_SLOTS:
void LoadSlot();
void AddToPlaylistSlot();
void OpenInNewPlaylistSlot();
void CopyToCollectionSlot();
void MoveToCollectionSlot();
void CopyToDeviceSlot();
void DeleteSlot();
void EditTagsSlot();
void ShowInBrowser();
private:
QMenu *menu_;
QItemSelection menu_selection_;
};
#endif // FILEVIEWTREE_H

View File

@@ -0,0 +1,52 @@
/*
* Strawberry Music Player
* Copyright 2025, 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 FILEVIEWTREEITEM_H
#define FILEVIEWTREEITEM_H
#include "config.h"
#include <QFileInfo>
#include "core/simpletreeitem.h"
class FileViewTreeItem : public SimpleTreeItem<FileViewTreeItem> {
public:
enum class Type {
Root, // Hidden root
VirtualRoot, // User-configured root paths
Directory, // File system directory
File // File system file
};
explicit FileViewTreeItem(SimpleTreeModel<FileViewTreeItem> *_model) : SimpleTreeItem<FileViewTreeItem>(_model), type(Type::Root), lazy_loaded(false) {}
explicit FileViewTreeItem(const Type _type, FileViewTreeItem *_parent = nullptr) : SimpleTreeItem<FileViewTreeItem>(_parent), type(_type), lazy_loaded(false) {}
Type type;
QString file_path; // Absolute file system path
QFileInfo file_info; // Cached file info
bool lazy_loaded; // Whether children have been loaded
private:
Q_DISABLE_COPY(FileViewTreeItem)
};
Q_DECLARE_METATYPE(FileViewTreeItem::Type)
#endif // FILEVIEWTREEITEM_H

View File

@@ -0,0 +1,246 @@
/*
* Strawberry Music Player
* Copyright 2025, 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 <QVariant>
#include <QString>
#include <QStringList>
#include <QList>
#include <QMap>
#include <QDir>
#include <QFileInfo>
#include <QFileIconProvider>
#include <QMimeData>
#include <QUrl>
#include <QIcon>
#include "core/simpletreemodel.h"
#include "core/logging.h"
#include "fileviewtreemodel.h"
#include "fileviewtreeitem.h"
using namespace Qt::Literals::StringLiterals;
FileViewTreeModel::FileViewTreeModel(QObject *parent)
: SimpleTreeModel<FileViewTreeItem>(new FileViewTreeItem(this), parent),
icon_provider_(new QFileIconProvider()) {
}
FileViewTreeModel::~FileViewTreeModel() {
delete root_;
delete icon_provider_;
}
Qt::ItemFlags FileViewTreeModel::flags(const QModelIndex &idx) const {
const FileViewTreeItem *item = IndexToItem(idx);
if (!item) return Qt::NoItemFlags;
switch (item->type) {
case FileViewTreeItem::Type::VirtualRoot:
case FileViewTreeItem::Type::Directory:
case FileViewTreeItem::Type::File:
return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled;
case FileViewTreeItem::Type::Root:
default:
return Qt::ItemIsEnabled;
}
}
QVariant FileViewTreeModel::data(const QModelIndex &idx, const int role) const {
if (!idx.isValid()) return QVariant();
const FileViewTreeItem *item = IndexToItem(idx);
if (!item) return QVariant();
switch (role) {
case Qt::DisplayRole:
if (item->type == FileViewTreeItem::Type::VirtualRoot) {
return item->display_text.isEmpty() ? item->file_path : item->display_text;
}
return item->file_info.fileName();
case Qt::DecorationRole:
return GetIcon(item);
case Role_Type:
return QVariant::fromValue(item->type);
case Role_FilePath:
return item->file_path;
case Role_FileName:
return item->file_info.fileName();
default:
return QVariant();
}
}
bool FileViewTreeModel::hasChildren(const QModelIndex &parent) const {
const FileViewTreeItem *item = IndexToItem(parent);
if (!item) return false;
// Root and VirtualRoot always have children (or can have them)
if (item->type == FileViewTreeItem::Type::Root) return true;
if (item->type == FileViewTreeItem::Type::VirtualRoot) return true;
// Directories can have children
if (item->type == FileViewTreeItem::Type::Directory) {
return true;
}
// Files don't have children
return false;
}
bool FileViewTreeModel::canFetchMore(const QModelIndex &parent) const {
const FileViewTreeItem *item = IndexToItem(parent);
if (!item) return false;
// Can fetch more if not yet lazy loaded
return !item->lazy_loaded && (item->type == FileViewTreeItem::Type::VirtualRoot || item->type == FileViewTreeItem::Type::Directory);
}
void FileViewTreeModel::fetchMore(const QModelIndex &parent) {
FileViewTreeItem *item = IndexToItem(parent);
if (!item || item->lazy_loaded) return;
LazyLoad(item);
}
void FileViewTreeModel::LazyLoad(FileViewTreeItem *item) {
if (item->lazy_loaded) return;
QDir dir(item->file_path);
if (!dir.exists()) {
item->lazy_loaded = true;
return;
}
// Apply name filters
const QDir::Filters filters = QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot;
if (!name_filters_.isEmpty()) {
dir.setNameFilters(name_filters_);
}
const QFileInfoList entries = dir.entryInfoList(filters, QDir::Name | QDir::DirsFirst);
if (!entries.isEmpty()) {
BeginInsert(item, 0, static_cast<int>(entries.count()) - 1);
for (const QFileInfo &entry : entries) {
FileViewTreeItem *child = new FileViewTreeItem(
entry.isDir() ? FileViewTreeItem::Type::Directory : FileViewTreeItem::Type::File,
item
);
child->file_path = entry.absoluteFilePath();
child->file_info = entry;
child->lazy_loaded = false;
child->display_text = entry.fileName();
}
EndInsert();
}
item->lazy_loaded = true;
}
QIcon FileViewTreeModel::GetIcon(const FileViewTreeItem *item) const {
if (!item) return QIcon();
switch (item->type) {
case FileViewTreeItem::Type::VirtualRoot:
case FileViewTreeItem::Type::Directory:
return icon_provider_->icon(QFileIconProvider::Folder);
case FileViewTreeItem::Type::File:
return icon_provider_->icon(item->file_info);
default:
return QIcon();
}
}
QStringList FileViewTreeModel::mimeTypes() const {
return QStringList() << u"text/uri-list"_s;
}
QMimeData *FileViewTreeModel::mimeData(const QModelIndexList &indexes) const {
if (indexes.isEmpty()) return nullptr;
QList<QUrl> urls;
for (const QModelIndex &idx : indexes) {
const FileViewTreeItem *item = IndexToItem(idx);
if (item && (item->type == FileViewTreeItem::Type::File || item->type == FileViewTreeItem::Type::Directory || item->type == FileViewTreeItem::Type::VirtualRoot)) {
urls << QUrl::fromLocalFile(item->file_path);
}
}
if (urls.isEmpty()) return nullptr;
QMimeData *data = new QMimeData();
data->setUrls(urls);
return data;
}
void FileViewTreeModel::SetRootPaths(const QStringList &paths) {
Reset();
for (const QString &path : paths) {
QFileInfo info(path);
if (!info.exists() || !info.isDir()) continue;
FileViewTreeItem *virtual_root = new FileViewTreeItem(FileViewTreeItem::Type::VirtualRoot, root_);
virtual_root->file_path = info.absoluteFilePath();
virtual_root->file_info = info;
virtual_root->display_text = info.absoluteFilePath();
virtual_root->lazy_loaded = false;
}
}
void FileViewTreeModel::SetNameFilters(const QStringList &filters) {
name_filters_ = filters;
}
void FileViewTreeModel::Reset() {
beginResetModel();
// Clear children without notifications since we're in a reset
qDeleteAll(root_->children);
root_->children.clear();
endResetModel();
}

View File

@@ -0,0 +1,72 @@
/*
* Strawberry Music Player
* Copyright 2025, 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 FILEVIEWTREEMODEL_H
#define FILEVIEWTREEMODEL_H
#include "config.h"
#include <QObject>
#include <QVariant>
#include <QStringList>
#include <QIcon>
#include "core/simpletreemodel.h"
#include "fileviewtreeitem.h"
class QFileIconProvider;
class QMimeData;
class FileViewTreeModel : public SimpleTreeModel<FileViewTreeItem> {
Q_OBJECT
public:
explicit FileViewTreeModel(QObject *parent = nullptr);
~FileViewTreeModel() override;
enum Role {
Role_Type = Qt::UserRole + 1,
Role_FilePath,
Role_FileName,
RoleCount
};
// QAbstractItemModel
Qt::ItemFlags flags(const QModelIndex &idx) const override;
QVariant data(const QModelIndex &idx, const int role) const override;
bool hasChildren(const QModelIndex &parent) const override;
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
QStringList mimeTypes() const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
void SetRootPaths(const QStringList &paths);
void SetNameFilters(const QStringList &filters);
private:
void Reset();
void LazyLoad(FileViewTreeItem *item);
QIcon GetIcon(const FileViewTreeItem *item) const;
private:
QFileIconProvider *icon_provider_;
QStringList name_filters_;
};
#endif // FILEVIEWTREEMODEL_H

View File

@@ -1,478 +0,0 @@
/*
* 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

@@ -1,123 +0,0 @@
/*
* 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

@@ -1,231 +0,0 @@
/*
* 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

@@ -1,98 +0,0 @@
/*
* 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

@@ -1,223 +0,0 @@
/*
* 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

@@ -1,89 +0,0 @@
/*
* 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

@@ -1,422 +0,0 @@
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

@@ -1,638 +0,0 @@
/*
* 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

@@ -1,120 +0,0 @@
/*
* 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

@@ -1,442 +0,0 @@
/*
* 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

@@ -1,96 +0,0 @@
/*
* 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,7 +57,6 @@
#include "playlistview.h"
#include "playlistsaveoptionsdialog.h"
#include "playlistparsers/playlistparser.h"
#include "queue/queue.h"
#include "dialogs/saveplaylistsdialog.h"
using namespace Qt::Literals::StringLiterals;
@@ -186,9 +185,9 @@ Playlist *PlaylistManager::AddPlaylist(const int id, const QString &name, const
}
int PlaylistManager::New(const QString &name, const SongList &songs, const QString &special_type) {
void PlaylistManager::New(const QString &name, const SongList &songs, const QString &special_type) {
if (name.isNull()) return -1;
if (name.isNull()) return;
int id = playlist_backend_->CreatePlaylist(name, special_type);
@@ -204,8 +203,6 @@ int PlaylistManager::New(const QString &name, const SongList &songs, const QStri
Rename(id, QStringLiteral("%1 %2").arg(name).arg(id));
}
return id;
}
void PlaylistManager::Load(const QString &filename) {
@@ -617,22 +614,3 @@ 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:
int New(const QString &name, const SongList &songs = SongList(), const QString &special_type = QString()) override;
void 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,9 +144,6 @@ 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 int New(const QString &name, const SongList &songs = SongList(), const QString &special_type = QString()) = 0;
virtual void 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,6 +51,7 @@ 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);
@@ -78,7 +79,6 @@ class Queue : public QAbstractProxyModel {
public Q_SLOTS:
void UpdateSummaryText();
void ToggleTracks(const QModelIndexList &source_indexes);
Q_SIGNALS:
void TotalLengthChanged(const quint64 length);

View File

@@ -1,157 +0,0 @@
/*
* 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

@@ -1,46 +0,0 @@
/*
* 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/>.
*
*/
#ifndef NETWORKREMOTESETTINGSPAGE_H
#define NETWORKREMOTESETTINGSPAGE_H
#include "settingspage.h"
class Ui_NetworkRemoteSettingsPage;
class NetworkRemoteSettingsPage : public SettingsPage {
Q_OBJECT
public:
explicit NetworkRemoteSettingsPage(SettingsDialog *dialog);
~NetworkRemoteSettingsPage();
void Load();
void Save();
private Q_SLOTS:
void Options();
private:
Ui_NetworkRemoteSettingsPage *ui_;
};
#endif // NETWORKREMOTESETTINGSPAGE_H

View File

@@ -1,298 +0,0 @@
<?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,9 +90,6 @@
# include "qobuz/qobuzservice.h"
# include "qobuzsettingspage.h"
#endif
#ifdef HAVE_NETWORKREMOTE
# include "networkremotesettingspage.h"
#endif
#include "ui_settingsdialog.h"
@@ -164,10 +161,6 @@ 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,7 +93,6 @@ class SettingsDialog : public QDialog {
Tidal,
Qobuz,
Spotify,
NetworkRemote
};
enum Role {

View File

@@ -20,8 +20,6 @@
#include <QByteArray>
#include <QString>
#include <QCryptographicHash>
#include <QFile>
#include <QIODevice>
#include "cryptutils.h"
@@ -64,17 +62,4 @@ 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,7 +23,6 @@
#include <QByteArray>
#include <QString>
#include <QCryptographicHash>
#include <QFile>
namespace Utilities {
@@ -31,7 +30,6 @@ 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,116 +0,0 @@
#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

@@ -1,40 +0,0 @@
#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