import { postgrestLocalUrl, siteUrl } from './constants'; import { getDateFromSafeDate, getSafeDate } from './dates'; import { IVtuber, IStream, ITimestamp, IVod } from '@futureporn/types'; import qs from 'qs'; import { ITagVodRelation } from './tag-vod-relations'; import { IMeta, IMuxAsset, IMuxAssetResponse } from '@futureporn/types'; import { IS3File, IS3FileResponse } from '@/app/lib/b2File'; import fetchAPI from './fetch-api'; import { IUserResponse } from './users'; import { getCountFromHeaders } from './fetchers'; /** * Dec 2024 UUIDs were introduced. * Going forward, use UUIDs where possible. * safeDates are retained for backwards compatibility. * * @see https://www.w3.org/Provider/Style/URI */ export interface IVodPageProps { params: { safeDateOrUUID: string; slug: string; }; } const fetchVodsOptions = { next: { tags: ['vods'] } } export async function getVodFromSafeDateOrUUID(safeDateOrUUID: string): Promise { let vod: IVod | null; let date: Date; if (!safeDateOrUUID) { console.log(`safeDateOrUUID was missing`); return null; } else if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(safeDateOrUUID)) { console.log('this is a UUID!'); vod = await getVodByUUID(safeDateOrUUID); if (!vod) return null; } else { console.log(`this is a safe date. ${safeDateOrUUID}`) date = getDateFromSafeDate(safeDateOrUUID); console.log(`date=${date.toISOString()}`) if (!date) { console.log('there is no date') return null; } vod = await getVodForDate(date); } return vod; } export function getUrl(vod: IVod, slug: string, date: string): string { return `/vt/${slug}/vod/${getSafeDate(date)}` } export function getPaginatedUrl(): (slug: string, pageNumber: number) => string { return (slug: string, pageNumber: number) => { return `${siteUrl}/vt/${slug}/vods/${pageNumber}` } } /** @deprecated old format for futureporn.net/api/v1.json, which is deprecated. Please use getUrl() instead */ export function getDeprecatedUrl(vod: IVod): string { return `${siteUrl}/vods/${getSafeDate(vod.date_2)}` } export async function getNextVod(vod: IVod): Promise { const query = new URLSearchParams({ select: 'date_2,title,announce_title,vtuber:vtubers(slug)', [`date_2`]: `gt.${vod.date_2}`, [`vtuber.slug`]: `eq.${vod.vtuber.slug}`, ['published_at']: 'not.is.null', limit: '1', order: 'date_2.asc', }).toString(); const res = await fetch(`${postgrestLocalUrl}/vods?${query}`, fetchVodsOptions) const data = await res.json() if (!res.ok) { throw new Error(`res.status=${res.status} res.statusText=${res.statusText} \ndata=${JSON.stringify(data, null, 2)}`); } return data[0]; } export function getLocalizedDate(vod: IVod): string { return new Date(vod.date_2).toLocaleDateString() } export async function getPreviousVod(vod: IVod): Promise { const query = new URLSearchParams({ select: 'date_2,title,announce_title,vtuber:vtubers(slug)', [`date_2`]: `lt.${vod.date_2}`, [`vtuber.slug`]: `eq.${vod.vtuber.slug}`, limit: '1', order: 'date_2.desc', }).toString(); const res = await fetch(`${postgrestLocalUrl}/vods?${query}`, fetchVodsOptions) const data = await res.json() if (!res.ok) { throw new Error(`res.status=${res.status} res.statusText=${res.statusText} \ndata=${JSON.stringify(data, null, 2)}`); } return data[0]; } export async function getVodByUUID(uuid: string): Promise { const query = new URLSearchParams({ uuid: `eq.${uuid}`, select: ` vtuber(slug,displayName,image,imageBlur,themeColor), muxAsset(playbackId,assetId), thumbnail(cdnUrl,url), tagVodRelations(tag,createdAt,creatorId,tag(*)), videoSrcB2(url,key,uploadId,cdnUrl), stream(archiveStatus,date,tweet,uuid), timestamps_vod_links(timestamps(timestamps_tag_links(tags(id,name)))) `.replace(/\s+/g, ''), // Remove whitespace for URL encoding }); try { const res = await fetch(`${postgrestLocalUrl}/vods?${query}`, { cache: 'no-store', next: { tags: ['vods'] } }) if (!res.ok) { throw new Error('failed to fetch getVodByUUID') } const json = await res.json() const vod = json.data[0] if (!vod) return null; return vod; } catch (e) { if (e instanceof Error) { console.error(e) } return null; } } export async function getVodForDate(date: Date): Promise { const iso8601DateString = date.toISOString(); console.log(`getVodForDate ison8601DateString=${iso8601DateString}`) const selectFields = [ 'id', 'date_2', 'title', 'announce_title', 'vtuber:vtubers(slug,display_name,image,image_blur,theme_color)', 'timestamps_vod_links(timestamps(timestamps_tag_links(tags(id,name))))', ].join(','); const queryParams = { select: selectFields, date_2: `eq.${iso8601DateString}`, published_at: 'not.is.null', limit: '1', }; const query = new URLSearchParams(queryParams).toString(); console.log(query) const res = await fetch(`${postgrestLocalUrl}/vods?${query}`, { cache: 'no-store', next: { tags: ['vods'] } }); const json = await res.json(); if (!res.ok) { console.log(`res.status=${res.status} res.statusText=${res.statusText} body=${JSON.stringify(json, null, 2)}`) throw new Error('Failed to fetch vodForDate'); } const vod = json[0]; // PostgREST returns an array of results if (!vod) return null; return vod; } export async function getVod(id: number): Promise { const query = qs.stringify( { filters: { id: { $eq: id } } } ) const res = await fetch(`${postgrestLocalUrl}/vods?${query}`, fetchVodsOptions); if (!res.ok) return null; const data = await res.json(); return data; } export async function getVods(page: number = 1, pageSize: number = 25, sortDesc = true): Promise<{ vods: IVod[], count: number }> { // const query = qs.stringify( // { // populate: { // vtuber: { // fields: ['slug', 'displayName', 'image', 'imageBlur'] // }, // muxAsset: { // fields: ['playbackId', 'assetId'] // }, // thumbnail: { // fields: ['cdnUrl', 'url'] // }, // tagVodRelations: { // fields: ['tag'], // populate: ['tag'] // }, // videoSrcB2: { // fields: ['url', 'key', 'uploadId', 'cdnUrl'] // } // }, // sort: { // date: (sortDesc) ? 'desc' : 'asc' // }, // pagination: { // pageSize: pageSize, // page: page // } // } // ) // console.log(`postgrestLocalUrl=${postgrestLocalUrl} query=${query}`) // const url = `${configs.postgrestUrl}/vods?select=*,segments(*),recording:recordings(is_aborted)&id=eq.${vodId}` const selects = [ '*', 'vtuber:vtubers(id,slug,image,display_name,image_blur)', // 'mux_asset:mux_assets(playback_id,asset_id)', 'thumbnail:b2_files(cdn_url)' ] const queryObject = { select: selects.join(','), limit: pageSize, offset: page*pageSize }; const query = qs.stringify(queryObject, { encode: false }); const res = await fetch( `${postgrestLocalUrl}/vods?${query}`, Object.assign({}, fetchVodsOptions, { headers: { 'Prefer': 'count=exact' }}) ); const data = await res.json() if (!res.ok) { throw new Error(`Failed to fetch vods status=${res.status}, statusText=${res.statusText}, data=${data}`); } console.log(`${data.length} vods. sample as follows.`) console.log(data[0]) const count = getCountFromHeaders(res) return { vods: data, count: count } } export async function getAllVods(): Promise { const pageSize = 100; // Adjust this value as needed const sortDesc = true; // Adjust the sorting direction as needed const allVods: IVod[] = []; let currentPage = 1; while (true) { const query = qs.stringify({ populate: { vtuber: { fields: ['slug', 'displayName', 'image', 'imageBlur'], }, muxAsset: { fields: ['playbackId', 'assetId'], }, thumbnail: { fields: ['cdnUrl', 'url'], }, tagVodRelations: { fields: ['tag'], populate: ['tag'], }, videoSrcB2: { fields: ['url', 'key', 'uploadId', 'cdnUrl'], }, }, sort: { date: sortDesc ? 'desc' : 'asc', }, pagination: { pageSize, page: currentPage, }, }); try { const response = await fetch(`${postgrestLocalUrl}/vods?${query}`, fetchVodsOptions); if (!response.ok) { // Handle non-successful response (e.g., HTTP error) throw new Error(`HTTP error! Status: ${response.status}`); } const responseData = await response.json(); if (!responseData.data || responseData.data.length === 0) { // No more data to fetch break; } allVods.push(...responseData.data); currentPage++; } catch (error) { // Handle fetch error if (error instanceof Error) { console.error('Error fetching data:', error.message); } return null; } } return allVods; } export async function getVodsForVtuber(vtuberId: number, page: number = 1, pageSize: number = 25, sortDesc = true): Promise<{ vods: IVod[] | null, count: number }> { const res = await fetch(`${postgrestLocalUrl}/vods?limit=${pageSize}&offset=${page*pageSize}&select=*,thumbnail:b2_files(cdn_url),vtuber:vtubers(id,slug,image,display_name,image_blur)&vtuber.id=eq.${vtuberId}`, fetchVodsOptions) if (!res.ok) { const body = await res.text() console.error(`getVodsForVtuber() failed. ok=${res.ok} status=${res.status} statusText=${res.statusText} body=${body}`); return { vods: null, count: 0 }; } const vods = await res.json() as IVod[]; const count = getCountFromHeaders(res) return { vods, count }; } export async function getVodsForTag(tag: string): Promise { // const query = qs.stringify( // { // populate: { // vtuber: { // fields: ['slug', 'displayName', 'image', 'imageBlur'] // }, // videoSrcB2: { // fields: ['url', 'key', 'uploadId', 'cdnUrl'] // } // }, // filters: { // tagVodRelations: { // tag: { // name: { // $eq: tag // } // } // } // } // } // ) const query = 'select(*),vtuber:vtubers(id,slug,image,display_name,image_blur),thumbnail:b2_files(cdn_url)' const res = await fetch(`${postgrestLocalUrl}/vods?${query}`, fetchVodsOptions) if (!res.ok) return null; const vods = await res.json() return vods; } /** * This returns stale data, because futureporn-historian is broken. * @todo get live data from historian * @see https://git.futureporn.net/futureporn/futureporn-historian/issues/1 */ export async function getProgress(vtuberSlug: string): Promise<{ complete: number; total: number }> { const query = qs.stringify({ filters: { vtuber: { slug: { $eq: vtuberSlug } } } }) const data = await fetch(`${postgrestLocalUrl}/vods?${query}`, fetchVodsOptions) .then((res) => res.json()) .then((g) => { return g }) const total = data.meta.pagination.total return { complete: total, total: total } }