dvds go brrrrr
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: add proper bounds checking and unify types

+80 -64
+18
packages/ifo-parse/platforms.json
··· 1 + { 2 + "darwin-arm64": { 3 + "dir": "darwin-arm64", 4 + "ext": ".dylib" 5 + }, 6 + "darwin-x64": { 7 + "dir": "darwin-x64", 8 + "ext": ".dylib" 9 + }, 10 + "linux-arm64": { 11 + "dir": "linux-arm64", 12 + "ext": ".so" 13 + }, 14 + "linux-x64": { 15 + "dir": "linux-x64", 16 + "ext": ".so" 17 + } 18 + }
+3
packages/ifo-parse/shim/build.sh
··· 1 1 #!/bin/bash 2 2 set -e 3 3 4 + # Platform detection logic must match platforms.json 5 + # When adding a new platform, update both this file and platforms.json 6 + 4 7 # Detect platform 5 8 PLATFORM=$(uname -s) 6 9 ARCH=$(uname -m)
+12 -11
packages/ifo-parse/shim/ifo_shim.c
··· 5 5 #include <dvdread/dvd_reader.h> 6 6 #include "cJSON.h" 7 7 8 + // Convert playback_time_t to seconds 9 + static double playback_time_to_seconds(playback_time_t t) { 10 + return (double)t.hour * 3600.0 + 11 + (double)t.minute * 60.0 + 12 + (double)t.second; 13 + } 14 + 8 15 // Safely extract language code (2 lowercase letters or "un") 9 16 static void safe_lang(char *out, uint16_t lang_code) { 10 17 out[0] = 'u'; ··· 172 179 if (pgcit && pgc_ix > 0 && pgc_ix <= pgcit->nr_of_pgci_srp) { 173 180 pgc_t *pgc = pgcit->pgci_srp[pgc_ix - 1].pgc; 174 181 175 - double duration = (double)pgc->playback_time.hour * 3600.0 + 176 - (double)pgc->playback_time.minute * 60.0 + 177 - (double)pgc->playback_time.second; 182 + double duration = playback_time_to_seconds(pgc->playback_time); 178 183 cJSON_AddNumberToObject(tobj, "length", duration); 179 184 180 185 // PGC navigation ··· 212 217 int start_cell = pgc->program_map[c]; 213 218 for (int cell = 0; cell < start_cell - 1 && cell < pgc->nr_of_cells; cell++) { 214 219 cell_playback_t *cp = &pgc->cell_playback[cell]; 215 - start_time += (double)cp->playback_time.hour * 3600.0 + 216 - (double)cp->playback_time.minute * 60.0 + 217 - (double)cp->playback_time.second; 220 + start_time += playback_time_to_seconds(cp->playback_time); 218 221 } 219 222 cJSON_AddNumberToObject(chap, "startTime", start_time); 220 223 cJSON_AddItemToArray(chapters, chap); ··· 229 232 cJSON *cellobj = cJSON_CreateObject(); 230 233 cJSON_AddNumberToObject(cellobj, "ix", c + 1); 231 234 232 - double cell_duration = (double)cp->playback_time.hour * 3600.0 + 233 - (double)cp->playback_time.minute * 60.0 + 234 - (double)cp->playback_time.second; 235 + double cell_duration = playback_time_to_seconds(cp->playback_time); 235 236 cJSON_AddNumberToObject(cellobj, "duration", cell_duration); 236 237 237 238 cJSON_AddNumberToObject(cellobj, "startSector", cp->first_sector); ··· 299 300 cJSON *audio_array = cJSON_AddArrayToObject(tobj, "audio"); 300 301 if (vts->vtsi_mat) { 301 302 int n_audio = vts->vtsi_mat->nr_of_vts_audio_streams; 303 + if (n_audio > 8) n_audio = 8; // DVD spec limit 302 304 for (int a = 0; a < n_audio; a++) { 303 305 audio_attr_t *aattr = &vts->vtsi_mat->vts_audio_attr[a]; 304 306 ··· 319 321 cJSON_AddStringToObject(aobj, "type", "audio"); 320 322 cJSON_AddNumberToObject(aobj, "ix", a); 321 323 cJSON_AddStringToObject(aobj, "langCode", lang); 322 - cJSON_AddStringToObject(aobj, "language", lang); 323 324 cJSON_AddStringToObject(aobj, "format", fmt); 324 325 cJSON_AddNumberToObject(aobj, "channels", aattr->channels + 1); 325 326 cJSON_AddNumberToObject(aobj, "sampleRate", sample_rate); ··· 337 338 cJSON *sub_array = cJSON_AddArrayToObject(tobj, "subtitles"); 338 339 if (vts->vtsi_mat) { 339 340 int n_sub = vts->vtsi_mat->nr_of_vts_subp_streams; 341 + if (n_sub > 32) n_sub = 32; // DVD spec limit 340 342 for (int s = 0; s < n_sub; s++) { 341 343 subp_attr_t *sattr = &vts->vtsi_mat->vts_subp_attr[s]; 342 344 ··· 347 349 cJSON_AddStringToObject(sobj, "type", "subtitle"); 348 350 cJSON_AddNumberToObject(sobj, "ix", s); 349 351 cJSON_AddStringToObject(sobj, "langCode", lang); 350 - cJSON_AddStringToObject(sobj, "language", lang); 351 352 cJSON_AddStringToObject(sobj, "format", "VobSub"); 352 353 cJSON_AddNumberToObject(sobj, "codeMode", sattr->code_mode); 353 354 cJSON_AddNumberToObject(sobj, "subpType", sattr->type);
+37 -29
packages/ifo-parse/src/classify.ts
··· 1 1 import { DiscInfo, Title, TitleType, ClassificationConfidence } from "./types" 2 2 3 3 export function classifyTitles(disc: DiscInfo): DiscInfo { 4 - const sequentialTitles = disc.titles.filter(t => t.ifoTitleType === 1) 5 - const lengths = sequentialTitles.map(t => t.length) 6 - const maxLength = lengths.length > 0 ? Math.max(...lengths) : 0 7 - const avgLength = lengths.length > 0 ? lengths.reduce((a, b) => a + b, 0) / lengths.length : 0 8 - 9 4 const titles = disc.titles.map(title => ({ 10 5 ...title, 11 - classification: classifyTitle(title, sequentialTitles, maxLength, avgLength), 6 + classification: classifyTitle(title, disc.titles), 12 7 classificationConfidence: getConfidence(title), 13 8 })) 14 9 ··· 20 15 21 16 function classifyTitle( 22 17 title: Title, 23 - sequentialTitles: Title[], 24 - maxLength: number, 25 - avgLength: number 18 + allTitles: Title[] 26 19 ): TitleType { 27 20 // Menu/utility titles — never rip 28 21 if (title.ifoTitleType === 0) { 29 22 return "menu" 30 23 } 31 24 25 + // Multi-angle — alternate angle feature (usually main content) 26 + if (title.ifoTitleType === 3) { 27 + return "main_feature" 28 + } 29 + 30 + // Non-sequential — extras, bonus content 31 + if (title.ifoTitleType === 2) { 32 + return "featurette" 33 + } 34 + 32 35 // One sequential PGC — main feature or episode 33 36 if (title.ifoTitleType === 1) { 37 + const sequentialTitles = allTitles.filter(t => t.ifoTitleType === 1) 38 + 34 39 // If only one sequential title on disc → main feature 35 40 if (sequentialTitles.length === 1) { 36 41 return "main_feature" 37 42 } 38 43 39 - // Multiple sequential titles — check if one is significantly longer 40 - // If this title is the longest and much longer than average, it's the main feature 41 - if (title.length === maxLength && maxLength > avgLength * 5 && maxLength > 3600) { 44 + // Many sequential titles (>10) → TV disc, all episodes 45 + if (sequentialTitles.length > 10) { 46 + return "episode" 47 + } 48 + 49 + // Multiple sequential titles (2-10) — use chapter count and duration to distinguish 50 + // Main features typically have many chapters (>8) and are significantly longer 51 + const chapterCount = title.nrOfPttSearchPointers ?? 0 52 + const hasManyChapters = chapterCount > 8 53 + const lengths = sequentialTitles.map(t => t.length) 54 + const maxLength = Math.max(...lengths) 55 + const isLongest = title.length >= maxLength * 0.95 // within 5% of longest 56 + const isLong = title.length > 3600 // over 1 hour 57 + 58 + // If this title is the longest (or close to it), has many chapters, and is over an hour → main feature 59 + if (isLongest && hasManyChapters && isLong) { 60 + return "main_feature" 61 + } 62 + 63 + // Fallback: if chapter data is missing, use duration alone 64 + // If this title is by far the longest, treat it as the main feature 65 + if (chapterCount === 0 && isLongest && title.length > 7200) { 42 66 return "main_feature" 43 67 } 44 68 45 69 // Otherwise treat as TV disc with episodes 46 70 return "episode" 47 - } 48 - 49 - // Multi-angle — alternate angle feature 50 - if (title.ifoTitleType === 3) { 51 - return "main_feature" 52 - } 53 - 54 - // Non-sequential — extras, bonus content 55 - if (title.ifoTitleType === 2) { 56 - return "featurette" 57 71 } 58 72 59 73 throw new Error(`Unhandled IFO title type: ${title.ifoTitleType}`) ··· 87 101 } 88 102 89 103 export function extras(disc: DiscInfo): Title[] { 90 - return disc.titles.filter(t => 91 - t.classification === "featurette" || 92 - t.classification === "behind_the_scenes" || 93 - t.classification === "deleted_scenes" || 94 - t.classification === "interview" || 95 - t.classification === "trailer" 96 - ) 104 + return disc.titles.filter(t => t.classification === "featurette") 97 105 }
+9 -17
packages/ifo-parse/src/ffi.ts
··· 1 1 import { dlopen, CString, ptr } from "bun:ffi" 2 2 import path from "path" 3 + import platforms from "../platforms.json" 4 + 5 + // Platform detection logic must match shim/build.sh 6 + // When adding a new platform, update both this file and shim/build.sh 3 7 4 8 // Resolve the shim library path based on platform 5 9 function getShimPath(): string { 6 10 const platform = process.platform 7 11 const arch = process.arch 8 - 9 - let dir: string 10 - if (platform === "darwin") { 11 - if (arch === "arm64") { 12 - dir = "darwin-arm64" 13 - } else { 14 - dir = "darwin-x64" 15 - } 16 - } else if (platform === "linux") { 17 - if (arch === "arm64" || arch === "aarch64") { 18 - dir = "linux-arm64" 19 - } else { 20 - dir = "linux-x64" 21 - } 22 - } else { 12 + const key = `${platform}-${arch}` as keyof typeof platforms 13 + 14 + const config = platforms[key] 15 + if (!config) { 23 16 throw new Error(`Unsupported platform: ${platform} ${arch}`) 24 17 } 25 18 26 - const ext = platform === "darwin" ? ".dylib" : ".so" 27 - return path.join(__dirname, "..", "prebuilds", dir, `ifo_shim${ext}`) 19 + return path.join(__dirname, "..", "prebuilds", config.dir, `ifo_shim${config.ext}`) 28 20 } 29 21 30 22 const shimPath = getShimPath()
+1 -7
packages/ifo-parse/src/types.ts
··· 2 2 3 3 export interface VideoStream { 4 4 type: "video" 5 - format: string // "MPEG2" | "MPEG4" | "H264" 5 + format: string // "MPEG1" | "MPEG2" 6 6 width: number 7 7 height: number 8 8 fps: number // 25 | 29.97 ··· 18 18 type: "audio" 19 19 ix: number // 0-based stream index 20 20 langCode: string // ISO 639-1, e.g. "en" 21 - language: string // "English" 22 21 format: string // "AC3" | "DTS" | "LPCM" | "MPEG" 23 22 channels: number // 2 | 6 | 8 24 23 sampleRate: number // Hz, typically 48000 ··· 34 33 type: "subtitle" 35 34 ix: number 36 35 langCode: string 37 - language: string 38 36 format: string // "VobSub" 39 37 codeMode?: number // 0=run-length, 1=extended, 2=other 40 38 subpType?: number // 0=not specified, 1=language, 2=other ··· 103 101 | "main_feature" 104 102 | "episode" 105 103 | "featurette" 106 - | "behind_the_scenes" 107 - | "deleted_scenes" 108 - | "interview" 109 - | "trailer" 110 104 | "menu" 111 105 | "unknown" 112 106