commits
tags
#include "downloader.h"
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include "util.h"
typedef void (*line_cb_t)(void *ud, const char *line);
struct stderr_reader_args {
int fd;
char *buf;
size_t cap;
};
static void *stderr_reader_thread(void *arg) {
struct stderr_reader_args *a = arg;
size_t used = 0;
char chunk[512];
ssize_t n;
while ((n = read(a->fd, chunk, sizeof(chunk))) > 0) {
if (a->cap > 0 && used < a->cap - 1) {
size_t room = a->cap - 1 - used;
size_t take = (size_t)n < room ? (size_t)n : room;
memcpy(a->buf + used, chunk, take);
used += take;
a->buf[used] = '\0';
}
/* otherwise keep draining and discard, so the child never blocks on a full pipe */
}
return NULL;
}
/* Forks + execs argv, streams stdout lines to stdout_cb, captures stderr into err.
* Returns 0 if the child exited with status 0, -1 otherwise. */
static int run_process(char *const argv[], line_cb_t stdout_cb, void *stdout_ud,
char *err, size_t err_cap) {
int out_pipe[2], err_pipe[2];
if (pipe(out_pipe) != 0 || pipe(err_pipe) != 0) {
snprintf(err, err_cap, "pipe() failed: %s", strerror(errno));
return -1;
}
pid_t pid = fork();
if (pid < 0) {
snprintf(err, err_cap, "fork() failed: %s", strerror(errno));
return -1;
}
if (pid == 0) {
dup2(out_pipe[1], STDOUT_FILENO);
dup2(err_pipe[1], STDERR_FILENO);
close(out_pipe[0]);
close(out_pipe[1]);
close(err_pipe[0]);
close(err_pipe[1]);
execvp(argv[0], argv);
_exit(127);
}
close(out_pipe[1]);
close(err_pipe[1]);
if (err_cap > 0) err[0] = '\0';
struct stderr_reader_args sargs = {.fd = err_pipe[0], .buf = err, .cap = err_cap};
pthread_t err_thread;
pthread_create(&err_thread, NULL, stderr_reader_thread, &sargs);
FILE *out_f = fdopen(out_pipe[0], "r");
if (out_f) {
char *line = NULL;
size_t linecap = 0;
ssize_t len;
while ((len = getline(&line, &linecap, out_f)) >= 0) {
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
line[--len] = '\0';
}
if (stdout_cb) stdout_cb(stdout_ud, line);
}
free(line);
fclose(out_f);
} else {
close(out_pipe[0]);
}
pthread_join(err_thread, NULL);
close(err_pipe[0]);
int status = 0;
waitpid(pid, &status, 0);
if (WIFEXITED(status) && WEXITSTATUS(status) == 0) return 0;
return -1;
}
/* ---- resolve -------------------------------------------------------- */
#define RESOLVE_TITLE_CAP 512
#define RESOLVE_URL_CAP 2048
struct resolve_ctx {
entry_cb_t on_entry;
void *user_data;
char *playlist_title;
size_t playlist_title_cap;
int got_playlist_title;
int entry_count;
};
static void resolve_line_cb(void *ud, const char *line) {
struct resolve_ctx *ctx = ud;
const char *sep1 = strchr(line, '\x1f');
if (!sep1) return;
const char *sep2 = strchr(sep1 + 1, '\x1f');
if (!sep2) return;
char title[RESOLVE_TITLE_CAP];
char url[RESOLVE_URL_CAP];
size_t title_len = (size_t)(sep1 - line);
size_t url_len = (size_t)(sep2 - (sep1 + 1));
if (title_len >= sizeof(title)) title_len = sizeof(title) - 1;
if (url_len >= sizeof(url)) url_len = sizeof(url) - 1;
memcpy(title, line, title_len);
title[title_len] = '\0';
memcpy(url, sep1 + 1, url_len);
url[url_len] = '\0';
const char *playlist_title_field = sep2 + 1;
if (!ctx->got_playlist_title && *playlist_title_field) {
safe_strcpy(ctx->playlist_title, ctx->playlist_title_cap, playlist_title_field);
ctx->got_playlist_title = 1;
}
if (*url && ctx->on_entry) {
ctx->on_entry(ctx->user_data, url, *title ? title : url);
ctx->entry_count++;
}
}
int downloader_resolve(const char *url, entry_cb_t on_entry, void *user_data,
char *playlist_title, size_t playlist_title_cap,
char *err, size_t err_cap) {
if (playlist_title_cap > 0) playlist_title[0] = '\0';
static const char *template = "%(title)s\x1f%(webpage_url,url)s\x1f%(playlist_title|)s";
char *argv[] = {
"yt-dlp", "--flat-playlist", "--skip-download",
"--no-cache-dir", /* concurrent yt-dlp invocations sharing ~/.cache/yt-dlp can race */
"--print", (char *)template,
(char *)url, NULL,
};
struct resolve_ctx ctx = {
.on_entry = on_entry,
.user_data = user_data,
.playlist_title = playlist_title,
.playlist_title_cap = playlist_title_cap,
.got_playlist_title = 0,
.entry_count = 0,
};
char proc_err[DL_ERROR_CAP];
run_process(argv, resolve_line_cb, &ctx, proc_err, sizeof(proc_err));
if (ctx.entry_count == 0) {
if (*proc_err) {
snprintf(err, err_cap, "%s", proc_err);
} else {
snprintf(err, err_cap, "Nothing downloadable found at %s", url);
}
return -1;
}
return 0;
}
/* ---- download --------------------------------------------------------- */
struct download_ctx {
progress_cb_t cb;
void *user_data;
};
static void download_line_cb(void *ud, const char *line) {
struct download_ctx *ctx = ud;
if (strncmp(line, "[download]", 10) != 0) return;
float percent;
char speed[64];
char eta[32];
int n = sscanf(line, "[download] %f%% of %*s at %63s ETA %31s", &percent, speed, eta);
if (n == 3 && ctx->cb) {
ctx->cb(ctx->user_data, percent, speed, eta);
}
}
int downloader_download(const char *url, const char *output_dir, const char *audio_format,
progress_cb_t cb, void *user_data,
char *err, size_t err_cap) {
if (mkdir_p(output_dir) != 0) {
snprintf(err, err_cap, "Could not create %s: %s", output_dir, strerror(errno));
return -1;
}
char outtmpl[2200];
snprintf(outtmpl, sizeof(outtmpl), "%s/%%(title)s.%%(ext)s", output_dir);
char *argv[] = {
"yt-dlp",
"-f", "bestaudio/best",
"-x", "--audio-format", (char *)audio_format,
"--audio-quality", "0",
"--embed-thumbnail",
"--embed-metadata",
"--no-cache-dir", /* concurrent yt-dlp invocations sharing ~/.cache/yt-dlp can race */
"--newline",
"-o", outtmpl,
(char *)url,
NULL,
};
struct download_ctx ctx = {.cb = cb, .user_data = user_data};
char proc_err[DL_ERROR_CAP];
int rc = run_process(argv, download_line_cb, &ctx, proc_err, sizeof(proc_err));
if (rc != 0) {
if (*proc_err) {
snprintf(err, err_cap, "%s", proc_err);
} else {
snprintf(err, err_cap, "yt-dlp exited with an error");
}
return -1;
}
return 0;
}