Add action to add streams via youtube-dl

youtube-dl [0] is a "command-line program to download videos from
YouTube.com" and many other websites, e.g. media libraries of public
broadcasters.

This commit adds a new action to ncmpcpp's playlist view: Pressing 'D',
prompts for the URL of a website.  The input is passed to youtube-dl to
retrieve the URL of the audio stream and available metadata.  If
successful, the URL is added to the MPD playlist.

[0]: https://github.com/ytdl-org/youtube-dl
This commit is contained in:
Lennart Braun
2020-03-27 00:04:14 +01:00
parent 231c0970c9
commit 523b7d4135
6 changed files with 116 additions and 0 deletions

View File

@@ -25,8 +25,17 @@
#include <boost/filesystem/operations.hpp>
#include <boost/locale/conversion.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/process/child.hpp>
#include <boost/process/io.hpp>
#include <boost/process/pipe.hpp>
#include <boost/process/search_path.hpp>
#include <boost/property_tree/exceptions.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/ptree_fwd.hpp>
#include <algorithm>
#include <iostream>
#include <mpd/tag.h>
#include "actions.h"
#include "charset.h"
@@ -2746,6 +2755,94 @@ void ShowServerInfo::run()
myServerInfo->switchTo();
}
bool AddYoutubeDLItem::canBeRun()
{
return myScreen == myPlaylist;
}
void AddYoutubeDLItem::run()
{
using Global::wFooter;
namespace bp = boost::process;
namespace pt = boost::property_tree;
std::string url;
{
Statusbar::ScopedLock slock;
Statusbar::put() << "Add via youtube-dl: ";
url = wFooter->prompt();
}
// do nothing if no url is given
if (url.empty())
return;
// search the youtube-dl executable in the PATH
auto ydl_path = bp::search_path("youtube-dl");
if (ydl_path.empty()) {
Statusbar::ScopedLock slock;
Statusbar::put() << "youtube-dl was not found in PATH";
return;
}
{
Statusbar::ScopedLock slock;
Statusbar::put() << "Calling youtube-dl with '" << url << "' ...";
wFooter->refresh();
}
// start youtube-dl in a child process
// -j: output as JSON, each playlist item on a separate line
// -f bestaudio/best: selects the best available audio-only stream, or
// alternatively the best audio+video stream
bp::ipstream output;
bp::child child_process(ydl_path, url, "-j", "-f", "bestaudio/best", bp::std_out > output,
bp::std_err > bp::null);
// extract the URL and metadata from a ptree object and add
auto add_song = [] (const pt::ptree& ptree) {
auto download_url = ptree.get<std::string>("url");
auto title = ptree.get_optional<std::string>("title");
auto artist = ptree.get_optional<std::string>("creator");
if (!artist.has_value()) {
artist = ptree.get_optional<std::string>("uploader");
}
auto album = ptree.get_optional<std::string>("album");
auto id = Mpd.AddSong(download_url);
if (id == -1) {
return;
}
if (title.has_value()) {
Mpd.AddTag(id, MPD_TAG_TITLE, *title);
}
if (artist.has_value()) {
Mpd.AddTag(id, MPD_TAG_ARTIST, *artist);
}
if (album.has_value()) {
Mpd.AddTag(id, MPD_TAG_ALBUM, *album);
}
};
std::string line;
pt::ptree ptree;
while (std::getline(output, line)) {
try {
std::istringstream line_stream(line);
pt::read_json(line_stream, ptree);
add_song(ptree);
} catch (pt::ptree_error &e) {
Statusbar::ScopedLock slock;
Statusbar::put() << "An error occurred while calling youtube-dl or parsing its output";
wFooter->refresh();
}
}
if (child_process.running()) {
child_process.terminate();
}
}
}
namespace {
@@ -2884,6 +2981,7 @@ void populateActions()
insert_action(new Actions::ShowVisualizer());
insert_action(new Actions::ShowClock());
insert_action(new Actions::ShowServerInfo());
insert_action(new Actions::AddYoutubeDLItem());
for (size_t i = 0; i < AvailableActions.size(); ++i)
{
if (AvailableActions[i] == nullptr)

View File

@@ -164,6 +164,7 @@ enum class Type
ShowVisualizer,
ShowClock,
ShowServerInfo,
AddYoutubeDLItem,
_numberOfActions // needed to dynamically calculate size of action array
};
@@ -1425,6 +1426,15 @@ private:
virtual void run() override;
};
struct AddYoutubeDLItem: BaseAction
{
AddYoutubeDLItem(): BaseAction(Type::AddYoutubeDLItem, "add_youtube-dl_item") { }
private:
virtual bool canBeRun() override;
virtual void run() override;
};
}
#endif // NCMPCPP_ACTIONS_H

View File

@@ -762,6 +762,8 @@ void BindingsConfiguration::generateDefaults()
bind(k, Actions::Type::SetSelectedItemsPriority);
if (notBound(k = stringToKey("q")))
bind(k, Actions::Type::Quit);
if (notBound(k = stringToKey("D")))
bind(k, Actions::Type::AddYoutubeDLItem);
}
const Command *BindingsConfiguration::findCommand(const std::string &name)

View File

@@ -567,6 +567,10 @@ int Connection::AddSong(const Song &s, int pos)
return AddSong((!s.isFromDatabase() ? "file://" : "") + s.getURI(), pos);
}
void Connection::AddTag(int id, mpd_tag_type tag, const std::string &value) {
mpd_run_add_tag_id(m_connection.get(), id, tag, value.c_str());
}
bool Connection::Add(const std::string &path)
{
bool result;

View File

@@ -552,6 +552,7 @@ struct Connection
int AddSong(const std::string &, int = -1); // returns id of added song
int AddSong(const Song &, int = -1); // returns id of added song
void AddTag(int id, mpd_tag_type, const std::string &);
bool AddRandomTag(mpd_tag_type, size_t, std::mt19937 &rng);
bool AddRandomSongs(size_t number, const std::string &random_exclude_pattern, std::mt19937 &rng);
bool Add(const std::string &path);

View File

@@ -258,6 +258,7 @@ void write_bindings(NC::Scrollpad &w)
key(w, Type::ReversePlaylist, "Reverse range");
key(w, Type::JumpToPlayingSong, "Jump to current song");
key(w, Type::TogglePlayingSongCentering, "Toggle playing song centering");
key(w, Type::AddYoutubeDLItem, "Add items via youtube-dl");
key_section(w, "Browser");
key(w, Type::EnterDirectory, "Enter directory");