import { postgrestLocalUrl, siteUrl } from './constants'; import { getDateFromSafeDate, getSafeDate } from './dates'; import { IVtuber, IStream, IStreamResponse } from '@futureporn/types'; import qs from 'qs'; import { ITagVodRelationsResponse } from './tag-vod-relations'; import { ITimestampsResponse } from './timestamps'; import { IMeta, IMuxAsset, IMuxAssetResponse } from '@futureporn/types'; import { IB2File, IB2FileResponse } from '@/app/lib/b2File'; import fetchAPI from './fetch-api'; import { IUserResponse } from './users'; /** * Dec 2023 CUIDs were introduced. * Going forward, use CUIDs where possible. * safeDates are retained for backwards compatibility. * * @see https://www.w3.org/Provider/Style/URI */ export interface IVodPageProps { params: { safeDateOrCuid: string; slug: string; }; } export interface IVodsResponse { data: IVod[]; meta: IMeta; } export interface IVodResponse { data: IVod; meta: IMeta; } export interface IVod { id: number; stream: IStreamResponse; published_at?: string; cuid: string; title?: string; duration?: number; date: string; date2: string; mux_asset: IMuxAsset; thumbnail?: IB2File; vtuber: IVtuber; tag_vod_relations: ITagVodRelationsResponse; timestamps: ITimestampsResponse; video240Hash: string; videoSrcHash: string; videoSrcB2: IB2FileResponse | null; announce_title: string; announce_url: string; uploader: IUserResponse; note: string; } const fetchVodsOptions = { next: { tags: ['vods'] } } export async function getVodFromSafeDateOrCuid(safeDateOrCuid: string): Promise { let vod: IVod | null; let date: Date; if (!safeDateOrCuid) { console.log(`safeDateOrCuid was missing`); return null; } else if (/^[0-9a-z]{10}$/.test(safeDateOrCuid)) { console.log('this is a CUID!'); vod = await getVodByCuid(safeDateOrCuid); if (!vod) return null; } else { date = await getDateFromSafeDate(safeDateOrCuid); 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.date2)}` } export async function getNextVod(vod: IVod): Promise { const query = qs.stringify({ filters: { date2: { $gt: vod.date2 }, vtuber: { slug: { $eq: vod.vtuber.slug } }, publishedAt: { $notNull: true } }, sort: { date2: 'asc' }, fields: ['date2', 'title', 'announceTitle'], populate: { vtuber: { fields: ['slug'] } } }) const res = await fetch(`${postgrestLocalUrl}/vods?${query}`, fetchVodsOptions); if (!res.ok) throw new Error('could not fetch next vod'); const json = await res.json(); const nextVod = json.data[0]; if (!nextVod) return null; return nextVod } export function getLocalizedDate(vod: IVod): string { return new Date(vod.date2).toLocaleDateString() } export async function getPreviousVod(vod: IVod): Promise { const res = await fetchAPI( '/vods', { filters: { date2: { $lt: vod.date2 }, vtuber: { slug: { $eq: vod.vtuber.slug } } }, sort: { date2: 'desc' }, fields: ['date2', 'title', 'announceTitle'], populate: { vtuber: { fields: ['slug'] } }, pagination: { limit: 1 } }, fetchVodsOptions ) return res.data[0]; } export async function getVodByCuid(cuid: string): Promise { const query = qs.stringify( { filters: { cuid: { $eq: cuid } }, populate: { vtuber: { fields: ['slug', 'displayName', 'image', 'imageBlur', 'themeColor'] }, muxAsset: { fields: ['playbackId', 'assetId'] }, thumbnail: { fields: ['cdnUrl', 'url'] }, tagVodRelations: { fields: ['tag', 'createdAt', 'creatorId'], populate: ['tag'] }, videoSrcB2: { fields: ['url', 'key', 'uploadId', 'cdnUrl'] }, stream: { fields: ['archiveStatus', 'date', 'tweet', 'cuid'] } } }) try { const res = await fetch(`${postgrestLocalUrl}/vods?${query}`, { cache: 'no-store', next: { tags: ['vods'] } }) if (!res.ok) { throw new Error('failed to fetch vodForDate') } 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 { // if (!date) return null; // console.log(date) // console.log(`getting vod for ${date.toISOString()}`) try { const iso8601DateString = date.toISOString().split('T')[0]; const query = qs.stringify( { filters: { date2: { $eq: date.toISOString() } }, populate: { vtuber: { fields: ['slug', 'displayName', 'image', 'imageBlur', 'themeColor'] }, muxAsset: { fields: ['playbackId', 'assetId'] }, thumbnail: { fields: ['cdnUrl', 'url'] }, tagVodRelations: { fields: ['tag', 'createdAt', 'creatorId'], populate: ['tag'] }, videoSrcB2: { fields: ['url', 'key', 'uploadId', 'cdnUrl'] }, stream: { fields: ['archiveStatus', 'date', 'tweet', 'cuid'] } } } ) const res = await fetch(`${postgrestLocalUrl}/vods?${query}`, { cache: 'no-store', next: { tags: ['vods'] } }) if (!res.ok) { throw new Error('failed to fetch vodForDate') } const json = await res.json() const vod = json.data[0] if (!vod) return null; return vod; } catch (e) { return null; } } 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 { // 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(','), }; const query = qs.stringify(queryObject, { encode: false }); const res = await fetch( `${postgrestLocalUrl}/vods?${query}`, fetchVodsOptions ); if (!res.ok) { const body = await res.text() throw new Error(`Failed to fetch vods status=${res.status}, statusText=${res.statusText}, body=${body}`); } const json = await res.json() // console.log('vods as follows') // console.log(json) return json; } 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 { const res = await fetch(`${postgrestLocalUrl}/vods?select=*,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 null; } const data = await res.json() as IVod[]; return data; } 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 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 } }