Compare commits
1 Commits
networkrem
...
podcasts
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46f368a68e |
143
CMakeLists.txt
143
CMakeLists.txt
@@ -208,6 +208,8 @@ else()
|
||||
pkg_check_modules(TAGLIB REQUIRED IMPORTED_TARGET taglib>=1.12)
|
||||
endif()
|
||||
|
||||
pkg_check_modules(LIBMYGPO libmygpo-qt6)
|
||||
|
||||
find_package(GTest)
|
||||
|
||||
pkg_check_modules(LIBSPARSEHASH IMPORTED_TARGET libsparsehash)
|
||||
@@ -218,7 +220,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 +280,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,17 +383,7 @@ 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()
|
||||
|
||||
optional_component(PODCASTS ON "Podcasts support" DEPENDS "libmygpo" LIBMYGPO_FOUND)
|
||||
|
||||
if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ)
|
||||
set(HAVE_CHROMAPRINT ON)
|
||||
@@ -772,7 +763,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
|
||||
@@ -1071,7 +1061,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
|
||||
@@ -1495,56 +1484,68 @@ 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()
|
||||
optional_source(HAVE_PODCASTS
|
||||
SOURCES
|
||||
podcasts/gpoddersync.cpp
|
||||
podcasts/gpoddertoptagsmodel.cpp
|
||||
podcasts/gpoddertoptagspage.cpp
|
||||
podcasts/itunessearchpage.cpp
|
||||
podcasts/podcastbackend.cpp
|
||||
podcasts/podcastservice.cpp
|
||||
podcasts/podcast.cpp
|
||||
podcasts/podcastdownloader.cpp
|
||||
podcasts/podcastupdater.cpp
|
||||
podcasts/podcastdeleter.cpp
|
||||
podcasts/podcastdiscoverymodel.cpp
|
||||
podcasts/podcastepisode.cpp
|
||||
podcasts/podcastinfodialog.cpp
|
||||
podcasts/podcastinfowidget.cpp
|
||||
podcasts/podcastparser.cpp
|
||||
podcasts/podcastservicemodel.cpp
|
||||
podcasts/podcasturlloader.cpp
|
||||
podcasts/gpoddersearchpage.cpp
|
||||
podcasts/addpodcastbyurl.cpp
|
||||
podcasts/addpodcastdialog.cpp
|
||||
podcasts/addpodcastpage.cpp
|
||||
podcasts/episodeinfowidget.cpp
|
||||
podcasts/fixedopmlpage.cpp
|
||||
settings/podcastsettingspage.cpp
|
||||
HEADERS
|
||||
podcasts/addpodcastbyurl.h
|
||||
podcasts/addpodcastdialog.h
|
||||
podcasts/addpodcastpage.h
|
||||
podcasts/episodeinfowidget.h
|
||||
podcasts/fixedopmlpage.h
|
||||
podcasts/gpoddersync.h
|
||||
podcasts/gpoddertoptagsmodel.h
|
||||
podcasts/gpoddertoptagspage.h
|
||||
podcasts/itunessearchpage.h
|
||||
podcasts/opmlcontainer.h
|
||||
podcasts/podcastbackend.h
|
||||
podcasts/podcastdeleter.h
|
||||
podcasts/podcastdiscoverymodel.h
|
||||
podcasts/podcastdownloader.h
|
||||
podcasts/podcastepisode.h
|
||||
podcasts/podcast.h
|
||||
podcasts/podcastinfodialog.h
|
||||
podcasts/podcastinfowidget.h
|
||||
podcasts/podcastparser.h
|
||||
podcasts/podcastservice.h
|
||||
podcasts/podcastservicemodel.h
|
||||
podcasts/podcastupdater.h
|
||||
podcasts/podcasturlloader.h
|
||||
podcasts/gpoddersearchpage.h
|
||||
settings/podcastsettingspage.h
|
||||
UI
|
||||
podcasts/addpodcastbyurl.ui
|
||||
podcasts/addpodcastdialog.ui
|
||||
podcasts/episodeinfowidget.ui
|
||||
podcasts/itunessearchpage.ui
|
||||
podcasts/podcastinfodialog.ui
|
||||
podcasts/podcastinfowidget.ui
|
||||
podcasts/gpoddersearchpage.ui
|
||||
settings/podcastsettingspage.ui
|
||||
)
|
||||
|
||||
qt_wrap_cpp(SOURCES ${HEADERS})
|
||||
qt_wrap_ui(SOURCES ${UI})
|
||||
@@ -1584,12 +1585,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 +1617,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 +1636,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 +1654,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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -33,7 +33,7 @@
|
||||
#cmakedefine HAVE_SPOTIFY
|
||||
#cmakedefine HAVE_QOBUZ
|
||||
#cmakedefine HAVE_DISCORD_RPC
|
||||
#cmakedefine HAVE_NETWORKREMOTE
|
||||
#cmakedefine HAVE_PODCASTS
|
||||
|
||||
#cmakedefine HAVE_TAGLIB_DSFFILE
|
||||
#cmakedefine HAVE_TAGLIB_DSDIFFFILE
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -110,14 +110,19 @@
|
||||
# include "moodbar/moodbarloader.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_NETWORKREMOTE
|
||||
# include "networkremote/networkremote.h"
|
||||
#endif
|
||||
|
||||
#include "radios/radioservices.h"
|
||||
#include "radios/radiobackend.h"
|
||||
|
||||
#ifdef HAVE_PODCASTS
|
||||
# include "podcasts/podcastbackend.h"
|
||||
# include "podcasts/gpoddersync.h"
|
||||
# include "podcasts/podcastdownloader.h"
|
||||
# include "podcasts/podcastupdater.h"
|
||||
# include "podcasts/podcastdeleter.h"
|
||||
#endif
|
||||
|
||||
using std::make_shared;
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
class ApplicationImpl {
|
||||
@@ -221,11 +226,19 @@ class ApplicationImpl {
|
||||
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;
|
||||
#ifdef HAVE_PODCASTS
|
||||
podcast_backend_([app]() {
|
||||
PodcastBackend* backend = new PodcastBackend(app, app);
|
||||
app->MoveToThread(backend, database_->thread());
|
||||
return backend;
|
||||
}),
|
||||
gpodder_sync_([app]() { return new GPodderSync(app, app); }),
|
||||
podcast_downloader_([app]() { return new PodcastDownloader(app, app); }),
|
||||
podcast_updater_([app]() { return new PodcastUpdater(app, app); }),
|
||||
podcast_deleter_([app]() {
|
||||
PodcastDeleter* deleter = new PodcastDeleter(app, app);
|
||||
app->MoveToNewThread(deleter);
|
||||
return deleter;
|
||||
}),
|
||||
#endif
|
||||
lastfm_import_([app]() { return new LastFMImport(app->network()); })
|
||||
@@ -253,8 +266,12 @@ class ApplicationImpl {
|
||||
Lazy<MoodbarLoader> moodbar_loader_;
|
||||
Lazy<MoodbarController> moodbar_controller_;
|
||||
#endif
|
||||
#ifdef HAVE_NETWORKREMOTE
|
||||
Lazy<NetworkRemote> network_remote_;
|
||||
#ifdef HAVE_PODCASTS
|
||||
Lazy<PodcastBackend> podcast_backend_;
|
||||
Lazy<GPodderSync> gpodder_sync_;
|
||||
Lazy<PodcastDownloader> podcast_downloader_;
|
||||
Lazy<PodcastUpdater> podcast_updater_;
|
||||
Lazy<PodcastDeleter> podcast_deleter_;
|
||||
#endif
|
||||
Lazy<LastFMImport> lastfm_import_;
|
||||
|
||||
@@ -404,6 +421,10 @@ 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(); }
|
||||
#ifdef HAVE_PODCASTS
|
||||
PodcastBackend *Application::podcast_backend() const { return p_->podcast_backend_.get(); }
|
||||
GPodderSync *Application::gpodder_sync() const { return p_->gpodder_sync_.get(); }
|
||||
PodcastDownloader *Application::podcast_downloader() const { return p_->podcast_downloader_.get(); }
|
||||
PodcastUpdater *Application::podcast_updater() const { return p_->podcast_updater_.get(); }
|
||||
PodcastDeleter *Application::podcast_deleter() const { return p_->podcast_deleter_.get(); }
|
||||
#endif
|
||||
|
||||
@@ -63,10 +63,15 @@ class RadioServices;
|
||||
class MoodbarController;
|
||||
class MoodbarLoader;
|
||||
#endif
|
||||
#ifdef HAVE_NETWORKREMOTE
|
||||
class NetworkRemote;
|
||||
#ifdef HAVE_PODCASTS
|
||||
class PodcastBackend;
|
||||
class GPodderSync;
|
||||
class PodcastDownloader;
|
||||
class PodcastUpdater;
|
||||
class PodcastDeleter;
|
||||
#endif
|
||||
|
||||
|
||||
class Application : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
@@ -105,9 +110,12 @@ class Application : public QObject {
|
||||
SharedPtr<MoodbarController> moodbar_controller() const;
|
||||
SharedPtr<MoodbarLoader> moodbar_loader() const;
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_NETWORKREMOTE
|
||||
SharedPtr<NetworkRemote> network_remote() const;
|
||||
#ifdef HAVE_PODCASTS
|
||||
PodcastBackend *podcast_backend() const;
|
||||
GPodderSync *gpodder_sync() const;
|
||||
PodcastDownloader *podcast_downloader() const;
|
||||
PodcastUpdater *podcast_updater() const;
|
||||
PodcastDeleter *podcast_deleter() const;
|
||||
#endif
|
||||
|
||||
SharedPtr<LastFMImport> lastfm_import() const;
|
||||
|
||||
@@ -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
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -977,10 +977,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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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(); }
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
116
src/podcasts/addpodcastbyurl.cpp
Normal file
116
src/podcasts/addpodcastbyurl.cpp
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 <QString>
|
||||
#include <QUrl>
|
||||
#include <QClipboard>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include "core/iconloader.h"
|
||||
#include "podcastdiscoverymodel.h"
|
||||
#include "podcasturlloader.h"
|
||||
#include "addpodcastbyurl.h"
|
||||
#include "ui_addpodcastbyurl.h"
|
||||
|
||||
AddPodcastByUrl::AddPodcastByUrl(Application *app, QWidget *parent)
|
||||
: AddPodcastPage(app, parent),
|
||||
ui_(new Ui_AddPodcastByUrl),
|
||||
loader_(new PodcastUrlLoader(this)) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
QObject::connect(ui_->go, &QPushButton::clicked, this, &AddPodcastByUrl::GoClicked);
|
||||
setWindowIcon(IconLoader::Load("podcast"));
|
||||
|
||||
}
|
||||
|
||||
AddPodcastByUrl::~AddPodcastByUrl() { delete ui_; }
|
||||
|
||||
void AddPodcastByUrl::SetUrlAndGo(const QUrl &url) {
|
||||
|
||||
ui_->url->setText(url.toString());
|
||||
GoClicked();
|
||||
|
||||
}
|
||||
|
||||
void AddPodcastByUrl::SetOpml(const OpmlContainer &opml) {
|
||||
|
||||
ui_->url->setText(opml.url.toString());
|
||||
model()->clear();
|
||||
model()->CreateOpmlContainerItems(opml, model()->invisibleRootItem());
|
||||
|
||||
}
|
||||
|
||||
void AddPodcastByUrl::GoClicked() {
|
||||
|
||||
emit Busy(true);
|
||||
model()->clear();
|
||||
|
||||
PodcastUrlLoaderReply* reply = loader_->Load(ui_->url->text());
|
||||
ui_->url->setText(reply->url().toString());
|
||||
|
||||
QObject::connect(reply, &PodcastUrlLoaderReply::Finished, this, [this, reply]() { RequestFinished(reply); });
|
||||
|
||||
}
|
||||
|
||||
void AddPodcastByUrl::RequestFinished(PodcastUrlLoaderReply *reply) {
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
emit Busy(false);
|
||||
|
||||
if (!reply->is_success()) {
|
||||
QMessageBox::warning(this, tr("Failed to load podcast"), reply->error_text(), QMessageBox::Close);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (reply->result_type()) {
|
||||
case PodcastUrlLoaderReply::Type_Podcast:
|
||||
for (const Podcast& podcast : reply->podcast_results()) {
|
||||
model()->appendRow(model()->CreatePodcastItem(podcast));
|
||||
}
|
||||
break;
|
||||
|
||||
case PodcastUrlLoaderReply::Type_Opml:
|
||||
model()->CreateOpmlContainerItems(reply->opml_results(), model()->invisibleRootItem());
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void AddPodcastByUrl::Show() {
|
||||
|
||||
ui_->url->setFocus();
|
||||
|
||||
const QClipboard *clipboard = QApplication::clipboard();
|
||||
QStringList contents;
|
||||
contents << clipboard->text(QClipboard::Selection) << clipboard->text(QClipboard::Clipboard);
|
||||
|
||||
for (const QString &content : contents) {
|
||||
if (content.contains("://")) {
|
||||
ui_->url->setText(content);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
60
src/podcasts/addpodcastbyurl.h
Normal file
60
src/podcasts/addpodcastbyurl.h
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 ADDPODCASTBYURL_H
|
||||
#define ADDPODCASTBYURL_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
|
||||
#include "addpodcastpage.h"
|
||||
#include "opmlcontainer.h"
|
||||
|
||||
class Application;
|
||||
class AddPodcastPage;
|
||||
class PodcastUrlLoader;
|
||||
class PodcastUrlLoaderReply;
|
||||
class Ui_AddPodcastByUrl;
|
||||
|
||||
class AddPodcastByUrl : public AddPodcastPage {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AddPodcastByUrl(Application *app, QWidget *parent = nullptr);
|
||||
~AddPodcastByUrl();
|
||||
|
||||
void Show();
|
||||
|
||||
void SetOpml(const OpmlContainer &opml);
|
||||
void SetUrlAndGo(const QUrl &url);
|
||||
|
||||
private slots:
|
||||
void GoClicked();
|
||||
void RequestFinished(PodcastUrlLoaderReply *reply);
|
||||
|
||||
private:
|
||||
Ui_AddPodcastByUrl *ui_;
|
||||
PodcastUrlLoader *loader_;
|
||||
};
|
||||
|
||||
#endif // ADDPODCASTBYURL_H
|
||||
61
src/podcasts/addpodcastbyurl.ui
Normal file
61
src/podcasts/addpodcastbyurl.ui
Normal file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>AddPodcastByUrl</class>
|
||||
<widget class="QWidget" name="AddPodcastByUrl">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>431</width>
|
||||
<height>51</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Enter a URL</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>If you know the URL of a podcast, enter it below and press Go.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="url"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="go">
|
||||
<property name="text">
|
||||
<string>Go</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>url</sender>
|
||||
<signal>returnPressed()</signal>
|
||||
<receiver>go</receiver>
|
||||
<slot>click()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>109</x>
|
||||
<y>24</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>429</x>
|
||||
<y>49</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
270
src/podcasts/addpodcastdialog.cpp
Normal file
270
src/podcasts/addpodcastdialog.cpp
Normal file
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 <QDialog>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QTimer>
|
||||
#include <QPushButton>
|
||||
#include <QListWidget>
|
||||
#include <QItemSelectionModel>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "widgets/widgetfadehelper.h"
|
||||
#include "fixedopmlpage.h"
|
||||
#include "gpoddersearchpage.h"
|
||||
#include "gpoddertoptagspage.h"
|
||||
#include "itunessearchpage.h"
|
||||
#include "podcastbackend.h"
|
||||
#include "podcastdiscoverymodel.h"
|
||||
#include "addpodcastbyurl.h"
|
||||
#include "podcastinfowidget.h"
|
||||
#include "addpodcastdialog.h"
|
||||
#include "ui_addpodcastdialog.h"
|
||||
|
||||
const char *AddPodcastDialog::kBbcOpmlUrl = "http://www.bbc.co.uk/podcasts.opml";
|
||||
const char *AddPodcastDialog::kCbcOpmlUrl = "http://cbc.ca/podcasts.opml";
|
||||
|
||||
AddPodcastDialog::AddPodcastDialog(Application *app, QWidget *parent)
|
||||
: QDialog(parent),
|
||||
app_(app),
|
||||
ui_(new Ui_AddPodcastDialog),
|
||||
last_opml_path_(QDir::homePath()) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
ui_->details->SetApplication(app);
|
||||
ui_->results->SetExpandOnReset(false);
|
||||
ui_->results->SetAddOnDoubleClick(false);
|
||||
ui_->results_stack->setCurrentWidget(ui_->results_page);
|
||||
|
||||
fader_ = new WidgetFadeHelper(ui_->details_scroll_area);
|
||||
|
||||
QObject::connect(ui_->provider_list, &QListWidget::currentRowChanged, this, &AddPodcastDialog::ChangePage);
|
||||
QObject::connect(ui_->details, &PodcastInfoWidget::LoadingFinished, fader_, &WidgetFadeHelper::StartFade);
|
||||
QObject::connect(ui_->results, &AutoExpandingTreeView::doubleClicked, this, &AddPodcastDialog::PodcastDoubleClicked);
|
||||
|
||||
// Create Add and Remove Podcast buttons
|
||||
add_button_ = new QPushButton(IconLoader::Load("list-add"), tr("Add Podcast"), this);
|
||||
add_button_->setEnabled(false);
|
||||
connect(add_button_, &QPushButton::clicked, this, &AddPodcastDialog::AddPodcast);
|
||||
ui_->button_box->addButton(add_button_, QDialogButtonBox::ActionRole);
|
||||
|
||||
remove_button_ = new QPushButton(IconLoader::Load("list-remove"), tr("Unsubscribe"), this);
|
||||
remove_button_->setEnabled(false);
|
||||
connect(remove_button_, &QPushButton::clicked, this, &AddPodcastDialog::RemovePodcast);
|
||||
ui_->button_box->addButton(remove_button_, QDialogButtonBox::ActionRole);
|
||||
|
||||
QPushButton *settings_button = new QPushButton(IconLoader::Load("configure"), tr("Configure podcasts..."), this);
|
||||
connect(settings_button, &QPushButton::clicked, this, &AddPodcastDialog::OpenSettingsPage);
|
||||
ui_->button_box->addButton(settings_button, QDialogButtonBox::ResetRole);
|
||||
|
||||
// Create an Open OPML file button
|
||||
QPushButton *open_opml_button = new QPushButton(IconLoader::Load("document-open"), tr("Open OPML file..."), this);
|
||||
QObject::connect(open_opml_button, &QPushButton::clicked, this, &AddPodcastDialog::OpenOPMLFile);
|
||||
ui_->button_box->addButton(open_opml_button, QDialogButtonBox::ResetRole);
|
||||
|
||||
// Add providers
|
||||
by_url_page_ = new AddPodcastByUrl(app, this);
|
||||
AddPage(by_url_page_);
|
||||
AddPage(new FixedOpmlPage(QUrl(kBbcOpmlUrl), tr("BBC Podcasts"), IconLoader::Load("bbc"), app, this));
|
||||
AddPage(new FixedOpmlPage(QUrl(kCbcOpmlUrl), tr("CBC Podcasts"), IconLoader::Load("cbc"), app, this));
|
||||
AddPage(new GPodderTopTagsPage(app, this));
|
||||
AddPage(new GPodderSearchPage(app, this));
|
||||
AddPage(new ITunesSearchPage(app, this));
|
||||
|
||||
ui_->provider_list->setCurrentRow(0);
|
||||
}
|
||||
|
||||
AddPodcastDialog::~AddPodcastDialog() { delete ui_; }
|
||||
|
||||
void AddPodcastDialog::ShowWithUrl(const QUrl& url) {
|
||||
|
||||
by_url_page_->SetUrlAndGo(url);
|
||||
ui_->provider_list->setCurrentRow(0);
|
||||
show();
|
||||
|
||||
}
|
||||
|
||||
void AddPodcastDialog::ShowWithOpml(const OpmlContainer& opml) {
|
||||
|
||||
by_url_page_->SetOpml(opml);
|
||||
ui_->provider_list->setCurrentRow(0);
|
||||
show();
|
||||
|
||||
}
|
||||
|
||||
void AddPodcastDialog::AddPage(AddPodcastPage *page) {
|
||||
|
||||
pages_.append(page);
|
||||
page_is_busy_.append(false);
|
||||
|
||||
ui_->stack->addWidget(page);
|
||||
new QListWidgetItem(page->windowIcon(), page->windowTitle(), ui_->provider_list);
|
||||
|
||||
QObject::connect(page, &AddPodcastPage::Busy, this, &AddPodcastDialog::PageBusyChanged);
|
||||
|
||||
}
|
||||
|
||||
void AddPodcastDialog::ChangePage(const int index) {
|
||||
|
||||
AddPodcastPage *page = pages_[index];
|
||||
|
||||
ui_->stack->setCurrentIndex(index);
|
||||
ui_->stack->setVisible(page->has_visible_widget());
|
||||
ui_->results->setModel(page->model());
|
||||
|
||||
ui_->results_stack->setCurrentWidget(page_is_busy_[index] ? ui_->busy_page : ui_->results_page);
|
||||
|
||||
QObject::connect(ui_->results->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &AddPodcastDialog::ChangePodcast);
|
||||
ChangePodcast(QModelIndex());
|
||||
CurrentPageBusyChanged(page_is_busy_[index]);
|
||||
|
||||
page->Show();
|
||||
|
||||
}
|
||||
|
||||
void AddPodcastDialog::ChangePodcast(const QModelIndex ¤t) {
|
||||
|
||||
QVariant podcast_variant = current.data(PodcastDiscoveryModel::Role_Podcast);
|
||||
|
||||
// If the selected item is invalid or not a podcast, hide the details pane.
|
||||
if (podcast_variant.isNull()) {
|
||||
ui_->details_scroll_area->hide();
|
||||
add_button_->setEnabled(false);
|
||||
remove_button_->setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
current_podcast_ = podcast_variant.value<Podcast>();
|
||||
|
||||
// Start the blur+fade if there's already a podcast in the details pane.
|
||||
if (ui_->details_scroll_area->isVisible()) {
|
||||
fader_->StartBlur();
|
||||
}
|
||||
else {
|
||||
ui_->details_scroll_area->show();
|
||||
}
|
||||
|
||||
// Update the details pane
|
||||
ui_->details->SetPodcast(current_podcast_);
|
||||
|
||||
// Is the user already subscribed to this podcast?
|
||||
Podcast subscribed_podcast = app_->podcast_backend()->GetSubscriptionByUrl(current_podcast_.url());
|
||||
const bool is_subscribed = subscribed_podcast.url().isValid();
|
||||
|
||||
if (is_subscribed) {
|
||||
// Use the one from the database which will contain the ID.
|
||||
current_podcast_ = subscribed_podcast;
|
||||
}
|
||||
|
||||
add_button_->setEnabled(!is_subscribed);
|
||||
remove_button_->setEnabled(is_subscribed);
|
||||
|
||||
}
|
||||
|
||||
void AddPodcastDialog::PageBusyChanged(const bool busy) {
|
||||
|
||||
const int index = pages_.indexOf(qobject_cast<AddPodcastPage*>(sender()));
|
||||
if (index == -1) return;
|
||||
|
||||
page_is_busy_[index] = busy;
|
||||
|
||||
if (index == ui_->provider_list->currentRow()) {
|
||||
CurrentPageBusyChanged(busy);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void AddPodcastDialog::CurrentPageBusyChanged(const bool busy) {
|
||||
|
||||
ui_->results_stack->setCurrentWidget(busy ? ui_->busy_page : ui_->results_page);
|
||||
ui_->stack->setDisabled(busy);
|
||||
|
||||
QTimer::singleShot(0, this, &AddPodcastDialog::SelectFirstPodcast);
|
||||
|
||||
}
|
||||
|
||||
void AddPodcastDialog::SelectFirstPodcast() {
|
||||
|
||||
// Select the first item if there was one.
|
||||
const PodcastDiscoveryModel *model = pages_[ui_->provider_list->currentRow()]->model();
|
||||
if (model->rowCount() > 0) {
|
||||
ui_->results->selectionModel()->setCurrentIndex(model->index(0, 0), QItemSelectionModel::ClearAndSelect);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void AddPodcastDialog::AddPodcast() {
|
||||
|
||||
app_->podcast_backend()->Subscribe(¤t_podcast_);
|
||||
add_button_->setEnabled(false);
|
||||
remove_button_->setEnabled(true);
|
||||
|
||||
}
|
||||
|
||||
void AddPodcastDialog::PodcastDoubleClicked(const QModelIndex &idx) {
|
||||
|
||||
QVariant podcast_variant = idx.data(PodcastDiscoveryModel::Role_Podcast);
|
||||
if (podcast_variant.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
current_podcast_ = podcast_variant.value<Podcast>();
|
||||
app_->podcast_backend()->Subscribe(¤t_podcast_);
|
||||
|
||||
add_button_->setEnabled(false);
|
||||
remove_button_->setEnabled(true);
|
||||
|
||||
}
|
||||
|
||||
void AddPodcastDialog::RemovePodcast() {
|
||||
|
||||
app_->podcast_backend()->Unsubscribe(current_podcast_);
|
||||
current_podcast_.set_database_id(-1);
|
||||
add_button_->setEnabled(true);
|
||||
remove_button_->setEnabled(false);
|
||||
|
||||
}
|
||||
|
||||
void AddPodcastDialog::OpenSettingsPage() {
|
||||
//app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Podcasts);
|
||||
}
|
||||
|
||||
void AddPodcastDialog::OpenOPMLFile() {
|
||||
|
||||
const QString filename = QFileDialog::getOpenFileName(this, tr("Open OPML file"), last_opml_path_, "OPML files (*.opml)");
|
||||
|
||||
if (filename.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
last_opml_path_ = filename;
|
||||
|
||||
by_url_page_->SetUrlAndGo(QUrl::fromLocalFile(last_opml_path_));
|
||||
ChangePage(ui_->stack->indexOf(by_url_page_));
|
||||
|
||||
}
|
||||
91
src/podcasts/addpodcastdialog.h
Normal file
91
src/podcasts/addpodcastdialog.h
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 ADDPODCASTDIALOG_H
|
||||
#define ADDPODCASTDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include "podcast.h"
|
||||
|
||||
class Application;
|
||||
class AddPodcastByUrl;
|
||||
class AddPodcastPage;
|
||||
class OpmlContainer;
|
||||
class WidgetFadeHelper;
|
||||
class Ui_AddPodcastDialog;
|
||||
|
||||
class AddPodcastDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AddPodcastDialog(Application *app, QWidget *parent = nullptr);
|
||||
~AddPodcastDialog();
|
||||
|
||||
// Convenience methods that open the dialog at the Add By Url page and fill it with either a URL (which is then fetched), or a pre-fetched OPML container.
|
||||
void ShowWithUrl(const QUrl &url);
|
||||
void ShowWithOpml(const OpmlContainer &opml);
|
||||
|
||||
private slots:
|
||||
void OpenSettingsPage();
|
||||
void AddPodcast();
|
||||
void PodcastDoubleClicked(const QModelIndex &idx);
|
||||
void RemovePodcast();
|
||||
void ChangePage(const int index);
|
||||
void ChangePodcast(const QModelIndex ¤t);
|
||||
|
||||
void PageBusyChanged(const bool busy);
|
||||
void CurrentPageBusyChanged(const bool busy);
|
||||
|
||||
void SelectFirstPodcast();
|
||||
|
||||
void OpenOPMLFile();
|
||||
|
||||
private:
|
||||
void AddPage(AddPodcastPage *page);
|
||||
|
||||
private:
|
||||
static const char *kBbcOpmlUrl;
|
||||
static const char *kCbcOpmlUrl;
|
||||
|
||||
Application *app_;
|
||||
|
||||
Ui_AddPodcastDialog *ui_;
|
||||
QPushButton *add_button_;
|
||||
QPushButton *remove_button_;
|
||||
|
||||
QList<AddPodcastPage*> pages_;
|
||||
QList<bool> page_is_busy_;
|
||||
AddPodcastByUrl *by_url_page_;
|
||||
|
||||
WidgetFadeHelper *fader_;
|
||||
|
||||
Podcast current_podcast_;
|
||||
|
||||
QString last_opml_path_;
|
||||
};
|
||||
|
||||
#endif // ADDPODCASTDIALOG_H
|
||||
276
src/podcasts/addpodcastdialog.ui
Normal file
276
src/podcasts/addpodcastdialog.ui
Normal file
@@ -0,0 +1,276 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>AddPodcastDialog</class>
|
||||
<widget class="QDialog" name="AddPodcastDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>941</width>
|
||||
<height>473</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Add podcast</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QListWidget" name="provider_list">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="uniformItemSizes">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="stack">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="results_stack">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="results_page">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<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="AutoExpandingTreeView" name="results">
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="uniformRowHeights">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<attribute name="headerVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="busy_page">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<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>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>192</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="BusyIndicator" name="widget" native="true">
|
||||
<property name="text" stdset="0">
|
||||
<string>Loading...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QScrollArea" name="details_scroll_area">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>250</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>250</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="PodcastInfoWidget" name="details">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>248</width>
|
||||
<height>415</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="button_box">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Close</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>BusyIndicator</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>widgets/busyindicator.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>PodcastInfoWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>podcasts/podcastinfowidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>AutoExpandingTreeView</class>
|
||||
<extends>QTreeView</extends>
|
||||
<header>widgets/autoexpandingtreeview.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>button_box</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>AddPodcastDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>836</x>
|
||||
<y>463</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>button_box</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>AddPodcastDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>885</x>
|
||||
<y>463</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
36
src/podcasts/addpodcastpage.cpp
Normal file
36
src/podcasts/addpodcastpage.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 <QWidget>
|
||||
|
||||
#include "addpodcastpage.h"
|
||||
|
||||
#include "podcastdiscoverymodel.h"
|
||||
|
||||
AddPodcastPage::AddPodcastPage(Application *app, QWidget *parent)
|
||||
: QWidget(parent), model_(new PodcastDiscoveryModel(app, this)) {}
|
||||
|
||||
void AddPodcastPage::SetModel(PodcastDiscoveryModel *model) {
|
||||
delete model_;
|
||||
model_ = model;
|
||||
}
|
||||
53
src/podcasts/addpodcastpage.h
Normal file
53
src/podcasts/addpodcastpage.h
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 ADDPODCASTPAGE_H
|
||||
#define ADDPODCASTPAGE_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class Application;
|
||||
class PodcastDiscoveryModel;
|
||||
|
||||
class AddPodcastPage : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AddPodcastPage(Application *app, QWidget *parent = nullptr);
|
||||
|
||||
PodcastDiscoveryModel *model() const { return model_; }
|
||||
|
||||
virtual bool has_visible_widget() const { return true; }
|
||||
virtual void Show() {}
|
||||
|
||||
signals:
|
||||
void Busy(bool busy);
|
||||
|
||||
protected:
|
||||
void SetModel(PodcastDiscoveryModel *model);
|
||||
|
||||
private:
|
||||
PodcastDiscoveryModel *model_;
|
||||
};
|
||||
|
||||
#endif // ADDPODCASTPAGE_H
|
||||
49
src/podcasts/episodeinfowidget.cpp
Normal file
49
src/podcasts/episodeinfowidget.cpp
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
Copyright 2018, Jim Broadus <jbroadus@gmail.com>
|
||||
* Copyright 2019-2021, 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 <QWidget>
|
||||
|
||||
#include "core/utilities.h"
|
||||
|
||||
#include "episodeinfowidget.h"
|
||||
#include "ui_episodeinfowidget.h"
|
||||
|
||||
EpisodeInfoWidget::EpisodeInfoWidget(QWidget *parent)
|
||||
: QWidget(parent), ui_(new Ui_EpisodeInfoWidget), app_(nullptr) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
|
||||
}
|
||||
|
||||
EpisodeInfoWidget::~EpisodeInfoWidget() { delete ui_; }
|
||||
|
||||
void EpisodeInfoWidget::SetApplication(Application *app) { app_ = app; }
|
||||
|
||||
void EpisodeInfoWidget::SetEpisode(const PodcastEpisode &episode) {
|
||||
|
||||
episode_ = episode;
|
||||
ui_->title->setText(episode.title());
|
||||
ui_->description->setText(episode.description());
|
||||
ui_->author->setText(episode.author());
|
||||
ui_->date->setText(episode.publication_date().toString("d MMMM yyyy"));
|
||||
ui_->duration->setText(Utilities::PrettyTime(episode.duration_secs()));
|
||||
|
||||
}
|
||||
50
src/podcasts/episodeinfowidget.h
Normal file
50
src/podcasts/episodeinfowidget.h
Normal file
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
Copyright 2018, Jim Broadus <jbroadus@gmail.com>
|
||||
* Copyright 2019-2021, 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 EPISODEINFOWIDGET_H
|
||||
#define EPISODEINFOWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "podcastepisode.h"
|
||||
|
||||
class Application;
|
||||
class Ui_EpisodeInfoWidget;
|
||||
|
||||
class EpisodeInfoWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit EpisodeInfoWidget(QWidget *parent = nullptr);
|
||||
~EpisodeInfoWidget();
|
||||
|
||||
void SetApplication(Application *app);
|
||||
|
||||
void SetEpisode(const PodcastEpisode &episode);
|
||||
|
||||
private:
|
||||
Ui_EpisodeInfoWidget *ui_;
|
||||
|
||||
Application *app_;
|
||||
PodcastEpisode episode_;
|
||||
};
|
||||
|
||||
#endif // EPISODEINFOWIDGET_H
|
||||
137
src/podcasts/episodeinfowidget.ui
Normal file
137
src/podcasts/episodeinfowidget.ui
Normal file
@@ -0,0 +1,137 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>EpisodeInfoWidget</class>
|
||||
<widget class="QWidget" name="EpisodeInfoWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>398</width>
|
||||
<height>551</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#description {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
QLineEdit {
|
||||
background: transparent;
|
||||
}</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinAndMaxSize</enum>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="title">
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="description">
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinAndMaxSize</enum>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="author_label">
|
||||
<property name="text">
|
||||
<string>Author</string>
|
||||
</property>
|
||||
<property name="field_label" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="date">
|
||||
<property name="frame">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="author">
|
||||
<property name="frame">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="date_label">
|
||||
<property name="text">
|
||||
<string>Date</string>
|
||||
</property>
|
||||
<property name="field_label" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="duration_label">
|
||||
<property name="text">
|
||||
<string>Duration</string>
|
||||
</property>
|
||||
<property name="field_label" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="duration">
|
||||
<property name="frame">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
82
src/podcasts/fixedopmlpage.cpp
Normal file
82
src/podcasts/fixedopmlpage.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 <QString>
|
||||
#include <QUrl>
|
||||
#include <QIcon>
|
||||
#include <QMessageBox>
|
||||
|
||||
#include "podcastdiscoverymodel.h"
|
||||
#include "podcasturlloader.h"
|
||||
|
||||
#include "fixedopmlpage.h"
|
||||
|
||||
FixedOpmlPage::FixedOpmlPage(const QUrl &opml_url, const QString &title, const QIcon &icon, Application *app, QWidget *parent)
|
||||
: AddPodcastPage(app, parent),
|
||||
loader_(new PodcastUrlLoader(this)),
|
||||
opml_url_(opml_url),
|
||||
done_initial_load_(false) {
|
||||
|
||||
setWindowTitle(title);
|
||||
setWindowIcon(icon);
|
||||
|
||||
}
|
||||
|
||||
void FixedOpmlPage::Show() {
|
||||
|
||||
if (!done_initial_load_) {
|
||||
emit Busy(true);
|
||||
done_initial_load_ = true;
|
||||
|
||||
PodcastUrlLoaderReply *reply = loader_->Load(opml_url_);
|
||||
QObject::connect(reply, &PodcastUrlLoaderReply::Finished, this, [this, reply]() { LoadFinished(reply); });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void FixedOpmlPage::LoadFinished(PodcastUrlLoaderReply *reply) {
|
||||
|
||||
reply->deleteLater();
|
||||
emit Busy(false);
|
||||
|
||||
if (!reply->is_success()) {
|
||||
QMessageBox::warning(this, tr("Failed to load podcast"), reply->error_text(), QMessageBox::Close);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (reply->result_type()) {
|
||||
case PodcastUrlLoaderReply::Type_Podcast:{
|
||||
const PodcastList podcasts = reply->podcast_results();
|
||||
for (const Podcast &podcast : podcasts) {
|
||||
model()->appendRow(model()->CreatePodcastItem(podcast));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case PodcastUrlLoaderReply::Type_Opml:
|
||||
model()->CreateOpmlContainerItems(reply->opml_results(), model()->invisibleRootItem());
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
56
src/podcasts/fixedopmlpage.h
Normal file
56
src/podcasts/fixedopmlpage.h
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 FIXEDOPMLPAGE_H
|
||||
#define FIXEDOPMLPAGE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
#include <QIcon>
|
||||
|
||||
#include "addpodcastpage.h"
|
||||
|
||||
class Application;
|
||||
class PodcastUrlLoader;
|
||||
class PodcastUrlLoaderReply;
|
||||
|
||||
class FixedOpmlPage : public AddPodcastPage {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
FixedOpmlPage(const QUrl &opml_url, const QString &title, const QIcon &icon, Application *app, QWidget *parent = nullptr);
|
||||
|
||||
bool has_visible_widget() const { return false; }
|
||||
void Show();
|
||||
|
||||
private slots:
|
||||
void LoadFinished(PodcastUrlLoaderReply *reply);
|
||||
|
||||
private:
|
||||
PodcastUrlLoader *loader_;
|
||||
QUrl opml_url_;
|
||||
|
||||
bool done_initial_load_;
|
||||
};
|
||||
|
||||
#endif // FIXEDOPMLPAGE_H
|
||||
100
src/podcasts/gpoddersearchpage.cpp
Normal file
100
src/podcasts/gpoddersearchpage.cpp
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 <QMessageBox>
|
||||
#include <QPushButton>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/networkaccessmanager.h"
|
||||
#include "podcast.h"
|
||||
#include "podcastdiscoverymodel.h"
|
||||
|
||||
#include "gpoddersearchpage.h"
|
||||
#include "ui_gpoddersearchpage.h"
|
||||
|
||||
GPodderSearchPage::GPodderSearchPage(Application *app, QWidget *parent)
|
||||
: AddPodcastPage(app, parent),
|
||||
ui_(new Ui_GPodderSearchPage),
|
||||
network_(new NetworkAccessManager(this)),
|
||||
api_(new mygpo::ApiRequest(network_)) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
QObject::connect(ui_->search, &QPushButton::clicked, this, &GPodderSearchPage::SearchClicked);
|
||||
setWindowIcon(IconLoader::Load("mygpo"));
|
||||
|
||||
}
|
||||
|
||||
GPodderSearchPage::~GPodderSearchPage() {
|
||||
|
||||
delete ui_;
|
||||
delete api_;
|
||||
|
||||
}
|
||||
|
||||
void GPodderSearchPage::SearchClicked() {
|
||||
|
||||
emit Busy(true);
|
||||
|
||||
mygpo::PodcastListPtr list(api_->search(ui_->query->text()));
|
||||
QObject::connect(list.data(), &mygpo::PodcastList::finished, this, [this, list]() { SearchFinished(list); });
|
||||
QObject::connect(list.data(), &mygpo::PodcastList::parseError, this, [this, list]() { SearchFailed(list); });
|
||||
QObject::connect(list.data(), &mygpo::PodcastList::requestError, this, [this, list]() { SearchFailed(list); });
|
||||
|
||||
}
|
||||
|
||||
void GPodderSearchPage::SearchFinished(mygpo::PodcastListPtr list) {
|
||||
|
||||
emit Busy(false);
|
||||
|
||||
model()->clear();
|
||||
|
||||
for (mygpo::PodcastPtr gpo_podcast : list->list()) {
|
||||
Podcast podcast;
|
||||
podcast.InitFromGpo(gpo_podcast.data());
|
||||
|
||||
model()->appendRow(model()->CreatePodcastItem(podcast));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GPodderSearchPage::SearchFailed(mygpo::PodcastListPtr list) {
|
||||
|
||||
emit Busy(false);
|
||||
|
||||
model()->clear();
|
||||
|
||||
if (QMessageBox::warning(
|
||||
nullptr, tr("Failed to fetch podcasts"),
|
||||
tr("There was a problem communicating with gpodder.net"),
|
||||
QMessageBox::Retry | QMessageBox::Close,
|
||||
QMessageBox::Retry) != QMessageBox::Retry) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try doing the search again.
|
||||
SearchClicked();
|
||||
|
||||
}
|
||||
|
||||
void GPodderSearchPage::Show() { ui_->query->setFocus(); }
|
||||
57
src/podcasts/gpoddersearchpage.h
Normal file
57
src/podcasts/gpoddersearchpage.h
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 GPODDERSEARCHPAGE_H
|
||||
#define GPODDERSEARCHPAGE_H
|
||||
|
||||
#include <ApiRequest.h>
|
||||
|
||||
#include "addpodcastpage.h"
|
||||
|
||||
class QNetworkAccessManager;
|
||||
|
||||
class Application;
|
||||
class Ui_GPodderSearchPage;
|
||||
|
||||
class GPodderSearchPage : public AddPodcastPage {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GPodderSearchPage(Application *app, QWidget *parent = nullptr);
|
||||
~GPodderSearchPage();
|
||||
|
||||
void Show();
|
||||
|
||||
private slots:
|
||||
void SearchClicked();
|
||||
void SearchFinished(mygpo::PodcastListPtr list);
|
||||
void SearchFailed(mygpo::PodcastListPtr list);
|
||||
|
||||
private:
|
||||
Ui_GPodderSearchPage *ui_;
|
||||
|
||||
QNetworkAccessManager *network_;
|
||||
mygpo::ApiRequest *api_;
|
||||
};
|
||||
|
||||
#endif // GPODDERSEARCHPAGE_H
|
||||
61
src/podcasts/gpoddersearchpage.ui
Normal file
61
src/podcasts/gpoddersearchpage.ui
Normal file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>GPodderSearchPage</class>
|
||||
<widget class="QWidget" name="GPodderSearchPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>538</width>
|
||||
<height>69</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Search gpodder.net</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Enter search terms below to find podcasts on gpodder.net</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="query"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="search">
|
||||
<property name="text">
|
||||
<string>Search</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>query</sender>
|
||||
<signal>returnPressed()</signal>
|
||||
<receiver>search</receiver>
|
||||
<slot>click()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>130</x>
|
||||
<y>45</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>198</x>
|
||||
<y>46</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
415
src/podcasts/gpoddersync.cpp
Normal file
415
src/podcasts/gpoddersync.cpp
Normal file
@@ -0,0 +1,415 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 <QCoreApplication>
|
||||
#include <QSet>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QHostInfo>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkCookieJar>
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/networkaccessmanager.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "core/utilities.h"
|
||||
#include "podcastbackend.h"
|
||||
#include "podcasturlloader.h"
|
||||
#include "gpoddersync.h"
|
||||
|
||||
const char *GPodderSync::kSettingsGroup = "Podcasts";
|
||||
const int GPodderSync::kFlushUpdateQueueDelay = 30 * kMsecPerSec; // 30 seconds
|
||||
const int GPodderSync::kGetUpdatesInterval = 30 * 60 * kMsecPerSec; // 30 minutes
|
||||
const int GPodderSync::kRequestTimeout = 30 * kMsecPerSec; // 30 seconds
|
||||
|
||||
GPodderSync::GPodderSync(Application *app, QObject *parent)
|
||||
: QObject(parent),
|
||||
app_(app),
|
||||
network_(new NetworkAccessManager(this)),
|
||||
backend_(app_->podcast_backend()),
|
||||
loader_(new PodcastUrlLoader(this)),
|
||||
get_updates_timer_(new QTimer(this)),
|
||||
flush_queue_timer_(new QTimer(this)),
|
||||
flushing_queue_(false) {
|
||||
|
||||
ReloadSettings();
|
||||
LoadQueue();
|
||||
|
||||
QObject::connect(app_, &Application::SettingsChanged, this, &GPodderSync::ReloadSettings);
|
||||
QObject::connect(backend_, &PodcastBackend::SubscriptionAdded, this, &GPodderSync::SubscriptionAdded);
|
||||
QObject::connect(backend_, &PodcastBackend::SubscriptionRemoved, this, &GPodderSync::SubscriptionRemoved);
|
||||
|
||||
get_updates_timer_->setInterval(kGetUpdatesInterval);
|
||||
connect(get_updates_timer_, &QTimer::timeout, this, &GPodderSync::GetUpdatesNow);
|
||||
|
||||
flush_queue_timer_->setInterval(kFlushUpdateQueueDelay);
|
||||
flush_queue_timer_->setSingleShot(true);
|
||||
QObject::connect(flush_queue_timer_, &QTimer::timeout, this, &GPodderSync::FlushUpdateQueue);
|
||||
|
||||
if (is_logged_in()) {
|
||||
GetUpdatesNow();
|
||||
flush_queue_timer_->start();
|
||||
get_updates_timer_->start();
|
||||
}
|
||||
}
|
||||
|
||||
GPodderSync::~GPodderSync() {}
|
||||
|
||||
QString GPodderSync::DeviceId() {
|
||||
|
||||
return QString("%1-%2").arg(qApp->applicationName(), QHostInfo::localHostName()).toLower();
|
||||
|
||||
}
|
||||
|
||||
QString GPodderSync::DefaultDeviceName() {
|
||||
return tr("%1 on %2").arg(qApp->applicationName(), QHostInfo::localHostName());
|
||||
}
|
||||
|
||||
bool GPodderSync::is_logged_in() const {
|
||||
return !username_.isEmpty() && !password_.isEmpty() && api_;
|
||||
}
|
||||
|
||||
void GPodderSync::ReloadSettings() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
|
||||
username_ = s.value("gpodder_username").toString();
|
||||
password_ = s.value("gpodder_password").toString();
|
||||
last_successful_get_ = s.value("gpodder_last_get").toDateTime();
|
||||
|
||||
s.endGroup();
|
||||
|
||||
if (!username_.isEmpty() && !password_.isEmpty()) {
|
||||
api_.reset(new mygpo::ApiRequest(username_, password_, network_));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GPodderSync::Login(const QString &username, const QString &password, const QString &device_name) {
|
||||
|
||||
api_.reset(new mygpo::ApiRequest(username, password, network_));
|
||||
|
||||
QNetworkReply *reply = api_->renameDevice(username, DeviceId(), device_name, mygpo::Device::DESKTOP);
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, username, password]() { LoginFinished(reply, username, password); });
|
||||
|
||||
}
|
||||
|
||||
void GPodderSync::LoginFinished(QNetworkReply *reply, const QString &username, const QString &password) {
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
username_ = username;
|
||||
password_ = password;
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue("gpodder_username", username);
|
||||
s.setValue("gpodder_password", password);
|
||||
s.endGroup();
|
||||
|
||||
DoInitialSync();
|
||||
emit LoginSuccess();
|
||||
}
|
||||
else {
|
||||
api_.reset();
|
||||
emit LoginFailure(reply->errorString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GPodderSync::Logout() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.remove("gpodder_username");
|
||||
s.remove("gpodder_password");
|
||||
s.remove("gpodder_last_get");
|
||||
s.endGroup();
|
||||
|
||||
api_.reset();
|
||||
|
||||
// Remove session cookies. QNetworkAccessManager takes ownership of the new object and frees the previous.
|
||||
network_->setCookieJar(new QNetworkCookieJar());
|
||||
|
||||
}
|
||||
|
||||
void GPodderSync::GetUpdatesNow() {
|
||||
|
||||
if (!is_logged_in()) return;
|
||||
|
||||
qlonglong timestamp = 0;
|
||||
if (last_successful_get_.isValid()) {
|
||||
timestamp = last_successful_get_.toSecsSinceEpoch();
|
||||
}
|
||||
|
||||
mygpo::DeviceUpdatesPtr reply(api_->deviceUpdates(username_, DeviceId(), timestamp));
|
||||
QObject::connect(reply.data(), &mygpo::DeviceUpdates::finished, this, [this, reply]() { DeviceUpdatesFinished(reply); });
|
||||
QObject::connect(reply.data(), &mygpo::DeviceUpdates::parseError, this, &GPodderSync::DeviceUpdatesParseError);
|
||||
QObject::connect(reply.data(), &mygpo::DeviceUpdates::requestError, this, &GPodderSync::DeviceUpdatesRequestError);
|
||||
|
||||
}
|
||||
|
||||
void GPodderSync::DeviceUpdatesParseError() {
|
||||
qLog(Warning) << "Failed to get gpodder device updates: parse error";
|
||||
}
|
||||
|
||||
void GPodderSync::DeviceUpdatesRequestError(QNetworkReply::NetworkError error) {
|
||||
qLog(Warning) << "Failed to get gpodder device updates:" << error;
|
||||
}
|
||||
|
||||
void GPodderSync::DeviceUpdatesFinished(mygpo::DeviceUpdatesPtr reply) {
|
||||
|
||||
// Remember episode actions for each podcast, so when we add a new podcast
|
||||
// we can apply the actions immediately.
|
||||
QMap<QUrl, QList<mygpo::EpisodePtr>> episodes_by_podcast;
|
||||
for (mygpo::EpisodePtr episode : reply->updateList()) {
|
||||
episodes_by_podcast[episode->podcastUrl()].append(episode);
|
||||
}
|
||||
|
||||
for (mygpo::PodcastPtr podcast : reply->addList()) {
|
||||
const QUrl url(podcast->url());
|
||||
|
||||
// Are we subscribed to this podcast already?
|
||||
Podcast existing_podcast = backend_->GetSubscriptionByUrl(url);
|
||||
if (existing_podcast.is_valid()) {
|
||||
// Just apply actions to this existing podcast
|
||||
ApplyActions(episodes_by_podcast[url], existing_podcast.mutable_episodes());
|
||||
backend_->UpdateEpisodes(existing_podcast.episodes());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start loading the podcast. Remember actions and apply them after we have a list of the episodes.
|
||||
PodcastUrlLoaderReply *loader_reply = loader_->Load(url);
|
||||
QObject::connect(loader_reply, &PodcastUrlLoaderReply::Finished, this, [this, loader_reply, url, episodes_by_podcast]() { NewPodcastLoaded(loader_reply, url, episodes_by_podcast[url]); });
|
||||
}
|
||||
|
||||
// Unsubscribe from podcasts that were removed.
|
||||
for (const QUrl &url : reply->removeList()) {
|
||||
backend_->Unsubscribe(backend_->GetSubscriptionByUrl(url));
|
||||
}
|
||||
|
||||
last_successful_get_ = QDateTime::currentDateTime();
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue("gpodder_last_get", last_successful_get_);
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void GPodderSync::NewPodcastLoaded(PodcastUrlLoaderReply *reply, const QUrl &url, const QList<mygpo::EpisodePtr> &actions) {
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (!reply->is_success()) {
|
||||
qLog(Warning) << "Error fetching podcast at" << url << ":" << reply->error_text();
|
||||
return;
|
||||
}
|
||||
|
||||
if (reply->result_type() != PodcastUrlLoaderReply::Type_Podcast) {
|
||||
qLog(Warning) << "The URL" << url << "no longer contains a podcast";
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply the actions to the episodes in the podcast.
|
||||
for (Podcast podcast : reply->podcast_results()) {
|
||||
ApplyActions(actions, podcast.mutable_episodes());
|
||||
|
||||
// Add the subscription
|
||||
backend_->Subscribe(&podcast);
|
||||
}
|
||||
}
|
||||
|
||||
void GPodderSync::ApplyActions(const QList<QSharedPointer<mygpo::Episode>> &actions, PodcastEpisodeList *episodes) {
|
||||
|
||||
for (PodcastEpisodeList::iterator it = episodes->begin(); it != episodes->end(); ++it) {
|
||||
// Find an action for this episode
|
||||
for (mygpo::EpisodePtr action : actions) {
|
||||
if (action->url() != it->url()) continue;
|
||||
|
||||
switch (action->status()) {
|
||||
case mygpo::Episode::PLAY:
|
||||
case mygpo::Episode::DOWNLOAD:
|
||||
it->set_listened(true);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GPodderSync::SubscriptionAdded(const Podcast &podcast) {
|
||||
|
||||
if (!is_logged_in()) return;
|
||||
|
||||
const QUrl &url = podcast.url();
|
||||
|
||||
queued_remove_subscriptions_.remove(url);
|
||||
queued_add_subscriptions_.insert(url);
|
||||
|
||||
SaveQueue();
|
||||
flush_queue_timer_->start();
|
||||
|
||||
}
|
||||
|
||||
void GPodderSync::SubscriptionRemoved(const Podcast &podcast) {
|
||||
|
||||
if (!is_logged_in()) return;
|
||||
|
||||
const QUrl &url = podcast.url();
|
||||
|
||||
queued_remove_subscriptions_.insert(url);
|
||||
queued_add_subscriptions_.remove(url);
|
||||
|
||||
SaveQueue();
|
||||
flush_queue_timer_->start();
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
template<typename T>
|
||||
void WriteContainer(const T &container, QSettings *s, const char *array_name, const char *item_name) {
|
||||
|
||||
s->beginWriteArray(array_name, container.count());
|
||||
int index = 0;
|
||||
for (const auto &item : container) {
|
||||
s->setArrayIndex(index++);
|
||||
s->setValue(item_name, item);
|
||||
}
|
||||
s->endArray();
|
||||
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void ReadContainer(T *container, QSettings *s, const char *array_name, const char *item_name) {
|
||||
|
||||
container->clear();
|
||||
const int count = s->beginReadArray(array_name);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
s->setArrayIndex(i);
|
||||
*container << s->value(item_name).value<typename T::value_type>();
|
||||
}
|
||||
s->endArray();
|
||||
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void GPodderSync::SaveQueue() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
WriteContainer(queued_add_subscriptions_, &s, "gpodder_queued_add_subscriptions", "url");
|
||||
WriteContainer(queued_remove_subscriptions_, &s, "gpodder_queued_remove_subscriptions", "url");
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void GPodderSync::LoadQueue() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
ReadContainer(&queued_add_subscriptions_, &s, "gpodder_queued_add_subscriptions", "url");
|
||||
ReadContainer(&queued_remove_subscriptions_, &s, "gpodder_queued_remove_subscriptions", "url");
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void GPodderSync::FlushUpdateQueue() {
|
||||
|
||||
if (!is_logged_in() || flushing_queue_) return;
|
||||
|
||||
QSet<QUrl> all_urls = queued_add_subscriptions_ + queued_remove_subscriptions_;
|
||||
if (all_urls.isEmpty()) return;
|
||||
|
||||
flushing_queue_ = true;
|
||||
mygpo::AddRemoveResultPtr reply(api_->addRemoveSubscriptions(username_, DeviceId(), queued_add_subscriptions_.values(), queued_remove_subscriptions_.values()));
|
||||
|
||||
qLog(Info) << "Sending" << all_urls.count() << "changes to gpodder.net";
|
||||
|
||||
QObject::connect(reply.data(), &mygpo::AddRemoveResult::finished, this, [this, all_urls]() { AddRemoveFinished(all_urls.values()); });
|
||||
QObject::connect(reply.data(), &mygpo::AddRemoveResult::parseError, this, &GPodderSync::AddRemoveParseError);
|
||||
QObject::connect(reply.data(), &mygpo::AddRemoveResult::requestError, this, &GPodderSync::AddRemoveRequestError);
|
||||
}
|
||||
|
||||
void GPodderSync::AddRemoveParseError() {
|
||||
|
||||
flushing_queue_ = false;
|
||||
qLog(Warning) << "Failed to update gpodder subscriptions: parse error";
|
||||
|
||||
}
|
||||
|
||||
void GPodderSync::AddRemoveRequestError(QNetworkReply::NetworkError err) {
|
||||
|
||||
flushing_queue_ = false;
|
||||
qLog(Warning) << "Failed to update gpodder subscriptions:" << err;
|
||||
|
||||
}
|
||||
|
||||
void GPodderSync::AddRemoveFinished(const QList<QUrl> &affected_urls) {
|
||||
|
||||
flushing_queue_ = false;
|
||||
|
||||
// Remove the URLs from the queue.
|
||||
for (const QUrl &url : affected_urls) {
|
||||
queued_add_subscriptions_.remove(url);
|
||||
queued_remove_subscriptions_.remove(url);
|
||||
}
|
||||
|
||||
SaveQueue();
|
||||
|
||||
// Did more change in the mean time?
|
||||
if (!queued_add_subscriptions_.isEmpty() ||
|
||||
!queued_remove_subscriptions_.isEmpty()) {
|
||||
flush_queue_timer_->start();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GPodderSync::DoInitialSync() {
|
||||
|
||||
// Get updates from the server
|
||||
GetUpdatesNow();
|
||||
get_updates_timer_->start();
|
||||
|
||||
// Send our complete list of subscriptions
|
||||
queued_remove_subscriptions_.clear();
|
||||
queued_add_subscriptions_.clear();
|
||||
for (const Podcast &podcast : backend_->GetAllSubscriptions()) {
|
||||
queued_add_subscriptions_.insert(podcast.url());
|
||||
}
|
||||
|
||||
SaveQueue();
|
||||
FlushUpdateQueue();
|
||||
|
||||
}
|
||||
125
src/podcasts/gpoddersync.h
Normal file
125
src/podcasts/gpoddersync.h
Normal file
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 GPODDERSYNC_H
|
||||
#define GPODDERSYNC_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QScopedPointer>
|
||||
#include <QSet>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QDateTime>
|
||||
#include <QUrl>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include <ApiRequest.h>
|
||||
|
||||
#include "podcastepisode.h"
|
||||
|
||||
class QTimer;
|
||||
|
||||
class Application;
|
||||
class NetworkAccessManager;
|
||||
class Podcast;
|
||||
class PodcastBackend;
|
||||
class PodcastUrlLoader;
|
||||
class PodcastUrlLoaderReply;
|
||||
|
||||
class GPodderSync : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GPodderSync(Application *app, QObject *parent = nullptr);
|
||||
~GPodderSync();
|
||||
|
||||
static const char *kSettingsGroup;
|
||||
static const int kFlushUpdateQueueDelay;
|
||||
static const int kGetUpdatesInterval;
|
||||
static const int kRequestTimeout;
|
||||
|
||||
static QString DefaultDeviceName();
|
||||
static QString DeviceId();
|
||||
|
||||
bool is_logged_in() const;
|
||||
|
||||
// Tries to login using the given username and password. Also sets the device name and type on gpodder.net.
|
||||
// If login succeeds the username and password will be saved in QSettings.
|
||||
void Login(const QString &username, const QString &password, const QString &device_name);
|
||||
|
||||
// Clears any saved username and password from QSettings.
|
||||
void Logout();
|
||||
|
||||
signals:
|
||||
void LoginSuccess();
|
||||
void LoginFailure(const QString &error);
|
||||
|
||||
public slots:
|
||||
void GetUpdatesNow();
|
||||
|
||||
private slots:
|
||||
void ReloadSettings();
|
||||
void LoginFinished(QNetworkReply *reply, const QString &username, const QString &password);
|
||||
|
||||
void DeviceUpdatesFinished(mygpo::DeviceUpdatesPtr reply);
|
||||
void DeviceUpdatesParseError();
|
||||
void DeviceUpdatesRequestError(QNetworkReply::NetworkError error);
|
||||
|
||||
void NewPodcastLoaded(PodcastUrlLoaderReply *reply, const QUrl &url, const QList<mygpo::EpisodePtr> &actions);
|
||||
|
||||
void ApplyActions(const QList<mygpo::EpisodePtr> &actions, PodcastEpisodeList *episodes);
|
||||
|
||||
void SubscriptionAdded(const Podcast &podcast);
|
||||
void SubscriptionRemoved(const Podcast &podcast);
|
||||
void FlushUpdateQueue();
|
||||
|
||||
void AddRemoveFinished(const QList<QUrl> &affected_urls);
|
||||
void AddRemoveParseError();
|
||||
void AddRemoveRequestError(QNetworkReply::NetworkError error);
|
||||
|
||||
private:
|
||||
void LoadQueue();
|
||||
void SaveQueue();
|
||||
|
||||
void DoInitialSync();
|
||||
|
||||
private:
|
||||
Application *app_;
|
||||
NetworkAccessManager *network_;
|
||||
QScopedPointer<mygpo::ApiRequest> api_;
|
||||
|
||||
PodcastBackend *backend_;
|
||||
PodcastUrlLoader *loader_;
|
||||
|
||||
QString username_;
|
||||
QString password_;
|
||||
QDateTime last_successful_get_;
|
||||
QTimer *get_updates_timer_;
|
||||
|
||||
QTimer *flush_queue_timer_;
|
||||
QSet<QUrl> queued_add_subscriptions_;
|
||||
QSet<QUrl> queued_remove_subscriptions_;
|
||||
bool flushing_queue_;
|
||||
};
|
||||
|
||||
#endif // GPODDERSYNC_H
|
||||
115
src/podcasts/gpoddertoptagsmodel.cpp
Normal file
115
src/podcasts/gpoddertoptagsmodel.cpp
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 <QMessageBox>
|
||||
|
||||
#include <ApiRequest.h>
|
||||
|
||||
#include "core/application.h"
|
||||
|
||||
#include "gpoddertoptagsmodel.h"
|
||||
#include "gpoddertoptagspage.h"
|
||||
#include "podcast.h"
|
||||
|
||||
GPodderTopTagsModel::GPodderTopTagsModel(mygpo::ApiRequest *api, Application *app, QObject *parent)
|
||||
: PodcastDiscoveryModel(app, parent), api_(api) {}
|
||||
|
||||
bool GPodderTopTagsModel::hasChildren(const QModelIndex &parent) const {
|
||||
|
||||
if (parent.isValid() && parent.data(Role_Type).toInt() == Type_Folder) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return PodcastDiscoveryModel::hasChildren(parent);
|
||||
|
||||
}
|
||||
|
||||
bool GPodderTopTagsModel::canFetchMore(const QModelIndex &parent) const {
|
||||
|
||||
if (parent.isValid() && parent.data(Role_Type).toInt() == Type_Folder &&
|
||||
!parent.data(Role_HasLazyLoaded).toBool()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return PodcastDiscoveryModel::canFetchMore(parent);
|
||||
|
||||
}
|
||||
|
||||
void GPodderTopTagsModel::fetchMore(const QModelIndex &parent) {
|
||||
|
||||
if (!parent.isValid() || parent.data(Role_Type).toInt() != Type_Folder ||
|
||||
parent.data(Role_HasLazyLoaded).toBool()) {
|
||||
return;
|
||||
}
|
||||
setData(parent, true, Role_HasLazyLoaded);
|
||||
|
||||
// Create a little Loading... item.
|
||||
itemFromIndex(parent)->appendRow(CreateLoadingIndicator());
|
||||
|
||||
mygpo::PodcastListPtr list(api_->podcastsOfTag(GPodderTopTagsPage::kMaxTagCount, parent.data().toString()));
|
||||
|
||||
QObject::connect(list.get(), &mygpo::PodcastList::finished, this, [this, parent, list]() { PodcastsOfTagFinished(parent, list.data()); });
|
||||
QObject::connect(list.get(), &mygpo::PodcastList::parseError, this, [this, parent, list]() { PodcastsOfTagFailed(parent, list.data()); });
|
||||
QObject::connect(list.get(), &mygpo::PodcastList::requestError, this, [this, parent, list]() { PodcastsOfTagFailed(parent, list.data()); });
|
||||
|
||||
}
|
||||
|
||||
void GPodderTopTagsModel::PodcastsOfTagFinished(const QModelIndex &parent, mygpo::PodcastList *list) {
|
||||
|
||||
QStandardItem *parent_item = itemFromIndex(parent);
|
||||
if (!parent_item) return;
|
||||
|
||||
// Remove the Loading... item.
|
||||
while (parent_item->hasChildren()) {
|
||||
parent_item->removeRow(0);
|
||||
}
|
||||
|
||||
for (mygpo::PodcastPtr gpo_podcast : list->list()) {
|
||||
Podcast podcast;
|
||||
podcast.InitFromGpo(gpo_podcast.data());
|
||||
|
||||
parent_item->appendRow(CreatePodcastItem(podcast));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GPodderTopTagsModel::PodcastsOfTagFailed(const QModelIndex &parent, mygpo::PodcastList*) {
|
||||
|
||||
QStandardItem *parent_item = itemFromIndex(parent);
|
||||
if (!parent_item) return;
|
||||
|
||||
// Remove the Loading... item.
|
||||
while (parent_item->hasChildren()) {
|
||||
parent_item->removeRow(0);
|
||||
}
|
||||
|
||||
if (QMessageBox::warning(nullptr, tr("Failed to fetch podcasts"), tr("There was a problem communicating with gpodder.net"), QMessageBox::Retry | QMessageBox::Close, QMessageBox::Retry) != QMessageBox::Retry) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try fetching the list again.
|
||||
setData(parent, false, Role_HasLazyLoaded);
|
||||
fetchMore(parent);
|
||||
|
||||
}
|
||||
61
src/podcasts/gpoddertoptagsmodel.h
Normal file
61
src/podcasts/gpoddertoptagsmodel.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 GPODDERTOPTAGSMODEL_H
|
||||
#define GPODDERTOPTAGSMODEL_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "podcastdiscoverymodel.h"
|
||||
|
||||
namespace mygpo {
|
||||
class ApiRequest;
|
||||
class PodcastList;
|
||||
} // namespace mygpo
|
||||
|
||||
class Application;
|
||||
|
||||
class GPodderTopTagsModel : public PodcastDiscoveryModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
GPodderTopTagsModel(mygpo::ApiRequest *api, Application *app, QObject *parent = nullptr);
|
||||
|
||||
enum Role {
|
||||
Role_HasLazyLoaded = PodcastDiscoveryModel::RoleCount,
|
||||
RoleCount
|
||||
};
|
||||
|
||||
bool hasChildren(const QModelIndex &parent) const;
|
||||
bool canFetchMore(const QModelIndex &parent) const;
|
||||
void fetchMore(const QModelIndex &parent);
|
||||
|
||||
private slots:
|
||||
void PodcastsOfTagFinished(const QModelIndex &parent, mygpo::PodcastList *list);
|
||||
void PodcastsOfTagFailed(const QModelIndex &parent, mygpo::PodcastList *list);
|
||||
|
||||
private:
|
||||
mygpo::ApiRequest *api_;
|
||||
};
|
||||
|
||||
#endif // GPODDERTOPTAGSMODEL_H
|
||||
93
src/podcasts/gpoddertoptagspage.cpp
Normal file
93
src/podcasts/gpoddertoptagspage.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 <QMessageBox>
|
||||
|
||||
#include "TagList.h"
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/networkaccessmanager.h"
|
||||
#include "core/iconloader.h"
|
||||
|
||||
#include "gpoddertoptagsmodel.h"
|
||||
#include "gpoddertoptagspage.h"
|
||||
|
||||
const int GPodderTopTagsPage::kMaxTagCount = 100;
|
||||
|
||||
GPodderTopTagsPage::GPodderTopTagsPage(Application *app, QWidget *parent)
|
||||
: AddPodcastPage(app, parent),
|
||||
network_(new NetworkAccessManager(this)),
|
||||
api_(new mygpo::ApiRequest(network_)),
|
||||
done_initial_load_(false) {
|
||||
|
||||
setWindowTitle(tr("gpodder.net directory"));
|
||||
setWindowIcon(IconLoader::Load("mygpo"));
|
||||
|
||||
SetModel(new GPodderTopTagsModel(api_, app, this));
|
||||
|
||||
}
|
||||
|
||||
GPodderTopTagsPage::~GPodderTopTagsPage() { delete api_; }
|
||||
|
||||
void GPodderTopTagsPage::Show() {
|
||||
|
||||
if (!done_initial_load_) {
|
||||
// Start the request for list of top-level tags
|
||||
emit Busy(true);
|
||||
done_initial_load_ = true;
|
||||
|
||||
mygpo::TagListPtr tag_list(api_->topTags(kMaxTagCount));
|
||||
QObject::connect(tag_list.get(), &mygpo::TagList::finished, this, [this, tag_list]() { TagListLoaded(tag_list); });
|
||||
QObject::connect(tag_list.get(), &mygpo::TagList::parseError, this, [this]() { TagListFailed(); });
|
||||
QObject::connect(tag_list.get(), &mygpo::TagList::requestError, this, [this]() { TagListFailed(); });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GPodderTopTagsPage::TagListLoaded(mygpo::TagListPtr tag_list) {
|
||||
|
||||
emit Busy(false);
|
||||
|
||||
for (mygpo::TagPtr tag : tag_list->list()) {
|
||||
model()->appendRow(model()->CreateFolder(tag->tag()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GPodderTopTagsPage::TagListFailed() {
|
||||
|
||||
emit Busy(false);
|
||||
done_initial_load_ = false;
|
||||
|
||||
if (QMessageBox::warning(
|
||||
nullptr, tr("Failed to fetch directory"),
|
||||
tr("There was a problem communicating with gpodder.net"),
|
||||
QMessageBox::Retry | QMessageBox::Close,
|
||||
QMessageBox::Retry) != QMessageBox::Retry) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try doing the search again.
|
||||
Show();
|
||||
|
||||
}
|
||||
59
src/podcasts/gpoddertoptagspage.h
Normal file
59
src/podcasts/gpoddertoptagspage.h
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 GPODDERTOPTAGSPAGE_H
|
||||
#define GPODDERTOPTAGSPAGE_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include <ApiRequest.h>
|
||||
|
||||
#include "addpodcastpage.h"
|
||||
|
||||
class Application;
|
||||
class NetworkAccessManager;
|
||||
|
||||
class GPodderTopTagsPage : public AddPodcastPage {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GPodderTopTagsPage(Application *app, QWidget *parent = nullptr);
|
||||
~GPodderTopTagsPage();
|
||||
|
||||
static const int kMaxTagCount;
|
||||
|
||||
virtual bool has_visible_widget() const { return false; }
|
||||
virtual void Show();
|
||||
|
||||
private slots:
|
||||
void TagListLoaded(mygpo::TagListPtr tag_list);
|
||||
void TagListFailed();
|
||||
|
||||
private:
|
||||
NetworkAccessManager *network_;
|
||||
mygpo::ApiRequest *api_;
|
||||
|
||||
bool done_initial_load_;
|
||||
};
|
||||
|
||||
#endif // GPODDERTOPTAGSPAGE_H
|
||||
133
src/podcasts/itunessearchpage.cpp
Normal file
133
src/podcasts/itunessearchpage.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 <QJsonDocument>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
|
||||
#include "core/networkaccessmanager.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "podcast.h"
|
||||
#include "podcastdiscoverymodel.h"
|
||||
#include "itunessearchpage.h"
|
||||
#include "ui_itunessearchpage.h"
|
||||
|
||||
const char* ITunesSearchPage::kUrlBase = "http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStoreServices.woa/wa/wsSearch?country=US&media=podcast";
|
||||
|
||||
ITunesSearchPage::ITunesSearchPage(Application* app, QWidget* parent)
|
||||
: AddPodcastPage(app, parent),
|
||||
ui_(new Ui_ITunesSearchPage),
|
||||
network_(new NetworkAccessManager(this)) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
|
||||
QObject::connect(ui_->search, &QPushButton::clicked, this, &ITunesSearchPage::SearchClicked);
|
||||
setWindowIcon(IconLoader::Load("itunes"));
|
||||
|
||||
}
|
||||
|
||||
ITunesSearchPage::~ITunesSearchPage() { delete ui_; }
|
||||
|
||||
void ITunesSearchPage::SearchClicked() {
|
||||
|
||||
emit Busy(true);
|
||||
|
||||
QUrl url(QUrl::fromEncoded(kUrlBase));
|
||||
QUrlQuery url_query;
|
||||
url_query.addQueryItem("term", ui_->query->text());
|
||||
url.setQuery(url_query);
|
||||
|
||||
QNetworkReply *reply = network_->get(QNetworkRequest(url));
|
||||
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { SearchFinished(reply); });
|
||||
|
||||
}
|
||||
|
||||
void ITunesSearchPage::SearchFinished(QNetworkReply* reply) {
|
||||
|
||||
reply->deleteLater();
|
||||
emit Busy(false);
|
||||
|
||||
model()->clear();
|
||||
|
||||
// Was there a network error?
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
QMessageBox::warning(this, tr("Failed to fetch podcasts"), reply->errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument json_document = QJsonDocument::fromJson(reply->readAll(), &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
QMessageBox::warning(this, tr("Failed to fetch podcasts"), tr("There was a problem parsing the response from the iTunes Store"));
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json_data = json_document.object();
|
||||
|
||||
// Was there an error message in the JSON?
|
||||
if (json_data.contains("errorMessage")) {
|
||||
QMessageBox::warning(this, tr("Failed to fetch podcasts"), json_data["errorMessage"].toString());
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray array = json_data["results"].toArray();
|
||||
for (const QJsonValueRef &result : array) {
|
||||
if (!result.isObject()) continue;
|
||||
QJsonObject json_result = result.toObject();
|
||||
if (json_result["kind"].toString() != "podcast") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!json_result.contains("artistName") ||
|
||||
!json_result.contains("trackName") ||
|
||||
!json_result.contains("feedUrl") ||
|
||||
!json_result.contains("trackViewUrl") ||
|
||||
!json_result.contains("artworkUrl30") ||
|
||||
!json_result.contains("artworkUrl100")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Podcast podcast;
|
||||
podcast.set_author(json_result["artistName"].toString());
|
||||
podcast.set_title(json_result["trackName"].toString());
|
||||
podcast.set_url(QUrl(json_result["feedUrl"].toString()));
|
||||
podcast.set_link(QUrl(json_result["trackViewUrl"].toString()));
|
||||
podcast.set_image_url_small(QUrl(json_result["artworkUrl30"].toString()));
|
||||
podcast.set_image_url_large(QUrl(json_result["artworkUrl100"].toString()));
|
||||
|
||||
model()->appendRow(model()->CreatePodcastItem(podcast));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void ITunesSearchPage::Show() { ui_->query->setFocus(); }
|
||||
56
src/podcasts/itunessearchpage.h
Normal file
56
src/podcasts/itunessearchpage.h
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 ITUNESSEARCHPAGE_H
|
||||
#define ITUNESSEARCHPAGE_H
|
||||
|
||||
#include "addpodcastpage.h"
|
||||
|
||||
class Ui_ITunesSearchPage;
|
||||
|
||||
class QNetworkReply;
|
||||
|
||||
class NetworkAccessManager;
|
||||
|
||||
class ITunesSearchPage : public AddPodcastPage {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ITunesSearchPage(Application *app, QWidget *parent);
|
||||
~ITunesSearchPage();
|
||||
|
||||
void Show();
|
||||
|
||||
private slots:
|
||||
void SearchClicked();
|
||||
void SearchFinished(QNetworkReply *reply);
|
||||
|
||||
private:
|
||||
static const char *kUrlBase;
|
||||
|
||||
Ui_ITunesSearchPage *ui_;
|
||||
|
||||
NetworkAccessManager *network_;
|
||||
};
|
||||
|
||||
#endif // ITUNESSEARCHPAGE_H
|
||||
61
src/podcasts/itunessearchpage.ui
Normal file
61
src/podcasts/itunessearchpage.ui
Normal file
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ITunesSearchPage</class>
|
||||
<widget class="QWidget" name="ITunesSearchPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>516</width>
|
||||
<height>69</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Search iTunes</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Enter search terms below to find podcasts in the iTunes Store</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="query"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="search">
|
||||
<property name="text">
|
||||
<string>Search</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>query</sender>
|
||||
<signal>returnPressed()</signal>
|
||||
<receiver>search</receiver>
|
||||
<slot>click()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>237</x>
|
||||
<y>52</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>461</x>
|
||||
<y>55</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -1,8 +1,10 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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
|
||||
@@ -19,28 +21,25 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef NETWORKREMOTESETTINGSPAGE_H
|
||||
#define NETWORKREMOTESETTINGSPAGE_H
|
||||
#ifndef OPMLCONTAINER_H
|
||||
#define OPMLCONTAINER_H
|
||||
|
||||
#include "settingspage.h"
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
class Ui_NetworkRemoteSettingsPage;
|
||||
|
||||
class NetworkRemoteSettingsPage : public SettingsPage {
|
||||
Q_OBJECT
|
||||
#include "podcast.h"
|
||||
|
||||
class OpmlContainer {
|
||||
public:
|
||||
explicit NetworkRemoteSettingsPage(SettingsDialog *dialog);
|
||||
~NetworkRemoteSettingsPage();
|
||||
// Only set for the top-level container
|
||||
QUrl url;
|
||||
|
||||
void Load();
|
||||
void Save();
|
||||
|
||||
private Q_SLOTS:
|
||||
void Options();
|
||||
|
||||
private:
|
||||
Ui_NetworkRemoteSettingsPage *ui_;
|
||||
QString name;
|
||||
QList<OpmlContainer> containers;
|
||||
PodcastList feeds;
|
||||
};
|
||||
|
||||
#endif // NETWORKREMOTESETTINGSPAGE_H
|
||||
Q_DECLARE_METATYPE(OpmlContainer)
|
||||
|
||||
#endif // OPMLCONTAINER_H
|
||||
194
src/podcasts/podcast.cpp
Normal file
194
src/podcasts/podcast.cpp
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 <QSharedData>
|
||||
#include <QVariant>
|
||||
#include <QVariantMap>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QDataStream>
|
||||
#include <QDateTime>
|
||||
#include <QSqlQuery>
|
||||
|
||||
#include "core/utilities.h"
|
||||
|
||||
#include "podcast.h"
|
||||
|
||||
#include <Podcast.h>
|
||||
|
||||
const QStringList Podcast::kColumns = QStringList() << "url"
|
||||
<< "title"
|
||||
<< "description"
|
||||
<< "copyright"
|
||||
<< "link"
|
||||
<< "image_url_large"
|
||||
<< "image_url_small"
|
||||
<< "author"
|
||||
<< "owner_name"
|
||||
<< "owner_email"
|
||||
<< "last_updated"
|
||||
<< "last_update_error"
|
||||
<< "extra";
|
||||
|
||||
const QString Podcast::kColumnSpec = Podcast::kColumns.join(", ");
|
||||
const QString Podcast::kJoinSpec = Utilities::Prepend("p.", Podcast::kColumns).join(", ");
|
||||
const QString Podcast::kBindSpec = Utilities::Prepend(":", Podcast::kColumns).join(", ");
|
||||
const QString Podcast::kUpdateSpec = Utilities::Updateify(Podcast::kColumns).join(", ");
|
||||
|
||||
struct Podcast::Private : public QSharedData {
|
||||
Private();
|
||||
|
||||
int database_id_;
|
||||
QUrl url_;
|
||||
|
||||
QString title_;
|
||||
QString description_;
|
||||
QString copyright_;
|
||||
QUrl link_;
|
||||
QUrl image_url_large_;
|
||||
QUrl image_url_small_;
|
||||
|
||||
// iTunes extensions
|
||||
QString author_;
|
||||
QString owner_name_;
|
||||
QString owner_email_;
|
||||
|
||||
QDateTime last_updated_;
|
||||
QString last_update_error_;
|
||||
|
||||
QVariantMap extra_;
|
||||
|
||||
// These are stored in a different table
|
||||
PodcastEpisodeList episodes_;
|
||||
};
|
||||
|
||||
Podcast::Private::Private() : database_id_(-1) {}
|
||||
|
||||
Podcast::Podcast() : d(new Private) {}
|
||||
|
||||
Podcast::Podcast(const Podcast &other) : d(other.d) {}
|
||||
|
||||
Podcast::~Podcast() {}
|
||||
|
||||
Podcast &Podcast::operator=(const Podcast &other) {
|
||||
d = other.d;
|
||||
return *this;
|
||||
}
|
||||
|
||||
int Podcast::database_id() const { return d->database_id_; }
|
||||
const QUrl &Podcast::url() const { return d->url_; }
|
||||
const QString &Podcast::title() const { return d->title_; }
|
||||
const QString &Podcast::description() const { return d->description_; }
|
||||
const QString &Podcast::copyright() const { return d->copyright_; }
|
||||
const QUrl &Podcast::link() const { return d->link_; }
|
||||
const QUrl &Podcast::image_url_large() const { return d->image_url_large_; }
|
||||
const QUrl &Podcast::image_url_small() const { return d->image_url_small_; }
|
||||
const QString &Podcast::author() const { return d->author_; }
|
||||
const QString &Podcast::owner_name() const { return d->owner_name_; }
|
||||
const QString &Podcast::owner_email() const { return d->owner_email_; }
|
||||
const QDateTime &Podcast::last_updated() const { return d->last_updated_; }
|
||||
const QString &Podcast::last_update_error() const {
|
||||
return d->last_update_error_;
|
||||
}
|
||||
const QVariantMap &Podcast::extra() const { return d->extra_; }
|
||||
QVariant Podcast::extra(const QString &key) const { return d->extra_[key]; }
|
||||
|
||||
void Podcast::set_database_id(const int v) { d->database_id_ = v; }
|
||||
void Podcast::set_url(const QUrl &v) { d->url_ = v; }
|
||||
void Podcast::set_title(const QString &v) { d->title_ = v; }
|
||||
void Podcast::set_description(const QString &v) { d->description_ = v; }
|
||||
void Podcast::set_copyright(const QString &v) { d->copyright_ = v; }
|
||||
void Podcast::set_link(const QUrl &v) { d->link_ = v; }
|
||||
void Podcast::set_image_url_large(const QUrl &v) { d->image_url_large_ = v; }
|
||||
void Podcast::set_image_url_small(const QUrl &v) { d->image_url_small_ = v; }
|
||||
void Podcast::set_author(const QString &v) { d->author_ = v; }
|
||||
void Podcast::set_owner_name(const QString &v) { d->owner_name_ = v; }
|
||||
void Podcast::set_owner_email(const QString &v) { d->owner_email_ = v; }
|
||||
void Podcast::set_last_updated(const QDateTime &v) { d->last_updated_ = v; }
|
||||
void Podcast::set_last_update_error(const QString &v) { d->last_update_error_ = v; }
|
||||
void Podcast::set_extra(const QVariantMap &v) { d->extra_ = v; }
|
||||
void Podcast::set_extra(const QString &key, const QVariant &value) { d->extra_[key] = value; }
|
||||
|
||||
const PodcastEpisodeList &Podcast::episodes() const { return d->episodes_; }
|
||||
PodcastEpisodeList* Podcast::mutable_episodes() { return &d->episodes_; }
|
||||
void Podcast::set_episodes(const PodcastEpisodeList &v) { d->episodes_ = v; }
|
||||
void Podcast::add_episode(const PodcastEpisode &episode) { d->episodes_.append(episode); }
|
||||
|
||||
void Podcast::InitFromQuery(const QSqlQuery &query) {
|
||||
|
||||
d->database_id_ = query.value(0).toInt();
|
||||
d->url_ = QUrl::fromEncoded(query.value(1).toByteArray());
|
||||
d->title_ = query.value(2).toString();
|
||||
d->description_ = query.value(3).toString();
|
||||
d->copyright_ = query.value(4).toString();
|
||||
d->link_ = QUrl::fromEncoded(query.value(5).toByteArray());
|
||||
d->image_url_large_ = QUrl::fromEncoded(query.value(6).toByteArray());
|
||||
d->image_url_small_ = QUrl::fromEncoded(query.value(7).toByteArray());
|
||||
d->author_ = query.value(8).toString();
|
||||
d->owner_name_ = query.value(9).toString();
|
||||
d->owner_email_ = query.value(10).toString();
|
||||
d->last_updated_ = QDateTime::fromSecsSinceEpoch(query.value(11).toUInt());
|
||||
d->last_update_error_ = query.value(12).toString();
|
||||
|
||||
QDataStream extra_stream(query.value(13).toByteArray());
|
||||
extra_stream >> d->extra_;
|
||||
|
||||
}
|
||||
|
||||
void Podcast::BindToQuery(QSqlQuery* query) const {
|
||||
|
||||
query->bindValue(":url", d->url_.toEncoded());
|
||||
query->bindValue(":title", d->title_);
|
||||
query->bindValue(":description", d->description_);
|
||||
query->bindValue(":copyright", d->copyright_);
|
||||
query->bindValue(":link", d->link_.toEncoded());
|
||||
query->bindValue(":image_url_large", d->image_url_large_.toEncoded());
|
||||
query->bindValue(":image_url_small", d->image_url_small_.toEncoded());
|
||||
query->bindValue(":author", d->author_);
|
||||
query->bindValue(":owner_name", d->owner_name_);
|
||||
query->bindValue(":owner_email", d->owner_email_);
|
||||
query->bindValue(":last_updated", d->last_updated_.toSecsSinceEpoch());
|
||||
query->bindValue(":last_update_error", d->last_update_error_);
|
||||
|
||||
QByteArray extra;
|
||||
QDataStream extra_stream(&extra, QIODevice::WriteOnly);
|
||||
extra_stream << d->extra_;
|
||||
|
||||
query->bindValue(":extra", extra);
|
||||
|
||||
}
|
||||
|
||||
void Podcast::InitFromGpo(const mygpo::Podcast* podcast) {
|
||||
|
||||
d->url_ = podcast->url();
|
||||
d->title_ = podcast->title();
|
||||
d->description_ = podcast->description();
|
||||
d->link_ = podcast->website();
|
||||
d->image_url_large_ = podcast->logoUrl();
|
||||
|
||||
set_extra("gpodder:subscribers", podcast->subscribers());
|
||||
set_extra("gpodder:subscribers_last_week", podcast->subscribersLastWeek());
|
||||
set_extra("gpodder:page", podcast->mygpoUrl());
|
||||
|
||||
}
|
||||
114
src/podcasts/podcast.h
Normal file
114
src/podcasts/podcast.h
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 PODCAST_H
|
||||
#define PODCAST_H
|
||||
|
||||
#include <QSharedData>
|
||||
#include <QSharedDataPointer>
|
||||
#include <QVariant>
|
||||
#include <QVariantMap>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QSqlQuery>
|
||||
|
||||
#include "podcastepisode.h"
|
||||
|
||||
namespace mygpo {
|
||||
class Podcast;
|
||||
} // namespace mygpo
|
||||
|
||||
class Podcast {
|
||||
public:
|
||||
Podcast();
|
||||
Podcast(const Podcast &other);
|
||||
~Podcast();
|
||||
|
||||
static const QStringList kColumns;
|
||||
static const QString kColumnSpec;
|
||||
static const QString kJoinSpec;
|
||||
static const QString kBindSpec;
|
||||
static const QString kUpdateSpec;
|
||||
|
||||
void InitFromQuery(const QSqlQuery &query);
|
||||
void InitFromGpo(const mygpo::Podcast *podcast);
|
||||
|
||||
void BindToQuery(QSqlQuery *query) const;
|
||||
|
||||
bool is_valid() const { return database_id() != -1; }
|
||||
|
||||
int database_id() const;
|
||||
const QUrl &url() const;
|
||||
const QString &title() const;
|
||||
const QString &description() const;
|
||||
const QString ©right() const;
|
||||
const QUrl &link() const;
|
||||
const QUrl &image_url_large() const;
|
||||
const QUrl &image_url_small() const;
|
||||
const QString &author() const;
|
||||
const QString &owner_name() const;
|
||||
const QString &owner_email() const;
|
||||
const QDateTime &last_updated() const;
|
||||
const QString &last_update_error() const;
|
||||
const QVariantMap &extra() const;
|
||||
QVariant extra(const QString &key) const;
|
||||
|
||||
void set_database_id(const int v);
|
||||
void set_url(const QUrl &v);
|
||||
void set_title(const QString &v);
|
||||
void set_description(const QString &v);
|
||||
void set_copyright(const QString &v);
|
||||
void set_link(const QUrl &v);
|
||||
void set_image_url_large(const QUrl &v);
|
||||
void set_image_url_small(const QUrl &v);
|
||||
void set_author(const QString &v);
|
||||
void set_owner_name(const QString &v);
|
||||
void set_owner_email(const QString &v);
|
||||
void set_last_updated(const QDateTime &v);
|
||||
void set_last_update_error(const QString &v);
|
||||
void set_extra(const QVariantMap &v);
|
||||
void set_extra(const QString &key, const QVariant &value);
|
||||
|
||||
// Small images are suitable for 16x16 icons in lists. Large images are used in detailed information displays.
|
||||
const QUrl &ImageUrlLarge() const { return image_url_large().isValid() ? image_url_large() : image_url_small(); }
|
||||
const QUrl &ImageUrlSmall() const { return image_url_small().isValid() ? image_url_small() : image_url_large(); }
|
||||
|
||||
// These are stored in a different database table, and aren't loaded or persisted by InitFromQuery or BindToQuery.
|
||||
const PodcastEpisodeList &episodes() const;
|
||||
PodcastEpisodeList *mutable_episodes();
|
||||
void set_episodes(const PodcastEpisodeList &v);
|
||||
void add_episode(const PodcastEpisode &episode);
|
||||
|
||||
Podcast &operator=(const Podcast &other);
|
||||
|
||||
private:
|
||||
struct Private;
|
||||
QSharedDataPointer<Private> d;
|
||||
};
|
||||
Q_DECLARE_METATYPE(Podcast)
|
||||
|
||||
typedef QList<Podcast> PodcastList;
|
||||
Q_DECLARE_METATYPE(QList<Podcast>)
|
||||
|
||||
#endif // PODCAST_H
|
||||
368
src/podcasts/podcastbackend.cpp
Normal file
368
src/podcasts/podcastbackend.cpp
Normal file
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 <QMutexLocker>
|
||||
#include <QSqlDatabase>
|
||||
#include <QSqlQuery>
|
||||
#include <QUrl>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/database.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/scopedtransaction.h"
|
||||
|
||||
#include "podcastbackend.h"
|
||||
|
||||
PodcastBackend::PodcastBackend(Application *app, QObject *parent)
|
||||
: QObject(parent), app_(app), db_(app->database()) {}
|
||||
|
||||
void PodcastBackend::Subscribe(Podcast *podcast) {
|
||||
|
||||
// If this podcast is already in the database, do nothing
|
||||
if (podcast->is_valid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If there's an entry in the database with the same URL, take its data.
|
||||
Podcast existing_podcast = GetSubscriptionByUrl(podcast->url());
|
||||
if (existing_podcast.is_valid()) {
|
||||
*podcast = existing_podcast;
|
||||
return;
|
||||
}
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
ScopedTransaction t(&db);
|
||||
|
||||
// Insert the podcast.
|
||||
QSqlQuery q(db);
|
||||
q.prepare("INSERT INTO podcasts (" + Podcast::kColumnSpec + ") VALUES (" + Podcast::kBindSpec + ")");
|
||||
podcast->BindToQuery(&q);
|
||||
|
||||
q.exec();
|
||||
if (db_->CheckErrors(q)) return;
|
||||
|
||||
// Update the database ID.
|
||||
const int database_id = q.lastInsertId().toInt();
|
||||
podcast->set_database_id(database_id);
|
||||
|
||||
// Update the IDs of any episodes.
|
||||
PodcastEpisodeList *episodes = podcast->mutable_episodes();
|
||||
for (auto it = episodes->begin(); it != episodes->end(); ++it) {
|
||||
it->set_podcast_database_id(database_id);
|
||||
}
|
||||
|
||||
// Add those episodes to the database.
|
||||
AddEpisodes(episodes, &db);
|
||||
|
||||
t.Commit();
|
||||
|
||||
emit SubscriptionAdded(*podcast);
|
||||
}
|
||||
|
||||
void PodcastBackend::Unsubscribe(const Podcast &podcast) {
|
||||
|
||||
// If this podcast is not already in the database, do nothing
|
||||
if (!podcast.is_valid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
ScopedTransaction t(&db);
|
||||
|
||||
// Remove the podcast.
|
||||
QSqlQuery q(db);
|
||||
q.prepare("DELETE FROM podcasts WHERE ROWID = :id");
|
||||
q.bindValue(":id", podcast.database_id());
|
||||
q.exec();
|
||||
if (db_->CheckErrors(q)) return;
|
||||
|
||||
// Remove all episodes in the podcast
|
||||
q.prepare("DELETE FROM podcast_episodes WHERE podcast_id = :id");
|
||||
q.bindValue(":id", podcast.database_id());
|
||||
q.exec();
|
||||
if (db_->CheckErrors(q)) return;
|
||||
|
||||
t.Commit();
|
||||
|
||||
emit SubscriptionRemoved(podcast);
|
||||
|
||||
}
|
||||
|
||||
void PodcastBackend::AddEpisodes(PodcastEpisodeList *episodes, QSqlDatabase *db) {
|
||||
|
||||
QSqlQuery q(*db);
|
||||
q.prepare("INSERT INTO podcast_episodes (" + PodcastEpisode::kColumnSpec + ") VALUES (" + PodcastEpisode::kBindSpec + ")");
|
||||
|
||||
for (auto it = episodes->begin(); it != episodes->end(); ++it) {
|
||||
it->BindToQuery(&q);
|
||||
q.exec();
|
||||
if (db_->CheckErrors(q)) continue;
|
||||
|
||||
const int database_id = q.lastInsertId().toInt();
|
||||
it->set_database_id(database_id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastBackend::AddEpisodes(PodcastEpisodeList *episodes) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
ScopedTransaction t(&db);
|
||||
|
||||
AddEpisodes(episodes, &db);
|
||||
t.Commit();
|
||||
|
||||
emit EpisodesAdded(*episodes);
|
||||
|
||||
}
|
||||
|
||||
void PodcastBackend::UpdateEpisodes(const PodcastEpisodeList &episodes) {
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
ScopedTransaction t(&db);
|
||||
|
||||
QSqlQuery q(db);
|
||||
q.prepare("UPDATE podcast_episodes SET listened = :listened, listened_date = :listened_date, downloaded = :downloaded, local_url = :local_url WHERE ROWID = :id");
|
||||
|
||||
for (const PodcastEpisode &episode : episodes) {
|
||||
q.bindValue(":listened", episode.listened());
|
||||
q.bindValue(":listened_date", episode.listened_date().toSecsSinceEpoch());
|
||||
q.bindValue(":downloaded", episode.downloaded());
|
||||
q.bindValue(":local_url", episode.local_url().toEncoded());
|
||||
q.bindValue(":id", episode.database_id());
|
||||
q.exec();
|
||||
db_->CheckErrors(q);
|
||||
}
|
||||
|
||||
t.Commit();
|
||||
|
||||
emit EpisodesUpdated(episodes);
|
||||
|
||||
}
|
||||
|
||||
PodcastList PodcastBackend::GetAllSubscriptions() {
|
||||
|
||||
PodcastList ret;
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q(db);
|
||||
q.prepare("SELECT ROWID, " + Podcast::kColumnSpec + " FROM podcasts");
|
||||
q.exec();
|
||||
if (db_->CheckErrors(q)) return ret;
|
||||
|
||||
while (q.next()) {
|
||||
Podcast podcast;
|
||||
podcast.InitFromQuery(q);
|
||||
ret << podcast;
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
Podcast PodcastBackend::GetSubscriptionById(const int id) {
|
||||
|
||||
Podcast ret;
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q(db);
|
||||
q.prepare("SELECT ROWID, " + Podcast::kColumnSpec + " FROM podcasts WHERE ROWID = :id");
|
||||
q.bindValue(":id", id);
|
||||
q.exec();
|
||||
if (!db_->CheckErrors(q) && q.next()) {
|
||||
ret.InitFromQuery(q);
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
Podcast PodcastBackend::GetSubscriptionByUrl(const QUrl &url) {
|
||||
|
||||
Podcast ret;
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q(db);
|
||||
q.prepare("SELECT ROWID, " + Podcast::kColumnSpec + " FROM podcasts WHERE url = :url");
|
||||
q.bindValue(":url", url.toEncoded());
|
||||
q.exec();
|
||||
if (!db_->CheckErrors(q) && q.next()) {
|
||||
ret.InitFromQuery(q);
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
PodcastEpisodeList PodcastBackend::GetEpisodes(const int podcast_id) {
|
||||
|
||||
PodcastEpisodeList ret;
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q(db);
|
||||
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE podcast_id = :id ORDER BY publication_date DESC");
|
||||
q.bindValue(":id", podcast_id);
|
||||
q.exec();
|
||||
if (db_->CheckErrors(q)) return ret;
|
||||
|
||||
while (q.next()) {
|
||||
PodcastEpisode episode;
|
||||
episode.InitFromQuery(q);
|
||||
ret << episode;
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
PodcastEpisode PodcastBackend::GetEpisodeById(const int id) {
|
||||
|
||||
PodcastEpisode ret;
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q(db);
|
||||
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE ROWID = :id");
|
||||
q.bindValue(":id", id);
|
||||
q.exec();
|
||||
if (!db_->CheckErrors(q) && q.next()) {
|
||||
ret.InitFromQuery(q);
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
PodcastEpisode PodcastBackend::GetEpisodeByUrl(const QUrl &url) {
|
||||
|
||||
PodcastEpisode ret;
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q(db);
|
||||
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE url = :url");
|
||||
q.bindValue(":url", url.toEncoded());
|
||||
q.exec();
|
||||
if (!db_->CheckErrors(q) && q.next()) {
|
||||
ret.InitFromQuery(q);
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
PodcastEpisode PodcastBackend::GetEpisodeByUrlOrLocalUrl(const QUrl &url) {
|
||||
|
||||
PodcastEpisode ret;
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q(db);
|
||||
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE url = :url OR local_url = :url");
|
||||
q.bindValue(":url", url.toEncoded());
|
||||
q.exec();
|
||||
if (!db_->CheckErrors(q) && q.next()) {
|
||||
ret.InitFromQuery(q);
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
PodcastEpisodeList PodcastBackend::GetOldDownloadedEpisodes(const QDateTime &max_listened_date) {
|
||||
|
||||
PodcastEpisodeList ret;
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q(db);
|
||||
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE downloaded = 'true' AND listened_date <= :max_listened_date");
|
||||
q.bindValue(":max_listened_date", max_listened_date.toSecsSinceEpoch());
|
||||
q.exec();
|
||||
if (db_->CheckErrors(q)) return ret;
|
||||
|
||||
while (q.next()) {
|
||||
PodcastEpisode episode;
|
||||
episode.InitFromQuery(q);
|
||||
ret << episode;
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
PodcastEpisode PodcastBackend::GetOldestDownloadedListenedEpisode() {
|
||||
|
||||
PodcastEpisode ret;
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q(db);
|
||||
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE downloaded = 'true' AND listened = 'true' ORDER BY listened_date ASC");
|
||||
q.exec();
|
||||
if (db_->CheckErrors(q)) return ret;
|
||||
q.next();
|
||||
ret.InitFromQuery(q);
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
PodcastEpisodeList PodcastBackend::GetNewDownloadedEpisodes() {
|
||||
|
||||
PodcastEpisodeList ret;
|
||||
|
||||
QMutexLocker l(db_->Mutex());
|
||||
QSqlDatabase db(db_->Connect());
|
||||
|
||||
QSqlQuery q(db);
|
||||
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE downloaded = 'true' AND listened = 'false'");
|
||||
q.exec();
|
||||
if (db_->CheckErrors(q)) return ret;
|
||||
|
||||
while (q.next()) {
|
||||
PodcastEpisode episode;
|
||||
episode.InitFromQuery(q);
|
||||
ret << episode;
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
98
src/podcasts/podcastbackend.h
Normal file
98
src/podcasts/podcastbackend.h
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 PODCASTBACKEND_H
|
||||
#define PODCASTBACKEND_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QDateTime>
|
||||
#include <QUrl>
|
||||
|
||||
#include "podcast.h"
|
||||
|
||||
class QSqlDatabase;
|
||||
|
||||
class Application;
|
||||
class Database;
|
||||
|
||||
class PodcastBackend : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PodcastBackend(Application *app, QObject *parent = nullptr);
|
||||
|
||||
// Adds the podcast and any included Episodes to the database.
|
||||
// Updates the podcast with a database ID.
|
||||
// If this podcast already has an ID set, this function does nothing.
|
||||
// If a podcast with this URL already exists in the database,
|
||||
// this function just updates the ID field in the provided podcast.
|
||||
void Subscribe(Podcast *podcast);
|
||||
|
||||
// Removes the Podcast with the given ID from the database.
|
||||
// Also removes any episodes associated with this podcast.
|
||||
void Unsubscribe(const Podcast &podcast);
|
||||
|
||||
// Returns a list of all the subscribed podcasts.
|
||||
// For efficiency the Podcast objects returned won't contain any PodcastEpisode objects - get them separately if you want them.
|
||||
PodcastList GetAllSubscriptions();
|
||||
Podcast GetSubscriptionById(const int id);
|
||||
Podcast GetSubscriptionByUrl(const QUrl &url);
|
||||
|
||||
// Returns podcast episodes that match various keys. All these queries are indexed.
|
||||
PodcastEpisodeList GetEpisodes(const int podcast_id);
|
||||
PodcastEpisode GetEpisodeById(const int id);
|
||||
PodcastEpisode GetEpisodeByUrl(const QUrl &url);
|
||||
PodcastEpisode GetEpisodeByUrlOrLocalUrl(const QUrl &url);
|
||||
PodcastEpisode GetOldestDownloadedListenedEpisode();
|
||||
|
||||
// Returns a list of episodes that have local data (downloaded=true) but were last listened to before the given QDateTime.
|
||||
// This query is NOT indexed so it involves a full search of the table.
|
||||
PodcastEpisodeList GetOldDownloadedEpisodes(const QDateTime &max_listened_date);
|
||||
PodcastEpisodeList GetNewDownloadedEpisodes();
|
||||
|
||||
// Adds episodes to the database. Every episode must have a valid podcast_database_id set already.
|
||||
void AddEpisodes(PodcastEpisodeList *episodes);
|
||||
|
||||
// Updates the editable fields (listened, listened_date, downloaded, and local_url) on episodes that must already exist in the database.
|
||||
void UpdateEpisodes(const PodcastEpisodeList &episodes);
|
||||
|
||||
signals:
|
||||
void SubscriptionAdded(const Podcast &podcast);
|
||||
void SubscriptionRemoved(const Podcast &podcast);
|
||||
|
||||
// Emitted when episodes are added to a subscription that *already exists*.
|
||||
void EpisodesAdded(const PodcastEpisodeList &episodes);
|
||||
|
||||
// Emitted when existing episodes are updated.
|
||||
void EpisodesUpdated(const PodcastEpisodeList &episodes);
|
||||
|
||||
private:
|
||||
// Adds each episode to the database, setting their IDs after inserting each one.
|
||||
void AddEpisodes(PodcastEpisodeList *episodes, QSqlDatabase *db);
|
||||
|
||||
private:
|
||||
Application *app_;
|
||||
Database *db_;
|
||||
};
|
||||
|
||||
#endif // PODCASTBACKEND_H
|
||||
124
src/podcasts/podcastdeleter.cpp
Normal file
124
src/podcasts/podcastdeleter.cpp
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 <QFile>
|
||||
#include <QDateTime>
|
||||
#include <QTimer>
|
||||
#include <QSettings>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "core/utilities.h"
|
||||
#include "collection/collectiondirectorymodel.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "podcastbackend.h"
|
||||
#include "podcastdeleter.h"
|
||||
|
||||
const char *PodcastDeleter::kSettingsGroup = "Podcasts";
|
||||
const int PodcastDeleter::kAutoDeleteCheckIntervalMsec = 60 * 6 * 60 * kMsecPerSec;
|
||||
|
||||
PodcastDeleter::PodcastDeleter(Application *app, QObject *parent)
|
||||
: QObject(parent),
|
||||
app_(app),
|
||||
backend_(app_->podcast_backend()),
|
||||
delete_after_secs_(0),
|
||||
auto_delete_timer_(new QTimer(this)) {
|
||||
|
||||
ReloadSettings();
|
||||
auto_delete_timer_->setSingleShot(true);
|
||||
AutoDelete();
|
||||
QObject::connect(auto_delete_timer_, &QTimer::timeout, this, &PodcastDeleter::AutoDelete);
|
||||
QObject::connect(app_, &Application::SettingsChanged, this, &PodcastDeleter::ReloadSettings);
|
||||
|
||||
}
|
||||
|
||||
void PodcastDeleter::DeleteEpisode(const PodcastEpisode &episode) {
|
||||
|
||||
// Delete the local file
|
||||
if (!QFile::remove(episode.local_url().toLocalFile())) {
|
||||
qLog(Warning) << "The local file" << episode.local_url().toLocalFile() << "could not be removed";
|
||||
}
|
||||
|
||||
// Update the episode in the DB
|
||||
PodcastEpisode episode_copy(episode);
|
||||
episode_copy.set_downloaded(false);
|
||||
episode_copy.set_local_url(QUrl());
|
||||
episode_copy.set_listened_date(QDateTime());
|
||||
backend_->UpdateEpisodes(PodcastEpisodeList() << episode_copy);
|
||||
|
||||
}
|
||||
|
||||
void PodcastDeleter::ReloadSettings() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
delete_after_secs_ = s.value("delete_after", 0).toInt();
|
||||
s.endGroup();
|
||||
|
||||
AutoDelete();
|
||||
|
||||
}
|
||||
|
||||
void PodcastDeleter::AutoDelete() {
|
||||
|
||||
if (delete_after_secs_ <= 0) {
|
||||
return;
|
||||
}
|
||||
auto_delete_timer_->stop();
|
||||
QDateTime max_date = QDateTime::currentDateTime();
|
||||
qint64 timeout_ms;
|
||||
PodcastEpisode oldest_episode;
|
||||
QDateTime oldest_episode_time;
|
||||
max_date = max_date.addSecs(-delete_after_secs_);
|
||||
|
||||
PodcastEpisodeList old_episodes = backend_->GetOldDownloadedEpisodes(max_date);
|
||||
|
||||
qLog(Info) << "Deleting" << old_episodes.count()
|
||||
<< "episodes because they were last listened to"
|
||||
<< (delete_after_secs_ / kSecsPerDay) << "days ago";
|
||||
|
||||
for (const PodcastEpisode& episode : old_episodes) {
|
||||
DeleteEpisode(episode);
|
||||
}
|
||||
|
||||
oldest_episode = backend_->GetOldestDownloadedListenedEpisode();
|
||||
if (!oldest_episode.listened_date().isValid()) {
|
||||
oldest_episode_time = QDateTime::currentDateTime();
|
||||
}
|
||||
else {
|
||||
oldest_episode_time = oldest_episode.listened_date();
|
||||
}
|
||||
|
||||
timeout_ms = QDateTime::currentDateTime().toMSecsSinceEpoch();
|
||||
timeout_ms -= oldest_episode_time.toMSecsSinceEpoch();
|
||||
timeout_ms = (delete_after_secs_ * kMsecPerSec) - timeout_ms;
|
||||
qLog(Info) << "Timeout for autodelete set to:" << timeout_ms << "ms";
|
||||
if (timeout_ms >= 0) {
|
||||
auto_delete_timer_->setInterval(timeout_ms);
|
||||
}
|
||||
else {
|
||||
auto_delete_timer_->setInterval(kAutoDeleteCheckIntervalMsec);
|
||||
}
|
||||
auto_delete_timer_->start();
|
||||
|
||||
}
|
||||
56
src/podcasts/podcastdeleter.h
Normal file
56
src/podcasts/podcastdeleter.h
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 PODCASTDELETER_H
|
||||
#define PODCASTDELETER_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "podcast.h"
|
||||
#include "podcastepisode.h"
|
||||
|
||||
class QTimer;
|
||||
|
||||
class Application;
|
||||
class PodcastBackend;
|
||||
|
||||
class PodcastDeleter : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PodcastDeleter(Application *app, QObject *parent = nullptr);
|
||||
static const char *kSettingsGroup;
|
||||
static const int kAutoDeleteCheckIntervalMsec;
|
||||
|
||||
public slots:
|
||||
// Deletes downloaded data for this episode
|
||||
void DeleteEpisode(const PodcastEpisode &episode);
|
||||
void AutoDelete();
|
||||
void ReloadSettings();
|
||||
|
||||
private:
|
||||
Application *app_;
|
||||
PodcastBackend *backend_;
|
||||
int delete_after_secs_;
|
||||
QTimer *auto_delete_timer_;
|
||||
};
|
||||
|
||||
#endif // PODCASTDELETER_H
|
||||
125
src/podcasts/podcastdiscoverymodel.cpp
Normal file
125
src/podcasts/podcastdiscoverymodel.cpp
Normal file
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 "podcastdiscoverymodel.h"
|
||||
|
||||
#include <QStandardItemModel>
|
||||
#include <QStandardItem>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QIcon>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/standarditemiconloader.h"
|
||||
#include "opmlcontainer.h"
|
||||
#include "podcast.h"
|
||||
|
||||
PodcastDiscoveryModel::PodcastDiscoveryModel(Application *app, QObject *parent)
|
||||
: QStandardItemModel(parent),
|
||||
app_(app),
|
||||
icon_loader_(new StandardItemIconLoader(app->album_cover_loader(), this)),
|
||||
default_icon_(IconLoader::Load("podcast")) {
|
||||
|
||||
icon_loader_->SetModel(this);
|
||||
|
||||
}
|
||||
|
||||
QVariant PodcastDiscoveryModel::data(const QModelIndex &idx, int role) const {
|
||||
|
||||
if (idx.isValid() && role == Qt::DecorationRole && !QStandardItemModel::data(idx, Role_StartedLoadingImage).toBool()) {
|
||||
const QUrl image_url = QStandardItemModel::data(idx, Role_ImageUrl).toUrl();
|
||||
if (image_url.isValid()) {
|
||||
const_cast<PodcastDiscoveryModel*>(this)->LazyLoadImage(image_url, idx);
|
||||
}
|
||||
}
|
||||
|
||||
return QStandardItemModel::data(idx, role);
|
||||
|
||||
}
|
||||
|
||||
QStandardItem *PodcastDiscoveryModel::CreatePodcastItem(const Podcast &podcast) {
|
||||
|
||||
QStandardItem *item = new QStandardItem;
|
||||
item->setIcon(default_icon_);
|
||||
item->setText(podcast.title());
|
||||
item->setData(QVariant::fromValue(podcast), Role_Podcast);
|
||||
item->setData(Type_Podcast, Role_Type);
|
||||
item->setData(podcast.ImageUrlSmall(), Role_ImageUrl);
|
||||
return item;
|
||||
|
||||
}
|
||||
|
||||
QStandardItem *PodcastDiscoveryModel::CreateFolder(const QString &name) {
|
||||
|
||||
if (folder_icon_.isNull()) {
|
||||
folder_icon_ = IconLoader::Load("folder");
|
||||
}
|
||||
|
||||
QStandardItem *item = new QStandardItem;
|
||||
item->setIcon(folder_icon_);
|
||||
item->setText(name);
|
||||
item->setData(Type_Folder, Role_Type);
|
||||
return item;
|
||||
|
||||
}
|
||||
|
||||
QStandardItem *PodcastDiscoveryModel::CreateOpmlContainerItem(const OpmlContainer &container) {
|
||||
|
||||
QStandardItem *item = CreateFolder(container.name);
|
||||
CreateOpmlContainerItems(container, item);
|
||||
return item;
|
||||
|
||||
}
|
||||
|
||||
void PodcastDiscoveryModel::CreateOpmlContainerItems(const OpmlContainer &container, QStandardItem *parent) {
|
||||
|
||||
for (const OpmlContainer &child : container.containers) {
|
||||
QStandardItem *child_item = CreateOpmlContainerItem(child);
|
||||
parent->appendRow(child_item);
|
||||
}
|
||||
|
||||
for (const Podcast &child : container.feeds) {
|
||||
QStandardItem *child_item = CreatePodcastItem(child);
|
||||
parent->appendRow(child_item);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastDiscoveryModel::LazyLoadImage(const QUrl &url, const QModelIndex &idx) {
|
||||
|
||||
QStandardItem *item = itemFromIndex(idx);
|
||||
item->setData(true, Role_StartedLoadingImage);
|
||||
icon_loader_->LoadIcon(url, QUrl(), item);
|
||||
|
||||
}
|
||||
|
||||
QStandardItem *PodcastDiscoveryModel::CreateLoadingIndicator() {
|
||||
|
||||
QStandardItem *item = new QStandardItem;
|
||||
item->setText(tr("Loading..."));
|
||||
item->setData(Type_LoadingIndicator, Role_Type);
|
||||
return item;
|
||||
|
||||
}
|
||||
79
src/podcasts/podcastdiscoverymodel.h
Normal file
79
src/podcasts/podcastdiscoverymodel.h
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 PODCASTDISCOVERYMODEL_H
|
||||
#define PODCASTDISCOVERYMODEL_H
|
||||
|
||||
#include <QStandardItemModel>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QIcon>
|
||||
|
||||
#include "covermanager/albumcoverloaderoptions.h"
|
||||
|
||||
class Application;
|
||||
class OpmlContainer;
|
||||
class OpmlFeed;
|
||||
class Podcast;
|
||||
class StandardItemIconLoader;
|
||||
|
||||
class PodcastDiscoveryModel : public QStandardItemModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PodcastDiscoveryModel(Application *app, QObject *parent = nullptr);
|
||||
|
||||
enum Type {
|
||||
Type_Folder,
|
||||
Type_Podcast,
|
||||
Type_LoadingIndicator
|
||||
};
|
||||
|
||||
enum Role {
|
||||
Role_Podcast = Qt::UserRole,
|
||||
Role_Type,
|
||||
Role_ImageUrl,
|
||||
Role_StartedLoadingImage,
|
||||
RoleCount
|
||||
};
|
||||
|
||||
void CreateOpmlContainerItems(const OpmlContainer &container, QStandardItem *parent);
|
||||
QStandardItem *CreateOpmlContainerItem(const OpmlContainer &container);
|
||||
QStandardItem *CreatePodcastItem(const Podcast &podcast);
|
||||
QStandardItem *CreateFolder(const QString &name);
|
||||
QStandardItem *CreateLoadingIndicator();
|
||||
|
||||
QVariant data(const QModelIndex &idx, int role) const override;
|
||||
|
||||
private:
|
||||
void LazyLoadImage(const QUrl &url, const QModelIndex &idx);
|
||||
|
||||
private:
|
||||
Application *app_;
|
||||
StandardItemIconLoader *icon_loader_;
|
||||
|
||||
QIcon default_icon_;
|
||||
QIcon folder_icon_;
|
||||
};
|
||||
|
||||
#endif // PODCASTDISCOVERYMODEL_H
|
||||
288
src/podcasts/podcastdownloader.cpp
Normal file
288
src/podcasts/podcastdownloader.cpp
Normal file
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 "podcastdownloader.h"
|
||||
|
||||
#include <QString>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/networkaccessmanager.h"
|
||||
#include "core/tagreaderclient.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "core/utilities.h"
|
||||
#include "podcastbackend.h"
|
||||
|
||||
const char *PodcastDownloader::kSettingsGroup = "Podcasts";
|
||||
|
||||
Task::Task(const PodcastEpisode &episode, QFile *file, PodcastBackend *backend)
|
||||
: file_(file),
|
||||
episode_(episode),
|
||||
backend_(backend),
|
||||
network_(new NetworkAccessManager(this)),
|
||||
req_(QNetworkRequest(episode.url())),
|
||||
reply_(network_->get(req_)) {
|
||||
|
||||
QObject::connect(reply_, &QNetworkReply::readyRead, this, &Task::reading);
|
||||
QObject::connect(reply_, &QNetworkReply::finished, this, &Task::finishedInternal);
|
||||
QObject::connect(reply_, &QNetworkReply::downloadProgress, this, &Task::downloadProgressInternal);
|
||||
|
||||
emit ProgressChanged(episode_, PodcastDownload::Queued, 0);
|
||||
|
||||
}
|
||||
|
||||
PodcastEpisode Task::episode() const { return episode_; }
|
||||
|
||||
void Task::reading() {
|
||||
|
||||
qint64 bytes = 0;
|
||||
forever {
|
||||
bytes = reply_->bytesAvailable();
|
||||
if (bytes <= 0) break;
|
||||
|
||||
file_->write(reply_->read(bytes));
|
||||
}
|
||||
|
||||
}
|
||||
void Task::finishedPublic() {
|
||||
|
||||
disconnect(reply_, &QNetworkReply::readyRead, nullptr, nullptr);
|
||||
disconnect(reply_, &QNetworkReply::downloadProgress, nullptr, nullptr);
|
||||
disconnect(reply_, &QNetworkReply::finished, nullptr, nullptr);
|
||||
|
||||
emit ProgressChanged(episode_, PodcastDownload::NotDownloading, 0);
|
||||
|
||||
// Delete the file
|
||||
file_->remove();
|
||||
|
||||
emit finished(this);
|
||||
|
||||
}
|
||||
|
||||
void Task::finishedInternal() {
|
||||
|
||||
reply_->deleteLater();
|
||||
|
||||
if (reply_->error() != QNetworkReply::NoError) {
|
||||
qLog(Warning) << "Error downloading episode:" << reply_->errorString();
|
||||
emit ProgressChanged(episode_, PodcastDownload::NotDownloading, 0);
|
||||
// Delete the file
|
||||
file_->remove();
|
||||
emit finished(this);
|
||||
reply_ = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
qLog(Info) << "Download of" << file_->fileName() << "finished";
|
||||
|
||||
// Tell the database the episode has been updated. Get it from the DB again in case the listened field changed in the mean time.
|
||||
PodcastEpisode episode = episode_;
|
||||
episode.set_downloaded(true);
|
||||
episode.set_local_url(QUrl::fromLocalFile(file_->fileName()));
|
||||
backend_->UpdateEpisodes(PodcastEpisodeList() << episode);
|
||||
Podcast podcast = backend_->GetSubscriptionById(episode.podcast_database_id());
|
||||
Song song = episode_.ToSong(podcast);
|
||||
|
||||
emit ProgressChanged(episode_, PodcastDownload::Finished, 0);
|
||||
|
||||
// I didn't ecountered even a single podcast with a correct metadata
|
||||
TagReaderClient::Instance()->SaveFileBlocking(file_->fileName(), song);
|
||||
emit finished(this);
|
||||
|
||||
reply_ = nullptr;
|
||||
|
||||
}
|
||||
|
||||
void Task::downloadProgressInternal(qint64 received, qint64 total) {
|
||||
|
||||
if (total <= 0) {
|
||||
emit ProgressChanged(episode_, PodcastDownload::Downloading, 0);
|
||||
}
|
||||
else {
|
||||
emit ProgressChanged(episode_, PodcastDownload::Downloading, static_cast<float>(received) / total * 100);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PodcastDownloader::PodcastDownloader(Application *app, QObject *parent)
|
||||
: QObject(parent),
|
||||
app_(app),
|
||||
backend_(app_->podcast_backend()),
|
||||
network_(new NetworkAccessManager(this)),
|
||||
disallowed_filename_characters_("[^a-zA-Z0-9_~ -]"),
|
||||
auto_download_(false) {
|
||||
|
||||
QObject::connect(backend_, &PodcastBackend::EpisodesAdded, this, &PodcastDownloader::EpisodesAdded);
|
||||
QObject::connect(backend_, &PodcastBackend::SubscriptionAdded, this, &PodcastDownloader::SubscriptionAdded);
|
||||
QObject::connect(app_, &Application::SettingsChanged, this, &PodcastDownloader::ReloadSettings);
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
QString PodcastDownloader::DefaultDownloadDir() const {
|
||||
|
||||
return QDir::homePath() + "/Podcasts";
|
||||
|
||||
}
|
||||
|
||||
void PodcastDownloader::ReloadSettings() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
|
||||
auto_download_ = s.value("auto_download", false).toBool();
|
||||
download_dir_ = s.value("download_dir", DefaultDownloadDir()).toString();
|
||||
|
||||
}
|
||||
|
||||
QString PodcastDownloader::FilenameForEpisode(const QString &directory, const PodcastEpisode &episode) const {
|
||||
|
||||
const QString file_extension = QFileInfo(episode.url().path()).suffix();
|
||||
int count = 0;
|
||||
|
||||
// The file name contains the publication date and episode title
|
||||
QString base_filename = episode.publication_date().date().toString(Qt::ISODate) + "-" + SanitiseFilenameComponent(episode.title());
|
||||
|
||||
// Add numbers on to the end of the filename until we find one that doesn't exist.
|
||||
forever {
|
||||
QString filename;
|
||||
|
||||
if (count == 0) {
|
||||
filename = QString("%1/%2.%3").arg(directory, base_filename, file_extension);
|
||||
}
|
||||
else {
|
||||
filename = QString("%1/%2 (%3).%4").arg(directory, base_filename, QString::number(count), file_extension);
|
||||
}
|
||||
|
||||
if (!QFile::exists(filename)) {
|
||||
return filename;
|
||||
}
|
||||
|
||||
++count;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastDownloader::DownloadEpisode(const PodcastEpisode &episode) {
|
||||
|
||||
for (Task *tas : list_tasks_) {
|
||||
if (tas->episode().database_id() == episode.database_id()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Podcast podcast = backend_->GetSubscriptionById(episode.podcast_database_id());
|
||||
if (!podcast.is_valid()) {
|
||||
qLog(Warning) << "The podcast that contains episode" << episode.url() << "doesn't exist any more";
|
||||
return;
|
||||
}
|
||||
const QString directory = download_dir_ + "/" + SanitiseFilenameComponent(podcast.title());
|
||||
const QString filepath = FilenameForEpisode(directory, episode);
|
||||
|
||||
// Open the output file
|
||||
if (!QDir(directory).exists()) QDir().mkpath(directory);
|
||||
QFile *file = new QFile(filepath);
|
||||
if (!file->open(QIODevice::WriteOnly)) {
|
||||
qLog(Warning) << "Could not open the file" << filepath << "for writing";
|
||||
return;
|
||||
}
|
||||
|
||||
Task *task = new Task(episode, file, backend_);
|
||||
|
||||
list_tasks_ << task;
|
||||
qLog(Info) << "Downloading" << task->episode().url() << "to" << filepath;
|
||||
QObject::connect(task, &Task::finished, this, &PodcastDownloader::ReplyFinished);
|
||||
QObject::connect(task, &Task::ProgressChanged, this, &PodcastDownloader::ProgressChanged);
|
||||
|
||||
}
|
||||
|
||||
void PodcastDownloader::ReplyFinished(Task *task) {
|
||||
|
||||
list_tasks_.removeAll(task);
|
||||
delete task;
|
||||
|
||||
}
|
||||
|
||||
QString PodcastDownloader::SanitiseFilenameComponent(const QString &text) const {
|
||||
|
||||
return QString(text).replace(disallowed_filename_characters_, " ") .simplified();
|
||||
|
||||
}
|
||||
|
||||
void PodcastDownloader::SubscriptionAdded(const Podcast &podcast) {
|
||||
|
||||
EpisodesAdded(podcast.episodes());
|
||||
|
||||
}
|
||||
|
||||
void PodcastDownloader::EpisodesAdded(const PodcastEpisodeList &episodes) {
|
||||
|
||||
if (auto_download_) {
|
||||
for (const PodcastEpisode &episode : episodes) {
|
||||
DownloadEpisode(episode);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PodcastEpisodeList PodcastDownloader::EpisodesDownloading(const PodcastEpisodeList &episodes) {
|
||||
|
||||
PodcastEpisodeList ret;
|
||||
for (Task *tas : list_tasks_) {
|
||||
for (const PodcastEpisode &episode : episodes) {
|
||||
if (tas->episode().database_id() == episode.database_id()) {
|
||||
ret << episode;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
void PodcastDownloader::cancelDownload(const PodcastEpisodeList &episodes) {
|
||||
|
||||
QList<Task*> ta;
|
||||
for (Task *tas : list_tasks_) {
|
||||
for (const PodcastEpisode &episode : episodes) {
|
||||
if (tas->episode().database_id() == episode.database_id()) {
|
||||
ta << tas;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Task *tas : ta) {
|
||||
tas->finishedPublic();
|
||||
list_tasks_.removeAll(tas);
|
||||
}
|
||||
|
||||
}
|
||||
129
src/podcasts/podcastdownloader.h
Normal file
129
src/podcasts/podcastdownloader.h
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 PODCASTDOWNLOADER_H
|
||||
#define PODCASTDOWNLOADER_H
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
#include <QFile>
|
||||
#include <QSet>
|
||||
#include <QList>
|
||||
#include <QQueue>
|
||||
#include <QString>
|
||||
#include <QRegularExpression>
|
||||
#include <QNetworkRequest>
|
||||
|
||||
#include "core/networkaccessmanager.h"
|
||||
#include "podcast.h"
|
||||
#include "podcastepisode.h"
|
||||
|
||||
class Application;
|
||||
class PodcastBackend;
|
||||
|
||||
class NetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
|
||||
namespace PodcastDownload {
|
||||
enum State {
|
||||
NotDownloading,
|
||||
Queued,
|
||||
Downloading,
|
||||
Finished
|
||||
};
|
||||
}
|
||||
|
||||
class Task : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Task(const PodcastEpisode &episode, QFile *file, PodcastBackend *backend);
|
||||
PodcastEpisode episode() const;
|
||||
|
||||
signals:
|
||||
void ProgressChanged(const PodcastEpisode &episode, const PodcastDownload::State state, const int percent);
|
||||
void finished(Task *task);
|
||||
|
||||
public slots:
|
||||
void finishedPublic();
|
||||
|
||||
private slots:
|
||||
void reading();
|
||||
void downloadProgressInternal(qint64 received, qint64 total);
|
||||
void finishedInternal();
|
||||
|
||||
private:
|
||||
std::unique_ptr<QFile> file_;
|
||||
PodcastEpisode episode_;
|
||||
PodcastBackend *backend_;
|
||||
std::unique_ptr<NetworkAccessManager> network_;
|
||||
QNetworkRequest req_;
|
||||
QNetworkReply *reply_;
|
||||
};
|
||||
|
||||
class PodcastDownloader : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PodcastDownloader(Application *app, QObject *parent = nullptr);
|
||||
|
||||
PodcastEpisodeList EpisodesDownloading(const PodcastEpisodeList &episodes);
|
||||
QString DefaultDownloadDir() const;
|
||||
|
||||
public slots:
|
||||
// Adds the episode to the download queue
|
||||
void DownloadEpisode(const PodcastEpisode &episode);
|
||||
void cancelDownload(const PodcastEpisodeList &episodes);
|
||||
|
||||
signals:
|
||||
void ProgressChanged(const PodcastEpisode &episode, const PodcastDownload::State state, const int percent);
|
||||
|
||||
private slots:
|
||||
void ReloadSettings();
|
||||
|
||||
void SubscriptionAdded(const Podcast &podcast);
|
||||
void EpisodesAdded(const PodcastEpisodeList &episodes);
|
||||
|
||||
void ReplyFinished(Task *task);
|
||||
|
||||
private:
|
||||
QString FilenameForEpisode(const QString &directory, const PodcastEpisode &episode) const;
|
||||
QString SanitiseFilenameComponent(const QString &text) const;
|
||||
|
||||
private:
|
||||
static const char *kSettingsGroup;
|
||||
|
||||
Application *app_;
|
||||
PodcastBackend *backend_;
|
||||
NetworkAccessManager *network_;
|
||||
|
||||
QRegularExpression disallowed_filename_characters_;
|
||||
|
||||
bool auto_download_;
|
||||
QString download_dir_;
|
||||
|
||||
QList<Task*> list_tasks_;
|
||||
};
|
||||
|
||||
#endif // PODCASTDOWNLOADER_H
|
||||
231
src/podcasts/podcastepisode.cpp
Normal file
231
src/podcasts/podcastepisode.cpp
Normal file
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 <limits>
|
||||
|
||||
#include <QSharedData>
|
||||
#include <QIODevice>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QDataStream>
|
||||
#include <QVariant>
|
||||
#include <QVariantMap>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QDateTime>
|
||||
#include <QSqlQuery>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "core/utilities.h"
|
||||
#include "podcast.h"
|
||||
#include "podcastepisode.h"
|
||||
|
||||
const QStringList PodcastEpisode::kColumns = QStringList() << "podcast_id"
|
||||
<< "title"
|
||||
<< "description"
|
||||
<< "author"
|
||||
<< "publication_date"
|
||||
<< "duration_secs"
|
||||
<< "url"
|
||||
<< "listened"
|
||||
<< "listened_date"
|
||||
<< "downloaded"
|
||||
<< "local_url"
|
||||
<< "extra";
|
||||
|
||||
const QString PodcastEpisode::kColumnSpec = PodcastEpisode::kColumns.join(", ");
|
||||
const QString PodcastEpisode::kJoinSpec = Utilities::Prepend("e.", PodcastEpisode::kColumns).join(", ");
|
||||
const QString PodcastEpisode::kBindSpec = Utilities::Prepend(":", PodcastEpisode::kColumns).join(", ");
|
||||
const QString PodcastEpisode::kUpdateSpec = Utilities::Updateify(PodcastEpisode::kColumns).join(", ");
|
||||
|
||||
struct PodcastEpisode::Private : public QSharedData {
|
||||
Private();
|
||||
|
||||
int database_id_;
|
||||
int podcast_database_id_;
|
||||
|
||||
QString title_;
|
||||
QString description_;
|
||||
QString author_;
|
||||
QDateTime publication_date_;
|
||||
int duration_secs_;
|
||||
QUrl url_;
|
||||
|
||||
bool listened_;
|
||||
QDateTime listened_date_;
|
||||
|
||||
bool downloaded_;
|
||||
QUrl local_url_;
|
||||
|
||||
QVariantMap extra_;
|
||||
};
|
||||
|
||||
PodcastEpisode::Private::Private()
|
||||
: database_id_(-1),
|
||||
podcast_database_id_(-1),
|
||||
duration_secs_(-1),
|
||||
listened_(false),
|
||||
downloaded_(false) {}
|
||||
|
||||
PodcastEpisode::PodcastEpisode() : d(new Private) {}
|
||||
|
||||
PodcastEpisode::PodcastEpisode(const PodcastEpisode &other) : d(other.d) {}
|
||||
|
||||
PodcastEpisode::~PodcastEpisode() {}
|
||||
|
||||
PodcastEpisode &PodcastEpisode::operator=(const PodcastEpisode &other) {
|
||||
d = other.d;
|
||||
return *this;
|
||||
}
|
||||
|
||||
int PodcastEpisode::database_id() const { return d->database_id_; }
|
||||
int PodcastEpisode::podcast_database_id() const {
|
||||
return d->podcast_database_id_;
|
||||
}
|
||||
const QString &PodcastEpisode::title() const { return d->title_; }
|
||||
const QString &PodcastEpisode::description() const { return d->description_; }
|
||||
const QString &PodcastEpisode::author() const { return d->author_; }
|
||||
const QDateTime &PodcastEpisode::publication_date() const { return d->publication_date_; }
|
||||
int PodcastEpisode::duration_secs() const { return d->duration_secs_; }
|
||||
const QUrl &PodcastEpisode::url() const { return d->url_; }
|
||||
bool PodcastEpisode::listened() const { return d->listened_; }
|
||||
const QDateTime &PodcastEpisode::listened_date() const { return d->listened_date_; }
|
||||
bool PodcastEpisode::downloaded() const { return d->downloaded_; }
|
||||
const QUrl &PodcastEpisode::local_url() const { return d->local_url_; }
|
||||
const QVariantMap &PodcastEpisode::extra() const { return d->extra_; }
|
||||
QVariant PodcastEpisode::extra(const QString &key) const { return d->extra_[key]; }
|
||||
|
||||
void PodcastEpisode::set_database_id(const int v) { d->database_id_ = v; }
|
||||
void PodcastEpisode::set_podcast_database_id(const int v) { d->podcast_database_id_ = v; }
|
||||
void PodcastEpisode::set_title(const QString &v) { d->title_ = v; }
|
||||
void PodcastEpisode::set_description(const QString &v) { d->description_ = v; }
|
||||
void PodcastEpisode::set_author(const QString &v) { d->author_ = v; }
|
||||
void PodcastEpisode::set_publication_date(const QDateTime &v) { d->publication_date_ = v; }
|
||||
void PodcastEpisode::set_duration_secs(int v) { d->duration_secs_ = v; }
|
||||
void PodcastEpisode::set_url(const QUrl &v) { d->url_ = v; }
|
||||
void PodcastEpisode::set_listened(const bool v) { d->listened_ = v; }
|
||||
void PodcastEpisode::set_listened_date(const QDateTime &v) { d->listened_date_ = v; }
|
||||
void PodcastEpisode::set_downloaded(const bool v) { d->downloaded_ = v; }
|
||||
void PodcastEpisode::set_local_url(const QUrl &v) { d->local_url_ = v; }
|
||||
void PodcastEpisode::set_extra(const QVariantMap &v) { d->extra_ = v; }
|
||||
void PodcastEpisode::set_extra(const QString &key, const QVariant &value) { d->extra_[key] = value; }
|
||||
|
||||
void PodcastEpisode::InitFromQuery(const QSqlQuery &query) {
|
||||
|
||||
d->database_id_ = query.value(0).toInt();
|
||||
d->podcast_database_id_ = query.value(1).toInt();
|
||||
d->title_ = query.value(2).toString();
|
||||
d->description_ = query.value(3).toString();
|
||||
d->author_ = query.value(4).toString();
|
||||
d->publication_date_ = QDateTime::fromSecsSinceEpoch(query.value(5).toUInt());
|
||||
d->duration_secs_ = query.value(6).toInt();
|
||||
d->url_ = QUrl::fromEncoded(query.value(7).toByteArray());
|
||||
d->listened_ = query.value(8).toBool();
|
||||
|
||||
// After setting QDateTime to invalid state, it's saved into database as time_t,
|
||||
// when this number std::numeric_limits<unsigned int>::max() (4294967295) is read back from database, it creates a valid QDateTime.
|
||||
// So to make it behave consistently, this change is needed.
|
||||
if (query.value(9).toUInt() == std::numeric_limits<unsigned int>::max()) {
|
||||
d->listened_date_ = QDateTime();
|
||||
}
|
||||
else {
|
||||
d->listened_date_ = QDateTime::fromSecsSinceEpoch(query.value(9).toUInt());
|
||||
}
|
||||
|
||||
d->downloaded_ = query.value(10).toBool();
|
||||
d->local_url_ = QUrl::fromEncoded(query.value(11).toByteArray());
|
||||
|
||||
QDataStream extra_stream(query.value(12).toByteArray());
|
||||
extra_stream >> d->extra_;
|
||||
|
||||
}
|
||||
|
||||
void PodcastEpisode::BindToQuery(QSqlQuery* query) const {
|
||||
|
||||
query->bindValue(":podcast_id", d->podcast_database_id_);
|
||||
query->bindValue(":title", d->title_);
|
||||
query->bindValue(":description", d->description_);
|
||||
query->bindValue(":author", d->author_);
|
||||
query->bindValue(":publication_date", d->publication_date_.toSecsSinceEpoch());
|
||||
query->bindValue(":duration_secs", d->duration_secs_);
|
||||
query->bindValue(":url", d->url_.toEncoded());
|
||||
query->bindValue(":listened", d->listened_);
|
||||
query->bindValue(":listened_date", d->listened_date_.toSecsSinceEpoch());
|
||||
query->bindValue(":downloaded", d->downloaded_);
|
||||
query->bindValue(":local_url", d->local_url_.toEncoded());
|
||||
|
||||
QByteArray extra;
|
||||
QDataStream extra_stream(&extra, QIODevice::WriteOnly);
|
||||
extra_stream << d->extra_;
|
||||
|
||||
query->bindValue(":extra", extra);
|
||||
|
||||
}
|
||||
|
||||
Song PodcastEpisode::ToSong(const Podcast &podcast) const {
|
||||
|
||||
Song ret;
|
||||
ret.set_valid(true);
|
||||
ret.set_title(title().simplified());
|
||||
ret.set_artist(author().simplified());
|
||||
ret.set_length_nanosec(kNsecPerSec * duration_secs());
|
||||
ret.set_year(publication_date().date().year());
|
||||
ret.set_comment(description());
|
||||
ret.set_id(database_id());
|
||||
ret.set_ctime(publication_date().toSecsSinceEpoch());
|
||||
ret.set_genre(QString("Podcast"));
|
||||
//ret.set_genre_id3(186);
|
||||
|
||||
if (listened() && listened_date().isValid()) {
|
||||
ret.set_mtime(listened_date().toSecsSinceEpoch());
|
||||
}
|
||||
else {
|
||||
ret.set_mtime(publication_date().toSecsSinceEpoch());
|
||||
}
|
||||
|
||||
if (ret.length_nanosec() < 0) {
|
||||
ret.set_length_nanosec(-1);
|
||||
}
|
||||
|
||||
if (downloaded() && QFile::exists(local_url().toLocalFile())) {
|
||||
ret.set_url(local_url());
|
||||
}
|
||||
else {
|
||||
ret.set_url(url());
|
||||
}
|
||||
|
||||
ret.set_basefilename(QFileInfo(ret.url().path()).fileName());
|
||||
|
||||
// Use information from the podcast if it's set
|
||||
if (podcast.is_valid()) {
|
||||
ret.set_album(podcast.title().simplified());
|
||||
ret.set_art_automatic(podcast.ImageUrlLarge());
|
||||
|
||||
if (author().isEmpty()) ret.set_artist(podcast.title().simplified());
|
||||
}
|
||||
return ret;
|
||||
|
||||
}
|
||||
100
src/podcasts/podcastepisode.h
Normal file
100
src/podcasts/podcastepisode.h
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 PODCASTEPISODE_H
|
||||
#define PODCASTEPISODE_H
|
||||
|
||||
#include <QSharedDataPointer>
|
||||
#include <QVariant>
|
||||
#include <QVariantMap>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
#include <QDateTime>
|
||||
#include <QSqlQuery>
|
||||
|
||||
#include "core/song.h"
|
||||
|
||||
class Podcast;
|
||||
|
||||
class PodcastEpisode {
|
||||
public:
|
||||
PodcastEpisode();
|
||||
PodcastEpisode(const PodcastEpisode &other);
|
||||
~PodcastEpisode();
|
||||
|
||||
static const QStringList kColumns;
|
||||
static const QString kColumnSpec;
|
||||
static const QString kJoinSpec;
|
||||
static const QString kBindSpec;
|
||||
static const QString kUpdateSpec;
|
||||
|
||||
void InitFromQuery(const QSqlQuery &query);
|
||||
void BindToQuery(QSqlQuery *query) const;
|
||||
|
||||
Song ToSong(const Podcast &podcast) const;
|
||||
|
||||
bool is_valid() const { return database_id() != -1; }
|
||||
|
||||
int database_id() const;
|
||||
int podcast_database_id() const;
|
||||
const QString &title() const;
|
||||
const QString &description() const;
|
||||
const QString &author() const;
|
||||
const QDateTime &publication_date() const;
|
||||
int duration_secs() const;
|
||||
const QUrl &url() const;
|
||||
bool listened() const;
|
||||
const QDateTime &listened_date() const;
|
||||
bool downloaded() const;
|
||||
const QUrl &local_url() const;
|
||||
const QVariantMap &extra() const;
|
||||
QVariant extra(const QString &key) const;
|
||||
|
||||
void set_database_id(const int v);
|
||||
void set_podcast_database_id(int v);
|
||||
void set_title(const QString &v);
|
||||
void set_description(const QString &v);
|
||||
void set_author(const QString &v);
|
||||
void set_publication_date(const QDateTime &v);
|
||||
void set_duration_secs(int v);
|
||||
void set_url(const QUrl &v);
|
||||
void set_listened(const bool v);
|
||||
void set_listened_date(const QDateTime &v);
|
||||
void set_downloaded(const bool v);
|
||||
void set_local_url(const QUrl &v);
|
||||
void set_extra(const QVariantMap &v);
|
||||
void set_extra(const QString &key, const QVariant &value);
|
||||
|
||||
PodcastEpisode &operator=(const PodcastEpisode &other);
|
||||
|
||||
private:
|
||||
struct Private;
|
||||
QSharedDataPointer<Private> d;
|
||||
};
|
||||
Q_DECLARE_METATYPE(PodcastEpisode)
|
||||
|
||||
typedef QList<PodcastEpisode> PodcastEpisodeList;
|
||||
Q_DECLARE_METATYPE(QList<PodcastEpisode>)
|
||||
|
||||
#endif // PODCASTEPISODE_H
|
||||
59
src/podcasts/podcastinfodialog.cpp
Normal file
59
src/podcasts/podcastinfodialog.cpp
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
Copyright 2018, Jim Broadus <jbroadus@gmail.com>
|
||||
* Copyright 2019-2021, 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 <QDialog>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "podcastepisode.h"
|
||||
#include "podcastinfodialog.h"
|
||||
#include "ui_podcastinfodialog.h"
|
||||
|
||||
PodcastInfoDialog::PodcastInfoDialog(Application *app, QWidget *parent)
|
||||
: QDialog(parent), app_(app), ui_(new Ui_PodcastInfoDialog) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
ui_->podcast_details->SetApplication(app);
|
||||
ui_->episode_details->SetApplication(app);
|
||||
|
||||
}
|
||||
|
||||
PodcastInfoDialog::~PodcastInfoDialog() { delete ui_; }
|
||||
|
||||
void PodcastInfoDialog::ShowPodcast(const Podcast &podcast) {
|
||||
|
||||
ui_->episode_info_scroll_area->hide();
|
||||
ui_->podcast_url->setText(podcast.url().toString());
|
||||
ui_->podcast_url->setReadOnly(true);
|
||||
ui_->podcast_details->SetPodcast(podcast);
|
||||
show();
|
||||
|
||||
}
|
||||
|
||||
void PodcastInfoDialog::ShowEpisode(const PodcastEpisode &episode, const Podcast &podcast) {
|
||||
|
||||
ui_->episode_info_scroll_area->show();
|
||||
ui_->podcast_url->setText(episode.url().toString());
|
||||
ui_->podcast_url->setReadOnly(true);
|
||||
ui_->podcast_details->SetPodcast(podcast);
|
||||
ui_->episode_details->SetEpisode(episode);
|
||||
show();
|
||||
|
||||
}
|
||||
48
src/podcasts/podcastinfodialog.h
Normal file
48
src/podcasts/podcastinfodialog.h
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
Copyright 2018, Jim Broadus <jbroadus@gmail.com>
|
||||
* Copyright 2019-2021, 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 PODCASTINFODIALOG_H
|
||||
#define PODCASTINFODIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class Application;
|
||||
class Podcast;
|
||||
class PodcastEpisode;
|
||||
class Ui_PodcastInfoDialog;
|
||||
|
||||
class PodcastInfoDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PodcastInfoDialog(Application *app, QWidget *parent = nullptr);
|
||||
~PodcastInfoDialog();
|
||||
|
||||
void ShowPodcast(const Podcast &podcast);
|
||||
void ShowEpisode(const PodcastEpisode &episode, const Podcast &podcast);
|
||||
|
||||
private:
|
||||
Application *app_;
|
||||
|
||||
Ui_PodcastInfoDialog *ui_;
|
||||
};
|
||||
|
||||
#endif // PODCASTINFODIALOG_H
|
||||
142
src/podcasts/podcastinfodialog.ui
Normal file
142
src/podcasts/podcastinfodialog.ui
Normal file
@@ -0,0 +1,142 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>PodcastInfoDialog</class>
|
||||
<widget class="QDialog" name="PodcastInfoDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>493</width>
|
||||
<height>415</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Podcast Information</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="podcast_url"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QScrollArea" name="episode_info_scroll_area">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>250</width>
|
||||
<height>100</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="EpisodeInfoWidget" name="episode_details">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>473</width>
|
||||
<height>158</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QScrollArea" name="podcast_info_scroll_area">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>250</width>
|
||||
<height>100</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="PodcastInfoWidget" name="podcast_details">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>473</width>
|
||||
<height>157</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Close</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>PodcastInfoWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>podcasts/podcastinfowidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>EpisodeInfoWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header location="global">podcasts/episodeinfowidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>PodcastInfoDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>PodcastInfoDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
134
src/podcasts/podcastinfowidget.cpp
Normal file
134
src/podcasts/podcastinfowidget.cpp
Normal file
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 <QWidget>
|
||||
#include <QImage>
|
||||
#include <QPixmap>
|
||||
#include <QColor>
|
||||
#include <QPalette>
|
||||
#include <QLabel>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "covermanager/albumcoverloader.h"
|
||||
#include "covermanager/albumcoverloaderoptions.h"
|
||||
#include "covermanager/albumcoverloaderresult.h"
|
||||
#include "podcastinfowidget.h"
|
||||
#include "ui_podcastinfowidget.h"
|
||||
|
||||
PodcastInfoWidget::PodcastInfoWidget(QWidget *parent)
|
||||
: QWidget(parent),
|
||||
ui_(new Ui_PodcastInfoWidget),
|
||||
app_(nullptr),
|
||||
image_id_(0) {
|
||||
|
||||
ui_->setupUi(this);
|
||||
|
||||
cover_options_.desired_height_ = 180;
|
||||
ui_->image->setFixedSize(cover_options_.desired_height_, cover_options_.desired_height_);
|
||||
|
||||
// Set the colour of all the labels
|
||||
const bool light = palette().color(QPalette::Base).value() > 128;
|
||||
const QColor color = palette().color(QPalette::Dark);
|
||||
QPalette label_palette(palette());
|
||||
label_palette.setColor(QPalette::WindowText, light ? color.darker(150) : color.lighter(125));
|
||||
|
||||
for (QLabel* label : findChildren<QLabel*>()) {
|
||||
if (label->property("field_label").toBool()) {
|
||||
label->setPalette(label_palette);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PodcastInfoWidget::~PodcastInfoWidget() { delete ui_; }
|
||||
|
||||
void PodcastInfoWidget::SetApplication(Application *app) {
|
||||
|
||||
app_ = app;
|
||||
connect(app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &PodcastInfoWidget::AlbumCoverLoaded);
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
template<typename T>
|
||||
void SetText(const QString& value, T* label, QLabel* buddy_label = nullptr) {
|
||||
|
||||
const bool visible = !value.isEmpty();
|
||||
|
||||
label->setVisible(visible);
|
||||
if (buddy_label) {
|
||||
buddy_label->setVisible(visible);
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
label->setText(value);
|
||||
}
|
||||
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void PodcastInfoWidget::SetPodcast(const Podcast &podcast) {
|
||||
|
||||
if (image_id_) {
|
||||
app_->album_cover_loader()->CancelTask(image_id_);
|
||||
image_id_ = 0;
|
||||
}
|
||||
|
||||
podcast_ = podcast;
|
||||
|
||||
if (podcast.ImageUrlLarge().isValid()) {
|
||||
// Start loading an image for this item.
|
||||
image_id_ = app_->album_cover_loader()->LoadImageAsync(cover_options_, podcast.ImageUrlLarge(), QUrl());
|
||||
}
|
||||
|
||||
ui_->image->hide();
|
||||
|
||||
SetText(podcast.title(), ui_->title);
|
||||
SetText(podcast.description(), ui_->description);
|
||||
SetText(podcast.copyright(), ui_->copyright, ui_->copyright_label);
|
||||
SetText(podcast.author(), ui_->author, ui_->author_label);
|
||||
SetText(podcast.owner_name(), ui_->owner, ui_->owner_label);
|
||||
SetText(podcast.link().toString(), ui_->website, ui_->website_label);
|
||||
SetText(podcast.extra("gpodder:subscribers").toString(), ui_->subscribers, ui_->subscribers_label);
|
||||
|
||||
if (!image_id_) {
|
||||
emit LoadingFinished();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastInfoWidget::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result) {
|
||||
|
||||
if (id != image_id_) {
|
||||
return;
|
||||
}
|
||||
image_id_ = 0;
|
||||
|
||||
if (result.success && !result.image_scaled.isNull()) {
|
||||
ui_->image->setPixmap(QPixmap::fromImage(result.image_scaled));
|
||||
ui_->image->show();
|
||||
}
|
||||
|
||||
emit LoadingFinished();
|
||||
|
||||
}
|
||||
65
src/podcasts/podcastinfowidget.h
Normal file
65
src/podcasts/podcastinfowidget.h
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 PODCASTINFOWIDGET_H
|
||||
#define PODCASTINFOWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
#include "podcast.h"
|
||||
#include "covermanager/albumcoverloaderoptions.h"
|
||||
#include "covermanager/albumcoverloaderresult.h"
|
||||
|
||||
class Application;
|
||||
class Ui_PodcastInfoWidget;
|
||||
|
||||
class QLabel;
|
||||
|
||||
class PodcastInfoWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PodcastInfoWidget(QWidget *parent = nullptr);
|
||||
~PodcastInfoWidget();
|
||||
|
||||
void SetApplication(Application *app);
|
||||
|
||||
void SetPodcast(const Podcast& podcast);
|
||||
|
||||
signals:
|
||||
void LoadingFinished();
|
||||
|
||||
private slots:
|
||||
void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
|
||||
|
||||
private:
|
||||
Ui_PodcastInfoWidget *ui_;
|
||||
|
||||
AlbumCoverLoaderOptions cover_options_;
|
||||
|
||||
Application *app_;
|
||||
Podcast podcast_;
|
||||
quint64 image_id_;
|
||||
};
|
||||
|
||||
#endif // PODCASTINFOWIDGET_H
|
||||
220
src/podcasts/podcastinfowidget.ui
Normal file
220
src/podcasts/podcastinfowidget.ui
Normal file
@@ -0,0 +1,220 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>PodcastInfoWidget</class>
|
||||
<widget class="QWidget" name="PodcastInfoWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>398</width>
|
||||
<height>551</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#description {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
QLineEdit {
|
||||
background: transparent;
|
||||
}</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinAndMaxSize</enum>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="image">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="title">
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="description">
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinAndMaxSize</enum>
|
||||
</property>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLineEdit" name="website">
|
||||
<property name="frame">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="author_label">
|
||||
<property name="text">
|
||||
<string>Author</string>
|
||||
</property>
|
||||
<property name="field_label" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="owner">
|
||||
<property name="frame">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="website_label">
|
||||
<property name="text">
|
||||
<string>Website</string>
|
||||
</property>
|
||||
<property name="field_label" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="copyright">
|
||||
<property name="frame">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="copyright_label">
|
||||
<property name="text">
|
||||
<string>Copyright</string>
|
||||
</property>
|
||||
<property name="field_label" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="author">
|
||||
<property name="frame">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="owner_label">
|
||||
<property name="text">
|
||||
<string>Owner</string>
|
||||
</property>
|
||||
<property name="field_label" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="subscribers_label">
|
||||
<property name="text">
|
||||
<string>Subscribers</string>
|
||||
</property>
|
||||
<property name="field_label" stdset="0">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLineEdit" name="subscribers">
|
||||
<property name="frame">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
376
src/podcasts/podcastparser.cpp
Normal file
376
src/podcasts/podcastparser.cpp
Normal file
@@ -0,0 +1,376 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 <QUrl>
|
||||
#include <QDateTime>
|
||||
#include <QXmlStreamReader>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/utilities.h"
|
||||
#include "podcastparser.h"
|
||||
#include "opmlcontainer.h"
|
||||
|
||||
// Namespace constants must be lower case.
|
||||
const char *PodcastParser::kAtomNamespace = "http://www.w3.org/2005/atom";
|
||||
const char *PodcastParser::kItunesNamespace = "http://www.itunes.com/dtds/podcast-1.0.dtd";
|
||||
|
||||
PodcastParser::PodcastParser() {
|
||||
supported_mime_types_ << "application/rss+xml"
|
||||
<< "application/xml"
|
||||
<< "text/x-opml"
|
||||
<< "text/xml";
|
||||
}
|
||||
|
||||
bool PodcastParser::SupportsContentType(const QString &content_type) const {
|
||||
|
||||
if (content_type.isEmpty()) {
|
||||
// Why not have a go.
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const QString &mime_type : supported_mime_types()) {
|
||||
if (content_type.contains(mime_type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
bool PodcastParser::TryMagic(const QByteArray &data) const {
|
||||
QString str(QString::fromUtf8(data));
|
||||
return str.contains(QRegularExpression("<rss\\b")) || str.contains(QRegularExpression("<opml\\b"));
|
||||
}
|
||||
|
||||
QVariant PodcastParser::Load(QIODevice *device, const QUrl &url) const {
|
||||
|
||||
QXmlStreamReader reader(device);
|
||||
|
||||
while (!reader.atEnd()) {
|
||||
switch (reader.readNext()) {
|
||||
case QXmlStreamReader::StartElement: {
|
||||
const QString name = reader.name().toString();
|
||||
if (name == "rss") {
|
||||
Podcast podcast;
|
||||
if (!ParseRss(&reader, &podcast)) {
|
||||
return QVariant();
|
||||
}
|
||||
else {
|
||||
podcast.set_url(url);
|
||||
return QVariant::fromValue(podcast);
|
||||
}
|
||||
}
|
||||
else if (name == "opml") {
|
||||
OpmlContainer container;
|
||||
if (!ParseOpml(&reader, &container)) {
|
||||
return QVariant();
|
||||
}
|
||||
else {
|
||||
container.url = url;
|
||||
return QVariant::fromValue(container);
|
||||
}
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
|
||||
}
|
||||
|
||||
bool PodcastParser::ParseRss(QXmlStreamReader *reader, Podcast *ret) const {
|
||||
|
||||
if (!Utilities::ParseUntilElement(reader, "channel")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ParseChannel(reader, ret);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void PodcastParser::ParseChannel(QXmlStreamReader *reader, Podcast *ret) const {
|
||||
|
||||
while (!reader->atEnd()) {
|
||||
QXmlStreamReader::TokenType type = reader->readNext();
|
||||
switch (type) {
|
||||
case QXmlStreamReader::StartElement: {
|
||||
const QString name = reader->name().toString();
|
||||
const QString lower_namespace = reader->namespaceUri().toString().toLower();
|
||||
|
||||
if (name == "title") {
|
||||
ret->set_title(reader->readElementText());
|
||||
}
|
||||
else if (name == "link" && lower_namespace.isEmpty()) {
|
||||
ret->set_link(QUrl::fromEncoded(reader->readElementText().toLatin1()));
|
||||
}
|
||||
else if (name == "description") {
|
||||
ret->set_description(reader->readElementText());
|
||||
}
|
||||
else if (name == "owner" && lower_namespace == kItunesNamespace) {
|
||||
ParseItunesOwner(reader, ret);
|
||||
}
|
||||
else if (name == "image") {
|
||||
ParseImage(reader, ret);
|
||||
}
|
||||
else if (name == "copyright") {
|
||||
ret->set_copyright(reader->readElementText());
|
||||
}
|
||||
else if (name == "link" && lower_namespace == kAtomNamespace && ret->url().isEmpty() && reader->attributes().value("rel").toString() == "self") {
|
||||
ret->set_url(QUrl::fromEncoded(reader->readElementText().toLatin1()));
|
||||
}
|
||||
else if (name == "item") {
|
||||
ParseItem(reader, ret);
|
||||
}
|
||||
else {
|
||||
Utilities::ConsumeCurrentElement(reader);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case QXmlStreamReader::EndElement:
|
||||
return;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastParser::ParseImage(QXmlStreamReader *reader, Podcast *ret) const {
|
||||
|
||||
while (!reader->atEnd()) {
|
||||
QXmlStreamReader::TokenType type = reader->readNext();
|
||||
switch (type) {
|
||||
case QXmlStreamReader::StartElement: {
|
||||
const QString name = reader->name().toString();
|
||||
if (name == "url") {
|
||||
ret->set_image_url_large(
|
||||
QUrl::fromEncoded(reader->readElementText().toLatin1()));
|
||||
}
|
||||
else {
|
||||
Utilities::ConsumeCurrentElement(reader);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case QXmlStreamReader::EndElement:
|
||||
return;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastParser::ParseItunesOwner(QXmlStreamReader *reader, Podcast *ret) const {
|
||||
|
||||
while (!reader->atEnd()) {
|
||||
QXmlStreamReader::TokenType type = reader->readNext();
|
||||
switch (type) {
|
||||
case QXmlStreamReader::StartElement: {
|
||||
const QString name = reader->name().toString();
|
||||
if (name == "name") {
|
||||
ret->set_owner_name(reader->readElementText());
|
||||
}
|
||||
else if (name == "email") {
|
||||
ret->set_owner_email(reader->readElementText());
|
||||
}
|
||||
else {
|
||||
Utilities::ConsumeCurrentElement(reader);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case QXmlStreamReader::EndElement:
|
||||
return;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastParser::ParseItem(QXmlStreamReader *reader, Podcast *ret) const {
|
||||
|
||||
PodcastEpisode episode;
|
||||
|
||||
while (!reader->atEnd()) {
|
||||
QXmlStreamReader::TokenType type = reader->readNext();
|
||||
switch (type) {
|
||||
case QXmlStreamReader::StartElement: {
|
||||
const QString name = reader->name().toString();
|
||||
const QString lower_namespace = reader->namespaceUri().toString().toLower();
|
||||
|
||||
if (name == "title") {
|
||||
episode.set_title(reader->readElementText());
|
||||
}
|
||||
else if (name == "description") {
|
||||
episode.set_description(reader->readElementText());
|
||||
}
|
||||
else if (name == "pubDate") {
|
||||
QString date = reader->readElementText();
|
||||
episode.set_publication_date(Utilities::ParseRFC822DateTime(date));
|
||||
if (!episode.publication_date().isValid()) {
|
||||
qLog(Error) << "Unable to parse date:" << date;
|
||||
}
|
||||
}
|
||||
else if (name == "duration" && lower_namespace == kItunesNamespace) {
|
||||
// http://www.apple.com/itunes/podcasts/specs.html
|
||||
QStringList parts = reader->readElementText().split(':');
|
||||
if (parts.count() == 2) {
|
||||
episode.set_duration_secs(parts[0].toInt() * 60 + parts[1].toInt());
|
||||
}
|
||||
else if (parts.count() >= 3) {
|
||||
episode.set_duration_secs(parts[0].toInt() * 60 * 60 + parts[1].toInt() * 60 + parts[2].toInt());
|
||||
}
|
||||
}
|
||||
else if (name == "enclosure") {
|
||||
const QString type2 = reader->attributes().value("type").toString();
|
||||
const QUrl url = QUrl::fromEncoded(reader->attributes().value("url").toString().toLatin1());
|
||||
if (type2.startsWith("audio/") || type2.startsWith("x-audio/")) {
|
||||
episode.set_url(url);
|
||||
}
|
||||
// If the URL doesn't have a type, see if it's one of the obvious types
|
||||
else if (type2.isEmpty() && (url.path().endsWith(".mp3", Qt::CaseInsensitive) || url.path().endsWith(".m4a", Qt::CaseInsensitive) || url.path().endsWith(".wav", Qt::CaseInsensitive))) {
|
||||
episode.set_url(url);
|
||||
}
|
||||
Utilities::ConsumeCurrentElement(reader);
|
||||
}
|
||||
else if (name == "author" && lower_namespace == kItunesNamespace) {
|
||||
episode.set_author(reader->readElementText());
|
||||
}
|
||||
else {
|
||||
Utilities::ConsumeCurrentElement(reader);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case QXmlStreamReader::EndElement:
|
||||
if (!episode.publication_date().isValid()) {
|
||||
episode.set_publication_date(QDateTime::currentDateTime());
|
||||
}
|
||||
if (!episode.url().isEmpty()) {
|
||||
ret->add_episode(episode);
|
||||
}
|
||||
return;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool PodcastParser::ParseOpml(QXmlStreamReader *reader, OpmlContainer *ret) const {
|
||||
|
||||
if (!Utilities::ParseUntilElement(reader, "body")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ParseOutline(reader, ret);
|
||||
|
||||
// OPML files sometimes consist of a single top level container.
|
||||
OpmlContainer *top = ret;
|
||||
while (top->feeds.count() == 0 && top->containers.count() == 1) {
|
||||
top = &top->containers[0];
|
||||
}
|
||||
if (top != ret) {
|
||||
// Copy the sub-container to a temporary location first.
|
||||
OpmlContainer tmp = *top;
|
||||
*ret = tmp;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
void PodcastParser::ParseOutline(QXmlStreamReader *reader, OpmlContainer *ret) const {
|
||||
|
||||
while (!reader->atEnd()) {
|
||||
QXmlStreamReader::TokenType type = reader->readNext();
|
||||
switch (type) {
|
||||
case QXmlStreamReader::StartElement: {
|
||||
const QString name = reader->name().toString();
|
||||
if (name != "outline") {
|
||||
Utilities::ConsumeCurrentElement(reader);
|
||||
continue;
|
||||
}
|
||||
|
||||
QXmlStreamAttributes attributes = reader->attributes();
|
||||
|
||||
if (attributes.value("type").toString() == "rss") {
|
||||
// Parse the feed and add it to this container
|
||||
Podcast podcast;
|
||||
podcast.set_description(attributes.value("description").toString());
|
||||
|
||||
QString title = attributes.value("title").toString();
|
||||
if (title.isEmpty()) {
|
||||
title = attributes.value("text").toString();
|
||||
}
|
||||
podcast.set_title(title);
|
||||
podcast.set_image_url_large(QUrl::fromEncoded(attributes.value("imageHref").toString().toLatin1()));
|
||||
podcast.set_url(QUrl::fromEncoded(attributes.value("xmlUrl").toString().toLatin1()));
|
||||
ret->feeds.append(podcast);
|
||||
|
||||
// Consume any children and the EndElement.
|
||||
Utilities::ConsumeCurrentElement(reader);
|
||||
}
|
||||
else {
|
||||
// Create a new child container
|
||||
OpmlContainer child;
|
||||
|
||||
// Take the name from the fullname attribute first if it exists.
|
||||
child.name = attributes.value("fullname").toString();
|
||||
if (child.name.isEmpty()) {
|
||||
child.name = attributes.value("text").toString();
|
||||
}
|
||||
|
||||
// Parse its contents and add it to this container
|
||||
ParseOutline(reader, &child);
|
||||
ret->containers.append(child);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case QXmlStreamReader::EndElement:
|
||||
return;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
72
src/podcasts/podcastparser.h
Normal file
72
src/podcasts/podcastparser.h
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 PODCASTPARSER_H
|
||||
#define PODCASTPARSER_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
|
||||
#include "podcast.h"
|
||||
|
||||
class QIODevice;
|
||||
class QXmlStreamReader;
|
||||
|
||||
class OpmlContainer;
|
||||
|
||||
// Reads XML data from a QIODevice.
|
||||
// Returns either a Podcast or an OpmlContainer depending on what was inside the XML document.
|
||||
class PodcastParser {
|
||||
public:
|
||||
PodcastParser();
|
||||
|
||||
static const char *kAtomNamespace;
|
||||
static const char *kItunesNamespace;
|
||||
|
||||
const QStringList &supported_mime_types() const { return supported_mime_types_; }
|
||||
bool SupportsContentType(const QString &content_type) const;
|
||||
|
||||
// You should check the type of the returned QVariant to see whether it contains a Podcast or an OpmlContainer.
|
||||
// If the QVariant isNull then an error occurred parsing the XML.
|
||||
QVariant Load(QIODevice *device, const QUrl &url) const;
|
||||
|
||||
// Really quick test to see if some data might be supported. Load() might still return a null QVariant.
|
||||
bool TryMagic(const QByteArray &data) const;
|
||||
|
||||
private:
|
||||
bool ParseRss(QXmlStreamReader *reader, Podcast *ret) const;
|
||||
void ParseChannel(QXmlStreamReader *reader, Podcast *ret) const;
|
||||
void ParseImage(QXmlStreamReader *reader, Podcast *ret) const;
|
||||
void ParseItunesOwner(QXmlStreamReader *reader, Podcast *ret) const;
|
||||
void ParseItem(QXmlStreamReader *reader, Podcast *ret) const;
|
||||
|
||||
bool ParseOpml(QXmlStreamReader *reader, OpmlContainer *ret) const;
|
||||
void ParseOutline(QXmlStreamReader *reader, OpmlContainer *ret) const;
|
||||
|
||||
private:
|
||||
QStringList supported_mime_types_;
|
||||
};
|
||||
|
||||
#endif // PODCASTPARSER_H
|
||||
919
src/podcasts/podcastservice.cpp
Normal file
919
src/podcasts/podcastservice.cpp
Normal file
@@ -0,0 +1,919 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012-2013, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2013-2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2014, Simeon Bird <sbird@andrew.cmu.edu>
|
||||
* Copyright 2019-2021, 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 "podcastservice.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QtConcurrentRun>
|
||||
#include <QSet>
|
||||
#include <QMap>
|
||||
#include <QVariant>
|
||||
#include <QString>
|
||||
#include <QIcon>
|
||||
#include <QDateTime>
|
||||
#include <QFont>
|
||||
#include <QStandardItemModel>
|
||||
#include <QStandardItem>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QMenu>
|
||||
#include <QAction>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/mergedproxymodel.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "core/standarditemiconloader.h"
|
||||
//#include "podcastsmodel.h"
|
||||
#include "podcastservicemodel.h"
|
||||
#include "collection/collectionview.h"
|
||||
#include "opmlcontainer.h"
|
||||
#include "podcastbackend.h"
|
||||
#include "podcastdeleter.h"
|
||||
#include "podcastdownloader.h"
|
||||
#include "podcastinfodialog.h"
|
||||
#include "podcastupdater.h"
|
||||
#include "addpodcastdialog.h"
|
||||
#include "organize/organizedialog.h"
|
||||
#include "organize/organizeerrordialog.h"
|
||||
#include "playlist/playlistmanager.h"
|
||||
#include "device/devicemanager.h"
|
||||
#include "device/devicestatefiltermodel.h"
|
||||
#include "device/deviceview.h"
|
||||
|
||||
const char* PodcastService::kServiceName = "Podcasts";
|
||||
const char *PodcastService::kSettingsGroup = "Podcasts";
|
||||
|
||||
class PodcastSortProxyModel : public QSortFilterProxyModel {
|
||||
public:
|
||||
explicit PodcastSortProxyModel(QObject *parent = nullptr);
|
||||
|
||||
protected:
|
||||
bool lessThan(const QModelIndex &left, const QModelIndex &right) const;
|
||||
};
|
||||
|
||||
PodcastService::PodcastService(Application *app, QObject *parent)
|
||||
: InternetService(Song::Source_Unknown, kServiceName, QString(), QString(), SettingsDialog::Page_Appearance, app, parent),
|
||||
use_pretty_covers_(true),
|
||||
hide_listened_(false),
|
||||
show_episodes_(0),
|
||||
icon_loader_(new StandardItemIconLoader(app->album_cover_loader(), this)),
|
||||
backend_(app->podcast_backend()),
|
||||
model_(new PodcastServiceModel(this)),
|
||||
proxy_(new PodcastSortProxyModel(this)),
|
||||
root_(nullptr),
|
||||
organize_dialog_(new OrganizeDialog(app_->task_manager())) {
|
||||
|
||||
icon_loader_->SetModel(model_);
|
||||
proxy_->setSourceModel(model_);
|
||||
proxy_->setDynamicSortFilter(true);
|
||||
proxy_->sort(0);
|
||||
|
||||
QObject::connect(backend_, &PodcastBackend::SubscriptionAdded, this, &PodcastService::SubscriptionAdded);
|
||||
QObject::connect(backend_, &PodcastBackend::SubscriptionRemoved, this, &PodcastService::SubscriptionRemoved);
|
||||
QObject::connect(backend_, &PodcastBackend::EpisodesAdded, this, &PodcastService::EpisodesAdded);
|
||||
QObject::connect(backend_, &PodcastBackend::EpisodesUpdated, this, &PodcastService::EpisodesUpdated);
|
||||
|
||||
QObject::connect(app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, this, &PodcastService::CurrentSongChanged);
|
||||
QObject::connect(organize_dialog_.get(), &OrganizeDialog::FileCopied, this, &PodcastService::FileCopied);
|
||||
|
||||
}
|
||||
|
||||
PodcastService::~PodcastService() {}
|
||||
|
||||
PodcastSortProxyModel::PodcastSortProxyModel(QObject *parent) : QSortFilterProxyModel(parent) {}
|
||||
|
||||
bool PodcastSortProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const {
|
||||
|
||||
Q_UNUSED(left)
|
||||
Q_UNUSED(right)
|
||||
|
||||
#if 0
|
||||
const int left_type = left.data(InternetModel::Role_Type).toInt();
|
||||
const int right_type = right.data(InternetModel::Role_Type).toInt();
|
||||
|
||||
// The special Add Podcast item comes first
|
||||
if (left_type == PodcastService::Type_AddPodcast)
|
||||
return true;
|
||||
else if (right_type == PodcastService::Type_AddPodcast)
|
||||
return false;
|
||||
|
||||
// Otherwise we only compare identical typed items.
|
||||
if (left_type != right_type)
|
||||
return QSortFilterProxyModel::lessThan(left, right);
|
||||
|
||||
switch (left_type) {
|
||||
case PodcastService::Type_Podcast:
|
||||
return left.data().toString().localeAwareCompare(right.data().toString()) < 0;
|
||||
|
||||
case PodcastService::Type_Episode: {
|
||||
const PodcastEpisode left_episode = left.data(PodcastService::Role_Episode).value<PodcastEpisode>();
|
||||
const PodcastEpisode right_episode = right.data(PodcastService::Role_Episode).value<PodcastEpisode>();
|
||||
|
||||
return left_episode.publication_date() > right_episode.publication_date();
|
||||
}
|
||||
|
||||
default:
|
||||
return QSortFilterProxyModel::lessThan(left, right);
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
QStandardItem *PodcastService::CreateRootItem() {
|
||||
|
||||
#if 0
|
||||
root_ = new QStandardItem(IconLoader::Load("podcast"), tr("Podcasts"));
|
||||
root_->setData(true, InternetModel::Role_CanLazyLoad);
|
||||
return root_;
|
||||
#endif
|
||||
return nullptr;
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::CopyToDevice() {
|
||||
|
||||
if (selected_episodes_.isEmpty() && explicitly_selected_podcasts_.isEmpty()) {
|
||||
CopyToDevice(backend_->GetNewDownloadedEpisodes());
|
||||
}
|
||||
else {
|
||||
CopyToDevice(selected_episodes_, explicitly_selected_podcasts_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::CopyToDevice(const PodcastEpisodeList &episodes_list) {
|
||||
|
||||
SongList songs;
|
||||
Podcast podcast;
|
||||
for (const PodcastEpisode &episode : episodes_list) {
|
||||
podcast = backend_->GetSubscriptionById(episode.podcast_database_id());
|
||||
songs.append(episode.ToSong(podcast));
|
||||
}
|
||||
|
||||
if (songs.isEmpty()) return;
|
||||
|
||||
organize_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true);
|
||||
organize_dialog_->SetCopy(true);
|
||||
if (organize_dialog_->SetSongs(songs)) organize_dialog_->show();
|
||||
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::CopyToDevice(const QModelIndexList &episode_indexes, const QModelIndexList &podcast_indexes) {
|
||||
|
||||
PodcastEpisodeList episodes;
|
||||
for (const QModelIndex &idx : episode_indexes) {
|
||||
PodcastEpisode episode_tmp = idx.data(Role_Episode).value<PodcastEpisode>();
|
||||
if (episode_tmp.downloaded()) episodes << episode_tmp;
|
||||
}
|
||||
|
||||
for (const QModelIndex &idx : podcast_indexes) {
|
||||
for (int i = 0; i < idx.model()->rowCount(idx); ++i) {
|
||||
const QModelIndex &idx2 = idx.model()->index(i, 0, idx);
|
||||
PodcastEpisode episode_tmp = idx2.data(Role_Episode).value<PodcastEpisode>();
|
||||
if (episode_tmp.downloaded() && !episode_tmp.listened())
|
||||
episodes << episode_tmp;
|
||||
}
|
||||
}
|
||||
SongList songs;
|
||||
for (const PodcastEpisode &episode : episodes) {
|
||||
Podcast podcast = backend_->GetSubscriptionById(episode.podcast_database_id());
|
||||
songs.append(episode.ToSong(podcast));
|
||||
}
|
||||
|
||||
organize_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true);
|
||||
organize_dialog_->SetCopy(true);
|
||||
if (organize_dialog_->SetSongs(songs)) organize_dialog_->show();
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::CancelDownload() {
|
||||
CancelDownload(selected_episodes_, explicitly_selected_podcasts_);
|
||||
}
|
||||
|
||||
void PodcastService::CancelDownload(const QModelIndexList &episode_indexes, const QModelIndexList &podcast_indexes) {
|
||||
|
||||
PodcastEpisodeList episodes;
|
||||
for (const QModelIndex &idx : episode_indexes) {
|
||||
if (!idx.isValid()) continue;
|
||||
PodcastEpisode episode_tmp = idx.data(Role_Episode).value<PodcastEpisode>();
|
||||
episodes << episode_tmp;
|
||||
}
|
||||
|
||||
for (const QModelIndex &idx : podcast_indexes) {
|
||||
if (!idx.isValid()) continue;
|
||||
for (int i = 0; i < idx.model()->rowCount(idx); ++i) {
|
||||
const QModelIndex &idx2 = idx.model()->index(i, 0, idx);
|
||||
if (!idx2.isValid()) continue;
|
||||
PodcastEpisode episode_tmp = idx2.data(Role_Episode).value<PodcastEpisode>();
|
||||
episodes << episode_tmp;
|
||||
}
|
||||
}
|
||||
episodes = app_->podcast_downloader()->EpisodesDownloading(episodes);
|
||||
app_->podcast_downloader()->cancelDownload(episodes);
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::LazyPopulate(QStandardItem *parent) {
|
||||
|
||||
Q_UNUSED(parent)
|
||||
#if 0
|
||||
switch (parent->data(InternetModel::Role_Type).toInt()) {
|
||||
case InternetModel::Type_Service:
|
||||
PopulatePodcastList(model_->invisibleRootItem());
|
||||
model()->merged_model()->AddSubModel(parent->index(), proxy_);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::PopulatePodcastList(QStandardItem *parent) {
|
||||
|
||||
// Do this here since the downloader won't be created yet in the ctor.
|
||||
QObject::connect(app_->podcast_downloader(), &PodcastDownloader::ProgressChanged, this, &PodcastService::DownloadProgressChanged);
|
||||
|
||||
if (default_icon_.isNull()) {
|
||||
default_icon_ = IconLoader::Load("podcast");
|
||||
}
|
||||
|
||||
PodcastList podcasts = backend_->GetAllSubscriptions();
|
||||
for (const Podcast &podcast : podcasts) {
|
||||
parent->appendRow(CreatePodcastItem(podcast));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::ClearPodcastList(QStandardItem *parent) {
|
||||
parent->removeRows(0, parent->rowCount());
|
||||
}
|
||||
|
||||
void PodcastService::UpdatePodcastText(QStandardItem *item, const int unlistened_count) const {
|
||||
|
||||
const Podcast podcast = item->data(Role_Podcast).value<Podcast>();
|
||||
|
||||
QString title = podcast.title().simplified();
|
||||
QFont font;
|
||||
|
||||
if (unlistened_count > 0) {
|
||||
// Add the number of new episodes after the title.
|
||||
title.append(QString(" (%1)").arg(unlistened_count));
|
||||
|
||||
// Set a bold font
|
||||
font.setBold(true);
|
||||
}
|
||||
|
||||
item->setFont(font);
|
||||
item->setText(title);
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::UpdateEpisodeText(QStandardItem *item, const PodcastDownload::State state, const int percent) {
|
||||
|
||||
const PodcastEpisode episode = item->data(Role_Episode).value<PodcastEpisode>();
|
||||
|
||||
QString title = episode.title().simplified();
|
||||
QString tooltip;
|
||||
QFont font;
|
||||
QIcon icon;
|
||||
|
||||
// Unlistened episodes are bold
|
||||
if (!episode.listened()) {
|
||||
font.setBold(true);
|
||||
}
|
||||
|
||||
// Downloaded episodes get an icon
|
||||
if (episode.downloaded()) {
|
||||
if (downloaded_icon_.isNull()) {
|
||||
downloaded_icon_ = IconLoader::Load("document-save");
|
||||
}
|
||||
icon = downloaded_icon_;
|
||||
}
|
||||
|
||||
// Queued or downloading episodes get icons, tooltips, and maybe a title.
|
||||
switch (state) {
|
||||
case PodcastDownload::Queued:
|
||||
if (queued_icon_.isNull()) {
|
||||
queued_icon_ = IconLoader::Load("user-away");
|
||||
}
|
||||
icon = queued_icon_;
|
||||
tooltip = tr("Download queued");
|
||||
break;
|
||||
|
||||
case PodcastDownload::Downloading:
|
||||
if (downloading_icon_.isNull()) {
|
||||
downloading_icon_ = IconLoader::Load("go-down");
|
||||
}
|
||||
icon = downloading_icon_;
|
||||
tooltip = tr("Downloading (%1%)...").arg(percent);
|
||||
title = QString("[ %1% ] %2").arg(QString::number(percent), episode.title());
|
||||
break;
|
||||
|
||||
case PodcastDownload::Finished:
|
||||
case PodcastDownload::NotDownloading:
|
||||
break;
|
||||
}
|
||||
|
||||
item->setFont(font);
|
||||
item->setText(title);
|
||||
item->setIcon(icon);
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::UpdatePodcastText(QStandardItem *item, const PodcastDownload::State state, const int percent) {
|
||||
|
||||
const Podcast podcast = item->data(Role_Podcast).value<Podcast>();
|
||||
|
||||
QString tooltip;
|
||||
QIcon icon;
|
||||
|
||||
// Queued or downloading podcasts get icons, tooltips, and maybe a title.
|
||||
switch (state) {
|
||||
case PodcastDownload::Queued:
|
||||
if (queued_icon_.isNull()) {
|
||||
queued_icon_ = IconLoader::Load("user-away");
|
||||
}
|
||||
icon = queued_icon_;
|
||||
item->setIcon(icon);
|
||||
tooltip = tr("Download queued");
|
||||
break;
|
||||
|
||||
case PodcastDownload::Downloading:
|
||||
if (downloading_icon_.isNull()) {
|
||||
downloading_icon_ = IconLoader::Load("go-down");
|
||||
}
|
||||
icon = downloading_icon_;
|
||||
item->setIcon(icon);
|
||||
tooltip = tr("Downloading (%1%)...").arg(percent);
|
||||
break;
|
||||
|
||||
case PodcastDownload::Finished:
|
||||
case PodcastDownload::NotDownloading:
|
||||
if (podcast.ImageUrlSmall().isValid()) {
|
||||
icon_loader_->LoadIcon(podcast.ImageUrlSmall(), QUrl(), item);
|
||||
}
|
||||
else {
|
||||
item->setIcon(default_icon_);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QStandardItem *PodcastService::CreatePodcastItem(const Podcast &podcast) {
|
||||
|
||||
QStandardItem *item = new QStandardItem;
|
||||
|
||||
// Add the episodes in this podcast and gather aggregate stats.
|
||||
int unlistened_count = 0;
|
||||
qint64 number = 0;
|
||||
for (const PodcastEpisode &episode :
|
||||
backend_->GetEpisodes(podcast.database_id())) {
|
||||
if (!episode.listened()) {
|
||||
unlistened_count++;
|
||||
}
|
||||
|
||||
if (episode.listened() && hide_listened_) {
|
||||
continue;
|
||||
}
|
||||
else {
|
||||
item->appendRow(CreatePodcastEpisodeItem(episode));
|
||||
++number;
|
||||
}
|
||||
|
||||
if ((number >= show_episodes_) && (show_episodes_ != 0)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
item->setIcon(default_icon_);
|
||||
//item->setData(Type_Podcast, InternetModel::Role_Type);
|
||||
item->setData(QVariant::fromValue(podcast), Role_Podcast);
|
||||
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsSelectable);
|
||||
UpdatePodcastText(item, unlistened_count);
|
||||
|
||||
// Load the podcast's image if it has one
|
||||
if (podcast.ImageUrlSmall().isValid()) {
|
||||
icon_loader_->LoadIcon(podcast.ImageUrlSmall(), QUrl(), item);
|
||||
}
|
||||
|
||||
podcasts_by_database_id_[podcast.database_id()] = item;
|
||||
|
||||
return item;
|
||||
|
||||
}
|
||||
|
||||
QStandardItem *PodcastService::CreatePodcastEpisodeItem(const PodcastEpisode &episode) {
|
||||
|
||||
QStandardItem *item = new QStandardItem;
|
||||
item->setText(episode.title().simplified());
|
||||
//item->setData(Type_Episode, InternetModel::Role_Type);
|
||||
item->setData(QVariant::fromValue(episode), Role_Episode);
|
||||
//item->setData(InternetModel::PlayBehaviour_UseSongLoader, InternetModel::Role_PlayBehaviour);
|
||||
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsSelectable);
|
||||
|
||||
UpdateEpisodeText(item);
|
||||
|
||||
episodes_by_database_id_[episode.database_id()] = item;
|
||||
|
||||
return item;
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::ShowContextMenu(const QPoint &global_pos) {
|
||||
|
||||
if (!context_menu_) {
|
||||
context_menu_ = new QMenu;
|
||||
context_menu_->addAction(IconLoader::Load("list-add"), tr("Add podcast..."), this, &PodcastService::AddPodcast);
|
||||
context_menu_->addAction(IconLoader::Load("view-refresh"), tr("Update all podcasts"), app_->podcast_updater(), &PodcastUpdater::UpdateAllPodcastsNow);
|
||||
|
||||
context_menu_->addSeparator();
|
||||
//context_menu_->addActions(GetPlaylistActions());
|
||||
|
||||
context_menu_->addSeparator();
|
||||
update_selected_action_ = context_menu_->addAction(IconLoader::Load("view-refresh"), tr("Update this podcast"), this, &PodcastService::UpdateSelectedPodcast);
|
||||
download_selected_action_ = context_menu_->addAction(IconLoader::Load("download"), "", this, &PodcastService::DownloadSelectedEpisode);
|
||||
info_selected_action_ = context_menu_->addAction(IconLoader::Load("about-info"), tr("Podcast information"), this, &PodcastService::PodcastInfo);
|
||||
delete_downloaded_action_ = context_menu_->addAction(IconLoader::Load("edit-delete"), tr("Delete downloaded data"), this, &PodcastService::DeleteDownloadedData);
|
||||
copy_to_device_ = context_menu_->addAction(IconLoader::Load("multimedia-player-ipod-mini-blue"), tr("Copy to device..."), this, QOverload<>::of(&PodcastService::CopyToDevice));
|
||||
cancel_download_ = context_menu_->addAction(IconLoader::Load("cancel"), tr("Cancel download"), this, QOverload<>::of(&PodcastService::CancelDownload));
|
||||
remove_selected_action_ = context_menu_->addAction(IconLoader::Load("list-remove"), tr("Unsubscribe"), this, QOverload<>::of(&PodcastService::RemoveSelectedPodcast));
|
||||
|
||||
context_menu_->addSeparator();
|
||||
set_new_action_ = context_menu_->addAction(tr("Mark as new"), this, &PodcastService::SetNew);
|
||||
set_listened_action_ = context_menu_->addAction(tr("Mark as listened"), this, QOverload<>::of(&PodcastService::SetListened));
|
||||
|
||||
context_menu_->addSeparator();
|
||||
context_menu_->addAction(IconLoader::Load("configure"), tr("Configure podcasts..."), this, &PodcastService::ShowConfig);
|
||||
|
||||
copy_to_device_->setDisabled(app_->device_manager()->connected_devices_model()->rowCount() == 0);
|
||||
connect(app_->device_manager()->connected_devices_model(), &DeviceStateFilterModel::IsEmptyChanged, copy_to_device_, &QAction::setDisabled);
|
||||
}
|
||||
|
||||
selected_episodes_.clear();
|
||||
selected_podcasts_.clear();
|
||||
explicitly_selected_podcasts_.clear();
|
||||
QSet<int> podcast_ids;
|
||||
#if 0
|
||||
for (const QModelIndex &index : model()->selected_indexes()) {
|
||||
switch (index.data(InternetModel::Role_Type).toInt()) {
|
||||
case Type_Podcast: {
|
||||
const int id = index.data(Role_Podcast).value<Podcast>().database_id();
|
||||
if (!podcast_ids.contains(id)) {
|
||||
selected_podcasts_.append(index);
|
||||
explicitly_selected_podcasts_.append(index);
|
||||
podcast_ids.insert(id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Type_Episode: {
|
||||
selected_episodes_.append(index);
|
||||
|
||||
// Add the parent podcast as well.
|
||||
const QModelIndex parent = index.parent();
|
||||
const int id = parent.data(Role_Podcast).value<Podcast>().database_id();
|
||||
if (!podcast_ids.contains(id)) {
|
||||
selected_podcasts_.append(parent);
|
||||
podcast_ids.insert(id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
const bool episodes = !selected_episodes_.isEmpty();
|
||||
const bool podcasts = !selected_podcasts_.isEmpty();
|
||||
|
||||
update_selected_action_->setEnabled(podcasts);
|
||||
remove_selected_action_->setEnabled(podcasts);
|
||||
set_new_action_->setEnabled(episodes || podcasts);
|
||||
set_listened_action_->setEnabled(episodes || podcasts);
|
||||
cancel_download_->setEnabled(episodes || podcasts);
|
||||
|
||||
if (selected_episodes_.count() == 1) {
|
||||
const PodcastEpisode episode = selected_episodes_[0].data(Role_Episode).value<PodcastEpisode>();
|
||||
const bool downloaded = episode.downloaded();
|
||||
const bool listened = episode.listened();
|
||||
|
||||
download_selected_action_->setEnabled(!downloaded);
|
||||
delete_downloaded_action_->setEnabled(downloaded);
|
||||
|
||||
if (explicitly_selected_podcasts_.isEmpty()) {
|
||||
set_new_action_->setEnabled(listened);
|
||||
set_listened_action_->setEnabled(!listened || !episode.listened_date().isValid());
|
||||
}
|
||||
}
|
||||
else {
|
||||
download_selected_action_->setEnabled(episodes);
|
||||
delete_downloaded_action_->setEnabled(episodes);
|
||||
}
|
||||
|
||||
if (selected_podcasts_.count() == 1) {
|
||||
if (selected_episodes_.count() == 1) {
|
||||
info_selected_action_->setText(tr("Episode information"));
|
||||
info_selected_action_->setEnabled(true);
|
||||
}
|
||||
else {
|
||||
info_selected_action_->setText(tr("Podcast information"));
|
||||
info_selected_action_->setEnabled(true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
info_selected_action_->setText(tr("Podcast information"));
|
||||
info_selected_action_->setEnabled(false);
|
||||
}
|
||||
|
||||
if (explicitly_selected_podcasts_.isEmpty() && selected_episodes_.isEmpty()) {
|
||||
PodcastEpisodeList epis = backend_->GetNewDownloadedEpisodes();
|
||||
set_listened_action_->setEnabled(!epis.isEmpty());
|
||||
}
|
||||
|
||||
if (selected_episodes_.count() > 1) {
|
||||
download_selected_action_->setText(
|
||||
tr("Download %n episodes", "", selected_episodes_.count()));
|
||||
}
|
||||
else {
|
||||
download_selected_action_->setText(tr("Download this episode"));
|
||||
}
|
||||
|
||||
//GetAppendToPlaylistAction()->setEnabled(episodes || podcasts);
|
||||
//GetReplacePlaylistAction()->setEnabled(episodes || podcasts);
|
||||
//GetOpenInNewPlaylistAction()->setEnabled(episodes || podcasts);
|
||||
|
||||
context_menu_->popup(global_pos);
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::UpdateSelectedPodcast() {
|
||||
|
||||
for (const QModelIndex &index : selected_podcasts_) {
|
||||
app_->podcast_updater()->UpdatePodcastNow(
|
||||
index.data(Role_Podcast).value<Podcast>());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::RemoveSelectedPodcast() {
|
||||
|
||||
for (const QModelIndex &index : selected_podcasts_) {
|
||||
backend_->Unsubscribe(index.data(Role_Podcast).value<Podcast>());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::ReloadSettings() {
|
||||
|
||||
InitialLoadSettings();
|
||||
ClearPodcastList(model_->invisibleRootItem());
|
||||
PopulatePodcastList(model_->invisibleRootItem());
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::InitialLoadSettings() {
|
||||
|
||||
QSettings s;
|
||||
|
||||
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
|
||||
use_pretty_covers_ = s.value("pretty_covers", true).toBool();
|
||||
s.endGroup();
|
||||
|
||||
s.beginGroup(kSettingsGroup);
|
||||
hide_listened_ = s.value("hide_listened", false).toBool();
|
||||
show_episodes_ = s.value("show_episodes", 0).toInt();
|
||||
s.endGroup();
|
||||
|
||||
// TODO(notme): reload the podcast icons that are already loaded?
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::EnsureAddPodcastDialogCreated() {
|
||||
add_podcast_dialog_.reset(new AddPodcastDialog(app_));
|
||||
}
|
||||
|
||||
void PodcastService::AddPodcast() {
|
||||
|
||||
EnsureAddPodcastDialogCreated();
|
||||
add_podcast_dialog_->show();
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::FileCopied(int database_id) {
|
||||
SetListened(PodcastEpisodeList() << backend_->GetEpisodeById(database_id), true);
|
||||
}
|
||||
|
||||
void PodcastService::SubscriptionAdded(const Podcast &podcast) {
|
||||
|
||||
// Ensure the root item is lazy loaded already
|
||||
LazyLoadRoot();
|
||||
|
||||
// The podcast might already be in the list - maybe the LazyLoadRoot() above
|
||||
// added it.
|
||||
QStandardItem *item = podcasts_by_database_id_[podcast.database_id()];
|
||||
if (!item) {
|
||||
item = CreatePodcastItem(podcast);
|
||||
model_->appendRow(item);
|
||||
}
|
||||
|
||||
//emit ScrollToIndex(MapToMergedModel(item->index()));
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::SubscriptionRemoved(const Podcast &podcast) {
|
||||
|
||||
QStandardItem *item = podcasts_by_database_id_.take(podcast.database_id());
|
||||
if (item) {
|
||||
// Remove any episode ID -> item mappings for the episodes in this podcast.
|
||||
for (int i = 0; i < item->rowCount(); ++i) {
|
||||
QStandardItem *episode_item = item->child(i);
|
||||
const int episode_id = episode_item->data(Role_Episode).value<PodcastEpisode>().database_id();
|
||||
|
||||
episodes_by_database_id_.remove(episode_id);
|
||||
}
|
||||
|
||||
// Remove this episode's row
|
||||
model_->removeRow(item->row());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::EpisodesAdded(const PodcastEpisodeList &episodes) {
|
||||
|
||||
QSet<int> seen_podcast_ids;
|
||||
|
||||
for (const PodcastEpisode &episode : episodes) {
|
||||
const int database_id = episode.podcast_database_id();
|
||||
QStandardItem *parent = podcasts_by_database_id_[database_id];
|
||||
if (!parent) continue;
|
||||
|
||||
parent->appendRow(CreatePodcastEpisodeItem(episode));
|
||||
if (!seen_podcast_ids.contains(database_id)) {
|
||||
// Update the unlistened count text once for each podcast
|
||||
int unlistened_count = 0;
|
||||
for (const PodcastEpisode &i : backend_->GetEpisodes(database_id)) {
|
||||
if (!i.listened()) {
|
||||
++unlistened_count;
|
||||
}
|
||||
}
|
||||
|
||||
UpdatePodcastText(parent, unlistened_count);
|
||||
seen_podcast_ids.insert(database_id);
|
||||
}
|
||||
const Podcast podcast = parent->data(Role_Podcast).value<Podcast>();
|
||||
ReloadPodcast(podcast);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::EpisodesUpdated(const PodcastEpisodeList &episodes) {
|
||||
|
||||
QSet<int> seen_podcast_ids;
|
||||
QMap<int, Podcast> podcasts_map;
|
||||
|
||||
for (const PodcastEpisode &episode : episodes) {
|
||||
const int podcast_database_id = episode.podcast_database_id();
|
||||
QStandardItem *item = episodes_by_database_id_[episode.database_id()];
|
||||
QStandardItem *parent = podcasts_by_database_id_[podcast_database_id];
|
||||
if (!item || !parent) continue;
|
||||
// Update the episode data on the item, and update the item's text.
|
||||
item->setData(QVariant::fromValue(episode), Role_Episode);
|
||||
UpdateEpisodeText(item);
|
||||
|
||||
// Update the parent podcast's text too.
|
||||
if (!seen_podcast_ids.contains(podcast_database_id)) {
|
||||
// Update the unlistened count text once for each podcast
|
||||
int unlistened_count = 0;
|
||||
for (const PodcastEpisode &i : backend_->GetEpisodes(podcast_database_id)) {
|
||||
if (!i.listened()) {
|
||||
++unlistened_count;
|
||||
}
|
||||
}
|
||||
|
||||
UpdatePodcastText(parent, unlistened_count);
|
||||
seen_podcast_ids.insert(podcast_database_id);
|
||||
}
|
||||
const Podcast podcast = parent->data(Role_Podcast).value<Podcast>();
|
||||
podcasts_map[podcast.database_id()] = podcast;
|
||||
}
|
||||
|
||||
QList<Podcast> podcast_values = podcasts_map.values();
|
||||
for (const Podcast &podcast_tmp : podcast_values) {
|
||||
ReloadPodcast(podcast_tmp);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::DownloadSelectedEpisode() {
|
||||
|
||||
for (const QModelIndex &idx : selected_episodes_) {
|
||||
app_->podcast_downloader()->DownloadEpisode(idx.data(Role_Episode).value<PodcastEpisode>());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::PodcastInfo() {
|
||||
|
||||
if (selected_podcasts_.isEmpty()) {
|
||||
// Should never happen.
|
||||
return;
|
||||
}
|
||||
const Podcast podcast = selected_podcasts_[0].data(Role_Podcast).value<Podcast>();
|
||||
podcast_info_dialog_.reset(new PodcastInfoDialog(app_));
|
||||
|
||||
if (selected_episodes_.count() == 1) {
|
||||
const PodcastEpisode episode = selected_episodes_[0].data(Role_Episode).value<PodcastEpisode>();
|
||||
podcast_info_dialog_->ShowEpisode(episode, podcast);
|
||||
}
|
||||
else {
|
||||
podcast_info_dialog_->ShowPodcast(podcast);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::DeleteDownloadedData() {
|
||||
|
||||
for (const QModelIndex &idx : selected_episodes_) {
|
||||
app_->podcast_deleter()->DeleteEpisode(idx.data(Role_Episode).value<PodcastEpisode>());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::DownloadProgressChanged(const PodcastEpisode &episode, const PodcastDownload::State state, const int percent) {
|
||||
|
||||
QStandardItem *item = episodes_by_database_id_[episode.database_id()];
|
||||
QStandardItem *item2 = podcasts_by_database_id_[episode.podcast_database_id()];
|
||||
if (!item || !item2) return;
|
||||
|
||||
UpdateEpisodeText(item, state, percent);
|
||||
UpdatePodcastText(item2, state, percent);
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::ShowConfig() {
|
||||
//app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Podcasts);
|
||||
}
|
||||
|
||||
void PodcastService::CurrentSongChanged(const Song &metadata) {
|
||||
|
||||
// This does two db queries, and we are called on every song change, so run this off the main thread.
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
(void)QtConcurrent::run(&PodcastService::UpdatePodcastListenedStateAsync, this, metadata);
|
||||
#else
|
||||
(void)QtConcurrent::run(this, &PodcastService::UpdatePodcastListenedStateAsync, metadata);
|
||||
#endif
|
||||
}
|
||||
|
||||
void PodcastService::UpdatePodcastListenedStateAsync(const Song &metadata) {
|
||||
|
||||
// Check whether this song is one of our podcast episodes.
|
||||
PodcastEpisode episode = backend_->GetEpisodeByUrlOrLocalUrl(metadata.url());
|
||||
if (!episode.is_valid()) return;
|
||||
|
||||
// Mark it as listened if it's not already
|
||||
if (!episode.listened() || !episode.listened_date().isValid()) {
|
||||
episode.set_listened(true);
|
||||
episode.set_listened_date(QDateTime::currentDateTime());
|
||||
backend_->UpdateEpisodes(PodcastEpisodeList() << episode);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::SetNew() {
|
||||
SetListened(selected_episodes_, explicitly_selected_podcasts_, false);
|
||||
}
|
||||
|
||||
void PodcastService::SetListened() {
|
||||
|
||||
if (selected_episodes_.isEmpty() && explicitly_selected_podcasts_.isEmpty()) {
|
||||
SetListened(backend_->GetNewDownloadedEpisodes(), true);
|
||||
}
|
||||
else {
|
||||
SetListened(selected_episodes_, explicitly_selected_podcasts_, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::SetListened(const PodcastEpisodeList &episodes_list, const bool listened) {
|
||||
|
||||
PodcastEpisodeList episodes;
|
||||
QDateTime current_date_time = QDateTime::currentDateTime();
|
||||
for (PodcastEpisode episode : episodes_list) {
|
||||
episode.set_listened(listened);
|
||||
if (listened) {
|
||||
episode.set_listened_date(current_date_time);
|
||||
}
|
||||
episodes << episode;
|
||||
}
|
||||
|
||||
backend_->UpdateEpisodes(episodes);
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::SetListened(const QModelIndexList &episode_indexes, const QModelIndexList& podcast_indexes, const bool listened) {
|
||||
|
||||
PodcastEpisodeList episodes;
|
||||
|
||||
// Get all the episodes from the indexes.
|
||||
for (const QModelIndex& index : episode_indexes) {
|
||||
episodes << index.data(Role_Episode).value<PodcastEpisode>();
|
||||
}
|
||||
|
||||
for (const QModelIndex& podcast : podcast_indexes) {
|
||||
for (int i = 0; i < podcast.model()->rowCount(podcast); ++i) {
|
||||
const QModelIndex& index = podcast.model()->index(i, 0, podcast);
|
||||
episodes << index.data(Role_Episode).value<PodcastEpisode>();
|
||||
}
|
||||
}
|
||||
|
||||
// Update each one with the new state and maybe the listened time.
|
||||
QDateTime current_date_time = QDateTime::currentDateTime();
|
||||
for (int i = 0; i < episodes.count(); ++i) {
|
||||
PodcastEpisode *episode = &episodes[i];
|
||||
episode->set_listened(listened);
|
||||
if (listened) {
|
||||
episode->set_listened_date(current_date_time);
|
||||
}
|
||||
}
|
||||
|
||||
backend_->UpdateEpisodes(episodes);
|
||||
|
||||
}
|
||||
|
||||
QModelIndex PodcastService::MapToMergedModel(const QModelIndex &idx) const {
|
||||
|
||||
Q_UNUSED(idx)
|
||||
//return model()->merged_model()->mapFromSource(proxy_->mapFromSource(index));
|
||||
return QModelIndex();
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::LazyLoadRoot() {
|
||||
|
||||
#if 0
|
||||
if (root_->data(InternetModel::Role_CanLazyLoad).toBool()) {
|
||||
root_->setData(false, InternetModel::Role_CanLazyLoad);
|
||||
LazyPopulate(root_);
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::SubscribeAndShow(const QVariant &podcast_or_opml) {
|
||||
|
||||
if (podcast_or_opml.canConvert<Podcast>()) {
|
||||
Podcast podcast(podcast_or_opml.value<Podcast>());
|
||||
backend_->Subscribe(&podcast);
|
||||
|
||||
// Lazy load the root item if it hasn't been already
|
||||
LazyLoadRoot();
|
||||
|
||||
QStandardItem *item = podcasts_by_database_id_[podcast.database_id()];
|
||||
if (item) {
|
||||
// There will be an item already if this podcast was already there, otherwise it'll be scrolled to when the item is created.
|
||||
//emit ScrollToIndex(MapToMergedModel(item->index()));
|
||||
}
|
||||
}
|
||||
else if (podcast_or_opml.canConvert<OpmlContainer>()) {
|
||||
EnsureAddPodcastDialogCreated();
|
||||
|
||||
add_podcast_dialog_->ShowWithOpml(podcast_or_opml.value<OpmlContainer>());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastService::ReloadPodcast(const Podcast &podcast) {
|
||||
|
||||
if (!(hide_listened_ || (show_episodes_ > 0))) {
|
||||
return;
|
||||
}
|
||||
QStandardItem *item = podcasts_by_database_id_[podcast.database_id()];
|
||||
|
||||
model_->invisibleRootItem()->removeRow(item->row());
|
||||
model_->invisibleRootItem()->appendRow(CreatePodcastItem(podcast));
|
||||
|
||||
}
|
||||
178
src/podcasts/podcastservice.h
Normal file
178
src/podcasts/podcastservice.h
Normal file
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012-2013, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2013-2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2014, Simeon Bird <sbird@andrew.cmu.edu>
|
||||
* Copyright 2019-2021, 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 PODCASTSERVICE_H
|
||||
#define PODCASTSERVICE_H
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QMap>
|
||||
#include <QIcon>
|
||||
#include <QScopedPointer>
|
||||
|
||||
//#include "internet/internetmodel.h"
|
||||
#include "internet/internetservice.h"
|
||||
#include "podcastdeleter.h"
|
||||
#include "podcastdownloader.h"
|
||||
|
||||
class QMenu;
|
||||
class QAction;
|
||||
|
||||
class AddPodcastDialog;
|
||||
class PodcastInfoDialog;
|
||||
class OrganizeDialog;
|
||||
class Podcast;
|
||||
class PodcastBackend;
|
||||
class PodcastEpisode;
|
||||
class StandardItemIconLoader;
|
||||
|
||||
class QStandardItemModel;
|
||||
class QStandardItem;
|
||||
class QSortFilterProxyModel;
|
||||
|
||||
class PodcastService : public InternetService {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PodcastService(Application *app, QObject *parent);
|
||||
~PodcastService();
|
||||
|
||||
static const char *kServiceName;
|
||||
static const char *kSettingsGroup;
|
||||
|
||||
enum Type {
|
||||
Type_AddPodcast = 0,
|
||||
Type_Podcast,
|
||||
Type_Episode
|
||||
};
|
||||
|
||||
enum Role {
|
||||
Role_Podcast = 0,
|
||||
Role_Episode
|
||||
};
|
||||
|
||||
QStandardItem *CreateRootItem();
|
||||
void LazyPopulate(QStandardItem *parent);
|
||||
bool has_initial_load_settings() const { return true; }
|
||||
void ShowContextMenu(const QPoint &global_pos);
|
||||
void ReloadSettings();
|
||||
void InitialLoadSettings();
|
||||
// Called by SongLoader when the user adds a Podcast URL directly.
|
||||
// Adds a subscription to the podcast and displays it in the UI.
|
||||
// If the QVariant contains an OPML file then this displays it in the Add Podcast dialog.
|
||||
void SubscribeAndShow(const QVariant &podcast_or_opml);
|
||||
|
||||
public slots:
|
||||
void AddPodcast();
|
||||
void FileCopied(const int database_id);
|
||||
|
||||
private slots:
|
||||
void UpdateSelectedPodcast();
|
||||
void ReloadPodcast(const Podcast &podcast);
|
||||
void RemoveSelectedPodcast();
|
||||
void DownloadSelectedEpisode();
|
||||
void PodcastInfo();
|
||||
void DeleteDownloadedData();
|
||||
void SetNew();
|
||||
void SetListened();
|
||||
void ShowConfig();
|
||||
|
||||
void SubscriptionAdded(const Podcast &podcast);
|
||||
void SubscriptionRemoved(const Podcast &podcast);
|
||||
void EpisodesAdded(const PodcastEpisodeList &episodes);
|
||||
void EpisodesUpdated(const PodcastEpisodeList &episodes);
|
||||
|
||||
void DownloadProgressChanged(const PodcastEpisode &episode, PodcastDownload::State state, int percent);
|
||||
|
||||
void CurrentSongChanged(const Song &metadata);
|
||||
|
||||
void CopyToDevice();
|
||||
void CopyToDevice(const PodcastEpisodeList &episodes_list);
|
||||
void CopyToDevice(const QModelIndexList &episode_indexes, const QModelIndexList &podcast_indexes);
|
||||
void CancelDownload();
|
||||
void CancelDownload(const QModelIndexList &episode_indexes, const QModelIndexList &podcast_indexes);
|
||||
|
||||
private:
|
||||
void EnsureAddPodcastDialogCreated();
|
||||
|
||||
void UpdatePodcastListenedStateAsync(const Song &metadata);
|
||||
void PopulatePodcastList(QStandardItem *parent);
|
||||
void ClearPodcastList(QStandardItem *parent);
|
||||
void UpdatePodcastText(QStandardItem *item, const int unlistened_count) const;
|
||||
void UpdateEpisodeText(QStandardItem *item, const PodcastDownload::State state = PodcastDownload::NotDownloading, const int percent = 0);
|
||||
void UpdatePodcastText(QStandardItem *item, const PodcastDownload::State state = PodcastDownload::NotDownloading, const int percent = 0);
|
||||
|
||||
QStandardItem *CreatePodcastItem(const Podcast &podcast);
|
||||
QStandardItem *CreatePodcastEpisodeItem(const PodcastEpisode &episode);
|
||||
|
||||
QModelIndex MapToMergedModel(const QModelIndex &idx) const;
|
||||
|
||||
void SetListened(const QModelIndexList &episode_indexes, const QModelIndexList &podcast_indexes, const bool listened);
|
||||
void SetListened(const PodcastEpisodeList &episodes_list, bool listened);
|
||||
|
||||
void LazyLoadRoot();
|
||||
|
||||
private:
|
||||
bool use_pretty_covers_;
|
||||
bool hide_listened_;
|
||||
qint64 show_episodes_;
|
||||
StandardItemIconLoader *icon_loader_;
|
||||
|
||||
// The podcast icon
|
||||
QIcon default_icon_;
|
||||
|
||||
// Episodes get different icons depending on their state
|
||||
QIcon queued_icon_;
|
||||
QIcon downloading_icon_;
|
||||
QIcon downloaded_icon_;
|
||||
|
||||
PodcastBackend *backend_;
|
||||
QStandardItemModel *model_;
|
||||
QSortFilterProxyModel *proxy_;
|
||||
|
||||
QMenu *context_menu_;
|
||||
QAction *update_selected_action_;
|
||||
QAction *remove_selected_action_;
|
||||
QAction *download_selected_action_;
|
||||
QAction *info_selected_action_;
|
||||
QAction *delete_downloaded_action_;
|
||||
QAction *set_new_action_;
|
||||
QAction *set_listened_action_;
|
||||
QAction *copy_to_device_;
|
||||
QAction *cancel_download_;
|
||||
QStandardItem *root_;
|
||||
std::unique_ptr<OrganizeDialog> organize_dialog_;
|
||||
|
||||
QModelIndexList explicitly_selected_podcasts_;
|
||||
QModelIndexList selected_podcasts_;
|
||||
QModelIndexList selected_episodes_;
|
||||
|
||||
QMap<int, QStandardItem*> podcasts_by_database_id_;
|
||||
QMap<int, QStandardItem*> episodes_by_database_id_;
|
||||
|
||||
std::unique_ptr<AddPodcastDialog> add_podcast_dialog_;
|
||||
std::unique_ptr<PodcastInfoDialog> podcast_info_dialog_;
|
||||
};
|
||||
|
||||
#endif // PODCASTSERVICE_H
|
||||
101
src/podcasts/podcastservicemodel.cpp
Normal file
101
src/podcasts/podcastservicemodel.cpp
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 <QList>
|
||||
#include <QVariant>
|
||||
#include <QUrl>
|
||||
#include <QMimeData>
|
||||
|
||||
#include "podcastservicemodel.h"
|
||||
|
||||
#include "podcastservice.h"
|
||||
#include "playlist/songmimedata.h"
|
||||
|
||||
PodcastServiceModel::PodcastServiceModel(QObject* parent) : QStandardItemModel(parent) {}
|
||||
|
||||
QMimeData* PodcastServiceModel::mimeData(const QModelIndexList &indexes) const {
|
||||
SongMimeData *data = new SongMimeData;
|
||||
QList<QUrl> urls;
|
||||
#if 0
|
||||
for (const QModelIndex& index : indexes) {
|
||||
switch (index.data(InternetModel::Role_Type).toInt()) {
|
||||
case PodcastService::Type_Episode:
|
||||
MimeDataForEpisode(index, data, &urls);
|
||||
break;
|
||||
|
||||
case PodcastService::Type_Podcast:
|
||||
MimeDataForPodcast(index, data, &urls);
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
data->setUrls(urls);
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
void PodcastServiceModel::MimeDataForEpisode(const QModelIndex &idx, SongMimeData *data, QList<QUrl>* urls) const {
|
||||
|
||||
QVariant episode_variant = idx.data(PodcastService::Role_Episode);
|
||||
if (!episode_variant.isValid()) return;
|
||||
|
||||
PodcastEpisode episode(episode_variant.value<PodcastEpisode>());
|
||||
|
||||
// Get the podcast from the index's parent
|
||||
Podcast podcast;
|
||||
QVariant podcast_variant = idx.parent().data(PodcastService::Role_Podcast);
|
||||
if (podcast_variant.isValid()) {
|
||||
podcast = podcast_variant.value<Podcast>();
|
||||
}
|
||||
|
||||
Song song = episode.ToSong(podcast);
|
||||
|
||||
data->songs << song;
|
||||
*urls << song.url();
|
||||
|
||||
}
|
||||
|
||||
void PodcastServiceModel::MimeDataForPodcast(const QModelIndex &idx, SongMimeData *data, QList<QUrl> *urls) const {
|
||||
|
||||
// Get the podcast
|
||||
Podcast podcast;
|
||||
QVariant podcast_variant = idx.data(PodcastService::Role_Podcast);
|
||||
if (podcast_variant.isValid()) {
|
||||
podcast = podcast_variant.value<Podcast>();
|
||||
}
|
||||
|
||||
// Add each child episode
|
||||
const int children = idx.model()->rowCount(idx);
|
||||
for (int i = 0; i < children; ++i) {
|
||||
QVariant episode_variant = idx.model()->index(i, 0, idx).data(PodcastService::Role_Episode);
|
||||
if (!episode_variant.isValid()) continue;
|
||||
|
||||
PodcastEpisode episode(episode_variant.value<PodcastEpisode>());
|
||||
Song song = episode.ToSong(podcast);
|
||||
|
||||
data->songs << song;
|
||||
*urls << song.url();
|
||||
}
|
||||
|
||||
}
|
||||
46
src/podcasts/podcastservicemodel.h
Normal file
46
src/podcasts/podcastservicemodel.h
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 PODCASTSERVICEMODEL_H
|
||||
#define PODCASTSERVICEMODEL_H
|
||||
|
||||
#include <QStandardItemModel>
|
||||
#include <QList>
|
||||
#include <QUrl>
|
||||
|
||||
class SongMimeData;
|
||||
|
||||
class PodcastServiceModel : public QStandardItemModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PodcastServiceModel(QObject *parent = nullptr);
|
||||
|
||||
QMimeData* mimeData(const QModelIndexList &indexes) const;
|
||||
|
||||
private:
|
||||
void MimeDataForPodcast(const QModelIndex &idx, SongMimeData* data, QList<QUrl> *urls) const;
|
||||
void MimeDataForEpisode(const QModelIndex &idx, SongMimeData* data, QList<QUrl> *urls) const;
|
||||
};
|
||||
|
||||
#endif // PODCASTSERVICEMODEL_H
|
||||
194
src/podcasts/podcastupdater.cpp
Normal file
194
src/podcasts/podcastupdater.cpp
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 "podcastupdater.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
#include <QList>
|
||||
#include <QUrl>
|
||||
#include <QDateTime>
|
||||
#include <QTimer>
|
||||
#include <QSettings>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "podcastbackend.h"
|
||||
#include "podcasturlloader.h"
|
||||
|
||||
const char *PodcastUpdater::kSettingsGroup = "Podcasts";
|
||||
|
||||
PodcastUpdater::PodcastUpdater(Application *app, QObject *parent)
|
||||
: QObject(parent),
|
||||
app_(app),
|
||||
update_interval_secs_(0),
|
||||
update_timer_(new QTimer(this)),
|
||||
loader_(new PodcastUrlLoader(this)),
|
||||
pending_replies_(0) {
|
||||
|
||||
update_timer_->setSingleShot(true);
|
||||
|
||||
QObject::connect(app_, &Application::SettingsChanged, this, &PodcastUpdater::ReloadSettings);
|
||||
QObject::connect(update_timer_, &QTimer::timeout, this, &PodcastUpdater::UpdateAllPodcastsNow);
|
||||
QObject::connect(app_->podcast_backend(), &PodcastBackend::SubscriptionAdded, this, &PodcastUpdater::SubscriptionAdded);
|
||||
|
||||
ReloadSettings();
|
||||
|
||||
}
|
||||
|
||||
void PodcastUpdater::ReloadSettings() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
|
||||
last_full_update_ = s.value("last_full_update").toDateTime();
|
||||
update_interval_secs_ = s.value("update_interval_secs").toInt();
|
||||
|
||||
s.endGroup();
|
||||
|
||||
RestartTimer();
|
||||
|
||||
}
|
||||
|
||||
void PodcastUpdater::SaveSettings() {
|
||||
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
s.setValue("last_full_update", last_full_update_);
|
||||
s.endGroup();
|
||||
|
||||
}
|
||||
|
||||
void PodcastUpdater::RestartTimer() {
|
||||
|
||||
// Stop any existing timer
|
||||
update_timer_->stop();
|
||||
|
||||
if (pending_replies_ > 0) {
|
||||
// We're still waiting for replies from the last update - don't do anything.
|
||||
return;
|
||||
}
|
||||
|
||||
if (update_interval_secs_ > 0) {
|
||||
if (!last_full_update_.isValid()) {
|
||||
// Updates are enabled and we've never updated before. Do it now.
|
||||
qLog(Info) << "Updating podcasts for the first time";
|
||||
UpdateAllPodcastsNow();
|
||||
}
|
||||
else {
|
||||
const QDateTime next_update = last_full_update_.addSecs(update_interval_secs_);
|
||||
const int secs_until_next_update = QDateTime::currentDateTime().secsTo(next_update);
|
||||
|
||||
if (secs_until_next_update < 0) {
|
||||
qLog(Info) << "Updating podcasts" << (-secs_until_next_update) << "seconds late";
|
||||
UpdateAllPodcastsNow();
|
||||
}
|
||||
else {
|
||||
qLog(Info) << "Updating podcasts at" << next_update << "(in" << secs_until_next_update << "seconds)";
|
||||
update_timer_->start(secs_until_next_update * kMsecPerSec);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastUpdater::SubscriptionAdded(const Podcast& podcast) {
|
||||
|
||||
// Only update a new podcast immediately if it doesn't have an episode list.
|
||||
// We assume that the episode list has already been fetched recently otherwise.
|
||||
if (podcast.episodes().isEmpty()) {
|
||||
UpdatePodcastNow(podcast);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastUpdater::UpdatePodcastNow(const Podcast& podcast) {
|
||||
|
||||
PodcastUrlLoaderReply *reply = loader_->Load(podcast.url());
|
||||
QObject::connect(reply, &PodcastUrlLoaderReply::Finished, this, [this, reply, podcast]() { PodcastLoaded(reply, podcast, false); });
|
||||
|
||||
}
|
||||
|
||||
void PodcastUpdater::UpdateAllPodcastsNow() {
|
||||
|
||||
PodcastList podcasts = app_->podcast_backend()->GetAllSubscriptions();
|
||||
for (const Podcast &podcast : podcasts) {
|
||||
PodcastUrlLoaderReply *reply = loader_->Load(podcast.url());
|
||||
QObject::connect(reply, &PodcastUrlLoaderReply::Finished, this, [this, reply, podcast]() { PodcastLoaded(reply, podcast, true); });
|
||||
|
||||
++pending_replies_;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void PodcastUpdater::PodcastLoaded(PodcastUrlLoaderReply *reply, const Podcast& podcast, bool one_of_many) {
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (one_of_many) {
|
||||
--pending_replies_;
|
||||
if (pending_replies_ == 0) {
|
||||
// This was the last reply we were waiting for. Save this time as being
|
||||
// the last successful update and restart the timer.
|
||||
last_full_update_ = QDateTime::currentDateTime();
|
||||
SaveSettings();
|
||||
RestartTimer();
|
||||
}
|
||||
}
|
||||
|
||||
if (!reply->is_success()) {
|
||||
qLog(Warning) << "Error fetching podcast at" << podcast.url() << ":" << reply->error_text();
|
||||
return;
|
||||
}
|
||||
|
||||
if (reply->result_type() != PodcastUrlLoaderReply::Type_Podcast) {
|
||||
qLog(Warning) << "The URL" << podcast.url() << "no longer contains a podcast";
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the episode URLs we had for this podcast already.
|
||||
QSet<QUrl> existing_urls;
|
||||
for (const PodcastEpisode &episode :
|
||||
app_->podcast_backend()->GetEpisodes(podcast.database_id())) {
|
||||
existing_urls.insert(episode.url());
|
||||
}
|
||||
|
||||
// Add any new episodes
|
||||
PodcastEpisodeList new_episodes;
|
||||
PodcastList reply_podcasts = reply->podcast_results();
|
||||
for (const Podcast &reply_podcast : reply_podcasts) {
|
||||
PodcastEpisodeList episodes = reply_podcast.episodes();
|
||||
for (const PodcastEpisode &episode : episodes) {
|
||||
if (!existing_urls.contains(episode.url())) {
|
||||
PodcastEpisode episode_copy(episode);
|
||||
episode_copy.set_podcast_database_id(podcast.database_id());
|
||||
new_episodes.append(episode_copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app_->podcast_backend()->AddEpisodes(&new_episodes);
|
||||
qLog(Info) << "Added" << new_episodes.count() << "new episodes for" << podcast.url();
|
||||
|
||||
}
|
||||
71
src/podcasts/podcastupdater.h
Normal file
71
src/podcasts/podcastupdater.h
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 PODCASTUPDATER_H
|
||||
#define PODCASTUPDATER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QDateTime>
|
||||
|
||||
class Application;
|
||||
class Podcast;
|
||||
class PodcastUrlLoader;
|
||||
class PodcastUrlLoaderReply;
|
||||
|
||||
class QTimer;
|
||||
|
||||
// Responsible for updating podcasts when they're first subscribed to, and then updating them at regular intervals afterwards.
|
||||
class PodcastUpdater : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PodcastUpdater(Application *app, QObject *parent = nullptr);
|
||||
|
||||
public slots:
|
||||
void UpdateAllPodcastsNow();
|
||||
void UpdatePodcastNow(const Podcast &podcast);
|
||||
|
||||
private slots:
|
||||
void ReloadSettings();
|
||||
|
||||
void SubscriptionAdded(const Podcast &podcast);
|
||||
void PodcastLoaded(PodcastUrlLoaderReply *reply, const Podcast &podcast, const bool one_of_many);
|
||||
|
||||
private:
|
||||
void RestartTimer();
|
||||
void SaveSettings();
|
||||
|
||||
private:
|
||||
static const char *kSettingsGroup;
|
||||
|
||||
Application *app_;
|
||||
|
||||
QDateTime last_full_update_;
|
||||
int update_interval_secs_;
|
||||
|
||||
QTimer *update_timer_;
|
||||
PodcastUrlLoader *loader_;
|
||||
int pending_replies_;
|
||||
};
|
||||
|
||||
#endif // PODCASTUPDATER_H
|
||||
250
src/podcasts/podcasturlloader.cpp
Normal file
250
src/podcasts/podcasturlloader.cpp
Normal file
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include "core/logging.h"
|
||||
#include "core/networkaccessmanager.h"
|
||||
#include "core/utilities.h"
|
||||
#include "podcasturlloader.h"
|
||||
#include "podcastparser.h"
|
||||
|
||||
const int PodcastUrlLoader::kMaxRedirects = 5;
|
||||
|
||||
PodcastUrlLoader::PodcastUrlLoader(QObject* parent)
|
||||
: QObject(parent),
|
||||
network_(new NetworkAccessManager(this)),
|
||||
parser_(new PodcastParser),
|
||||
html_link_re_("<link (.*)>"),
|
||||
html_link_rel_re_("rel\\s*=\\s*['\"]?\\s*alternate"),
|
||||
html_link_type_re_("type\\s*=\\s*['\"]?([^'\" ]+)"),
|
||||
html_link_href_re_("href\\s*=\\s*['\"]?([^'\" ]+)") {
|
||||
|
||||
//html_link_re_.setMinimal(true);
|
||||
//html_link_re_.setCaseSensitivity(Qt::CaseInsensitive);
|
||||
|
||||
}
|
||||
|
||||
PodcastUrlLoader::~PodcastUrlLoader() { delete parser_; }
|
||||
|
||||
QUrl PodcastUrlLoader::FixPodcastUrl(const QString& url_text) {
|
||||
|
||||
QString url_text_copy(url_text.trimmed());
|
||||
|
||||
// Thanks gpodder!
|
||||
QuickPrefixList quick_prefixes = QuickPrefixList()
|
||||
<< QuickPrefix("fb:", "http://feeds.feedburner.com/%1")
|
||||
<< QuickPrefix("yt:", "https://www.youtube.com/rss/user/%1/videos.rss")
|
||||
<< QuickPrefix("sc:", "https://soundcloud.com/%1")
|
||||
<< QuickPrefix("fm4od:", "http://onapp1.orf.at/webcam/fm4/fod/%1.xspf")
|
||||
<< QuickPrefix("ytpl:", "https://gdata.youtube.com/feeds/api/playlists/%1");
|
||||
|
||||
// Check if it matches one of the quick prefixes.
|
||||
for (QuickPrefixList::const_iterator it = quick_prefixes.constBegin(); it != quick_prefixes.constEnd(); ++it) {
|
||||
if (url_text_copy.startsWith(it->first)) {
|
||||
url_text_copy = it->second.arg(url_text_copy.mid(it->first.length()));
|
||||
}
|
||||
}
|
||||
|
||||
if (!url_text_copy.contains("://")) {
|
||||
url_text_copy.prepend("http://");
|
||||
}
|
||||
|
||||
return FixPodcastUrl(QUrl(url_text_copy));
|
||||
|
||||
}
|
||||
|
||||
QUrl PodcastUrlLoader::FixPodcastUrl(const QUrl& url_orig) {
|
||||
|
||||
QUrl url(url_orig);
|
||||
QUrlQuery url_query(url);
|
||||
|
||||
// Replace schemes
|
||||
if (url.scheme().isEmpty() || url.scheme() == "feed" || url.scheme() == "itpc" || url.scheme() == "itms") {
|
||||
url.setScheme("http");
|
||||
}
|
||||
else if (url.scheme() == "zune" && url.host() == "subscribe" &&
|
||||
!url_query.queryItems().isEmpty()) {
|
||||
url = QUrl(url_query.queryItems()[0].second);
|
||||
}
|
||||
|
||||
return url;
|
||||
|
||||
}
|
||||
|
||||
PodcastUrlLoaderReply* PodcastUrlLoader::Load(const QString& url_text) {
|
||||
return Load(FixPodcastUrl(url_text));
|
||||
}
|
||||
|
||||
PodcastUrlLoaderReply* PodcastUrlLoader::Load(const QUrl& url) {
|
||||
|
||||
// Create a reply
|
||||
PodcastUrlLoaderReply* reply = new PodcastUrlLoaderReply(url, this);
|
||||
|
||||
// Create a state object to track this request
|
||||
RequestState* state = new RequestState;
|
||||
state->redirects_remaining_ = kMaxRedirects + 1;
|
||||
state->reply_ = reply;
|
||||
|
||||
// Start the first request
|
||||
NextRequest(url, state);
|
||||
|
||||
return reply;
|
||||
|
||||
}
|
||||
|
||||
void PodcastUrlLoader::SendErrorAndDelete(const QString& error_text, RequestState* state) {
|
||||
|
||||
state->reply_->SetFinished(error_text);
|
||||
delete state;
|
||||
|
||||
}
|
||||
|
||||
void PodcastUrlLoader::NextRequest(const QUrl& url, RequestState* state) {
|
||||
|
||||
// Stop the request if there have been too many redirects already.
|
||||
if (state->redirects_remaining_-- == 0) {
|
||||
SendErrorAndDelete(tr("Too many redirects"), state);
|
||||
return;
|
||||
}
|
||||
|
||||
qLog(Debug) << "Loading URL" << url;
|
||||
|
||||
QNetworkRequest req(url);
|
||||
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
|
||||
QNetworkReply* network_reply = network_->get(req);
|
||||
|
||||
QObject::connect(network_reply, &QNetworkReply::finished, this, [this, state, network_reply]() { RequestFinished(state, network_reply); });
|
||||
|
||||
}
|
||||
|
||||
void PodcastUrlLoader::RequestFinished(RequestState* state, QNetworkReply* reply) {
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isValid()) {
|
||||
const QUrl next_url = reply->url().resolved(reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl());
|
||||
|
||||
NextRequest(next_url, state);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for errors.
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
SendErrorAndDelete(reply->errorString(), state);
|
||||
return;
|
||||
}
|
||||
|
||||
const QVariant http_status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
||||
if (http_status.isValid() && http_status.toInt() != 200) {
|
||||
SendErrorAndDelete(
|
||||
QString("HTTP %1: %2").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) .toString(), reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString()),
|
||||
state);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the mime type.
|
||||
const QString content_type = reply->header(QNetworkRequest::ContentTypeHeader).toString();
|
||||
if (parser_->SupportsContentType(content_type)) {
|
||||
const QVariant ret = parser_->Load(reply, reply->url());
|
||||
|
||||
if (ret.canConvert<Podcast>()) {
|
||||
state->reply_->SetFinished(PodcastList() << ret.value<Podcast>());
|
||||
}
|
||||
else if (ret.canConvert<OpmlContainer>()) {
|
||||
state->reply_->SetFinished(ret.value<OpmlContainer>());
|
||||
}
|
||||
else {
|
||||
SendErrorAndDelete(tr("Failed to parse the XML for this RSS feed"),
|
||||
state);
|
||||
return;
|
||||
}
|
||||
|
||||
delete state;
|
||||
return;
|
||||
}
|
||||
else if (content_type.contains("text/html")) {
|
||||
// I don't want a full HTML parser here, so do this the dirty way.
|
||||
const QString page_text = QString::fromUtf8(reply->readAll());
|
||||
//int pos = 0;
|
||||
#if 0
|
||||
while ((pos = html_link_re_.indexIn(page_text, pos)) != -1) {
|
||||
const QString link = html_link_re_.cap(1).toLower();
|
||||
pos += html_link_re_.matchedLength();
|
||||
|
||||
if (html_link_rel_re_.indexIn(link) == -1 ||
|
||||
html_link_type_re_.indexIn(link) == -1 ||
|
||||
html_link_href_re_.indexIn(link) == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString link_type = html_link_type_re_.cap(1);
|
||||
const QString href = Utilities::DecodeHtmlEntities(html_link_href_re_.cap(1));
|
||||
|
||||
if (parser_->supported_mime_types().contains(link_type)) {
|
||||
NextRequest(QUrl(href), state);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
SendErrorAndDelete(tr("HTML page did not contain any RSS feeds"), state);
|
||||
}
|
||||
else {
|
||||
SendErrorAndDelete(tr("Unknown content-type") + ": " + content_type, state);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
PodcastUrlLoaderReply::PodcastUrlLoaderReply(const QUrl& url, QObject* parent)
|
||||
: QObject(parent), url_(url), finished_(false) {}
|
||||
|
||||
void PodcastUrlLoaderReply::SetFinished(const PodcastList& results) {
|
||||
|
||||
result_type_ = Type_Podcast;
|
||||
podcast_results_ = results;
|
||||
finished_ = true;
|
||||
emit Finished(true);
|
||||
|
||||
}
|
||||
|
||||
void PodcastUrlLoaderReply::SetFinished(const OpmlContainer& results) {
|
||||
|
||||
result_type_ = Type_Opml;
|
||||
opml_results_ = results;
|
||||
finished_ = true;
|
||||
emit Finished(true);
|
||||
|
||||
}
|
||||
|
||||
void PodcastUrlLoaderReply::SetFinished(const QString& error_text) {
|
||||
|
||||
error_text_ = error_text;
|
||||
finished_ = true;
|
||||
emit Finished(false);
|
||||
|
||||
}
|
||||
119
src/podcasts/podcasturlloader.h
Normal file
119
src/podcasts/podcasturlloader.h
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Strawberry Music Player
|
||||
* This file was part of Clementine.
|
||||
* Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
* Copyright 2019-2021, 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 PODCASTURLLOADER_H
|
||||
#define PODCASTURLLOADER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include "opmlcontainer.h"
|
||||
#include "podcast.h"
|
||||
|
||||
class PodcastParser;
|
||||
|
||||
class QNetworkAccessManager;
|
||||
class QNetworkReply;
|
||||
|
||||
class PodcastUrlLoaderReply : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PodcastUrlLoaderReply(const QUrl& url, QObject* parent);
|
||||
|
||||
enum ResultType { Type_Podcast,
|
||||
Type_Opml };
|
||||
|
||||
const QUrl& url() const { return url_; }
|
||||
bool is_finished() const { return finished_; }
|
||||
bool is_success() const { return error_text_.isEmpty(); }
|
||||
const QString& error_text() const { return error_text_; }
|
||||
|
||||
ResultType result_type() const { return result_type_; }
|
||||
const PodcastList& podcast_results() const { return podcast_results_; }
|
||||
const OpmlContainer& opml_results() const { return opml_results_; }
|
||||
|
||||
void SetFinished(const QString& error_text);
|
||||
void SetFinished(const PodcastList& results);
|
||||
void SetFinished(const OpmlContainer& results);
|
||||
|
||||
signals:
|
||||
void Finished(bool success);
|
||||
|
||||
private:
|
||||
QUrl url_;
|
||||
bool finished_;
|
||||
QString error_text_;
|
||||
|
||||
ResultType result_type_;
|
||||
PodcastList podcast_results_;
|
||||
OpmlContainer opml_results_;
|
||||
};
|
||||
|
||||
class PodcastUrlLoader : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PodcastUrlLoader(QObject* parent = nullptr);
|
||||
~PodcastUrlLoader();
|
||||
|
||||
static const int kMaxRedirects;
|
||||
|
||||
PodcastUrlLoaderReply* Load(const QString& url_text);
|
||||
PodcastUrlLoaderReply* Load(const QUrl& url);
|
||||
|
||||
// Both the FixPodcastUrl functions replace common podcatcher URL schemes
|
||||
// like itpc:// or zune:// with their http:// equivalents. The QString
|
||||
// overload also cleans up user-entered text a bit - stripping whitespace and
|
||||
// applying shortcuts like sc:tag.
|
||||
static QUrl FixPodcastUrl(const QString& url_text);
|
||||
static QUrl FixPodcastUrl(const QUrl& url);
|
||||
|
||||
private:
|
||||
struct RequestState {
|
||||
int redirects_remaining_;
|
||||
PodcastUrlLoaderReply* reply_;
|
||||
};
|
||||
|
||||
typedef QPair<QString, QString> QuickPrefix;
|
||||
typedef QList<QuickPrefix> QuickPrefixList;
|
||||
|
||||
private slots:
|
||||
void RequestFinished(RequestState* state, QNetworkReply* reply);
|
||||
|
||||
private:
|
||||
void SendErrorAndDelete(const QString& error_text, RequestState* state);
|
||||
void NextRequest(const QUrl& url, RequestState* state);
|
||||
|
||||
private:
|
||||
QNetworkAccessManager* network_;
|
||||
PodcastParser* parser_;
|
||||
|
||||
QRegularExpression html_link_re_;
|
||||
QRegularExpression whitespace_re_;
|
||||
QRegularExpression html_link_rel_re_;
|
||||
QRegularExpression html_link_type_re_;
|
||||
QRegularExpression html_link_href_re_;
|
||||
};
|
||||
|
||||
#endif // PODCASTURLLOADER_H
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
146
src/settings/podcastsettingspage.cpp
Normal file
146
src/settings/podcastsettingspage.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
|
||||
Clementine 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.
|
||||
|
||||
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "podcastsettingspage.h"
|
||||
|
||||
#include <QFileDialog>
|
||||
#include <QSettings>
|
||||
|
||||
#include "core/application.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "core/iconloader.h"
|
||||
#include "collection/collectiondirectorymodel.h"
|
||||
#include "collection/collectionmodel.h"
|
||||
#include "podcasts/gpoddersync.h"
|
||||
#include "podcasts/podcastdownloader.h"
|
||||
#include "settingsdialog.h"
|
||||
#include "ui_podcastsettingspage.h"
|
||||
|
||||
const char* PodcastSettingsPage::kSettingsGroup = "Podcasts";
|
||||
|
||||
PodcastSettingsPage::PodcastSettingsPage(SettingsDialog* dialog)
|
||||
: SettingsPage(dialog), ui_(new Ui_PodcastSettingsPage) {
|
||||
ui_->setupUi(this);
|
||||
setWindowIcon(IconLoader::Load("podcast"));
|
||||
|
||||
connect(ui_->login, SIGNAL(clicked()), SLOT(LoginClicked()));
|
||||
connect(ui_->login_state, SIGNAL(LoginClicked()), SLOT(LoginClicked()));
|
||||
connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked()));
|
||||
connect(ui_->download_dir_browse, SIGNAL(clicked()),
|
||||
SLOT(DownloadDirBrowse()));
|
||||
|
||||
GPodderSync* gsync = dialog->app()->gpodder_sync();
|
||||
connect(gsync, SIGNAL(LoginSuccess()), SLOT(GpodderLoginSuccess()));
|
||||
connect(gsync, SIGNAL(LoginFailure(const QString&)), SLOT(GpodderLoginFailure(const QString&)));
|
||||
|
||||
ui_->login_state->AddCredentialField(ui_->username);
|
||||
ui_->login_state->AddCredentialField(ui_->device_name);
|
||||
ui_->login_state->AddCredentialField(ui_->password);
|
||||
ui_->login_state->AddCredentialGroup(ui_->login_group);
|
||||
|
||||
ui_->check_interval->setItemData(0, 0); // manually
|
||||
ui_->check_interval->setItemData(1, 10 * 60); // 10 minutes
|
||||
ui_->check_interval->setItemData(2, 20 * 60); // 20 minutes
|
||||
ui_->check_interval->setItemData(3, 30 * 60); // 30 minutes
|
||||
ui_->check_interval->setItemData(4, 60 * 60); // 1 hour
|
||||
ui_->check_interval->setItemData(5, 2 * 60 * 60); // 2 hours
|
||||
ui_->check_interval->setItemData(6, 6 * 60 * 60); // 6 hours
|
||||
ui_->check_interval->setItemData(7, 12 * 60 * 60); // 12 hours
|
||||
}
|
||||
|
||||
PodcastSettingsPage::~PodcastSettingsPage() { delete ui_; }
|
||||
|
||||
void PodcastSettingsPage::Load() {
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
|
||||
const int update_interval = s.value("update_interval_secs", 0).toInt();
|
||||
ui_->check_interval->setCurrentIndex(
|
||||
ui_->check_interval->findData(update_interval));
|
||||
|
||||
const QString default_download_dir =
|
||||
dialog()->app()->podcast_downloader()->DefaultDownloadDir();
|
||||
ui_->download_dir->setText(QDir::toNativeSeparators(
|
||||
s.value("download_dir", default_download_dir).toString()));
|
||||
|
||||
ui_->auto_download->setChecked(s.value("auto_download", false).toBool());
|
||||
ui_->hide_listened->setChecked(s.value("hide_listened", false).toBool());
|
||||
ui_->delete_after->setValue(s.value("delete_after", 0).toInt() / kSecsPerDay);
|
||||
ui_->show_episodes->setValue(s.value("show_episodes", 0).toInt());
|
||||
ui_->username->setText(s.value("gpodder_username").toString());
|
||||
ui_->device_name->setText(
|
||||
s.value("gpodder_device_name", GPodderSync::DefaultDeviceName())
|
||||
.toString());
|
||||
|
||||
if (dialog()->app()->gpodder_sync()->is_logged_in()) {
|
||||
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn,
|
||||
ui_->username->text());
|
||||
}
|
||||
else {
|
||||
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut);
|
||||
}
|
||||
}
|
||||
|
||||
void PodcastSettingsPage::Save() {
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
|
||||
s.setValue("update_interval_secs", ui_->check_interval->itemData(ui_->check_interval->currentIndex()));
|
||||
s.setValue("download_dir",
|
||||
QDir::fromNativeSeparators(ui_->download_dir->text()));
|
||||
s.setValue("auto_download", ui_->auto_download->isChecked());
|
||||
s.setValue("hide_listened", ui_->hide_listened->isChecked());
|
||||
s.setValue("delete_after", ui_->delete_after->value() * kSecsPerDay);
|
||||
s.setValue("show_episodes", ui_->show_episodes->value());
|
||||
s.setValue("gpodder_device_name", ui_->device_name->text());
|
||||
}
|
||||
|
||||
void PodcastSettingsPage::LoginClicked() {
|
||||
ui_->login_state->SetLoggedIn(LoginStateWidget::LoginInProgress);
|
||||
|
||||
dialog()->app()->gpodder_sync()->Login(
|
||||
ui_->username->text(), ui_->password->text(), ui_->device_name->text());
|
||||
}
|
||||
|
||||
void PodcastSettingsPage::GpodderLoginSuccess() {
|
||||
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn,
|
||||
ui_->username->text());
|
||||
ui_->login_state->SetAccountTypeVisible(false);
|
||||
}
|
||||
|
||||
void PodcastSettingsPage::GpodderLoginFailure(const QString& error) {
|
||||
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut,
|
||||
ui_->username->text());
|
||||
ui_->login_state->SetAccountTypeVisible(true);
|
||||
ui_->login_state->SetAccountTypeText(tr("Login failed") + ": " + error);
|
||||
}
|
||||
|
||||
void PodcastSettingsPage::LogoutClicked() {
|
||||
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut);
|
||||
ui_->password->clear();
|
||||
dialog()->app()->gpodder_sync()->Logout();
|
||||
}
|
||||
|
||||
void PodcastSettingsPage::DownloadDirBrowse() {
|
||||
QString directory = QFileDialog::getExistingDirectory(
|
||||
this, tr("Choose podcast download directory"), ui_->download_dir->text());
|
||||
if (directory.isEmpty()) return;
|
||||
|
||||
ui_->download_dir->setText(QDir::toNativeSeparators(directory));
|
||||
}
|
||||
52
src/settings/podcastsettingspage.h
Normal file
52
src/settings/podcastsettingspage.h
Normal file
@@ -0,0 +1,52 @@
|
||||
/* This file is part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
Copyright 2014, John Maguire <john.maguire@gmail.com>
|
||||
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||
|
||||
Clementine 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.
|
||||
|
||||
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef PODCASTSETTINGSPAGE_H
|
||||
#define PODCASTSETTINGSPAGE_H
|
||||
|
||||
#include "settingspage.h"
|
||||
|
||||
class Ui_PodcastSettingsPage;
|
||||
|
||||
class PodcastSettingsPage : public SettingsPage {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PodcastSettingsPage(SettingsDialog* dialog);
|
||||
~PodcastSettingsPage();
|
||||
|
||||
static const char* kSettingsGroup;
|
||||
|
||||
void Load();
|
||||
void Save();
|
||||
|
||||
private slots:
|
||||
void LoginClicked();
|
||||
void LogoutClicked();
|
||||
|
||||
void GpodderLoginSuccess();
|
||||
void GpodderLoginFailure(const QString& error);
|
||||
|
||||
void DownloadDirBrowse();
|
||||
|
||||
private:
|
||||
Ui_PodcastSettingsPage* ui_;
|
||||
};
|
||||
|
||||
#endif // PODCASTSETTINGSPAGE_H
|
||||
290
src/settings/podcastsettingspage.ui
Normal file
290
src/settings/podcastsettingspage.ui
Normal file
@@ -0,0 +1,290 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>PodcastSettingsPage</class>
|
||||
<widget class="QWidget" name="PodcastSettingsPage">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>616</width>
|
||||
<height>656</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Podcasts</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Updating</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Check for new episodes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="check_interval">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Manually</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Every 10 minutes</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Every 20 minutes</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Every 30 minutes</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Every hour</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Every 2 hours</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Every 6 hours</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Every 12 hours</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Download episodes to</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="auto_download">
|
||||
<property name="text">
|
||||
<string>Download new episodes automatically</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="download_dir"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="download_dir_browse">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>Cleaning up</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout_2">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Delete played episodes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QSpinBox" name="delete_after">
|
||||
<property name="specialValueText">
|
||||
<string>Manually</string>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string> days</string>
|
||||
</property>
|
||||
<property name="prefix">
|
||||
<string>After </string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>30</number>
|
||||
</property>
|
||||
<property name="empty_text" stdset="0">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_4">
|
||||
<property name="title">
|
||||
<string>Appearance</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout_4">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="hide_listened">
|
||||
<property name="text">
|
||||
<string>Don't show listened episodes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QSpinBox" name="show_episodes">
|
||||
<property name="specialValueText">
|
||||
<string>All</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string>Number of episodes to show</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_3">
|
||||
<property name="title">
|
||||
<string>gpodder.net</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Clementine can synchronize your subscription list with your other computers and podcast applications. <a href="https://gpodder.net/register/">Create an account</a>.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="LoginStateWidget" name="login_state" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="login_group" native="true">
|
||||
<layout class="QFormLayout" name="formLayout_3">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Username</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="username"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="login">
|
||||
<property name="text">
|
||||
<string>Sign in</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>Password</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="password">
|
||||
<property name="echoMode">
|
||||
<enum>QLineEdit::Password</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>Device name</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="device_name"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>LoginStateWidget</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>widgets/loginstatewidget.h</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>check_interval</tabstop>
|
||||
<tabstop>download_dir</tabstop>
|
||||
<tabstop>download_dir_browse</tabstop>
|
||||
<tabstop>auto_download</tabstop>
|
||||
<tabstop>delete_after</tabstop>
|
||||
<tabstop>username</tabstop>
|
||||
<tabstop>password</tabstop>
|
||||
<tabstop>device_name</tabstop>
|
||||
<tabstop>login</tabstop>
|
||||
</tabstops>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -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_);
|
||||
|
||||
@@ -93,7 +93,6 @@ class SettingsDialog : public QDialog {
|
||||
Tidal,
|
||||
Qobuz,
|
||||
Spotify,
|
||||
NetworkRemote
|
||||
};
|
||||
|
||||
enum Role {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user