ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
yt_dlp.c
Go to the documentation of this file.
1
8#include <ascii-chat/common.h>
10#include <ascii-chat/log/io.h>
13#include <ascii-chat/util/url.h>
14#include <stdlib.h>
15#include <string.h>
16#include <time.h>
17#include <stdio.h>
18
19/* ============================================================================
20 * URL Extraction Cache
21 * ============================================================================ */
22
31typedef struct {
32 char url[2048]; // Original URL
33 char stream_url[8192]; // Extracted stream URL
34 char yt_dlp_options[512]; // yt-dlp options used
35 time_t extracted_time; // When URL was extracted
36 bool valid; // Whether cache entry is valid
38
39static yt_dlp_cache_entry_t g_yt_dlp_cache = {0};
40
47static bool yt_dlp_cache_is_valid(void) {
48 if (!g_yt_dlp_cache.valid) {
49 return false;
50 }
51
52 time_t now = time(NULL);
53 time_t age = now - g_yt_dlp_cache.extracted_time;
54
55 // Cache valid for 30 seconds
56 return (age >= 0 && age < 30);
57}
58
65static bool yt_dlp_cache_get(const char *url, const char *yt_dlp_options, char *output_url, size_t output_size) {
66 if (!yt_dlp_cache_is_valid()) {
67 return false;
68 }
69
70 // Check if cache matches requested URL and options
71 if (strcmp(g_yt_dlp_cache.url, url) != 0) {
72 return false;
73 }
74
75 // For cache key, compare options (NULL == "" for consistency)
76 const char *cached_opts = g_yt_dlp_cache.yt_dlp_options;
77 const char *req_opts = yt_dlp_options ? yt_dlp_options : "";
78 if (strcmp(cached_opts, req_opts) != 0) {
79 return false;
80 }
81
82 // Return cached URL (may be empty if cached failure)
83 size_t url_len = strlen(g_yt_dlp_cache.stream_url);
84 if (url_len >= output_size) {
85 return false;
86 }
87
88 SAFE_STRNCPY(output_url, g_yt_dlp_cache.stream_url, output_size - 1);
89 output_url[output_size - 1] = '\0';
90
91 if (url_len == 0) {
92 log_debug("Using cached yt-dlp failure for URL (failed %ld seconds ago)",
93 (long)(time(NULL) - g_yt_dlp_cache.extracted_time));
94 } else {
95 log_debug("Using cached yt-dlp stream URL (extracted %ld seconds ago)",
96 (long)(time(NULL) - g_yt_dlp_cache.extracted_time));
97 }
98 return true;
99}
100
107static void yt_dlp_cache_set(const char *url, const char *yt_dlp_options, const char *stream_url) {
108 if (strlen(url) >= sizeof(g_yt_dlp_cache.url)) {
109 return; // URL too long to cache
110 }
111
112 const char *opts = yt_dlp_options ? yt_dlp_options : "";
113 if (strlen(opts) >= sizeof(g_yt_dlp_cache.yt_dlp_options)) {
114 return; // Options too long to cache
115 }
116
117 if (stream_url && strlen(stream_url) >= sizeof(g_yt_dlp_cache.stream_url)) {
118 return; // Stream URL too long to cache
119 }
120
121 SAFE_STRNCPY(g_yt_dlp_cache.url, url, sizeof(g_yt_dlp_cache.url) - 1);
122 g_yt_dlp_cache.url[sizeof(g_yt_dlp_cache.url) - 1] = '\0';
123
124 SAFE_STRNCPY(g_yt_dlp_cache.yt_dlp_options, opts, sizeof(g_yt_dlp_cache.yt_dlp_options) - 1);
125 g_yt_dlp_cache.yt_dlp_options[sizeof(g_yt_dlp_cache.yt_dlp_options) - 1] = '\0';
126
127 if (stream_url) {
128 SAFE_STRNCPY(g_yt_dlp_cache.stream_url, stream_url, sizeof(g_yt_dlp_cache.stream_url) - 1);
129 } else {
130 // Mark as empty (failure state)
131 g_yt_dlp_cache.stream_url[0] = '\0';
132 }
133 g_yt_dlp_cache.stream_url[sizeof(g_yt_dlp_cache.stream_url) - 1] = '\0';
134
135 g_yt_dlp_cache.extracted_time = time(NULL);
136 g_yt_dlp_cache.valid = true;
137}
138
139/* ============================================================================
140 * Public API
141 * ============================================================================ */
142
144 const char *argv[] = {"yt-dlp", "--version", NULL};
145 int ret = 0;
146 LOG_IO("yt-dlp", { ret = platform_execute_subprocess("yt-dlp", argv, NULL, 0); });
147 return (ret == 0);
148}
149
150asciichat_error_t yt_dlp_extract_stream_url(const char *url, const char *yt_dlp_options, char *output_url,
151 size_t output_size) {
152 if (!url || !output_url || output_size < 256) {
153 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for yt-dlp URL extraction");
154 return ERROR_INVALID_PARAM;
155 }
156
157 // Check if yt-dlp is available
158 if (!yt_dlp_is_available()) {
159 SET_ERRNO(ERROR_YOUTUBE_EXTRACT_FAILED, "yt-dlp is not installed. Please install it with: "
160 "pip install yt-dlp (or: brew install yt-dlp on macOS)");
162 }
163
164 // Check if we have a cached extraction for this URL+options combination
165 char cached_url[8192] = {0};
166 if (yt_dlp_cache_get(url, yt_dlp_options, cached_url, sizeof(cached_url))) {
167 if (cached_url[0] != '\0') {
168 // Cached success - return the URL
169 SAFE_STRNCPY(output_url, cached_url, output_size - 1);
170 output_url[output_size - 1] = '\0';
171 return ASCIICHAT_OK;
172 } else {
173 // Cached failure - return error without logging again
175 }
176 }
177
178 // Build yt-dlp command
179 char command[4096];
180 const char *opts = yt_dlp_options ? yt_dlp_options : "";
181
182 int cmd_ret;
183 if (opts[0] != '\0') {
184 // User provided custom options
185 cmd_ret = safe_snprintf(command, sizeof(command),
186 "yt-dlp --quiet --no-warnings "
187 "--user-agent 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
188 "AppleWebKit/537.36' "
189 "%s "
190 "-f 'b' -O '%%(url)s' '%s' 2>&1",
191 opts, url);
192 } else {
193 // No custom options, use default
194 cmd_ret = safe_snprintf(command, sizeof(command),
195 "yt-dlp --quiet --no-warnings "
196 "--user-agent 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
197 "AppleWebKit/537.36' "
198 "-f 'b' -O '%%(url)s' '%s' 2>&1",
199 url);
200 }
201
202 if (cmd_ret < 0 || cmd_ret >= (int)sizeof(command)) {
203 SET_ERRNO(ERROR_INVALID_PARAM, "URL or yt-dlp options too long");
204 return ERROR_INVALID_PARAM;
205 }
206
207 log_debug("Executing yt-dlp: %s", command);
208
209 // Execute yt-dlp and capture output
210 FILE *pipe = NULL;
211 if (platform_popen("yt_dlp_extract", command, "r", &pipe) != ASCIICHAT_OK || !pipe) {
212 SET_ERRNO(ERROR_YOUTUBE_EXTRACT_FAILED, "Failed to execute yt-dlp subprocess");
214 }
215
216 // Read output looking for stream URL (should be last line starting with http)
217 char url_buffer[8192] = {0};
218 char full_output[16384] = {0};
219 size_t ytdlp_output_len = 0;
220 size_t url_size = 0;
221 int c;
222
223 while ((c = fgetc(pipe)) != EOF && ytdlp_output_len < sizeof(full_output) - 1) {
224 full_output[ytdlp_output_len++] = (char)c;
225
226 // Track current line - looking for http:// or https:// stream URL
227 if ((url_size == 0 && c == 'h') || (url_size > 0 && c != '\n' && url_size < sizeof(url_buffer) - 1)) {
228 url_buffer[url_size++] = (char)c;
229 } else if (c == '\n') {
230 if (url_size > 0) {
231 url_buffer[url_size] = '\0';
232 if (url_is_valid(url_buffer)) {
233 break; // Found valid URL
234 }
235 }
236 url_size = 0; // Reset for next line
237 }
238 }
239 full_output[ytdlp_output_len] = '\0';
240 url_buffer[url_size] = '\0';
241
242 int pclose_ret = (platform_pclose(&pipe) == ASCIICHAT_OK) ? 0 : -1;
243 if (pclose_ret != 0) {
244 yt_dlp_cache_set(url, yt_dlp_options, NULL); // Cache failure
245
246 log_debug("yt-dlp exited with code %d", pclose_ret);
247 if (ytdlp_output_len > 0) {
248 log_error("yt-dlp output:\n%s", full_output);
249 SET_ERRNO(ERROR_YOUTUBE_EXTRACT_FAILED, "yt-dlp failed to extract stream: %s", full_output);
250 } else {
251 SET_ERRNO(ERROR_YOUTUBE_EXTRACT_FAILED, "yt-dlp failed to extract stream");
252 }
254 }
255
256 if (url_size == 0 || (url_size == 2 && strncmp(url_buffer, "NA", 2) == 0)) {
257 yt_dlp_cache_set(url, yt_dlp_options, NULL);
258 log_error("yt-dlp returned empty output for URL: %s", url);
259 SET_ERRNO(ERROR_YOUTUBE_EXTRACT_FAILED, "yt-dlp returned no playable formats");
261 }
262
263 if (url_buffer[0] != 'h' || strncmp(url_buffer, "http", 4) != 0) {
264 yt_dlp_cache_set(url, yt_dlp_options, NULL);
265 log_error("Invalid URL from yt-dlp: %s (full output: %s)", url_buffer, full_output);
266 SET_ERRNO(ERROR_YOUTUBE_EXTRACT_FAILED, "yt-dlp returned invalid URL");
268 }
269
270 if (url_size >= output_size) {
271 SET_ERRNO(ERROR_INVALID_PARAM, "Stream URL too long for output buffer (%zu bytes, max %zu)", url_size, output_size);
272 return ERROR_INVALID_PARAM;
273 }
274
275 SAFE_STRNCPY(output_url, url_buffer, output_size - 1);
276 output_url[output_size - 1] = '\0';
277
278 // Cache successful result
279 yt_dlp_cache_set(url, yt_dlp_options, output_url);
280
281 log_debug("yt-dlp successfully extracted stream URL (%zu bytes)", url_size);
282 return ASCIICHAT_OK;
283}
⚠️‼️ Comprehensive thread-local error context system for ascii-chat
⚙️ Common definitions, error codes, macros, and types shared throughout the application
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_YOUTUBE_EXTRACT_FAILED
@ ERROR_INVALID_PARAM
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
asciichat_error_t yt_dlp_extract_stream_url(const char *url, const char *yt_dlp_options, char *output_url, size_t output_size)
Extract stream URL using yt-dlp with optional parameters.
Definition yt_dlp.c:150
bool yt_dlp_is_available(void)
Check if yt-dlp is installed and accessible.
Definition yt_dlp.c:143
asciichat_error_t platform_popen(const char *name, const char *command, const char *mode, FILE **out_stream)
Execute a command and return a file stream for reading/writing.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
int platform_execute_subprocess(const char *executable, const char **argv, char *output_buffer, size_t output_size)
Execute a subprocess and optionally capture its output.
asciichat_error_t platform_pclose(FILE **stream_ptr)
Close a process stream opened with platform_popen()
bool url_is_valid(const char *url)
Fast URL validation using production-grade regex (HTTP/HTTPS/WebSocket/TCP)
Definition url.c:81
Capture stdout/stderr and redirect to logging system.
#define LOG_IO(prefix, block)
Capture output from a code block and log it.
Definition io.h:81
📝 Logging API with multiple log levels and terminal output control
Cross-platform process types and execution utilities.
Cache entry for extracted stream URLs.
Definition yt_dlp.c:31
char yt_dlp_options[512]
Definition yt_dlp.c:34
char url[2048]
Definition yt_dlp.c:32
char stream_url[8192]
Definition yt_dlp.c:33
bool valid
Definition yt_dlp.c:36
time_t extracted_time
Definition yt_dlp.c:35
⏱️ High-precision timing utilities using sokol_time.h and uthash
🌐 Production-Grade URL Validation and Parsing
yt-dlp stream URL extraction