From 480474d89c6d669bb05d6447c9f2dd13082e720b Mon Sep 17 00:00:00 2001 From: lxzy Date: Thu, 6 Aug 2026 04:40:22 -0700 Subject: [PATCH 1/9] Add Cinejoy Activity --- websites/C/Cinejoy/metadata.json | 25 +++++++++ websites/C/Cinejoy/presence.ts | 95 ++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 websites/C/Cinejoy/metadata.json create mode 100644 websites/C/Cinejoy/presence.ts diff --git a/websites/C/Cinejoy/metadata.json b/websites/C/Cinejoy/metadata.json new file mode 100644 index 000000000000..785336991d12 --- /dev/null +++ b/websites/C/Cinejoy/metadata.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://schemas.premid.app/metadata/1.17", + "apiVersion": 1, + "author": { + "id": "1233363061440512011", + "name": "lxzy" + }, + "service": "Cinejoy", + "description": { + "en": "Watch free movies and TV shows on Cinejoy." + }, + "url": "cinejoy.to", + "regExp": "^https?[:][/][/]([a-z0-9-]+[.])*cinejoy[.]to[/]", + "version": "1.0.0", + "logo": "https://cinejoy.to/favicon.ico", + "thumbnail": "https://i.imgur.com/S4XjEAo.jpeg", + "color": "#95FF50", + "category": "videos", + "tags": [ + "movie", + "tv", + "series", + "streaming" + ] +} \ No newline at end of file diff --git a/websites/C/Cinejoy/presence.ts b/websites/C/Cinejoy/presence.ts new file mode 100644 index 000000000000..285bb0f2e8a2 --- /dev/null +++ b/websites/C/Cinejoy/presence.ts @@ -0,0 +1,95 @@ +import { Assets, getTimestampsFromMedia } from 'premid' + +const presence = new Presence({ + clientId: '1534742028368347156', +}) + +const TMDB_KEY = '8476a7ab80ad76f0936744df0430e67c' +const TMDB_IMG = 'https://image.tmdb.org/t/p/w500' + +enum ActivityAssets { + Logo = 'https://cinejoy.to/favicon.ico', +} + +function parsePath() { + const path = document.location.pathname + const parts = path.split('/').filter(Boolean) + + const type = parts.find(p => p === 'tv' || p === 'movie') + if (!type) + return null + + const typeIndex = parts.indexOf(type) + const idPart = parts[typeIndex + 1] + const idMatch = idPart?.match(/^\d+/) + const tmdbId = idMatch?.[0] + if (!tmdbId) + return null + + const seasonPart = parts[typeIndex + 2] + const episodePart = parts[typeIndex + 3] + const season = seasonPart?.match(/\d+/)?.[0] + const episode = episodePart?.match(/\d+/)?.[0] + + return { type, tmdbId, season, episode } +} + +let cache: { key: string, details: any } | null = null + +async function getTmdbDetails(type: string, tmdbId: string) { + const key = `${type}-${tmdbId}` + if (cache?.key === key) + return cache.details + + const res = await fetch( + `https://api.themoviedb.org/3/${type}/${tmdbId}?api_key=${TMDB_KEY}&language=en-US`, + ) + const data = await res.json() + cache = { key, details: data } + return data +} + +presence.on('UpdateData', async () => { + const presenceData: PresenceData = { + largeImageKey: ActivityAssets.Logo, + details: 'Browsing Cinejoy', + } + + const parsed = parsePath() + const video = document.querySelector('video') + + if (parsed) { + try { + const data = await getTmdbDetails(parsed.type, parsed.tmdbId) + const title = data.title ?? data.name ?? 'Unknown' + const posterPath = data.poster_path + + presenceData.details = title + + if (parsed.type === 'tv' && parsed.season && parsed.episode) { + presenceData.state = `S${parsed.season}:E${parsed.episode}` + } + else if (parsed.type === 'movie' && data.release_date) { + presenceData.state = data.release_date.split('-')[0] + } + + if (posterPath) { + presenceData.largeImageKey = `${TMDB_IMG}${posterPath}` + } + } + catch (err) { + presenceData.details = 'Watching Cinejoy' + } + } + + if (video && video.readyState > 0) { + presenceData.smallImageKey = video.paused ? Assets.Pause : Assets.Play + presenceData.smallImageText = video.paused ? 'Paused' : 'Playing' + + if (!video.paused && Number.isFinite(video.duration)) { + [presenceData.startTimestamp, presenceData.endTimestamp] = getTimestampsFromMedia(video) + } + } + + presence.setActivity(presenceData) +}) \ No newline at end of file From 13d5730443304572082122275604350ef7900f8a Mon Sep 17 00:00:00 2001 From: lxzy Date: Thu, 6 Aug 2026 13:55:47 -0700 Subject: [PATCH 2/9] fix: address lint and metadata errors --- websites/C/Cinejoy/metadata.json | 4 ++-- websites/C/Cinejoy/presence.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/websites/C/Cinejoy/metadata.json b/websites/C/Cinejoy/metadata.json index 785336991d12..b55ac158f1bb 100644 --- a/websites/C/Cinejoy/metadata.json +++ b/websites/C/Cinejoy/metadata.json @@ -12,7 +12,7 @@ "url": "cinejoy.to", "regExp": "^https?[:][/][/]([a-z0-9-]+[.])*cinejoy[.]to[/]", "version": "1.0.0", - "logo": "https://cinejoy.to/favicon.ico", + "logo": "https://i.imgur.com/O3iQzCt.png", "thumbnail": "https://i.imgur.com/S4XjEAo.jpeg", "color": "#95FF50", "category": "videos", @@ -22,4 +22,4 @@ "series", "streaming" ] -} \ No newline at end of file +} diff --git a/websites/C/Cinejoy/presence.ts b/websites/C/Cinejoy/presence.ts index 285bb0f2e8a2..c0e834b0d859 100644 --- a/websites/C/Cinejoy/presence.ts +++ b/websites/C/Cinejoy/presence.ts @@ -77,7 +77,7 @@ presence.on('UpdateData', async () => { presenceData.largeImageKey = `${TMDB_IMG}${posterPath}` } } - catch (err) { + catch { presenceData.details = 'Watching Cinejoy' } } @@ -92,4 +92,4 @@ presence.on('UpdateData', async () => { } presence.setActivity(presenceData) -}) \ No newline at end of file +}) From ca0d71106cd28bb2e1b8f9ea5922dc1c1c92dda8 Mon Sep 17 00:00:00 2001 From: lxzy Date: Fri, 7 Aug 2026 01:43:50 -0700 Subject: [PATCH 3/9] fix: resize logo to 512x512 --- websites/C/Cinejoy/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websites/C/Cinejoy/metadata.json b/websites/C/Cinejoy/metadata.json index b55ac158f1bb..053b4ff0b637 100644 --- a/websites/C/Cinejoy/metadata.json +++ b/websites/C/Cinejoy/metadata.json @@ -12,7 +12,7 @@ "url": "cinejoy.to", "regExp": "^https?[:][/][/]([a-z0-9-]+[.])*cinejoy[.]to[/]", "version": "1.0.0", - "logo": "https://i.imgur.com/O3iQzCt.png", + "logo": "https://i.imgur.com/3wpcyto.png", "thumbnail": "https://i.imgur.com/S4XjEAo.jpeg", "color": "#95FF50", "category": "videos", From 4fdeb96ebcc00a51c60727dc41dea3a77a197b97 Mon Sep 17 00:00:00 2001 From: lxzy Date: Wed, 12 Aug 2026 18:23:35 -0700 Subject: [PATCH 4/9] fix: use imgur logo instead of favicon --- websites/C/Cinejoy/presence.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websites/C/Cinejoy/presence.ts b/websites/C/Cinejoy/presence.ts index c0e834b0d859..deb616cbb38b 100644 --- a/websites/C/Cinejoy/presence.ts +++ b/websites/C/Cinejoy/presence.ts @@ -8,7 +8,7 @@ const TMDB_KEY = '8476a7ab80ad76f0936744df0430e67c' const TMDB_IMG = 'https://image.tmdb.org/t/p/w500' enum ActivityAssets { - Logo = 'https://cinejoy.to/favicon.ico', + Logo = 'https://i.imgur.com/3wpcyto.png', } function parsePath() { From fda8c3bc194cf9a6305d0d86d194f506c7cbad54 Mon Sep 17 00:00:00 2001 From: lxzy Date: Fri, 14 Aug 2026 17:26:01 -0700 Subject: [PATCH 5/9] added: more activity --- websites/C/Cinejoy/presence.ts | 49 ++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/websites/C/Cinejoy/presence.ts b/websites/C/Cinejoy/presence.ts index deb616cbb38b..393306ffa4c3 100644 --- a/websites/C/Cinejoy/presence.ts +++ b/websites/C/Cinejoy/presence.ts @@ -11,7 +11,7 @@ enum ActivityAssets { Logo = 'https://i.imgur.com/3wpcyto.png', } -function parsePath() { +function parseMediaPath() { const path = document.location.pathname const parts = path.split('/').filter(Boolean) @@ -34,6 +34,32 @@ function parsePath() { return { type, tmdbId, season, episode } } +function getStaticPageInfo(): { details: string, state?: string } { + const parts = document.location.pathname.split('/').filter(Boolean) + + if (parts.length === 0) + return { details: 'Browsing Cinejoy', state: 'On the home page' } + + switch (parts[0]) { + case 'movies': + return { details: 'Browsing Cinejoy', state: 'Browsing Movies' } + case 'series': + return { details: 'Browsing Cinejoy', state: 'Browsing TV Shows' } + case 'lists': + return { details: 'Browsing Cinejoy', state: 'Viewing My List' } + case 'continue-watching': + return { details: 'Browsing Cinejoy', state: 'Viewing Continue Watching' } + case 'shorts': + return { details: 'Browsing Cinejoy', state: 'Watching Shorts' } + case 'settings': + return { details: 'Browsing Cinejoy', state: 'In Settings' } + case 'search': + return { details: 'Browsing Cinejoy', state: 'Searching' } + default: + return { details: 'Browsing Cinejoy' } + } +} + let cache: { key: string, details: any } | null = null async function getTmdbDetails(type: string, tmdbId: string) { @@ -55,21 +81,21 @@ presence.on('UpdateData', async () => { details: 'Browsing Cinejoy', } - const parsed = parsePath() + const parsedMedia = parseMediaPath() const video = document.querySelector('video') - if (parsed) { + if (parsedMedia) { try { - const data = await getTmdbDetails(parsed.type, parsed.tmdbId) + const data = await getTmdbDetails(parsedMedia.type, parsedMedia.tmdbId) const title = data.title ?? data.name ?? 'Unknown' const posterPath = data.poster_path presenceData.details = title - if (parsed.type === 'tv' && parsed.season && parsed.episode) { - presenceData.state = `S${parsed.season}:E${parsed.episode}` + if (parsedMedia.type === 'tv' && parsedMedia.season && parsedMedia.episode) { + presenceData.state = `S${parsedMedia.season}:E${parsedMedia.episode}` } - else if (parsed.type === 'movie' && data.release_date) { + else if (parsedMedia.type === 'movie' && data.release_date) { presenceData.state = data.release_date.split('-')[0] } @@ -81,6 +107,13 @@ presence.on('UpdateData', async () => { presenceData.details = 'Watching Cinejoy' } } + else { + const pageInfo = getStaticPageInfo() + presenceData.details = pageInfo.details + if (pageInfo.state) { + presenceData.state = pageInfo.state + } + } if (video && video.readyState > 0) { presenceData.smallImageKey = video.paused ? Assets.Pause : Assets.Play @@ -92,4 +125,4 @@ presence.on('UpdateData', async () => { } presence.setActivity(presenceData) -}) +}) \ No newline at end of file From a291f04f3daf2a87fdca0e9e6d6e83070b44c480 Mon Sep 17 00:00:00 2001 From: lxzy Date: Fri, 14 Aug 2026 17:28:17 -0700 Subject: [PATCH 6/9] fix: use imgur logo instead of favicon --- websites/D/Disney+/metadata.json | 2 +- websites/D/Disney+/presence.ts | 27 +- websites/G/GeoGuessr/assets.ts | 12 +- websites/G/GeoGuessr/metadata.json | 2 +- websites/G/GeoGuessr/presence.ts | 136 +++++++++-- websites/H/HeiwaStream/metadata.json | 7 +- websites/H/HeiwaStream/presence.ts | 15 +- websites/M/Manthan/README.md | 22 ++ websites/M/Manthan/metadata.json | 26 ++ websites/M/Manthan/presence.ts | 120 +++++++++ websites/M/MovieBoxPro/metadata.json | 2 +- websites/M/MovieBoxPro/presence.ts | 30 +-- websites/N/Novel Mania/Novel Mania.json | 34 +++ websites/N/Novel Mania/metadata.json | 50 +++- websites/N/Novel Mania/presence.ts | 312 +++++++++++++++++++----- websites/S/SoundScout/metadata.json | 24 ++ websites/S/SoundScout/presence.ts | 192 +++++++++++++++ 17 files changed, 870 insertions(+), 143 deletions(-) create mode 100644 websites/M/Manthan/README.md create mode 100644 websites/M/Manthan/metadata.json create mode 100644 websites/M/Manthan/presence.ts create mode 100644 websites/N/Novel Mania/Novel Mania.json create mode 100644 websites/S/SoundScout/metadata.json create mode 100644 websites/S/SoundScout/presence.ts diff --git a/websites/D/Disney+/metadata.json b/websites/D/Disney+/metadata.json index ba463dba2234..10a0b539464e 100644 --- a/websites/D/Disney+/metadata.json +++ b/websites/D/Disney+/metadata.json @@ -33,7 +33,7 @@ }, "url": "www.disneyplus.com", "regExp": "^https?[:][/][/]([a-z0-9-]+[.])*(disneyplus|hotstar)[.]com[/]", - "version": "1.10.3", + "version": "1.11.0", "logo": "https://cdn.rcd.gg/PreMiD/websites/D/Disney%2B/assets/logo.png", "thumbnail": "https://cdn.rcd.gg/PreMiD/websites/D/Disney%2B/assets/thumbnail.png", "color": "#233778", diff --git a/websites/D/Disney+/presence.ts b/websites/D/Disney+/presence.ts index f622e48e098b..bfbc331e2443 100644 --- a/websites/D/Disney+/presence.ts +++ b/websites/D/Disney+/presence.ts @@ -1,21 +1,25 @@ -import { ActivityType, Assets, getTimestamps, getTimestampsFromMedia, StatusDisplayType, timestampFromFormat } from 'premid' +import { ActivityType, Assets, getTimestamps, getTimestampsFromMedia, StatusDisplayType } from 'premid' //* I think this is a browser bug because the custom element does not have any properties when accessing it directly... let imageId: string | undefined let title: string | undefined let subtitle: string | undefined +let playheadPositionMs: number | undefined +let programDurationMs: number | undefined window.addEventListener('message', (e) => { if (e.data.type === 'pmd-receive-data') - ({ imageId, title, subtitle } = e.data as { imageId?: string, title?: string, subtitle?: string }) + ({ imageId, title, subtitle, playheadPositionMs, programDurationMs } = e.data as { imageId?: string, title?: string, subtitle?: string, playheadPositionMs?: number, programDurationMs?: number }) }) const script = document.createElement('script') script.textContent = ` setInterval(() => { -const metadata = document.querySelector("disney-web-player")?.mediaPlayer?.mediaPlaybackCriteria?.metadata; +const mediaPlayer = document.querySelector("disney-web-player")?.mediaPlayer; +const metadata = mediaPlayer?.mediaPlaybackCriteria?.metadata; const images = metadata?.images_experience?.standard?.tile; if (!images) return; +const timelineInfo = mediaPlayer?.timeline?.info; const ratios = Object.keys(images); const goal = 100; @@ -23,7 +27,7 @@ const closest = ratios.reduce(function(prev, curr) { return (Math.abs((100 / curr) - goal) < Math.abs((100 / prev) - goal) ? curr : prev); }); -window.postMessage({ type: "pmd-receive-data", imageId: images?.[closest]?.imageId, title: metadata?.title?.text, subtitle: metadata?.subtitle?.text }, "*"); +window.postMessage({ type: "pmd-receive-data", imageId: images?.[closest]?.imageId, title: metadata?.title?.text, subtitle: metadata?.subtitle?.text, playheadPositionMs: timelineInfo?.playheadPositionMs, programDurationMs: timelineInfo?.programDurationMs }, "*"); }, 1000); ` document.head.appendChild(script) @@ -91,14 +95,11 @@ presence.on('UpdateData', async () => { const { paused } = video if (!paused) { - const allTime = document.querySelector('#app_body_content disney-web-player-ui progress-bar')?.shadowRoot?.querySelector('div[role="slider"]')?.getAttribute('aria-valuetext')?.split('of')[1]?.trim() - const remainingTime = document.querySelector('#app_body_content disney-web-player-ui time-remaining-indicator')?.shadowRoot?.querySelector('.time-remaining-indicator')?.textContent?.trim() - const timestamps = getTimestamps( - Number(timestampFromFormat(allTime ?? '') - timestampFromFormat(remainingTime ?? '')) ?? '00:00', - timestampFromFormat(allTime ?? '00:00'), - ) - presenceData.startTimestamp = timestamps[0] - presenceData.endTimestamp = timestamps[1] + if (playheadPositionMs && programDurationMs) { + const timestamps = getTimestamps(playheadPositionMs / 1000, programDurationMs / 1000) + presenceData.startTimestamp = timestamps[0] + presenceData.endTimestamp = timestamps[1] + } } else { presenceData.smallImageKey = Assets.Pause @@ -299,5 +300,5 @@ presence.on('UpdateData', async () => { if (presenceData.details) presence.setActivity(presenceData) - else presence.setActivity() + else presence.clearActivity() }) diff --git a/websites/G/GeoGuessr/assets.ts b/websites/G/GeoGuessr/assets.ts index 4625967da41b..2a70d2490e1b 100644 --- a/websites/G/GeoGuessr/assets.ts +++ b/websites/G/GeoGuessr/assets.ts @@ -1,10 +1,18 @@ export const divisionIcons: Record = { + 'Bronze IV': 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/17.png', + 'Bronze III': 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/18.png', + 'Bronze II': 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/3378.png', + 'Bronze I': 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/3379.png', + 'Silver IV': 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/3380.png', 'Silver III': 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/0.png', 'Silver II': 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/1.png', 'Silver I': 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/2.png', + 'Gold IV': 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/3381.png', 'Gold III': 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/3.png', 'Gold II': 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/4.png', 'Gold I': 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/5.png', + 'Master IV': 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/3382.png', + 'Master III': 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/3383.png', 'Master II': 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/6.png', 'Master I': 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/7.png', 'Champion': 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/8.png', @@ -20,8 +28,8 @@ export const gameModeIcons: Record = { live_challenge: 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/14.png', party: 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/15.png', quiz: 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/16.png', - solo_duels: 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/17.png', - team_duels: 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/18.png', + solo_duels: 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/3384.png', + team_duels: 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/3385.png', streaks: 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/19.png', halloween: 'https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/3376.png', } diff --git a/websites/G/GeoGuessr/metadata.json b/websites/G/GeoGuessr/metadata.json index 00d670731429..78dfb02f03c9 100644 --- a/websites/G/GeoGuessr/metadata.json +++ b/websites/G/GeoGuessr/metadata.json @@ -20,7 +20,7 @@ "geoguessr.com" ], "regExp": "^https?[:][/][/](www[.])?geoguessr[.]com[/]", - "version": "2.2.0", + "version": "2.3.1", "logo": "https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/logo.png", "thumbnail": "https://cdn.rcd.gg/PreMiD/websites/G/GeoGuessr/assets/thumbnail.png", "color": "#7950e5", diff --git a/websites/G/GeoGuessr/presence.ts b/websites/G/GeoGuessr/presence.ts index b158d2325b08..2259c76865ee 100644 --- a/websites/G/GeoGuessr/presence.ts +++ b/websites/G/GeoGuessr/presence.ts @@ -129,7 +129,7 @@ async function updateStrings() { details: strings.viewHelppage, }, 'activities': { - details: strings.viewActivity, + details: strings.viewActivities, }, 'community': { details: strings.viewCommunity, @@ -206,9 +206,14 @@ let urlCache: any let soloDivision: any let soloRatingString: any +let teamDivision: any +let teamRatingString: any let isUnranked: boolean = false let currentGameMode: string = '' +let currentDuelRound: any = null +let duelGameActive = false +let duelBaseDetails = '' presence.on('UpdateData', async () => { const currentRawURL = document.location.href @@ -227,6 +232,12 @@ presence.on('UpdateData', async () => { pathCache = currentPath urlCache = currentRawURL + if (isNewURL) { + currentDuelRound = null + if (currentPath[0] === 'multiplayer' && lastPath[0] !== 'multiplayer') + isUnranked = false + } + if (!strings) { await updateStrings() } @@ -273,23 +284,60 @@ presence.on('UpdateData', async () => { setTimeout(() => { cooldowns.health = false }, 5000) - const healthUI = document.querySelectorAll('[class^="health-bar-2_barLabel__"]') - let scoreText: any - if (healthUI.length >= 2) { + + const roundElement = document.querySelector('[class^="round-score_roundNumber__"]') + if (roundElement && roundElement.textContent) { + const roundNum = Number(roundElement.textContent.replace(/\D/g, '')) + // +1 because the post-round screen shows the round just completed + currentDuelRound = roundNum ? `Round ${roundNum + 1}` : null + } + + const healthUI = document.querySelectorAll('[class^="health-bar-2_barLabel__"], [class^="health-bar_barLabel__"]') + let scoreText: string | undefined + const hasHealth = healthUI.length >= 2 + if (hasHealth) { const firstLabel = healthUI[0] as HTMLElement const secondLabel = healthUI[1] as HTMLElement if (firstLabel && secondLabel && firstLabel.textContent && secondLabel.textContent) { - scoreText = ` | ${firstLabel.textContent} - ${secondLabel.textContent}` + scoreText = `${firstLabel.textContent} - ${secondLabel.textContent}` } } - if (autoSetGamemode && currentGameMode !== '') { + else { + if (!roundElement) { + currentDuelRound = null + } + } + + // default to Round 1 before any round completes + if (hasHealth && !currentDuelRound) { + currentDuelRound = 'Round 1' + } + + const hasGameState = !!(currentDuelRound || scoreText) + + if (hasGameState && autoSetGamemode && currentGameMode !== '') { prefix = `${currentGameMode} ${prefix}` } + if (!isConnected()) { return } - presenceData.details = `${prefix}${scoreText || ''}` + presenceData.details = prefix + duelBaseDetails = prefix + if (hasGameState) { + const stateParts: string[] = [] + if (currentDuelRound) + stateParts.push(currentDuelRound) + if (scoreText) + stateParts.push(scoreText) + presenceData.state = stateParts.join(' | ') + duelGameActive = true + } + else { + duelGameActive = false + } } + async function getMapIconFromInfo(mapInfo: any) { if (!isConnected() || !mapInfo) { return @@ -330,9 +378,12 @@ presence.on('UpdateData', async () => { return await getMapIconFromInfo(mapInfo) } - async function setDivisionInfo(divisionName: any = null, rating: any = null) { + async function setDivisionInfo(divisionName: any = null, rating: any = null, appendToDetails: boolean = false) { if (isUnranked) { - presenceData.state = strings.inUnranked + if (appendToDetails) + presenceData.details = `${duelBaseDetails} | ${strings.unranked}` + else + presenceData.state = strings.inUnranked presenceData.smallImageKey = divisionIcons.Unranked presenceData.smallImageText = strings.unranked return @@ -356,7 +407,10 @@ presence.on('UpdateData', async () => { if (!isConnected()) { return } - presenceData.state = strings.division.replace('{0}', divisionName) + if (appendToDetails) + presenceData.details = `${duelBaseDetails} | ${divisionName}` + else + presenceData.state = strings.division.replace('{0}', divisionName) const divisionIcon = divisionIcons[divisionName] if (divisionIcon) { presenceData.smallImageKey = divisionIcon @@ -523,27 +577,30 @@ presence.on('UpdateData', async () => { if (isRanked) { presenceData.largeImageKey = (isTeams && gameModeIcons.team_duels) || gameModeIcons.solo_duels - presenceData.state = strings.inRanked + if (!duelGameActive) + presenceData.state = strings.inRanked delete presenceData.smallImageKey if (!isTeams) { - setDivisionInfo(soloDivision, soloRatingString) + setDivisionInfo(soloDivision, soloRatingString, duelGameActive) } } else if (gameInfo.options.gameContext.type === 'PartyV2') { presenceData.smallImageKey = gameModeIcons.party presenceData.smallImageText = strings.party presenceData.largeImageKey = (isTeams && gameModeIcons.team_duels) || gameModeIcons.solo_duels - presenceData.state = strings.inParty + if (!duelGameActive) + presenceData.state = strings.inParty } else { presenceData.largeImageKey = (isTeams && gameModeIcons.team_duels) || gameModeIcons.solo_duels - presenceData.state = strings.inUnranked + if (!duelGameActive) + presenceData.state = strings.inUnranked presenceData.smallImageKey = divisionIcons.Unranked presenceData.smallImageText = strings.unranked } } } - else if (currentPath[0] && currentPath[0] === 'matchmaking') { + else if (currentPath[0] === 'matchmaking') { const gameModeTextList = document.querySelectorAll('[class*="game-mode-brand_subTitle___"]') if (!alreadyFetchedList[currentPath[0]] && gameModeTextList[0]) { const isTeams = gameModeTextList[0] && gameModeTextList[0].textContent @@ -627,16 +684,26 @@ presence.on('UpdateData', async () => { } } else if (currentPath[0] === 'multiplayer') { - if (!currentPath[1]) { + if (!currentPath[1] || currentPath[1] === 'duels') { // Duels (new HUD) setHealthUI(strings.duels, true) presenceData.largeImageKey = gameModeIcons.solo_duels + const rankText = document.querySelectorAll('[class^="division-header_title__"]') const unrankedHeader = document.querySelectorAll('[class^="division-header_unrankedRoot__"]') + const unrankedMatchmaking = document.querySelectorAll('[class^="unranked-matchmaking-layout_"]') + if (unrankedHeader[0]) { const unrankedHeaderShowing = document.querySelectorAll('[class*="division-header_reveal__"]') isUnranked = (unrankedHeaderShowing[0] && true) || false } + else if (unrankedMatchmaking[0]) { + isUnranked = true + } + else if (rankText[0]) { + isUnranked = false + } + if (rankText[0]) { const divisionString = rankText[0].textContent const ratingText = document.querySelectorAll('[class^="division-header_rating__"]') @@ -644,12 +711,13 @@ presence.on('UpdateData', async () => { if (ratingText[0]) { ratingString = ratingText[0].textContent } - setDivisionInfo(divisionString, ratingString) + setDivisionInfo(divisionString, ratingString, duelGameActive) } else if (lastPath[0] !== 'multiplayer' || lastPath[1]) { delete presenceData.smallImageKey - presenceData.state = strings.inRanked - setDivisionInfo(soloDivision, soloRatingString) + if (!duelGameActive) + presenceData.state = isUnranked ? strings.inUnranked : strings.inRanked + setDivisionInfo(soloDivision, soloRatingString, duelGameActive) } } else if (currentPath[1] === 'teams') { @@ -657,7 +725,7 @@ presence.on('UpdateData', async () => { isUnranked = false setHealthUI(strings.teamDuels, true) presenceData.largeImageKey = gameModeIcons.team_duels - if (isNewURL) { + if (isNewURL && !duelGameActive) { delete presenceData.smallImageKey presenceData.state = strings.inRanked } @@ -668,8 +736,18 @@ presence.on('UpdateData', async () => { let ratingString: any if (ratingText[0]) { ratingString = ratingText[0].textContent + teamRatingString = ratingString + } + teamDivision = divisionString + setDivisionInfo(divisionString, ratingString, duelGameActive) + } + else if (lastPath[0] !== 'multiplayer' || lastPath[1]) { + delete presenceData.smallImageKey + if (!duelGameActive) + presenceData.state = strings.inRanked + if (teamDivision) { + setDivisionInfo(teamDivision, teamRatingString, duelGameActive) } - setDivisionInfo(divisionString, ratingString) } } else if (currentPath[1] === 'battle-royale-countries') { @@ -693,10 +771,12 @@ presence.on('UpdateData', async () => { presenceData.smallImageText = strings.unranked } else { - presenceData.details = strings.multiplayer + presenceData.details = strings.inRanked presenceData.largeImageKey = logo - delete presenceData.state - delete presenceData.smallImageKey + if (!soloDivision) + await setDivisionInfo() + else + await setDivisionInfo(soloDivision, soloRatingString) } } else if (currentPath[0] === 'party') { @@ -809,6 +889,14 @@ presence.on('UpdateData', async () => { } } } + else if (currentPath[0] === 'world') { + delete presenceData.buttons + presenceData.state = 'World' + presenceData.largeImageKey = mapAvatarOfficial.world || logo + presenceData.details = strings.viewMap + presenceData.smallImageKey = movementIcons.moving + presenceData.smallImageText = strings.movementMoving + } else { presenceData.largeImageKey = logo presenceData.details = strings.inMenu diff --git a/websites/H/HeiwaStream/metadata.json b/websites/H/HeiwaStream/metadata.json index f2920fc1760f..951f1a4bc53c 100644 --- a/websites/H/HeiwaStream/metadata.json +++ b/websites/H/HeiwaStream/metadata.json @@ -11,11 +11,10 @@ "fr": "Parcourez les films, séries et animés disponibles sur HeiwaStream." }, "url": [ - "heiwastream.fr", - "zelyon.xyz" + "heiwastream.fr" ], - "regExp": "^https?[:][/][/](?:www[.])?(?:heiwastream[.]fr|zelyon[.]xyz)(?:[/]|$)", - "version": "1.0.2", + "regExp": "^https?[:][/][/](?:www[.])?heiwastream[.]fr(?:[/]|$)", + "version": "1.0.3", "logo": "https://cdn.rcd.gg/PreMiD/websites/H/HeiwaStream/assets/logo.png", "thumbnail": "https://cdn.rcd.gg/PreMiD/websites/H/HeiwaStream/assets/thumbnail.png", "color": "#e6a91a", diff --git a/websites/H/HeiwaStream/presence.ts b/websites/H/HeiwaStream/presence.ts index dc5a9437b9f7..f0a06a82274a 100644 --- a/websites/H/HeiwaStream/presence.ts +++ b/websites/H/HeiwaStream/presence.ts @@ -13,8 +13,6 @@ enum ActivityAssets { const supportedHosts = new Set([ 'heiwastream.fr', 'www.heiwastream.fr', - 'zelyon.xyz', - 'www.zelyon.xyz', ]) function lireNombre(value: string | undefined): number | null { @@ -46,6 +44,7 @@ function mettreAJourActivite(): void { const title = data.premidWatching if (!title) { + const pageUrl = lireUrlHeiwa(data.premidPageUrl) ?? lireUrlHeiwa(document.location.href) const presenceData: PresenceData = { name: 'HeiwaStream', type: ActivityType.Playing, @@ -56,6 +55,13 @@ function mettreAJourActivite(): void { smallImageText: 'Navigation', startTimestamp: lireNombre(data.premidSince) ?? browsingTimestamp, } + + if (pageUrl) { + presenceData.detailsUrl = pageUrl + presenceData.stateUrl = pageUrl + presenceData.largeImageUrl = pageUrl + } + presence.setActivity(presenceData) return } @@ -63,7 +69,10 @@ function mettreAJourActivite(): void { const playbackState = data.premidState ?? 'playing' const position = lireNombre(data.premidPosition) const duration = lireNombre(data.premidDuration) - const metadata = [data.premidSeason, data.premidEpisode, data.premidLanguage] + const episode = [data.premidSeason, data.premidEpisode] + .filter(Boolean) + .join('') + const metadata = [episode || null, data.premidLanguage] .filter(Boolean) .join(' · ') const stateLabel = playbackState === 'paused' diff --git a/websites/M/Manthan/README.md b/websites/M/Manthan/README.md new file mode 100644 index 000000000000..5cc037db4901 --- /dev/null +++ b/websites/M/Manthan/README.md @@ -0,0 +1,22 @@ +# Manthan + +Rich Presence for **Manthan**, an independent digital news platform covering India, Assam, and global stories. + +## Features + +- Homepage detection +- Latest news detection +- Feed support +- Article detection +- Dynamic article titles +- Reading timer +- Rich Presence buttons + +## Supported Pages + +- / +- /latest +- /feed +- /feed/\* +- /article/\* +- /about diff --git a/websites/M/Manthan/metadata.json b/websites/M/Manthan/metadata.json new file mode 100644 index 000000000000..f493fd4b03ef --- /dev/null +++ b/websites/M/Manthan/metadata.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://schemas.premid.app/metadata/1.17", + "apiVersion": 1, + "author": { + "id": "1040637807670075432", + "name": "emsuregot" + }, + "service": "Manthan", + "description": { + "en": "Independent digital news platform covering India, Assam, and global stories." + }, + "url": "manthanind.com", + "regExp": "^https?[:][/][/]([a-z0-9-]+[.])?manthanind[.]com[/]", + "version": "1.0.1", + "logo": "https://cdn.rcd.gg/PreMiD/websites/M/Manthan/assets/logo.png", + "thumbnail": "https://cdn.rcd.gg/PreMiD/websites/M/Manthan/assets/thumbnail.png", + "color": "#0d0d0d", + "category": "other", + "tags": [ + "news", + "india", + "assam", + "journalism", + "editorial" + ] +} diff --git a/websites/M/Manthan/presence.ts b/websites/M/Manthan/presence.ts new file mode 100644 index 000000000000..5d04bfa2a8f1 --- /dev/null +++ b/websites/M/Manthan/presence.ts @@ -0,0 +1,120 @@ +import { Assets } from 'premid' + +const presence = new Presence({ + clientId: '1534951495068549271', +}) + +const startTimestamp = Math.floor(Date.now() / 1000) + +enum ActivityAssets { + Logo = 'https://cdn.rcd.gg/PreMiD/websites/M/Manthan/assets/logo.png', +} + +let strings: Awaited> +let oldLang: string | null = null + +async function getStrings() { + return presence.getStrings({ + browsing: 'general.browsing', + searching: 'general.search', + reading: 'general.readingArticle', + readButton: 'general.buttonReadArticle', + }) +} + +function articleTitle(): string { + const h1 = document.querySelector('h1')?.textContent?.trim() + + if (h1) + return h1 + + return document.title + .replace(/\s*\|\s*Manthan$/i, '') + .replace(/\s*-\s*Manthan$/i, '') + .trim() +} + +function shorten(text: string, max = 70): string { + return text.length > max ? `${text.slice(0, max - 1)}…` : text +} + +presence.on('UpdateData', async () => { + const lang = await presence + .getSetting('lang') + .catch(() => 'en') + + if (!strings || lang !== oldLang) { + oldLang = lang + strings = await getStrings() + } + + const { pathname, href } = document.location + + const presenceData: PresenceData = { + largeImageKey: ActivityAssets.Logo, + startTimestamp, + } + + switch (true) { + // Homepage + case pathname === '/': + presenceData.details = strings.browsing + presenceData.state = 'Today\'s Headlines' + break + + // Latest + case pathname === '/latest': + presenceData.details = 'Latest News' + presenceData.state = 'Breaking & Trending' + break + + // Feed + case pathname === '/feed': + presenceData.details = 'Browsing Feed' + presenceData.state = 'Latest Stories' + break + + // Feed Article + case pathname.startsWith('/feed/'): + presenceData.details = strings.reading + presenceData.state = shorten(articleTitle()) + + presenceData.smallImageKey = Assets.Reading + + presenceData.buttons = [ + { + label: 'Read Story', + url: href, + }, + ] + break + + // Article + case pathname.startsWith('/article/'): + presenceData.details = strings.reading + presenceData.state = shorten(articleTitle()) + + presenceData.smallImageKey = Assets.Reading + + presenceData.buttons = [ + { + label: strings.readButton, + url: href, + }, + ] + break + + // About + case pathname === '/about': + presenceData.details = 'About Manthan' + presenceData.state = 'Independent Digital News' + break + + default: + presenceData.details = strings.browsing + presenceData.state = 'Manthan' + break + } + + presence.setActivity(presenceData) +}) diff --git a/websites/M/MovieBoxPro/metadata.json b/websites/M/MovieBoxPro/metadata.json index bc00a15c352f..5ca2fd34cc17 100644 --- a/websites/M/MovieBoxPro/metadata.json +++ b/websites/M/MovieBoxPro/metadata.json @@ -11,7 +11,7 @@ }, "url": "movieboxpro.app", "regExp": "^https?[:][/][/]([a-z0-9-]+[.])*movieboxpro[.]app[/]", - "version": "1.2.0", + "version": "1.3.0", "logo": "https://cdn.rcd.gg/PreMiD/websites/M/MovieBoxPro/assets/logo.jpg", "thumbnail": "https://cdn.rcd.gg/PreMiD/websites/M/MovieBoxPro/assets/thumbnail.png", "color": "#fcba03", diff --git a/websites/M/MovieBoxPro/presence.ts b/websites/M/MovieBoxPro/presence.ts index 9955c49b22a3..29a97c28b4a1 100644 --- a/websites/M/MovieBoxPro/presence.ts +++ b/websites/M/MovieBoxPro/presence.ts @@ -1,4 +1,4 @@ -import { ActivityType, Assets, getTimestamps, timestampFromFormat } from 'premid' +import { ActivityType, Assets, getTimestampsFromMedia } from 'premid' declare const Presence: any @@ -61,32 +61,8 @@ presence.on('UpdateData', async () => { } else { presenceData.smallImageKey = Assets.Play - presenceData.smallImageText = 'Playing' - - let rawCurrent = document.querySelector('.vjs-current-time-display')?.textContent ?? '' - let rawDuration = document.querySelector('.vjs-duration-display')?.textContent ?? '' - const rawRemaining = document.querySelector('.vjs-remaining-time-display')?.textContent ?? '' - - if (!rawCurrent.trim()) { - rawCurrent = document.querySelector('.jw-text-elapsed')?.textContent ?? '' - rawDuration = document.querySelector('.jw-text-duration')?.textContent ?? '' - } - - const currentSeconds = timestampFromFormat(rawCurrent.replace(/[^\d:]/g, '')) - const durationSeconds = timestampFromFormat(rawDuration.replace(/[^\d:]/g, '')) - const remainingSeconds = timestampFromFormat(rawRemaining.replace(/[^\d:]/g, '')) - - if (durationSeconds > 0) { - [presenceData.startTimestamp, presenceData.endTimestamp] = getTimestamps(currentSeconds, durationSeconds) - } - else if (remainingSeconds > 0) { - presenceData.startTimestamp = Date.now() - (currentSeconds * 1000) - presenceData.endTimestamp = Date.now() + (remainingSeconds * 1000) - } - else { - presenceData.startTimestamp = Date.now() - (currentSeconds * 1000) - delete presenceData.endTimestamp - } + presenceData.smallImageText = 'Playing'; + [presenceData.startTimestamp, presenceData.endTimestamp] = getTimestampsFromMedia(video) } } diff --git a/websites/N/Novel Mania/Novel Mania.json b/websites/N/Novel Mania/Novel Mania.json new file mode 100644 index 000000000000..df5b8d605a2d --- /dev/null +++ b/websites/N/Novel Mania/Novel Mania.json @@ -0,0 +1,34 @@ +{ + "novelmania.news": { + "description": "Text for when user's visualizing 'news' page.", + "message": "news" + }, + "novelmania.genre": { + "description": "Text for when user's browsing genres.", + "message": "genres" + }, + "novelmania.novel": { + "description": "Just a placeholder for a novel title. It doesn't matter what it is, it will be replaced by the actual title.", + "message": "novel" + }, + "novelmania.readNovelButton": { + "description": "Text from 'viewing novel's button'.", + "message": "Read this novel!" + }, + "novelmania.readChapterButton": { + "description": "Text from 'reading chapter button'. Call to read along!", + "message": "Read this chapter!" + }, + "novelmania.readListButton": { + "description": "Text from 'read this list button'.", + "message": "Read this list!" + }, + "novelmania.readNewsButton": { + "description": "Text from 'read this news button'.", + "message": "Read this news!" + }, + "novelmania.visitUserProfileButton": { + "description": "Text from 'spy on this user button'. Naughty, naughty...", + "message": "Spy on this user!" + } +} diff --git a/websites/N/Novel Mania/metadata.json b/websites/N/Novel Mania/metadata.json index 7ad8f79cd1de..0b093b105d98 100644 --- a/websites/N/Novel Mania/metadata.json +++ b/websites/N/Novel Mania/metadata.json @@ -1,11 +1,14 @@ { - "$schema": "https://schemas.premid.app/metadata/1.16", + "$schema": "https://schemas.premid.app/metadata/1.17", "apiVersion": 1, "author": { - "name": "Shuichi", + "name": "hitwizard", "id": "179440483796385792" }, "service": "Novel Mania", + "altnames": [ + "NovelMania" + ], "description": { "en": "Read webnovels in Portuguese anywhere and anytime.", "nl": "Lees overal en altijd webnovels in het Portugees.", @@ -13,14 +16,49 @@ }, "url": "novelmania.com.br", "regExp": "^https?[:][/][/]([a-z0-9-]+[.])*novelmania[.]com[.]br[/]", - "version": "1.1.0", + "version": "2.0.0", "logo": "https://cdn.rcd.gg/PreMiD/websites/N/Novel%20Mania/assets/logo.png", "thumbnail": "https://cdn.rcd.gg/PreMiD/websites/N/Novel%20Mania/assets/thumbnail.jpg", "color": "#008cff", "category": "other", "tags": [ - "novel", - "mania", - "translator" + "webnovel", + "lightnovel", + "translation", + "book", + "reader", + "light-novel-translation", + "web-novel-translation", + "scanlator", + "novel-translation", + "books", + "book-translation" + ], + "settings": [ + { + "id": "lang", + "multiLanguage": true + }, + { + "id": "hideInfo", + "title": "Privacy Mode", + "icon": "fad fa-user-secret", + "value": false + }, + { + "id": "showButtons", + "title": "Show Buttons", + "icon": "fas fa-compress-arrows-alt", + "value": true, + "if": { + "hideInfo": false + } + }, + { + "id": "showTimestamp", + "title": "Show Timestamp", + "icon": "fad fa-stopwatch", + "value": true + } ] } diff --git a/websites/N/Novel Mania/presence.ts b/websites/N/Novel Mania/presence.ts index f3b79755c8d9..e13c29537227 100644 --- a/websites/N/Novel Mania/presence.ts +++ b/websites/N/Novel Mania/presence.ts @@ -1,78 +1,268 @@ +import { ActivityType, Assets } from 'premid' + const presence = new Presence({ clientId: '738522217221980222', }) -const browsingTimestamp = Math.floor(Date.now() / 1000) +let browsingTimestamp = Math.floor(Date.now() / 1000) +let lastSlug: string = '' -presence.on('UpdateData', async () => { - const presenceData: PresenceData = { - largeImageKey: 'https://cdn.rcd.gg/PreMiD/websites/N/Novel%20Mania/assets/logo.png', - startTimestamp: browsingTimestamp, +function updateTimestampBySlug(slug: string) { + if (lastSlug !== slug) { + lastSlug = slug + browsingTimestamp = Math.floor(Date.now() / 1000) } - const path = document.location.pathname - const porcent = document.querySelector( - '#settings-section > div > ul > li:nth-child(1) > div > div', - ) - const currentChapTitle = document.querySelector( - 'body > div:nth-child(4) > main > section.landing.novel-single > div.novel-head.pt-3 > div > div > div:nth-child(1) > nav > ol > li.breadcrumb-item.active', - ) - const novelName = document.querySelector('h1') - const VolNumb = document.querySelector('#chapter-content > h3') - const NovelTitle = document.querySelector( - 'body > div:nth-child(4) > main > section.landing.novel-single > div.novel-head.pt-3 > div > div > div:nth-child(2) > div > h1 > a', - ) - const PagTitle = document.querySelector('h2') - if (path === '/' || !path) { - presenceData.details = 'Na página inicial' /* at home */ - presenceData.state = 'Só olhando... Que estranho!' /* Juist lookin, how strange! */ +} + +enum ActivityAssets { + Logo = 'https://cdn.rcd.gg/PreMiD/websites/N/Novel%20Mania/assets/logo.png', +} +let defaultCover: ActivityAssets = ActivityAssets.Logo + +async function getStrings() { + return presence.getStrings({ + page: 'general.page', + view: 'general.view', + news: 'novelmania.news', + home: 'general.viewHome', + search: 'general.search', + genre: 'novelmania.genre', + novel: 'novelmania.novel', + browse: 'general.browsing', + reading: 'general.reading', + chapter: 'general.chapter', + lists: 'general.viewAList', + profile: 'general.viewProfile', + searchfor: 'general.searchFor', + viewPage: 'general.buttonViewPage', + privacyProfile: 'general.viewAProfile', + readListButton: 'novelmania.readListButton', + readNewsButton: 'novelmania.readNewsButton', + readNovelButton: 'novelmania.readNovelButton', + readChapterButton: 'novelmania.readChapterButton', + visitUserProfileButton: 'novelmania.visitUserProfileButton', + }) +} +let oldUserLanguage: string | null = null +let strings: Awaited> + +// As there's no way to select a cover while reading, +// I'm storing the novel's name as a variable outside the loop just dont reset + +presence.on('UpdateData', async () => { + const [showButtons, showTime, hideInfo, userLanguage] = await Promise.all([ + presence.getSetting('showButtons') || true, + presence.getSetting('showTimestamp') || true, + presence.getSetting('hideInfo') || false, + presence.getSetting('lang').catch(() => 'pt'), + ]) + + if (oldUserLanguage !== userLanguage) { + oldUserLanguage = userLanguage + strings = await getStrings() } - else if (path.includes('/noticias/')) { - presenceData.details = 'Lendo Notícia:' /* reading a notice */ - presenceData.state = novelName?.textContent + + const presenceData: PresenceData = { + largeImageKey: defaultCover, + type: ActivityType.Watching, } - else if (path.includes('/novels/')) { - if (document.body.contains(document.querySelector('#myTab'))) { - presenceData.details = 'No Indice da Novel:' /* At the novels's indice */ - presenceData.state = novelName?.textContent + const { pathname, origin } = window.location + const cleanPath = pathname.replace(/\/$/, '') || '/' + const [part1, part2, part3, part4] = cleanPath.slice(1).split('/') // page, slug (if any), chapter (if any), volume/book (if any) + const getPageTitle = (): string => document.querySelector('#main h1')?.textContent || strings.novel + let buttons: [ButtonData, ButtonData?] | undefined + let currentCover: any + let currentPageTitle: string | undefined + + switch (part1) { + case '': { + if (cleanPath === '/') { + presenceData.state = strings.home + } + break + } + case 'u':{ /* Seeing some user profile */ + updateTimestampBySlug('u') + if (hideInfo) { + presenceData.state = `${strings.privacyProfile}` + break + } + + currentCover = document.querySelector('#main img')?.src + currentPageTitle = getPageTitle() + presenceData.state = `${strings.profile} ${currentPageTitle}` + + if (currentCover && currentCover !== defaultCover) { + defaultCover = currentCover + } + + buttons = [{ label: strings.visitUserProfileButton, url: `${origin}/u/${part2}` }] + break + } + case 'novels': { + if (!part2) { /* Searching some novel */ + updateTimestampBySlug('novel-searching') + if (hideInfo) { + presenceData.state = `${strings.browse} ${strings.novel}` + break + } + presenceData.details = `${strings.search}` + presenceData.state = getPageTitle() + const params = new URLSearchParams(window.location.search) + const searchTerm = (document.querySelector('input[name="q"]'))?.value || params.get('q') + + if (searchTerm) { + presenceData.details = `${strings.searchfor} ${decodeURIComponent(searchTerm)}` + } + + /* Extract query params while searching for some novel */ + if (searchTerm || window.location.search) { + const badges = document.querySelectorAll('span.inline-flex.rounded-full.border') + if (badges.length > 0) { + const visibleFilters: string[] = [] + for (let i = 0; i < badges.length; i++) { + const text = badges[i]?.childNodes[0]?.textContent?.trim() + if (text) + visibleFilters.push(text) + } + if (visibleFilters.length) { + presenceData.state = visibleFilters.join(', ') + } + } + } + } + if (part3 === 'capitulos' && part4) { /* Reading some novel's chapter */ + updateTimestampBySlug(`novel-${part2}-chapter-${part4}`) + if (hideInfo) { + presenceData.state = `${strings.reading} ${strings.novel} ${strings.chapter.toLowerCase()}` + break + } + const novelName = document.querySelector('#conteudo-principal > div > header > div > p')?.textContent || part2?.split('-').slice(0, 2).join(' ') || part2?.split('-').join(' ') + const noveltype = document.querySelector('#conteudo-principal > div > main > div > div > header > p')?.textContent || part4?.split('-').slice(0, 2).join(' ') || part4?.split('-').join(' ') + const currentChapTitle = document.querySelector('#reader-chapter-title')?.textContent || novelName + + presenceData.details = `${strings.reading} ${novelName}` + presenceData.state = `${currentChapTitle} - ${noveltype}` + + buttons = [{ label: strings.readNovelButton, url: `${origin}/novels/${part2}` }, { label: strings.readChapterButton, url: `${origin}/novels/${part2}/capitulos/${part4}` }] + + currentPageTitle = novelName + } + + if (part2 && !part3) { /* At some novel's page */ + updateTimestampBySlug(`novel-${part2}`) + if (hideInfo) { + presenceData.state = `${strings.view} ${strings.novel}` + break + } + const novelName = document.querySelector('#main > div > h1')?.textContent || part2.split('-').join(' ') + presenceData.state = `${strings.view} ${novelName}` + + currentCover = document.querySelector('#main img')?.src + if (currentCover && currentCover !== defaultCover) { + currentPageTitle = novelName // Its not showing and idk why + defaultCover = currentCover + } + buttons = [{ label: strings.readNovelButton, url: `${origin}/novels/${part2}` }] + } + break } - else if (document.body.contains(VolNumb)) { - presenceData.details = `Lendo ${NovelTitle?.textContent} || ${VolNumb?.textContent}` /* reading volume */ - presenceData.state = `Em ${porcent?.textContent} do ${currentChapTitle?.textContent}` /* in X% of... novel chapter */ + case 'listas':{ + updateTimestampBySlug('list-browsing') + if (!hideInfo && part2) { + updateTimestampBySlug('list-reading') + const listName = document.querySelector('#main > div > div:nth-child(2) > a')?.textContent + presenceData.details = strings.lists + presenceData.state = getPageTitle() || listName + + currentCover = document.querySelector('#main img')?.src + if (currentCover && currentCover !== defaultCover) { + defaultCover = currentCover + currentPageTitle = getPageTitle() || listName + } + + buttons = [{ label: strings.readListButton, url: `${origin}/listas/${part2}` }] + break + } + presenceData.state = `${strings.lists}` + break } - else { - presenceData.details = `Lendo${NovelTitle?.textContent}` /* reading *novel**/ - presenceData.state = `Em ${porcent?.textContent}do${currentChapTitle?.textContent}` /* in X% of... novel chapter */ + case 'noticias': { + updateTimestampBySlug('news-reading') + if (hideInfo) { + presenceData.state = `${strings.view} ${strings.news}` + break + } + const newsTitle = getPageTitle() + if (!part2) { /* Searching some news */ + updateTimestampBySlug('news-browsing') + presenceData.state = `${strings.reading} ${strings.news}` /* reading news list */ + + break + } + + presenceData.details = `${strings.reading} ${strings.news}` /* reading a news */ + presenceData.state = `${newsTitle}` + + currentCover = document.querySelector('#main img')?.src + if (currentCover) { + defaultCover = currentCover + currentPageTitle = newsTitle || defaultCover + } + + buttons = [{ label: strings.readNewsButton, url: `${origin}/noticias/${part2}` }] + break + } + case 'genero':{ + updateTimestampBySlug('genre-searching') + if (hideInfo) { + presenceData.state = `${strings.view} ${strings.genre}` + break + } + if (part2) { + presenceData.details = `${strings.browse} ${strings.genre}` + presenceData.state = getPageTitle() + } + break } + default: /* At any other page, it doesn't matter */ + updateTimestampBySlug('page-reading') + if (hideInfo) { + presenceData.state = `${strings.reading} ${strings.page}` + break + } + presenceData.state = `${strings.view} ${getPageTitle()}` + + currentCover = document.querySelector('#main img')?.src + if (currentCover && currentCover !== defaultCover) { + defaultCover = currentCover + currentPageTitle = getPageTitle() || defaultCover + } + buttons = [{ label: strings.viewPage, url: origin + pathname }] + + break } - else if (path.includes('/u/')) { - presenceData.details = 'Bisbilhotando:' /* Seeing the * user profile */ - presenceData.state = document.querySelector( - 'body > div > main > section.profile-top > div > div > div > div.col-sm-8.col-md-9.d-flex.align-items-center > div > ul > li.admin-name > h5', - )?.textContent + + presenceData.startTimestamp = browsingTimestamp + + if (buttons && showButtons && !hideInfo) { + presenceData.smallImageKey = Assets.Reading + presenceData.buttons = buttons + } + if (!showTime) { + delete presenceData.startTimestamp } - else if ( - path.includes('/editoria') - || path.includes('/salao-da-contribuicao') - || path.includes('/regras-setorias') - || path.includes('/politica-de-privacidade') - || path.includes('contato') - ) { - presenceData.details = 'Lendo Página: ' /* reading page */ - presenceData.state = PagTitle?.textContent + if (!part1 || !part2) { + defaultCover = ActivityAssets.Logo + delete presenceData.largeImageText + } + else { + presenceData.largeImageText = currentPageTitle } - else if ( - path.includes('/genero/') - || path.includes('/chinesa') - || path.includes('/japonesa') - || path.includes('/coreana') - || path.includes('/brasileira') - || path.includes('/americana') - ) { - presenceData.details = 'Procurando:' /* searching for */ - presenceData.state = PagTitle?.textContent + presenceData.largeImageKey = defaultCover + if (presenceData.state) { + presence.setActivity(presenceData) } else { - presenceData.details = 'Buscando...' /* searching... */ - presenceData.state = 'Algo Incrível' /* Something incredible */ + presence.clearActivity() } - presence.setActivity(presenceData) }) diff --git a/websites/S/SoundScout/metadata.json b/websites/S/SoundScout/metadata.json new file mode 100644 index 000000000000..89cade54bc9a --- /dev/null +++ b/websites/S/SoundScout/metadata.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://schemas.premid.app/metadata/1.17", + "apiVersion": 1, + "author": { + "id": "239809113125552129", + "name": "Snupai" + }, + "service": "SoundScout", + "description": { + "en": "SoundScout provides claim-free music licensing for creators, streamers, and brands." + }, + "url": "soundscout.com", + "regExp": "^https?[:][/][/]([a-z0-9-]+[.])*soundscout[.]com[/]", + "version": "1.0.1", + "logo": "https://cdn.rcd.gg/PreMiD/websites/S/SoundScout/assets/logo.png", + "thumbnail": "https://cdn.rcd.gg/PreMiD/websites/S/SoundScout/assets/thumbnail.jpeg", + "color": "#F1D383", + "category": "music", + "tags": [ + "streaming", + "royalty-free", + "creator" + ] +} diff --git a/websites/S/SoundScout/presence.ts b/websites/S/SoundScout/presence.ts new file mode 100644 index 000000000000..28888b1d42bc --- /dev/null +++ b/websites/S/SoundScout/presence.ts @@ -0,0 +1,192 @@ +import { ActivityType, Assets, getTimestamps } from 'premid' + +const presence = new Presence({ + clientId: '1535996328230780938', +}) +const browsingTimestamp = Math.floor(Date.now() / 1000) + +enum ActivityAssets { + Logo = 'https://cdn.rcd.gg/PreMiD/websites/S/SoundScout/assets/logo.png', +} + +function getBackgroundImageUrl(element: HTMLElement | null) { + return element?.style.backgroundImage.match(/^url\(["']?(.*?)["']?\)$/)?.[1] +} + +function getPageName() { + return document.querySelector('main h1')?.textContent?.trim() + || document.title.split('·')[0]?.trim() +} + +presence.on('UpdateData', async () => { + const presenceData: PresenceData = { + largeImageKey: ActivityAssets.Logo, + startTimestamp: browsingTimestamp, + type: ActivityType.Listening, + } + const { href, pathname } = document.location + + // SoundScout exposes metadata through Media Session, while playback timing + // can fall back to the controls in its persistent player bar. + const playerBar = document.querySelector('[data-testid="player-bar"]') + const audio = playerBar?.querySelector('audio') + ?? document.querySelector('audio') + const seekSlider = playerBar?.querySelector( + 'input[aria-label="Seek"]', + ) + const metadata = navigator.mediaSession.metadata + + if (metadata?.title) { + const isPlaying = navigator.mediaSession.playbackState === 'playing' + || Boolean(audio && !audio.paused) + + // Clear paused tracks so an abandoned tab does not leave a stale activity. + if (!isPlaying) { + presence.clearActivity() + return + } + + const trackUrl = document.querySelector( + '[data-testid="player-bar"] a[href^="/player/music/"]', + )?.href + const artistUrl = document.querySelector( + '[data-testid="player-bar"] a[href^="/player/artists/"]', + )?.href + + presenceData.details = metadata.title + presenceData.state = metadata.artist || 'Unknown artist' + presenceData.largeImageKey = metadata.artwork[0]?.src + || ActivityAssets.Logo + presenceData.smallImageKey = Assets.Play + presenceData.smallImageText = 'Playing' + + // Replace the browsing timer with synchronized progress for active media. + delete presenceData.startTimestamp + + const currentTime = audio?.currentTime + ?? seekSlider?.valueAsNumber + ?? Number.NaN + const audioDuration = audio?.duration + const duration = audioDuration !== undefined + && Number.isFinite(audioDuration) + ? audioDuration + : Number(seekSlider?.max) + + if ( + Number.isFinite(currentTime) + && Number.isFinite(duration) + && duration > 0 + ) { + [presenceData.startTimestamp, presenceData.endTimestamp] + = getTimestamps(currentTime, duration) + } + + if (trackUrl) { + presenceData.detailsUrl = trackUrl + presenceData.largeImageUrl = trackUrl + presenceData.buttons = [ + { + label: 'Listen on SoundScout', + url: trackUrl, + }, + ] + } + if (artistUrl) + presenceData.stateUrl = artistUrl + } + else { + presenceData.details = 'Browsing SoundScout' + + switch (true) { + case pathname === '/player': { + presenceData.state = 'Exploring music' + break + } + case pathname.includes('/search'): { + presenceData.state = 'Searching for music' + break + } + case pathname === '/player/my-library': { + presenceData.state = 'Browsing favorites' + break + } + case pathname.startsWith('/player/explore/'): { + // Detail routes can contain UUIDs, so prefer the rendered page heading. + const pathParts = pathname.split('/').filter(Boolean) + const category = pathParts[2] ?? 'music' + const selection = pathParts[3] + const collectionTypes: Record = { + activities: 'activity', + genres: 'genre', + instruments: 'instrument', + keys: 'key', + moods: 'mood', + themes: 'theme', + } + const pathLabel = selection + ? decodeURIComponent(selection).replaceAll('-', ' ') + : undefined + const collectionName = getPageName() || pathLabel + + presenceData.state = selection && collectionName + ? `Browsing ${collectionTypes[category] ?? category}: ${collectionName}` + : `Browsing ${category}` + break + } + case pathname.includes('/music/'): { + presenceData.state = 'Viewing a track' + break + } + case pathname.includes('/artists/'): { + const artistHero = document.querySelector( + '[data-testid="artist-hero"]', + ) + const artistName = artistHero?.querySelector('h1')?.textContent?.trim() + + // Artist portraits are CSS backgrounds rather than image elements. + const artistImage = getBackgroundImageUrl( + artistHero?.querySelector( + '[aria-hidden="true"] [style*="background-image"]', + ) ?? null, + ) + + presenceData.state = artistName + ? `Viewing ${artistName}` + : 'Viewing an artist' + + if (artistImage) { + presenceData.largeImageKey = artistImage + presenceData.largeImageUrl = href + } + break + } + case pathname.includes('/releases/'): { + presenceData.state = 'Viewing a release' + break + } + case pathname === '/player/playlists': + case pathname === '/player/my-playlists': { + presenceData.state = 'Browsing playlists' + break + } + case pathname.startsWith('/player/playlists/'): + case pathname.startsWith('/player/my-playlists/'): { + const playlistName = getPageName() + + presenceData.state = playlistName + ? `Browsing playlist: ${playlistName}` + : 'Browsing a playlist' + break + } + default: { + presenceData.state = 'Exploring music' + } + } + + presenceData.detailsUrl = href + } + + if (presenceData.details) + presence.setActivity(presenceData) + else presence.clearActivity() +}) From f96d1dbc4d09ec46ea74c2600850d9ee9c243fc2 Mon Sep 17 00:00:00 2001 From: lxzy Date: Fri, 14 Aug 2026 17:35:10 -0700 Subject: [PATCH 7/9] chore: trigger ci From e23356fb927f5a027c24d5ed0406c11120e10c58 Mon Sep 17 00:00:00 2001 From: lxzy Date: Fri, 14 Aug 2026 17:50:05 -0700 Subject: [PATCH 8/9] chore: trigger ci From fe5416f1a742dcac3fb00e43ccb79d89e89a62ea Mon Sep 17 00:00:00 2001 From: lxzy Date: Sat, 15 Aug 2026 15:55:29 -0700 Subject: [PATCH 9/9] fix: ensure newline at end of file --- websites/C/Cinejoy/presence.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websites/C/Cinejoy/presence.ts b/websites/C/Cinejoy/presence.ts index 393306ffa4c3..c45b6cf6de11 100644 --- a/websites/C/Cinejoy/presence.ts +++ b/websites/C/Cinejoy/presence.ts @@ -125,4 +125,4 @@ presence.on('UpdateData', async () => { } presence.setActivity(presenceData) -}) \ No newline at end of file +})