Skip to content

Commit 1a5caa5

Browse files
mmaarijtheusaf
andauthored
feat(mapple.tv): add activity (#9843)
Signed-off-by: Muhammad Maarij <84127728+mmaarij@users.noreply.github.com> Co-authored-by: Daniel Lau <32113157+theusaf@users.noreply.github.com>
1 parent 2105fa7 commit 1a5caa5

2 files changed

Lines changed: 231 additions & 0 deletions

File tree

websites/M/Mapple.tv/metadata.json

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"$schema": "https://schemas.premid.app/metadata/1.15",
3+
"apiVersion": 1,
4+
"author": {
5+
"id": "756072844584288316",
6+
"name": "casper"
7+
},
8+
"service": "Mapple.tv",
9+
"description": {
10+
"en": "Shows what you're watching on mapple.tv"
11+
},
12+
"url": "mapple.tv",
13+
"version": "1.0.0",
14+
"logo": "https://i.ibb.co/9PPkt6L/mappletv.png",
15+
"thumbnail": "https://i.ibb.co/Lhxcqm84/thumbnail.jpg",
16+
"color": "#000000",
17+
"category": "videos",
18+
"tags": ["streaming", "movies", "tv", "entertainment", "media", "sports", "video", "audiobook"],
19+
"settings": [
20+
{
21+
"id": "privacy",
22+
"title": "Privacy Mode",
23+
"description": "Hide all content details",
24+
"icon": "fad fa-user-secret",
25+
"value": false
26+
},
27+
{
28+
"id": "browse",
29+
"title": "Show Browsing Status",
30+
"description": "Display when you're browsing MappleTV content",
31+
"icon": "fad fa-book-reader",
32+
"value": true
33+
},
34+
{
35+
"id": "live",
36+
"title": "Show Livestreams",
37+
"description": "Display when you're watching live TV channels",
38+
"icon": "fad fa-podcast",
39+
"value": true
40+
},
41+
{
42+
"id": "movies",
43+
"title": "Show Movie Titles",
44+
"description": "Display specific movie titles you're watching",
45+
"icon": "fad fa-film",
46+
"value": true
47+
},
48+
{
49+
"id": "tvshows",
50+
"title": "Show TV Show Titles",
51+
"description": "Display specific TV show titles you're watching",
52+
"icon": "fad fa-tv",
53+
"value": true
54+
},
55+
{
56+
"id": "audiobooks",
57+
"title": "Show Audiobook Titles",
58+
"description": "Display specific audiobook titles and authors",
59+
"icon": "fad fa-headphones",
60+
"value": true
61+
},
62+
{
63+
"id": "sports",
64+
"title": "Show Sports Titles",
65+
"description": "Display the specific sports event you're watching",
66+
"icon": "fad fa-basketball-ball",
67+
"value": true
68+
}
69+
]
70+
}

websites/M/Mapple.tv/presence.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { Assets, getTimestampsFromMedia } from 'premid'
2+
3+
const presence = new Presence({
4+
clientId: '1400642676487098519',
5+
})
6+
7+
function getAction(): string {
8+
const href = document.location.href
9+
if (href.includes('/movie'))
10+
return 'movie'
11+
if (href.includes('/tv'))
12+
return 'tv'
13+
if (href.includes('/live-tv'))
14+
return 'live'
15+
if (href.includes('/watch/channel/'))
16+
return 'live'
17+
if (href.includes('/sports'))
18+
return 'sports'
19+
if (href.includes('/audiobooks'))
20+
return 'audiobooks'
21+
if (href.includes('/listen/'))
22+
return 'audiobooks'
23+
return 'home'
24+
}
25+
26+
function getText(selector: string): string {
27+
return document.querySelector(selector)?.textContent?.trim() || ''
28+
}
29+
30+
function getStatus(): string {
31+
const url = window.location.href
32+
33+
// Sports: extract from h2 element
34+
if (url.includes('/sports/')) {
35+
const h2 = document.querySelector<HTMLElement>('h2.text-transparent')
36+
return h2?.textContent?.trim() || 'Browsing'
37+
}
38+
39+
// Get content from meta tag
40+
const meta = document.querySelector<HTMLMetaElement>(
41+
'meta[name="twitter:title"]',
42+
)
43+
const rawTitle = meta?.content?.trim()
44+
if (!rawTitle)
45+
return 'Browsing'
46+
47+
// Audiobooks: extract book title and author
48+
if (url.includes('/audiobooks') || url.includes('/listen/')) {
49+
const cleanedTitle = rawTitle.replace(/\s*\s*MappleTV$/i, '')
50+
const titleMatch = cleanedTitle.match(/"([^"]+)"/)
51+
const byIndex = cleanedTitle.toLowerCase().indexOf(' by ')
52+
const author = byIndex !== -1 ? cleanedTitle.slice(byIndex + 4).trim() : 'Unknown Author'
53+
const book = titleMatch?.[1]?.trim() || 'Unknown Title'
54+
return `${book} by ${author}`
55+
}
56+
57+
// Live TV: remove 'Streaming - ' prefix
58+
if (url.includes('/watch/channel/')) {
59+
return rawTitle.replace(/^Streaming\s*-\s*/i, '').trim()
60+
}
61+
62+
return `${rawTitle}`
63+
}
64+
65+
const constructAction: Record<string, string> = {
66+
movie: 'Watching a Movie',
67+
tv: 'Watching a TV Series',
68+
live: 'Streaming Live TV',
69+
sports: 'Watching Sports',
70+
audiobooks: 'Listening to an Audiobook',
71+
home: 'Browsing',
72+
}
73+
74+
async function updatePresence() {
75+
const action = getAction()
76+
77+
const [
78+
privacy,
79+
showBrowsing,
80+
showLive,
81+
showMovies,
82+
showTVShows,
83+
showAudiobooks,
84+
showSports,
85+
] = await Promise.all([
86+
presence.getSetting<boolean>('privacy'),
87+
presence.getSetting<boolean>('browse'),
88+
presence.getSetting<boolean>('live'),
89+
presence.getSetting<boolean>('movies'),
90+
presence.getSetting<boolean>('tvshows'),
91+
presence.getSetting<boolean>('audiobooks'),
92+
presence.getSetting<boolean>('sports'),
93+
])
94+
95+
// Privacy mode enabled — show nothing except icon + timestamps (if any)
96+
if (privacy) {
97+
const presenceData: PresenceData = {
98+
largeImageKey: 'https://i.ibb.co/9PPkt6L/mappletv.png',
99+
}
100+
101+
const video = document.querySelector('video')
102+
if (video && getStatus().toLowerCase() !== 'pause') {
103+
const [start, end] = getTimestampsFromMedia(video)
104+
presenceData.startTimestamp = start
105+
presenceData.endTimestamp = end
106+
presenceData.smallImageKey = Assets.Play
107+
}
108+
else if (video) {
109+
presenceData.smallImageKey = Assets.Pause
110+
}
111+
112+
presence.setActivity(presenceData)
113+
return
114+
}
115+
116+
const presenceData: PresenceData = {
117+
largeImageKey: 'https://i.ibb.co/9PPkt6L/mappletv.png',
118+
details: constructAction[action],
119+
}
120+
121+
// Show 'Browsing' only if it's allowed
122+
if (!['movie', 'tv', 'sports', 'live', 'audiobooks'].includes(action)) {
123+
if (showBrowsing) {
124+
presenceData.details = 'Home'
125+
presenceData.startTimestamp = Math.floor(Date.now() / 1000)
126+
}
127+
else {
128+
delete presenceData.details
129+
}
130+
presence.setActivity(presenceData)
131+
return
132+
}
133+
134+
const allowDetail
135+
= (action === 'movie' && showMovies)
136+
|| (action === 'tv' && showTVShows)
137+
|| (action === 'live' && showLive)
138+
|| (action === 'audiobooks' && showAudiobooks)
139+
|| (action === 'sports' && showSports)
140+
141+
const video = document.querySelector('video')
142+
if (video && getStatus().toLowerCase() !== 'pause') {
143+
const [start, end] = getTimestampsFromMedia(video)
144+
presenceData.startTimestamp = start
145+
presenceData.endTimestamp = end
146+
presenceData.smallImageKey = Assets.Play
147+
}
148+
else if (video) {
149+
presenceData.smallImageKey = Assets.Pause
150+
}
151+
152+
if (allowDetail) {
153+
const subtitle
154+
= getText('.player-title-bar') || getText('[class*=player-title-bar]')
155+
presenceData.state = `${getStatus()}${subtitle ? ` | ${subtitle}` : ''}`
156+
}
157+
158+
presence.setActivity(presenceData)
159+
}
160+
161+
presence.on('UpdateData', updatePresence)

0 commit comments

Comments
 (0)