-
-
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 9 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,182 @@ | ||
| 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 coverCache = new Map<string, string>() | ||
| 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 getProfileImageElement(): HTMLImageElement | 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) | ||
| if (img?.complete && img.naturalWidth > 0) | ||
| return img | ||
| } | ||
|
|
||
| return undefined | ||
| } | ||
|
|
||
| export function getCoverImageElement(): HTMLImageElement | undefined { | ||
| const selectors = [ | ||
| 'img[src*="cover.jpg"]', | ||
| 'img[src*="cover.png"]', | ||
| 'img[src*="/cover"]', | ||
| 'img[src*="anilist.co"]', | ||
| '[class*="MuiPaper"] img', | ||
| ] | ||
|
|
||
| for (const selector of selectors) { | ||
| const img = document.querySelector<HTMLImageElement>(selector) | ||
| if (img?.complete && img.naturalWidth > 40) | ||
| return img | ||
| } | ||
|
|
||
| return undefined | ||
| } | ||
|
|
||
| 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 | ||
| } | ||
|
|
||
| async function fetchAniListCover(title?: string): Promise<string | undefined> { | ||
| if (!title) | ||
| return undefined | ||
|
|
||
| const cached = coverCache.get(`anilist:${title}`) | ||
| if (cached) | ||
| return cached | ||
|
|
||
| try { | ||
| const controller = new AbortController() | ||
| const timer = window.setTimeout(() => controller.abort(), 2000) | ||
| const response = await fetch('https://graphql.anilist.co', { | ||
| method: 'POST', | ||
| signal: controller.signal, | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| Accept: 'application/json', | ||
| }, | ||
| body: JSON.stringify({ | ||
| query: `query ($search: String) { | ||
| Media(search: $search, type: ANIME) { | ||
| coverImage { extraLarge large } | ||
| } | ||
| }`, | ||
| variables: { search: title }, | ||
| }), | ||
| }) | ||
| window.clearTimeout(timer) | ||
|
|
||
| if (!response.ok) | ||
| return undefined | ||
|
|
||
| const json = await response.json() as { | ||
| data?: { Media?: { coverImage?: { extraLarge?: string, large?: string } } } | ||
| } | ||
| const cover = json.data?.Media?.coverImage?.extraLarge | ||
| || json.data?.Media?.coverImage?.large | ||
|
|
||
| if (cover) | ||
| coverCache.set(`anilist:${title}`, cover) | ||
|
|
||
| return cover | ||
| } | ||
| catch { | ||
| return undefined | ||
Check failureCode scanning / ESLint Require quotes around object literal, type literal, interfaces and enums property names Error
Inconsistently quoted property 'Accept' found.
|
||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Safe covers only: | ||
| * - Already-loaded DOM <img> (PreMiD uploads locally) | ||
| * - AniList public CDN URL (Discord can fetch) | ||
| * - Logo fallback | ||
| * Never use BunnyCDN URLs as strings (Discord 403) and never block on blobs. | ||
| */ | ||
| export async function resolveCoverImage( | ||
| title?: string, | ||
| ): Promise<string | HTMLImageElement> { | ||
| const fromDom = getCoverImageElement() | ||
| if (fromDom) | ||
| return fromDom | ||
|
|
||
| const aniList = await fetchAniListCover(title) | ||
| if (aniList) | ||
| return aniList | ||
|
|
||
| return ActivityAssets.Logo | ||
| } | ||
|
|
||
| export function resolveProfileImage(): string | HTMLImageElement { | ||
| return getProfileImageElement() || 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.