Skip to content

Commit a68793b

Browse files
authored
feat(Primeshows): add activity (#10303)
* feat: Add PreMiD presence for Primeshows, including metadata and activity detection. * feat: Add Discord Rich Presence integration for Primeshows, supporting various page types and content details. * feat: Add Discord Rich Presence integration for Primeshows, supporting various browsing and watching activities. * Used clearActivity instead to hide the Activity.
1 parent cc9c9e8 commit a68793b

File tree

2 files changed

+371
-0
lines changed

2 files changed

+371
-0
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"$schema": "https://schemas.premid.app/metadata/1.16",
3+
"apiVersion": 1,
4+
"author": {
5+
"id": "1203605618745933880",
6+
"name": "friday.su"
7+
},
8+
"service": "Primeshows",
9+
"description": {
10+
"en": "Enjoy unlimited streaming of movies, TV shows, and anime in HD & 4K, completely free! Primeshows offers a seamless experience with multi-language support and auto-next playback, ensuring you never miss a moment. Watch your favorite content anytime, anywhere!"
11+
},
12+
"url": "www.primeshows.live",
13+
"regExp": "^https?[:][/][/]([a-z0-9-]+[.])*primeshows[.]live[/]",
14+
"version": "1.0.0",
15+
"logo": "https://i.ibb.co/fdn3gFWS/logo.png",
16+
"thumbnail": "https://i.ibb.co/7NtK5cn6/image.png",
17+
"color": "#121212",
18+
"category": "videos",
19+
"tags": [
20+
"video",
21+
"streaming",
22+
"movies",
23+
"anime",
24+
"tv"
25+
],
26+
"settings": [
27+
{
28+
"id": "lang",
29+
"multiLanguage": true
30+
},
31+
{
32+
"id": "privacy",
33+
"title": "Privacy Mode",
34+
"icon": "fas fa-user-secret",
35+
"value": false
36+
},
37+
{
38+
"id": "showButtons",
39+
"title": "Show Buttons",
40+
"icon": "fas fa-compress-arrows-alt",
41+
"value": true
42+
}
43+
]
44+
}

websites/P/Primeshows/presence.ts

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
import { ActivityType, Assets } from 'premid'
2+
3+
const presence = new Presence({
4+
clientId: '1369156087340728350',
5+
})
6+
7+
const browsingTimestamp = Math.floor(Date.now() / 1000)
8+
9+
enum ActivityAssets {
10+
Logo = 'https://i.ibb.co/fdn3gFWS/logo.png',
11+
}
12+
13+
/**
14+
* Formats a slug into a readable name.
15+
* e.g., "66732-stranger-things" -> "Stranger Things"
16+
*
17+
* @param slug - The slug string to format.
18+
*/
19+
function formatSlug(slug: string | undefined): string {
20+
if (!slug) {
21+
return ''
22+
}
23+
24+
return slug
25+
.split('-')
26+
.map((word) => {
27+
if (/^\d+$/.test(word)) {
28+
return ''
29+
} // Skip IDs
30+
return word.charAt(0).toUpperCase() + word.slice(1)
31+
})
32+
.filter(Boolean)
33+
.join(' ')
34+
}
35+
36+
/**
37+
* Attempts to retrieve a name/title from the DOM if the URL slug is insufficient.
38+
*/
39+
function getNameFromDOM(): string | null {
40+
const selectors = [
41+
'h1.text-white',
42+
'.movie-title',
43+
'.show-title',
44+
'h1',
45+
'header h1',
46+
'#Movie\\ Name',
47+
'#TV\\ Shows\\ Name',
48+
]
49+
50+
for (const selector of selectors) {
51+
const el = document.querySelector(selector)
52+
if (el && el.textContent) {
53+
return el.textContent.trim()
54+
}
55+
}
56+
57+
return null
58+
}
59+
60+
/**
61+
* Robustly fetches the rating from the page.
62+
*/
63+
function getRating(): string {
64+
const ratingEl = document.querySelector('.radial-progress span.text-white')
65+
|| document.querySelector('[class*="radial-progress"] span')
66+
|| document.querySelector('.rating-value')
67+
68+
const rating = ratingEl?.textContent?.trim() || 'N/A'
69+
70+
return rating === '0' || rating === '0.0' ? 'N/A' : rating
71+
}
72+
73+
/**
74+
* Robustly fetches the release date from the page.
75+
*
76+
* @param type - The type of content ('movie' or 'tv').
77+
*/
78+
function getReleaseDate(type: 'movie' | 'tv'): string {
79+
const selector = type === 'movie' ? '#Movie\\ Release\\ Date time p' : '#TV\\ Shows\\ Air\\ Date time'
80+
let date = document.querySelector(selector)?.textContent?.trim()
81+
|| document.querySelector('time')?.textContent?.trim()
82+
|| 'N/A'
83+
84+
// Format long dates like "Sunday, October 12, 2014" to "October 2014"
85+
if (date !== 'N/A') {
86+
const dateParts = date.split(', ')
87+
const p0 = dateParts[0]
88+
const p1 = dateParts[1]
89+
const p2 = dateParts[2]
90+
91+
if (dateParts.length === 3 && p1 && p2) {
92+
date = `${p1} ${p2}`
93+
}
94+
else if (dateParts.length === 2 && type === 'tv' && p0 && p1) {
95+
const monthYear = p0.split(' ')[0]
96+
if (monthYear) {
97+
date = `${monthYear} ${p1}`
98+
}
99+
}
100+
}
101+
102+
return date
103+
}
104+
105+
presence.on('UpdateData', async () => {
106+
let presenceData: PresenceData = {
107+
largeImageKey: ActivityAssets.Logo,
108+
startTimestamp: browsingTimestamp,
109+
details: 'Unsupported Page',
110+
}
111+
112+
const { pathname, search, href } = document.location
113+
const urlParams = new URLSearchParams(search)
114+
115+
const privacy = await presence.getSetting<boolean>('privacy')
116+
const showButtons = await presence.getSetting<boolean>('showButtons')
117+
118+
if (privacy) {
119+
presenceData.details = 'Watching Primeshows 🔒'
120+
presence.setActivity(presenceData)
121+
return
122+
}
123+
124+
// Static Pages
125+
const pages: Record<string, PresenceData> = {
126+
'/': {
127+
details: 'Viewing HomePage 🏠',
128+
smallImageKey: Assets.Viewing,
129+
},
130+
'/profile': {
131+
details: 'Viewing Profile 👤',
132+
smallImageKey: Assets.Viewing,
133+
},
134+
'/tv': {
135+
details: 'Browsing TV Shows 📺',
136+
smallImageKey: Assets.Viewing,
137+
},
138+
'/movies': {
139+
details: 'Browsing Movies 🎬',
140+
smallImageKey: Assets.Viewing,
141+
},
142+
'/trending': {
143+
details: 'Browsing Trending 🔥',
144+
smallImageKey: Assets.Viewing,
145+
},
146+
'/search': {
147+
details: 'Browsing Search 🔎',
148+
smallImageKey: Assets.Viewing,
149+
},
150+
'/livetv': {
151+
details: 'Browsing Live TV 📶',
152+
smallImageKey: Assets.Viewing,
153+
},
154+
'/sports': {
155+
details: 'Live Sports ⚽',
156+
smallImageKey: Assets.Viewing,
157+
},
158+
}
159+
160+
if (pages[pathname]) {
161+
presenceData = {
162+
...presenceData,
163+
...pages[pathname],
164+
type: ActivityType.Watching,
165+
}
166+
}
167+
168+
// Handle Dynamic Routes
169+
170+
// 1. TV Info Page: /tv/{id}-{slug}
171+
if (pathname.startsWith('/tv/') && pathname !== '/tv') {
172+
const match = pathname.match(/\/tv\/\d+(?:-([^/]+))?/)
173+
if (match) {
174+
const showName = formatSlug(match[1]) || getNameFromDOM() || 'Unknown Show'
175+
presenceData.details = `Viewing ${showName} 📺`
176+
presenceData.type = ActivityType.Watching
177+
presenceData.smallImageKey = Assets.Viewing
178+
179+
const rating = getRating()
180+
const releaseDate = getReleaseDate('tv')
181+
182+
const stateParts = []
183+
if (rating !== 'N/A') {
184+
stateParts.push(`⭐ ${rating}`)
185+
}
186+
if (releaseDate !== 'N/A') {
187+
stateParts.push(`🗓️ ${releaseDate}`)
188+
}
189+
presenceData.state = stateParts.length > 0 ? stateParts.join(' • ') : 'Viewing Details'
190+
presenceData.largeImageKey = document.querySelector<HTMLImageElement>('section.md\\:col-\\[1\\/4\\] img')?.src
191+
|| document.querySelector<HTMLImageElement>('img[alt*="Poster"]')?.src
192+
|| ActivityAssets.Logo
193+
194+
if (showButtons) {
195+
presenceData.buttons = [
196+
{ label: 'View Show 📺', url: href },
197+
]
198+
}
199+
}
200+
}
201+
202+
// 2. Movie Info Page: /movies/{id}-{slug}
203+
if (pathname.startsWith('/movies/') && pathname !== '/movies') {
204+
const match = pathname.match(/\/movies\/\d+(?:-([^/]+))?/)
205+
if (match) {
206+
const movieName = formatSlug(match[1]) || getNameFromDOM() || 'Unknown Movie'
207+
presenceData.details = `Viewing ${movieName} 🎬`
208+
presenceData.type = ActivityType.Watching
209+
presenceData.smallImageKey = Assets.Viewing
210+
211+
const rating = getRating()
212+
const runtime = document.querySelector('#Movie\\ Runtime time p')?.textContent?.match(/\d+/)?.[0] || 'N/A'
213+
const releaseDate = getReleaseDate('movie')
214+
215+
const stateParts = []
216+
if (rating !== 'N/A') {
217+
stateParts.push(`⭐ ${rating}`)
218+
}
219+
if (runtime !== 'N/A') {
220+
stateParts.push(`🕒 ${runtime}m`)
221+
}
222+
if (releaseDate !== 'N/A') {
223+
stateParts.push(`🗓️ ${releaseDate}`)
224+
}
225+
presenceData.state = stateParts.length > 0 ? stateParts.join(' • ') : 'Viewing Details'
226+
presenceData.largeImageKey = document.querySelector<HTMLImageElement>('figure img.object-cover')?.src
227+
|| document.querySelector<HTMLImageElement>('img[alt*="Poster"]')?.src
228+
|| ActivityAssets.Logo
229+
230+
if (showButtons) {
231+
presenceData.buttons = [
232+
{ label: 'View Movie 🎬', url: href },
233+
]
234+
}
235+
}
236+
}
237+
238+
// 3. Watch TV: /watch/tv/{id}
239+
if (pathname.startsWith('/watch/tv/')) {
240+
const match = pathname.match(/\/watch\/tv\/(\d+)/)
241+
if (match) {
242+
const tmdbId = match[1]
243+
const showName = getNameFromDOM() || 'Unknown Show'
244+
245+
const season = urlParams.get('season')
246+
const episode = urlParams.get('episode')
247+
248+
let seasonNo = '1'
249+
let episodeNo = '1'
250+
251+
if (season && episode) {
252+
seasonNo = season
253+
episodeNo = episode
254+
}
255+
else {
256+
const watchHistory = JSON.parse(localStorage.getItem('watch-history') || '{}')
257+
const showData = (tmdbId ? watchHistory[tmdbId] : null) || { last_season_watched: '1', last_episode_watched: '1' }
258+
seasonNo = showData.last_season_watched
259+
episodeNo = showData.last_episode_watched
260+
}
261+
262+
presenceData.details = `Watching ${showName} 🍿`
263+
presenceData.state = `S${seasonNo} E${episodeNo} • Streaming 📺`
264+
presenceData.type = ActivityType.Watching
265+
presenceData.smallImageKey = Assets.Play
266+
267+
presenceData.largeImageKey = document.querySelector<HTMLImageElement>('img.poster')?.src || ActivityAssets.Logo
268+
269+
if (showButtons) {
270+
presenceData.buttons = [
271+
{ label: 'Watch Now 🍿', url: href },
272+
]
273+
}
274+
}
275+
}
276+
277+
// 4. Watch Movie: /watch/movie/{id}
278+
if (pathname.startsWith('/watch/movie/')) {
279+
const movieName = getNameFromDOM() || 'Unknown Movie'
280+
presenceData.details = `Watching ${movieName} 🎬`
281+
presenceData.state = `Enjoying Movie 🍿`
282+
presenceData.type = ActivityType.Watching
283+
presenceData.smallImageKey = Assets.Play
284+
presenceData.largeImageKey = document.querySelector<HTMLImageElement>('img.poster')?.src || ActivityAssets.Logo
285+
286+
if (showButtons) {
287+
presenceData.buttons = [
288+
{ label: 'Watch Now 🍿', url: href },
289+
]
290+
}
291+
}
292+
293+
// 5. Watch Sports: /watch/sports/{details}
294+
if (pathname.startsWith('/watch/sports/')) {
295+
const sportsSlug = pathname.split('/').pop() || ''
296+
const sportsName = formatSlug(sportsSlug) || 'Live Sports'
297+
298+
presenceData.details = `Watching ${sportsName} 🏆`
299+
presenceData.type = ActivityType.Watching
300+
presenceData.smallImageKey = Assets.Play
301+
presenceData.state = 'Live Sports Event 📶'
302+
303+
if (showButtons) {
304+
presenceData.buttons = [
305+
{ label: 'Watch Live 📶', url: href },
306+
]
307+
}
308+
}
309+
310+
// 6. Search Page
311+
if (pathname.includes('/search')) {
312+
const query = urlParams.get('q') || document.querySelector('input')?.getAttribute('value')
313+
314+
presenceData.details = 'Searching Primeshows 🔎'
315+
if (query) {
316+
presenceData.state = `Looking for: ${query} ✨`
317+
}
318+
presenceData.smallImageKey = Assets.Search
319+
}
320+
321+
if (presenceData.details !== 'Unsupported Page') {
322+
presence.setActivity(presenceData)
323+
}
324+
else {
325+
presence.clearActivity()
326+
}
327+
})

0 commit comments

Comments
 (0)