commits
tags
#include "app.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "downloader.h"
#include "util.h"
#define MESSAGE_TTL_TICKS 30 /* ~ a few seconds at the UI's redraw rate */
#define DOWNLOAD_MAX_ATTEMPTS 3
#define DOWNLOAD_RETRY_DELAY_SECS 2
void app_init(App *app, const char *output_dir, const char *audio_format) {
memset(app, 0, sizeof(*app));
queue_init(&app->queue);
pthread_mutex_init(&app->settings_lock, NULL);
pthread_mutex_init(&app->work_lock, NULL);
pthread_cond_init(&app->work_cond, NULL);
pthread_mutex_init(&app->message_lock, NULL);
safe_strcpy(app->output_dir, sizeof(app->output_dir), output_dir);
safe_strcpy(app->audio_format, sizeof(app->audio_format), audio_format);
app->playlist_folders = 1;
}
/* ---- settings ---------------------------------------------------------- */
void app_set_output_dir(App *app, const char *path) {
pthread_mutex_lock(&app->settings_lock);
safe_strcpy(app->output_dir, sizeof(app->output_dir), path);
pthread_mutex_unlock(&app->settings_lock);
app_notify(app, 0, "Now saving to %s", path);
}
void app_get_output_dir(App *app, char *out, size_t cap) {
pthread_mutex_lock(&app->settings_lock);
safe_strcpy(out, cap, app->output_dir);
pthread_mutex_unlock(&app->settings_lock);
}
void app_set_playlist_folders(App *app, int enabled) {
pthread_mutex_lock(&app->settings_lock);
app->playlist_folders = enabled;
pthread_mutex_unlock(&app->settings_lock);
}
int app_get_playlist_folders(App *app) {
pthread_mutex_lock(&app->settings_lock);
int v = app->playlist_folders;
pthread_mutex_unlock(&app->settings_lock);
return v;
}
static void app_get_audio_format(App *app, char *out, size_t cap) {
pthread_mutex_lock(&app->settings_lock);
safe_strcpy(out, cap, app->audio_format);
pthread_mutex_unlock(&app->settings_lock);
}
/* ---- messages ------------------------------------------------------------ */
void app_notify(App *app, int is_error, const char *fmt, ...) {
char buf[MESSAGE_CAP];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
pthread_mutex_lock(&app->message_lock);
safe_strcpy(app->message, sizeof(app->message), buf);
app->message_ttl_ticks = MESSAGE_TTL_TICKS;
app->message_is_error = is_error;
pthread_mutex_unlock(&app->message_lock);
}
void app_get_message(App *app, char *out, size_t cap, int *is_error) {
pthread_mutex_lock(&app->message_lock);
if (app->message_ttl_ticks > 0) {
safe_strcpy(out, cap, app->message);
if (is_error) *is_error = app->message_is_error;
app->message_ttl_ticks--;
} else {
if (cap > 0) out[0] = '\0';
if (is_error) *is_error = 0;
}
pthread_mutex_unlock(&app->message_lock);
}
/* ---- work queue ---------------------------------------------------------- */
static void app_enqueue_work(App *app, int id) {
pthread_mutex_lock(&app->work_lock);
if (app->work_count < MAX_QUEUE_ITEMS) {
app->work_ids[app->work_tail] = id;
app->work_tail = (app->work_tail + 1) % MAX_QUEUE_ITEMS;
app->work_count++;
pthread_cond_signal(&app->work_cond);
}
pthread_mutex_unlock(&app->work_lock);
}
/* ---- resolving ------------------------------------------------------------ */
struct entry_ctx {
App *app;
int playlist_folders;
char *playlist_title; /* shared buffer, populated by downloader_resolve as entries stream in */
};
static void entry_trampoline(void *ud, const char *url, const char *title) {
struct entry_ctx *ctx = ud;
char subdir[SUBDIR_CAP] = "";
if (ctx->playlist_folders && ctx->playlist_title[0]) {
sanitize_dirname(ctx->playlist_title, subdir, sizeof(subdir));
}
int id = queue_add(&ctx->app->queue, url, title, subdir);
if (id >= 0) app_enqueue_work(ctx->app, id);
}
struct resolve_thread_arg {
App *app;
char url[URL_CAP];
};
static void *resolver_thread_fn(void *arg) {
struct resolve_thread_arg *rta = arg;
App *app = rta->app;
struct entry_ctx ectx;
ectx.app = app;
ectx.playlist_folders = app_get_playlist_folders(app);
char playlist_title[TITLE_CAP] = "";
ectx.playlist_title = playlist_title;
char err[DL_ERROR_CAP];
int rc = downloader_resolve(rta->url, entry_trampoline, &ectx, playlist_title,
sizeof(playlist_title), err, sizeof(err));
if (rc != 0) {
app_notify(app, 1, "Failed to resolve: %s", err);
}
free(rta);
return NULL;
}
void app_submit_url(App *app, const char *url) {
struct resolve_thread_arg *rta = malloc(sizeof(*rta));
rta->app = app;
safe_strcpy(rta->url, sizeof(rta->url), url);
app_notify(app, 0, "Resolving %s...", url);
pthread_t t;
pthread_create(&t, NULL, resolver_thread_fn, rta);
pthread_detach(t);
}
/* ---- downloading ----------------------------------------------------------- */
struct progress_ud {
Queue *q;
int id;
};
static void progress_trampoline(void *ud, float percent, const char *speed, const char *eta) {
struct progress_ud *p = ud;
queue_set_progress(p->q, p->id, percent, speed, eta);
}
static void process_item(App *app, int id) {
QueueItem item;
if (!queue_get(&app->queue, id, &item)) return;
if (item.status == STATUS_DONE) return;
queue_set_status(&app->queue, id, STATUS_DOWNLOADING, "");
char output_dir[OUTPUT_DIR_CAP];
char audio_format[AUDIO_FORMAT_CAP];
app_get_output_dir(app, output_dir, sizeof(output_dir));
app_get_audio_format(app, audio_format, sizeof(audio_format));
char target_dir[OUTPUT_DIR_CAP + SUBDIR_CAP + 2];
if (item.subdir[0]) {
snprintf(target_dir, sizeof(target_dir), "%s/%s", output_dir, item.subdir);
} else {
safe_strcpy(target_dir, sizeof(target_dir), output_dir);
}
struct progress_ud pud = {.q = &app->queue, .id = id};
char err[DL_ERROR_CAP];
int rc = -1;
/* YouTube occasionally returns a transient HTTP 403 on the signed media
* URL, especially under concurrent downloads; a fresh extraction
* (a brand new yt-dlp invocation) gets a new URL and usually succeeds.
* Retry a couple of times with a short backoff before giving up. */
for (int attempt = 0; attempt < DOWNLOAD_MAX_ATTEMPTS; attempt++) {
if (attempt > 0) sleep(DOWNLOAD_RETRY_DELAY_SECS);
rc = downloader_download(item.url, target_dir, audio_format, progress_trampoline, &pud,
err, sizeof(err));
if (rc == 0) break;
}
if (rc == 0) {
queue_set_status(&app->queue, id, STATUS_DONE, NULL);
} else {
queue_set_status(&app->queue, id, STATUS_ERROR, err);
}
}
static void *worker_thread_fn(void *arg) {
App *app = arg;
for (;;) {
pthread_mutex_lock(&app->work_lock);
while (app->work_count == 0 && !app->shutdown) {
pthread_cond_wait(&app->work_cond, &app->work_lock);
}
if (app->shutdown && app->work_count == 0) {
pthread_mutex_unlock(&app->work_lock);
break;
}
int id = app->work_ids[app->work_head];
app->work_head = (app->work_head + 1) % MAX_QUEUE_ITEMS;
app->work_count--;
pthread_mutex_unlock(&app->work_lock);
process_item(app, id);
}
return NULL;
}
void app_start_workers(App *app) {
for (int i = 0; i < CONCURRENT_DOWNLOADS; i++) {
pthread_create(&app->workers[i], NULL, worker_thread_fn, app);
}
}
void app_shutdown(App *app) {
pthread_mutex_lock(&app->work_lock);
app->shutdown = 1;
pthread_cond_broadcast(&app->work_cond);
pthread_mutex_unlock(&app->work_lock);
for (int i = 0; i < CONCURRENT_DOWNLOADS; i++) {
pthread_join(app->workers[i], NULL);
}
}
/* ---- queue actions ----------------------------------------------------------- */
void app_clear_finished(App *app) {
int n = queue_clear_finished(&app->queue);
if (n > 0) {
app_notify(app, 0, "Cleared %d finished item(s)", n);
} else {
app_notify(app, 0, "Nothing to clear");
}
}
void app_remove_item(App *app, int id) {
queue_remove(&app->queue, id);
}
void app_retry_item(App *app, int id) {
QueueItem item;
if (!queue_get(&app->queue, id, &item)) return;
if (item.status != STATUS_ERROR) return;
queue_reset_for_retry(&app->queue, id);
app_enqueue_work(app, id);
}