Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 166 additions & 86 deletions src/actions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,21 @@
#include <cassert>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <boost/date_time/posix_time/posix_time.hpp>
#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 <cstdio>
#include <sstream>
#include <sys/wait.h>
#include <unistd.h>
#include <mpd/tag.h>

#include "actions.h"
Expand Down Expand Up @@ -2764,90 +2765,169 @@ bool AddYoutubeDLItem::canBeRun()

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::print("youtube-dl was not found in PATH");
return;
}

Statusbar::printf("Calling youtube-dl with '%1%' ...", url);

// 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", "--playlist-end", "100", bp::std_out > output,
bp::std_err > bp::null);
using Global::wFooter;
namespace pt = boost::property_tree;

auto find_executable = [] (const std::string &name) -> std::string {
const char *path_env = std::getenv("PATH");
if (!path_env)
return {};

std::string path(path_env);
std::stringstream ss(path);
std::string dir;

std::vector<std::string> dirs;
while (std::getline(ss, dir, ':'))
{
if (dir.empty())
dir = ".";
dirs.push_back(dir);
}

for (const auto &d : dirs)
{
std::string candidate = d + '/' + name;
if (access(candidate.c_str(), X_OK) == 0)
return candidate;
}

return {};
};

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 yt-dlp/youtube-dl executable in the PATH (prefer yt-dlp)
auto ydl_path = find_executable("yt-dlp");
if (ydl_path.empty())
ydl_path = find_executable("youtube-dl");

if (ydl_path.empty()) {
Statusbar::print("yt-dlp (or youtube-dl) was not found in PATH");
return;
}

Statusbar::printf("Calling yt-dlp with '%1%' ...", url);

// start yt-dlp in a child process
// -j: output as JSON, each playlist item on a separate line
// --no-flat-playlist: ensure streaming URLs are included per entry
// -f bestaudio/best: selects the best available audio-only stream, or
// alternatively the best audio+video stream
std::string escaped_url = url;
escapeSingleQuotes(escaped_url);
std::string command = ydl_path + " '" + escaped_url + "' -j --no-flat-playlist -f bestaudio/best 2>/dev/null";

FILE *pipe = popen(command.c_str(), "r");
if (!pipe) {
Statusbar::print("Failed to start youtube-dl");
return;
}

// 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 0;
}
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);
}
return 1;
};

std::string line;
pt::ptree ptree;
unsigned num_songs_added = 0;

while (std::getline(output, line)) {
try {
std::istringstream line_stream(line);
pt::read_json(line_stream, ptree);
num_songs_added += add_song(ptree);
} catch (pt::ptree_error &e) {
Statusbar::print("An error occurred while parsing the output of youtube-dl");
continue;
}
Statusbar::printf("Added %1% item(s) to playlist", num_songs_added);
}

if (child_process.running()) {
child_process.terminate();
}
child_process.wait();

auto ec = child_process.exit_code();
if (ec == 0) {
Statusbar::printf("Added %1% item(s) to playlist", num_songs_added);
} else {
Statusbar::printf("Added %1% item(s) to playlist (youtube-dl exited with exit code %2%)", num_songs_added, ec);
}
auto add_song = [] (const pt::ptree& ptree) {
std::string final_url;

if (auto requested_formats = ptree.get_child_optional("requested_formats")) {
for (auto &format : *requested_formats) {
if (auto url = format.second.get_optional<std::string>("url")) {
final_url = *url;
break;
}
}
}

if (final_url.empty()) {
if (auto formats = ptree.get_child_optional("formats")) {
for (auto &format : *formats) {
auto acodec = format.second.get<std::string>("acodec", "");
auto vcodec = format.second.get<std::string>("vcodec", "");

if (vcodec == "none" && !acodec.empty()) {
if (auto url = format.second.get_optional<std::string>("url")) {
final_url = *url;
break;
}
}
}
}
}

if (final_url.empty()) {
return 0;
}

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(final_url);
if (id == -1) {
return 0;
}
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);
}
return 1;
};

std::string line;
pt::ptree ptree;
unsigned num_songs_added = 0;

auto process_line = [&] (const std::string &json_line) {
try {
std::istringstream line_stream(json_line);
pt::read_json(line_stream, ptree);
num_songs_added += add_song(ptree);
} catch (pt::ptree_error &) {
Statusbar::print("An error occurred while parsing the output of youtube-dl");
return;
}
Statusbar::printf("Added %1% item(s) to playlist", num_songs_added);
};

char buffer[4096];
while (fgets(buffer, sizeof(buffer), pipe))
{
line.append(buffer);
if (!line.empty() && line.back() == '\n')
{
line.pop_back();
process_line(line);
line.clear();
}
}
if (!line.empty())
process_line(line);

int raw_status = pclose(pipe);
int ec = raw_status;
if (raw_status >= 0 && WIFEXITED(raw_status))
ec = WEXITSTATUS(raw_status);

if (ec == 0) {
Statusbar::printf("Added %1% item(s) to playlist", num_songs_added);
} else {
Statusbar::printf("Added %1% item(s) to playlist (youtube-dl exited with exit code %2%)", num_songs_added, ec);
}
}

}
Expand Down
8 changes: 4 additions & 4 deletions src/screens/help.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,10 @@ void write_bindings(NC::Scrollpad &w)
key(w, Type::SavePlaylist, "Save playlist");
key(w, Type::Shuffle, "Shuffle range");
key(w, Type::SortPlaylist, "Sort range");
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(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 yt-dlp/youtube-dl");

key_section(w, "Browser");
key(w, Type::EnterDirectory, "Enter directory");
Expand Down