fp/services/next/app/lib/vods.ts

496 lines
14 KiB
TypeScript

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<IVod | null> {
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<IVod | null> {
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<IVod> {
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<IVod | null> {
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<IVod | null> {
// 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<IVod | null> {
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])
// https://postgrest.org/en/latest/references/api/pagination_count.html
// HTTP/1.1 206 Partial Content
// Content-Range: 0-24/3572000
const count = parseInt(res.headers.get('Content-Range')?.split('/').at(-1) || '0')
return {
vods: data,
count: count
}
}
export async function getAllVods(): Promise<IVod[] | null> {
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<IVod[] | null> {
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<IVodsResponse | null> {
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
}
}