diff medi/main.c @ 217:7ef4c9d2a72d hg-web

[DO NOT PUSH] Random values.
author MrJuneJune <me@mrjunejune.com>
date Sun, 25 Jan 2026 10:44:04 -0800
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/medi/main.c	Sun Jan 25 10:44:04 2026 -0800
@@ -0,0 +1,531 @@
+#include "seobeo/seobeo.h"
+#include "media_processor/media_processor.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <dirent.h>
+#include <pthread.h>
+#include <unistd.h>
+
+#define UPLOAD_DIR "./uploads"
+#define PROCESSED_DIR "./media"
+#define MAX_UPLOAD_SIZE (100 * 1024 * 1024)  // 100MB
+
+// Background processing queue
+typedef struct ProcessJob {
+    char* input_path;
+    char* id;
+    MediaType type;
+    struct ProcessJob* next;
+} ProcessJob;
+
+static ProcessJob* job_queue_head = NULL;
+static ProcessJob* job_queue_tail = NULL;
+static pthread_mutex_t job_queue_mutex = PTHREAD_MUTEX_INITIALIZER;
+static pthread_cond_t job_queue_cond = PTHREAD_COND_INITIALIZER;
+
+// ============================================================================
+// Helper Functions
+// ============================================================================
+
+static void ensure_directories(void) {
+    mkdir(UPLOAD_DIR, 0755);
+    mkdir(PROCESSED_DIR, 0755);
+}
+
+static char* extract_filename_from_content_disposition(const char* header, Dowa_Arena* arena) {
+    const char* filename_start = strstr(header, "filename=\"");
+    if (!filename_start) return NULL;
+
+    filename_start += 10;  // Skip 'filename="'
+    const char* filename_end = strchr(filename_start, '"');
+    if (!filename_end) return NULL;
+
+    size_t len = filename_end - filename_start;
+    char* filename = Dowa_Arena_Allocate(arena, len + 1);
+    memcpy(filename, filename_start, len);
+    filename[len] = '\0';
+    return filename;
+}
+
+static char* find_multipart_boundary(const char* content_type, Dowa_Arena* arena) {
+    const char* boundary_start = strstr(content_type, "boundary=");
+    if (!boundary_start) return NULL;
+
+    boundary_start += 9;  // Skip 'boundary='
+
+    // Handle quoted boundary
+    if (*boundary_start == '"') {
+        boundary_start++;
+        const char* boundary_end = strchr(boundary_start, '"');
+        if (!boundary_end) return NULL;
+        size_t len = boundary_end - boundary_start;
+        char* boundary = Dowa_Arena_Allocate(arena, len + 1);
+        memcpy(boundary, boundary_start, len);
+        boundary[len] = '\0';
+        return boundary;
+    }
+
+    // Unquoted boundary
+    size_t len = strlen(boundary_start);
+    char* boundary = Dowa_Arena_Allocate(arena, len + 1);
+    strcpy(boundary, boundary_start);
+    // Trim whitespace/newline
+    while (len > 0 && (boundary[len-1] == '\r' || boundary[len-1] == '\n' || boundary[len-1] == ' ')) {
+        boundary[--len] = '\0';
+    }
+    return boundary;
+}
+
+// ============================================================================
+// Background Processing Worker
+// ============================================================================
+
+static void enqueue_job(const char* input_path, const char* id, MediaType type) {
+    ProcessJob* job = malloc(sizeof(ProcessJob));
+    job->input_path = strdup(input_path);
+    job->id = strdup(id);
+    job->type = type;
+    job->next = NULL;
+
+    pthread_mutex_lock(&job_queue_mutex);
+    if (job_queue_tail) {
+        job_queue_tail->next = job;
+        job_queue_tail = job;
+    } else {
+        job_queue_head = job_queue_tail = job;
+    }
+    pthread_cond_signal(&job_queue_cond);
+    pthread_mutex_unlock(&job_queue_mutex);
+}
+
+static void* processing_worker(void* arg) {
+    (void)arg;
+
+    while (1) {
+        pthread_mutex_lock(&job_queue_mutex);
+        while (!job_queue_head) {
+            pthread_cond_wait(&job_queue_cond, &job_queue_mutex);
+        }
+        ProcessJob* job = job_queue_head;
+        job_queue_head = job->next;
+        if (!job_queue_head) job_queue_tail = NULL;
+        pthread_mutex_unlock(&job_queue_mutex);
+
+        // Process the job
+        Seobeo_Log(SEOBEO_INFO, "Processing media: %s (id=%s)", job->input_path, job->id);
+
+        MediaProcessorOpts opts = media_processor_opts_default(PROCESSED_DIR);
+        MediaResult* result = media_process_file(job->input_path, job->id, &opts);
+
+        if (result->status == MEDIA_STATUS_COMPLETED) {
+            Seobeo_Log(SEOBEO_INFO, "Processing completed for %s", job->id);
+        } else {
+            Seobeo_Log(SEOBEO_ERROR, "Processing failed for %s: %s", job->id, result->error_message);
+        }
+
+        media_result_free(result);
+
+        // Clean up temp upload file
+        unlink(job->input_path);
+
+        free(job->input_path);
+        free(job->id);
+        free(job);
+    }
+
+    return NULL;
+}
+
+void Seobeo_Render_Html(
+    char *final_body,
+    char *template,
+    Dowa_Arena *arena
+)
+{
+  size_t current_offset = 0;
+  char *cursor = template;
+
+  int32 token_len = 2;
+
+  while (1)
+  {
+    char *start_tag = strstr(cursor, "{{");
+    if (!start_tag) break;
+
+    char *end_tag = strstr(start_tag, "}}");
+    if (!end_tag) break;
+
+    size_t leading_len = start_tag - cursor;
+    memcpy(final_body + current_offset, cursor, leading_len);
+    current_offset += leading_len;
+
+    size_t name_len = end_tag - (start_tag + token_len);
+    char *include_name = Dowa_Arena_Allocate(arena, name_len + 1);
+    memcpy(include_name, start_tag + token_len, name_len);
+    include_name[name_len] = '\0';
+
+    size_t sub_file_size = 0;
+    char *sub_content = Seobeo_Web_LoadFile(include_name, &sub_file_size);
+    Seobeo_Log(SEOBEO_DEBUG, "[Curr] Sub content: %s\n", sub_content);
+    if (sub_content)
+    {
+      memcpy(final_body + current_offset, sub_content, sub_file_size);
+      current_offset += sub_file_size;
+      free(sub_content);
+    }
+
+    cursor = end_tag + 2;
+  }
+  strcpy(final_body + current_offset, cursor);
+}
+
+
+void Seobeo_Render_Html_FilePath(
+    char *final_body,
+    char *path,
+    Dowa_Arena *arena
+) {
+  Seobeo_Log(SEOBEO_DEBUG, "[Curr] %s\n", path);
+  size_t html_size = 0;
+  char *template = Seobeo_Web_LoadFile(path, &html_size);
+  if (!template) return;
+  Seobeo_Render_Html(final_body, template, arena);
+}
+
+// ============================================================================
+// API Route Handlers
+// ============================================================================
+
+// GET / - Serve the main HTML page
+Seobeo_Request_Entry* handle_index(Seobeo_Request_Entry* req, Dowa_Arena* arena) {
+  Seobeo_Request_Entry *resp = NULL; 
+  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
+  Seobeo_Render_Html_FilePath(final_body, "/index.html", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
+  return resp;
+}
+
+// POST /api/upload - Handle file upload
+Seobeo_Request_Entry* handle_upload(Seobeo_Request_Entry* req, Dowa_Arena* arena) {
+    const char* content_type = Dowa_HashMap_Get(req, "content-type");
+    const char* body = Dowa_HashMap_Get(req, "Body");
+    const char* content_length_str = Dowa_HashMap_Get(req, "content-length");
+
+    Seobeo_Request_Entry* resp = NULL;
+
+    if (!content_type || !body) {
+        Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
+        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+        Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Missing content\"}", arena);
+        return resp;
+    }
+
+    size_t content_length = content_length_str ? atol(content_length_str) : strlen(body);
+
+    if (content_length > MAX_UPLOAD_SIZE) {
+        Dowa_HashMap_Push_Arena(resp, "status", "413", arena);
+        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+        Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"File too large\"}", arena);
+        return resp;
+    }
+
+    // Parse multipart form data
+    char* boundary = find_multipart_boundary(content_type, arena);
+    if (!boundary) {
+        Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
+        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+        Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Invalid multipart data\"}", arena);
+        return resp;
+    }
+
+    // Find file content in multipart data
+    char boundary_marker[256];
+    snprintf(boundary_marker, sizeof(boundary_marker), "--%s", boundary);
+
+    const char* part_start = strstr(body, boundary_marker);
+    if (!part_start) {
+        Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
+        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+        Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"No file found\"}", arena);
+        return resp;
+    }
+
+    // Skip to headers
+    part_start = strstr(part_start, "\r\n");
+    if (!part_start) {
+        Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
+        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+        Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Invalid format\"}", arena);
+        return resp;
+    }
+    part_start += 2;
+
+    // Find filename from Content-Disposition
+    char* filename = NULL;
+    const char* header_end = strstr(part_start, "\r\n\r\n");
+    if (header_end) {
+        char headers[1024] = {0};
+        size_t headers_len = header_end - part_start;
+        if (headers_len < sizeof(headers)) {
+            memcpy(headers, part_start, headers_len);
+            filename = extract_filename_from_content_disposition(headers, arena);
+        }
+    }
+
+    if (!filename) {
+        filename = "upload";
+    }
+
+    // Find file content
+    const char* file_start = header_end + 4;  // Skip \r\n\r\n
+
+    // Find end boundary
+    char end_boundary[256];
+    snprintf(end_boundary, sizeof(end_boundary), "\r\n--%s", boundary);
+    const char* file_end = strstr(file_start, end_boundary);
+
+    if (!file_end) {
+        Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
+        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+        Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Incomplete upload\"}", arena);
+        return resp;
+    }
+
+    size_t file_size = file_end - file_start;
+
+    // Generate ID and determine type
+    char* id = media_generate_id();
+    MediaType type = media_detect_type(filename);
+
+    if (type == MEDIA_TYPE_UNKNOWN) {
+        free(id);
+        Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
+        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+        Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Unsupported file type\"}", arena);
+        return resp;
+    }
+
+    // Save to temp file
+    const char* ext = media_get_extension(filename);
+    char temp_path[512];
+    snprintf(temp_path, sizeof(temp_path), "%s/%s%s", UPLOAD_DIR, id, ext ? ext : "");
+
+    FILE* f = fopen(temp_path, "wb");
+    if (!f) {
+        free(id);
+        Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
+        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+        Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Failed to save file\"}", arena);
+        return resp;
+    }
+
+    fwrite(file_start, 1, file_size, f);
+    fclose(f);
+
+    // Enqueue for processing
+    enqueue_job(temp_path, id, type);
+
+    // Return success with ID
+    char response_body[256];
+    snprintf(response_body, sizeof(response_body),
+        "{\"id\":\"%s\",\"type\":\"%s\",\"status\":\"processing\"}",
+        id, type == MEDIA_TYPE_IMAGE ? "image" : "video");
+
+    char* body_copy = Dowa_Arena_Allocate(arena, strlen(response_body) + 1);
+    strcpy(body_copy, response_body);
+
+    Dowa_HashMap_Push_Arena(resp, "status", "202", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "Body", body_copy, arena);
+
+    free(id);
+    return resp;
+}
+
+// GET /api/media/:id - Get media info/status
+Seobeo_Request_Entry* handle_media_info(Seobeo_Request_Entry* req, Dowa_Arena* arena) {
+    const char* path = Dowa_HashMap_Get(req, "path");
+
+    Seobeo_Request_Entry* resp = NULL;
+
+    // Extract ID from path (e.g., /api/media/123456)
+    const char* id = path + strlen("/api/media/");
+    if (!id || strlen(id) == 0) {
+        Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
+        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+        Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Missing media ID\"}", arena);
+        return resp;
+    }
+
+    // Check if processed directory exists
+    char media_dir[512];
+    snprintf(media_dir, sizeof(media_dir), "%s/%s", PROCESSED_DIR, id);
+
+    struct stat st;
+    if (stat(media_dir, &st) != 0) {
+        // Check if still processing
+        char upload_pattern[512];
+        snprintf(upload_pattern, sizeof(upload_pattern), "%s/%s", UPLOAD_DIR, id);
+
+        DIR* dir = opendir(UPLOAD_DIR);
+        boolean found = FALSE;
+        if (dir) {
+            struct dirent* entry;
+            while ((entry = readdir(dir)) != NULL) {
+                if (strncmp(entry->d_name, id, strlen(id)) == 0) {
+                    found = TRUE;
+                    break;
+                }
+            }
+            closedir(dir);
+        }
+
+        if (found) {
+            Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
+            Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+            Dowa_HashMap_Push_Arena(resp, "Body", "{\"status\":\"processing\"}", arena);
+        } else {
+            Dowa_HashMap_Push_Arena(resp, "status", "404", arena);
+            Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+            Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Media not found\"}", arena);
+        }
+        return resp;
+    }
+
+    // Check what files exist
+    char webp_path[512], hls_path[512], thumb_path[512];
+    snprintf(webp_path, sizeof(webp_path), "%s/image.webp", media_dir);
+    snprintf(hls_path, sizeof(hls_path), "%s/video.m3u8", media_dir);
+    snprintf(thumb_path, sizeof(thumb_path), "%s/video_thumb.jpg", media_dir);
+
+    boolean is_image = (stat(webp_path, &st) == 0);
+    boolean is_video = (stat(hls_path, &st) == 0);
+
+    char response[1024];
+    if (is_image) {
+        snprintf(response, sizeof(response),
+            "{\"id\":\"%s\",\"type\":\"image\",\"status\":\"completed\","
+            "\"webp\":\"/media/%s/image.webp\","
+            "\"original\":\"/media/%s/original\"}",
+            id, id, id);
+    } else if (is_video) {
+        snprintf(response, sizeof(response),
+            "{\"id\":\"%s\",\"type\":\"video\",\"status\":\"completed\","
+            "\"hls\":\"/media/%s/video.m3u8\","
+            "\"thumbnail\":\"/media/%s/video_thumb.jpg\","
+            "\"original\":\"/media/%s/original\"}",
+            id, id, id, id);
+    } else {
+        snprintf(response, sizeof(response), "{\"id\":\"%s\",\"status\":\"processing\"}", id);
+    }
+
+    char* body = Dowa_Arena_Allocate(arena, strlen(response) + 1);
+    strcpy(body, response);
+
+    Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "Body", body, arena);
+
+    return resp;
+}
+
+// GET /api/media - List all media
+Seobeo_Request_Entry* handle_media_list(Seobeo_Request_Entry* req, Dowa_Arena* arena) {
+    (void)req;
+
+    Seobeo_Request_Entry* resp = NULL;
+
+    DIR* dir = opendir(PROCESSED_DIR);
+    if (!dir) {
+        Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
+        Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+        Dowa_HashMap_Push_Arena(resp, "Body", "{\"items\":[]}", arena);
+        return resp;
+    }
+
+    // Build JSON array
+    char json[16384] = "{\"items\":[";
+    size_t json_len = strlen(json);
+    boolean first = TRUE;
+
+    struct dirent* entry;
+    while ((entry = readdir(dir)) != NULL) {
+        if (entry->d_name[0] == '.') continue;
+
+        char media_dir[512];
+        snprintf(media_dir, sizeof(media_dir), "%s/%s", PROCESSED_DIR, entry->d_name);
+
+        struct stat st;
+        if (stat(media_dir, &st) != 0 || !S_ISDIR(st.st_mode)) continue;
+
+        // Check type
+        char webp_path[512], hls_path[512];
+        snprintf(webp_path, sizeof(webp_path), "%s/image.webp", media_dir);
+        snprintf(hls_path, sizeof(hls_path), "%s/video.m3u8", media_dir);
+
+        const char* type = "unknown";
+        if (stat(webp_path, &st) == 0) type = "image";
+        else if (stat(hls_path, &st) == 0) type = "video";
+
+        char item[256];
+        snprintf(item, sizeof(item), "%s{\"id\":\"%s\",\"type\":\"%s\"}",
+            first ? "" : ",", entry->d_name, type);
+
+        size_t item_len = strlen(item);
+        if (json_len + item_len + 10 < sizeof(json)) {
+            strcpy(json + json_len, item);
+            json_len += item_len;
+            first = FALSE;
+        }
+    }
+
+    closedir(dir);
+
+    strcpy(json + json_len, "]}");
+
+    char* body = Dowa_Arena_Allocate(arena, strlen(json) + 1);
+    strcpy(body, json);
+
+    Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
+    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
+    Dowa_HashMap_Push_Arena(resp, "Body", body, arena);
+
+    return resp;
+}
+
+// ============================================================================
+// Main
+// ============================================================================
+
+int main(int argc, char** argv) {
+    const char* port = "8080";
+
+    printf("===========================================\n");
+    printf("  Medi - Media Upload & Processing Server\n");
+    printf("===========================================\n");
+    printf("Starting on port %s...\n\n", port);
+
+    // Create directories
+    ensure_directories();
+
+    // Start background processing worker
+    pthread_t worker_thread;
+    pthread_create(&worker_thread, NULL, processing_worker, NULL);
+
+    // Initialize router
+    Seobeo_Router_Init();
+
+    // Register routes
+    Seobeo_Router_Register("GET", "/", handle_index);
+    Seobeo_Router_Register("POST", "/api/upload", handle_upload);
+    Seobeo_Router_Register("GET", "/api/media", handle_media_list);
+    Seobeo_Router_Register("GET", "/api/media/*", handle_media_info);
+
+    // Start server (serves static files from ./media for /media/* paths)
+    Seobeo_Web_Server_Start("medi/src", port, SEOBEO_MODE_EDGE, 4);
+
+    return 0;
+}