Skip to content

Commit aacc116

Browse files
committed
feat(404Anime): add activity
1 parent 3418647 commit aacc116

2 files changed

Lines changed: 247 additions & 0 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"apiVersion": 1,
3+
"author": {
4+
"name": "error.4.04",
5+
"id": "692495122025152583"
6+
},
7+
"service": "404Anime",
8+
"description": {
9+
"en": "Anime streaming watch activity for 404Anime."
10+
},
11+
"url": "404anime.vercel.app",
12+
"regExp": "^https?[:][/][/]404anime[.]vercel[.]app[/]",
13+
"version": "1.0.0",
14+
"logo": "https://404anime.vercel.app/logo.png",
15+
"thumbnail": "https://404anime.vercel.app/og-image.png",
16+
"color": "#facc15",
17+
"category": "anime",
18+
"tags": [
19+
"anime",
20+
"video",
21+
"streaming"
22+
]
23+
}

websites/0-9/404Anime/presence.ts

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import { ActivityType, Assets, getTimestamps, supports } from 'premid'
2+
3+
const presence = new Presence({
4+
clientId: '503557087041683458',
5+
})
6+
7+
const browsingTimestamp = Math.floor(Date.now() / 1000)
8+
9+
enum ActivityAssets {
10+
Logo = 'https://404anime.vercel.app/logo.png',
11+
}
12+
13+
interface Anime404PremidPresence {
14+
page?: string
15+
mediaType?: 'anime' | 'movie' | 'tv'
16+
animeId?: number
17+
animeTitle?: string
18+
episode?: number
19+
season?: number
20+
episodeTitle?: string | null
21+
audio?: 'sub' | 'dub'
22+
server?: string
23+
cover?: string | null
24+
url?: string
25+
currentTime?: number
26+
duration?: number
27+
paused?: boolean
28+
}
29+
30+
function truncate(value: string, max = 128): string {
31+
return value.length > max ? `${value.slice(0, max - 1)}...` : value
32+
}
33+
34+
function getVideoFallback(): Pick<
35+
Anime404PremidPresence,
36+
'currentTime' | 'duration' | 'paused'
37+
> {
38+
const video = document.querySelector<HTMLVideoElement>('video')
39+
40+
if (!video) {
41+
return {
42+
currentTime: 0,
43+
duration: 0,
44+
paused: true,
45+
}
46+
}
47+
48+
return {
49+
currentTime: Number.isFinite(video.currentTime) ? video.currentTime : 0,
50+
duration: Number.isFinite(video.duration) ? video.duration : 0,
51+
paused: video.paused,
52+
}
53+
}
54+
55+
async function getBridgeData(): Promise<Anime404PremidPresence | null> {
56+
if (supports(presence, 'execInPage')) {
57+
try {
58+
const data = await presence.execInPage<Anime404PremidPresence | null>({
59+
get: '__SANIME_PREMID__',
60+
})
61+
62+
if (data?.animeTitle || data?.episode)
63+
return data
64+
}
65+
catch {
66+
// Fall back to the older variable reader below.
67+
}
68+
}
69+
70+
try {
71+
const page = await presence.getPageVariable<{
72+
__SANIME_PREMID__?: Anime404PremidPresence | null
73+
}>('__SANIME_PREMID__')
74+
75+
return page.__SANIME_PREMID__ ?? null
76+
}
77+
catch {
78+
return null
79+
}
80+
}
81+
82+
function applyPlayback(
83+
presenceData: PresenceData,
84+
currentTime = 0,
85+
duration = 0,
86+
paused = true,
87+
): void {
88+
if (paused) {
89+
presenceData.smallImageKey = Assets.Pause
90+
presenceData.smallImageText = 'Paused'
91+
delete presenceData.startTimestamp
92+
delete presenceData.endTimestamp
93+
return
94+
}
95+
96+
presenceData.smallImageKey = Assets.Play
97+
presenceData.smallImageText = 'Playing'
98+
99+
if (duration > 0) {
100+
[presenceData.startTimestamp, presenceData.endTimestamp] = getTimestamps(
101+
currentTime,
102+
duration,
103+
)
104+
}
105+
else {
106+
presenceData.startTimestamp = Date.now() - currentTime * 1000
107+
delete presenceData.endTimestamp
108+
}
109+
}
110+
111+
function getBrowsingState(pathname: string): string {
112+
if (pathname === '/welcome')
113+
return 'Getting started'
114+
115+
if (pathname === '/' || pathname === '/index.html')
116+
return 'Browsing anime'
117+
118+
if (pathname.startsWith('/search'))
119+
return 'Searching anime'
120+
121+
if (pathname.startsWith('/latest'))
122+
return 'Browsing latest releases'
123+
124+
if (pathname.startsWith('/schedule'))
125+
return 'Checking the schedule'
126+
127+
if (pathname.startsWith('/seasons'))
128+
return 'Browsing seasons'
129+
130+
if (pathname.startsWith('/movies'))
131+
return 'Browsing movies and shows'
132+
133+
if (pathname.startsWith('/watchlist'))
134+
return 'Viewing watchlist'
135+
136+
if (pathname.startsWith('/history'))
137+
return 'Viewing watch history'
138+
139+
if (pathname.startsWith('/profile'))
140+
return 'Viewing profile'
141+
142+
if (pathname.startsWith('/login') || pathname.startsWith('/auth'))
143+
return 'Signing in'
144+
145+
if (pathname.startsWith('/privacy'))
146+
return 'Reading privacy policy'
147+
148+
if (pathname.startsWith('/anime/'))
149+
return 'Viewing anime details'
150+
151+
if (pathname.startsWith('/movie/'))
152+
return 'Viewing movie details'
153+
154+
if (pathname.startsWith('/tv/'))
155+
return 'Viewing show details'
156+
157+
if (pathname.startsWith('/studio/'))
158+
return 'Viewing studio details'
159+
160+
if (pathname.startsWith('/person/'))
161+
return 'Viewing staff details'
162+
163+
return 'Exploring 404Anime'
164+
}
165+
166+
presence.on('UpdateData', async () => {
167+
const bridge = await getBridgeData()
168+
const fallback = getVideoFallback()
169+
const data = {
170+
...fallback,
171+
...bridge,
172+
}
173+
174+
const isWatchPage = document.location.pathname.includes('/watch/')
175+
const isMovieWatchPage = /^\/(?:movie|tv)\//.test(document.location.pathname)
176+
const title = data.animeTitle || document.title.replace(/\s*[-|].*$/, '')
177+
const episodeLabel = data.episode
178+
? data.season
179+
? `S${data.season}E${data.episode}`
180+
: `Episode ${data.episode}`
181+
: null
182+
183+
const presenceData: PresenceData = {
184+
name: '404Anime',
185+
type: ActivityType.Watching,
186+
largeImageKey: data.cover || ActivityAssets.Logo,
187+
largeImageText: data.episode
188+
? `Season 1, Episode ${data.episode}`
189+
: '404Anime',
190+
}
191+
192+
if ((isWatchPage || isMovieWatchPage) && title) {
193+
presenceData.details = truncate(
194+
episodeLabel ? `${title} - ${episodeLabel}` : title,
195+
)
196+
presenceData.state = truncate(
197+
[
198+
data.mediaType === 'movie'
199+
? 'Movie'
200+
: data.mediaType === 'tv'
201+
? data.episodeTitle || 'TV Episode'
202+
: data.audio ? data.audio.toUpperCase() : null,
203+
]
204+
.filter(Boolean)
205+
.join(' - '),
206+
)
207+
208+
applyPlayback(
209+
presenceData,
210+
data.currentTime,
211+
data.duration,
212+
data.paused,
213+
)
214+
}
215+
else {
216+
presenceData.details = '404Anime'
217+
presenceData.state = getBrowsingState(document.location.pathname)
218+
presenceData.startTimestamp = browsingTimestamp
219+
presenceData.smallImageKey = Assets.Search
220+
presenceData.smallImageText = 'Browsing'
221+
}
222+
223+
presence.setActivity(presenceData)
224+
})

0 commit comments

Comments
 (0)