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

414 lines
13 KiB
TypeScript
Raw Normal View History

2024-01-20 16:16:14 +00:00
2024-11-05 19:48:21 +00:00
import { postgrestLocalUrl, siteUrl } from './constants';
2024-01-20 16:16:14 +00:00
import { getDateFromSafeDate, getSafeDate } from './dates';
2025-01-11 03:10:04 +00:00
import { IVtuber, IStream, ITimestamp, IVod } from '@futureporn/types';
2024-01-20 16:16:14 +00:00
import qs from 'qs';
2025-01-11 03:10:04 +00:00
import { ITagVodRelation } from './tag-vod-relations';
2024-07-15 16:07:04 +00:00
import { IMeta, IMuxAsset, IMuxAssetResponse } from '@futureporn/types';
2025-01-11 03:10:04 +00:00
import { IS3File, IS3FileResponse } from '@/app/lib/b2File';
2024-01-20 16:16:14 +00:00
import fetchAPI from './fetch-api';
import { IUserResponse } from './users';
2025-01-11 03:10:04 +00:00
import { getCountFromHeaders } from './fetchers';
2024-01-20 16:16:14 +00:00
/**
2025-01-11 03:10:04 +00:00
* Dec 2024 UUIDs were introduced.
* Going forward, use UUIDs where possible.
2024-01-20 16:16:14 +00:00
* safeDates are retained for backwards compatibility.
*
* @see https://www.w3.org/Provider/Style/URI
*/
export interface IVodPageProps {
params: {
2025-01-11 03:10:04 +00:00
safeDateOrUUID: string;
2024-01-20 16:16:14 +00:00
slug: string;
};
}
2024-12-16 20:39:23 +00:00
2024-01-20 16:16:14 +00:00
const fetchVodsOptions = {
next: {
tags: ['vods']
}
}
2025-01-11 03:10:04 +00:00
export async function getVodFromSafeDateOrUUID(safeDateOrUUID: string): Promise<IVod | null> {
2024-12-12 07:23:46 +00:00
let vod: IVod | null;
2024-01-20 16:16:14 +00:00
let date: Date;
2025-01-11 03:10:04 +00:00
if (!safeDateOrUUID) {
console.log(`safeDateOrUUID was missing`);
2024-01-20 16:16:14 +00:00
return null;
2025-01-11 03:10:04 +00:00
} 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);
2024-01-20 16:16:14 +00:00
if (!vod) return null;
} else {
2025-01-11 03:10:04 +00:00
console.log(`this is a safe date. ${safeDateOrUUID}`)
date = getDateFromSafeDate(safeDateOrUUID);
console.log(`date=${date.toISOString()}`)
2024-01-20 16:16:14 +00:00
if (!date) {
console.log('there is no date')
return null;
}
vod = await getVodForDate(date);
}
return vod;
}
2025-01-11 03:10:04 +00:00
2024-01-20 16:16:14 +00:00
export function getUrl(vod: IVod, slug: string, date: string): string {
2024-03-29 07:28:02 +00:00
return `/vt/${slug}/vod/${getSafeDate(date)}`
2024-01-20 16:16:14 +00:00
}
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 {
2025-01-11 03:10:04 +00:00
return `${siteUrl}/vods/${getSafeDate(vod.date_2)}`
2024-01-20 16:16:14 +00:00
}
2025-01-11 03:10:04 +00:00
export async function getNextVod(vod: IVod): Promise<IVod|null> {
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];
2024-01-20 16:16:14 +00:00
}
2025-01-11 03:10:04 +00:00
2024-01-20 16:16:14 +00:00
export function getLocalizedDate(vod: IVod): string {
2025-01-11 03:10:04 +00:00
return new Date(vod.date_2).toLocaleDateString()
2024-01-20 16:16:14 +00:00
}
export async function getPreviousVod(vod: IVod): Promise<IVod> {
2025-01-11 03:10:04 +00:00
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];
2024-01-20 16:16:14 +00:00
}
2025-01-11 03:10:04 +00:00
export async function getVodByUUID(uuid: string): Promise<IVod | null> {
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
});
2024-01-20 16:16:14 +00:00
2024-12-12 07:23:46 +00:00
try {
const res = await fetch(`${postgrestLocalUrl}/vods?${query}`, { cache: 'no-store', next: { tags: ['vods'] } })
if (!res.ok) {
2025-01-11 03:10:04 +00:00
throw new Error('failed to fetch getVodByUUID')
2024-12-12 07:23:46 +00:00
}
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)
2024-01-20 16:16:14 +00:00
}
2024-12-12 07:23:46 +00:00
return null;
}
2024-01-20 16:16:14 +00:00
}
export async function getVodForDate(date: Date): Promise<IVod | null> {
2025-01-11 03:10:04 +00:00
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',
};
2024-01-20 16:16:14 +00:00
2025-01-11 03:10:04 +00:00
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');
2024-01-20 16:16:14 +00:00
}
2025-01-11 03:10:04 +00:00
const vod = json[0]; // PostgREST returns an array of results
if (!vod) return null;
return vod;
2024-01-20 16:16:14 +00:00
}
2025-01-11 03:10:04 +00:00
2024-12-12 07:23:46 +00:00
export async function getVod(id: number): Promise<IVod | null> {
2024-01-20 16:16:14 +00:00
const query = qs.stringify(
{
filters: {
id: {
$eq: id
}
}
}
)
2024-11-05 19:48:21 +00:00
const res = await fetch(`${postgrestLocalUrl}/vods?${query}`, fetchVodsOptions);
2024-01-20 16:16:14 +00:00
if (!res.ok) return null;
const data = await res.json();
return data;
}
2024-12-16 20:39:23 +00:00
export async function getVods(page: number = 1, pageSize: number = 25, sortDesc = true): Promise<{ vods: IVod[], count: number }> {
2024-12-12 07:23:46 +00:00
// const query = qs.stringify(
2024-12-16 20:39:23 +00:00
// {
2024-12-12 07:23:46 +00:00
// 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)',
2024-12-16 20:39:23 +00:00
'thumbnail:b2_files(cdn_url)'
2024-12-12 07:23:46 +00:00
]
const queryObject = {
select: selects.join(','),
2024-12-16 20:39:23 +00:00
limit: pageSize,
offset: page*pageSize
2024-12-12 07:23:46 +00:00
};
const query = qs.stringify(queryObject, { encode: false });
2024-01-20 16:16:14 +00:00
2024-12-12 07:23:46 +00:00
const res = await fetch(
`${postgrestLocalUrl}/vods?${query}`,
2024-12-16 20:39:23 +00:00
Object.assign({}, fetchVodsOptions, { headers: {
'Prefer': 'count=exact'
}})
2024-12-12 07:23:46 +00:00
);
2024-12-16 20:39:23 +00:00
const data = await res.json()
2024-01-20 16:16:14 +00:00
if (!res.ok) {
2024-12-16 20:39:23 +00:00
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])
2025-01-11 03:10:04 +00:00
const count = getCountFromHeaders(res)
2024-12-16 20:39:23 +00:00
return {
vods: data,
count: count
2024-01-20 16:16:14 +00:00
}
}
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 {
2024-11-05 19:48:21 +00:00
const response = await fetch(`${postgrestLocalUrl}/vods?${query}`, fetchVodsOptions);
2024-01-20 16:16:14 +00:00
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;
}
2025-01-11 03:10:04 +00:00
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)
2024-12-12 07:23:46 +00:00
if (!res.ok) {
const body = await res.text()
console.error(`getVodsForVtuber() failed. ok=${res.ok} status=${res.status} statusText=${res.statusText} body=${body}`);
2025-01-11 03:10:04 +00:00
return { vods: null, count: 0 };
2024-12-12 07:23:46 +00:00
}
2025-01-11 03:10:04 +00:00
const vods = await res.json() as IVod[];
const count = getCountFromHeaders(res)
return { vods, count };
2024-01-20 16:16:14 +00:00
}
2025-01-11 03:10:04 +00:00
export async function getVodsForTag(tag: string): Promise<IVod[] | 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 query = 'select(*),vtuber:vtubers(id,slug,image,display_name,image_blur),thumbnail:b2_files(cdn_url)'
2024-11-05 19:48:21 +00:00
const res = await fetch(`${postgrestLocalUrl}/vods?${query}`, fetchVodsOptions)
2024-01-20 16:16:14 +00:00
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
}
}
}
})
2024-11-05 19:48:21 +00:00
const data = await fetch(`${postgrestLocalUrl}/vods?${query}`, fetchVodsOptions)
2024-01-20 16:16:14 +00:00
.then((res) => res.json())
.then((g) => {
return g
})
const total = data.meta.pagination.total
return {
complete: total,
total: total
}
}