-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(AniKitty): add AniKitty activity #11048
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 4 commits
44c77eb
39699d9
4d13589
11b335b
5a293a1
299e23d
2143caa
cf52a14
225d1e6
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,60 @@ | ||
| { | ||
| "$schema": "https://schemas.premid.app/metadata/1.16", | ||
| "apiVersion": 1, | ||
| "author": { | ||
| "name": "fampep", | ||
| "id": "1485578143850037248" | ||
| }, | ||
| "service": "AniKitty", | ||
| "description": { | ||
| "en": "AniKitty is a free anime streaming site with sub and dub, resume, and watch together." | ||
| }, | ||
| "url": [ | ||
| "anikitty.moe", | ||
| "anikitty.online" | ||
| ], | ||
| "regExp": "^https?[:][/][/]([a-z0-9-]+[.])?(anikitty[.]moe|anikitty[.]online)[/]", | ||
|
fampep marked this conversation as resolved.
Outdated
|
||
| "version": "1.0.1", | ||
Check failureCode scanning / PMD Makes sure the version is bumped Error
Expected initial version of activity AniKitty to be 1.0.0
|
||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| "logo": "https://anikitty.moe/anikitty-logo.png", | ||
Check failureCode scanning / PMD Makes sure all images (logo and URLs) are exactly 512x512 pixels Error
Image URL dimensions must be exactly 512x512 pixels, got 1024x1024 for URL: https://anikitty.moe/anikitty-logo.png
|
||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| "thumbnail": "https://anikitty.moe/og-share.png", | ||
| "color": "#ffffff", | ||
| "category": "anime", | ||
| "tags": [ | ||
| "anime", | ||
| "streaming", | ||
| "video", | ||
| "watch" | ||
| ], | ||
| "settings": [ | ||
| { | ||
| "id": "privacy", | ||
| "title": "Privacy Mode", | ||
| "icon": "fad fa-user-secret", | ||
| "value": false | ||
| }, | ||
| { | ||
| "id": "timestamps", | ||
| "title": "Show Timestamps", | ||
| "icon": "fad fa-stopwatch", | ||
| "value": true | ||
| }, | ||
| { | ||
| "id": "cover", | ||
| "if": { | ||
| "privacy": false | ||
| }, | ||
| "title": "Show Cover", | ||
| "icon": "fad fa-images", | ||
| "value": true | ||
| }, | ||
| { | ||
| "id": "buttons", | ||
| "if": { | ||
| "privacy": false | ||
| }, | ||
| "title": "Show Buttons", | ||
| "icon": "fas fa-compress-arrows-alt", | ||
| "value": true | ||
| } | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| import { ActivityType, Assets, getTimestampsFromMedia } from 'premid' | ||
|
|
||
| const presence = new Presence({ | ||
| clientId: '1520723875003105361', | ||
| }) | ||
|
|
||
| const browsingTimestamp = Math.floor(Date.now() / 1000) | ||
|
|
||
| enum ActivityAssets { | ||
| Logo = 'https://anikitty.moe/anikitty-logo.png', | ||
Check failureCode scanning / PMD Makes sure all images (logo and URLs) are exactly 512x512 pixels Error
Image URL dimensions must be exactly 512x512 pixels, got 1024x1024 for URL: https://anikitty.moe/anikitty-logo.png
|
||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| } | ||
|
|
||
| function parseWatchTitle(raw: string): { anime: string, episode: string | null } { | ||
| const cleaned = raw.replace(/\s*\|\s*AniKitty\s*$/i, '').trim() | ||
| if (!cleaned || /^anikitty$/i.test(cleaned)) | ||
| return { anime: 'Anime', episode: null } | ||
|
|
||
| const separators = [' — ', ' – ', ' - '] as const | ||
| for (const sep of separators) { | ||
| const index = cleaned.lastIndexOf(sep) | ||
| if (index === -1) | ||
| continue | ||
| const anime = cleaned.slice(0, index).trim() | ||
| const episode = cleaned.slice(index + sep.length).trim() | ||
| if (anime && /^Episode\s+\d+$/i.test(episode)) | ||
| return { anime, episode } | ||
| } | ||
|
|
||
| return { anime: cleaned, episode: null } | ||
| } | ||
|
|
||
| function getCover(): string | null { | ||
| return ( | ||
| document.querySelector('meta[property="og:image"]')?.getAttribute('content') | ||
| || document.querySelector('meta[name="twitter:image"]')?.getAttribute('content') | ||
| || null | ||
| ) | ||
| } | ||
|
|
||
| function applyVideoState( | ||
| presenceData: PresenceData, | ||
| video: HTMLVideoElement, | ||
| showTimestamps: boolean, | ||
| ): void { | ||
| const duration = video.duration | ||
| const currentTime = video.currentTime | ||
| const hasDuration = Number.isFinite(duration) && duration > 0 | ||
|
|
||
| if (video.paused || video.ended) { | ||
| presenceData.smallImageKey = Assets.Pause | ||
| presenceData.smallImageText = 'Paused' | ||
| delete presenceData.startTimestamp | ||
| delete presenceData.endTimestamp | ||
| return | ||
| } | ||
|
|
||
| presenceData.smallImageKey = Assets.Play | ||
| presenceData.smallImageText = 'Playing' | ||
|
|
||
| if (showTimestamps && hasDuration && Number.isFinite(currentTime)) { | ||
| [presenceData.startTimestamp, presenceData.endTimestamp] = getTimestampsFromMedia(video) | ||
| } | ||
| } | ||
|
|
||
| presence.on('UpdateData', async () => { | ||
| const privacy = await presence.getSetting<boolean>('privacy').catch(() => false) | ||
| const showTimestamps = await presence.getSetting<boolean>('timestamps').catch(() => true) | ||
| const showCover = await presence.getSetting<boolean>('cover').catch(() => true) | ||
| const showButtons = await presence.getSetting<boolean>('buttons').catch(() => true) | ||
|
fampep marked this conversation as resolved.
|
||
|
|
||
| const { pathname, href, search } = document.location | ||
| const presenceData: PresenceData = { | ||
| largeImageKey: ActivityAssets.Logo, | ||
| startTimestamp: browsingTimestamp, | ||
| details: 'Browsing AniKitty', | ||
| } as PresenceData | ||
|
fampep marked this conversation as resolved.
Outdated
|
||
|
|
||
| if (pathname.startsWith('/anime/watch')) { | ||
| ;(presenceData as PresenceData).type = ActivityType.Watching | ||
|
|
||
| if (privacy) { | ||
| presenceData.details = 'Watching anime' | ||
| } | ||
| else { | ||
| const { anime, episode } = parseWatchTitle(document.title) | ||
| const episodeFromUrl = new URLSearchParams(search).get('episode')?.match(/(\d+)/)?.[1] | ||
| const episodeLabel = episode || (episodeFromUrl ? `Episode ${episodeFromUrl}` : null) | ||
|
|
||
| presenceData.details = anime | ||
| presenceData.state = episodeLabel || 'Watching' | ||
|
|
||
| const cover = getCover() | ||
| if (showCover && cover) { | ||
| presenceData.largeImageKey = cover | ||
| presenceData.smallImageKey = ActivityAssets.Logo | ||
| presenceData.smallImageText = 'AniKitty' | ||
| } | ||
|
|
||
| const video = document.querySelector('video') | ||
| if (video && video.readyState > 0) | ||
| applyVideoState(presenceData, video, showTimestamps) | ||
|
|
||
| if (showButtons) { | ||
| presenceData.buttons = [ | ||
| { | ||
| label: 'Watch on AniKitty', | ||
| url: href.split('#')[0]!, | ||
| }, | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| else if ( | ||
| pathname.startsWith('/anime/') | ||
| && pathname !== '/anime' | ||
| && !pathname.startsWith('/anime/watch') | ||
| ) { | ||
| if (privacy) { | ||
| presenceData.details = 'Browsing anime' | ||
| } | ||
| else { | ||
| const title | ||
| = document.querySelector('h1')?.textContent?.trim() | ||
| || document.title.replace(/\s*\|\s*AniKitty\s*$/i, '').trim() | ||
| || 'Anime' | ||
| presenceData.details = 'Viewing anime' | ||
| presenceData.state = title | ||
|
|
||
| const cover = getCover() | ||
| if (showCover && cover) { | ||
| presenceData.largeImageKey = cover | ||
| presenceData.smallImageKey = ActivityAssets.Logo | ||
| } | ||
|
|
||
| if (showButtons) { | ||
| presenceData.buttons = [ | ||
| { | ||
| label: 'View Anime', | ||
| url: href.split('#')[0]!, | ||
| }, | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| else if (pathname.startsWith('/search')) { | ||
| presenceData.details = privacy ? 'Searching' : 'Searching for anime' | ||
| presenceData.smallImageKey = Assets.Search | ||
| if (!privacy) { | ||
| const q | ||
| = new URLSearchParams(search).get('q') | ||
| || new URLSearchParams(search).get('query') | ||
| if (q) | ||
| presenceData.state = q | ||
| } | ||
| } | ||
| else if (pathname.startsWith('/schedule')) { | ||
| presenceData.details = 'Browsing schedule' | ||
| } | ||
| else if (pathname.startsWith('/trending')) { | ||
| presenceData.details = 'Browsing trending' | ||
| } | ||
| else if (pathname.startsWith('/forum')) { | ||
| presenceData.details = 'Browsing forum' | ||
| } | ||
| else if (pathname.startsWith('/watch-together')) { | ||
| presenceData.details = 'Watch together' | ||
| } | ||
| else if (pathname.startsWith('/profile/')) { | ||
| presenceData.details = privacy ? 'Viewing a profile' : 'Viewing profile' | ||
| if (!privacy) { | ||
| const user = pathname.split('/')[2] | ||
| if (user) | ||
| presenceData.state = decodeURIComponent(user) | ||
| } | ||
| } | ||
| else if (pathname === '/' || pathname.startsWith('/home')) { | ||
| presenceData.details = 'Browsing homepage' | ||
| } | ||
| else if (pathname.startsWith('/history')) { | ||
| presenceData.details = 'Viewing watch history' | ||
| } | ||
| else if ( | ||
| pathname.startsWith('/settings') | ||
| || pathname.startsWith('/messages') | ||
| || pathname.startsWith('/chat') | ||
| ) { | ||
| presence.clearActivity() | ||
| return | ||
| } | ||
|
|
||
| presence.setActivity(presenceData) | ||
| }) | ||
Uh oh!
There was an error while loading. Please reload this page.