From 3141066ee11cba12ecf735466d0eb3d0158d2e01 Mon Sep 17 00:00:00 2001 From: Ed Bruck Date: Sun, 20 Oct 2019 13:52:49 -0700 Subject: [PATCH] Merged v0.2.7 development branch --- .circleci/config.yml | 14 ++ CMakeLists.txt | 19 ++- README.md | 8 +- data/bookmarks.json | 51 +------- docker/fedora/31/Dockerfile | 6 + docker/rebuild_and_publish.sh | 6 + docker/ubuntu/19.10/Dockerfile | 24 ++++ include/radiotray-ng/i_bookmarks.hpp | 1 + .../radiotray-ng/i_playlist_downloader.hpp | 3 +- package/CMakeLists.txt | 12 +- package/changelog-deb | 10 ++ package/changelog-rpm | 6 + src/radiotray-ng/bookmarks/bookmarks.cpp | 121 ++++++++++++++---- src/radiotray-ng/bookmarks/bookmarks.hpp | 1 + src/radiotray-ng/config/config.cpp | 12 +- src/radiotray-ng/extras/scripts/rt2rtng | 12 +- src/radiotray-ng/extras/scripts/rtng-dbus | 6 +- .../gui/appindicator/appindicator_gui.cpp | 7 +- src/radiotray-ng/gui/editor/editor_frame.cpp | 4 - src/radiotray-ng/player/player.cpp | 70 +++++----- src/radiotray-ng/player/player.hpp | 15 +-- .../playlist/playlist_downloader.cpp | 14 +- .../playlist/playlist_downloader.hpp | 4 +- src/radiotray-ng/radiotray_ng.cpp | 7 +- tests/bookmarks_test.cpp | 18 ++- tests/runners/playlist_runner.cpp | 3 +- 26 files changed, 289 insertions(+), 165 deletions(-) create mode 100644 docker/fedora/31/Dockerfile create mode 100644 docker/ubuntu/19.10/Dockerfile diff --git a/.circleci/config.yml b/.circleci/config.yml index 9b2fd4a..10cbfaa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -47,12 +47,24 @@ jobs: - image: "radiotrayng/circleci:ubuntu-19.04" environment: - CI_OS: linux + Ubuntu-19_10-build: + <<: *commonStepsUbuntu + docker: + - image: "radiotrayng/circleci:ubuntu-19.10" + environment: + - CI_OS: linux Fedora-30-build: <<: *commonStepsFedora docker: - image: "radiotrayng/circleci:fedora-30" environment: - CI_OS: linux + Fedora-31-build: + <<: *commonStepsFedora + docker: + - image: "radiotrayng/circleci:fedora-31" + environment: + - CI_OS: linux workflows: version: 2 @@ -61,4 +73,6 @@ workflows: - Ubuntu-16_04-build - Ubuntu-18_04-build - Ubuntu-19_04-build + - Ubuntu-19_10-build - Fedora-30-build + - Fedora-31-build diff --git a/CMakeLists.txt b/CMakeLists.txt index ddba1be..1acdc7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.10) -project(radiotray-ng VERSION 0.2.5 LANGUAGES CXX) +project(radiotray-ng CXX) # workaround for Eclipse if (${CMAKE_EXTRA_GENERATOR} MATCHES "Eclipse CDT4") @@ -13,7 +13,7 @@ endif() # version for user agent creation set(PROJECT_VERSION_MAJOR 0) set(PROJECT_VERSION_MINOR 2) -set(PROJECT_VERSION_PATCH 6) +set(PROJECT_VERSION_PATCH 7) set(PROJECT_VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}") # output dir @@ -30,6 +30,19 @@ endif(CCACHE_FOUND) find_package(CURL REQUIRED) find_package(PkgConfig REQUIRED) find_package(Boost REQUIRED filesystem system log program_options thread) + +# set up wxWidgets to prefer gtk3 +execute_process(COMMAND wx-config --selected-config --toolkit=gtk3 + RESULT_VARIABLE ret + OUTPUT_QUIET) +if(ret EQUAL "0") + message(STATUS "wxWidgets: Found gtk3 version, using it.") + set(wxWidgets_CONFIG_OPTIONS_DEFAULT "--toolkit=gtk3") +else() + message(STATUS "wxWidgets: No gtk3 version found, falling back to default (likely gtk2)") +endif() +unset(ret) +set(wxWidgets_CONFIG_OPTIONS "${wxWidgets_CONFIG_OPTIONS_DEFAULT}" ON STRING "wxWidgets toolkit options") find_package(wxWidgets REQUIRED core base adv) pkg_search_module(JSONCPP REQUIRED jsoncpp) @@ -43,7 +56,7 @@ endif(NOT APPLE) set(CMAKE_CXX_STANDARD 14) add_definitions("-DBOOST_LOG_DYN_LINK") add_compile_options("-fdiagnostics-color=auto") -set(warnings "-Wno-deprecated-declarations -Wall -Wextra -Werror -Wpedantic") +set(warnings "-Wall -Wextra -Werror -Wpedantic") set(CMAKE_CXX_FLAGS ${warnings}) set(CMAKE_C_FLAGS ${warnings}) set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -s") diff --git a/README.md b/README.md index 813eb39..4231763 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ A config (radiotray-ng.json) is created in your ~/.config/radiotray-ng directory "tag-info-verbose" : true, "volume-level" : 100, "volume-step" : 1, + "volume-max-level" : 200, "wrap-track-info" : true, "wrap-track-info-len" : 40, "media-key-mapping" : false, @@ -114,6 +115,7 @@ A config (radiotray-ng.json) is created in your ~/.config/radiotray-ng directory tag-info-verbose: displays in the menu stream information such as bitrate etc. track-info-copy: enable/disable track clicking to copy into clipboard volume-step: value used to increment/decrement the volume level + volume-max-level: maximum volume level wrap-track-info: enable/disable the wrapping of title & artist menu text wrap-track-info-len: maximum title & artist line length media-key-mapping: enable the mapping of media keys to volume up/down etc. (Previous, Next, Rewind, FastForward etc.) @@ -232,12 +234,16 @@ Download a release or clone the repo and build the latest Debian package. https://github.com/ebruck/radiotray-ng/releases +## Fedora Install ## +``` +$ dnf install radiotray-ng +``` ## To Build on Ubuntu: ## Install these packages: ``` -lsb-release libcurl4-openssl-dev libjsoncpp-dev libxdg-basedir-dev libnotify-dev libboost-filesystem-dev libgstreamer1.0-dev libappindicator3-dev libboost-log-dev libboost-program-options-dev libgtk-3-dev libnotify-dev lsb-release libbsd-dev libncurses5-dev libglibmm-2.4-dev libwxgtk3.0-dev libwxgtk3.0-0v5 cmake +lsb-release libcurl4-openssl-dev libjsoncpp-dev libxdg-basedir-dev libnotify-dev libboost-filesystem-dev libgstreamer1.0-dev libappindicator3-dev libboost-log-dev libboost-program-options-dev libgtk-3-dev libnotify-dev lsb-release libbsd-dev libncurses5-dev libglibmm-2.4-dev libwxgtk3.0-gtk3-dev libwxgtk3.0-gtk3-0v5 cmake ``` ## Build Radiotray-NG & Debian Package ## diff --git a/data/bookmarks.json b/data/bookmarks.json index d3d1db6..d75e002 100644 --- a/data/bookmarks.json +++ b/data/bookmarks.json @@ -1,15 +1,12 @@ [ { "group" : "Jazz", - "image" : null, "stations" : [ { - "image" : null, "name" : "Smooth Jazz", "url" : "http://smoothjazz.com/streams/smoothjazz_128.pls" }, { - "image" : null, "name" : "Sonic Universe", "url" : "http://somafm.com/sonicuniverse.pls" } @@ -17,15 +14,12 @@ }, { "group" : "Latin", - "image" : null, "stations" : [ { - "image" : null, "name" : "Top Latino Radio", "url" : "http://online.radiodifusion.net:8020/listen.pls" }, { - "image" : null, "name" : "Reggaeton 24/7", "url" : "http://cc.net2streams.com/tunein.php/reggaeton/playlist.pls" } @@ -33,20 +27,16 @@ }, { "group" : "Classic Rock", - "image" : null, "stations" : [ { - "image" : null, "name" : "181.FM Classic Hits", "url" : "http://www.181.fm/winamp.pls?station=181-greatoldies&file=181-greatoldies.pls" }, { - "image" : null, "name" : ".977 Classic Rock", "url" : "http://www.977music.com/tunein/web/classicrock.asx" }, { - "image" : null, "name" : "Covers", "url" : "http://somafm.com/covers.pls" } @@ -54,35 +44,28 @@ }, { "group" : "Classical", - "image" : null, "stations" : [ { - "image" : null, "name" : "KDFC", "url" : "http://provisioning.streamtheworld.com/pls/KDFCFM.pls" }, { - "image" : null, "name" : "Classic FM", "url" : "http://media-ice.musicradio.com/ClassicFMMP3.m3u" }, { - "image" : null, "name" : "WCPE", "url" : "http://www.ibiblio.org/wcpe/wcpe.pls" }, { - "image" : null, "name" : "CINEMIX", "url" : "http://cinemix.us/cine.asx" }, { - "image" : null, "name" : "WQXR", "url" : "http://www.wqxr.org/stream/wqxr/mp3.pls" }, { - "image" : null, "name" : "BBC Radio 3", "url" : "http://bbcmedia.ic.llnwd.net/stream/bbcmedia_radio3_mf_p" } @@ -90,25 +73,20 @@ }, { "group" : "Pop / Rock", - "image" : null, "stations" : [ { - "image" : null, "name" : "Radio Paradise", "url" : "http://www.radioparadise.com/musiclinks/rp_128.m3u" }, { - "image" : null, "name" : ".977 The Hitz Channel", "url" : "http://www.977music.com/tunein/web/hitz.asx" }, { - "image" : null, "name" : "Indie Pop Rocks!", "url" : "http://somafm.com/indiepop.pls" }, { - "image" : null, "name" : "PopTron", "url" : "http://somafm.com/poptron.pls" } @@ -116,25 +94,20 @@ }, { "group" : "Oldies", - "image" : null, "stations" : [ { - "image" : null, "name" : "ABN Antioch OTR", "url" : "http://radio.macinmind.com/listen.m3u" }, { - "image" : null, "name" : "AM600 Conyers OTR", "url" : "http://www.conyersradio.net/listen.m3u" }, { - "image" : null, "name" : "Rumsey Retro Radio", "url" : "https://www.rumseyretro.ca/assets/rumseyretro.pls" }, { - "image" : null, "name" : "WNAR-AM Radio", "url" : "http://live.wnar-am.com:8500/listen.pls" } @@ -142,30 +115,24 @@ }, { "group" : "Chill", - "image" : null, "stations" : [ { - "image" : null, "name" : "Lounge Radio", "url" : "http://www.lounge-radio.com/listen128.m3u" }, { - "image" : null, "name" : "Beat Blender", "url" : "http://somafm.com/beatblender.pls" }, { - "image" : null, "name" : "Secret Agent", "url" : "http://somafm.com/secretagent.pls" }, { - "image" : null, "name" : "Groove Salad", "url" : "http://somafm.com/groovesalad.pls" }, { - "image" : null, "name" : "Illinois Street Lounge", "url" : "http://somafm.com/illstreet.pls" } @@ -173,25 +140,20 @@ }, { "group" : "Country", - "image" : null, "stations" : [ { - "image" : null, "name" : "Boot Liquor", "url" : "http://somafm.com/bootliquor.pls" }, { - "image" : null, "name" : "Highway 181", "url" : "http://www.181.fm/winamp.pls?station=181-highway&file=181-highway.pls" }, { - "image" : null, "name" : "Country 108", "url" : "http://www.country108.com/listen.pls" }, { - "image" : null, "name" : "WAMU Bluegrass Country", "url" : "http://ice24.securenetsystems.net/WAMU2" } @@ -199,35 +161,28 @@ }, { "group" : "Techno / Electronic", - "image" : null, "stations" : [ { - "image" : null, "name" : "Drone Zone", "url" : "http://somafm.com/dronezone.pls" }, { - "image" : null, "name" : "Space Station Soma", "url" : "http://somafm.com/spacestation.pls" }, { - "image" : null, "name" : "cliqhop idm", "url" : "http://somafm.com/cliqhop.pls" }, { - "image" : null, "name" : "Black Rock FM", "url" : "http://somafm.com/brfm.pls" }, { - "image" : null, "name" : "New Dance Radio", "url" : "http://jbstream.net/tunein.php/blackoutworm/playlist.asx" }, { - "image" : null, "name" : "DubStep Beyond", "url" : "http://somafm.com/play/dubstep32" } @@ -235,12 +190,10 @@ }, { "group" : "Community", - "image" : null, "stations" : [ { - "image" : null, - "name" : "Jupiter Broadcast", - "url" : "http://jblive.fm/" + "name" : "Jupiter Broadcasting", + "url" : "http://jblive.stream/" } ] } diff --git a/docker/fedora/31/Dockerfile b/docker/fedora/31/Dockerfile new file mode 100644 index 0000000..c64e9ef --- /dev/null +++ b/docker/fedora/31/Dockerfile @@ -0,0 +1,6 @@ +FROM fedora:31 + +RUN set -ex; \ + dnf -y update \ + && dnf -y install git openssh-clients rpm-build redhat-lsb cmake libcurl-devel boost-devel wxGTK3-devel jsoncpp-devel gstreamer1-devel libxdg-basedir-devel libbsd-devel libappindicator-gtk3-devel libnotify-devel glibmm24-devel \ + && dnf clean all diff --git a/docker/rebuild_and_publish.sh b/docker/rebuild_and_publish.sh index 898564f..eb594c4 100755 --- a/docker/rebuild_and_publish.sh +++ b/docker/rebuild_and_publish.sh @@ -11,5 +11,11 @@ docker push radiotrayng/circleci:ubuntu-18.04 docker build -t radiotrayng/circleci:ubuntu-19.04 - +#include class IPlaylistDownloader @@ -25,5 +26,5 @@ class IPlaylistDownloader public: virtual ~IPlaylistDownloader() = default; - virtual bool download_playlist(const std::string& url, playlist_t& playlist) = 0; + virtual bool download_playlist(const IBookmarks::station_data_t& std, playlist_t& playlist) = 0; }; diff --git a/package/CMakeLists.txt b/package/CMakeLists.txt index 7c48e86..43476de 100644 --- a/package/CMakeLists.txt +++ b/package/CMakeLists.txt @@ -48,13 +48,19 @@ if (LSB_RELEASE_EXECUTABLE) set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE ${CPACK_SYSTEM_NAME}) set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional") set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Edward G. Bruck ") - set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libbsd0, libcurl4, libjsoncpp1, libxdg-basedir1, libnotify4, python2.7, python-lxml, libglibmm-2.4-1v5, libboost-filesystem${BOOST_DEB_VERSION}, libboost-system${BOOST_DEB_VERSION}, libboost-log${BOOST_DEB_VERSION}, libboost-thread${BOOST_DEB_VERSION}, libboost-program-options${BOOST_DEB_VERSION}, libgstreamer1.0-0, libappindicator3-1, gstreamer1.0-plugins-good, gstreamer1.0-plugins-bad, gstreamer1.0-plugins-ugly, libwxgtk3.0-0v5") + set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libbsd0, libcurl4, libjsoncpp1, libxdg-basedir1, libnotify4, python3-lxml, libglibmm-2.4-1v5, libboost-filesystem${BOOST_DEB_VERSION}, libboost-system${BOOST_DEB_VERSION}, libboost-log${BOOST_DEB_VERSION}, libboost-thread${BOOST_DEB_VERSION}, libboost-program-options${BOOST_DEB_VERSION}, libgstreamer1.0-0, libappindicator3-1, gstreamer1.0-plugins-good, gstreamer1.0-plugins-bad, gstreamer1.0-plugins-ugly, libwxgtk3.0-gtk3-0v5") set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA ${PROJECT_SOURCE_DIR}/package/postinst;${PROJECT_SOURCE_DIR}/package/postrm;${PROJECT_SOURCE_DIR}/package/conffiles) # no libcurl4 on Debian Stretch # https://bugs.launchpad.net/ubuntu/+source/curl/+bug/1754294 if (${RELEASE} STREQUAL "16.04" OR ${CODENAME} STREQUAL "stretch") - string(REPLACE "libcurl4" "libcurl3" CPACK_DEBIAN_PACKAGE_DEPENDS ${CPACK_DEBIAN_PACKAGE_DEPENDS}) + string(REPLACE "libcurl4" "libcurl3" CPACK_DEBIAN_PACKAGE_DEPENDS ${CPACK_DEBIAN_PACKAGE_DEPENDS}) + endif() + + # official packages for older distros will continue to use gtk2 for the editor + if (${RELEASE} STREQUAL "16.04" OR ${RELEASE} STREQUAL "18.04" OR ${RELEASE} STREQUAL "19.04" OR ${CODENAME} STREQUAL "stretch") + message(STATUS "Official packaging assumes gtk2 dependency for 'rtng-bookmark-editor'") + string(REPLACE "libwxgtk3.0-gtk3-0v5" "libwxgtk3.0-0v5" CPACK_DEBIAN_PACKAGE_DEPENDS ${CPACK_DEBIAN_PACKAGE_DEPENDS}) endif() execute_process(COMMAND gzip -n -9 -c "${PROJECT_SOURCE_DIR}/package/changelog-deb" WORKING_DIRECTORY ${PROJECT_BINARY_DIR} OUTPUT_FILE "${PROJECT_BINARY_DIR}/changelog.Debian.gz") @@ -73,7 +79,7 @@ if (LSB_RELEASE_EXECUTABLE) if (CPACK_GENERATOR MATCHES "RPM") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Simple Internet Radio Player. Radiotray-NG runs in the system tray allowing you to select and play configured radio stations.") set(CPACK_RPM_PACKAGE_URL "https://www.github.com/ebruck/radiotray-ng") - set(CPACK_RPM_PACKAGE_REQUIRES "glibc, libcurl, jsoncpp, boost, libxdg-basedir, libbsd, libappindicator-gtk3, libnotify, glibmm24, python2-lxml, gstreamer-plugins-base, gstreamer-plugins-good, gstreamer-plugins-bad-free, wxGTK3") + set(CPACK_RPM_PACKAGE_REQUIRES "glibc, libcurl, jsoncpp, boost, libxdg-basedir, libbsd, libappindicator-gtk3, libnotify, glibmm24, python3-lxml, gstreamer-plugins-base, gstreamer-plugins-good, gstreamer-plugins-bad-free, wxGTK3") set(CPACK_RPM_PACKAGE_GROUP "Applications/Multimedia") set(CPACK_RPM_PACKAGE_LICENSE "GPLv3") set(CPACK_RPM_COMPRESSION_TYPE "xz") diff --git a/package/changelog-deb b/package/changelog-deb index a29321f..d71b623 100644 --- a/package/changelog-deb +++ b/package/changelog-deb @@ -1,3 +1,13 @@ +radiotray-ng (0.2.7) unstable; urgency=low + + * Removed python2 dependency + * Bookmark saving no longer includes default entries + * Volume level changes using pavucontrol etc. are now detected + * Minor cleanup & bug fixes + + -- Edward G. Bruck Sun, 20 Oct 2019 13:53:10 -0700 + + radiotray-ng (0.2.6) unstable; urgency=low * Added disable-logging config option diff --git a/package/changelog-rpm b/package/changelog-rpm index 0c6b0ac..3754216 100644 --- a/package/changelog-rpm +++ b/package/changelog-rpm @@ -1,3 +1,9 @@ +* Sun Oct 20 2019 Edward G. Bruck - 0.2.7 +- Removed python2 dependency +- Bookmark saving no longer includes default entries +- Volume level changes using pavucontrol etc. are now detected +- Minor cleanup & bug fixes + * Sun Jul 07 2019 Edward G. Bruck - 0.2.6 - Added disable-logging config option - Added Yaru & Breeze icons diff --git a/src/radiotray-ng/bookmarks/bookmarks.cpp b/src/radiotray-ng/bookmarks/bookmarks.cpp index 66ddf92..f9bcf6d 100644 --- a/src/radiotray-ng/bookmarks/bookmarks.cpp +++ b/src/radiotray-ng/bookmarks/bookmarks.cpp @@ -35,12 +35,13 @@ bool Bookmarks::load() std::ifstream ifile(this->bookmarks_file); ifile.exceptions(std::ios::failbit); - Json::Reader reader; + Json::CharReaderBuilder rbuilder; Json::Value new_bookmarks; + std::string parse_errors; - if (!reader.parse(ifile, new_bookmarks)) + if (!Json::parseFromStream(rbuilder, ifile, &new_bookmarks, &parse_errors)) { - LOG(error) << "Failed to parse: " << this->bookmarks_file << " : " << reader.getFormattedErrorMessages(); + LOG(error) << "Failed to parse: " << this->bookmarks_file << " : " << parse_errors; return false; } @@ -78,7 +79,9 @@ bool Bookmarks::save_as(const std::string& new_filename) std::ofstream ofile(filename); ofile.exceptions(std::ios::failbit); - ofile << Json::StyledWriter().write(this->bookmarks); + Json::StreamWriterBuilder wbuilder; + std::unique_ptr const writer(wbuilder.newStreamWriter()); + writer->write(this->bookmarks, &ofile); } catch(std::exception& /*e*/) { @@ -129,9 +132,14 @@ bool Bookmarks::add_group(const std::string& group_name, const std::string& imag Json::Value& value = this->bookmarks.append(Json::Value(Json::objectValue)); - value[GROUP_KEY] = group_name; - value[GROUP_IMAGE_KEY] = image; - value[STATIONS_KEY] = Json::Value(Json::arrayValue); + value[GROUP_KEY] = group_name; + + if (!image.empty()) + { + value[GROUP_IMAGE_KEY] = image; + } + + value[STATIONS_KEY] = Json::Value(Json::arrayValue); return true; } @@ -158,7 +166,13 @@ bool Bookmarks::update_group(const std::string& group_name, const std::string& n if (this->find_group(group_name, group_index)) { - this->bookmarks[group_index][GROUP_IMAGE_KEY] = new_group_image; + this->bookmarks[group_index].removeMember(GROUP_IMAGE_KEY); + + if (!new_group_image.empty()) + { + this->bookmarks[group_index][GROUP_IMAGE_KEY] = new_group_image; + } + return true; } @@ -197,8 +211,16 @@ bool Bookmarks::add_station(const std::string& group_name, const std::string& st value[STATION_NAME_KEY] = station_name; value[STATION_URL_KEY] = station_url; - value[STATION_IMAGE_KEY] = station_image; - value[STATION_NOTIFICATIONS_KEY] = notifications; + + if (!station_image.empty()) + { + value[STATION_IMAGE_KEY] = station_image; + } + + if (!notifications) + { + value[STATION_NOTIFICATIONS_KEY] = notifications; + } this->bookmarks[group_index][STATIONS_KEY].append(value); @@ -263,9 +285,19 @@ bool Bookmarks::update_station(const std::string& group_name, const std::string& if (this->find_station(group_index, station_name, station_index)) { - this->bookmarks[group_index][STATIONS_KEY][station_index][STATION_URL_KEY] = new_station_url; - this->bookmarks[group_index][STATIONS_KEY][station_index][STATION_IMAGE_KEY] = new_station_image; - this->bookmarks[group_index][STATIONS_KEY][station_index][STATION_NOTIFICATIONS_KEY] = new_notifications; + this->bookmarks[group_index][STATIONS_KEY][station_index][STATION_URL_KEY] = new_station_url; + + this->bookmarks[group_index][STATIONS_KEY][station_index].removeMember(STATION_IMAGE_KEY); + if (!new_station_image.empty()) + { + this->bookmarks[group_index][STATIONS_KEY][station_index][STATION_IMAGE_KEY] = new_station_image; + } + + this->bookmarks[group_index][STATIONS_KEY][station_index].removeMember(STATION_NOTIFICATIONS_KEY); + if (!new_notifications) + { + this->bookmarks[group_index][STATIONS_KEY][station_index][STATION_NOTIFICATIONS_KEY] = new_notifications; + } return true; } @@ -386,12 +418,16 @@ IBookmarks::group_data_t Bookmarks::operator[](const size_t group_index) IBookmarks::group_data_t stations; stations.group = this->bookmarks[Json::ArrayIndex(group_index)][GROUP_KEY].asString(); - stations.image = this->bookmarks[Json::ArrayIndex(group_index)][GROUP_IMAGE_KEY].asString(); + + if (this->bookmarks[Json::ArrayIndex(group_index)].isMember(GROUP_IMAGE_KEY)) + { + stations.image = this->bookmarks[Json::ArrayIndex(group_index)][GROUP_IMAGE_KEY].asString(); + } for(auto& station : this->bookmarks[Json::ArrayIndex(group_index)][STATIONS_KEY]) { - stations.stations.push_back({station[STATION_NAME_KEY].asString(), station[STATION_URL_KEY].asString(), station[STATION_IMAGE_KEY].asString(), - this->get_station_notifications(station)}); + stations.stations.push_back({station[STATION_NAME_KEY].asString(), station[STATION_URL_KEY].asString(), (station.isMember(STATION_IMAGE_KEY) ? station[STATION_IMAGE_KEY].asString() : ""), + this->get_station_notifications(station), station.isMember(STATION_DIRECT_KEY) ? station[STATION_DIRECT_KEY].asBool() : false}); } return stations; @@ -408,8 +444,8 @@ bool Bookmarks::get_group_stations(const std::string& group_name, std::vectorbookmarks[Json::ArrayIndex(group_index)][STATIONS_KEY]) { - stations.push_back({station[STATION_NAME_KEY].asString(), station[STATION_URL_KEY].asString(), station[STATION_IMAGE_KEY].asString(), - this->get_station_notifications(station)}); + stations.push_back({station[STATION_NAME_KEY].asString(), station[STATION_URL_KEY].asString(), station.isMember(STATION_IMAGE_KEY) ? station[STATION_IMAGE_KEY].asString() : "", + this->get_station_notifications(station), station.isMember(STATION_DIRECT_KEY) ? station[STATION_DIRECT_KEY].asBool() : false}); } return true; @@ -430,13 +466,34 @@ bool Bookmarks::get_station(const std::string& group_name, const std::string& st { station_data.name = this->bookmarks[group_index][STATIONS_KEY][station_index][STATION_NAME_KEY].asString(); station_data.url = this->bookmarks[group_index][STATIONS_KEY][station_index][STATION_URL_KEY].asString(); - station_data.image = this->bookmarks[group_index][STATIONS_KEY][station_index][STATION_IMAGE_KEY].asString(); + + if (this->bookmarks[group_index][STATIONS_KEY][station_index].isMember(STATION_IMAGE_KEY)) + { + station_data.image = this->bookmarks[group_index][STATIONS_KEY][station_index][STATION_IMAGE_KEY].asString(); + } + else + { + station_data.image.clear(); + } + station_data.notifications = this->get_station_notifications(this->bookmarks[group_index][STATIONS_KEY][station_index]); + if (this->bookmarks[group_index][STATIONS_KEY][station_index].isMember(STATION_DIRECT_KEY)) + { + station_data.direct = this->bookmarks[group_index][STATIONS_KEY][station_index][STATION_DIRECT_KEY].asBool(); + } + else + { + station_data.direct = false; + } + // use group image if not overridden if (station_data.image.empty()) { - station_data.image = this->bookmarks[Json::ArrayIndex(group_index)][GROUP_IMAGE_KEY].asString(); + if (this->bookmarks[Json::ArrayIndex(group_index)].isMember(GROUP_IMAGE_KEY)) + { + station_data.image = this->bookmarks[Json::ArrayIndex(group_index)][GROUP_IMAGE_KEY].asString(); + } } // clean up... @@ -474,7 +531,9 @@ bool Bookmarks::get_group_as_json(const std::string& group_name, std::string& js if (this->find_group(group_name, group_index)) { - json = Json::StyledWriter().write(this->bookmarks[Json::ArrayIndex(group_index)]); + Json::StreamWriterBuilder wbuilder; + json = Json::writeString(wbuilder, this->bookmarks[Json::ArrayIndex(group_index)]); + return true; } @@ -493,10 +552,14 @@ bool Bookmarks::get_station_as_json(const std::string& group_name, const std::st { auto station = this->bookmarks[group_index][STATIONS_KEY][station_index]; - // may not be there... - station[NOTIFICATION_KEY] = this->get_station_notifications(station); + if (!this->get_station_notifications(station)) + { + station[NOTIFICATION_KEY] = false; + } + + Json::StreamWriterBuilder wbuilder; + json = Json::writeString(wbuilder, station); - json = Json::StyledWriter().write(station); return true; } } @@ -508,16 +571,18 @@ bool Bookmarks::get_station_as_json(const std::string& group_name, const std::st bool Bookmarks::add_station_from_json(const std::string& group_name, const std::string& json, std::string& station_name) { // first, validate the station - Json::Reader reader; Json::Value station; + Json::CharReaderBuilder rbuilder; + std::unique_ptr const reader(rbuilder.newCharReader()); + std::string parse_errors; - if (!reader.parse(json, station)) + if (!reader->parse(json.c_str(), json.c_str() + json.size(), &station, &parse_errors)) { - LOG(error) << "Failed to parse:\n<<" << json << ">>\n" << reader.getFormattedErrorMessages(); + LOG(error) << "Failed to parse:\n<<" << json << ">>\n" << parse_errors; return false; } - if (!station.isMember(STATION_NAME_KEY)|| !station.isMember(STATION_URL_KEY) || !station.isMember(STATION_IMAGE_KEY)) + if (!station.isMember(STATION_NAME_KEY) || !station.isMember(STATION_URL_KEY)) { LOG(warning) << "Insufficient station data ...\n<<" << json << "<<"; return false; diff --git a/src/radiotray-ng/bookmarks/bookmarks.hpp b/src/radiotray-ng/bookmarks/bookmarks.hpp index 9084eee..c0a2edb 100644 --- a/src/radiotray-ng/bookmarks/bookmarks.hpp +++ b/src/radiotray-ng/bookmarks/bookmarks.hpp @@ -83,6 +83,7 @@ class Bookmarks final : public IBookmarks const std::string STATION_URL_KEY{"url"}; const std::string STATION_IMAGE_KEY{"image"}; const std::string STATION_NOTIFICATIONS_KEY{"notifications"}; + const std::string STATION_DIRECT_KEY{"direct"}; bool find_group(const std::string& group_name, Json::ArrayIndex& group_index); diff --git a/src/radiotray-ng/config/config.cpp b/src/radiotray-ng/config/config.cpp index 96973d8..504b841 100644 --- a/src/radiotray-ng/config/config.cpp +++ b/src/radiotray-ng/config/config.cpp @@ -31,10 +31,12 @@ bool Config::load() std::ifstream ifile(this->config_file); ifile.exceptions(std::ios::failbit); - Json::Reader reader; - if (!reader.parse(ifile, this->config)) + Json::CharReaderBuilder rbuilder; + std::string parse_err; + + if (!Json::parseFromStream(rbuilder, ifile, &this->config, &parse_err)) { - LOG(error) << "Failed to parse: " << this->config_file << " : " << reader.getFormattedErrorMessages(); + LOG(error) << "Failed to parse: " << this->config_file << " : " << parse_err; return false; } } @@ -57,7 +59,9 @@ bool Config::save() std::ofstream ofile(this->config_file); ofile.exceptions(std::ios::failbit); - ofile << Json::StyledWriter().write(this->config); + Json::StreamWriterBuilder wbuilder; + std::unique_ptr const writer(wbuilder.newStreamWriter()); + writer->write(this->config, &ofile); } catch(std::exception& /*e*/) { diff --git a/src/radiotray-ng/extras/scripts/rt2rtng b/src/radiotray-ng/extras/scripts/rt2rtng index 66f1c2e..2bf53ba 100755 --- a/src/radiotray-ng/extras/scripts/rt2rtng +++ b/src/radiotray-ng/extras/scripts/rt2rtng @@ -1,6 +1,6 @@ -#!/usr/bin/env python2.7 +#!/usr/bin/env python3 # -# Copyright 2017 Edward G. Bruck +# Copyright 2017-2019 Edward G. Bruck # # This file is part of Radiotray-NG. # @@ -43,7 +43,7 @@ def walk_bookmarks(root, group_func, bookmark_func, group=""): def group_callback(group_name): if len(bookmarks): - if not bookmarks[0].has_key(group_name): + if group_name not in bookmarks[0]: bookmarks.append({'group' : group_name, 'image' : '', 'stations' : [] }) else: bookmarks.append({'group' : group_name, 'image' : '', 'stations' : [] }) @@ -74,13 +74,13 @@ def prune_empty_groups(): if __name__ == "__main__": if len(sys.argv) != 2: - print "usage: rt2rtng " + print("usage: rt2rtng ") exit(1) if os.path.exists(sys.argv[1]): root = etree.parse(sys.argv[1]).getroot() walk_bookmarks(root, group_callback, bookmark_callback) prune_empty_groups() - print json.dumps(bookmarks, indent=4) + print(json.dumps(bookmarks, indent=4)) else: - print sys.argv[1], "not found!" + print(sys.argv[1], "not found!") diff --git a/src/radiotray-ng/extras/scripts/rtng-dbus b/src/radiotray-ng/extras/scripts/rtng-dbus index bbfca23..f33a462 100755 --- a/src/radiotray-ng/extras/scripts/rtng-dbus +++ b/src/radiotray-ng/extras/scripts/rtng-dbus @@ -45,11 +45,11 @@ if [ "$#" -gt 0 ]; then DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus fi - QDBUS_CMD="qdbus com.github.radiotray_ng /com/github/radiotray_ng com.github.radiotray_ng.$1" + DBUS_SEND_CMD="dbus-send --session --print-reply=literal --type=method_call --dest=com.github.radiotray_ng /com/github/radiotray_ng com.github.radiotray_ng.$1" - for arg in "${@:2}"; do ARGS="$ARGS '$arg'"; done + for arg in "${@:2}"; do ARGS="$ARGS 'string:$arg'"; done - eval $QDBUS_CMD $ARGS + eval $DBUS_SEND_CMD $ARGS if [ ! -z "${DBUS_SESSION_BUS_PID}" ]; then kill $DBUS_SESSION_BUS_PID diff --git a/src/radiotray-ng/gui/appindicator/appindicator_gui.cpp b/src/radiotray-ng/gui/appindicator/appindicator_gui.cpp index 14a3792..b863a9a 100644 --- a/src/radiotray-ng/gui/appindicator/appindicator_gui.cpp +++ b/src/radiotray-ng/gui/appindicator/appindicator_gui.cpp @@ -82,8 +82,13 @@ void AppindicatorGui::on_state_event(const IEventBus::event& /*ev*/, IEventBus:: } -void AppindicatorGui::on_volume_event(const IEventBus::event& /*ev*/, IEventBus::event_data_t& /*data*/) +void AppindicatorGui::on_volume_event(const IEventBus::event& /*ev*/, IEventBus::event_data_t& data) { + if (data.count(VOLUME_LEVEL_KEY)) + { + this->radiotray_ng->set_volume(data[VOLUME_LEVEL_KEY]); + } + this->update_volume_menu_item(); } diff --git a/src/radiotray-ng/gui/editor/editor_frame.cpp b/src/radiotray-ng/gui/editor/editor_frame.cpp index 5a65961..58186f0 100644 --- a/src/radiotray-ng/gui/editor/editor_frame.cpp +++ b/src/radiotray-ng/gui/editor/editor_frame.cpp @@ -509,10 +509,6 @@ EditorFrame::onAbout(wxCommandEvent& /* event */) version += "\n(" RTNG_GIT_VERSION ")"; } - std::string name = PROJECT_NAME; - name += "\n"; - name += APPLICATION_NAME; - aboutInfo.SetName(APPLICATION_NAME); aboutInfo.SetDescription(version); diff --git a/src/radiotray-ng/player/player.cpp b/src/radiotray-ng/player/player.cpp index 316a185..7943411 100644 --- a/src/radiotray-ng/player/player.cpp +++ b/src/radiotray-ng/player/player.cpp @@ -16,17 +16,12 @@ // along with Radiotray-NG. If not, see . #include -#include +#include Player::Player(std::shared_ptr config, std::shared_ptr event_bus) - : pipeline(nullptr) - , souphttpsrc(nullptr) - , clock(nullptr) - , clock_id(nullptr) - , event_bus(std::move(event_bus)) + : event_bus(std::move(event_bus)) , config(std::move(config)) - , gst_bus(nullptr) { LOG(info) << "starting gstreamer"; @@ -62,7 +57,14 @@ bool Player::play_next() LOG(debug) << BUFFER_SIZE_KEY << "=" << std::to_string(buffer_size * buffer_duration) << ", " << BUFFER_DURATION_KEY << "=" << buffer_duration; - this->volume(this->config->get_uint32(VOLUME_LEVEL_KEY, DEFAULT_VOLUME_LEVEL_VALUE)); + if (!this->has_played) + { + const auto volume = this->config->get_uint32(VOLUME_LEVEL_KEY, DEFAULT_VOLUME_LEVEL_VALUE); + + LOG(debug) << "setting startup volume: " << volume; + + this->volume(volume); + } if (gst_element_set_state(this->pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) { @@ -123,7 +125,7 @@ void Player::stop() // abort outstanding callback... if (this->clock_id) { - LOG(info) << "canceling outstanding clock request"; + LOG(debug) << "canceling outstanding clock request"; gst_clock_id_unschedule(this->clock_id); gst_clock_id_unref(this->clock_id); this->clock_id = nullptr; @@ -168,25 +170,6 @@ bool Player::is_muted() } -void Player::notify_source_cb(GObject* obj, GParamSpec* /*param*/, gpointer /*user_data*/) -{ - // set our user-agent... - if (g_object_class_find_property(G_OBJECT_GET_CLASS(obj), "source")) - { - GObject* source_element; - g_object_get(obj, "source", &source_element, NULL); - - // todo: detect distro at runtime instead of what we were compiled on... - if (g_object_class_find_property(G_OBJECT_GET_CLASS(source_element), "user-agent")) - { - g_object_set(source_element, "user-agent", RTNG_USER_AGENT, NULL); - } - - g_object_unref(source_element); - } -} - - gboolean Player::timer_cb(GstClock* /*clock*/, GstClockTime /*time*/, GstClockID /*id*/, gpointer user_data) { auto player{static_cast(user_data)}; @@ -208,6 +191,31 @@ gboolean Player::timer_cb(GstClock* /*clock*/, GstClockTime /*time*/, GstClockID } +gboolean Player::notify_volume_cb(GstBus* /*bus*/, GstMessage* /*message*/, gpointer user_data) +{ + auto player{static_cast(user_data)}; + + gdouble volume; + g_object_get(G_OBJECT(player->pipeline), "volume", &volume, NULL); + + // update volume as it may of been changed using another application... + const uint32_t new_volume = std::round(volume * 100); + + // only save if it's different... + if (player->config->get_uint32(VOLUME_LEVEL_KEY, DEFAULT_VOLUME_LEVEL_VALUE) != new_volume) + { + LOG(debug) << "volume: " << new_volume; + + player->config->set_uint32(VOLUME_LEVEL_KEY, new_volume); + player->config->save(); + + player->event_bus->publish_only(IEventBus::event::volume_changed, VOLUME_LEVEL_KEY, std::to_string(new_volume)); + } + + return TRUE; +} + + gboolean Player::handle_messages_cb(GstBus* /*bus*/, GstMessage* message, gpointer user_data) { auto player{static_cast(user_data)}; @@ -313,6 +321,7 @@ gboolean Player::handle_messages_cb(GstBus* /*bus*/, GstMessage* message, gpoint GstState old_state; GstState new_state; gst_message_parse_state_changed(message, &old_state, &new_state, nullptr); + player->has_played = true; if (GST_MESSAGE_SRC(message) == GST_OBJECT(player->pipeline)) { @@ -379,7 +388,7 @@ void Player::for_each_tag_cb(const GstTagList* list, const gchar* tag, gpointer str = g_strdup_value_contents(gst_tag_list_get_value_index(list, tag, i)); } - // todo: for now ignore anything that looks encoded... + // Ignore anything that looks encoded... if (std::string(str).find("gst_bus = gst_element_get_bus(this->pipeline); gst_bus_add_watch(this->gst_bus, static_cast(&Player::handle_messages_cb), this); - - g_signal_connect(G_OBJECT(this->pipeline), "notify::source", G_CALLBACK(&Player::notify_source_cb), this); + g_signal_connect(this->pipeline, "notify::volume", G_CALLBACK(&Player::notify_volume_cb), this); } diff --git a/src/radiotray-ng/player/player.hpp b/src/radiotray-ng/player/player.hpp index e9a6b31..28155ce 100644 --- a/src/radiotray-ng/player/player.hpp +++ b/src/radiotray-ng/player/player.hpp @@ -48,25 +48,24 @@ class Player final : public IPlayer void gst_stop(); static gboolean handle_messages_cb(GstBus* bus, GstMessage* message, gpointer user_data); - - static void notify_source_cb(GObject* obj, GParamSpec* param, gpointer user_data); - static gboolean timer_cb(GstClock* clock, GstClockTime time, GstClockID id, gpointer user_data); + static gboolean notify_volume_cb(GstBus* bus, GstMessage* message, gpointer user_data); static void for_each_tag_cb(const GstTagList* list, const gchar* tag, gpointer user_data); bool play_next(); - GstElement* pipeline; - GstElement* souphttpsrc; - GstClock* clock; - GstClockID clock_id; + GstElement* pipeline = nullptr; + GstElement* souphttpsrc = nullptr; + GstClock* clock = nullptr; + GstClockID clock_id = nullptr; bool buffering = false; + bool has_played = false; playlist_t current_playlist; std::shared_ptr event_bus; std::shared_ptr config; - GstBus* gst_bus; + GstBus* gst_bus = nullptr; }; diff --git a/src/radiotray-ng/playlist/playlist_downloader.cpp b/src/radiotray-ng/playlist/playlist_downloader.cpp index cbca72f..38c48c1 100644 --- a/src/radiotray-ng/playlist/playlist_downloader.cpp +++ b/src/radiotray-ng/playlist/playlist_downloader.cpp @@ -49,7 +49,7 @@ void PlaylistDownloader::install_decoders() } -bool PlaylistDownloader::download_playlist(const std::string& url, playlist_t& playlist) +bool PlaylistDownloader::download_playlist(const IBookmarks::station_data_t& std, playlist_t& playlist) { playlist.clear(); @@ -60,17 +60,17 @@ bool PlaylistDownloader::download_playlist(const std::string& url, playlist_t& p for(auto& decoder : this->decoders) { - if (decoder->is_url_direct_stream(url)) + if (std.direct || decoder->is_url_direct_stream(std.url)) { - LOG(info) << "detected as a direct stream, decoder: " << decoder->get_name(); + LOG(info) << "detected as a direct stream, decoder: " << ((std.direct) ? "direct=true" : decoder->get_name()); - playlist.push_back(url); + playlist.push_back(std.url); return true; } } // Try downloading N bytes in case it's a media stream - if (!this->download(url, content_type, content, http_resp_code, 4096)) + if (!this->download(std.url, content_type, content, http_resp_code, 4096)) { LOG(error) << "Could not download playlist!"; @@ -78,7 +78,7 @@ bool PlaylistDownloader::download_playlist(const std::string& url, playlist_t& p { // Must be a media stream? LOG(info) << "decoder: none"; - playlist.push_back(url); + playlist.push_back(std.url); return true; } @@ -100,7 +100,7 @@ bool PlaylistDownloader::download_playlist(const std::string& url, playlist_t& p LOG(info) << "no decoders, assuming direct media stream of content-type: " << content_type; - playlist.push_back(url); + playlist.push_back(std.url); return true; } diff --git a/src/radiotray-ng/playlist/playlist_downloader.hpp b/src/radiotray-ng/playlist/playlist_downloader.hpp index 4cd55f7..7e9fe7c 100644 --- a/src/radiotray-ng/playlist/playlist_downloader.hpp +++ b/src/radiotray-ng/playlist/playlist_downloader.hpp @@ -35,11 +35,11 @@ class PlaylistDownloader final : public IPlaylistDownloader /** * Downloads playlist and runs the output through the list of supported decoders. * - * @param url playlist location + * @param std station data * @param playlist playlist * @return true if download and parsing was successful */ - bool download_playlist(const std::string& url, playlist_t& playlist); + bool download_playlist(const IBookmarks::station_data_t& std, playlist_t& playlist); private: std::shared_ptr inspect(const std::string& content_type, const std::string& content); diff --git a/src/radiotray-ng/radiotray_ng.cpp b/src/radiotray-ng/radiotray_ng.cpp index 816dd25..51a5ea8 100644 --- a/src/radiotray-ng/radiotray_ng.cpp +++ b/src/radiotray-ng/radiotray_ng.cpp @@ -510,7 +510,7 @@ void RadiotrayNG::play(const std::string& group, const std::string& station) this->event_bus->publish_only(IEventBus::event::state_changed, STATE_KEY, STATE_CONNECTING); - if (PlaylistDownloader(this->config).download_playlist(std.url, pls)) + if (PlaylistDownloader(this->config).download_playlist(std, pls)) { if (group != this->play_url_group) { @@ -614,12 +614,7 @@ void RadiotrayNG::set_and_save_volume(uint32_t new_volume) if (new_volume != volume) { this->volume = std::to_string(new_volume); - this->player->volume(new_volume); - - LOG(debug) << "volume: " << this->volume; - - this->config->save(); } } diff --git a/tests/bookmarks_test.cpp b/tests/bookmarks_test.cpp index ad5f008..2d72356 100644 --- a/tests/bookmarks_test.cpp +++ b/tests/bookmarks_test.cpp @@ -80,8 +80,10 @@ TEST(Bookmarks, test_that_a_group_is_added_and_removed) { EXPECT_TRUE(bm.get_group_as_json(GROUP_A, json_str)); Json::Value json; - Json::Reader reader; - reader.parse(json_str, json); + Json::CharReaderBuilder rbuilder; + std::unique_ptr const reader(rbuilder.newCharReader()); + std::string parse_errors; + EXPECT_TRUE(reader->parse(json_str.c_str(), json_str.c_str() + json_str.size(), &json, &parse_errors)); EXPECT_EQ(json["group"].asString(), GROUP_A); EXPECT_EQ(json["image"].asString(), GROUP_IMAGE_A); @@ -89,7 +91,6 @@ TEST(Bookmarks, test_that_a_group_is_added_and_removed) EXPECT_EQ(json["stations"][0]["image"].asString(), "image"); EXPECT_EQ(json["stations"][0]["name"].asString(), "name"); EXPECT_EQ(json["stations"][0]["url"].asString(), "url"); - EXPECT_EQ(json["stations"][0]["notifications"].asBool(), true); } json_str.clear(); @@ -101,20 +102,23 @@ TEST(Bookmarks, test_that_a_group_is_added_and_removed) "{" "\"name\" : \"name_json\"," "\"image\" : \"image_json\"," - "\"url\" : \"url_json\"" + "\"url\" : \"url_json\"," + "\"notifications\" : false" "}"; std::string station_name; EXPECT_TRUE(bm.add_station_from_json(GROUP_A, json_str, station_name)); EXPECT_TRUE(bm.get_station_as_json(GROUP_A, station_name, json_str)); Json::Value json; - Json::Reader reader; - reader.parse(json_str, json); + Json::CharReaderBuilder rbuilder; + std::unique_ptr const reader(rbuilder.newCharReader()); + std::string parse_errors; + EXPECT_TRUE(reader->parse(json_str.c_str(), json_str.c_str() + json_str.size(), &json, &parse_errors)); EXPECT_EQ(json["image"].asString(), "image_json"); EXPECT_EQ(json["name"].asString(), "name_json"); EXPECT_EQ(json["url"].asString(), "url_json"); - EXPECT_EQ(json["notifications"].asBool(), true); + EXPECT_EQ(json["notifications"].asBool(), false); } } diff --git a/tests/runners/playlist_runner.cpp b/tests/runners/playlist_runner.cpp index 3fdbbc9..41eb5bd 100644 --- a/tests/runners/playlist_runner.cpp +++ b/tests/runners/playlist_runner.cpp @@ -31,7 +31,8 @@ int main(int argc, char** argv) PlaylistDownloader pld(cfg); playlist_t pls; - pld.download_playlist(argv[1], pls); + IBookmarks::station_data_t std{"", argv[1], "", false, false}; + pld.download_playlist(std, pls); for (auto& url: pls) {