496 lines
14 KiB
TypeScript
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
|
|
}
|
|
} |