ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
yt_dlp.c
Go to the documentation of this file.
1
7#include <ascii-chat/media/yt_dlp.h>
8#include <ascii-chat/common.h>
9#include <ascii-chat/log/logging.h>
10#include <ascii-chat/asciichat_errno.h>
11#include <ascii-chat/platform/process.h>
12#include <ascii-chat/util/url.h>
13#include <stdlib.h>
14#include <string.h>
15#include <time.h>
16#include <stdio.h>
17
18/* ============================================================================
19 * URL Extraction Cache
20 * ============================================================================ */
21
30typedef struct {
31 char url[2048]; // Original URL
32 char stream_url[8192]; // Extracted stream URL
33 char yt_dlp_options[512]; // yt-dlp options used
34 time_t extracted_time; // When URL was extracted
35 bool valid; // Whether cache entry is valid
37
38static yt_dlp_cache_entry_t g_yt_dlp_cache = {0};
39
46static bool yt_dlp_cache_is_valid(void) {
47 if (!g_yt_dlp_cache.valid) {
48 return false;
49 }
50
51 time_t now = time(NULL);
52 time_t age = now - g_yt_dlp_cache.extracted_time;
53
54 // Cache valid for 30 seconds
55 return (age >= 0 && age < 30);
56}
57
64static bool yt_dlp_cache_get(const char *url, const char *yt_dlp_options, char *output_url, size_t output_size) {
65 if (!yt_dlp_cache_is_valid()) {
66 return false;
67 }
68
69 // Check if cache matches requested URL and options
70 if (strcmp(g_yt_dlp_cache.url, url) != 0) {
71 return false;
72 }
73
74 // For cache key, compare options (NULL == "" for consistency)
75 const char *cached_opts = g_yt_dlp_cache.yt_dlp_options;
76 const char *req_opts = yt_dlp_options ? yt_dlp_options : "";
77 if (strcmp(cached_opts, req_opts) != 0) {
78 return false;
79 }
80
81 // Return cached URL (may be empty if cached failure)
82 size_t url_len = strlen(g_yt_dlp_cache.stream_url);
83 if (url_len >= output_size) {
84 return false;
85 }
86
87 SAFE_STRNCPY(output_url, g_yt_dlp_cache.stream_url, output_size - 1);
88 output_url[output_size - 1] = '\0';
89
90 if (url_len == 0) {
91 log_debug("Using cached yt-dlp failure for URL (failed %ld seconds ago)",
92 (long)(time(NULL) - g_yt_dlp_cache.extracted_time));
93 } else {
94 log_debug("Using cached yt-dlp stream URL (extracted %ld seconds ago)",
95 (long)(time(NULL) - g_yt_dlp_cache.extracted_time));
96 }
97 return true;
98}
99
106static void yt_dlp_cache_set(const char *url, const char *yt_dlp_options, const char *stream_url) {
107 if (strlen(url) >= sizeof(g_yt_dlp_cache.url)) {
108 return; // URL too long to cache
109 }
110
111 const char *opts = yt_dlp_options ? yt_dlp_options : "";
112 if (strlen(opts) >= sizeof(g_yt_dlp_cache.yt_dlp_options)) {
113 return; // Options too long to cache
114 }
115
116 if (stream_url && strlen(stream_url) >= sizeof(g_yt_dlp_cache.stream_url)) {
117 return; // Stream URL too long to cache
118 }
119
120 SAFE_STRNCPY(g_yt_dlp_cache.url, url, sizeof(g_yt_dlp_cache.url) - 1);
121 g_yt_dlp_cache.url[sizeof(g_yt_dlp_cache.url) - 1] = '\0';
122
123 SAFE_STRNCPY(g_yt_dlp_cache.yt_dlp_options, opts, sizeof(g_yt_dlp_cache.yt_dlp_options) - 1);
124 g_yt_dlp_cache.yt_dlp_options[sizeof(g_yt_dlp_cache.yt_dlp_options) - 1] = '\0';
125
126 if (stream_url) {
127 SAFE_STRNCPY(g_yt_dlp_cache.stream_url, stream_url, sizeof(g_yt_dlp_cache.stream_url) - 1);
128 } else {
129 // Mark as empty (failure state)
130 g_yt_dlp_cache.stream_url[0] = '\0';
131 }
132 g_yt_dlp_cache.stream_url[sizeof(g_yt_dlp_cache.stream_url) - 1] = '\0';
133
134 g_yt_dlp_cache.extracted_time = time(NULL);
135 g_yt_dlp_cache.valid = true;
136}
137
138/* ============================================================================
139 * Public API
140 * ============================================================================ */
141
143 int ret = system("yt-dlp --version >/dev/null 2>&1");
144 return (ret == 0);
145}
146
147asciichat_error_t yt_dlp_extract_stream_url(const char *url, const char *yt_dlp_options, char *output_url,
148 size_t output_size) {
149 if (!url || !output_url || output_size < 256) {
150 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for yt-dlp URL extraction");
151 return ERROR_INVALID_PARAM;
152 }
153
154 // Check if yt-dlp is available
155 if (!yt_dlp_is_available()) {
156 SET_ERRNO(ERROR_YOUTUBE_EXTRACT_FAILED, "yt-dlp is not installed. Please install it with: "
157 "pip install yt-dlp (or: brew install yt-dlp on macOS)");
158 return ERROR_YOUTUBE_EXTRACT_FAILED;
159 }
160
161 // Check if we have a cached extraction for this URL+options combination
162 char cached_url[8192] = {0};
163 if (yt_dlp_cache_get(url, yt_dlp_options, cached_url, sizeof(cached_url))) {
164 if (cached_url[0] != '\0') {
165 // Cached success - return the URL
166 SAFE_STRNCPY(output_url, cached_url, output_size - 1);
167 output_url[output_size - 1] = '\0';
168 return ASCIICHAT_OK;
169 } else {
170 // Cached failure - return error without logging again
171 return ERROR_YOUTUBE_EXTRACT_FAILED;
172 }
173 }
174
175 // Build yt-dlp command
176 char command[4096];
177 const char *opts = yt_dlp_options ? yt_dlp_options : "";
178
179 int cmd_ret;
180 if (opts[0] != '\0') {
181 // User provided custom options
182 cmd_ret = safe_snprintf(command, sizeof(command),
183 "yt-dlp --quiet --no-warnings "
184 "--user-agent 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
185 "AppleWebKit/537.36' "
186 "%s "
187 "-f 'b' -O '%%(url)s' '%s' 2>&1",
188 opts, url);
189 } else {
190 // No custom options, use default
191 cmd_ret = safe_snprintf(command, sizeof(command),
192 "yt-dlp --quiet --no-warnings "
193 "--user-agent 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
194 "AppleWebKit/537.36' "
195 "-f 'b' -O '%%(url)s' '%s' 2>&1",
196 url);
197 }
198
199 if (cmd_ret < 0 || cmd_ret >= (int)sizeof(command)) {
200 SET_ERRNO(ERROR_INVALID_PARAM, "URL or yt-dlp options too long");
201 return ERROR_INVALID_PARAM;
202 }
203
204 log_debug("Executing yt-dlp: %s", command);
205
206 // Execute yt-dlp and capture output
207 FILE *pipe = NULL;
208 if (platform_popen(command, "r", &pipe) != ASCIICHAT_OK || !pipe) {
209 SET_ERRNO(ERROR_YOUTUBE_EXTRACT_FAILED, "Failed to execute yt-dlp subprocess");
210 return ERROR_YOUTUBE_EXTRACT_FAILED;
211 }
212
213 // Read output looking for stream URL (should be last line starting with http)
214 char url_buffer[8192] = {0};
215 char full_output[16384] = {0};
216 size_t ytdlp_output_len = 0;
217 size_t url_size = 0;
218 int c;
219
220 while ((c = fgetc(pipe)) != EOF && ytdlp_output_len < sizeof(full_output) - 1) {
221 full_output[ytdlp_output_len++] = (char)c;
222
223 // Track current line - looking for http:// or https:// stream URL
224 if ((url_size == 0 && c == 'h') || (url_size > 0 && c != '\n' && url_size < sizeof(url_buffer) - 1)) {
225 url_buffer[url_size++] = (char)c;
226 } else if (c == '\n') {
227 if (url_size > 0) {
228 url_buffer[url_size] = '\0';
229 if (url_is_valid(url_buffer)) {
230 break; // Found valid URL
231 }
232 }
233 url_size = 0; // Reset for next line
234 }
235 }
236 full_output[ytdlp_output_len] = '\0';
237 url_buffer[url_size] = '\0';
238
239 int pclose_ret = (platform_pclose(&pipe) == ASCIICHAT_OK) ? 0 : -1;
240 if (pclose_ret != 0) {
241 yt_dlp_cache_set(url, yt_dlp_options, NULL); // Cache failure
242
243 log_debug("yt-dlp exited with code %d", pclose_ret);
244 if (ytdlp_output_len > 0) {
245 log_error("yt-dlp output:\n%s", full_output);
246 SET_ERRNO(ERROR_YOUTUBE_EXTRACT_FAILED, "yt-dlp failed to extract stream: %s", full_output);
247 } else {
248 SET_ERRNO(ERROR_YOUTUBE_EXTRACT_FAILED, "yt-dlp failed to extract stream");
249 }
250 return ERROR_YOUTUBE_EXTRACT_FAILED;
251 }
252
253 if (url_size == 0 || (url_size == 2 && strncmp(url_buffer, "NA", 2) == 0)) {
254 yt_dlp_cache_set(url, yt_dlp_options, NULL);
255 log_error("yt-dlp returned empty output for URL: %s", url);
256 SET_ERRNO(ERROR_YOUTUBE_EXTRACT_FAILED, "yt-dlp returned no playable formats");
257 return ERROR_YOUTUBE_EXTRACT_FAILED;
258 }
259
260 if (url_buffer[0] != 'h' || strncmp(url_buffer, "http", 4) != 0) {
261 yt_dlp_cache_set(url, yt_dlp_options, NULL);
262 log_error("Invalid URL from yt-dlp: %s (full output: %s)", url_buffer, full_output);
263 SET_ERRNO(ERROR_YOUTUBE_EXTRACT_FAILED, "yt-dlp returned invalid URL");
264 return ERROR_YOUTUBE_EXTRACT_FAILED;
265 }
266
267 if (url_size >= output_size) {
268 SET_ERRNO(ERROR_INVALID_PARAM, "Stream URL too long for output buffer (%zu bytes, max %zu)", url_size, output_size);
269 return ERROR_INVALID_PARAM;
270 }
271
272 SAFE_STRNCPY(output_url, url_buffer, output_size - 1);
273 output_url[output_size - 1] = '\0';
274
275 // Cache successful result
276 yt_dlp_cache_set(url, yt_dlp_options, output_url);
277
278 log_debug("yt-dlp successfully extracted stream URL (%zu bytes)", url_size);
279 return ASCIICHAT_OK;
280}
Cache entry for extracted stream URLs.
Definition yt_dlp.c:30
char yt_dlp_options[512]
Definition yt_dlp.c:33
char url[2048]
Definition yt_dlp.c:31
char stream_url[8192]
Definition yt_dlp.c:32
bool valid
Definition yt_dlp.c:35
time_t extracted_time
Definition yt_dlp.c:34
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
bool url_is_valid(const char *url)
Definition url.c:81
asciichat_error_t yt_dlp_extract_stream_url(const char *url, const char *yt_dlp_options, char *output_url, size_t output_size)
Definition yt_dlp.c:147
bool yt_dlp_is_available(void)
Definition yt_dlp.c:142