1- -- match_clips: the durable record of a rendered clip.
2- -- One row per successful render that the user chose to save to library.
3- -- `download`-destination renders write a short-lived row only while the
4- -- download URL is live and are reaped on the same window the file is.
51create table if not exists " public" ." match_clips" (
62 " id" uuid not null default gen_random_uuid(),
73 " user_steam_id" bigint not null ,
84 " match_map_id" uuid not null ,
95 " title" text ,
10- -- Wallclock duration of the rendered mp4. Filled by the pod after
11- -- ffprobe; null while the upload is still in flight.
126 " duration_ms" integer ,
13- -- Final S3 location served via the same backblaze-proxy worker the
14- -- demo files use. Null while the pod is still writing.
15- " s3_url" text ,
7+ " file" text ,
168 " thumbnail_url" text ,
17- -- private | unlisted | match — only `private` is exposed by the v1
18- -- editor; the column exists so phase-2 sharing flows don't need a
19- -- migration. Default `private` so existing rows stay locked down.
209 " visibility" text not null default ' private' ,
10+ " target_steam_id" bigint ,
2111 " created_at" timestamptz not null default now(),
2212 primary key (" id" ),
2313 foreign key (" user_steam_id" ) references " public" ." players" (" steam_id" )
2414 on update cascade on delete cascade ,
2515 foreign key (" match_map_id" ) references " public" ." match_maps" (" id" )
2616 on update cascade on delete cascade ,
17+ foreign key (" target_steam_id" ) references " public" ." players" (" steam_id" )
18+ on update cascade on delete set null ,
2719 constraint match_clips_visibility_chk
28- check (visibility in (' private' , ' unlisted' , ' match' ))
20+ check (visibility in (' private' , ' unlisted' , ' match' , ' public ' ))
2921);
3022
3123create index if not exists " match_clips_user_steam_id_idx"
@@ -34,30 +26,23 @@ create index if not exists "match_clips_user_steam_id_idx"
3426create index if not exists " match_clips_match_map_id_idx"
3527 on " public" ." match_clips" (" match_map_id" );
3628
37- -- clip_render_jobs: ephemeral per-render record. The web subscribes to
38- -- this row to drive the progress UI; the row is reaped once the clip
39- -- is uploaded + match_clips row written (clip_id then points at it).
29+ create index if not exists " match_clips_target_steam_id_idx"
30+ on " public" ." match_clips" (" target_steam_id" );
31+
32+ create index if not exists " match_clips_public_created_at_idx"
33+ on " public" ." match_clips" (" created_at" desc )
34+ where visibility = ' public' ;
35+
4036create table if not exists " public" ." clip_render_jobs" (
4137 " id" uuid not null default gen_random_uuid(),
4238 " user_steam_id" bigint not null ,
4339 " match_map_id" uuid not null ,
44- -- Pod auth: the render pod posts back to /clip-renders/:id/status +
45- -- /clip-renders/:id/upload with `x-origin-auth: <id>:<token>`. Same
46- -- pattern as match_demo_sessions.session_token.
4740 " session_token" text not null ,
4841 " k8s_job_name" text not null ,
49- -- The full ClipSpec sent by the editor (segments + overlays + audio
50- -- + output). Stored as-is so a re-render later (or a debugging
51- -- inspection) reproduces what was originally requested.
5242 " spec" jsonb not null ,
53- -- queued | rendering | uploading | done | error | cancelled
5443 " status" text not null default ' queued' ,
55- -- 0..1 wallclock progress reported by the pod. Null while queued.
5644 " progress" numeric (4 , 3 ),
5745 " error_message" text ,
58- -- Once the upload completes the pod creates a match_clips row and
59- -- writes the id back here. The web subscription joins on this to
60- -- show the final clip card.
6146 " clip_id" uuid,
6247 " status_history" jsonb not null default ' []' ::jsonb,
6348 " last_status_at" timestamptz not null default now(),
@@ -75,15 +60,52 @@ create table if not exists "public"."clip_render_jobs" (
7560 check (progress is null or (progress >= 0 and progress <= 1 ))
7661);
7762
78- -- One in-flight job per user. Done/error/cancelled rows are excluded
79- -- so the user can submit a new render once the previous finishes —
80- -- this is the per-user concurrency cap referenced in the plan.
81- create unique index if not exists " clip_render_jobs_one_in_flight_per_user"
82- on " public" ." clip_render_jobs" (" user_steam_id" )
83- where status in (' queued' , ' rendering' , ' uploading' );
84-
8563create index if not exists " clip_render_jobs_user_steam_id_idx"
8664 on " public" ." clip_render_jobs" (" user_steam_id" );
8765
8866create index if not exists " clip_render_jobs_match_map_id_idx"
8967 on " public" ." clip_render_jobs" (" match_map_id" );
68+
69+ CREATE OR REPLACE FUNCTION public .clip_download_url(match_clips public .match_clips )
70+ RETURNS text
71+ LANGUAGE plpgsql STABLE
72+ AS $$
73+ DECLARE
74+ worker_url text ;
75+ slug text ;
76+ basename text ;
77+ download_name text ;
78+ BEGIN
79+ SELECT value INTO worker_url
80+ FROM settings
81+ WHERE name = ' cloudflare_worker_url' ;
82+
83+ IF worker_url IS NULL OR match_clips .file IS NULL THEN
84+ RETURN NULL ;
85+ END IF;
86+
87+ slug := NULL ;
88+ IF match_clips .title IS NOT NULL AND length(trim (match_clips .title )) > 0 THEN
89+ slug := regexp_replace(trim (match_clips .title ), ' [^a-zA-Z0-9_-]+' , ' -' , ' g' );
90+ slug := regexp_replace(slug, ' ^-+|-+$' , ' ' , ' g' );
91+ IF length(slug) > 80 THEN
92+ slug := substring (slug from 1 for 80 );
93+ END IF;
94+ IF length(slug) = 0 THEN
95+ slug := NULL ;
96+ END IF;
97+ END IF;
98+
99+ basename := regexp_replace(match_clips .file , ' ^.*/' , ' ' );
100+ IF slug IS NOT NULL THEN
101+ download_name := slug || ' .mp4' ;
102+ ELSE
103+ download_name := basename;
104+ END IF;
105+
106+ RETURN CONCAT(worker_url, ' /' , match_clips .file , ' ?name=' , download_name);
107+ END;
108+ $$;
109+
110+ alter table " public" ." match_map_demos"
111+ add column if not exists " players" jsonb null ;
0 commit comments