Files
materialious/materialious/src/lib/api/youtubejs/subscriptions.ts
T
2026-02-13 02:57:32 +13:00

174 lines
5.1 KiB
TypeScript

import { localDb } from '$lib/dexie';
import { clearCaches } from '$lib/misc';
import { cleanNumber } from '$lib/numbers';
import { relativeTimestamp } from '$lib/time';
import { get } from 'svelte/store';
import type { Feed, Subscription, Thumbnail } from '../model';
import { getChannelYTjs } from './channel';
import { engineCooldownYTStore, engineCullYTStore } from '$lib/store';
export async function getSubscriptionsYTjs(): Promise<Subscription[]> {
const subscriptions: Subscription[] = [];
await localDb.channelSubscriptions.each((subscription) => {
subscriptions.push({
author: subscription.channelName,
authorId: subscription.channelId
});
});
return subscriptions;
}
export async function amSubscribedYTjs(authorId: string): Promise<boolean> {
return (await localDb.channelSubscriptions.where('channelId').equals(authorId).count()) > 0;
}
export async function postSubscribeYTjs(
authorId: string,
authorName: string | undefined = undefined
) {
if (!authorName) {
const channel = await getChannelYTjs(authorId);
authorName = channel.author;
}
try {
await localDb.channelSubscriptions.add({
channelId: authorId,
channelName: authorName,
lastRSSFetch: new Date(0)
});
} catch {
// Continue regardless
}
// Store channel into db in background
parseChannelRSS(authorId);
}
export async function deleteUnsubscribeYTjs(authorId: string) {
await localDb.channelSubscriptions.where('channelId').equals(authorId).delete();
await localDb.subscriptionFeed.where('authorId').equals(authorId).delete();
clearCaches();
}
export async function parseChannelRSS(channelId: string): Promise<void> {
const feedUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`;
const response = await fetch(feedUrl);
const text = await response.text();
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(text, 'text/xml');
const entries = xmlDoc.getElementsByTagName('entry');
for (const entry of entries) {
const videoId = entry.getElementsByTagName('yt:videoId')[0]?.textContent || '';
const title = entry.getElementsByTagName('title')[0]?.textContent || 'Untitled';
const description =
entry.getElementsByTagName('media:description')[0]?.textContent || 'No description available';
const descriptionHtml = description;
const publishedAt = new Date(
entry.getElementsByTagName('published')[0]?.textContent || new Date()
);
const published = publishedAt.getTime();
const publishedText = relativeTimestamp(published, false);
const author =
entry.getElementsByTagName('author')[0]?.getElementsByTagName('name')[0]?.textContent ||
'Unknown Author';
const authorId =
entry
.getElementsByTagName('author')[0]
?.getElementsByTagName('uri')[0]
?.textContent.split('/')[4] || '';
const authorUrl = authorId ? `/channel/${authorId}` : '';
const authorVerified = false;
// Extracting video thumbnails
const videoThumbnails: Thumbnail[] = Array.from(
entry.getElementsByTagName('media:group')[0]?.getElementsByTagName('media:thumbnail') || []
).map((thumbElement) => ({
url: thumbElement.getAttribute('url') || '',
width: parseInt(thumbElement.getAttribute('width') || '0'),
height: parseInt(thumbElement.getAttribute('height') || '0')
}));
const liveNow = false;
const isUpcoming = false;
const premium = false;
const lengthSeconds = 0;
const viewCountText =
entry.getElementsByTagName('media:statistics')[0]?.getAttribute('views') || '0';
const viewCount = parseInt(viewCountText) || 0;
try {
await localDb.subscriptionFeed.add({
type: 'video',
videoId,
title,
videoThumbnails,
author,
authorId,
description,
descriptionHtml,
published,
publishedText,
liveNow,
premium,
isUpcoming,
lengthSeconds,
viewCountText: cleanNumber(viewCount),
viewCount,
authorUrl,
authorVerified
});
} catch {
// Continue regardless of error
}
await localDb.channelSubscriptions.where('channelId').equals(channelId).modify({
lastRSSFetch: new Date()
});
}
}
export async function getFeedYTjs(): Promise<Feed> {
const channelSubscriptions = await localDb.channelSubscriptions.toArray();
const toUpdatePromises: Promise<void>[] = [];
const now = new Date();
for (const channel of channelSubscriptions) {
const lastRSSFetch = new Date(channel.lastRSSFetch);
const timeDifference = now.getTime() - lastRSSFetch.getTime();
const cooldownTime = get(engineCooldownYTStore) * 60 * 60 * 1000;
if (timeDifference > cooldownTime) {
toUpdatePromises.push(parseChannelRSS(channel.channelId));
}
}
if (toUpdatePromises) {
await Promise.all(toUpdatePromises);
}
let videos = await localDb.subscriptionFeed.toArray();
videos.sort((a, b) => b.published - a.published);
const cullAfter = get(engineCullYTStore);
if (videos.length > cullAfter) {
const videosToDelete = videos.slice(cullAfter);
const videoIdsToDelete = videosToDelete.map((video) => video.videoId);
await localDb.subscriptionFeed.where('videoId').anyOf(videoIdsToDelete).delete();
// Don't display culled videos.
videos = videos.slice(0, cullAfter);
}
return {
notifications: [],
videos: videos
};
}