-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(AniTilky): update activity for anitilky.com #11001
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 10 commits
76d2e8f
0a5e50e
d5b7b0b
f5904c3
ae8c3e1
9d297bf
115bfcd
df10eb9
1d33446
bdd77bc
ff45ea6
52e0975
8fa4e60
d7de6a6
8c0a327
f98b794
f0cb030
b64f313
94bac12
3281f13
c5ceb32
a1fe1b5
b610889
51ba7d6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import type { Anime } from '../types.js' | ||
| import { API_URL, fetchCached } from './helpers.js' | ||
|
|
||
| export async function fetchAnime(id: string): Promise<Anime | null> { | ||
| return fetchCached(`anime:${id}`, async () => { | ||
| try { | ||
| const response = await fetch(`${API_URL}/anime/${encodeURIComponent(id)}`) | ||
| if (!response.ok) | ||
| return null | ||
| return await response.json() as Anime | ||
| } | ||
| catch { | ||
| return null | ||
| } | ||
| }) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import type { UserProfile } from '../types.js' | ||
| import { API_URL, fetchCached } from './helpers.js' | ||
|
|
||
| export async function fetchUser(username: string): Promise<UserProfile | null> { | ||
| return fetchCached(`user:${username.toLowerCase()}`, async () => { | ||
| try { | ||
| const response = await fetch(`${API_URL}/user/profile/${encodeURIComponent(username)}`) | ||
| if (!response.ok) | ||
| return null | ||
| return await response.json() as UserProfile | ||
| } | ||
| catch { | ||
| return null | ||
| } | ||
| }) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,230 @@ | ||
| import type { Anime, AnimeEpisode, AnimeSeason } from '../types.js' | ||
|
|
||
| export const BASE_URL = 'https://anitilky.com' | ||
| export const API_URL = 'https://api.anitilky.com/api' | ||
|
|
||
| export enum ActivityAssets { | ||
| Logo = 'https://cdn.rcd.gg/PreMiD/websites/A/AniTilky/assets/logo.png', | ||
| } | ||
|
|
||
| const dataCache = new Map<string, { data: unknown, expires: number }>() | ||
| const blobCache = new Map<string, Blob>() | ||
| const inflight = new Map<string, Promise<Blob | undefined>>() | ||
| const CACHE_TTL = 60_000 | ||
|
|
||
| export async function fetchCached<T>(key: string, fetcher: () => Promise<T | null>): Promise<T | null> { | ||
| const cached = dataCache.get(key) | ||
| if (cached && cached.expires > Date.now()) | ||
| return cached.data as T | ||
|
|
||
| const data = await fetcher() | ||
| if (data) | ||
| dataCache.set(key, { data, expires: Date.now() + CACHE_TTL }) | ||
|
|
||
| return data | ||
| } | ||
|
|
||
| export function getAnimeTitle(anime?: Anime | null): string | undefined { | ||
| if (!anime?.title) | ||
| return undefined | ||
| return anime.title.romaji || anime.title.english || anime.title.native | ||
| } | ||
|
|
||
| export function findSeason(anime: Anime | null | undefined, seasonNumber: number): AnimeSeason | undefined { | ||
| return anime?.seasons?.find(s => Number(s.seasonNumber) === Number(seasonNumber)) | ||
| } | ||
|
|
||
| export function findEpisode(season: AnimeSeason | undefined, episodeNumber: number): AnimeEpisode | undefined { | ||
| return season?.episodes?.find(e => Number(e.episodeNumber) === Number(episodeNumber)) | ||
| } | ||
|
|
||
| export function getPageTitle(): string { | ||
| return document.title | ||
| .replace(/\s*\|\s*ANITILKY.*$/i, '') | ||
| .replace(/\s*Türkçe.*$/i, '') | ||
| .trim() | ||
| } | ||
|
|
||
| export function getStoredUsername(): string | undefined { | ||
| try { | ||
| const raw = localStorage.getItem('userData') || sessionStorage.getItem('userData') | ||
| if (!raw) | ||
| return undefined | ||
| return (JSON.parse(raw) as { username?: string }).username | ||
| } | ||
| catch { | ||
| return undefined | ||
| } | ||
| } | ||
|
|
||
| export function getProfileUsernameFromDom(): string | undefined { | ||
| return document.querySelector<HTMLElement>('main h4, [class*="MuiTypography-h4"]')?.textContent?.trim() | ||
| || getStoredUsername() | ||
| } | ||
|
|
||
| export function getProfileImageUrl(): string | undefined { | ||
| const selectors = [ | ||
| 'img[src*="profile-images"]', | ||
| 'img[src*="b-cdn.net"][src*="profile"]', | ||
| '[class*="MuiAvatar"] img', | ||
| ] | ||
|
|
||
| for (const selector of selectors) { | ||
| const img = document.querySelector<HTMLImageElement>(selector) | ||
| const src = img?.currentSrc || img?.src | ||
| if (src && !src.startsWith('data:')) | ||
| return src | ||
| } | ||
|
|
||
| return undefined | ||
| } | ||
|
|
||
| export function getOgImage(): string | undefined { | ||
| return document.querySelector<HTMLMetaElement>('meta[property="og:image"]')?.content | ||
| || document.querySelector<HTMLMetaElement>('meta[name="twitter:image"]')?.content | ||
| } | ||
|
|
||
| export function parsePathSegments(pathname: string): string[] { | ||
| return pathname.split('/').filter(Boolean) | ||
| } | ||
|
|
||
| export function formatEpisodeState( | ||
| season: number, | ||
| episode: number, | ||
| episodeTitle?: string, | ||
| ): string { | ||
| const base = `S${season} E${episode}` | ||
| return episodeTitle ? `${base} — ${episodeTitle}` : base | ||
| } | ||
|
|
||
| function normalizeUrl(url?: string | null): string | undefined { | ||
| if (!url) | ||
| return undefined | ||
| try { | ||
| return new URL(url, document.location.origin).href | ||
| } | ||
| catch { | ||
| return undefined | ||
| } | ||
| } | ||
|
|
||
| function isGifUrl(url: string): boolean { | ||
| return /\.gif(?:$|\?)/i.test(url) | ||
| } | ||
|
|
||
| /** | ||
| * BunnyCDN needs a site Referer (curl -e anitilky.com). Browser fetch from the page works. | ||
| * Return raw Blob — GIF stays GIF, everything else kept as fetched (or JPEG if mistyped). | ||
| */ | ||
| async function curlImageBlob(url: string): Promise<Blob | undefined> { | ||
| try { | ||
| const controller = new AbortController() | ||
| const timer = window.setTimeout(() => controller.abort(), 5000) | ||
|
|
||
| const response = await fetch(url, { | ||
| method: 'GET', | ||
| mode: 'cors', | ||
| credentials: 'omit', | ||
| referrer: `${BASE_URL}/`, | ||
| referrerPolicy: 'origin', | ||
| signal: controller.signal, | ||
| headers: { | ||
| Accept: 'image/gif,image/webp,image/avif,image/apng,image/*,*/*;q=0.8', | ||
| }, | ||
| }) | ||
|
|
||
| window.clearTimeout(timer) | ||
|
|
||
| if (!response.ok) | ||
| return undefined | ||
|
|
||
| const buffer = await response.arrayBuffer() | ||
| if (buffer.byteLength < 100) | ||
| return undefined | ||
|
|
||
| const headerType = (response.headers.get('content-type') || '').split(';')[0]!.trim().toLowerCase() | ||
| const wantGif = isGifUrl(url) || headerType === 'image/gif' | ||
| const type = wantGif | ||
| ? 'image/gif' | ||
| : (headerType.startsWith('image/') ? headerType : 'image/jpeg') | ||
|
|
||
| return new Blob([buffer], { type }) | ||
| } | ||
| catch { | ||
| return undefined | ||
| } | ||
| } | ||
|
|
||
| function ensureBlobFetch(url: string): void { | ||
| if (blobCache.has(url) || inflight.has(url)) | ||
| return | ||
|
|
||
| const job = curlImageBlob(url).then((blob) => { | ||
| if (blob) | ||
| blobCache.set(url, blob) | ||
| inflight.delete(url) | ||
| return blob | ||
| }).catch(() => { | ||
| inflight.delete(url) | ||
| return undefined | ||
| }) | ||
|
|
||
| inflight.set(url, job) | ||
| } | ||
|
|
||
| /** | ||
| * Non-blocking: return cached Blob immediately, otherwise Logo and prefetch in background. | ||
| * Next UpdateData tick will show the BunnyCDN image / GIF. | ||
| */ | ||
| export function resolvePresenceImage( | ||
| ...candidates: Array<string | undefined> | ||
| ): string | Blob { | ||
| const urls = [...candidates, getOgImage()] | ||
| .map(normalizeUrl) | ||
| .filter((url): url is string => Boolean(url)) | ||
|
|
||
| for (const url of urls) { | ||
| const cached = blobCache.get(url) | ||
| if (cached) | ||
| return cached | ||
| } | ||
|
|
||
| for (const url of urls) | ||
| ensureBlobFetch(url) | ||
|
|
||
| return ActivityAssets.Logo | ||
| } | ||
|
|
||
| /** Optional short wait used once when we already have an inflight fetch. */ | ||
| export async function resolvePresenceImageAsync( | ||
| ...candidates: Array<string | undefined> | ||
| ): Promise<string | Blob> { | ||
| const urls = [...candidates, getOgImage()] | ||
| .map(normalizeUrl) | ||
| .filter((url): url is string => Boolean(url)) | ||
|
|
||
| for (const url of urls) { | ||
| const cached = blobCache.get(url) | ||
| if (cached) | ||
| return cached | ||
| } | ||
|
|
||
| for (const url of urls) | ||
| ensureBlobFetch(url) | ||
|
|
||
| // Wait briefly for the first candidate only (does not hang presence forever) | ||
| const first = urls[0] | ||
| if (first) { | ||
| const pending = inflight.get(first) | ||
| if (pending) { | ||
| const blob = await Promise.race([ | ||
| pending, | ||
| new Promise<undefined>(resolve => window.setTimeout(() => resolve(undefined), 1200)), | ||
Check failureCode scanning / ESLint Prefer passing function and arguments directly to setTimeout/setInterval instead of wrapping in an arrow function or using bind Error
Pass function and arguments directly to timer function to avoid allocating an extra function
|
||
| ]) | ||
| if (blob) | ||
| return blob | ||
| } | ||
| } | ||
|
|
||
| return ActivityAssets.Logo | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| export interface PresenceStrings { | ||
| play: string | ||
| pause: string | ||
| browse: string | ||
| search: string | ||
| viewPage: string | ||
| viewSeries: string | ||
| watchEpisode: string | ||
| viewProfile: string | ||
| watchingSeries: string | ||
| seasonEpisode: string | ||
| home: string | ||
| animeList: string | ||
| schedule: string | ||
| users: string | ||
| notifications: string | ||
| settings: string | ||
| donation: string | ||
| login: string | ||
| register: string | ||
| admin: string | ||
| viewingAnime: string | ||
| viewingProfile: string | ||
| ownProfile: string | ||
| tabPosts: string | ||
| tabMedia: string | ||
| tabWatchlist: string | ||
| tabFavorites: string | ||
| tabHistory: string | ||
| privacyWatching: string | ||
| privacyBrowsing: string | ||
| } | ||
|
|
||
| const SITE_STRINGS: Record<'en' | 'tr', Omit<PresenceStrings, | ||
| 'play' | 'pause' | 'browse' | 'search' | 'viewPage' | 'viewSeries' | 'watchEpisode' | 'watchingSeries' | ||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| >> = { | ||
| en: { | ||
Check failureCode scanning / ESLint Having line breaks styles to object, array and named imports Error
Should not have line breaks between items, in node TSTypeParameterInstantiation
Check failureCode scanning / ESLint Having line breaks styles to object, array and named imports Error
Should not have line breaks between items, in node TSTypeParameterInstantiation
|
||
| viewProfile: 'View profile', | ||
| seasonEpisode: 'Season {0} · Episode {1}', | ||
| home: 'Home', | ||
| animeList: 'Anime catalog', | ||
| schedule: 'Release schedule', | ||
| users: 'Community', | ||
| notifications: 'Notifications', | ||
| settings: 'Account settings', | ||
| donation: 'Support AniTilky', | ||
| login: 'Signing in', | ||
| register: 'Creating account', | ||
| admin: 'Admin panel', | ||
| viewingAnime: 'Viewing anime', | ||
| viewingProfile: 'Viewing profile', | ||
| ownProfile: 'My profile', | ||
| tabPosts: 'Posts', | ||
| tabMedia: 'Media', | ||
| tabWatchlist: 'Watchlist', | ||
| tabFavorites: 'Favorites', | ||
| tabHistory: 'Watch history', | ||
| privacyWatching: 'Watching content', | ||
| privacyBrowsing: 'Browsing AniTilky', | ||
| }, | ||
| tr: { | ||
| viewProfile: 'Profile git', | ||
| seasonEpisode: 'Sezon {0} · Bölüm {1}', | ||
| home: 'Ana sayfa', | ||
| animeList: 'Anime kataloğu', | ||
| schedule: 'Yayın takvimi', | ||
| users: 'Topluluk', | ||
| notifications: 'Bildirimler', | ||
| settings: 'Hesap ayarları', | ||
| donation: 'AniTilky\'ye destek', | ||
| login: 'Giriş yapılıyor', | ||
| register: 'Hesap oluşturuluyor', | ||
| admin: 'Yönetim paneli', | ||
| viewingAnime: 'Anime inceleniyor', | ||
| viewingProfile: 'Profil görüntüleniyor', | ||
| ownProfile: 'Kendi profili', | ||
| tabPosts: 'Gönderiler', | ||
| tabMedia: 'Medya', | ||
| tabWatchlist: 'İzleme listesi', | ||
| tabFavorites: 'Favoriler', | ||
| tabHistory: 'İzleme geçmişi', | ||
| privacyWatching: 'İçerik izleniyor', | ||
| privacyBrowsing: 'AniTilky\'de geziniyor', | ||
| }, | ||
| } | ||
|
|
||
| export async function getStrings(presence: Presence, lang: string): Promise<PresenceStrings> { | ||
| const normalized = lang.startsWith('tr') ? 'tr' : 'en' | ||
| const general = await presence.getStrings( | ||
Check failureCode scanning / ESLint Disallow using code marked as `@deprecated` Error
getStrings is deprecated. 2.5 - Passing language is deprecated
Get translations from the extension |
||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| { | ||
| play: 'general.playing', | ||
| pause: 'general.paused', | ||
| browse: 'general.browsing', | ||
| search: 'general.search', | ||
| viewPage: 'general.viewPage', | ||
| viewSeries: 'general.buttonViewSeries', | ||
| watchEpisode: 'general.buttonViewEpisode', | ||
| watchingSeries: 'general.watchingSeries', | ||
| }, | ||
| lang, | ||
| ) | ||
|
|
||
| return { ...general, ...SITE_STRINGS[normalized] } | ||
| } | ||
|
|
||
| export type ProfileTabKey = keyof Pick<PresenceStrings, | ||
| 'tabPosts' | 'tabMedia' | 'tabWatchlist' | 'tabFavorites' | 'tabHistory' | ||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| > | ||
|
|
||
Check failureCode scanning / ESLint Having line breaks styles to object, array and named imports Error
Should not have line breaks between items, in node TSTypeParameterInstantiation
Check failureCode scanning / ESLint Having line breaks styles to object, array and named imports Error
Should not have line breaks between items, in node TSTypeParameterInstantiation
|
||
| export const PROFILE_TABS: Record<string, ProfileTabKey> = { | ||
| posts: 'tabPosts', | ||
| media: 'tabMedia', | ||
| watchlist: 'tabWatchlist', | ||
| favorites: 'tabFavorites', | ||
| history: 'tabHistory', | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.