7158 lines
271 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Build Power. Not Rent It. Own your digital infrastructure.">
<meta name="author" content="Bunker Operations">
<link rel="canonical" href="https://bnkserve.org/v2/backend/modules/media/">
<link rel="prev" href="../pages/">
<link rel="next" href="../../services/">
<link rel="icon" href="../../../../assets/favicon.png">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.1">
<title>Media Module - Changemaker Lite</title>
<link rel="stylesheet" href="../../../../assets/stylesheets/main.484c7ddc.min.css">
<link rel="stylesheet" href="../../../../assets/stylesheets/palette.ab4e12ef.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:300,300i,400,400i,700,700i%7CJetBrains+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Inter";--md-code-font:"JetBrains Mono"}</style>
<link rel="stylesheet" href="../../../../stylesheets/extra.css">
<link rel="stylesheet" href="../../../../stylesheets/home.css">
<link rel="stylesheet" href="../../../../assets/css/video-player.css">
<script>__md_scope=new URL("../../../..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
<meta property="og:type" content="website" />
<meta property="og:title" content="Media Module - Changemaker Lite" />
<meta property="og:description" content="Build Power. Not Rent It. Own your digital infrastructure." />
<meta property="og:image" content="https://bnkserve.org/assets/images/social/v2/backend/modules/media.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://bnkserve.org/v2/backend/modules/media/" />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content="Media Module - Changemaker Lite" />
<meta property="twitter:description" content="Build Power. Not Rent It. Own your digital infrastructure." />
<meta property="twitter:image" content="https://bnkserve.org/assets/images/social/v2/backend/modules/media.png" />
</head>
<body dir="ltr" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="amber">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#media-module-fastify-video-library-api" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
</div>
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="Header">
<a href="../../../.." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
<img src="../../../../assets/logo.png" alt="logo">
</a>
<label class="md-header__button md-icon" for="__drawer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
Changemaker Lite
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
Media Module
</span>
</div>
</div>
</div>
<form class="md-header__option" data-md-component="palette">
<input class="md-option" data-md-color-media="" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="amber" aria-label="Switch to light mode" type="radio" name="__palette" id="__palette_0">
<label class="md-header__button md-icon" title="Switch to light mode" for="__palette_1" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m17.75 4.09-2.53 1.94.91 3.06-2.63-1.81-2.63 1.81.91-3.06-2.53-1.94L12.44 4l1.06-3 1.06 3zm3.5 6.91-1.64 1.25.59 1.98-1.7-1.17-1.7 1.17.59-1.98L15.75 11l2.06-.05L18.5 9l.69 1.95zm-2.28 4.95c.83-.08 1.72 1.1 1.19 1.85-.32.45-.66.87-1.08 1.27C15.17 23 8.84 23 4.94 19.07c-3.91-3.9-3.91-10.24 0-14.14.4-.4.82-.76 1.27-1.08.75-.53 1.93.36 1.85 1.19-.27 2.86.69 5.83 2.89 8.02a9.96 9.96 0 0 0 8.02 2.89m-1.64 2.02a12.08 12.08 0 0 1-7.8-3.47c-2.17-2.19-3.33-5-3.49-7.82-2.81 3.14-2.7 7.96.31 10.98 3.02 3.01 7.84 3.12 10.98.31"/></svg>
</label>
<input class="md-option" data-md-color-media="" data-md-color-scheme="default" data-md-color-primary="deep-purple" data-md-color-accent="amber" aria-label="Switch to dark mode" type="radio" name="__palette" id="__palette_1">
<label class="md-header__button md-icon" title="Switch to dark mode" for="__palette_0" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 7a5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5m0 2a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3m0-7 2.39 3.42C13.65 5.15 12.84 5 12 5s-1.65.15-2.39.42zM3.34 7l4.16-.35A7.2 7.2 0 0 0 5.94 8.5c-.44.74-.69 1.5-.83 2.29zm.02 10 1.76-3.77a7.131 7.131 0 0 0 2.38 4.14zM20.65 7l-1.77 3.79a7.02 7.02 0 0 0-2.38-4.15zm-.01 10-4.14.36c.59-.51 1.12-1.14 1.54-1.86.42-.73.69-1.5.83-2.29zM12 22l-2.41-3.44c.74.27 1.55.44 2.41.44.82 0 1.63-.17 2.37-.44z"/></svg>
</label>
</form>
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
<label class="md-header__button md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
</label>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<a href="javascript:void(0)" class="md-search__icon md-icon" title="Share" aria-label="Share" data-clipboard data-clipboard-text="" data-md-component="search-share" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3 3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66 0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08"/></svg>
</a>
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
<div class="md-search__suggest" data-md-component="search-suggest"></div>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
<div class="md-header__source">
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
changemaker.lite
</div>
</a>
</div>
</nav>
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
<div class="md-grid">
<ul class="md-tabs__list">
<li class="md-tabs__item">
<a href="../../../.." class="md-tabs__link">
Home
</a>
</li>
<li class="md-tabs__item md-tabs__item--active">
<a href="../../../" class="md-tabs__link">
V2 Documentation
</a>
</li>
<li class="md-tabs__item">
<a href="../../../../phil/" class="md-tabs__link">
Philosophy
</a>
</li>
<li class="md-tabs__item">
<a href="../../../../v1/" class="md-tabs__link">
V1 Documentation (Legacy)
</a>
</li>
<li class="md-tabs__item">
<a href="../../../../blog/" class="md-tabs__link">
Blog
</a>
</li>
</ul>
</div>
</nav>
</header>
<div class="md-container" data-md-component="container">
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary md-nav--lifted" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href="../../../.." title="Changemaker Lite" class="md-nav__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
<img src="../../../../assets/logo.png" alt="logo">
</a>
Changemaker Lite
</label>
<div class="md-nav__source">
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
changemaker.lite
</div>
</a>
</div>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../.." class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2" checked>
<div class="md-nav__link md-nav__container">
<a href="../../../" class="md-nav__link ">
<span class="md-ellipsis">
V2 Documentation
</span>
</a>
<label class="md-nav__link " for="__nav_2" id="__nav_2_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_2_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_2">
<span class="md-nav__icon md-icon"></span>
V2 Documentation
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_2" >
<div class="md-nav__link md-nav__container">
<a href="../../../getting-started/" class="md-nav__link ">
<span class="md-ellipsis">
Getting Started
</span>
</a>
<label class="md-nav__link " for="__nav_2_2" id="__nav_2_2_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_2_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_2">
<span class="md-nav__icon md-icon"></span>
Getting Started
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../getting-started/quick-start/" class="md-nav__link">
<span class="md-ellipsis">
Quick Start
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_3" >
<div class="md-nav__link md-nav__container">
<a href="../../../architecture/" class="md-nav__link ">
<span class="md-ellipsis">
Architecture
</span>
</a>
<label class="md-nav__link " for="__nav_2_3" id="__nav_2_3_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_3_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_3">
<span class="md-nav__icon md-icon"></span>
Architecture
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../architecture/dual-api/" class="md-nav__link">
<span class="md-ellipsis">
Dual API System
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../architecture/authentication/" class="md-nav__link">
<span class="md-ellipsis">
Authentication & Security
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2_4" checked>
<div class="md-nav__link md-nav__container">
<a href="../../" class="md-nav__link ">
<span class="md-ellipsis">
Backend
</span>
</a>
<label class="md-nav__link " for="__nav_2_4" id="__nav_2_4_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_4_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_2_4">
<span class="md-nav__icon md-icon"></span>
Backend
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--active md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2_4_2" checked>
<div class="md-nav__link md-nav__container">
<a href="../" class="md-nav__link ">
<span class="md-ellipsis">
Modules
</span>
</a>
<label class="md-nav__link " for="__nav_2_4_2" id="__nav_2_4_2_label" tabindex="0">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="3" aria-labelledby="__nav_2_4_2_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_2_4_2">
<span class="md-nav__icon md-icon"></span>
Modules
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../auth/" class="md-nav__link">
<span class="md-ellipsis">
Auth Module
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../users/" class="md-nav__link">
<span class="md-ellipsis">
Users Module
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../settings/" class="md-nav__link">
<span class="md-ellipsis">
Settings Module
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../campaigns/" class="md-nav__link">
<span class="md-ellipsis">
Campaigns Module
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../representatives/" class="md-nav__link">
<span class="md-ellipsis">
Representatives Module
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../responses/" class="md-nav__link">
<span class="md-ellipsis">
Responses Module
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../locations/" class="md-nav__link">
<span class="md-ellipsis">
Locations Module
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../shifts/" class="md-nav__link">
<span class="md-ellipsis">
Shifts Module
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../canvass/" class="md-nav__link">
<span class="md-ellipsis">
Canvass Module
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../pages/" class="md-nav__link">
<span class="md-ellipsis">
Pages Module
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active">
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
<label class="md-nav__link md-nav__link--active" for="__toc">
<span class="md-ellipsis">
Media Module
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<a href="./" class="md-nav__link md-nav__link--active">
<span class="md-ellipsis">
Media Module
</span>
</a>
<nav class="md-nav md-nav--secondary" aria-label="On this page">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
On this page
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#overview" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#file-paths" class="md-nav__link">
<span class="md-ellipsis">
File Paths
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#database-models-drizzle-orm" class="md-nav__link">
<span class="md-ellipsis">
Database Models (Drizzle ORM)
</span>
</a>
<nav class="md-nav" aria-label="Database Models (Drizzle ORM)">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#videos-table" class="md-nav__link">
<span class="md-ellipsis">
Videos Table
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#public-media-table" class="md-nav__link">
<span class="md-ellipsis">
Public Media Table
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#upvotes-table" class="md-nav__link">
<span class="md-ellipsis">
Upvotes Table
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#video-reactions-table" class="md-nav__link">
<span class="md-ellipsis">
Video Reactions Table
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#jobs-table" class="md-nav__link">
<span class="md-ellipsis">
Jobs Table
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#compilations-table" class="md-nav__link">
<span class="md-ellipsis">
Compilations Table
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#api-endpoints" class="md-nav__link">
<span class="md-ellipsis">
API Endpoints
</span>
</a>
<nav class="md-nav" aria-label="API Endpoints">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#admin-endpoints-videos" class="md-nav__link">
<span class="md-ellipsis">
Admin Endpoints (Videos)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#public-media-endpoints" class="md-nav__link">
<span class="md-ellipsis">
Public Media Endpoints
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#reaction-endpoints" class="md-nav__link">
<span class="md-ellipsis">
Reaction Endpoints
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#comment-endpoints" class="md-nav__link">
<span class="md-ellipsis">
Comment Endpoints
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#endpoint-details" class="md-nav__link">
<span class="md-ellipsis">
Endpoint Details
</span>
</a>
<nav class="md-nav" aria-label="Endpoint Details">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#get-apivideos" class="md-nav__link">
<span class="md-ellipsis">
GET /api/videos
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#get-apimediapublic" class="md-nav__link">
<span class="md-ellipsis">
GET /api/media/public
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#get-apimediapublicid" class="md-nav__link">
<span class="md-ellipsis">
GET /api/media/public/:id
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#post-apimediapublicidupvote" class="md-nav__link">
<span class="md-ellipsis">
POST /api/media/public/:id/upvote
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#delete-apimediapublicidupvote" class="md-nav__link">
<span class="md-ellipsis">
DELETE /api/media/public/:id/upvote
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#post-apireactions" class="md-nav__link">
<span class="md-ellipsis">
POST /api/reactions
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#get-apireactions" class="md-nav__link">
<span class="md-ellipsis">
GET /api/reactions
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#get-apireactionsconfig" class="md-nav__link">
<span class="md-ellipsis">
GET /api/reactions/config
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#fastify-vs-express-differences" class="md-nav__link">
<span class="md-ellipsis">
Fastify vs Express Differences
</span>
</a>
<nav class="md-nav" aria-label="Fastify vs Express Differences">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#code-pattern-comparison" class="md-nav__link">
<span class="md-ellipsis">
Code Pattern Comparison
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#frontend-integration" class="md-nav__link">
<span class="md-ellipsis">
Frontend Integration
</span>
</a>
<nav class="md-nav" aria-label="Frontend Integration">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#admin-pages" class="md-nav__link">
<span class="md-ellipsis">
Admin Pages
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#public-pages" class="md-nav__link">
<span class="md-ellipsis">
Public Pages
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#performance-considerations" class="md-nav__link">
<span class="md-ellipsis">
Performance Considerations
</span>
</a>
<nav class="md-nav" aria-label="Performance Considerations">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#denormalized-counters" class="md-nav__link">
<span class="md-ellipsis">
Denormalized Counters
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#fire-and-forget-view-tracking" class="md-nav__link">
<span class="md-ellipsis">
Fire-and-Forget View Tracking
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#fingerprint-based-deduplication" class="md-nav__link">
<span class="md-ellipsis">
Fingerprint-Based Deduplication
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#troubleshooting" class="md-nav__link">
<span class="md-ellipsis">
Troubleshooting
</span>
</a>
<nav class="md-nav" aria-label="Troubleshooting">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#media-api-not-starting" class="md-nav__link">
<span class="md-ellipsis">
Media API Not Starting
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#cors-errors-on-media-api" class="md-nav__link">
<span class="md-ellipsis">
CORS Errors on Media API
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#upvote-not-working" class="md-nav__link">
<span class="md-ellipsis">
Upvote Not Working
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#reactions-not-appearing" class="md-nav__link">
<span class="md-ellipsis">
Reactions Not Appearing
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#job-queue-not-processing" class="md-nav__link">
<span class="md-ellipsis">
Job Queue Not Processing
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#related-documentation" class="md-nav__link">
<span class="md-ellipsis">
Related Documentation
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../services/" class="md-nav__link">
<span class="md-ellipsis">
Services
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../middleware/" class="md-nav__link">
<span class="md-ellipsis">
Middleware
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../utilities/" class="md-nav__link">
<span class="md-ellipsis">
Utilities
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_5" >
<div class="md-nav__link md-nav__container">
<a href="../../../frontend/" class="md-nav__link ">
<span class="md-ellipsis">
Frontend
</span>
</a>
<label class="md-nav__link " for="__nav_2_5" id="__nav_2_5_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_5_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_5">
<span class="md-nav__icon md-icon"></span>
Frontend
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../frontend/components/" class="md-nav__link">
<span class="md-ellipsis">
Components
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../frontend/layouts/" class="md-nav__link">
<span class="md-ellipsis">
Layouts
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../frontend/pages/" class="md-nav__link">
<span class="md-ellipsis">
Pages
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_6" >
<div class="md-nav__link md-nav__container">
<a href="../../../database/" class="md-nav__link ">
<span class="md-ellipsis">
Database
</span>
</a>
<label class="md-nav__link " for="__nav_2_6" id="__nav_2_6_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_6_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_6">
<span class="md-nav__icon md-icon"></span>
Database
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../database/schema/" class="md-nav__link">
<span class="md-ellipsis">
Schema Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../database/migrations/" class="md-nav__link">
<span class="md-ellipsis">
Migrations
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../database/seeding/" class="md-nav__link">
<span class="md-ellipsis">
Seeding
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../database/indexes/" class="md-nav__link">
<span class="md-ellipsis">
Indexes
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../database/models/" class="md-nav__link">
<span class="md-ellipsis">
Models
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_7" >
<div class="md-nav__link md-nav__container">
<a href="../../../features/" class="md-nav__link ">
<span class="md-ellipsis">
Features
</span>
</a>
<label class="md-nav__link " for="__nav_2_7" id="__nav_2_7_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_7_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_7">
<span class="md-nav__icon md-icon"></span>
Features
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../features/influence/" class="md-nav__link">
<span class="md-ellipsis">
Influence
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../features/map/" class="md-nav__link">
<span class="md-ellipsis">
Map
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../features/landing-pages/" class="md-nav__link">
<span class="md-ellipsis">
Landing Pages
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../features/email-templates/" class="md-nav__link">
<span class="md-ellipsis">
Email Templates
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../features/media/" class="md-nav__link">
<span class="md-ellipsis">
Media
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../features/newsletter/" class="md-nav__link">
<span class="md-ellipsis">
Newsletter
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../features/observability/" class="md-nav__link">
<span class="md-ellipsis">
Observability
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../features/tunnel/" class="md-nav__link">
<span class="md-ellipsis">
Tunnel
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_8" >
<div class="md-nav__link md-nav__container">
<a href="../../../deployment/" class="md-nav__link ">
<span class="md-ellipsis">
Deployment
</span>
</a>
<label class="md-nav__link " for="__nav_2_8" id="__nav_2_8_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_8_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_8">
<span class="md-nav__icon md-icon"></span>
Deployment
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../deployment/docker-compose/" class="md-nav__link">
<span class="md-ellipsis">
Docker Compose
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/environment-variables/" class="md-nav__link">
<span class="md-ellipsis">
Environment Variables
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/nginx/" class="md-nav__link">
<span class="md-ellipsis">
Nginx Configuration
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/ssl-tls/" class="md-nav__link">
<span class="md-ellipsis">
SSL/TLS
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/tunneling/" class="md-nav__link">
<span class="md-ellipsis">
Tunneling
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/monitoring-stack/" class="md-nav__link">
<span class="md-ellipsis">
Monitoring Stack
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/healthchecks/" class="md-nav__link">
<span class="md-ellipsis">
Health Checks
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/scaling/" class="md-nav__link">
<span class="md-ellipsis">
Scaling
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../deployment/backup-restore/" class="md-nav__link">
<span class="md-ellipsis">
Backup & Restore
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_9" >
<div class="md-nav__link md-nav__container">
<a href="../../../development/" class="md-nav__link ">
<span class="md-ellipsis">
Development
</span>
</a>
<label class="md-nav__link " for="__nav_2_9" id="__nav_2_9_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_9_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_9">
<span class="md-nav__icon md-icon"></span>
Development
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../development/local-setup/" class="md-nav__link">
<span class="md-ellipsis">
Local Setup
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/docker-workflow/" class="md-nav__link">
<span class="md-ellipsis">
Docker Workflow
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/git-workflow/" class="md-nav__link">
<span class="md-ellipsis">
Git Workflow
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/npm-commands/" class="md-nav__link">
<span class="md-ellipsis">
NPM Commands
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/migrations/" class="md-nav__link">
<span class="md-ellipsis">
Migrations
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/typescript/" class="md-nav__link">
<span class="md-ellipsis">
TypeScript
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/testing/" class="md-nav__link">
<span class="md-ellipsis">
Testing
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/debugging/" class="md-nav__link">
<span class="md-ellipsis">
Debugging
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../development/code-style/" class="md-nav__link">
<span class="md-ellipsis">
Code Style
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_10" >
<div class="md-nav__link md-nav__container">
<a href="../../../api-reference/" class="md-nav__link ">
<span class="md-ellipsis">
API Reference
</span>
</a>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_10_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_10">
<span class="md-nav__icon md-icon"></span>
API Reference
</label>
<ul class="md-nav__list" data-md-scrollfix>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_11" >
<div class="md-nav__link md-nav__container">
<a href="../../../user-guides/" class="md-nav__link ">
<span class="md-ellipsis">
User Guides
</span>
</a>
<label class="md-nav__link " for="__nav_2_11" id="__nav_2_11_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_11_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_11">
<span class="md-nav__icon md-icon"></span>
User Guides
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../user-guides/admin-guide/" class="md-nav__link">
<span class="md-ellipsis">
Admin Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../user-guides/campaign-manager-guide/" class="md-nav__link">
<span class="md-ellipsis">
Campaign Manager Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../user-guides/map-organizer-guide/" class="md-nav__link">
<span class="md-ellipsis">
Map Organizer Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../user-guides/content-editor-guide/" class="md-nav__link">
<span class="md-ellipsis">
Content Editor Guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../user-guides/volunteer-guide/" class="md-nav__link">
<span class="md-ellipsis">
Volunteer Guide
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_12" >
<div class="md-nav__link md-nav__container">
<a href="../../../troubleshooting/" class="md-nav__link ">
<span class="md-ellipsis">
Troubleshooting
</span>
</a>
<label class="md-nav__link " for="__nav_2_12" id="__nav_2_12_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_12_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_12">
<span class="md-nav__icon md-icon"></span>
Troubleshooting
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../troubleshooting/faq/" class="md-nav__link">
<span class="md-ellipsis">
FAQ
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/common-errors/" class="md-nav__link">
<span class="md-ellipsis">
Common Errors
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/auth-issues/" class="md-nav__link">
<span class="md-ellipsis">
Auth Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/database-issues/" class="md-nav__link">
<span class="md-ellipsis">
Database Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/docker-issues/" class="md-nav__link">
<span class="md-ellipsis">
Docker Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/email-issues/" class="md-nav__link">
<span class="md-ellipsis">
Email Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/geocoding-issues/" class="md-nav__link">
<span class="md-ellipsis">
Geocoding Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/monitoring-issues/" class="md-nav__link">
<span class="md-ellipsis">
Monitoring Issues
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../troubleshooting/performance-optimization/" class="md-nav__link">
<span class="md-ellipsis">
Performance Optimization
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_13" >
<div class="md-nav__link md-nav__container">
<a href="../../../migration/" class="md-nav__link ">
<span class="md-ellipsis">
Migration
</span>
</a>
<label class="md-nav__link " for="__nav_2_13" id="__nav_2_13_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_13_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_13">
<span class="md-nav__icon md-icon"></span>
Migration
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../migration/feature-parity/" class="md-nav__link">
<span class="md-ellipsis">
Feature Parity
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../migration/breaking-changes/" class="md-nav__link">
<span class="md-ellipsis">
Breaking Changes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../migration/api-changes/" class="md-nav__link">
<span class="md-ellipsis">
API Changes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../migration/data-migration/" class="md-nav__link">
<span class="md-ellipsis">
Data Migration
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle md-toggle--indeterminate" type="checkbox" id="__nav_2_14" >
<div class="md-nav__link md-nav__container">
<a href="../../../contributing/" class="md-nav__link ">
<span class="md-ellipsis">
Contributing
</span>
</a>
<label class="md-nav__link " for="__nav_2_14" id="__nav_2_14_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_14_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_2_14">
<span class="md-nav__icon md-icon"></span>
Contributing
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../../contributing/development-setup/" class="md-nav__link">
<span class="md-ellipsis">
Development Setup
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../contributing/code-of-conduct/" class="md-nav__link">
<span class="md-ellipsis">
Code of Conduct
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../contributing/pull-requests/" class="md-nav__link">
<span class="md-ellipsis">
Pull Requests
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../../../contributing/roadmap/" class="md-nav__link">
<span class="md-ellipsis">
Roadmap
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../../phil/" class="md-nav__link">
<span class="md-ellipsis">
Philosophy
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../../v1/" class="md-nav__link">
<span class="md-ellipsis">
V1 Documentation (Legacy)
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../../blog/" class="md-nav__link">
<span class="md-ellipsis">
Blog
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--secondary" aria-label="On this page">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
On this page
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#overview" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#file-paths" class="md-nav__link">
<span class="md-ellipsis">
File Paths
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#database-models-drizzle-orm" class="md-nav__link">
<span class="md-ellipsis">
Database Models (Drizzle ORM)
</span>
</a>
<nav class="md-nav" aria-label="Database Models (Drizzle ORM)">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#videos-table" class="md-nav__link">
<span class="md-ellipsis">
Videos Table
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#public-media-table" class="md-nav__link">
<span class="md-ellipsis">
Public Media Table
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#upvotes-table" class="md-nav__link">
<span class="md-ellipsis">
Upvotes Table
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#video-reactions-table" class="md-nav__link">
<span class="md-ellipsis">
Video Reactions Table
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#jobs-table" class="md-nav__link">
<span class="md-ellipsis">
Jobs Table
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#compilations-table" class="md-nav__link">
<span class="md-ellipsis">
Compilations Table
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#api-endpoints" class="md-nav__link">
<span class="md-ellipsis">
API Endpoints
</span>
</a>
<nav class="md-nav" aria-label="API Endpoints">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#admin-endpoints-videos" class="md-nav__link">
<span class="md-ellipsis">
Admin Endpoints (Videos)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#public-media-endpoints" class="md-nav__link">
<span class="md-ellipsis">
Public Media Endpoints
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#reaction-endpoints" class="md-nav__link">
<span class="md-ellipsis">
Reaction Endpoints
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#comment-endpoints" class="md-nav__link">
<span class="md-ellipsis">
Comment Endpoints
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#endpoint-details" class="md-nav__link">
<span class="md-ellipsis">
Endpoint Details
</span>
</a>
<nav class="md-nav" aria-label="Endpoint Details">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#get-apivideos" class="md-nav__link">
<span class="md-ellipsis">
GET /api/videos
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#get-apimediapublic" class="md-nav__link">
<span class="md-ellipsis">
GET /api/media/public
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#get-apimediapublicid" class="md-nav__link">
<span class="md-ellipsis">
GET /api/media/public/:id
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#post-apimediapublicidupvote" class="md-nav__link">
<span class="md-ellipsis">
POST /api/media/public/:id/upvote
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#delete-apimediapublicidupvote" class="md-nav__link">
<span class="md-ellipsis">
DELETE /api/media/public/:id/upvote
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#post-apireactions" class="md-nav__link">
<span class="md-ellipsis">
POST /api/reactions
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#get-apireactions" class="md-nav__link">
<span class="md-ellipsis">
GET /api/reactions
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#get-apireactionsconfig" class="md-nav__link">
<span class="md-ellipsis">
GET /api/reactions/config
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#fastify-vs-express-differences" class="md-nav__link">
<span class="md-ellipsis">
Fastify vs Express Differences
</span>
</a>
<nav class="md-nav" aria-label="Fastify vs Express Differences">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#code-pattern-comparison" class="md-nav__link">
<span class="md-ellipsis">
Code Pattern Comparison
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#frontend-integration" class="md-nav__link">
<span class="md-ellipsis">
Frontend Integration
</span>
</a>
<nav class="md-nav" aria-label="Frontend Integration">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#admin-pages" class="md-nav__link">
<span class="md-ellipsis">
Admin Pages
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#public-pages" class="md-nav__link">
<span class="md-ellipsis">
Public Pages
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#performance-considerations" class="md-nav__link">
<span class="md-ellipsis">
Performance Considerations
</span>
</a>
<nav class="md-nav" aria-label="Performance Considerations">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#denormalized-counters" class="md-nav__link">
<span class="md-ellipsis">
Denormalized Counters
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#fire-and-forget-view-tracking" class="md-nav__link">
<span class="md-ellipsis">
Fire-and-Forget View Tracking
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#fingerprint-based-deduplication" class="md-nav__link">
<span class="md-ellipsis">
Fingerprint-Based Deduplication
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#troubleshooting" class="md-nav__link">
<span class="md-ellipsis">
Troubleshooting
</span>
</a>
<nav class="md-nav" aria-label="Troubleshooting">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#media-api-not-starting" class="md-nav__link">
<span class="md-ellipsis">
Media API Not Starting
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#cors-errors-on-media-api" class="md-nav__link">
<span class="md-ellipsis">
CORS Errors on Media API
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#upvote-not-working" class="md-nav__link">
<span class="md-ellipsis">
Upvote Not Working
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#reactions-not-appearing" class="md-nav__link">
<span class="md-ellipsis">
Reactions Not Appearing
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#job-queue-not-processing" class="md-nav__link">
<span class="md-ellipsis">
Job Queue Not Processing
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#related-documentation" class="md-nav__link">
<span class="md-ellipsis">
Related Documentation
</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<nav class="md-path" aria-label="Navigation" >
<ol class="md-path__list">
<li class="md-path__item">
<a href="../../../.." class="md-path__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-path__item">
<a href="../../../" class="md-path__link">
<span class="md-ellipsis">
V2 Documentation
</span>
</a>
</li>
<li class="md-path__item">
<a href="../../" class="md-path__link">
<span class="md-ellipsis">
Backend
</span>
</a>
</li>
<li class="md-path__item">
<a href="../" class="md-path__link">
<span class="md-ellipsis">
Modules
</span>
</a>
</li>
</ol>
</nav>
<article class="md-content__inner md-typeset">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/v2/backend/modules/media.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/v2/backend/modules/media.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
<h1 id="media-module-fastify-video-library-api">Media Module (Fastify Video Library API)<a class="headerlink" href="#media-module-fastify-video-library-api" title="Permanent link">&para;</a></h1>
<h2 id="overview">Overview<a class="headerlink" href="#overview" title="Permanent link">&para;</a></h2>
<p>The Media module is a <strong>separate Fastify microservice</strong> running on port 4100 (separate from the main Express API on port 4000). It provides a complete video library management system with public gallery features, reaction tracking, and job queue for video processing. The module uses <strong>Drizzle ORM</strong> (unlike the main API's Prisma ORM) and shares the same PostgreSQL database.</p>
<p><strong>Key Features:</strong></p>
<ul>
<li><strong>Dual API architecture:</strong></li>
<li>Main Express API (port 4000) — Prisma ORM</li>
<li>Media Fastify API (port 4100) — Drizzle ORM</li>
<li>Shared PostgreSQL 16 database</li>
<li><strong>Video library management:</strong></li>
<li>Directory-based organization (studios, gifs, private, inbox, curated, etc.)</li>
<li>Metadata tracking (duration, quality, orientation, file size, dimensions)</li>
<li>Thumbnail generation and storage</li>
<li>File hash-based deduplication</li>
<li><strong>Public gallery system:</strong></li>
<li>Category-based organization</li>
<li>Engagement tracking (views, upvotes, comments, watch time)</li>
<li>Lock/unlock system for controlling public visibility</li>
<li>Session-based upvoting (no auth required)</li>
<li><strong>Reaction system:</strong></li>
<li>6 emoji reactions (👍 like, ❤️ love, 😂 laugh, 😮 wow, 😢 sad, 😠 angry)</li>
<li>Timestamped reactions (mark specific moments in videos)</li>
<li>User-based tracking (authenticated users)</li>
<li><strong>Job queue:</strong></li>
<li>Video processing job management</li>
<li>Resource category allocation (GPU AI, GPU encode, CPU)</li>
<li>Queue position tracking with VRAM requirements</li>
<li>Pipeline integration for multi-step processing</li>
<li><strong>Compilation management:</strong></li>
<li>Multi-video compilation tracking</li>
<li>Settings preservation</li>
<li><strong>Feature flag:</strong> <code>ENABLE_MEDIA_FEATURES=true</code> (opt-in)</li>
</ul>
<h2 id="file-paths">File Paths<a class="headerlink" href="#file-paths" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>File</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>api/src/media-server.ts</code></td>
<td>Fastify server entry point (port 4100)</td>
</tr>
<tr>
<td><code>api/src/modules/media/db/schema.ts</code></td>
<td>Drizzle schema (15+ tables, 1,400+ lines)</td>
</tr>
<tr>
<td><code>api/src/modules/media/routes/videos.routes.ts</code></td>
<td>Video CRUD routes (99 lines)</td>
</tr>
<tr>
<td><code>api/src/modules/media/routes/public-media.routes.ts</code></td>
<td>Public gallery routes (12,852 lines)</td>
</tr>
<tr>
<td><code>api/src/modules/media/routes/reactions.routes.ts</code></td>
<td>Reaction routes (135 lines)</td>
</tr>
<tr>
<td><code>api/src/modules/media/routes/comments.routes.ts</code></td>
<td>Comment routes (4,827 lines)</td>
</tr>
<tr>
<td><code>api/src/modules/media/middleware/auth.ts</code></td>
<td>Fastify auth middleware (JWT verification)</td>
</tr>
<tr>
<td><code>api/src/modules/media/types/enums.ts</code></td>
<td>Shared enums</td>
</tr>
</tbody>
</table>
<h2 id="database-models-drizzle-orm">Database Models (Drizzle ORM)<a class="headerlink" href="#database-models-drizzle-orm" title="Permanent link">&para;</a></h2>
<h3 id="videos-table">Videos Table<a class="headerlink" href="#videos-table" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="k">export</span><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">videos</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">pgTable</span><span class="p">(</span><span class="s1">&#39;videos&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2" href="#__codelineno-0-2"></a><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">serial</span><span class="p">(</span><span class="s1">&#39;id&#39;</span><span class="p">).</span><span class="nx">primaryKey</span><span class="p">(),</span>
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3" href="#__codelineno-0-3"></a><span class="w"> </span><span class="nx">path</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;path&#39;</span><span class="p">).</span><span class="nx">notNull</span><span class="p">().</span><span class="nx">unique</span><span class="p">(),</span>
</span><span id="__span-0-4"><a id="__codelineno-0-4" name="__codelineno-0-4" href="#__codelineno-0-4"></a><span class="w"> </span><span class="nx">filename</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;filename&#39;</span><span class="p">).</span><span class="nx">notNull</span><span class="p">(),</span>
</span><span id="__span-0-5"><a id="__codelineno-0-5" name="__codelineno-0-5" href="#__codelineno-0-5"></a><span class="w"> </span><span class="nx">producer</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;producer&#39;</span><span class="p">),</span>
</span><span id="__span-0-6"><a id="__codelineno-0-6" name="__codelineno-0-6" href="#__codelineno-0-6"></a><span class="w"> </span><span class="nx">creator</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;creator&#39;</span><span class="p">),</span>
</span><span id="__span-0-7"><a id="__codelineno-0-7" name="__codelineno-0-7" href="#__codelineno-0-7"></a><span class="w"> </span><span class="nx">title</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;title&#39;</span><span class="p">),</span>
</span><span id="__span-0-8"><a id="__codelineno-0-8" name="__codelineno-0-8" href="#__codelineno-0-8"></a><span class="w"> </span><span class="nx">durationSeconds</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;duration_seconds&#39;</span><span class="p">),</span>
</span><span id="__span-0-9"><a id="__codelineno-0-9" name="__codelineno-0-9" href="#__codelineno-0-9"></a><span class="w"> </span><span class="nx">quality</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;quality&#39;</span><span class="p">),</span>
</span><span id="__span-0-10"><a id="__codelineno-0-10" name="__codelineno-0-10" href="#__codelineno-0-10"></a><span class="w"> </span><span class="nx">orientation</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;orientation&#39;</span><span class="p">),</span>
</span><span id="__span-0-11"><a id="__codelineno-0-11" name="__codelineno-0-11" href="#__codelineno-0-11"></a><span class="w"> </span><span class="nx">hasAudio</span><span class="o">:</span><span class="w"> </span><span class="kt">boolean</span><span class="p">(</span><span class="s1">&#39;has_audio&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">(</span><span class="kc">true</span><span class="p">),</span>
</span><span id="__span-0-12"><a id="__codelineno-0-12" name="__codelineno-0-12" href="#__codelineno-0-12"></a><span class="w"> </span><span class="nx">fileSize</span><span class="o">:</span><span class="w"> </span><span class="kt">bigint</span><span class="p">(</span><span class="s1">&#39;file_size&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">mode</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;number&#39;</span><span class="w"> </span><span class="p">}),</span>
</span><span id="__span-0-13"><a id="__codelineno-0-13" name="__codelineno-0-13" href="#__codelineno-0-13"></a><span class="w"> </span><span class="nx">fileHash</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;file_hash&#39;</span><span class="p">),</span>
</span><span id="__span-0-14"><a id="__codelineno-0-14" name="__codelineno-0-14" href="#__codelineno-0-14"></a><span class="w"> </span><span class="nx">width</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;width&#39;</span><span class="p">),</span>
</span><span id="__span-0-15"><a id="__codelineno-0-15" name="__codelineno-0-15" href="#__codelineno-0-15"></a><span class="w"> </span><span class="nx">height</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;height&#39;</span><span class="p">),</span>
</span><span id="__span-0-16"><a id="__codelineno-0-16" name="__codelineno-0-16" href="#__codelineno-0-16"></a><span class="w"> </span><span class="nx">lastValidated</span><span class="o">:</span><span class="w"> </span><span class="kt">timestamp</span><span class="p">(</span><span class="s1">&#39;last_validated&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">withTimezone</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">}),</span>
</span><span id="__span-0-17"><a id="__codelineno-0-17" name="__codelineno-0-17" href="#__codelineno-0-17"></a><span class="w"> </span><span class="nx">isValid</span><span class="o">:</span><span class="w"> </span><span class="kt">boolean</span><span class="p">(</span><span class="s1">&#39;is_valid&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">(</span><span class="kc">true</span><span class="p">),</span>
</span><span id="__span-0-18"><a id="__codelineno-0-18" name="__codelineno-0-18" href="#__codelineno-0-18"></a><span class="w"> </span><span class="nx">thumbnailPath</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;thumbnail_path&#39;</span><span class="p">),</span>
</span><span id="__span-0-19"><a id="__codelineno-0-19" name="__codelineno-0-19" href="#__codelineno-0-19"></a><span class="w"> </span><span class="nx">createdAt</span><span class="o">:</span><span class="w"> </span><span class="kt">timestamp</span><span class="p">(</span><span class="s1">&#39;created_at&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">withTimezone</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">}).</span><span class="nx">defaultNow</span><span class="p">(),</span>
</span><span id="__span-0-20"><a id="__codelineno-0-20" name="__codelineno-0-20" href="#__codelineno-0-20"></a><span class="w"> </span><span class="nx">tags</span><span class="o">:</span><span class="w"> </span><span class="kt">jsonb</span><span class="p">(</span><span class="s1">&#39;tags&#39;</span><span class="p">).</span><span class="nx">$type</span><span class="o">&lt;</span><span class="kt">string</span><span class="p">[]</span><span class="o">&gt;</span><span class="p">(),</span>
</span><span id="__span-0-21"><a id="__codelineno-0-21" name="__codelineno-0-21" href="#__codelineno-0-21"></a>
</span><span id="__span-0-22"><a id="__codelineno-0-22" name="__codelineno-0-22" href="#__codelineno-0-22"></a><span class="w"> </span><span class="c1">// Directory type for efficient filtering</span>
</span><span id="__span-0-23"><a id="__codelineno-0-23" name="__codelineno-0-23" href="#__codelineno-0-23"></a><span class="w"> </span><span class="nx">directoryType</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;directory_type&#39;</span><span class="p">).</span><span class="nx">$type</span><span class="o">&lt;</span><span class="nx">DirectoryType</span><span class="o">&gt;</span><span class="p">(),</span>
</span><span id="__span-0-24"><a id="__codelineno-0-24" name="__codelineno-0-24" href="#__codelineno-0-24"></a>
</span><span id="__span-0-25"><a id="__codelineno-0-25" name="__codelineno-0-25" href="#__codelineno-0-25"></a><span class="w"> </span><span class="c1">// Historical engagement stats (preserved when moved from public media)</span>
</span><span id="__span-0-26"><a id="__codelineno-0-26" name="__codelineno-0-26" href="#__codelineno-0-26"></a><span class="w"> </span><span class="nx">publicViewCount</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;public_view_count&#39;</span><span class="p">),</span>
</span><span id="__span-0-27"><a id="__codelineno-0-27" name="__codelineno-0-27" href="#__codelineno-0-27"></a><span class="w"> </span><span class="nx">publicUpvoteCount</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;public_upvote_count&#39;</span><span class="p">),</span>
</span><span id="__span-0-28"><a id="__codelineno-0-28" name="__codelineno-0-28" href="#__codelineno-0-28"></a><span class="w"> </span><span class="nx">publicCommentCount</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;public_comment_count&#39;</span><span class="p">),</span>
</span><span id="__span-0-29"><a id="__codelineno-0-29" name="__codelineno-0-29" href="#__codelineno-0-29"></a><span class="w"> </span><span class="nx">publicCompletionCount</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;public_completion_count&#39;</span><span class="p">),</span>
</span><span id="__span-0-30"><a id="__codelineno-0-30" name="__codelineno-0-30" href="#__codelineno-0-30"></a><span class="w"> </span><span class="nx">publicTotalWatchTime</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;public_total_watch_time&#39;</span><span class="p">),</span>
</span><span id="__span-0-31"><a id="__codelineno-0-31" name="__codelineno-0-31" href="#__codelineno-0-31"></a><span class="w"> </span><span class="nx">movedFromPublicAt</span><span class="o">:</span><span class="w"> </span><span class="kt">timestamp</span><span class="p">(</span><span class="s1">&#39;moved_from_public_at&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">withTimezone</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">}),</span>
</span><span id="__span-0-32"><a id="__codelineno-0-32" name="__codelineno-0-32" href="#__codelineno-0-32"></a>
</span><span id="__span-0-33"><a id="__codelineno-0-33" name="__codelineno-0-33" href="#__codelineno-0-33"></a><span class="w"> </span><span class="c1">// Name standardization tracking</span>
</span><span id="__span-0-34"><a id="__codelineno-0-34" name="__codelineno-0-34" href="#__codelineno-0-34"></a><span class="w"> </span><span class="nx">originalFilename</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;original_filename&#39;</span><span class="p">),</span>
</span><span id="__span-0-35"><a id="__codelineno-0-35" name="__codelineno-0-35" href="#__codelineno-0-35"></a><span class="w"> </span><span class="nx">originalPath</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;original_path&#39;</span><span class="p">),</span>
</span><span id="__span-0-36"><a id="__codelineno-0-36" name="__codelineno-0-36" href="#__codelineno-0-36"></a><span class="w"> </span><span class="nx">standardizedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">timestamp</span><span class="p">(</span><span class="s1">&#39;standardized_at&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">withTimezone</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">}),</span>
</span><span id="__span-0-37"><a id="__codelineno-0-37" name="__codelineno-0-37" href="#__codelineno-0-37"></a><span class="p">},</span><span class="w"> </span><span class="p">(</span><span class="nx">table</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
</span><span id="__span-0-38"><a id="__codelineno-0-38" name="__codelineno-0-38" href="#__codelineno-0-38"></a><span class="w"> </span><span class="nx">orientationIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_orientation&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="nx">table</span><span class="p">.</span><span class="nx">orientation</span><span class="p">),</span>
</span><span id="__span-0-39"><a id="__codelineno-0-39" name="__codelineno-0-39" href="#__codelineno-0-39"></a><span class="w"> </span><span class="nx">producerIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_producer&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="nx">table</span><span class="p">.</span><span class="nx">producer</span><span class="p">),</span>
</span><span id="__span-0-40"><a id="__codelineno-0-40" name="__codelineno-0-40" href="#__codelineno-0-40"></a><span class="w"> </span><span class="nx">isValidIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_is_valid&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="nx">table</span><span class="p">.</span><span class="nx">isValid</span><span class="p">),</span>
</span><span id="__span-0-41"><a id="__codelineno-0-41" name="__codelineno-0-41" href="#__codelineno-0-41"></a><span class="w"> </span><span class="nx">directoryTypeIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_directory_type&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="nx">table</span><span class="p">.</span><span class="nx">directoryType</span><span class="p">),</span>
</span><span id="__span-0-42"><a id="__codelineno-0-42" name="__codelineno-0-42" href="#__codelineno-0-42"></a><span class="w"> </span><span class="nx">fingerprintIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_videos_fingerprint&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span>
</span><span id="__span-0-43"><a id="__codelineno-0-43" name="__codelineno-0-43" href="#__codelineno-0-43"></a><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">durationSeconds</span><span class="p">,</span><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">fileSize</span><span class="p">,</span><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">width</span><span class="p">,</span><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">height</span>
</span><span id="__span-0-44"><a id="__codelineno-0-44" name="__codelineno-0-44" href="#__codelineno-0-44"></a><span class="w"> </span><span class="p">),</span>
</span><span id="__span-0-45"><a id="__codelineno-0-45" name="__codelineno-0-45" href="#__codelineno-0-45"></a><span class="w"> </span><span class="nx">directoryValidOrientationIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_videos_directory_valid_orientation&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span>
</span><span id="__span-0-46"><a id="__codelineno-0-46" name="__codelineno-0-46" href="#__codelineno-0-46"></a><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">directoryType</span><span class="p">,</span><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">isValid</span><span class="p">,</span><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">orientation</span>
</span><span id="__span-0-47"><a id="__codelineno-0-47" name="__codelineno-0-47" href="#__codelineno-0-47"></a><span class="w"> </span><span class="p">),</span>
</span><span id="__span-0-48"><a id="__codelineno-0-48" name="__codelineno-0-48" href="#__codelineno-0-48"></a><span class="p">}));</span>
</span><span id="__span-0-49"><a id="__codelineno-0-49" name="__codelineno-0-49" href="#__codelineno-0-49"></a>
</span><span id="__span-0-50"><a id="__codelineno-0-50" name="__codelineno-0-50" href="#__codelineno-0-50"></a><span class="c1">// Directory types</span>
</span><span id="__span-0-51"><a id="__codelineno-0-51" name="__codelineno-0-51" href="#__codelineno-0-51"></a><span class="k">export</span><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">DIRECTORY_TYPES</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span>
</span><span id="__span-0-52"><a id="__codelineno-0-52" name="__codelineno-0-52" href="#__codelineno-0-52"></a><span class="w"> </span><span class="s1">&#39;studios&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;gifs&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;private&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;inbox&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;curated&#39;</span><span class="p">,</span>
</span><span id="__span-0-53"><a id="__codelineno-0-53" name="__codelineno-0-53" href="#__codelineno-0-53"></a><span class="w"> </span><span class="s1">&#39;playback&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;compilations&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;videos&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;highlights&#39;</span>
</span><span id="__span-0-54"><a id="__codelineno-0-54" name="__codelineno-0-54" href="#__codelineno-0-54"></a><span class="p">]</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="kd">const</span><span class="p">;</span>
</span><span id="__span-0-55"><a id="__codelineno-0-55" name="__codelineno-0-55" href="#__codelineno-0-55"></a><span class="k">export</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="nx">DirectoryType</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">typeof</span><span class="w"> </span><span class="nx">DIRECTORY_TYPES</span><span class="p">[</span><span class="kt">number</span><span class="p">];</span>
</span></code></pre></div>
<p><strong>Key Features:</strong></p>
<ul>
<li><strong>Unique path constraint</strong> — Prevents duplicate entries</li>
<li><strong>File hash</strong> — Enables deduplication based on content</li>
<li><strong>Fingerprint index</strong> — Fast duplicate detection (duration + fileSize + width + height)</li>
<li><strong>Directory type</strong> — Efficient filtering by category</li>
<li><strong>Historical stats</strong> — Preserves engagement metrics when moving from public gallery</li>
<li><strong>Standardization tracking</strong> — Tracks original filename before renaming</li>
</ul>
<hr />
<h3 id="public-media-table">Public Media Table<a class="headerlink" href="#public-media-table" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1" href="#__codelineno-1-1"></a><span class="k">export</span><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">publicMedia</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">pgTable</span><span class="p">(</span><span class="s1">&#39;public_media&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2" href="#__codelineno-1-2"></a><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">serial</span><span class="p">(</span><span class="s1">&#39;id&#39;</span><span class="p">).</span><span class="nx">primaryKey</span><span class="p">(),</span>
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3" href="#__codelineno-1-3"></a><span class="w"> </span><span class="nx">path</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;path&#39;</span><span class="p">).</span><span class="nx">notNull</span><span class="p">().</span><span class="nx">unique</span><span class="p">(),</span>
</span><span id="__span-1-4"><a id="__codelineno-1-4" name="__codelineno-1-4" href="#__codelineno-1-4"></a><span class="w"> </span><span class="nx">filename</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;filename&#39;</span><span class="p">).</span><span class="nx">notNull</span><span class="p">(),</span>
</span><span id="__span-1-5"><a id="__codelineno-1-5" name="__codelineno-1-5" href="#__codelineno-1-5"></a><span class="w"> </span><span class="nx">category</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;category&#39;</span><span class="p">).</span><span class="nx">notNull</span><span class="p">(),</span>
</span><span id="__span-1-6"><a id="__codelineno-1-6" name="__codelineno-1-6" href="#__codelineno-1-6"></a><span class="w"> </span><span class="nx">durationSeconds</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;duration_seconds&#39;</span><span class="p">),</span>
</span><span id="__span-1-7"><a id="__codelineno-1-7" name="__codelineno-1-7" href="#__codelineno-1-7"></a><span class="w"> </span><span class="nx">quality</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;quality&#39;</span><span class="p">),</span>
</span><span id="__span-1-8"><a id="__codelineno-1-8" name="__codelineno-1-8" href="#__codelineno-1-8"></a><span class="w"> </span><span class="nx">orientation</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;orientation&#39;</span><span class="p">),</span>
</span><span id="__span-1-9"><a id="__codelineno-1-9" name="__codelineno-1-9" href="#__codelineno-1-9"></a><span class="w"> </span><span class="nx">thumbnailPath</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;thumbnail_path&#39;</span><span class="p">),</span>
</span><span id="__span-1-10"><a id="__codelineno-1-10" name="__codelineno-1-10" href="#__codelineno-1-10"></a><span class="w"> </span><span class="nx">fileSize</span><span class="o">:</span><span class="w"> </span><span class="kt">bigint</span><span class="p">(</span><span class="s1">&#39;file_size&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">mode</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;number&#39;</span><span class="w"> </span><span class="p">}),</span>
</span><span id="__span-1-11"><a id="__codelineno-1-11" name="__codelineno-1-11" href="#__codelineno-1-11"></a>
</span><span id="__span-1-12"><a id="__codelineno-1-12" name="__codelineno-1-12" href="#__codelineno-1-12"></a><span class="w"> </span><span class="c1">// Denormalized counters for performance</span>
</span><span id="__span-1-13"><a id="__codelineno-1-13" name="__codelineno-1-13" href="#__codelineno-1-13"></a><span class="w"> </span><span class="nx">viewCount</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;view_count&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">(</span><span class="mf">0</span><span class="p">),</span>
</span><span id="__span-1-14"><a id="__codelineno-1-14" name="__codelineno-1-14" href="#__codelineno-1-14"></a><span class="w"> </span><span class="nx">upvoteCount</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;upvote_count&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">(</span><span class="mf">0</span><span class="p">),</span>
</span><span id="__span-1-15"><a id="__codelineno-1-15" name="__codelineno-1-15" href="#__codelineno-1-15"></a><span class="w"> </span><span class="nx">commentCount</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;comment_count&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">(</span><span class="mf">0</span><span class="p">),</span>
</span><span id="__span-1-16"><a id="__codelineno-1-16" name="__codelineno-1-16" href="#__codelineno-1-16"></a><span class="w"> </span><span class="nx">finishCount</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;finish_count&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">(</span><span class="mf">0</span><span class="p">),</span>
</span><span id="__span-1-17"><a id="__codelineno-1-17" name="__codelineno-1-17" href="#__codelineno-1-17"></a><span class="w"> </span><span class="nx">totalWatchTime</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;total_watch_time&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">(</span><span class="mf">0</span><span class="p">),</span>
</span><span id="__span-1-18"><a id="__codelineno-1-18" name="__codelineno-1-18" href="#__codelineno-1-18"></a>
</span><span id="__span-1-19"><a id="__codelineno-1-19" name="__codelineno-1-19" href="#__codelineno-1-19"></a><span class="w"> </span><span class="c1">// Lock system</span>
</span><span id="__span-1-20"><a id="__codelineno-1-20" name="__codelineno-1-20" href="#__codelineno-1-20"></a><span class="w"> </span><span class="nx">isLocked</span><span class="o">:</span><span class="w"> </span><span class="kt">boolean</span><span class="p">(</span><span class="s1">&#39;is_locked&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">(</span><span class="kc">false</span><span class="p">),</span>
</span><span id="__span-1-21"><a id="__codelineno-1-21" name="__codelineno-1-21" href="#__codelineno-1-21"></a><span class="w"> </span><span class="nx">lockedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">timestamp</span><span class="p">(</span><span class="s1">&#39;locked_at&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">withTimezone</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">}),</span>
</span><span id="__span-1-22"><a id="__codelineno-1-22" name="__codelineno-1-22" href="#__codelineno-1-22"></a><span class="w"> </span><span class="nx">lockedReason</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;locked_reason&#39;</span><span class="p">),</span>
</span><span id="__span-1-23"><a id="__codelineno-1-23" name="__codelineno-1-23" href="#__codelineno-1-23"></a>
</span><span id="__span-1-24"><a id="__codelineno-1-24" name="__codelineno-1-24" href="#__codelineno-1-24"></a><span class="w"> </span><span class="nx">createdAt</span><span class="o">:</span><span class="w"> </span><span class="kt">timestamp</span><span class="p">(</span><span class="s1">&#39;created_at&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">withTimezone</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">}).</span><span class="nx">defaultNow</span><span class="p">(),</span>
</span><span id="__span-1-25"><a id="__codelineno-1-25" name="__codelineno-1-25" href="#__codelineno-1-25"></a><span class="w"> </span><span class="nx">updatedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">timestamp</span><span class="p">(</span><span class="s1">&#39;updated_at&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">withTimezone</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">}).</span><span class="nx">defaultNow</span><span class="p">(),</span>
</span><span id="__span-1-26"><a id="__codelineno-1-26" name="__codelineno-1-26" href="#__codelineno-1-26"></a><span class="p">},</span><span class="w"> </span><span class="p">(</span><span class="nx">table</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
</span><span id="__span-1-27"><a id="__codelineno-1-27" name="__codelineno-1-27" href="#__codelineno-1-27"></a><span class="w"> </span><span class="nx">categoryIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_public_media_category&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="nx">table</span><span class="p">.</span><span class="nx">category</span><span class="p">),</span>
</span><span id="__span-1-28"><a id="__codelineno-1-28" name="__codelineno-1-28" href="#__codelineno-1-28"></a><span class="w"> </span><span class="nx">orientationIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_public_media_orientation&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="nx">table</span><span class="p">.</span><span class="nx">orientation</span><span class="p">),</span>
</span><span id="__span-1-29"><a id="__codelineno-1-29" name="__codelineno-1-29" href="#__codelineno-1-29"></a><span class="w"> </span><span class="nx">viewCountIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_public_media_views&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="nx">table</span><span class="p">.</span><span class="nx">viewCount</span><span class="p">),</span>
</span><span id="__span-1-30"><a id="__codelineno-1-30" name="__codelineno-1-30" href="#__codelineno-1-30"></a><span class="w"> </span><span class="nx">upvoteCountIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_public_media_upvotes&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="nx">table</span><span class="p">.</span><span class="nx">upvoteCount</span><span class="p">),</span>
</span><span id="__span-1-31"><a id="__codelineno-1-31" name="__codelineno-1-31" href="#__codelineno-1-31"></a><span class="w"> </span><span class="nx">isLockedIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_public_media_locked&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="nx">table</span><span class="p">.</span><span class="nx">isLocked</span><span class="p">),</span>
</span><span id="__span-1-32"><a id="__codelineno-1-32" name="__codelineno-1-32" href="#__codelineno-1-32"></a><span class="p">}));</span>
</span></code></pre></div>
<p><strong>Key Features:</strong></p>
<ul>
<li><strong>Denormalized counters</strong> — Fast sorting by popularity (no joins)</li>
<li><strong>Lock system</strong> — Admin can lock videos to prevent public access</li>
<li><strong>Category organization</strong> — Flexible categorization system</li>
<li><strong>Performance indexes</strong> — Optimized for sorting by views/upvotes</li>
</ul>
<hr />
<h3 id="upvotes-table">Upvotes Table<a class="headerlink" href="#upvotes-table" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-2-1"><a id="__codelineno-2-1" name="__codelineno-2-1" href="#__codelineno-2-1"></a><span class="k">export</span><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">upvotes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">pgTable</span><span class="p">(</span><span class="s1">&#39;upvotes&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-2-2"><a id="__codelineno-2-2" name="__codelineno-2-2" href="#__codelineno-2-2"></a><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">serial</span><span class="p">(</span><span class="s1">&#39;id&#39;</span><span class="p">).</span><span class="nx">primaryKey</span><span class="p">(),</span>
</span><span id="__span-2-3"><a id="__codelineno-2-3" name="__codelineno-2-3" href="#__codelineno-2-3"></a><span class="w"> </span><span class="nx">mediaId</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;media_id&#39;</span><span class="p">).</span><span class="nx">notNull</span><span class="p">(),</span>
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4" href="#__codelineno-2-4"></a><span class="w"> </span><span class="nx">sessionId</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;session_id&#39;</span><span class="p">).</span><span class="nx">notNull</span><span class="p">(),</span>
</span><span id="__span-2-5"><a id="__codelineno-2-5" name="__codelineno-2-5" href="#__codelineno-2-5"></a><span class="w"> </span><span class="nx">createdAt</span><span class="o">:</span><span class="w"> </span><span class="kt">timestamp</span><span class="p">(</span><span class="s1">&#39;created_at&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">withTimezone</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">}).</span><span class="nx">defaultNow</span><span class="p">(),</span>
</span><span id="__span-2-6"><a id="__codelineno-2-6" name="__codelineno-2-6" href="#__codelineno-2-6"></a><span class="p">},</span><span class="w"> </span><span class="p">(</span><span class="nx">table</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
</span><span id="__span-2-7"><a id="__codelineno-2-7" name="__codelineno-2-7" href="#__codelineno-2-7"></a><span class="w"> </span><span class="nx">uniqueVoteIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_upvotes_unique&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="nx">table</span><span class="p">.</span><span class="nx">mediaId</span><span class="p">,</span><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">sessionId</span><span class="p">),</span>
</span><span id="__span-2-8"><a id="__codelineno-2-8" name="__codelineno-2-8" href="#__codelineno-2-8"></a><span class="w"> </span><span class="nx">mediaIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_upvotes_media&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="nx">table</span><span class="p">.</span><span class="nx">mediaId</span><span class="p">),</span>
</span><span id="__span-2-9"><a id="__codelineno-2-9" name="__codelineno-2-9" href="#__codelineno-2-9"></a><span class="p">}));</span>
</span></code></pre></div>
<p><strong>Key Features:</strong></p>
<ul>
<li><strong>Session-based</strong> — No authentication required (anonymous upvoting)</li>
<li><strong>Unique constraint</strong> — One upvote per session per media item</li>
<li><strong>Denormalized</strong> — upvoteCount in publicMedia table updated via trigger or application logic</li>
</ul>
<hr />
<h3 id="video-reactions-table">Video Reactions Table<a class="headerlink" href="#video-reactions-table" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-3-1"><a id="__codelineno-3-1" name="__codelineno-3-1" href="#__codelineno-3-1"></a><span class="k">export</span><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">REACTION_TYPES</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s1">&#39;like&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;love&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;laugh&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;wow&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;sad&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;angry&#39;</span><span class="p">]</span><span class="w"> </span><span class="kr">as</span><span class="w"> </span><span class="kd">const</span><span class="p">;</span>
</span><span id="__span-3-2"><a id="__codelineno-3-2" name="__codelineno-3-2" href="#__codelineno-3-2"></a><span class="k">export</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="nx">ReactionType</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">typeof</span><span class="w"> </span><span class="nx">REACTION_TYPES</span><span class="p">[</span><span class="kt">number</span><span class="p">];</span>
</span><span id="__span-3-3"><a id="__codelineno-3-3" name="__codelineno-3-3" href="#__codelineno-3-3"></a>
</span><span id="__span-3-4"><a id="__codelineno-3-4" name="__codelineno-3-4" href="#__codelineno-3-4"></a><span class="k">export</span><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">videoReactions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">pgTable</span><span class="p">(</span><span class="s1">&#39;video_reactions&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-3-5"><a id="__codelineno-3-5" name="__codelineno-3-5" href="#__codelineno-3-5"></a><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">serial</span><span class="p">(</span><span class="s1">&#39;id&#39;</span><span class="p">).</span><span class="nx">primaryKey</span><span class="p">(),</span>
</span><span id="__span-3-6"><a id="__codelineno-3-6" name="__codelineno-3-6" href="#__codelineno-3-6"></a><span class="w"> </span><span class="nx">userId</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;user_id&#39;</span><span class="p">).</span><span class="nx">notNull</span><span class="p">(),</span>
</span><span id="__span-3-7"><a id="__codelineno-3-7" name="__codelineno-3-7" href="#__codelineno-3-7"></a><span class="w"> </span><span class="nx">mediaId</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;media_id&#39;</span><span class="p">).</span><span class="nx">notNull</span><span class="p">(),</span>
</span><span id="__span-3-8"><a id="__codelineno-3-8" name="__codelineno-3-8" href="#__codelineno-3-8"></a><span class="w"> </span><span class="nx">reactionType</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;reaction_type&#39;</span><span class="p">).</span><span class="nx">notNull</span><span class="p">(),</span>
</span><span id="__span-3-9"><a id="__codelineno-3-9" name="__codelineno-3-9" href="#__codelineno-3-9"></a><span class="w"> </span><span class="nx">videoTimestamp</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;video_timestamp&#39;</span><span class="p">).</span><span class="nx">notNull</span><span class="p">(),</span><span class="w"> </span><span class="c1">// seconds into video</span>
</span><span id="__span-3-10"><a id="__codelineno-3-10" name="__codelineno-3-10" href="#__codelineno-3-10"></a><span class="w"> </span><span class="nx">createdAt</span><span class="o">:</span><span class="w"> </span><span class="kt">timestamp</span><span class="p">(</span><span class="s1">&#39;created_at&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">withTimezone</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">}).</span><span class="nx">notNull</span><span class="p">(),</span>
</span><span id="__span-3-11"><a id="__codelineno-3-11" name="__codelineno-3-11" href="#__codelineno-3-11"></a><span class="p">},</span><span class="w"> </span><span class="p">(</span><span class="nx">table</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
</span><span id="__span-3-12"><a id="__codelineno-3-12" name="__codelineno-3-12" href="#__codelineno-3-12"></a><span class="w"> </span><span class="nx">userMediaTypeIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_video_reactions_user_media_type&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span>
</span><span id="__span-3-13"><a id="__codelineno-3-13" name="__codelineno-3-13" href="#__codelineno-3-13"></a><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">userId</span><span class="p">,</span><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">mediaId</span><span class="p">,</span><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">reactionType</span>
</span><span id="__span-3-14"><a id="__codelineno-3-14" name="__codelineno-3-14" href="#__codelineno-3-14"></a><span class="w"> </span><span class="p">),</span>
</span><span id="__span-3-15"><a id="__codelineno-3-15" name="__codelineno-3-15" href="#__codelineno-3-15"></a><span class="w"> </span><span class="nx">mediaTimestampIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_video_reactions_media_timestamp&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span>
</span><span id="__span-3-16"><a id="__codelineno-3-16" name="__codelineno-3-16" href="#__codelineno-3-16"></a><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">mediaId</span><span class="p">,</span><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">videoTimestamp</span>
</span><span id="__span-3-17"><a id="__codelineno-3-17" name="__codelineno-3-17" href="#__codelineno-3-17"></a><span class="w"> </span><span class="p">),</span>
</span><span id="__span-3-18"><a id="__codelineno-3-18" name="__codelineno-3-18" href="#__codelineno-3-18"></a><span class="w"> </span><span class="nx">mediaIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_video_reactions_media&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="nx">table</span><span class="p">.</span><span class="nx">mediaId</span><span class="p">),</span>
</span><span id="__span-3-19"><a id="__codelineno-3-19" name="__codelineno-3-19" href="#__codelineno-3-19"></a><span class="w"> </span><span class="nx">createdAtIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_video_reactions_created&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="nx">table</span><span class="p">.</span><span class="nx">createdAt</span><span class="p">),</span>
</span><span id="__span-3-20"><a id="__codelineno-3-20" name="__codelineno-3-20" href="#__codelineno-3-20"></a><span class="p">}));</span>
</span></code></pre></div>
<p><strong>Reaction Emojis:</strong></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Emoji</th>
<th>Label</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>like</code></td>
<td>👍</td>
<td>Like</td>
</tr>
<tr>
<td><code>love</code></td>
<td>❤️</td>
<td>Love</td>
</tr>
<tr>
<td><code>laugh</code></td>
<td>😂</td>
<td>Laugh</td>
</tr>
<tr>
<td><code>wow</code></td>
<td>😮</td>
<td>Wow</td>
</tr>
<tr>
<td><code>sad</code></td>
<td>😢</td>
<td>Sad</td>
</tr>
<tr>
<td><code>angry</code></td>
<td>😠</td>
<td>Angry</td>
</tr>
</tbody>
</table>
<p><strong>Key Features:</strong></p>
<ul>
<li><strong>Timestamped reactions</strong> — Mark specific moments in videos</li>
<li><strong>User-based</strong> — Requires authentication</li>
<li><strong>Timeline visualization</strong> — Can show reaction heatmap across video timeline</li>
</ul>
<hr />
<h3 id="jobs-table">Jobs Table<a class="headerlink" href="#jobs-table" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-4-1"><a id="__codelineno-4-1" name="__codelineno-4-1" href="#__codelineno-4-1"></a><span class="k">export</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="nx">ResourceCategory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;gpu_ai&#39;</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="s1">&#39;gpu_encode&#39;</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="s1">&#39;cpu&#39;</span><span class="p">;</span>
</span><span id="__span-4-2"><a id="__codelineno-4-2" name="__codelineno-4-2" href="#__codelineno-4-2"></a><span class="k">export</span><span class="w"> </span><span class="kr">type</span><span class="w"> </span><span class="nx">JobStatus</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;pending&#39;</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="s1">&#39;queued&#39;</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="s1">&#39;running&#39;</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="s1">&#39;completed&#39;</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="s1">&#39;failed&#39;</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="s1">&#39;cancelled&#39;</span><span class="p">;</span>
</span><span id="__span-4-3"><a id="__codelineno-4-3" name="__codelineno-4-3" href="#__codelineno-4-3"></a>
</span><span id="__span-4-4"><a id="__codelineno-4-4" name="__codelineno-4-4" href="#__codelineno-4-4"></a><span class="k">export</span><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">jobs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">pgTable</span><span class="p">(</span><span class="s1">&#39;jobs&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-4-5"><a id="__codelineno-4-5" name="__codelineno-4-5" href="#__codelineno-4-5"></a><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">serial</span><span class="p">(</span><span class="s1">&#39;id&#39;</span><span class="p">).</span><span class="nx">primaryKey</span><span class="p">(),</span>
</span><span id="__span-4-6"><a id="__codelineno-4-6" name="__codelineno-4-6" href="#__codelineno-4-6"></a><span class="w"> </span><span class="kr">type</span><span class="o">:</span><span class="w"> </span><span class="nx">text</span><span class="p">(</span><span class="s1">&#39;type&#39;</span><span class="p">).</span><span class="nx">notNull</span><span class="p">(),</span>
</span><span id="__span-4-7"><a id="__codelineno-4-7" name="__codelineno-4-7" href="#__codelineno-4-7"></a><span class="w"> </span><span class="nx">status</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;status&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">(</span><span class="s1">&#39;pending&#39;</span><span class="p">).</span><span class="nx">$type</span><span class="o">&lt;</span><span class="nx">JobStatus</span><span class="o">&gt;</span><span class="p">(),</span>
</span><span id="__span-4-8"><a id="__codelineno-4-8" name="__codelineno-4-8" href="#__codelineno-4-8"></a><span class="w"> </span><span class="nx">progress</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;progress&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">(</span><span class="mf">0</span><span class="p">),</span>
</span><span id="__span-4-9"><a id="__codelineno-4-9" name="__codelineno-4-9" href="#__codelineno-4-9"></a><span class="w"> </span><span class="nx">log</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;log&#39;</span><span class="p">),</span>
</span><span id="__span-4-10"><a id="__codelineno-4-10" name="__codelineno-4-10" href="#__codelineno-4-10"></a><span class="w"> </span><span class="nx">params</span><span class="o">:</span><span class="w"> </span><span class="kt">jsonb</span><span class="p">(</span><span class="s1">&#39;params&#39;</span><span class="p">).</span><span class="nx">$type</span><span class="o">&lt;</span><span class="nx">Record</span><span class="o">&lt;</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">unknown</span><span class="o">&gt;&gt;</span><span class="p">(),</span>
</span><span id="__span-4-11"><a id="__codelineno-4-11" name="__codelineno-4-11" href="#__codelineno-4-11"></a><span class="w"> </span><span class="nx">startedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">timestamp</span><span class="p">(</span><span class="s1">&#39;started_at&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">withTimezone</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">}),</span>
</span><span id="__span-4-12"><a id="__codelineno-4-12" name="__codelineno-4-12" href="#__codelineno-4-12"></a><span class="w"> </span><span class="nx">completedAt</span><span class="o">:</span><span class="w"> </span><span class="kt">timestamp</span><span class="p">(</span><span class="s1">&#39;completed_at&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">withTimezone</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">}),</span>
</span><span id="__span-4-13"><a id="__codelineno-4-13" name="__codelineno-4-13" href="#__codelineno-4-13"></a><span class="w"> </span><span class="nx">createdAt</span><span class="o">:</span><span class="w"> </span><span class="kt">timestamp</span><span class="p">(</span><span class="s1">&#39;created_at&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">withTimezone</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">}).</span><span class="nx">defaultNow</span><span class="p">(),</span>
</span><span id="__span-4-14"><a id="__codelineno-4-14" name="__codelineno-4-14" href="#__codelineno-4-14"></a>
</span><span id="__span-4-15"><a id="__codelineno-4-15" name="__codelineno-4-15" href="#__codelineno-4-15"></a><span class="w"> </span><span class="c1">// Queue management</span>
</span><span id="__span-4-16"><a id="__codelineno-4-16" name="__codelineno-4-16" href="#__codelineno-4-16"></a><span class="w"> </span><span class="nx">resourceCategory</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;resource_category&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">(</span><span class="s1">&#39;cpu&#39;</span><span class="p">).</span><span class="nx">$type</span><span class="o">&lt;</span><span class="nx">ResourceCategory</span><span class="o">&gt;</span><span class="p">(),</span>
</span><span id="__span-4-17"><a id="__codelineno-4-17" name="__codelineno-4-17" href="#__codelineno-4-17"></a><span class="w"> </span><span class="nx">vramRequired</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;vram_required&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">(</span><span class="mf">0</span><span class="p">),</span>
</span><span id="__span-4-18"><a id="__codelineno-4-18" name="__codelineno-4-18" href="#__codelineno-4-18"></a><span class="w"> </span><span class="nx">queuePosition</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;queue_position&#39;</span><span class="p">),</span>
</span><span id="__span-4-19"><a id="__codelineno-4-19" name="__codelineno-4-19" href="#__codelineno-4-19"></a><span class="w"> </span><span class="nx">waitingReason</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;waiting_reason&#39;</span><span class="p">),</span>
</span><span id="__span-4-20"><a id="__codelineno-4-20" name="__codelineno-4-20" href="#__codelineno-4-20"></a><span class="w"> </span><span class="nx">priority</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;priority&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">(</span><span class="mf">5</span><span class="p">),</span>
</span><span id="__span-4-21"><a id="__codelineno-4-21" name="__codelineno-4-21" href="#__codelineno-4-21"></a>
</span><span id="__span-4-22"><a id="__codelineno-4-22" name="__codelineno-4-22" href="#__codelineno-4-22"></a><span class="w"> </span><span class="c1">// Pipeline integration</span>
</span><span id="__span-4-23"><a id="__codelineno-4-23" name="__codelineno-4-23" href="#__codelineno-4-23"></a><span class="w"> </span><span class="nx">pipelineId</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;pipeline_id&#39;</span><span class="p">),</span>
</span><span id="__span-4-24"><a id="__codelineno-4-24" name="__codelineno-4-24" href="#__codelineno-4-24"></a><span class="w"> </span><span class="nx">pipelineStepId</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;pipeline_step_id&#39;</span><span class="p">),</span>
</span><span id="__span-4-25"><a id="__codelineno-4-25" name="__codelineno-4-25" href="#__codelineno-4-25"></a><span class="p">},</span><span class="w"> </span><span class="p">(</span><span class="nx">table</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">({</span>
</span><span id="__span-4-26"><a id="__codelineno-4-26" name="__codelineno-4-26" href="#__codelineno-4-26"></a><span class="w"> </span><span class="nx">queueIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_jobs_queue&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="nx">table</span><span class="p">.</span><span class="nx">status</span><span class="p">,</span><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">priority</span><span class="p">,</span><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">createdAt</span><span class="p">),</span>
</span><span id="__span-4-27"><a id="__codelineno-4-27" name="__codelineno-4-27" href="#__codelineno-4-27"></a><span class="w"> </span><span class="nx">resourceIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_jobs_resource&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="nx">table</span><span class="p">.</span><span class="nx">resourceCategory</span><span class="p">,</span><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">status</span><span class="p">),</span>
</span><span id="__span-4-28"><a id="__codelineno-4-28" name="__codelineno-4-28" href="#__codelineno-4-28"></a><span class="w"> </span><span class="nx">pipelineIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_jobs_pipeline&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="nx">table</span><span class="p">.</span><span class="nx">pipelineId</span><span class="p">),</span>
</span><span id="__span-4-29"><a id="__codelineno-4-29" name="__codelineno-4-29" href="#__codelineno-4-29"></a><span class="p">}));</span>
</span></code></pre></div>
<p><strong>Job Types:</strong></p>
<ul>
<li><code>compilation</code> — Multi-video compilation</li>
<li><code>scan</code>, <code>public_scan</code> — Video library scanning</li>
<li><code>organize</code>, <code>organize_studio</code> — Automatic organization</li>
<li><code>reencode_streaming</code> — Transcode for web streaming</li>
<li><code>compile_random</code>, <code>compile_quad</code>, <code>compile_quad_horizontal</code>, etc. — Compilation variants</li>
<li><code>generate_gif</code>, <code>fetch</code>, <code>digest</code>, <code>clip_generate</code>, <code>highlight_generate</code> — Content generation</li>
<li><code>tag_generation</code>, <code>scene_extract</code>, <code>clip_extract_only</code>, <code>auto_organize_publish</code> — AI-powered tasks</li>
</ul>
<p><strong>Resource Categories:</strong></p>
<ul>
<li><strong><code>gpu_ai</code></strong> — AI/ML tasks (scene detection, tagging, etc.) — High VRAM</li>
<li><strong><code>gpu_encode</code></strong> — Video encoding/transcoding — Medium VRAM</li>
<li><strong><code>cpu</code></strong> — General processing — No GPU required</li>
</ul>
<hr />
<h3 id="compilations-table">Compilations Table<a class="headerlink" href="#compilations-table" title="Permanent link">&para;</a></h3>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-5-1"><a id="__codelineno-5-1" name="__codelineno-5-1" href="#__codelineno-5-1"></a><span class="k">export</span><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">compilations</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">pgTable</span><span class="p">(</span><span class="s1">&#39;compilations&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-5-2"><a id="__codelineno-5-2" name="__codelineno-5-2" href="#__codelineno-5-2"></a><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">serial</span><span class="p">(</span><span class="s1">&#39;id&#39;</span><span class="p">).</span><span class="nx">primaryKey</span><span class="p">(),</span>
</span><span id="__span-5-3"><a id="__codelineno-5-3" name="__codelineno-5-3" href="#__codelineno-5-3"></a><span class="w"> </span><span class="nx">filename</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;filename&#39;</span><span class="p">).</span><span class="nx">notNull</span><span class="p">(),</span>
</span><span id="__span-5-4"><a id="__codelineno-5-4" name="__codelineno-5-4" href="#__codelineno-5-4"></a><span class="w"> </span><span class="nx">path</span><span class="o">:</span><span class="w"> </span><span class="kt">text</span><span class="p">(</span><span class="s1">&#39;path&#39;</span><span class="p">),</span>
</span><span id="__span-5-5"><a id="__codelineno-5-5" name="__codelineno-5-5" href="#__codelineno-5-5"></a><span class="w"> </span><span class="nx">durationSeconds</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;duration_seconds&#39;</span><span class="p">),</span>
</span><span id="__span-5-6"><a id="__codelineno-5-6" name="__codelineno-5-6" href="#__codelineno-5-6"></a><span class="w"> </span><span class="nx">videoIds</span><span class="o">:</span><span class="w"> </span><span class="kt">jsonb</span><span class="p">(</span><span class="s1">&#39;video_ids&#39;</span><span class="p">).</span><span class="nx">$type</span><span class="o">&lt;</span><span class="kt">number</span><span class="p">[]</span><span class="o">&gt;</span><span class="p">(),</span>
</span><span id="__span-5-7"><a id="__codelineno-5-7" name="__codelineno-5-7" href="#__codelineno-5-7"></a><span class="w"> </span><span class="nx">settings</span><span class="o">:</span><span class="w"> </span><span class="kt">jsonb</span><span class="p">(</span><span class="s1">&#39;settings&#39;</span><span class="p">).</span><span class="nx">$type</span><span class="o">&lt;</span><span class="nx">Record</span><span class="o">&lt;</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">unknown</span><span class="o">&gt;&gt;</span><span class="p">(),</span>
</span><span id="__span-5-8"><a id="__codelineno-5-8" name="__codelineno-5-8" href="#__codelineno-5-8"></a><span class="w"> </span><span class="nx">createdAt</span><span class="o">:</span><span class="w"> </span><span class="kt">timestamp</span><span class="p">(</span><span class="s1">&#39;created_at&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">withTimezone</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">}).</span><span class="nx">defaultNow</span><span class="p">(),</span>
</span><span id="__span-5-9"><a id="__codelineno-5-9" name="__codelineno-5-9" href="#__codelineno-5-9"></a><span class="p">});</span>
</span></code></pre></div>
<p><strong>Key Features:</strong></p>
<ul>
<li><strong>Multi-video tracking</strong> — Stores array of source video IDs</li>
<li><strong>Settings preservation</strong> — Stores compilation parameters (layout, transitions, etc.)</li>
</ul>
<hr />
<h2 id="api-endpoints">API Endpoints<a class="headerlink" href="#api-endpoints" title="Permanent link">&para;</a></h2>
<h3 id="admin-endpoints-videos">Admin Endpoints (Videos)<a class="headerlink" href="#admin-endpoints-videos" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>Path</th>
<th>Auth</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>GET</td>
<td><code>/api/videos</code></td>
<td>Admin roles</td>
<td>List videos with pagination</td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/videos/:id</code></td>
<td>Admin roles</td>
<td>Get single video</td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/videos/health</code></td>
<td>None</td>
<td>Health check</td>
</tr>
</tbody>
</table>
<p><strong>Admin Roles:</strong> Requires admin role via Fastify auth middleware</p>
<h3 id="public-media-endpoints">Public Media Endpoints<a class="headerlink" href="#public-media-endpoints" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>Path</th>
<th>Auth</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>GET</td>
<td><code>/api/media/public</code></td>
<td>None</td>
<td>List shared media (paginated, filterable, sorted)</td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/media/public/:id</code></td>
<td>None</td>
<td>Get single media + increment view count</td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/media/public/:id/upvote</code></td>
<td>None</td>
<td>Upvote media (session-based)</td>
</tr>
<tr>
<td>DELETE</td>
<td><code>/api/media/public/:id/upvote</code></td>
<td>None</td>
<td>Remove upvote</td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/media/public/:id/finish</code></td>
<td>None</td>
<td>Mark video as finished</td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/media/public/:id/watch-time</code></td>
<td>None</td>
<td>Track watch time</td>
</tr>
</tbody>
</table>
<h3 id="reaction-endpoints">Reaction Endpoints<a class="headerlink" href="#reaction-endpoints" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>Path</th>
<th>Auth</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>POST</td>
<td><code>/api/reactions</code></td>
<td>Required</td>
<td>Add reaction to video</td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/reactions</code></td>
<td>None</td>
<td>Get reactions (filterable by mediaId/userId)</td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/reactions/config</code></td>
<td>None</td>
<td>Get available reaction types</td>
</tr>
</tbody>
</table>
<h3 id="comment-endpoints">Comment Endpoints<a class="headerlink" href="#comment-endpoints" title="Permanent link">&para;</a></h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>Path</th>
<th>Auth</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>POST</td>
<td><code>/api/media/comments</code></td>
<td>Optional</td>
<td>Add comment (auth optional, session-based)</td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/media/comments</code></td>
<td>None</td>
<td>List comments for media</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="endpoint-details">Endpoint Details<a class="headerlink" href="#endpoint-details" title="Permanent link">&para;</a></h2>
<h3 id="get-apivideos">GET /api/videos<a class="headerlink" href="#get-apivideos" title="Permanent link">&para;</a></h3>
<p>List videos with pagination and search (admin only).</p>
<p><strong>Query Parameters:</strong></p>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>limit</td>
<td>number</td>
<td>50</td>
<td>Results per page (max 100)</td>
</tr>
<tr>
<td>offset</td>
<td>number</td>
<td>0</td>
<td>Skip N results</td>
</tr>
<tr>
<td>search</td>
<td>string</td>
<td>-</td>
<td>Search title (case-insensitive)</td>
</tr>
</tbody>
</table>
<p><strong>Example Request:</strong></p>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-6-1"><a id="__codelineno-6-1" name="__codelineno-6-1" href="#__codelineno-6-1"></a>curl<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Authorization: Bearer &lt;token&gt;&quot;</span><span class="w"> </span><span class="se">\</span>
</span><span id="__span-6-2"><a id="__codelineno-6-2" name="__codelineno-6-2" href="#__codelineno-6-2"></a><span class="w"> </span><span class="s2">&quot;http://localhost:4100/api/videos?limit=20&amp;offset=0&amp;search=demo&quot;</span>
</span></code></pre></div>
<p><strong>Response (200 OK):</strong></p>
<div class="language-json highlight"><pre><span></span><code><span id="__span-7-1"><a id="__codelineno-7-1" name="__codelineno-7-1" href="#__codelineno-7-1"></a><span class="p">{</span>
</span><span id="__span-7-2"><a id="__codelineno-7-2" name="__codelineno-7-2" href="#__codelineno-7-2"></a><span class="w"> </span><span class="nt">&quot;videos&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
</span><span id="__span-7-3"><a id="__codelineno-7-3" name="__codelineno-7-3" href="#__codelineno-7-3"></a><span class="w"> </span><span class="p">{</span>
</span><span id="__span-7-4"><a id="__codelineno-7-4" name="__codelineno-7-4" href="#__codelineno-7-4"></a><span class="w"> </span><span class="nt">&quot;id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">123</span><span class="p">,</span>
</span><span id="__span-7-5"><a id="__codelineno-7-5" name="__codelineno-7-5" href="#__codelineno-7-5"></a><span class="w"> </span><span class="nt">&quot;title&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Demo Video&quot;</span><span class="p">,</span>
</span><span id="__span-7-6"><a id="__codelineno-7-6" name="__codelineno-7-6" href="#__codelineno-7-6"></a><span class="w"> </span><span class="nt">&quot;filename&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;demo-video.mp4&quot;</span><span class="p">,</span>
</span><span id="__span-7-7"><a id="__codelineno-7-7" name="__codelineno-7-7" href="#__codelineno-7-7"></a><span class="w"> </span><span class="nt">&quot;duration&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">300</span><span class="p">,</span>
</span><span id="__span-7-8"><a id="__codelineno-7-8" name="__codelineno-7-8" href="#__codelineno-7-8"></a><span class="w"> </span><span class="nt">&quot;fileSize&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">52428800</span><span class="p">,</span>
</span><span id="__span-7-9"><a id="__codelineno-7-9" name="__codelineno-7-9" href="#__codelineno-7-9"></a><span class="w"> </span><span class="nt">&quot;width&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1920</span><span class="p">,</span>
</span><span id="__span-7-10"><a id="__codelineno-7-10" name="__codelineno-7-10" href="#__codelineno-7-10"></a><span class="w"> </span><span class="nt">&quot;height&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1080</span><span class="p">,</span>
</span><span id="__span-7-11"><a id="__codelineno-7-11" name="__codelineno-7-11" href="#__codelineno-7-11"></a><span class="w"> </span><span class="nt">&quot;createdAt&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;2026-02-01T12:00:00.000Z&quot;</span><span class="p">,</span>
</span><span id="__span-7-12"><a id="__codelineno-7-12" name="__codelineno-7-12" href="#__codelineno-7-12"></a><span class="w"> </span><span class="nt">&quot;updatedAt&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;2026-02-11T14:30:00.000Z&quot;</span>
</span><span id="__span-7-13"><a id="__codelineno-7-13" name="__codelineno-7-13" href="#__codelineno-7-13"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-7-14"><a id="__codelineno-7-14" name="__codelineno-7-14" href="#__codelineno-7-14"></a><span class="w"> </span><span class="p">],</span>
</span><span id="__span-7-15"><a id="__codelineno-7-15" name="__codelineno-7-15" href="#__codelineno-7-15"></a><span class="w"> </span><span class="nt">&quot;total&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">45</span><span class="p">,</span>
</span><span id="__span-7-16"><a id="__codelineno-7-16" name="__codelineno-7-16" href="#__codelineno-7-16"></a><span class="w"> </span><span class="nt">&quot;limit&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">20</span><span class="p">,</span>
</span><span id="__span-7-17"><a id="__codelineno-7-17" name="__codelineno-7-17" href="#__codelineno-7-17"></a><span class="w"> </span><span class="nt">&quot;offset&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span>
</span><span id="__span-7-18"><a id="__codelineno-7-18" name="__codelineno-7-18" href="#__codelineno-7-18"></a><span class="p">}</span>
</span></code></pre></div>
<hr />
<h3 id="get-apimediapublic">GET /api/media/public<a class="headerlink" href="#get-apimediapublic" title="Permanent link">&para;</a></h3>
<p>List shared media with pagination, filtering, and sorting (no auth required).</p>
<p><strong>Query Parameters:</strong></p>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>category</td>
<td>string</td>
<td>-</td>
<td>Filter by category</td>
</tr>
<tr>
<td>search</td>
<td>string</td>
<td>-</td>
<td>Search filename/path</td>
</tr>
<tr>
<td>sort</td>
<td>enum</td>
<td><code>recent</code></td>
<td>Sort: <code>recent</code>, <code>popular</code>, <code>most_viewed</code></td>
</tr>
<tr>
<td>orientation</td>
<td>string</td>
<td>-</td>
<td>Filter by orientation</td>
</tr>
<tr>
<td>limit</td>
<td>number</td>
<td>24</td>
<td>Results per page (max 100)</td>
</tr>
<tr>
<td>offset</td>
<td>number</td>
<td>0</td>
<td>Skip N results</td>
</tr>
</tbody>
</table>
<p><strong>Example Request:</strong></p>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-8-1"><a id="__codelineno-8-1" name="__codelineno-8-1" href="#__codelineno-8-1"></a>curl<span class="w"> </span><span class="s2">&quot;http://localhost:4100/api/media/public?category=highlights&amp;sort=popular&amp;limit=12&quot;</span>
</span></code></pre></div>
<p><strong>Response (200 OK):</strong></p>
<div class="language-json highlight"><pre><span></span><code><span id="__span-9-1"><a id="__codelineno-9-1" name="__codelineno-9-1" href="#__codelineno-9-1"></a><span class="p">{</span>
</span><span id="__span-9-2"><a id="__codelineno-9-2" name="__codelineno-9-2" href="#__codelineno-9-2"></a><span class="w"> </span><span class="nt">&quot;videos&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
</span><span id="__span-9-3"><a id="__codelineno-9-3" name="__codelineno-9-3" href="#__codelineno-9-3"></a><span class="w"> </span><span class="p">{</span>
</span><span id="__span-9-4"><a id="__codelineno-9-4" name="__codelineno-9-4" href="#__codelineno-9-4"></a><span class="w"> </span><span class="nt">&quot;id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">456</span><span class="p">,</span>
</span><span id="__span-9-5"><a id="__codelineno-9-5" name="__codelineno-9-5" href="#__codelineno-9-5"></a><span class="w"> </span><span class="nt">&quot;filename&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;highlight-2024-01-15.mp4&quot;</span><span class="p">,</span>
</span><span id="__span-9-6"><a id="__codelineno-9-6" name="__codelineno-9-6" href="#__codelineno-9-6"></a><span class="w"> </span><span class="nt">&quot;category&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;highlights&quot;</span><span class="p">,</span>
</span><span id="__span-9-7"><a id="__codelineno-9-7" name="__codelineno-9-7" href="#__codelineno-9-7"></a><span class="w"> </span><span class="nt">&quot;durationSeconds&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">45</span><span class="p">,</span>
</span><span id="__span-9-8"><a id="__codelineno-9-8" name="__codelineno-9-8" href="#__codelineno-9-8"></a><span class="w"> </span><span class="nt">&quot;quality&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;1080p&quot;</span><span class="p">,</span>
</span><span id="__span-9-9"><a id="__codelineno-9-9" name="__codelineno-9-9" href="#__codelineno-9-9"></a><span class="w"> </span><span class="nt">&quot;orientation&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;landscape&quot;</span><span class="p">,</span>
</span><span id="__span-9-10"><a id="__codelineno-9-10" name="__codelineno-9-10" href="#__codelineno-9-10"></a><span class="w"> </span><span class="nt">&quot;thumbnailPath&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;/thumbnails/highlight-2024-01-15.jpg&quot;</span><span class="p">,</span>
</span><span id="__span-9-11"><a id="__codelineno-9-11" name="__codelineno-9-11" href="#__codelineno-9-11"></a><span class="w"> </span><span class="nt">&quot;viewCount&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1250</span><span class="p">,</span>
</span><span id="__span-9-12"><a id="__codelineno-9-12" name="__codelineno-9-12" href="#__codelineno-9-12"></a><span class="w"> </span><span class="nt">&quot;upvoteCount&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">89</span><span class="p">,</span>
</span><span id="__span-9-13"><a id="__codelineno-9-13" name="__codelineno-9-13" href="#__codelineno-9-13"></a><span class="w"> </span><span class="nt">&quot;commentCount&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">12</span><span class="p">,</span>
</span><span id="__span-9-14"><a id="__codelineno-9-14" name="__codelineno-9-14" href="#__codelineno-9-14"></a><span class="w"> </span><span class="nt">&quot;isLocked&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
</span><span id="__span-9-15"><a id="__codelineno-9-15" name="__codelineno-9-15" href="#__codelineno-9-15"></a><span class="w"> </span><span class="nt">&quot;createdAt&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;2026-01-15T10:00:00.000Z&quot;</span>
</span><span id="__span-9-16"><a id="__codelineno-9-16" name="__codelineno-9-16" href="#__codelineno-9-16"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-9-17"><a id="__codelineno-9-17" name="__codelineno-9-17" href="#__codelineno-9-17"></a><span class="w"> </span><span class="p">],</span>
</span><span id="__span-9-18"><a id="__codelineno-9-18" name="__codelineno-9-18" href="#__codelineno-9-18"></a><span class="w"> </span><span class="nt">&quot;pagination&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-9-19"><a id="__codelineno-9-19" name="__codelineno-9-19" href="#__codelineno-9-19"></a><span class="w"> </span><span class="nt">&quot;total&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">145</span><span class="p">,</span>
</span><span id="__span-9-20"><a id="__codelineno-9-20" name="__codelineno-9-20" href="#__codelineno-9-20"></a><span class="w"> </span><span class="nt">&quot;limit&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">12</span><span class="p">,</span>
</span><span id="__span-9-21"><a id="__codelineno-9-21" name="__codelineno-9-21" href="#__codelineno-9-21"></a><span class="w"> </span><span class="nt">&quot;offset&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span>
</span><span id="__span-9-22"><a id="__codelineno-9-22" name="__codelineno-9-22" href="#__codelineno-9-22"></a><span class="w"> </span><span class="nt">&quot;hasMore&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span>
</span><span id="__span-9-23"><a id="__codelineno-9-23" name="__codelineno-9-23" href="#__codelineno-9-23"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-9-24"><a id="__codelineno-9-24" name="__codelineno-9-24" href="#__codelineno-9-24"></a><span class="p">}</span>
</span></code></pre></div>
<p><strong>Sort Modes:</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-10-1"><a id="__codelineno-10-1" name="__codelineno-10-1" href="#__codelineno-10-1"></a><span class="k">switch</span><span class="w"> </span><span class="p">(</span><span class="nx">sort</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-10-2"><a id="__codelineno-10-2" name="__codelineno-10-2" href="#__codelineno-10-2"></a><span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">&#39;popular&#39;</span><span class="o">:</span>
</span><span id="__span-10-3"><a id="__codelineno-10-3" name="__codelineno-10-3" href="#__codelineno-10-3"></a><span class="w"> </span><span class="nx">orderBy</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="nx">desc</span><span class="p">(</span><span class="nx">publicMedia</span><span class="p">.</span><span class="nx">upvoteCount</span><span class="p">),</span><span class="w"> </span><span class="nx">desc</span><span class="p">(</span><span class="nx">publicMedia</span><span class="p">.</span><span class="nx">createdAt</span><span class="p">)];</span>
</span><span id="__span-10-4"><a id="__codelineno-10-4" name="__codelineno-10-4" href="#__codelineno-10-4"></a><span class="w"> </span><span class="k">break</span><span class="p">;</span>
</span><span id="__span-10-5"><a id="__codelineno-10-5" name="__codelineno-10-5" href="#__codelineno-10-5"></a><span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">&#39;most_viewed&#39;</span><span class="o">:</span>
</span><span id="__span-10-6"><a id="__codelineno-10-6" name="__codelineno-10-6" href="#__codelineno-10-6"></a><span class="w"> </span><span class="nx">orderBy</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="nx">desc</span><span class="p">(</span><span class="nx">publicMedia</span><span class="p">.</span><span class="nx">viewCount</span><span class="p">),</span><span class="w"> </span><span class="nx">desc</span><span class="p">(</span><span class="nx">publicMedia</span><span class="p">.</span><span class="nx">createdAt</span><span class="p">)];</span>
</span><span id="__span-10-7"><a id="__codelineno-10-7" name="__codelineno-10-7" href="#__codelineno-10-7"></a><span class="w"> </span><span class="k">break</span><span class="p">;</span>
</span><span id="__span-10-8"><a id="__codelineno-10-8" name="__codelineno-10-8" href="#__codelineno-10-8"></a><span class="w"> </span><span class="k">case</span><span class="w"> </span><span class="s1">&#39;recent&#39;</span><span class="o">:</span>
</span><span id="__span-10-9"><a id="__codelineno-10-9" name="__codelineno-10-9" href="#__codelineno-10-9"></a><span class="w"> </span><span class="nx">default</span><span class="o">:</span>
</span><span id="__span-10-10"><a id="__codelineno-10-10" name="__codelineno-10-10" href="#__codelineno-10-10"></a><span class="w"> </span><span class="kt">orderBy</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="nx">desc</span><span class="p">(</span><span class="nx">publicMedia</span><span class="p">.</span><span class="nx">createdAt</span><span class="p">)];</span>
</span><span id="__span-10-11"><a id="__codelineno-10-11" name="__codelineno-10-11" href="#__codelineno-10-11"></a><span class="w"> </span><span class="k">break</span><span class="p">;</span>
</span><span id="__span-10-12"><a id="__codelineno-10-12" name="__codelineno-10-12" href="#__codelineno-10-12"></a><span class="p">}</span>
</span></code></pre></div>
<hr />
<h3 id="get-apimediapublicid">GET /api/media/public/:id<a class="headerlink" href="#get-apimediapublicid" title="Permanent link">&para;</a></h3>
<p>Get single media details and increment view count (no auth required).</p>
<p><strong>Path Parameters:</strong></p>
<ul>
<li><code>id</code> (number): Media ID</li>
</ul>
<p><strong>Example Request:</strong></p>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-11-1"><a id="__codelineno-11-1" name="__codelineno-11-1" href="#__codelineno-11-1"></a>curl<span class="w"> </span><span class="s2">&quot;http://localhost:4100/api/media/public/456&quot;</span>
</span></code></pre></div>
<p><strong>Response (200 OK):</strong></p>
<div class="language-json highlight"><pre><span></span><code><span id="__span-12-1"><a id="__codelineno-12-1" name="__codelineno-12-1" href="#__codelineno-12-1"></a><span class="p">{</span>
</span><span id="__span-12-2"><a id="__codelineno-12-2" name="__codelineno-12-2" href="#__codelineno-12-2"></a><span class="w"> </span><span class="nt">&quot;id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">456</span><span class="p">,</span>
</span><span id="__span-12-3"><a id="__codelineno-12-3" name="__codelineno-12-3" href="#__codelineno-12-3"></a><span class="w"> </span><span class="nt">&quot;path&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;/public/highlights/highlight-2024-01-15.mp4&quot;</span><span class="p">,</span>
</span><span id="__span-12-4"><a id="__codelineno-12-4" name="__codelineno-12-4" href="#__codelineno-12-4"></a><span class="w"> </span><span class="nt">&quot;filename&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;highlight-2024-01-15.mp4&quot;</span><span class="p">,</span>
</span><span id="__span-12-5"><a id="__codelineno-12-5" name="__codelineno-12-5" href="#__codelineno-12-5"></a><span class="w"> </span><span class="nt">&quot;category&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;highlights&quot;</span><span class="p">,</span>
</span><span id="__span-12-6"><a id="__codelineno-12-6" name="__codelineno-12-6" href="#__codelineno-12-6"></a><span class="w"> </span><span class="nt">&quot;durationSeconds&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">45</span><span class="p">,</span>
</span><span id="__span-12-7"><a id="__codelineno-12-7" name="__codelineno-12-7" href="#__codelineno-12-7"></a><span class="w"> </span><span class="nt">&quot;quality&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;1080p&quot;</span><span class="p">,</span>
</span><span id="__span-12-8"><a id="__codelineno-12-8" name="__codelineno-12-8" href="#__codelineno-12-8"></a><span class="w"> </span><span class="nt">&quot;orientation&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;landscape&quot;</span><span class="p">,</span>
</span><span id="__span-12-9"><a id="__codelineno-12-9" name="__codelineno-12-9" href="#__codelineno-12-9"></a><span class="w"> </span><span class="nt">&quot;thumbnailPath&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;/thumbnails/highlight-2024-01-15.jpg&quot;</span><span class="p">,</span>
</span><span id="__span-12-10"><a id="__codelineno-12-10" name="__codelineno-12-10" href="#__codelineno-12-10"></a><span class="w"> </span><span class="nt">&quot;fileSize&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">15728640</span><span class="p">,</span>
</span><span id="__span-12-11"><a id="__codelineno-12-11" name="__codelineno-12-11" href="#__codelineno-12-11"></a><span class="w"> </span><span class="nt">&quot;viewCount&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1251</span><span class="p">,</span>
</span><span id="__span-12-12"><a id="__codelineno-12-12" name="__codelineno-12-12" href="#__codelineno-12-12"></a><span class="w"> </span><span class="nt">&quot;upvoteCount&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">89</span><span class="p">,</span>
</span><span id="__span-12-13"><a id="__codelineno-12-13" name="__codelineno-12-13" href="#__codelineno-12-13"></a><span class="w"> </span><span class="nt">&quot;commentCount&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">12</span><span class="p">,</span>
</span><span id="__span-12-14"><a id="__codelineno-12-14" name="__codelineno-12-14" href="#__codelineno-12-14"></a><span class="w"> </span><span class="nt">&quot;finishCount&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">420</span><span class="p">,</span>
</span><span id="__span-12-15"><a id="__codelineno-12-15" name="__codelineno-12-15" href="#__codelineno-12-15"></a><span class="w"> </span><span class="nt">&quot;totalWatchTime&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">48600</span><span class="p">,</span>
</span><span id="__span-12-16"><a id="__codelineno-12-16" name="__codelineno-12-16" href="#__codelineno-12-16"></a><span class="w"> </span><span class="nt">&quot;isLocked&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
</span><span id="__span-12-17"><a id="__codelineno-12-17" name="__codelineno-12-17" href="#__codelineno-12-17"></a><span class="w"> </span><span class="nt">&quot;createdAt&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;2026-01-15T10:00:00.000Z&quot;</span><span class="p">,</span>
</span><span id="__span-12-18"><a id="__codelineno-12-18" name="__codelineno-12-18" href="#__codelineno-12-18"></a><span class="w"> </span><span class="nt">&quot;updatedAt&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;2026-02-11T15:45:00.000Z&quot;</span>
</span><span id="__span-12-19"><a id="__codelineno-12-19" name="__codelineno-12-19" href="#__codelineno-12-19"></a><span class="p">}</span>
</span></code></pre></div>
<p><strong>Side Effect:</strong></p>
<p>View count is incremented <strong>fire-and-forget</strong> (does not block response):</p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-13-1"><a id="__codelineno-13-1" name="__codelineno-13-1" href="#__codelineno-13-1"></a><span class="c1">// Increment view count (fire and forget)</span>
</span><span id="__span-13-2"><a id="__codelineno-13-2" name="__codelineno-13-2" href="#__codelineno-13-2"></a><span class="nx">db</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">publicMedia</span><span class="p">)</span>
</span><span id="__span-13-3"><a id="__codelineno-13-3" name="__codelineno-13-3" href="#__codelineno-13-3"></a><span class="w"> </span><span class="p">.</span><span class="nx">set</span><span class="p">({</span><span class="w"> </span><span class="nx">viewCount</span><span class="o">:</span><span class="w"> </span><span class="kt">sql</span><span class="sb">`</span><span class="si">${</span><span class="nx">publicMedia</span><span class="p">.</span><span class="nx">viewCount</span><span class="si">}</span><span class="sb"> + 1`</span><span class="w"> </span><span class="p">})</span>
</span><span id="__span-13-4"><a id="__codelineno-13-4" name="__codelineno-13-4" href="#__codelineno-13-4"></a><span class="w"> </span><span class="p">.</span><span class="nx">where</span><span class="p">(</span><span class="nx">eq</span><span class="p">(</span><span class="nx">publicMedia</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="nx">mediaId</span><span class="p">))</span>
</span><span id="__span-13-5"><a id="__codelineno-13-5" name="__codelineno-13-5" href="#__codelineno-13-5"></a><span class="w"> </span><span class="p">.</span><span class="nx">execute</span><span class="p">()</span>
</span><span id="__span-13-6"><a id="__codelineno-13-6" name="__codelineno-13-6" href="#__codelineno-13-6"></a><span class="w"> </span><span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">err</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">logger</span><span class="p">.</span><span class="nx">error</span><span class="p">({</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="s1">&#39;Failed to increment view count&#39;</span><span class="p">));</span>
</span></code></pre></div>
<hr />
<h3 id="post-apimediapublicidupvote">POST /api/media/public/:id/upvote<a class="headerlink" href="#post-apimediapublicidupvote" title="Permanent link">&para;</a></h3>
<p>Upvote media (session-based, no auth required).</p>
<p><strong>Path Parameters:</strong></p>
<ul>
<li><code>id</code> (number): Media ID</li>
</ul>
<p><strong>Request Body:</strong></p>
<div class="language-json highlight"><pre><span></span><code><span id="__span-14-1"><a id="__codelineno-14-1" name="__codelineno-14-1" href="#__codelineno-14-1"></a><span class="p">{</span>
</span><span id="__span-14-2"><a id="__codelineno-14-2" name="__codelineno-14-2" href="#__codelineno-14-2"></a><span class="w"> </span><span class="nt">&quot;sessionId&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;sess_abc123def456&quot;</span>
</span><span id="__span-14-3"><a id="__codelineno-14-3" name="__codelineno-14-3" href="#__codelineno-14-3"></a><span class="p">}</span>
</span></code></pre></div>
<p><strong>Response (200 OK):</strong></p>
<div class="language-json highlight"><pre><span></span><code><span id="__span-15-1"><a id="__codelineno-15-1" name="__codelineno-15-1" href="#__codelineno-15-1"></a><span class="p">{</span>
</span><span id="__span-15-2"><a id="__codelineno-15-2" name="__codelineno-15-2" href="#__codelineno-15-2"></a><span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
</span><span id="__span-15-3"><a id="__codelineno-15-3" name="__codelineno-15-3" href="#__codelineno-15-3"></a><span class="w"> </span><span class="nt">&quot;upvoted&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
</span><span id="__span-15-4"><a id="__codelineno-15-4" name="__codelineno-15-4" href="#__codelineno-15-4"></a><span class="w"> </span><span class="nt">&quot;upvoteCount&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">90</span>
</span><span id="__span-15-5"><a id="__codelineno-15-5" name="__codelineno-15-5" href="#__codelineno-15-5"></a><span class="p">}</span>
</span></code></pre></div>
<p><strong>Behavior:</strong></p>
<ul>
<li><strong>Idempotent</strong> — If already upvoted, returns existing upvote</li>
<li><strong>Denormalized counter</strong> — Updates <code>publicMedia.upvoteCount</code> atomically</li>
<li><strong>Session-based</strong> — No authentication required</li>
</ul>
<p><strong>Duplicate Prevention:</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-16-1"><a id="__codelineno-16-1" name="__codelineno-16-1" href="#__codelineno-16-1"></a><span class="c1">// Check if already upvoted</span>
</span><span id="__span-16-2"><a id="__codelineno-16-2" name="__codelineno-16-2" href="#__codelineno-16-2"></a><span class="kd">const</span><span class="w"> </span><span class="p">[</span><span class="nx">existingVote</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">db</span>
</span><span id="__span-16-3"><a id="__codelineno-16-3" name="__codelineno-16-3" href="#__codelineno-16-3"></a><span class="w"> </span><span class="p">.</span><span class="nx">select</span><span class="p">()</span>
</span><span id="__span-16-4"><a id="__codelineno-16-4" name="__codelineno-16-4" href="#__codelineno-16-4"></a><span class="w"> </span><span class="p">.</span><span class="kr">from</span><span class="p">(</span><span class="nx">upvotes</span><span class="p">)</span>
</span><span id="__span-16-5"><a id="__codelineno-16-5" name="__codelineno-16-5" href="#__codelineno-16-5"></a><span class="w"> </span><span class="p">.</span><span class="nx">where</span><span class="p">(</span><span class="nx">and</span><span class="p">(</span>
</span><span id="__span-16-6"><a id="__codelineno-16-6" name="__codelineno-16-6" href="#__codelineno-16-6"></a><span class="w"> </span><span class="nx">eq</span><span class="p">(</span><span class="nx">upvotes</span><span class="p">.</span><span class="nx">mediaId</span><span class="p">,</span><span class="w"> </span><span class="nx">mediaId</span><span class="p">),</span>
</span><span id="__span-16-7"><a id="__codelineno-16-7" name="__codelineno-16-7" href="#__codelineno-16-7"></a><span class="w"> </span><span class="nx">eq</span><span class="p">(</span><span class="nx">upvotes</span><span class="p">.</span><span class="nx">sessionId</span><span class="p">,</span><span class="w"> </span><span class="nx">sessionId</span><span class="p">)</span>
</span><span id="__span-16-8"><a id="__codelineno-16-8" name="__codelineno-16-8" href="#__codelineno-16-8"></a><span class="w"> </span><span class="p">));</span>
</span><span id="__span-16-9"><a id="__codelineno-16-9" name="__codelineno-16-9" href="#__codelineno-16-9"></a>
</span><span id="__span-16-10"><a id="__codelineno-16-10" name="__codelineno-16-10" href="#__codelineno-16-10"></a><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">existingVote</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-16-11"><a id="__codelineno-16-11" name="__codelineno-16-11" href="#__codelineno-16-11"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">reply</span><span class="p">.</span><span class="nx">send</span><span class="p">({</span><span class="w"> </span><span class="nx">success</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="p">,</span><span class="w"> </span><span class="nx">upvoted</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="p">,</span><span class="w"> </span><span class="nx">upvoteCount</span><span class="o">:</span><span class="w"> </span><span class="kt">media.upvoteCount</span><span class="w"> </span><span class="p">});</span>
</span><span id="__span-16-12"><a id="__codelineno-16-12" name="__codelineno-16-12" href="#__codelineno-16-12"></a><span class="p">}</span>
</span></code></pre></div>
<hr />
<h3 id="delete-apimediapublicidupvote">DELETE /api/media/public/:id/upvote<a class="headerlink" href="#delete-apimediapublicidupvote" title="Permanent link">&para;</a></h3>
<p>Remove upvote (session-based).</p>
<p><strong>Path Parameters:</strong></p>
<ul>
<li><code>id</code> (number): Media ID</li>
</ul>
<p><strong>Query Parameters:</strong></p>
<ul>
<li><code>sessionId</code> (string): Session ID</li>
</ul>
<p><strong>Response (200 OK):</strong></p>
<div class="language-json highlight"><pre><span></span><code><span id="__span-17-1"><a id="__codelineno-17-1" name="__codelineno-17-1" href="#__codelineno-17-1"></a><span class="p">{</span>
</span><span id="__span-17-2"><a id="__codelineno-17-2" name="__codelineno-17-2" href="#__codelineno-17-2"></a><span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
</span><span id="__span-17-3"><a id="__codelineno-17-3" name="__codelineno-17-3" href="#__codelineno-17-3"></a><span class="w"> </span><span class="nt">&quot;upvoted&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
</span><span id="__span-17-4"><a id="__codelineno-17-4" name="__codelineno-17-4" href="#__codelineno-17-4"></a><span class="w"> </span><span class="nt">&quot;upvoteCount&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">89</span>
</span><span id="__span-17-5"><a id="__codelineno-17-5" name="__codelineno-17-5" href="#__codelineno-17-5"></a><span class="p">}</span>
</span></code></pre></div>
<hr />
<h3 id="post-apireactions">POST /api/reactions<a class="headerlink" href="#post-apireactions" title="Permanent link">&para;</a></h3>
<p>Add reaction to video (authenticated users only).</p>
<p><strong>Request Body:</strong></p>
<div class="language-json highlight"><pre><span></span><code><span id="__span-18-1"><a id="__codelineno-18-1" name="__codelineno-18-1" href="#__codelineno-18-1"></a><span class="p">{</span>
</span><span id="__span-18-2"><a id="__codelineno-18-2" name="__codelineno-18-2" href="#__codelineno-18-2"></a><span class="w"> </span><span class="nt">&quot;mediaId&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">456</span><span class="p">,</span>
</span><span id="__span-18-3"><a id="__codelineno-18-3" name="__codelineno-18-3" href="#__codelineno-18-3"></a><span class="w"> </span><span class="nt">&quot;reactionType&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;love&quot;</span><span class="p">,</span>
</span><span id="__span-18-4"><a id="__codelineno-18-4" name="__codelineno-18-4" href="#__codelineno-18-4"></a><span class="w"> </span><span class="nt">&quot;videoTimestamp&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">27</span>
</span><span id="__span-18-5"><a id="__codelineno-18-5" name="__codelineno-18-5" href="#__codelineno-18-5"></a><span class="p">}</span>
</span></code></pre></div>
<p><strong>Response (200 OK):</strong></p>
<div class="language-json highlight"><pre><span></span><code><span id="__span-19-1"><a id="__codelineno-19-1" name="__codelineno-19-1" href="#__codelineno-19-1"></a><span class="p">{</span>
</span><span id="__span-19-2"><a id="__codelineno-19-2" name="__codelineno-19-2" href="#__codelineno-19-2"></a><span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
</span><span id="__span-19-3"><a id="__codelineno-19-3" name="__codelineno-19-3" href="#__codelineno-19-3"></a><span class="w"> </span><span class="nt">&quot;reaction&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-19-4"><a id="__codelineno-19-4" name="__codelineno-19-4" href="#__codelineno-19-4"></a><span class="w"> </span><span class="nt">&quot;id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">789</span><span class="p">,</span>
</span><span id="__span-19-5"><a id="__codelineno-19-5" name="__codelineno-19-5" href="#__codelineno-19-5"></a><span class="w"> </span><span class="nt">&quot;mediaId&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">456</span><span class="p">,</span>
</span><span id="__span-19-6"><a id="__codelineno-19-6" name="__codelineno-19-6" href="#__codelineno-19-6"></a><span class="w"> </span><span class="nt">&quot;userId&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">123</span><span class="p">,</span>
</span><span id="__span-19-7"><a id="__codelineno-19-7" name="__codelineno-19-7" href="#__codelineno-19-7"></a><span class="w"> </span><span class="nt">&quot;reactionType&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;love&quot;</span><span class="p">,</span>
</span><span id="__span-19-8"><a id="__codelineno-19-8" name="__codelineno-19-8" href="#__codelineno-19-8"></a><span class="w"> </span><span class="nt">&quot;videoTimestamp&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">27</span><span class="p">,</span>
</span><span id="__span-19-9"><a id="__codelineno-19-9" name="__codelineno-19-9" href="#__codelineno-19-9"></a><span class="w"> </span><span class="nt">&quot;emoji&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;❤️&quot;</span><span class="p">,</span>
</span><span id="__span-19-10"><a id="__codelineno-19-10" name="__codelineno-19-10" href="#__codelineno-19-10"></a><span class="w"> </span><span class="nt">&quot;formattedTime&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;0:27&quot;</span><span class="p">,</span>
</span><span id="__span-19-11"><a id="__codelineno-19-11" name="__codelineno-19-11" href="#__codelineno-19-11"></a><span class="w"> </span><span class="nt">&quot;createdAt&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;2026-02-11T15:50:00.000Z&quot;</span>
</span><span id="__span-19-12"><a id="__codelineno-19-12" name="__codelineno-19-12" href="#__codelineno-19-12"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-19-13"><a id="__codelineno-19-13" name="__codelineno-19-13" href="#__codelineno-19-13"></a><span class="p">}</span>
</span></code></pre></div>
<p><strong>Validation:</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-20-1"><a id="__codelineno-20-1" name="__codelineno-20-1" href="#__codelineno-20-1"></a><span class="kd">const</span><span class="w"> </span><span class="nx">REACTION_EMOJIS</span><span class="o">:</span><span class="w"> </span><span class="kt">Record</span><span class="o">&lt;</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="kt">string</span><span class="o">&gt;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-20-2"><a id="__codelineno-20-2" name="__codelineno-20-2" href="#__codelineno-20-2"></a><span class="w"> </span><span class="nx">like</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;👍&#39;</span><span class="p">,</span>
</span><span id="__span-20-3"><a id="__codelineno-20-3" name="__codelineno-20-3" href="#__codelineno-20-3"></a><span class="w"> </span><span class="nx">love</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;❤️&#39;</span><span class="p">,</span>
</span><span id="__span-20-4"><a id="__codelineno-20-4" name="__codelineno-20-4" href="#__codelineno-20-4"></a><span class="w"> </span><span class="nx">laugh</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;😂&#39;</span><span class="p">,</span>
</span><span id="__span-20-5"><a id="__codelineno-20-5" name="__codelineno-20-5" href="#__codelineno-20-5"></a><span class="w"> </span><span class="nx">wow</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;😮&#39;</span><span class="p">,</span>
</span><span id="__span-20-6"><a id="__codelineno-20-6" name="__codelineno-20-6" href="#__codelineno-20-6"></a><span class="w"> </span><span class="nx">sad</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;😢&#39;</span><span class="p">,</span>
</span><span id="__span-20-7"><a id="__codelineno-20-7" name="__codelineno-20-7" href="#__codelineno-20-7"></a><span class="w"> </span><span class="nx">angry</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;😠&#39;</span><span class="p">,</span>
</span><span id="__span-20-8"><a id="__codelineno-20-8" name="__codelineno-20-8" href="#__codelineno-20-8"></a><span class="p">};</span>
</span><span id="__span-20-9"><a id="__codelineno-20-9" name="__codelineno-20-9" href="#__codelineno-20-9"></a>
</span><span id="__span-20-10"><a id="__codelineno-20-10" name="__codelineno-20-10" href="#__codelineno-20-10"></a><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">REACTION_EMOJIS</span><span class="p">[</span><span class="nx">reactionType</span><span class="p">])</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-20-11"><a id="__codelineno-20-11" name="__codelineno-20-11" href="#__codelineno-20-11"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">fastify</span><span class="p">.</span><span class="nx">httpErrors</span><span class="p">.</span><span class="nx">badRequest</span><span class="p">(</span><span class="s1">&#39;Invalid reaction type&#39;</span><span class="p">);</span>
</span><span id="__span-20-12"><a id="__codelineno-20-12" name="__codelineno-20-12" href="#__codelineno-20-12"></a><span class="p">}</span>
</span></code></pre></div>
<p><strong>Time Formatting:</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-21-1"><a id="__codelineno-21-1" name="__codelineno-21-1" href="#__codelineno-21-1"></a><span class="kd">function</span><span class="w"> </span><span class="nx">formatVideoTime</span><span class="p">(</span><span class="nx">seconds</span><span class="o">:</span><span class="w"> </span><span class="kt">number</span><span class="p">)</span><span class="o">:</span><span class="w"> </span><span class="kt">string</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-21-2"><a id="__codelineno-21-2" name="__codelineno-21-2" href="#__codelineno-21-2"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">h</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nx">seconds</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mf">3600</span><span class="p">);</span>
</span><span id="__span-21-3"><a id="__codelineno-21-3" name="__codelineno-21-3" href="#__codelineno-21-3"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">m</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">((</span><span class="nx">seconds</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mf">3600</span><span class="p">)</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mf">60</span><span class="p">);</span>
</span><span id="__span-21-4"><a id="__codelineno-21-4" name="__codelineno-21-4" href="#__codelineno-21-4"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">s</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">seconds</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mf">60</span><span class="p">;</span>
</span><span id="__span-21-5"><a id="__codelineno-21-5" name="__codelineno-21-5" href="#__codelineno-21-5"></a>
</span><span id="__span-21-6"><a id="__codelineno-21-6" name="__codelineno-21-6" href="#__codelineno-21-6"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">h</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-21-7"><a id="__codelineno-21-7" name="__codelineno-21-7" href="#__codelineno-21-7"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="sb">`</span><span class="si">${</span><span class="nx">h</span><span class="si">}</span><span class="sb">:</span><span class="si">${</span><span class="nx">m</span><span class="p">.</span><span class="nx">toString</span><span class="p">().</span><span class="nx">padStart</span><span class="p">(</span><span class="mf">2</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;0&#39;</span><span class="p">)</span><span class="si">}</span><span class="sb">:</span><span class="si">${</span><span class="nx">s</span><span class="p">.</span><span class="nx">toString</span><span class="p">().</span><span class="nx">padStart</span><span class="p">(</span><span class="mf">2</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;0&#39;</span><span class="p">)</span><span class="si">}</span><span class="sb">`</span><span class="p">;</span>
</span><span id="__span-21-8"><a id="__codelineno-21-8" name="__codelineno-21-8" href="#__codelineno-21-8"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-21-9"><a id="__codelineno-21-9" name="__codelineno-21-9" href="#__codelineno-21-9"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="sb">`</span><span class="si">${</span><span class="nx">m</span><span class="si">}</span><span class="sb">:</span><span class="si">${</span><span class="nx">s</span><span class="p">.</span><span class="nx">toString</span><span class="p">().</span><span class="nx">padStart</span><span class="p">(</span><span class="mf">2</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;0&#39;</span><span class="p">)</span><span class="si">}</span><span class="sb">`</span><span class="p">;</span>
</span><span id="__span-21-10"><a id="__codelineno-21-10" name="__codelineno-21-10" href="#__codelineno-21-10"></a><span class="p">}</span>
</span><span id="__span-21-11"><a id="__codelineno-21-11" name="__codelineno-21-11" href="#__codelineno-21-11"></a>
</span><span id="__span-21-12"><a id="__codelineno-21-12" name="__codelineno-21-12" href="#__codelineno-21-12"></a><span class="c1">// Examples:</span>
</span><span id="__span-21-13"><a id="__codelineno-21-13" name="__codelineno-21-13" href="#__codelineno-21-13"></a><span class="c1">// 27 → &quot;0:27&quot;</span>
</span><span id="__span-21-14"><a id="__codelineno-21-14" name="__codelineno-21-14" href="#__codelineno-21-14"></a><span class="c1">// 90 → &quot;1:30&quot;</span>
</span><span id="__span-21-15"><a id="__codelineno-21-15" name="__codelineno-21-15" href="#__codelineno-21-15"></a><span class="c1">// 3661 → &quot;1:01:01&quot;</span>
</span></code></pre></div>
<hr />
<h3 id="get-apireactions">GET /api/reactions<a class="headerlink" href="#get-apireactions" title="Permanent link">&para;</a></h3>
<p>Get reactions (filterable by mediaId/userId).</p>
<p><strong>Query Parameters:</strong></p>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>mediaId</td>
<td>number</td>
<td>Filter by media ID</td>
</tr>
<tr>
<td>userId</td>
<td>string</td>
<td>Filter by user ID</td>
</tr>
<tr>
<td>limit</td>
<td>number</td>
<td>Results per page (default 50)</td>
</tr>
</tbody>
</table>
<p><strong>Example Request:</strong></p>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-22-1"><a id="__codelineno-22-1" name="__codelineno-22-1" href="#__codelineno-22-1"></a>curl<span class="w"> </span><span class="s2">&quot;http://localhost:4100/api/reactions?mediaId=456&amp;limit=20&quot;</span>
</span></code></pre></div>
<p><strong>Response (200 OK):</strong></p>
<div class="language-json highlight"><pre><span></span><code><span id="__span-23-1"><a id="__codelineno-23-1" name="__codelineno-23-1" href="#__codelineno-23-1"></a><span class="p">{</span>
</span><span id="__span-23-2"><a id="__codelineno-23-2" name="__codelineno-23-2" href="#__codelineno-23-2"></a><span class="w"> </span><span class="nt">&quot;reactions&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
</span><span id="__span-23-3"><a id="__codelineno-23-3" name="__codelineno-23-3" href="#__codelineno-23-3"></a><span class="w"> </span><span class="p">{</span>
</span><span id="__span-23-4"><a id="__codelineno-23-4" name="__codelineno-23-4" href="#__codelineno-23-4"></a><span class="w"> </span><span class="nt">&quot;id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">789</span><span class="p">,</span>
</span><span id="__span-23-5"><a id="__codelineno-23-5" name="__codelineno-23-5" href="#__codelineno-23-5"></a><span class="w"> </span><span class="nt">&quot;mediaId&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">456</span><span class="p">,</span>
</span><span id="__span-23-6"><a id="__codelineno-23-6" name="__codelineno-23-6" href="#__codelineno-23-6"></a><span class="w"> </span><span class="nt">&quot;userId&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">123</span><span class="p">,</span>
</span><span id="__span-23-7"><a id="__codelineno-23-7" name="__codelineno-23-7" href="#__codelineno-23-7"></a><span class="w"> </span><span class="nt">&quot;reactionType&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;love&quot;</span><span class="p">,</span>
</span><span id="__span-23-8"><a id="__codelineno-23-8" name="__codelineno-23-8" href="#__codelineno-23-8"></a><span class="w"> </span><span class="nt">&quot;videoTimestamp&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">27</span><span class="p">,</span>
</span><span id="__span-23-9"><a id="__codelineno-23-9" name="__codelineno-23-9" href="#__codelineno-23-9"></a><span class="w"> </span><span class="nt">&quot;emoji&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;❤️&quot;</span><span class="p">,</span>
</span><span id="__span-23-10"><a id="__codelineno-23-10" name="__codelineno-23-10" href="#__codelineno-23-10"></a><span class="w"> </span><span class="nt">&quot;formattedTime&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;0:27&quot;</span><span class="p">,</span>
</span><span id="__span-23-11"><a id="__codelineno-23-11" name="__codelineno-23-11" href="#__codelineno-23-11"></a><span class="w"> </span><span class="nt">&quot;createdAt&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;2026-02-11T15:50:00.000Z&quot;</span>
</span><span id="__span-23-12"><a id="__codelineno-23-12" name="__codelineno-23-12" href="#__codelineno-23-12"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-23-13"><a id="__codelineno-23-13" name="__codelineno-23-13" href="#__codelineno-23-13"></a><span class="w"> </span><span class="p">{</span>
</span><span id="__span-23-14"><a id="__codelineno-23-14" name="__codelineno-23-14" href="#__codelineno-23-14"></a><span class="w"> </span><span class="nt">&quot;id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">790</span><span class="p">,</span>
</span><span id="__span-23-15"><a id="__codelineno-23-15" name="__codelineno-23-15" href="#__codelineno-23-15"></a><span class="w"> </span><span class="nt">&quot;mediaId&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">456</span><span class="p">,</span>
</span><span id="__span-23-16"><a id="__codelineno-23-16" name="__codelineno-23-16" href="#__codelineno-23-16"></a><span class="w"> </span><span class="nt">&quot;userId&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">124</span><span class="p">,</span>
</span><span id="__span-23-17"><a id="__codelineno-23-17" name="__codelineno-23-17" href="#__codelineno-23-17"></a><span class="w"> </span><span class="nt">&quot;reactionType&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;laugh&quot;</span><span class="p">,</span>
</span><span id="__span-23-18"><a id="__codelineno-23-18" name="__codelineno-23-18" href="#__codelineno-23-18"></a><span class="w"> </span><span class="nt">&quot;videoTimestamp&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">42</span><span class="p">,</span>
</span><span id="__span-23-19"><a id="__codelineno-23-19" name="__codelineno-23-19" href="#__codelineno-23-19"></a><span class="w"> </span><span class="nt">&quot;emoji&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;😂&quot;</span><span class="p">,</span>
</span><span id="__span-23-20"><a id="__codelineno-23-20" name="__codelineno-23-20" href="#__codelineno-23-20"></a><span class="w"> </span><span class="nt">&quot;formattedTime&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;0:42&quot;</span><span class="p">,</span>
</span><span id="__span-23-21"><a id="__codelineno-23-21" name="__codelineno-23-21" href="#__codelineno-23-21"></a><span class="w"> </span><span class="nt">&quot;createdAt&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;2026-02-11T15:51:00.000Z&quot;</span>
</span><span id="__span-23-22"><a id="__codelineno-23-22" name="__codelineno-23-22" href="#__codelineno-23-22"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-23-23"><a id="__codelineno-23-23" name="__codelineno-23-23" href="#__codelineno-23-23"></a><span class="w"> </span><span class="p">]</span>
</span><span id="__span-23-24"><a id="__codelineno-23-24" name="__codelineno-23-24" href="#__codelineno-23-24"></a><span class="p">}</span>
</span></code></pre></div>
<hr />
<h3 id="get-apireactionsconfig">GET /api/reactions/config<a class="headerlink" href="#get-apireactionsconfig" title="Permanent link">&para;</a></h3>
<p>Get available reaction types.</p>
<p><strong>Example Request:</strong></p>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-24-1"><a id="__codelineno-24-1" name="__codelineno-24-1" href="#__codelineno-24-1"></a>curl<span class="w"> </span><span class="s2">&quot;http://localhost:4100/api/reactions/config&quot;</span>
</span></code></pre></div>
<p><strong>Response (200 OK):</strong></p>
<div class="language-json highlight"><pre><span></span><code><span id="__span-25-1"><a id="__codelineno-25-1" name="__codelineno-25-1" href="#__codelineno-25-1"></a><span class="p">{</span>
</span><span id="__span-25-2"><a id="__codelineno-25-2" name="__codelineno-25-2" href="#__codelineno-25-2"></a><span class="w"> </span><span class="nt">&quot;reactions&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
</span><span id="__span-25-3"><a id="__codelineno-25-3" name="__codelineno-25-3" href="#__codelineno-25-3"></a><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nt">&quot;type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;like&quot;</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;emoji&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;👍&quot;</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;label&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Like&quot;</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-25-4"><a id="__codelineno-25-4" name="__codelineno-25-4" href="#__codelineno-25-4"></a><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nt">&quot;type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;love&quot;</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;emoji&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;❤️&quot;</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;label&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Love&quot;</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-25-5"><a id="__codelineno-25-5" name="__codelineno-25-5" href="#__codelineno-25-5"></a><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nt">&quot;type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;laugh&quot;</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;emoji&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;😂&quot;</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;label&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Laugh&quot;</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-25-6"><a id="__codelineno-25-6" name="__codelineno-25-6" href="#__codelineno-25-6"></a><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nt">&quot;type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;wow&quot;</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;emoji&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;😮&quot;</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;label&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Wow&quot;</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-25-7"><a id="__codelineno-25-7" name="__codelineno-25-7" href="#__codelineno-25-7"></a><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nt">&quot;type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;sad&quot;</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;emoji&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;😢&quot;</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;label&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Sad&quot;</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-25-8"><a id="__codelineno-25-8" name="__codelineno-25-8" href="#__codelineno-25-8"></a><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nt">&quot;type&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;angry&quot;</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;emoji&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;😠&quot;</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;label&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Angry&quot;</span><span class="w"> </span><span class="p">}</span>
</span><span id="__span-25-9"><a id="__codelineno-25-9" name="__codelineno-25-9" href="#__codelineno-25-9"></a><span class="w"> </span><span class="p">]</span>
</span><span id="__span-25-10"><a id="__codelineno-25-10" name="__codelineno-25-10" href="#__codelineno-25-10"></a><span class="p">}</span>
</span></code></pre></div>
<hr />
<h2 id="fastify-vs-express-differences">Fastify vs Express Differences<a class="headerlink" href="#fastify-vs-express-differences" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Feature</th>
<th>Express API (port 4000)</th>
<th>Fastify Media API (port 4100)</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Framework</strong></td>
<td>Express 5</td>
<td>Fastify</td>
</tr>
<tr>
<td><strong>ORM</strong></td>
<td>Prisma</td>
<td>Drizzle</td>
</tr>
<tr>
<td><strong>Schema Validation</strong></td>
<td>Zod + middleware</td>
<td>Fastify built-in</td>
</tr>
<tr>
<td><strong>Auth Middleware</strong></td>
<td><code>authenticate</code>, <code>requireRole</code></td>
<td><code>authenticate</code>, <code>requireAdminRole</code>, <code>optionalAuth</code></td>
</tr>
<tr>
<td><strong>Error Handling</strong></td>
<td><code>AppError</code> class + error handler middleware</td>
<td><code>fastify.httpErrors</code> + decorators</td>
</tr>
<tr>
<td><strong>Route Registration</strong></td>
<td><code>router.get(...)</code></td>
<td><code>fastify.register(routes, { prefix })</code></td>
</tr>
<tr>
<td><strong>Request Handler</strong></td>
<td><code>(req, res, next) =&gt; {}</code></td>
<td><code>async (request, reply) =&gt; {}</code></td>
</tr>
<tr>
<td><strong>Database Client</strong></td>
<td><code>import { prisma }</code></td>
<td><code>import { db }</code></td>
</tr>
<tr>
<td><strong>Query Builder</strong></td>
<td>Prisma fluent API</td>
<td>Drizzle query builder</td>
</tr>
</tbody>
</table>
<h3 id="code-pattern-comparison">Code Pattern Comparison<a class="headerlink" href="#code-pattern-comparison" title="Permanent link">&para;</a></h3>
<p><strong>Express (Prisma):</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-26-1"><a id="__codelineno-26-1" name="__codelineno-26-1" href="#__codelineno-26-1"></a><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">Router</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;express&#39;</span><span class="p">;</span>
</span><span id="__span-26-2"><a id="__codelineno-26-2" name="__codelineno-26-2" href="#__codelineno-26-2"></a><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">prisma</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;../../config/database&#39;</span><span class="p">;</span>
</span><span id="__span-26-3"><a id="__codelineno-26-3" name="__codelineno-26-3" href="#__codelineno-26-3"></a><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">authenticate</span><span class="p">,</span><span class="w"> </span><span class="nx">requireRole</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;../../middleware/auth.middleware&#39;</span><span class="p">;</span>
</span><span id="__span-26-4"><a id="__codelineno-26-4" name="__codelineno-26-4" href="#__codelineno-26-4"></a>
</span><span id="__span-26-5"><a id="__codelineno-26-5" name="__codelineno-26-5" href="#__codelineno-26-5"></a><span class="kd">const</span><span class="w"> </span><span class="nx">router</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">Router</span><span class="p">();</span>
</span><span id="__span-26-6"><a id="__codelineno-26-6" name="__codelineno-26-6" href="#__codelineno-26-6"></a>
</span><span id="__span-26-7"><a id="__codelineno-26-7" name="__codelineno-26-7" href="#__codelineno-26-7"></a><span class="nx">router</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;/&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">authenticate</span><span class="p">,</span><span class="w"> </span><span class="nx">requireRole</span><span class="p">(</span><span class="s1">&#39;ADMIN&#39;</span><span class="p">),</span><span class="w"> </span><span class="k">async</span><span class="w"> </span><span class="p">(</span><span class="nx">req</span><span class="p">,</span><span class="w"> </span><span class="nx">res</span><span class="p">,</span><span class="w"> </span><span class="nx">next</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-26-8"><a id="__codelineno-26-8" name="__codelineno-26-8" href="#__codelineno-26-8"></a><span class="w"> </span><span class="k">try</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-26-9"><a id="__codelineno-26-9" name="__codelineno-26-9" href="#__codelineno-26-9"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">users</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">prisma</span><span class="p">.</span><span class="nx">user</span><span class="p">.</span><span class="nx">findMany</span><span class="p">({</span>
</span><span id="__span-26-10"><a id="__codelineno-26-10" name="__codelineno-26-10" href="#__codelineno-26-10"></a><span class="w"> </span><span class="nx">where</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">role</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;ADMIN&#39;</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-26-11"><a id="__codelineno-26-11" name="__codelineno-26-11" href="#__codelineno-26-11"></a><span class="w"> </span><span class="nx">select</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="p">,</span><span class="w"> </span><span class="nx">email</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-26-12"><a id="__codelineno-26-12" name="__codelineno-26-12" href="#__codelineno-26-12"></a><span class="w"> </span><span class="p">});</span>
</span><span id="__span-26-13"><a id="__codelineno-26-13" name="__codelineno-26-13" href="#__codelineno-26-13"></a><span class="w"> </span><span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">users</span><span class="p">);</span>
</span><span id="__span-26-14"><a id="__codelineno-26-14" name="__codelineno-26-14" href="#__codelineno-26-14"></a><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">catch</span><span class="w"> </span><span class="p">(</span><span class="nx">err</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-26-15"><a id="__codelineno-26-15" name="__codelineno-26-15" href="#__codelineno-26-15"></a><span class="w"> </span><span class="nx">next</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
</span><span id="__span-26-16"><a id="__codelineno-26-16" name="__codelineno-26-16" href="#__codelineno-26-16"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-26-17"><a id="__codelineno-26-17" name="__codelineno-26-17" href="#__codelineno-26-17"></a><span class="p">});</span>
</span><span id="__span-26-18"><a id="__codelineno-26-18" name="__codelineno-26-18" href="#__codelineno-26-18"></a>
</span><span id="__span-26-19"><a id="__codelineno-26-19" name="__codelineno-26-19" href="#__codelineno-26-19"></a><span class="k">export</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="nx">router</span><span class="p">;</span>
</span></code></pre></div>
<p><strong>Fastify (Drizzle):</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-27-1"><a id="__codelineno-27-1" name="__codelineno-27-1" href="#__codelineno-27-1"></a><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">FastifyInstance</span><span class="p">,</span><span class="w"> </span><span class="nx">FastifyRequest</span><span class="p">,</span><span class="w"> </span><span class="nx">FastifyReply</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;fastify&#39;</span><span class="p">;</span>
</span><span id="__span-27-2"><a id="__codelineno-27-2" name="__codelineno-27-2" href="#__codelineno-27-2"></a><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">db</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;../db&#39;</span><span class="p">;</span>
</span><span id="__span-27-3"><a id="__codelineno-27-3" name="__codelineno-27-3" href="#__codelineno-27-3"></a><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">users</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;../db/schema&#39;</span><span class="p">;</span>
</span><span id="__span-27-4"><a id="__codelineno-27-4" name="__codelineno-27-4" href="#__codelineno-27-4"></a><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">eq</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;drizzle-orm&#39;</span><span class="p">;</span>
</span><span id="__span-27-5"><a id="__codelineno-27-5" name="__codelineno-27-5" href="#__codelineno-27-5"></a><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">requireAdminRole</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s1">&#39;../middleware/auth&#39;</span><span class="p">;</span>
</span><span id="__span-27-6"><a id="__codelineno-27-6" name="__codelineno-27-6" href="#__codelineno-27-6"></a>
</span><span id="__span-27-7"><a id="__codelineno-27-7" name="__codelineno-27-7" href="#__codelineno-27-7"></a><span class="k">export</span><span class="w"> </span><span class="k">async</span><span class="w"> </span><span class="kd">function</span><span class="w"> </span><span class="nx">usersRoutes</span><span class="p">(</span><span class="nx">fastify</span><span class="o">:</span><span class="w"> </span><span class="kt">FastifyInstance</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-27-8"><a id="__codelineno-27-8" name="__codelineno-27-8" href="#__codelineno-27-8"></a><span class="w"> </span><span class="nx">fastify</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span>
</span><span id="__span-27-9"><a id="__codelineno-27-9" name="__codelineno-27-9" href="#__codelineno-27-9"></a><span class="w"> </span><span class="s1">&#39;/&#39;</span><span class="p">,</span>
</span><span id="__span-27-10"><a id="__codelineno-27-10" name="__codelineno-27-10" href="#__codelineno-27-10"></a><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">preHandler</span><span class="o">:</span><span class="w"> </span><span class="kt">requireAdminRole</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-27-11"><a id="__codelineno-27-11" name="__codelineno-27-11" href="#__codelineno-27-11"></a><span class="w"> </span><span class="k">async</span><span class="w"> </span><span class="p">(</span><span class="nx">request</span><span class="o">:</span><span class="w"> </span><span class="kt">FastifyRequest</span><span class="p">,</span><span class="w"> </span><span class="nx">reply</span><span class="o">:</span><span class="w"> </span><span class="kt">FastifyReply</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-27-12"><a id="__codelineno-27-12" name="__codelineno-27-12" href="#__codelineno-27-12"></a><span class="w"> </span><span class="kd">const</span><span class="w"> </span><span class="nx">results</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">db</span>
</span><span id="__span-27-13"><a id="__codelineno-27-13" name="__codelineno-27-13" href="#__codelineno-27-13"></a><span class="w"> </span><span class="p">.</span><span class="nx">select</span><span class="p">({</span><span class="w"> </span><span class="nx">id</span><span class="o">:</span><span class="w"> </span><span class="kt">users.id</span><span class="p">,</span><span class="w"> </span><span class="nx">email</span><span class="o">:</span><span class="w"> </span><span class="kt">users.email</span><span class="w"> </span><span class="p">})</span>
</span><span id="__span-27-14"><a id="__codelineno-27-14" name="__codelineno-27-14" href="#__codelineno-27-14"></a><span class="w"> </span><span class="p">.</span><span class="kr">from</span><span class="p">(</span><span class="nx">users</span><span class="p">)</span>
</span><span id="__span-27-15"><a id="__codelineno-27-15" name="__codelineno-27-15" href="#__codelineno-27-15"></a><span class="w"> </span><span class="p">.</span><span class="nx">where</span><span class="p">(</span><span class="nx">eq</span><span class="p">(</span><span class="nx">users</span><span class="p">.</span><span class="nx">role</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;ADMIN&#39;</span><span class="p">));</span>
</span><span id="__span-27-16"><a id="__codelineno-27-16" name="__codelineno-27-16" href="#__codelineno-27-16"></a>
</span><span id="__span-27-17"><a id="__codelineno-27-17" name="__codelineno-27-17" href="#__codelineno-27-17"></a><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="nx">reply</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">results</span><span class="p">);</span>
</span><span id="__span-27-18"><a id="__codelineno-27-18" name="__codelineno-27-18" href="#__codelineno-27-18"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-27-19"><a id="__codelineno-27-19" name="__codelineno-27-19" href="#__codelineno-27-19"></a><span class="w"> </span><span class="p">);</span>
</span><span id="__span-27-20"><a id="__codelineno-27-20" name="__codelineno-27-20" href="#__codelineno-27-20"></a><span class="p">}</span>
</span></code></pre></div>
<hr />
<h2 id="frontend-integration">Frontend Integration<a class="headerlink" href="#frontend-integration" title="Permanent link">&para;</a></h2>
<p>The Media module integrates with multiple frontend pages:</p>
<h3 id="admin-pages">Admin Pages<a class="headerlink" href="#admin-pages" title="Permanent link">&para;</a></h3>
<ul>
<li><strong>LibraryPage</strong> (<code>admin/src/pages/media/LibraryPage.tsx</code>)</li>
<li>Video grid with thumbnails</li>
<li>Filter by directory type</li>
<li>Search by filename</li>
<li>
<p>Bulk operations (lock, unlock, delete)</p>
</li>
<li>
<p><strong>SharedMediaPage</strong> (<code>admin/src/pages/media/SharedMediaPage.tsx</code>)</p>
</li>
<li>Public gallery admin</li>
<li>Category management</li>
<li>Lock/unlock controls</li>
<li>
<p>Engagement metrics display</p>
</li>
<li>
<p><strong>MediaJobsPage</strong> (<code>admin/src/pages/media/MediaJobsPage.tsx</code>)</p>
</li>
<li>Job queue monitoring</li>
<li>Job status tracking (pending, queued, running, completed, failed)</li>
<li>Progress visualization</li>
<li>Resource category filtering</li>
</ul>
<h3 id="public-pages">Public Pages<a class="headerlink" href="#public-pages" title="Permanent link">&para;</a></h3>
<ul>
<li><strong>MediaGalleryPage</strong> (<code>admin/src/pages/public/MediaGalleryPage.tsx</code>)</li>
<li>Public video gallery</li>
<li>Category filtering</li>
<li>Sort by recent/popular/most viewed</li>
<li>Upvote functionality (session-based)</li>
<li>
<p>View count display</p>
</li>
<li>
<p><strong>MediaViewerPage</strong> (<code>admin/src/pages/public/MediaViewerPage.tsx</code>)</p>
</li>
<li>Video player with reactions</li>
<li>Timestamped reactions overlay</li>
<li>Comment section</li>
<li>Related videos</li>
<li>Share functionality</li>
</ul>
<p><strong>State Management:</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-28-1"><a id="__codelineno-28-1" name="__codelineno-28-1" href="#__codelineno-28-1"></a><span class="c1">// Admin: useMediaApi hook</span>
</span><span id="__span-28-2"><a id="__codelineno-28-2" name="__codelineno-28-2" href="#__codelineno-28-2"></a><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">videos</span><span class="p">,</span><span class="w"> </span><span class="nx">loading</span><span class="p">,</span><span class="w"> </span><span class="nx">error</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">useMediaApi</span><span class="p">(</span><span class="s1">&#39;/api/videos&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-28-3"><a id="__codelineno-28-3" name="__codelineno-28-3" href="#__codelineno-28-3"></a><span class="w"> </span><span class="nx">limit</span><span class="o">:</span><span class="w"> </span><span class="kt">24</span><span class="p">,</span>
</span><span id="__span-28-4"><a id="__codelineno-28-4" name="__codelineno-28-4" href="#__codelineno-28-4"></a><span class="w"> </span><span class="nx">offset</span><span class="o">:</span><span class="w"> </span><span class="kt">0</span><span class="p">,</span>
</span><span id="__span-28-5"><a id="__codelineno-28-5" name="__codelineno-28-5" href="#__codelineno-28-5"></a><span class="w"> </span><span class="nx">search</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;&#39;</span><span class="p">,</span>
</span><span id="__span-28-6"><a id="__codelineno-28-6" name="__codelineno-28-6" href="#__codelineno-28-6"></a><span class="p">});</span>
</span><span id="__span-28-7"><a id="__codelineno-28-7" name="__codelineno-28-7" href="#__codelineno-28-7"></a>
</span><span id="__span-28-8"><a id="__codelineno-28-8" name="__codelineno-28-8" href="#__codelineno-28-8"></a><span class="c1">// Public: Direct axios calls to media API</span>
</span><span id="__span-28-9"><a id="__codelineno-28-9" name="__codelineno-28-9" href="#__codelineno-28-9"></a><span class="kd">const</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">axios</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;http://localhost:4100/api/media/public&#39;</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-28-10"><a id="__codelineno-28-10" name="__codelineno-28-10" href="#__codelineno-28-10"></a><span class="w"> </span><span class="nx">params</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">category</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;highlights&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">sort</span><span class="o">:</span><span class="w"> </span><span class="s1">&#39;popular&#39;</span><span class="p">,</span><span class="w"> </span><span class="nx">limit</span><span class="o">:</span><span class="w"> </span><span class="kt">12</span><span class="w"> </span><span class="p">},</span>
</span><span id="__span-28-11"><a id="__codelineno-28-11" name="__codelineno-28-11" href="#__codelineno-28-11"></a><span class="p">});</span>
</span></code></pre></div>
<hr />
<h2 id="performance-considerations">Performance Considerations<a class="headerlink" href="#performance-considerations" title="Permanent link">&para;</a></h2>
<h3 id="denormalized-counters">Denormalized Counters<a class="headerlink" href="#denormalized-counters" title="Permanent link">&para;</a></h3>
<p>The <code>publicMedia</code> table uses denormalized counters for engagement metrics:</p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-29-1"><a id="__codelineno-29-1" name="__codelineno-29-1" href="#__codelineno-29-1"></a><span class="nx">viewCount</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;view_count&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">(</span><span class="mf">0</span><span class="p">),</span>
</span><span id="__span-29-2"><a id="__codelineno-29-2" name="__codelineno-29-2" href="#__codelineno-29-2"></a><span class="nx">upvoteCount</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;upvote_count&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">(</span><span class="mf">0</span><span class="p">),</span>
</span><span id="__span-29-3"><a id="__codelineno-29-3" name="__codelineno-29-3" href="#__codelineno-29-3"></a><span class="nx">commentCount</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;comment_count&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">(</span><span class="mf">0</span><span class="p">),</span>
</span><span id="__span-29-4"><a id="__codelineno-29-4" name="__codelineno-29-4" href="#__codelineno-29-4"></a><span class="nx">finishCount</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;finish_count&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">(</span><span class="mf">0</span><span class="p">),</span>
</span><span id="__span-29-5"><a id="__codelineno-29-5" name="__codelineno-29-5" href="#__codelineno-29-5"></a><span class="nx">totalWatchTime</span><span class="o">:</span><span class="w"> </span><span class="kt">integer</span><span class="p">(</span><span class="s1">&#39;total_watch_time&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">(</span><span class="mf">0</span><span class="p">),</span>
</span></code></pre></div>
<p><strong>Pros:</strong></p>
<ul>
<li><strong>Fast sorting</strong> — No joins or aggregations needed</li>
<li><strong>Instant popularity ranking</strong> — Direct sorting on indexed columns</li>
<li><strong>Simple queries</strong> — No complex GROUP BY clauses</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><strong>Consistency risk</strong> — Counters can drift if transactions fail</li>
<li><strong>Update overhead</strong> — Must update counter on every upvote/view</li>
</ul>
<p><strong>Mitigation:</strong></p>
<ul>
<li>Use atomic updates: <code>sql\</code>${publicMedia.viewCount} + 1``</li>
<li>Run periodic reconciliation job to fix drift</li>
</ul>
<hr />
<h3 id="fire-and-forget-view-tracking">Fire-and-Forget View Tracking<a class="headerlink" href="#fire-and-forget-view-tracking" title="Permanent link">&para;</a></h3>
<p>View count increments are fire-and-forget to avoid blocking response:</p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-30-1"><a id="__codelineno-30-1" name="__codelineno-30-1" href="#__codelineno-30-1"></a><span class="c1">// Increment view count (fire and forget)</span>
</span><span id="__span-30-2"><a id="__codelineno-30-2" name="__codelineno-30-2" href="#__codelineno-30-2"></a><span class="nx">db</span><span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">publicMedia</span><span class="p">)</span>
</span><span id="__span-30-3"><a id="__codelineno-30-3" name="__codelineno-30-3" href="#__codelineno-30-3"></a><span class="w"> </span><span class="p">.</span><span class="nx">set</span><span class="p">({</span><span class="w"> </span><span class="nx">viewCount</span><span class="o">:</span><span class="w"> </span><span class="kt">sql</span><span class="sb">`</span><span class="si">${</span><span class="nx">publicMedia</span><span class="p">.</span><span class="nx">viewCount</span><span class="si">}</span><span class="sb"> + 1`</span><span class="w"> </span><span class="p">})</span>
</span><span id="__span-30-4"><a id="__codelineno-30-4" name="__codelineno-30-4" href="#__codelineno-30-4"></a><span class="w"> </span><span class="p">.</span><span class="nx">where</span><span class="p">(</span><span class="nx">eq</span><span class="p">(</span><span class="nx">publicMedia</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span><span class="w"> </span><span class="nx">mediaId</span><span class="p">))</span>
</span><span id="__span-30-5"><a id="__codelineno-30-5" name="__codelineno-30-5" href="#__codelineno-30-5"></a><span class="w"> </span><span class="p">.</span><span class="nx">execute</span><span class="p">()</span>
</span><span id="__span-30-6"><a id="__codelineno-30-6" name="__codelineno-30-6" href="#__codelineno-30-6"></a><span class="w"> </span><span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">err</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">logger</span><span class="p">.</span><span class="nx">error</span><span class="p">({</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="s1">&#39;Failed to increment view count&#39;</span><span class="p">));</span>
</span><span id="__span-30-7"><a id="__codelineno-30-7" name="__codelineno-30-7" href="#__codelineno-30-7"></a>
</span><span id="__span-30-8"><a id="__codelineno-30-8" name="__codelineno-30-8" href="#__codelineno-30-8"></a><span class="c1">// Return immediately (don&#39;t await)</span>
</span><span id="__span-30-9"><a id="__codelineno-30-9" name="__codelineno-30-9" href="#__codelineno-30-9"></a><span class="k">return</span><span class="w"> </span><span class="nx">reply</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">media</span><span class="p">);</span>
</span></code></pre></div>
<p><strong>Trade-off:</strong></p>
<ul>
<li><strong>Faster response</strong> — User doesn't wait for view count update</li>
<li><strong>Eventual consistency</strong> — View count may be slightly behind</li>
</ul>
<hr />
<h3 id="fingerprint-based-deduplication">Fingerprint-Based Deduplication<a class="headerlink" href="#fingerprint-based-deduplication" title="Permanent link">&para;</a></h3>
<p>The <code>videos</code> table includes a composite index for fast duplicate detection:</p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-31-1"><a id="__codelineno-31-1" name="__codelineno-31-1" href="#__codelineno-31-1"></a><span class="nx">fingerprintIdx</span><span class="o">:</span><span class="w"> </span><span class="kt">index</span><span class="p">(</span><span class="s1">&#39;idx_videos_fingerprint&#39;</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span>
</span><span id="__span-31-2"><a id="__codelineno-31-2" name="__codelineno-31-2" href="#__codelineno-31-2"></a><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">durationSeconds</span><span class="p">,</span><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">fileSize</span><span class="p">,</span><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">width</span><span class="p">,</span><span class="w"> </span><span class="nx">table</span><span class="p">.</span><span class="nx">height</span>
</span><span id="__span-31-3"><a id="__codelineno-31-3" name="__codelineno-31-3" href="#__codelineno-31-3"></a><span class="p">),</span>
</span></code></pre></div>
<p><strong>Usage:</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-32-1"><a id="__codelineno-32-1" name="__codelineno-32-1" href="#__codelineno-32-1"></a><span class="kd">const</span><span class="w"> </span><span class="nx">duplicates</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">db</span>
</span><span id="__span-32-2"><a id="__codelineno-32-2" name="__codelineno-32-2" href="#__codelineno-32-2"></a><span class="w"> </span><span class="p">.</span><span class="nx">select</span><span class="p">()</span>
</span><span id="__span-32-3"><a id="__codelineno-32-3" name="__codelineno-32-3" href="#__codelineno-32-3"></a><span class="w"> </span><span class="p">.</span><span class="kr">from</span><span class="p">(</span><span class="nx">videos</span><span class="p">)</span>
</span><span id="__span-32-4"><a id="__codelineno-32-4" name="__codelineno-32-4" href="#__codelineno-32-4"></a><span class="w"> </span><span class="p">.</span><span class="nx">where</span><span class="p">(</span><span class="nx">and</span><span class="p">(</span>
</span><span id="__span-32-5"><a id="__codelineno-32-5" name="__codelineno-32-5" href="#__codelineno-32-5"></a><span class="w"> </span><span class="nx">eq</span><span class="p">(</span><span class="nx">videos</span><span class="p">.</span><span class="nx">durationSeconds</span><span class="p">,</span><span class="w"> </span><span class="nx">newVideo</span><span class="p">.</span><span class="nx">durationSeconds</span><span class="p">),</span>
</span><span id="__span-32-6"><a id="__codelineno-32-6" name="__codelineno-32-6" href="#__codelineno-32-6"></a><span class="w"> </span><span class="nx">eq</span><span class="p">(</span><span class="nx">videos</span><span class="p">.</span><span class="nx">fileSize</span><span class="p">,</span><span class="w"> </span><span class="nx">newVideo</span><span class="p">.</span><span class="nx">fileSize</span><span class="p">),</span>
</span><span id="__span-32-7"><a id="__codelineno-32-7" name="__codelineno-32-7" href="#__codelineno-32-7"></a><span class="w"> </span><span class="nx">eq</span><span class="p">(</span><span class="nx">videos</span><span class="p">.</span><span class="nx">width</span><span class="p">,</span><span class="w"> </span><span class="nx">newVideo</span><span class="p">.</span><span class="nx">width</span><span class="p">),</span>
</span><span id="__span-32-8"><a id="__codelineno-32-8" name="__codelineno-32-8" href="#__codelineno-32-8"></a><span class="w"> </span><span class="nx">eq</span><span class="p">(</span><span class="nx">videos</span><span class="p">.</span><span class="nx">height</span><span class="p">,</span><span class="w"> </span><span class="nx">newVideo</span><span class="p">.</span><span class="nx">height</span><span class="p">),</span>
</span><span id="__span-32-9"><a id="__codelineno-32-9" name="__codelineno-32-9" href="#__codelineno-32-9"></a><span class="w"> </span><span class="p">));</span>
</span><span id="__span-32-10"><a id="__codelineno-32-10" name="__codelineno-32-10" href="#__codelineno-32-10"></a>
</span><span id="__span-32-11"><a id="__codelineno-32-11" name="__codelineno-32-11" href="#__codelineno-32-11"></a><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">duplicates</span><span class="p">.</span><span class="nx">length</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mf">0</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nx">duplicates</span><span class="p">[</span><span class="mf">0</span><span class="p">].</span><span class="nx">fileHash</span><span class="w"> </span><span class="o">===</span><span class="w"> </span><span class="nx">newVideo</span><span class="p">.</span><span class="nx">fileHash</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-32-12"><a id="__codelineno-32-12" name="__codelineno-32-12" href="#__codelineno-32-12"></a><span class="w"> </span><span class="k">throw</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="ne">Error</span><span class="p">(</span><span class="s1">&#39;Duplicate video detected&#39;</span><span class="p">);</span>
</span><span id="__span-32-13"><a id="__codelineno-32-13" name="__codelineno-32-13" href="#__codelineno-32-13"></a><span class="p">}</span>
</span></code></pre></div>
<p><strong>Why Fingerprint Index:</strong></p>
<ul>
<li><strong>Fast pre-filter</strong> — Index lookup narrows candidates</li>
<li><strong>File hash check</strong> — Confirms exact duplicate (expensive, only on candidates)</li>
<li><strong>Two-stage approach</strong> — Balances speed and accuracy</li>
</ul>
<hr />
<h2 id="troubleshooting">Troubleshooting<a class="headerlink" href="#troubleshooting" title="Permanent link">&para;</a></h2>
<h3 id="media-api-not-starting">Media API Not Starting<a class="headerlink" href="#media-api-not-starting" title="Permanent link">&para;</a></h3>
<p><strong>Problem:</strong></p>
<p>Docker logs show "Media API server closed" immediately.</p>
<p><strong>Diagnosis:</strong></p>
<p>Check env vars:</p>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-33-1"><a id="__codelineno-33-1" name="__codelineno-33-1" href="#__codelineno-33-1"></a>docker<span class="w"> </span>compose<span class="w"> </span><span class="nb">exec</span><span class="w"> </span>api<span class="w"> </span>printenv<span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span>MEDIA
</span></code></pre></div>
<p><strong>Required vars:</strong></p>
<div class="language-text highlight"><pre><span></span><code><span id="__span-34-1"><a id="__codelineno-34-1" name="__codelineno-34-1" href="#__codelineno-34-1"></a>MEDIA_API_PORT=4100
</span><span id="__span-34-2"><a id="__codelineno-34-2" name="__codelineno-34-2" href="#__codelineno-34-2"></a>ENABLE_MEDIA_FEATURES=true
</span><span id="__span-34-3"><a id="__codelineno-34-3" name="__codelineno-34-3" href="#__codelineno-34-3"></a>MAX_UPLOAD_SIZE_GB=10
</span></code></pre></div>
<p><strong>Solution:</strong></p>
<ul>
<li>Verify <code>ENABLE_MEDIA_FEATURES=true</code> in <code>.env</code></li>
<li>Check port conflicts: <code>lsof -i :4100</code></li>
<li>Check database connection (shares same DATABASE_URL)</li>
</ul>
<hr />
<h3 id="cors-errors-on-media-api">CORS Errors on Media API<a class="headerlink" href="#cors-errors-on-media-api" title="Permanent link">&para;</a></h3>
<p><strong>Problem:</strong></p>
<p>Frontend gets CORS errors when calling media API endpoints.</p>
<p><strong>Diagnosis:</strong></p>
<p>Check CORS origins:</p>
<div class="language-text highlight"><pre><span></span><code><span id="__span-35-1"><a id="__codelineno-35-1" name="__codelineno-35-1" href="#__codelineno-35-1"></a>CORS_ORIGINS=http://localhost:3000,http://localhost:3010
</span></code></pre></div>
<p><strong>Behavior:</strong></p>
<div class="language-typescript highlight"><pre><span></span><code><span id="__span-36-1"><a id="__codelineno-36-1" name="__codelineno-36-1" href="#__codelineno-36-1"></a><span class="k">await</span><span class="w"> </span><span class="nx">fastify</span><span class="p">.</span><span class="nx">register</span><span class="p">(</span><span class="nx">cors</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-36-2"><a id="__codelineno-36-2" name="__codelineno-36-2" href="#__codelineno-36-2"></a><span class="w"> </span><span class="nx">origin</span><span class="o">:</span><span class="w"> </span><span class="p">(</span><span class="nx">origin</span><span class="p">,</span><span class="w"> </span><span class="nx">cb</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-36-3"><a id="__codelineno-36-3" name="__codelineno-36-3" href="#__codelineno-36-3"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nx">origin</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-36-4"><a id="__codelineno-36-4" name="__codelineno-36-4" href="#__codelineno-36-4"></a><span class="w"> </span><span class="nx">cb</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span><span class="w"> </span><span class="kc">true</span><span class="p">);</span><span class="w"> </span><span class="c1">// Allow no origin (mobile, curl)</span>
</span><span id="__span-36-5"><a id="__codelineno-36-5" name="__codelineno-36-5" href="#__codelineno-36-5"></a><span class="w"> </span><span class="k">return</span><span class="p">;</span>
</span><span id="__span-36-6"><a id="__codelineno-36-6" name="__codelineno-36-6" href="#__codelineno-36-6"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-36-7"><a id="__codelineno-36-7" name="__codelineno-36-7" href="#__codelineno-36-7"></a>
</span><span id="__span-36-8"><a id="__codelineno-36-8" name="__codelineno-36-8" href="#__codelineno-36-8"></a><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="nx">allowedOrigins</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">origin</span><span class="p">))</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-36-9"><a id="__codelineno-36-9" name="__codelineno-36-9" href="#__codelineno-36-9"></a><span class="w"> </span><span class="nx">cb</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span><span class="w"> </span><span class="kc">true</span><span class="p">);</span>
</span><span id="__span-36-10"><a id="__codelineno-36-10" name="__codelineno-36-10" href="#__codelineno-36-10"></a><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
</span><span id="__span-36-11"><a id="__codelineno-36-11" name="__codelineno-36-11" href="#__codelineno-36-11"></a><span class="w"> </span><span class="nx">cb</span><span class="p">(</span><span class="ow">new</span><span class="w"> </span><span class="ne">Error</span><span class="p">(</span><span class="s1">&#39;CORS not allowed&#39;</span><span class="p">),</span><span class="w"> </span><span class="kc">false</span><span class="p">);</span>
</span><span id="__span-36-12"><a id="__codelineno-36-12" name="__codelineno-36-12" href="#__codelineno-36-12"></a><span class="w"> </span><span class="p">}</span>
</span><span id="__span-36-13"><a id="__codelineno-36-13" name="__codelineno-36-13" href="#__codelineno-36-13"></a><span class="w"> </span><span class="p">},</span>
</span><span id="__span-36-14"><a id="__codelineno-36-14" name="__codelineno-36-14" href="#__codelineno-36-14"></a><span class="w"> </span><span class="nx">credentials</span><span class="o">:</span><span class="w"> </span><span class="kt">true</span><span class="p">,</span>
</span><span id="__span-36-15"><a id="__codelineno-36-15" name="__codelineno-36-15" href="#__codelineno-36-15"></a><span class="p">});</span>
</span></code></pre></div>
<p><strong>Solution:</strong></p>
<p>Add missing origins to <code>CORS_ORIGINS</code> in <code>.env</code>:</p>
<div class="language-text highlight"><pre><span></span><code><span id="__span-37-1"><a id="__codelineno-37-1" name="__codelineno-37-1" href="#__codelineno-37-1"></a>CORS_ORIGINS=http://localhost:3000,http://localhost:3010,http://localhost:3100
</span></code></pre></div>
<hr />
<h3 id="upvote-not-working">Upvote Not Working<a class="headerlink" href="#upvote-not-working" title="Permanent link">&para;</a></h3>
<p><strong>Problem:</strong></p>
<p>Upvote button doesn't work, returns 400 error.</p>
<p><strong>Diagnosis:</strong></p>
<p>Check request body:</p>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-38-1"><a id="__codelineno-38-1" name="__codelineno-38-1" href="#__codelineno-38-1"></a>curl<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="se">\</span>
</span><span id="__span-38-2"><a id="__codelineno-38-2" name="__codelineno-38-2" href="#__codelineno-38-2"></a><span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Content-Type: application/json&quot;</span><span class="w"> </span><span class="se">\</span>
</span><span id="__span-38-3"><a id="__codelineno-38-3" name="__codelineno-38-3" href="#__codelineno-38-3"></a><span class="w"> </span>-d<span class="w"> </span><span class="s1">&#39;{&quot;sessionId&quot;:&quot;sess_abc123&quot;}&#39;</span><span class="w"> </span><span class="se">\</span>
</span><span id="__span-38-4"><a id="__codelineno-38-4" name="__codelineno-38-4" href="#__codelineno-38-4"></a><span class="w"> </span>http://localhost:4100/api/media/public/456/upvote
</span></code></pre></div>
<p><strong>Common Issues:</strong></p>
<ol>
<li>
<p><strong>Missing sessionId:</strong>
<div class="language-json highlight"><pre><span></span><code><span id="__span-39-1"><a id="__codelineno-39-1" name="__codelineno-39-1" href="#__codelineno-39-1"></a><span class="p">{</span><span class="w"> </span><span class="nt">&quot;error&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;sessionId is required&quot;</span><span class="w"> </span><span class="p">}</span>
</span></code></pre></div></p>
</li>
<li>
<p><strong>Media not found:</strong>
<div class="language-json highlight"><pre><span></span><code><span id="__span-40-1"><a id="__codelineno-40-1" name="__codelineno-40-1" href="#__codelineno-40-1"></a><span class="p">{</span><span class="w"> </span><span class="nt">&quot;error&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Media not found&quot;</span><span class="w"> </span><span class="p">}</span>
</span></code></pre></div></p>
</li>
<li>
<p><strong>Locked media:</strong>
<div class="language-json highlight"><pre><span></span><code><span id="__span-41-1"><a id="__codelineno-41-1" name="__codelineno-41-1" href="#__codelineno-41-1"></a><span class="p">{</span><span class="w"> </span><span class="nt">&quot;error&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Media is locked&quot;</span><span class="w"> </span><span class="p">}</span>
</span></code></pre></div></p>
</li>
</ol>
<p><strong>Solution:</strong></p>
<ul>
<li>Generate session ID in frontend: <code>crypto.randomUUID()</code> or <code>nanoid()</code></li>
<li>Verify media exists in <code>public_media</code> table</li>
<li>Check <code>isLocked</code> status</li>
</ul>
<hr />
<h3 id="reactions-not-appearing">Reactions Not Appearing<a class="headerlink" href="#reactions-not-appearing" title="Permanent link">&para;</a></h3>
<p><strong>Problem:</strong></p>
<p>Reactions submitted but not appearing in frontend.</p>
<p><strong>Diagnosis:</strong></p>
<p>Check reaction data:</p>
<div class="language-sql highlight"><pre><span></span><code><span id="__span-42-1"><a id="__codelineno-42-1" name="__codelineno-42-1" href="#__codelineno-42-1"></a><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">video_reactions</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="ss">&quot;mediaId&quot;</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">456</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="ss">&quot;createdAt&quot;</span><span class="w"> </span><span class="k">DESC</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span>
</span></code></pre></div>
<p><strong>Verify:</strong></p>
<ul>
<li><code>userId</code> matches authenticated user</li>
<li><code>mediaId</code> matches video ID</li>
<li><code>reactionType</code> is valid emoji type</li>
</ul>
<p><strong>Common Issues:</strong></p>
<ol>
<li><strong>Authentication failed:</strong></li>
<li>Reaction requires auth</li>
<li>
<p>Check JWT token in Authorization header</p>
</li>
<li>
<p><strong>Invalid reaction type:</strong>
<div class="language-json highlight"><pre><span></span><code><span id="__span-43-1"><a id="__codelineno-43-1" name="__codelineno-43-1" href="#__codelineno-43-1"></a><span class="p">{</span><span class="w"> </span><span class="nt">&quot;error&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Invalid reaction type&quot;</span><span class="w"> </span><span class="p">}</span>
</span></code></pre></div></p>
</li>
<li>
<p><strong>Video not found:</strong>
<div class="language-json highlight"><pre><span></span><code><span id="__span-44-1"><a id="__codelineno-44-1" name="__codelineno-44-1" href="#__codelineno-44-1"></a><span class="p">{</span><span class="w"> </span><span class="nt">&quot;error&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Video not found&quot;</span><span class="w"> </span><span class="p">}</span>
</span></code></pre></div></p>
</li>
</ol>
<p><strong>Solution:</strong></p>
<ul>
<li>Verify JWT token is valid and not expired</li>
<li>Use valid reaction types: <code>like</code>, <code>love</code>, <code>laugh</code>, <code>wow</code>, <code>sad</code>, <code>angry</code></li>
<li>Check video exists in <code>videos</code> table (not just <code>public_media</code>)</li>
</ul>
<hr />
<h3 id="job-queue-not-processing">Job Queue Not Processing<a class="headerlink" href="#job-queue-not-processing" title="Permanent link">&para;</a></h3>
<p><strong>Problem:</strong></p>
<p>Jobs stuck in <code>pending</code> status, never transition to <code>running</code>.</p>
<p><strong>Diagnosis:</strong></p>
<p>Check job queue:</p>
<div class="language-sql highlight"><pre><span></span><code><span id="__span-45-1"><a id="__codelineno-45-1" name="__codelineno-45-1" href="#__codelineno-45-1"></a><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="k">type</span><span class="p">,</span><span class="w"> </span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="ss">&quot;resourceCategory&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">&quot;queuePosition&quot;</span><span class="p">,</span><span class="w"> </span><span class="ss">&quot;waitingReason&quot;</span>
</span><span id="__span-45-2"><a id="__codelineno-45-2" name="__codelineno-45-2" href="#__codelineno-45-2"></a><span class="k">FROM</span><span class="w"> </span><span class="n">jobs</span>
</span><span id="__span-45-3"><a id="__codelineno-45-3" name="__codelineno-45-3" href="#__codelineno-45-3"></a><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;pending&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;queued&#39;</span><span class="p">)</span>
</span><span id="__span-45-4"><a id="__codelineno-45-4" name="__codelineno-45-4" href="#__codelineno-45-4"></a><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">priority</span><span class="w"> </span><span class="k">DESC</span><span class="p">,</span><span class="w"> </span><span class="ss">&quot;createdAt&quot;</span><span class="w"> </span><span class="k">ASC</span><span class="p">;</span>
</span></code></pre></div>
<p><strong>Common Issues:</strong></p>
<ol>
<li><strong>No worker running:</strong></li>
<li>Check if job worker process is running</li>
<li>
<p>Verify <code>ENABLE_MEDIA_FEATURES=true</code></p>
</li>
<li>
<p><strong>Resource exhaustion:</strong></p>
</li>
<li>GPU jobs waiting for VRAM</li>
<li>
<p>Check <code>vramRequired</code> vs available VRAM</p>
</li>
<li>
<p><strong>Pipeline blocking:</strong></p>
</li>
<li>Pipeline step depends on previous step completion</li>
</ol>
<p><strong>Solution:</strong></p>
<ul>
<li>Start job worker: <code>npm run worker:media</code> or check Docker Compose</li>
<li>Adjust resource limits or priority</li>
<li>Check pipeline configuration for blocking issues</li>
</ul>
<hr />
<h2 id="related-documentation">Related Documentation<a class="headerlink" href="#related-documentation" title="Permanent link">&para;</a></h2>
<ul>
<li><a href="/v2/architecture/dual-api.md">Dual API Architecture</a> - Express + Fastify architecture</li>
<li><a href="/v2/database/drizzle.md">Drizzle ORM</a> - Drizzle query builder (media tables)</li>
<li><a href="/v2/frontend/pages/media/library-page.md">Frontend: LibraryPage</a> - Video library management UI</li>
<li><a href="/v2/frontend/pages/public/media-gallery-page.md">Frontend: MediaGalleryPage</a> - Public gallery</li>
<li><a href="/v2/frontend/pages/public/media-viewer-page.md">Frontend: MediaViewerPage</a> - Video player with reactions</li>
<li><a href="/v2/features/media/overview.md">Features: Media Manager</a> - Complete feature guide</li>
<li><a href="/v2/api-reference/media.md">API Reference: Media</a> - Complete endpoint reference</li>
<li><a href="/v2/user-guides/media-admin-guide.md">User Guide: Media Admin</a> - Managing video library</li>
<li><a href="/v2/troubleshooting/media-issues.md">Troubleshooting: Media API Issues</a> - Debugging guide</li>
</ul>
</article>
</div>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg>
Back to top
</button>
</main>
<footer class="md-footer">
<nav class="md-footer__inner md-grid" aria-label="Footer" >
<a href="../pages/" class="md-footer__link md-footer__link--prev" aria-label="Previous: Pages Module">
<div class="md-footer__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</div>
<div class="md-footer__title">
<span class="md-footer__direction">
Previous
</span>
<div class="md-ellipsis">
Pages Module
</div>
</div>
</a>
<a href="../../services/" class="md-footer__link md-footer__link--next" aria-label="Next: Backend Services">
<div class="md-footer__title">
<span class="md-footer__direction">
Next
</span>
<div class="md-ellipsis">
Backend Services
</div>
</div>
<div class="md-footer__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 11v2h12l-5.5 5.5 1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5 16 11z"/></svg>
</div>
</a>
</nav>
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
<div class="md-copyright__highlight">
Copyright &copy; 2024 The Bunker Operations <a href="#__consent">Change cookie settings</a>
</div>
</div>
<div class="md-social">
<a href="https://gitea.bnkops.com/admin" target="_blank" rel="noopener" title="Gitea Repository" class="md-social__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M173.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6m-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3m44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9M252.8 8C114.1 8 8 113.3 8 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C436.2 457.8 504 362.9 504 252 504 113.3 391.5 8 252.8 8M105.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1m-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7m32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1m-11.4-14.7c-1.6 1-1.6 3.6 0 5.9s4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2"/></svg>
</a>
<a href="https://listmonk.bnkops.com/subscription/form" target="_blank" rel="noopener" title="Newsletter" class="md-social__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M536.4-26.3c9.8-3.5 20.6-1 28 6.3s9.8 18.2 6.3 28l-178 496.9c-5 13.9-18.1 23.1-32.8 23.1-14.2 0-27-8.6-32.3-21.7l-64.2-158c-4.5-11-2.5-23.6 5.2-32.6l94.5-112.4c5.1-6.1 4.7-15-.9-20.6s-14.6-6-20.6-.9l-112.4 94.3c-9.1 7.6-21.6 9.6-32.6 5.2L38.1 216.8c-13.1-5.3-21.7-18.1-21.7-32.3 0-14.7 9.2-27.8 23.1-32.8z"/></svg>
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<script id="__config" type="application/json">{"annotate": null, "base": "../../../..", "features": ["announce.dismiss", "content.action.edit", "content.action.view", "content.code.annotate", "content.code.copy", "content.tooltips", "navigation.expand", "navigation.footer", "navigation.indexes", "navigation.path", "navigation.prune", "navigation.sections", "navigation.tabs", "navigation.tabs.sticky", "navigation.top", "navigation.tracking", "search.highlight", "search.share", "search.suggest", "toc.follow"], "search": "../../../../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="../../../../assets/javascripts/bundle.79ae519e.min.js"></script>
<script src="../../../../javascripts/home.js"></script>
<script src="../../../../javascripts/github-widget.js"></script>
<script src="../../../../javascripts/gitea-widget.js"></script>
<script src="../../../../assets/js/env-config.js"></script>
<script src="../../../../assets/js/video-player.js"></script>
</body>
</html>