Implemented channel avatars on feed

This commit is contained in:
WardPearce
2026-03-07 23:59:03 +13:00
parent 9c1767e6d7
commit 2b141b82ee
7 changed files with 112 additions and 34 deletions
@@ -1,3 +1,4 @@
import { associateAvatar } from '$lib/thumbnail';
import type { ChannelContent, ChannelOptions, ChannelPage } from '../model';
import { buildPath, fetchErrorHandle } from './request';
@@ -8,7 +9,13 @@ export async function getChannelInvidious(
const resp = await fetchErrorHandle(
await fetch(buildPath(`channels/${channelId}`), fetchOptions)
);
return await resp.json();
const respJson = await resp.json();
if (resp.ok) {
associateAvatar(channelId, respJson.authorBanners);
}
return respJson;
}
export async function getChannelContentInvidious(
+9 -1
View File
@@ -4,6 +4,7 @@ import { buildPath, fetchErrorHandle } from './request';
import { getVideoYTjs } from '../youtubejs/video';
import { playerYouTubeJsFallback } from '$lib/store';
import { isUnrestrictedPlatform } from '$lib/misc';
import { associateAvatar } from '$lib/thumbnail';
export async function getVideoInvidious(
videoId: string,
@@ -20,5 +21,12 @@ export async function getVideoInvidious(
} else {
await fetchErrorHandle(resp);
}
return await resp.json();
const respJson = await resp.json();
if (resp.ok) {
associateAvatar(respJson.authorId, respJson.authorThumbnails);
}
return respJson;
}
@@ -9,6 +9,7 @@ import type {
Video
} from '../model';
import { invidiousItemSchema } from './schema';
import { associateAvatar } from '$lib/thumbnail';
export async function getChannelYTjs(channelId: string): Promise<ChannelPage> {
const innertube = await getInnertube();
@@ -23,6 +24,7 @@ export async function getChannelYTjs(channelId: string): Promise<ChannelPage> {
innerResults.header.content?.is(YTNodes.PageHeaderView)
) {
authorBanners = innerResults.header.content.banner?.image ?? [];
associateAvatar(channelId, authorBanners);
}
const description = innerResults.metadata.description ?? '';
@@ -17,6 +17,7 @@ import { Utils, YT, YTNodes, Platform, type IGetChallengeResponse } from 'youtub
import { getInnertube } from '.';
import { isUnrestrictedPlatform } from '$lib/misc';
import { webPoTokenMinter } from '$lib/web/youtube/minter';
import { associateAvatar } from '$lib/thumbnail';
Platform.shim.eval = async (
data: Types.BuildScriptResult,
@@ -149,6 +150,8 @@ export async function getVideoYTjs(videoId: string): Promise<VideoPlay> {
if (video.basic_info.channel_id) {
const channel = await innertube.getChannel(video.basic_info.channel_id);
authorThumbnails = channel.metadata.avatar as Image[];
associateAvatar(video.basic_info.channel_id, authorThumbnails);
} else {
authorThumbnails = [];
}
@@ -19,6 +19,8 @@
import { queueGetWatchHistory } from '$lib/api/historyPool';
import { page } from '$app/state';
import { getDeArrow, getThumbnailDeArrow } from '$lib/api/dearrow';
import { avatarFromChannelId } from '$lib/thumbnail';
import { mergeAttrs } from 'melt';
interface Props {
video: VideoBase | Video | Notification | PlaylistPageVideo | VideoWatchHistory;
@@ -70,6 +72,7 @@
}
let thumbnailHeight = $state(0);
let thumbnailHTMLElement: HTMLImageElement | undefined = $state();
const thumbnail = new Avatar({
src: () => thumbnailSrc,
@@ -78,7 +81,10 @@
}
});
let thumbnailHTMLElement: HTMLImageElement | undefined = $state();
let authorAvatarSrc = $state('');
const authorAvatar = new Avatar({
src: () => authorAvatarSrc
});
let startedSideways = sideways === true;
function disableSideways() {
@@ -90,6 +96,12 @@
}
onMount(async () => {
if ('authorId' in video) {
avatarFromChannelId(video.authorId).then((url) => {
if (url) authorAvatarSrc = url;
});
}
// Check if sideways should be enabled or disabled.
disableSideways();
@@ -181,39 +193,50 @@
<span class="bold">{letterCase(video.title.trimEnd())}</span>
</a>
<div>
{#if 'authorId' in video && video.authorId}
<a
tabindex="-1"
class:author={!sideways}
href={resolve(`/channel/[authorId]`, { authorId: video.authorId })}
data-sveltekit-preload-data="off"
>{video.author}
</a>
{:else}
<p>{video.author}</p>
<nav>
{#if !sideways}
<img class="circle small" {...authorAvatar.image} alt="Channel profile" />
<button
class="secondary-container"
{...mergeAttrs(authorAvatar.fallback, {
style: 'text-transform: uppercase;border-radius: 2.5rem !important;'
})}>{video.author[0]}</button
>
{/if}
<div>
{#if 'authorId' in video && video.authorId}
<a
tabindex="-1"
class:author={!sideways}
href={resolve(`/channel/[authorId]`, { authorId: video.authorId })}
data-sveltekit-preload-data="off"
>{video.author}
</a>
{:else}
<p>{video.author}</p>
{/if}
{#if 'promotedBy' in video && video.promotedBy === 'favourited'}
<i>star</i>
{/if}
{#if 'promotedBy' in video && video.promotedBy === 'favourited'}
<i>star</i>
{/if}
{#if !('publishedText' in video) && 'viewCountText' in video}
{video.viewCountText ?? cleanNumber(video.viewCount ?? 0)}
{$_('views')}
{/if}
{#if 'published' in video}
<div class="max">
{video.viewCountText ?? cleanNumber(video.viewCount ?? 0)}
{#if !('publishedText' in video) && 'viewCountText' in video}
{video.published && video.published !== 0
? relativeTimestamp(video.published * 1000, false)
: video.publishedText}
</div>
{/if}
</div>
{video.viewCountText ?? cleanNumber(video.viewCount ?? 0)}
{$_('views')}
{/if}
{#if 'published' in video}
<div>
{video.viewCountText ?? cleanNumber(video.viewCount ?? 0)}
{video.published && video.published !== 0
? relativeTimestamp(video.published * 1000, false)
: video.publishedText}
</div>
{/if}
</div>
</nav>
</div>
</div>
</div>
+10 -2
View File
@@ -13,19 +13,27 @@ export interface ChannelSubscriptions {
lastRSSFetch: Date;
}
export interface ChannelAvatars {
channelId: string;
avatarUrl: string;
updated: Date;
}
export class MaterialiousDb extends Dexie {
favouriteChannels!: Table<FavouriteChannels>;
channelSubscriptions!: Table<ChannelSubscriptions>;
subscriptionFeed!: Table<Video>;
watchHistory!: Table<VideoWatchHistory>;
channelAvatars!: Table<ChannelAvatars>;
constructor() {
super('materialious');
this.version(3).stores({
this.version(4).stores({
favouriteChannels: 'channelId',
channelSubscriptions: 'channelId',
subscriptionFeed: 'videoId, authorId, published',
watchHistory: 'videoId'
watchHistory: 'videoId',
channelAvatars: 'channelId'
});
}
}
+27
View File
@@ -0,0 +1,27 @@
import type { Image } from './api/model';
import { localDb } from './dexie';
import { getBestThumbnail } from './images';
export async function associateAvatar(channelId: string, avatars: Image[]) {
if (avatars.length === 0) return;
await localDb.channelAvatars.put(
{
channelId: channelId,
avatarUrl: getBestThumbnail(avatars),
updated: new Date()
},
{ channelId: channelId }
);
const oneMonthAgo = new Date();
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
localDb.channelAvatars.where('updated').below(oneMonthAgo).delete();
}
export async function avatarFromChannelId(channelId: string): Promise<void | string> {
const result = await localDb.channelAvatars.get({ channelId });
if (result) {
return result.avatarUrl;
}
}