125 lines
3.4 KiB
TypeScript
125 lines
3.4 KiB
TypeScript
import type { AppSupabaseClient } from '../supabase/client';
|
|
|
|
export interface WatchSessionState {
|
|
playing: boolean;
|
|
positionSeconds: number;
|
|
updatedAtMs: number;
|
|
}
|
|
|
|
export interface WatchSession {
|
|
id: string;
|
|
conversationId: string;
|
|
ownerUserId: string;
|
|
videoId: string;
|
|
startedAt: string;
|
|
endedAt: string | null;
|
|
currentState: WatchSessionState;
|
|
}
|
|
|
|
export function parseYouTubeUrl(input: string): string | null {
|
|
const s = input.trim();
|
|
if (!s) return null;
|
|
if (/^[A-Za-z0-9_-]{11}$/.test(s)) return s;
|
|
const patterns = [
|
|
/[?&]v=([A-Za-z0-9_-]{11})/,
|
|
/youtu\.be\/([A-Za-z0-9_-]{11})/,
|
|
/youtube\.com\/embed\/([A-Za-z0-9_-]{11})/,
|
|
/youtube\.com\/shorts\/([A-Za-z0-9_-]{11})/,
|
|
];
|
|
for (const re of patterns) {
|
|
const m = re.exec(s);
|
|
if (m && m[1]) return m[1];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export async function createWatchSession(
|
|
client: AppSupabaseClient,
|
|
params: { conversationId: string; videoId: string },
|
|
): Promise<WatchSession> {
|
|
const { data: session } = await client.auth.getUser();
|
|
if (!session.user) throw new Error('not authenticated');
|
|
const { data, error } = await client
|
|
.from('conversation_watch_sessions')
|
|
.insert({
|
|
conversation_id: params.conversationId,
|
|
owner_user_id: session.user.id,
|
|
video_id: params.videoId,
|
|
})
|
|
.select('id, conversation_id, owner_user_id, video_id, started_at, ended_at, current_state')
|
|
.single();
|
|
if (error) throw error;
|
|
return mapRow(data);
|
|
}
|
|
|
|
export async function getWatchSession(
|
|
client: AppSupabaseClient,
|
|
sessionId: string,
|
|
): Promise<WatchSession | null> {
|
|
const { data, error } = await client
|
|
.from('conversation_watch_sessions')
|
|
.select('id, conversation_id, owner_user_id, video_id, started_at, ended_at, current_state')
|
|
.eq('id', sessionId)
|
|
.maybeSingle();
|
|
if (error) throw error;
|
|
return data ? mapRow(data) : null;
|
|
}
|
|
|
|
export async function updateWatchSessionState(
|
|
client: AppSupabaseClient,
|
|
sessionId: string,
|
|
state: WatchSessionState,
|
|
): Promise<void> {
|
|
const { error } = await client
|
|
.from('conversation_watch_sessions')
|
|
.update({
|
|
current_state: {
|
|
playing: state.playing,
|
|
position_seconds: state.positionSeconds,
|
|
updated_at_ms: state.updatedAtMs,
|
|
},
|
|
})
|
|
.eq('id', sessionId);
|
|
if (error) throw error;
|
|
}
|
|
|
|
export async function endWatchSession(
|
|
client: AppSupabaseClient,
|
|
sessionId: string,
|
|
): Promise<void> {
|
|
const { error } = await client
|
|
.from('conversation_watch_sessions')
|
|
.update({ ended_at: new Date().toISOString() })
|
|
.eq('id', sessionId);
|
|
if (error) throw error;
|
|
}
|
|
|
|
function mapRow(row: {
|
|
id: string;
|
|
conversation_id: string;
|
|
owner_user_id: string;
|
|
video_id: string;
|
|
started_at: string;
|
|
ended_at: string | null;
|
|
current_state: unknown;
|
|
}): WatchSession {
|
|
const raw = (row.current_state ?? {}) as Partial<{
|
|
playing: boolean;
|
|
position_seconds: number;
|
|
updated_at_ms: number;
|
|
}>;
|
|
return {
|
|
id: row.id,
|
|
conversationId: row.conversation_id,
|
|
ownerUserId: row.owner_user_id,
|
|
videoId: row.video_id,
|
|
startedAt: row.started_at,
|
|
endedAt: row.ended_at,
|
|
currentState: {
|
|
playing: typeof raw.playing === 'boolean' ? raw.playing : false,
|
|
positionSeconds: typeof raw.position_seconds === 'number' ? raw.position_seconds : 0,
|
|
updatedAtMs: typeof raw.updated_at_ms === 'number' ? raw.updated_at_ms : 0,
|
|
},
|
|
};
|
|
}
|