changeset 21:38d927bed5c3

mpc: initial import in C++
author David Demelier <markand@malikania.fr>
date Thu, 26 Apr 2018 17:15:30 +0200
parents 370213df9449
children cc87d1ed14a5
files CMakeLists.txt TODO.md dmenu-mpc/CMakeLists.txt dmenu-mpc/main.cpp libdmenutools/dmenu/join.hpp
diffstat 5 files changed, 430 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Thu Apr 26 13:23:44 2018 +0200
+++ b/CMakeLists.txt	Thu Apr 26 17:15:30 2018 +0200
@@ -26,3 +26,4 @@
 
 add_subdirectory(libdmenutools)
 add_subdirectory(dmenu-background)
+add_subdirectory(dmenu-mpc)
--- a/TODO.md	Thu Apr 26 13:23:44 2018 +0200
+++ b/TODO.md	Thu Apr 26 17:15:30 2018 +0200
@@ -10,5 +10,3 @@
 
 New tools
 ---------
-
-  - `dmenu-mpc`
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dmenu-mpc/CMakeLists.txt	Thu Apr 26 17:15:30 2018 +0200
@@ -0,0 +1,29 @@
+# CMakeLists.txt -- CMake build system for dmenutools
+#
+# Copyright (c) 2018 David Demelier <markand@malikania.fr>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+#
+
+project(dmenu-mpc)
+
+find_package(PkgConfig)
+
+pkg_check_modules(libmpdclient IMPORTED_TARGET libmpdclient)
+
+if (NOT libmpdclient_FOUND)
+    message("NOT building dmenu-mpc")
+else ()
+    add_executable(dmenu-mpc main.cpp)
+    target_link_libraries(dmenu-mpc libdmenutools PkgConfig::libmpdclient)
+endif ()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dmenu-mpc/main.cpp	Thu Apr 26 17:15:30 2018 +0200
@@ -0,0 +1,334 @@
+/*
+ * main.cpp -- mpd control
+ *
+ * Copyright (c) 2018 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <cstdint>
+#include <iostream>
+#include <map>
+#include <memory>
+#include <set>
+#include <sstream>
+#include <stdexcept>
+
+#include <boost/optional.hpp>
+
+#include <dmenu/dmenu.hpp>
+#include <dmenu/join.hpp>
+
+#include <mpd/client.h>
+
+namespace {
+
+// {{{ types
+
+/*
+ * Wrap mpd_connection in a safe way
+ */
+using connection = std::unique_ptr<struct mpd_connection, void (*)(struct mpd_connection*)>;
+
+// }}}
+
+// {{{ entry (class)
+
+/*
+ * Describe a file entry.
+ *
+ * This is used when listing all files in the following form
+ *
+ * artist/album 1/file 1.flac
+ * artist/album 1/file 2.flac
+ * artist/album 2/file 1.flac
+ * artist/album 2/file 2.flac
+ *
+ * The entry is decomposed as following:
+ *
+ * artist/album 2/file 2.flac
+ * ^^^^^^^^^^^^^^ ^^^^^^^^^^^
+ *       |              |
+ *       | parents      |
+ *                      | filename
+ *
+ */
+class entry {
+public:
+    std::vector<std::string> parents;
+    std::string filename;
+
+    entry(const std::string& file);
+};
+
+entry::entry(const std::string& file)
+{
+    std::vector<std::string> path;
+    std::istringstream iss(file);
+
+    for (std::string comp; std::getline(iss, comp, '/'); )
+        parents.push_back(comp);
+
+    // Remove last component, it's file the filename.
+    filename = parents.back();
+    parents.pop_back();
+}
+
+// }}}
+
+// {{{ tree (class)
+
+/*
+ * This class models the library as a tree view for an easier selection.
+ */
+class tree {
+public:
+    std::set<std::string> entries;
+    std::map<std::string, tree> children;
+
+    tree() = default;
+
+    tree(const std::vector<std::string>& files);
+
+    tree& parents(const std::vector<std::string>& path);
+
+    bool has_child(const std::string& name) const noexcept;
+};
+
+tree::tree(const std::vector<std::string>& files)
+{
+    for (const auto& file : files) {
+        const entry path(file);
+
+        parents(path.parents).entries.insert(path.filename);
+    }
+}
+
+tree& tree::parents(const std::vector<std::string>& path)
+{
+    tree* select = this;
+
+    for (std::size_t i = 0; i < path.size(); ++i)
+        select = &select->children[path[i]];
+
+    return *select;
+}
+
+bool tree::has_child(const std::string& name) const noexcept
+{
+    for (const auto& child : children)
+        if (child.first == name)
+            return true;
+
+    return false;
+}
+
+// }}}
+
+// {{{ connect
+
+connection connect()
+{
+    const auto section = dmenu::config("mpd");
+    const auto host = section.get("host").get_value();
+    const auto port = std::atoi(section.get("port").get_value().c_str());
+    const auto timeout = std::atoi(section.get("timeout").get_value().c_str());
+
+    connection conn(
+        mpd_connection_new(host.empty() ? nullptr : host.c_str(), port, timeout),
+        mpd_connection_free
+    );
+
+    if (mpd_connection_get_error(conn.get()) != MPD_ERROR_SUCCESS)
+        throw std::runtime_error(mpd_connection_get_error_message(conn.get()));
+
+    return conn;
+}
+
+// }}}
+
+// {{{ add
+
+std::vector<std::string> add_list_all(struct mpd_connection* conn)
+{
+    using song = std::unique_ptr<struct mpd_song, void (*)(struct mpd_song*)>;
+
+    std::vector<std::string> result;
+
+    if (!mpd_send_list_all(conn, nullptr))
+        throw std::runtime_error(mpd_connection_get_error_message(conn));
+
+    song iter(nullptr, nullptr);
+
+    while ((iter = { mpd_recv_song(conn), mpd_song_free }))
+        result.push_back(mpd_song_get_uri(iter.get()));
+
+    return result;
+}
+
+std::string add_select(const tree& tree, bool is_root)
+{
+    std::vector<std::string> lines{"."};
+
+    if (!is_root)
+        lines.push_back("..");
+    for (const auto& entry : tree.entries)
+        lines.push_back(entry);
+    for (const auto& child : tree.children)
+        lines.push_back(child.first);
+
+    return dmenu::run({ "-l 16" }, lines);
+}
+
+void add(struct mpd_connection* conn)
+{
+    tree root(add_list_all(conn));
+    std::vector<std::string> path;
+
+    for (;;) {
+        const auto item = add_select(root.parents(path), path.empty());
+
+        if (item == "..")
+            path.pop_back();
+        else if (root.parents(path).has_child(item))
+            path.push_back(item);
+        else {
+            if (item != ".")
+                path.push_back(item);
+
+            break;
+        }
+    }
+
+    const auto entry = dmenu::join(path.begin(), path.end(), "/");
+
+    if (!mpd_run_add(conn, entry.c_str()))
+        throw std::runtime_error(mpd_connection_get_error_message(conn));
+}
+
+// }}}
+
+// {{{ play
+
+void play(struct mpd_connection* conn)
+{
+    if (!mpd_run_play(conn))
+        throw std::runtime_error(mpd_connection_get_error_message(conn));
+}
+
+// }}}
+
+// {{{ pause
+
+void pause(struct mpd_connection* conn)
+{
+    if (!mpd_run_toggle_pause(conn))
+        throw std::runtime_error(mpd_connection_get_error_message(conn));
+}
+
+// }}}
+
+// {{{ stop
+
+void stop(struct mpd_connection* conn)
+{
+    if (!mpd_run_stop(conn))
+        throw std::runtime_error(mpd_connection_get_error_message(conn));
+}
+
+// }}}
+
+// {{{ next
+
+void next(struct mpd_connection* conn)
+{
+    if (!mpd_run_next(conn))
+        throw std::runtime_error(mpd_connection_get_error_message(conn));
+}
+
+// }}}
+
+// {{{ previous
+
+void previous(struct mpd_connection* conn)
+{
+    if (!mpd_run_previous(conn))
+        throw std::runtime_error(mpd_connection_get_error_message(conn));
+}
+
+// }}}
+
+// {{{ clear
+
+void clear(struct mpd_connection* conn)
+{
+    if (!mpd_run_clear(conn))
+        throw std::runtime_error(mpd_connection_get_error_message(conn));
+}
+
+// }}}
+
+// {{{ menu
+
+boost::optional<std::string> menu()
+{
+    static const std::vector<std::string> lines{
+        "add...",
+        "",
+        "play",
+        "pause",
+        "stop",
+        "next",
+        "previous",
+        "",
+        "clear"
+    };
+
+    const auto selection = dmenu::run({ "-l 16", "-p mpc" }, lines);
+
+    if (selection.empty())
+        return boost::none;
+
+    return selection;
+}
+
+// }}} menu
+
+} // !namespace
+
+int main()
+{
+    try {
+        if (auto selection = menu()) {
+            auto conn = connect();
+
+            if (*selection == "add...")
+                add(conn.get());
+            else if (*selection == "play")
+                play(conn.get());
+            else if (*selection == "pause")
+                pause(conn.get());
+            else if (*selection == "stop")
+                stop(conn.get());
+            else if (*selection == "next")
+                next(conn.get());
+            else if (*selection == "previous")
+                previous(conn.get());
+            else if (*selection == "clear")
+                clear(conn.get());
+        }
+    } catch (const std::exception& ex) {
+        std::cerr << "abort: " << ex.what() << std::endl;
+        return 1;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libdmenutools/dmenu/join.hpp	Thu Apr 26 17:15:30 2018 +0200
@@ -0,0 +1,66 @@
+/*
+ * join.hpp -- join lists into a string
+ *
+ * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/**
+ * \file join.hpp
+ * \brief Join function
+ */
+
+#include <initializer_list>
+#include <string>
+#include <sstream>
+
+namespace dmenu {
+
+/**
+ * Join values by a separator and return a string.
+ *
+ * \param first the first iterator
+ * \param last the last iterator
+ * \param delim the optional delimiter
+ * \return the string
+ */
+template <typename InputIt, typename DelimType = char>
+std::string join(InputIt first, InputIt last, DelimType delim = ':')
+{
+    std::ostringstream oss;
+
+    if (first != last) {
+        oss << *first;
+
+        while (++first != last)
+            oss << delim << *first;
+    }
+
+    return oss.str();
+}
+
+/**
+ * Convenient overload.
+ *
+ * \param list the initializer list
+ * \param delim the delimiter
+ * \return the string
+ */
+template <typename T, typename DelimType = char>
+inline std::string join(std::initializer_list<T> list, DelimType delim = ':')
+{
+    return join(list.begin(), list.end(), delim);
+}
+
+} // !dmenu