From a978533c9badac3019a1b41e84be6b88b95282d1 Mon Sep 17 00:00:00 2001 From: pierrequiroul <35348696+pierrequiroul@users.noreply.github.com> Date: Thu, 14 Nov 2024 03:02:12 +0100 Subject: [PATCH 1/3] feat(RTLplay): fixed timestamps and created utils --- websites/R/RTLplay/metadata.json | 18 +- websites/R/RTLplay/presence.ts | 519 ++++++++++--------------------- websites/R/RTLplay/util.ts | 266 ++++++++++++++++ 3 files changed, 433 insertions(+), 370 deletions(-) create mode 100644 websites/R/RTLplay/util.ts diff --git a/websites/R/RTLplay/metadata.json b/websites/R/RTLplay/metadata.json index e0e513fa92af..e2c772115e57 100644 --- a/websites/R/RTLplay/metadata.json +++ b/websites/R/RTLplay/metadata.json @@ -41,7 +41,7 @@ "multiLanguage": true }, { - "id": "privacy", + "id": "usePrivacyMode", "title": "Privacy Mode", "icon": "fad fa-user-secret", "value": false @@ -49,7 +49,7 @@ { "id": "usePresenceName", "if": { - "privacy": false + "usePrivacyMode": false }, "title": "Show Title as Presence", "icon": "fad fa-user-edit", @@ -58,7 +58,7 @@ { "id": "useChannelName", "if": { - "privacy": false, + "usePrivacyMode": false, "usePresenceName": true }, "title": "Show Live Channel as Presence", @@ -66,27 +66,27 @@ "value": true }, { - "id": "usePosterImage", + "id": "usePoster", "if": { - "privacy": false + "usePrivacyMode": false }, "title": "Show Poster Image", "icon": "fad fa-images", "value": true }, { - "id": "time", + "id": "useTimestamps", "if": { - "privacy": false + "usePrivacyMode": false }, "title": "Show Timestamps", "icon": "fad fa-stopwatch", "value": true }, { - "id": "buttons", + "id": "useButtons", "if": { - "privacy": false + "usePrivacyMode": false }, "title": "Show Buttons", "icon": "fas fa-compress-arrows-alt", diff --git a/websites/R/RTLplay/presence.ts b/websites/R/RTLplay/presence.ts index 5ee76d36ad4e..11b26d7c2d02 100644 --- a/websites/R/RTLplay/presence.ts +++ b/websites/R/RTLplay/presence.ts @@ -1,267 +1,58 @@ +import { + exist, + getThumbnail, + stringsMap, + getAdditionnalStrings, + limitText, + LargeAssets, + getChannel, +} from "./util"; + const presence = new Presence({ clientId: "1240716875927916616", }), browsingTimestamp = Math.floor(Date.now() / 1000), getStrings = async () => { return presence.getStrings( - { - play: "general.playing", - pause: "general.paused", - search: "general.search", - searchSomething: "general.searchSomething", - browsing: "general.browsing", - viewing: "general.viewing", - viewPage: "general.viewPage", - viewAPage: "general.viewAPage", - viewHome: "general.viewHome", - viewAccount: "general.viewAccount", - viewChannel: "general.viewChannel", - viewCategory: "general.viewCategory", - viewList: "netflix.viewList", - buttonViewPage: "general.buttonViewPage", - watching: "general.watching", - watchingAd: "youtube.ad", - watchingLive: "general.watchingLive", - watchingShow: "general.watchingShow", - watchingMovie: "general.watchingMovie", - listeningMusic: "general.listeningMusic", - buttonWatchStream: "general.buttonWatchStream", - buttonWatchVideo: "general.buttonWatchVideo", - buttonWatchEpisode: "general.buttonViewEpisode", - buttonWatchMovie: "general.buttonWatchMovie", - buttonListenAlong: "general.buttonListenAlong", - live: "general.live", - season: "general.season", - episode: "general.episode", - // Non-existent, should be general strings - deferred: "general.deferred", - }, + stringsMap, await presence.getSetting("lang").catch(() => "en") ); }; let oldLang: string = null, strings: Awaited>; -const enum Assets { - Logo = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/logo.png", - Animated = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/0.gif", - Deferred = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/1.gif", - LiveAnimated = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/2.gif", - Listening = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/3.png", - AdEn = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/4.png", - AdFr = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/5.png", - RTLPlay = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/6.png", - RTLTVi = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/7.png", - RTLClub = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/8.png", - RTLPlug = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/9.png", - BelRTL = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/10.png", - Contact = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/11.png", -} - -function getAdditionnalStrings(lang: string) { - switch (true) { - case ["fr-FR"].includes(lang): { - strings.deferred = "En Différé"; - break; - } - case ["nl-NL"].includes(lang): { - strings.deferred = "Uitgestelde"; - break; - } - case ["de-DE"].includes(lang): { - strings.deferred = "Zeitversetzt"; - break; - } - default: { - strings.deferred = "Deferred"; - break; - } - } -} - -function getChannel(channel: string) { - switch (true) { - case channel.includes("tvi"): { - return { - channel: "RTL TVi", - type: ActivityType.Watching, - logo: Assets.RTLTVi, - }; - } - case channel.includes("club"): { - return { - channel: "RTL club", - type: ActivityType.Watching, - logo: Assets.RTLClub, - }; - } - case channel.includes("plug"): { - return { - channel: "RTL plug", - type: ActivityType.Watching, - logo: Assets.RTLPlug, - }; - } - case channel.includes("bel"): { - return { - channel: "Bel RTL", - type: ActivityType.Listening, - logo: Assets.BelRTL, - }; - } - case channel.includes("contact"): { - return { - channel: "Radio Contact", - type: ActivityType.Listening, - logo: Assets.Contact, - }; - } - default: { - return { - channel, - type: ActivityType.Watching, - logo: Assets.RTLPlay, - }; - } - } -} - -function exist(selector: string) { - return document.querySelector(selector) !== null; -} - -// Adapted veryCrunchy's function from YouTube Presence https://github.com/PreMiD/Presences/pull/8000 -async function getThumbnail(src: string): Promise { - return new Promise(resolve => { - const img = new Image(), - wh = 320, - borderThickness = 15, // Thickness of the gradient border - cropLeftRightPercentage = 0.14, // Percentage to crop from left and right for landscape mode (e.g., 0.1 for 10%) - cropTopBottomPercentage = 0.025; // Percentage to crop from top and bottom for portrait mode (e.g., 0.1 for 10%) - img.crossOrigin = "anonymous"; - img.src = src; - - img.onload = function () { - let croppedWidth, - croppedHeight, - cropX = 0, - cropY = 0; - - if (img.width > img.height) { - // Landscape mode: crop left and right - const cropLeftRight = img.width * cropLeftRightPercentage; - croppedWidth = img.width - 2 * cropLeftRight; - croppedHeight = img.height; - cropX = cropLeftRight; - } else { - // Portrait mode: crop top and bottom - const cropTopBottom = img.height * cropTopBottomPercentage; - croppedWidth = img.width; - croppedHeight = img.height - 2 * cropTopBottom; - cropY = cropTopBottom; - } - - const isLandscape = croppedWidth >= croppedHeight; - let newWidth, newHeight, offsetX, offsetY; - - if (isLandscape) { - newWidth = wh; - newHeight = (wh / croppedWidth) * croppedHeight; - offsetX = 0; - offsetY = (wh - newHeight) / 2; - } else { - newHeight = wh; - newWidth = (wh / croppedHeight) * croppedWidth; - offsetX = (wh - newWidth) / 2; - offsetY = 0; - } - - const tempCanvas = document.createElement("canvas"); - tempCanvas.width = wh; - tempCanvas.height = wh; - const ctx = tempCanvas.getContext("2d"); - - // Fill the canvas with the background color - ctx.fillStyle = "#172e4e"; - ctx.fillRect(0, 0, wh, wh); - - // Create the gradient - const gradient = ctx.createLinearGradient(0, 0, wh, 0); - gradient.addColorStop(0, "rgba(245,3,26,1)"); - gradient.addColorStop(0.5, "rgba(63,187,244,1)"); - gradient.addColorStop(1, "rgba(164,215,12,1)"); - - // Draw the gradient borders - if (isLandscape) { - // Top border - ctx.fillStyle = gradient; - ctx.fillRect(0, offsetY - borderThickness, wh, borderThickness); - - // Bottom border - ctx.fillStyle = gradient; - ctx.fillRect(0, offsetY + newHeight, wh, borderThickness); - } else { - // Create a vertical gradient for portrait mode - const verticalGradient = ctx.createLinearGradient(0, 0, 0, wh); - verticalGradient.addColorStop(0, "rgba(245,3,26,1)"); - verticalGradient.addColorStop(0.5, "rgba(63,187,244,1)"); - verticalGradient.addColorStop(1, "rgba(164,215,12,1)"); - - // Left border - ctx.fillStyle = verticalGradient; - ctx.fillRect(offsetX - borderThickness, 0, borderThickness, wh); - - // Right border - ctx.fillStyle = verticalGradient; - ctx.fillRect(offsetX + newWidth, 0, borderThickness, wh); - } - - // Draw the cropped image - ctx.drawImage( - img, - cropX, - cropY, - croppedWidth, - croppedHeight, - offsetX, - offsetY, - newWidth, - newHeight - ); - - resolve(tempCanvas.toDataURL("image/png")); - }; - - img.onerror = function () { - resolve(src); - }; - }); -} - presence.on("UpdateData", async () => { const { hostname, href, pathname } = document.location, presenceData: PresenceData = { name: "RTLplay", largeImageKey: - hostname === "www.radiocontact.be" ? Assets.Contact : Assets.Animated, // Default + hostname === "www.radiocontact.be" + ? LargeAssets.Contact + : LargeAssets.Animated, // Default largeImageText: "RTLplay", type: ActivityType.Watching, }, - [lang, usePresenceName, useChannelName, privacy, time, buttons, poster] = - await Promise.all([ - presence.getSetting("lang").catch(() => "en"), - presence.getSetting("usePresenceName"), - presence.getSetting("useChannelName"), - presence.getSetting("privacy"), - presence.getSetting("timestamp"), - presence.getSetting("buttons"), - presence.getSetting("usePosterImage"), - ]); + [ + lang, + usePresenceName, + useChannelName, + usePrivacyMode, + useTimestamps, + useButtons, + usePoster, + ] = await Promise.all([ + presence.getSetting("lang").catch(() => "en"), + presence.getSetting("usePresenceName"), + presence.getSetting("useChannelName"), + presence.getSetting("usePrivacyMode"), + presence.getSetting("useTimestamps"), + presence.getSetting("useButtons"), + presence.getSetting("usePoster"), + ]); if (oldLang !== lang || !strings) { oldLang = lang; - strings = await getStrings(); - getAdditionnalStrings(lang); + strings = getAdditionnalStrings(lang, await getStrings()); } switch (true) { @@ -276,10 +67,10 @@ presence.on("UpdateData", async () => { presenceData.smallImageKey = Assets.Reading; presenceData.smallImageText = strings.browsing; - if (!privacy) { + if (!usePrivacyMode) { presenceData.state = strings.viewHome; - if (time) presenceData.startTimestamp = browsingTimestamp; + if (useTimestamps) presenceData.startTimestamp = browsingTimestamp; } break; } @@ -288,27 +79,20 @@ presence.on("UpdateData", async () => { (https://www.rtlplay.be/rtlplay/recherche) */ case ["recherche"].includes(pathname.split("/")[2]): { - if (privacy) presenceData.details = strings.searchSomething; + if (usePrivacyMode) presenceData.details = strings.searchSomething; else { - presenceData.details = JSON.parse( - document - .querySelector("ol.search__results") - ?.getAttribute("data-tracking") - ).searchQuery + const {searchQuery} = JSON.parse(document.querySelector("ol.search__results")?.getAttribute("data-tracking")); + presenceData.details = searchQuery ? strings.search : strings.searchSomething; - presenceData.state = JSON.parse( - document - .querySelector("ol.search__results") - ?.getAttribute("data-tracking") - ).searchQuery.term; + presenceData.state = searchQuery?.term; - if (time) presenceData.startTimestamp = browsingTimestamp; + if (useTimestamps) presenceData.startTimestamp = browsingTimestamp; - if (buttons) { + if (useButtons) { presenceData.buttons = [ { - label: strings.buttonViewPage, // Need to be a general string + label: strings.buttonViewPage, url: href, // We are not redirecting directly to the raw video stream, it's only the media page }, ]; @@ -326,9 +110,9 @@ presence.on("UpdateData", async () => { case ["ma-liste"].includes(pathname.split("/")[2]): { presenceData.details = strings.browsing; presenceData.state = strings.viewAPage; - if (!privacy) { + if (!usePrivacyMode) { presenceData.state = strings.viewList; - if (buttons) { + if (useButtons) { presenceData.buttons = [ { label: strings.buttonViewPage, @@ -346,7 +130,7 @@ presence.on("UpdateData", async () => { case ["collection", "series", "films", "divertissement"].includes( pathname.split("/")[2] ): { - if (privacy) presenceData.details = strings.viewAPage; + if (usePrivacyMode) presenceData.details = strings.viewAPage; else { const data = JSON.parse( document.querySelector("script[type='application/ld+json']") @@ -365,9 +149,9 @@ presence.on("UpdateData", async () => { presenceData.smallImageKey = Assets.Viewing; presenceData.smallImageText = strings.viewCategory; - if (time) presenceData.startTimestamp = browsingTimestamp; + if (useTimestamps) presenceData.startTimestamp = browsingTimestamp; - if (buttons) { + if (useButtons) { presenceData.buttons = [ { label: strings.buttonViewPage, @@ -390,8 +174,8 @@ presence.on("UpdateData", async () => { case hostname === "www.rtlplay.be": { if (exist("div.playerui__adBreakInfo")) { presenceData.smallImageKey = ["fr-FR"].includes(lang) - ? Assets.AdFr - : Assets.AdEn; + ? LargeAssets.AdFr + : LargeAssets.AdEn; presenceData.smallImageText = strings.watchingAd; } else if (exist("i.playerui__icon--name-play")) { // State paused @@ -399,17 +183,17 @@ presence.on("UpdateData", async () => { presenceData.smallImageText = strings.pause; } else if (exist("div.playerui__liveStat--deferred")) { // State deferred - presenceData.smallImageKey = Assets.Deferred; + presenceData.smallImageKey = LargeAssets.Deferred; presenceData.smallImageText = strings.deferred; } else { // State live - presenceData.smallImageKey = Assets.LiveAnimated; + presenceData.smallImageKey = LargeAssets.LiveAnimated; presenceData.smallImageText = strings.live; } - if (privacy) { + if (usePrivacyMode) { presenceData.details = strings.watchingLive; - presenceData.largeImageKey = Assets.Logo; + presenceData.largeImageKey = LargeAssets.Logo; } else { if ( !useChannelName && @@ -463,7 +247,7 @@ presence.on("UpdateData", async () => { ).channel; } - if (time && exist("span.playerui__controls__stat__time")) { + if (useTimestamps && exist("span.playerui__controls__stat__time")) { // Radio livestream doesn't have stat time [presenceData.startTimestamp, presenceData.endTimestamp] = presence.getTimestamps( @@ -490,7 +274,7 @@ presence.on("UpdateData", async () => { ) / 60 )} min`; } - if (buttons) { + if (useButtons) { presenceData.buttons = [ { label: strings.buttonWatchStream, @@ -506,14 +290,14 @@ presence.on("UpdateData", async () => { presenceData.type = getChannel("contact").type; if (exist('button[aria-label="stop"]')) { - presenceData.smallImageKey = Assets.Listening; + presenceData.smallImageKey = LargeAssets.Listening; presenceData.smallImageText = strings.listeningMusic; } else { presenceData.smallImageKey = Assets.Stop; presenceData.smallImageText = strings.pause; } - if (!privacy) { + if (!usePrivacyMode) { // Fetch the data from the API const response = await fetch( "https://core-search.radioplayer.cloud/056/qp/v4/events/?rpId=1" @@ -539,7 +323,7 @@ presence.on("UpdateData", async () => { presenceData.largeImageText = getChannel("contact").channel; - if (buttons) { + if (useButtons) { presenceData.buttons = [ { label: strings.buttonListenAlong, @@ -570,61 +354,90 @@ presence.on("UpdateData", async () => { /^(?.*?)\sS(?\d+)\sE(?\d+)\s(?.*)$/ ) || {} ).groups || {}; - if (privacy) { - presenceData.details = !episodeName + let isPaused = false; + presenceData.largeImageKey = LargeAssets.Logo; // Intializing default + + if (usePrivacyMode) { + presenceData.details = episodeName ? strings.watchingShow : strings.watchingMovie; - presenceData.largeImageKey = Assets.Logo; + + presenceData.smallImageKey = LargeAssets.Privacy; + presenceData.smallImageText = strings.privacy; } else { + // Media Infos if (usePresenceName) presenceData.name = mediaName; - presenceData.details = episodeName - ? `${usePresenceName ? "" : strings.watching} ${mediaName}` + presenceData.details = mediaName; + presenceData.state = episodeName + ? `S${seasonNumber} E${episodeNumber} - ${episodeName}` : strings.watchingMovie; - presenceData.state = episodeName || mediaName; - - if (poster) { - presenceData.largeImageKey = await getThumbnail( - document - .querySelector("#content > script") - .textContent.match( - /window\.App\.playerData\s*=\s*\{[\s\S]*?poster:\s*"(.*?)",/ - )[1] - .replace(/\\u0026/g, "&") - .replace(/\\/g, "") - ); - } if (seasonNumber && episodeNumber) presenceData.largeImageText = `${strings.season} ${seasonNumber} - ${strings.episode} ${episodeNumber}`; - if (time) { - [presenceData.startTimestamp, presenceData.endTimestamp] = - presence.getTimestamps( - presence.timestampFromFormat( - document - .querySelector("span.playerui__controls__stat__time") - .textContent.split("/")[0] - .trim() - ), - presence.timestampFromFormat( - document - .querySelector("span.playerui__controls__stat__time") - .textContent.split("/")[1] - .trim() - ) - ); + // Progress Bar / Timestamps + if (useTimestamps) { + const video = document.querySelector("video"); + if (video) { + // Getting timestamps directly from video + [presenceData.startTimestamp, presenceData.endTimestamp] = + presence.getTimestampsfromMedia(video as HTMLMediaElement); + isPaused = video.paused; + } else { + // Fallback method + const formattedTimestamps = document + .querySelector(".playerui__controls__stat__time") + ?.textContent.split("/"); + [presenceData.startTimestamp, presenceData.endTimestamp] = + presence.getTimestamps( + presence.timestampFromFormat(formattedTimestamps?.[0].trim()), + presence.timestampFromFormat(formattedTimestamps?.[1].trim()) + ); + isPaused = exist("i.playerui__icon--name-play"); + } } else { presenceData.largeImageText += ` - ${Math.round( - presence.timestampFromFormat( - document - .querySelector("span.playerui__controls__stat__time") - .textContent.split("/")[1] - .trim() - ) / 60 + presenceData.endTimestamp.valueOf() / 60 )} min`; } - if (buttons) { + + // Key Art - Poster + if (usePoster) { + presenceData.largeImageKey = await getThumbnail( + document + .querySelector("#content > script") + .textContent.match( + /window\.App\.playerData\s*=\s*\{[\s\S]*?poster:\s*"(.*?)",/ + )[1] + .replace(/\\u0026/g, "&") + .replace(/\\/g, "") + ); + } + + // Key Art - Status + const ad = exist("div.playerui__adBreakInfo"); + if (isPaused) { + // State paused + presenceData.smallImageKey = ad + ? ["fr-FR"].includes(lang) + ? LargeAssets.AdFr + : LargeAssets.AdEn + : Assets.Pause; + presenceData.smallImageText = ad ? strings.watchingAd : strings.pause; + delete presenceData.startTimestamp; + delete presenceData.endTimestamp; + } else { + // State playing + presenceData.smallImageKey = ad + ? ["fr-FR"].includes(lang) + ? LargeAssets.AdFr + : LargeAssets.AdEn + : Assets.Play; + presenceData.smallImageText = ad ? strings.watchingAd : strings.play; + } + + if (useButtons) { presenceData.buttons = [ { label: episodeName @@ -636,72 +449,56 @@ presence.on("UpdateData", async () => { } } - const ad = exist("div.playerui__adBreakInfo"); - if (exist("i.playerui__icon--name-play")) { - // State paused - presenceData.smallImageKey = ad - ? ["fr-FR"].includes(lang) - ? Assets.AdFr - : Assets.AdEn - : Assets.Pause; - presenceData.smallImageText = ad ? strings.watchingAd : strings.pause; - } else { - presenceData.smallImageKey = ad - ? ["fr-FR"].includes(lang) - ? Assets.AdFr - : Assets.AdEn - : Assets.Play; - presenceData.smallImageText = ad ? strings.watchingAd : strings.play; - } break; } /* MEDIA PAGE (Page de media) (https://www.rtlplay.be/rtlplay/salvation~2ab30366-51fe-4b29-a720-5e41c9bd6991) */ case pathname.split("/")[2].length > 15: { - presenceData.smallImageKey = Assets.Viewing; - presenceData.smallImageText = strings.viewAPage; + + + if (usePrivacyMode) { + presenceData.details = strings.browsing; + presenceData.state = strings.viewAPage; + presenceData.smallImageKey = LargeAssets.Privacy; + presenceData.smallImageText = strings.privacy; + } else { + const summaryElement = document.querySelector("p.detail__description"), + yearElement = document.querySelector('dd.detail__meta-label[title="Année de production"]'), + durationElement = document.querySelector('dd.detail__meta-label[title="Durée"]'), + seasonElement = document.querySelector("dd.detail__meta-label:not([title])"), + genresArray = document.querySelectorAll("dl:nth-child(1) > dd > a"), + isMovie = document.querySelector('meta[property="og:type"]').getAttribute("content").includes("movie"); + + let subtitle = isMovie ? strings.movie : strings.tvshow; + subtitle += yearElement ? ` - ${yearElement.textContent}` : ""; // Add Release Year + subtitle += seasonElement && !isMovie ? ` - ${seasonElement.textContent}` : ""; // Add amount of seasons + subtitle += durationElement ? ` - ${durationElement.textContent}` : ""; // Add Duration - if (privacy) presenceData.details = strings.viewAPage; - else { - let subtitle = document.querySelector( - 'dd.detail__meta-label[title="Année de production"]' - ) - ? document.querySelector( - 'dd.detail__meta-label[title="Année de production"]' - ).textContent - : ""; // Get Release Year - subtitle += document.querySelector( - 'dd.detail__meta-label[title="Durée"]' - ) - ? ` - ${ - document.querySelector('dd.detail__meta-label[title="Durée"]') - .textContent - }` - : ""; // Get Duration for ( - let i = 0; - document.querySelectorAll("dl:nth-child(1) > dd > a").length > i; - i++ // Get Genres + const element of genresArray // Add Genres ) { subtitle += ` - ${ - document.querySelectorAll("dl:nth-child(1) > dd > a")[i].textContent + element.textContent }`; } - presenceData.details = strings.viewPage; - presenceData.state = - document.querySelector("h1.detail__title").textContent; + presenceData.details = document.querySelector("h1.detail__title").textContent; // Title + presenceData.state = subtitle; + + presenceData.largeImageText = summaryElement ? limitText(summaryElement.textContent) : subtitle; // Summary if available - presenceData.largeImageText = subtitle; + presenceData.smallImageKey = LargeAssets.Binoculars; + presenceData.smallImageText = strings.browsing; - if (poster) { + if (usePoster) { + presenceData.largeImageKey = LargeAssets.Logo; // Temp placeholder presenceData.largeImageKey = await getThumbnail( - document.querySelector("img.detail__poster").getAttribute("src") + document.querySelector("img.detail__poster")?.getAttribute("src") ); } - if (buttons) { + if (useButtons) { presenceData.buttons = [ { label: strings.buttonViewPage, diff --git a/websites/R/RTLplay/util.ts b/websites/R/RTLplay/util.ts new file mode 100644 index 000000000000..309b6e82878c --- /dev/null +++ b/websites/R/RTLplay/util.ts @@ -0,0 +1,266 @@ +export function exist(selector: string) { + return document.querySelector(selector) !== null; +} + +export const stringsMap = { + play: "general.playing", + pause: "general.paused", + search: "general.search", + searchSomething: "general.searchSomething", + browsing: "general.browsing", + viewing: "general.viewing", + viewPage: "general.viewPage", + viewAPage: "general.viewAPage", + viewHome: "general.viewHome", + viewAccount: "general.viewAccount", + viewChannel: "general.viewChannel", + viewCategory: "general.viewCategory", + viewList: "netflix.viewList", + buttonViewPage: "general.buttonViewPage", + watching: "general.watching", + watchingAd: "youtube.ad", + watchingLive: "general.watchingLive", + watchingShow: "general.watchingShow", + watchingMovie: "general.watchingMovie", + listeningMusic: "general.listeningMusic", + buttonWatchStream: "general.buttonWatchStream", + buttonWatchVideo: "general.buttonWatchVideo", + buttonWatchEpisode: "general.buttonViewEpisode", + buttonWatchMovie: "general.buttonWatchMovie", + buttonListenAlong: "general.buttonListenAlong", + live: "general.live", + season: "general.season", + episode: "general.episode", + // Custom strings + deferred: "general.deferred", + movie: "general.movie", + tvshow: "general.tvshow", + privacy: "general.privacy", +}; + +export function getAdditionnalStrings(lang: string, strings: typeof stringsMap) { + switch (lang) { + case "fr-FR": { + strings.deferred = "En Différé"; + strings.movie = "Film"; + strings.tvshow = "Série"; + strings.privacy = "Lecture privée"; + + // Improved translation in the context of this website + strings.watchingShow = "Regarde une émission ou une série"; + break; + } + case "nl-NL": { + strings.deferred = "Uitgestelde"; + strings.movie = "Film"; + strings.tvshow = "Série"; + strings.privacy = ""; + break; + } + case "de-DE": { + strings.deferred = "Zeitversetzt"; + strings.movie = "Film"; + strings.tvshow = "Fernsehserie"; + strings.privacy = ""; + break; + } + default: { + strings.deferred = "Deferred"; + strings.movie = "Movie"; + strings.tvshow = "TV Serie"; + strings.privacy = "Private mode"; + } + } + return strings; +} + +// Mainly used to truncate largeImageKeyText because the limit is 128 characters +export function limitText(input: string) { + const maxLength = 128, + ellipsis = " ..."; + + // If input is within limit, return it as is + if (input.length <= maxLength) return input; + + // Truncate to 125 characters (leaving room for ellipsis) + let truncated = input.slice(0, maxLength - ellipsis.length); + + // If the truncated text ends mid-word, remove the partial word + if (truncated.lastIndexOf(" ") !== -1) + truncated = truncated.slice(0, truncated.lastIndexOf(" ")); + + return truncated + ellipsis; +} + +export const enum LargeAssets { + Logo = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/logo.png", + Animated = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/0.gif", + Deferred = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/1.gif", + LiveAnimated = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/2.gif", + Listening = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/3.png", + Binoculars = "https://imgur.com/aF3TWVK.png", + Privacy = "https://imgur.com/nokHvhE.png", + AdEn = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/4.png", + AdFr = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/5.png", + RTLPlay = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/6.png", + RTLTVi = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/7.png", + RTLClub = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/8.png", + RTLPlug = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/9.png", + BelRTL = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/10.png", + Contact = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/11.png", +} + +export function getChannel(channel: string) { + switch (true) { + case channel.includes("tvi"): { + return { + channel: "RTL TVi", + type: ActivityType.Watching, + logo: LargeAssets.RTLTVi, + }; + } + case channel.includes("club"): { + return { + channel: "RTL club", + type: ActivityType.Watching, + logo: LargeAssets.RTLClub, + }; + } + case channel.includes("plug"): { + return { + channel: "RTL plug", + type: ActivityType.Watching, + logo: LargeAssets.RTLPlug, + }; + } + case channel.includes("bel"): { + return { + channel: "Bel RTL", + type: ActivityType.Listening, + logo: LargeAssets.BelRTL, + }; + } + case channel.includes("contact"): { + return { + channel: "Radio Contact", + type: ActivityType.Listening, + logo: LargeAssets.Contact, + }; + } + default: { + return { + channel, + type: ActivityType.Watching, + logo: LargeAssets.RTLPlay, + }; + } + } +} + +// Adapted veryCrunchy's function from YouTube Presence https://github.com/PreMiD/Presences/pull/8000 +export async function getThumbnail(src: string): Promise { + return new Promise(resolve => { + const img = new Image(), + wh = 320, + borderThickness = 15, // Thickness of the gradient border + cropLeftRightPercentage = 0.14, // Percentage to crop from left and right for landscape mode (e.g., 0.1 for 10%) + cropTopBottomPercentage = 0.025; // Percentage to crop from top and bottom for portrait mode (e.g., 0.1 for 10%) + img.crossOrigin = "anonymous"; + img.src = src; + + img.onload = function () { + let croppedWidth, + croppedHeight, + cropX = 0, + cropY = 0; + + if (img.width > img.height) { + // Landscape mode: crop left and right + const cropLeftRight = img.width * cropLeftRightPercentage; + croppedWidth = img.width - 2 * cropLeftRight; + croppedHeight = img.height; + cropX = cropLeftRight; + } else { + // Portrait mode: crop top and bottom + const cropTopBottom = img.height * cropTopBottomPercentage; + croppedWidth = img.width; + croppedHeight = img.height - 2 * cropTopBottom; + cropY = cropTopBottom; + } + + const isLandscape = croppedWidth >= croppedHeight; + let newWidth, newHeight, offsetX, offsetY; + + if (isLandscape) { + newWidth = wh; + newHeight = (wh / croppedWidth) * croppedHeight; + offsetX = 0; + offsetY = (wh - newHeight) / 2; + } else { + newHeight = wh; + newWidth = (wh / croppedHeight) * croppedWidth; + offsetX = (wh - newWidth) / 2; + offsetY = 0; + } + + const tempCanvas = document.createElement("canvas"); + tempCanvas.width = wh; + tempCanvas.height = wh; + const ctx = tempCanvas.getContext("2d"); + + // Fill the canvas with the background color + ctx.fillStyle = "#172e4e"; + ctx.fillRect(0, 0, wh, wh); + + // Create the gradient + const gradient = ctx.createLinearGradient(0, 0, wh, 0); + gradient.addColorStop(0, "rgba(245,3,26,1)"); + gradient.addColorStop(0.5, "rgba(63,187,244,1)"); + gradient.addColorStop(1, "rgba(164,215,12,1)"); + + // Draw the gradient borders + if (isLandscape) { + // Top border + ctx.fillStyle = gradient; + ctx.fillRect(0, offsetY - borderThickness, wh, borderThickness); + + // Bottom border + ctx.fillStyle = gradient; + ctx.fillRect(0, offsetY + newHeight, wh, borderThickness); + } else { + // Create a vertical gradient for portrait mode + const verticalGradient = ctx.createLinearGradient(0, 0, 0, wh); + verticalGradient.addColorStop(0, "rgba(245,3,26,1)"); + verticalGradient.addColorStop(0.5, "rgba(63,187,244,1)"); + verticalGradient.addColorStop(1, "rgba(164,215,12,1)"); + + // Left border + ctx.fillStyle = verticalGradient; + ctx.fillRect(offsetX - borderThickness, 0, borderThickness, wh); + + // Right border + ctx.fillStyle = verticalGradient; + ctx.fillRect(offsetX + newWidth, 0, borderThickness, wh); + } + + // Draw the cropped image + ctx.drawImage( + img, + cropX, + cropY, + croppedWidth, + croppedHeight, + offsetX, + offsetY, + newWidth, + newHeight + ); + + resolve(tempCanvas.toDataURL("image/png")); + }; + + img.onerror = function () { + resolve(src); + }; + }); +} \ No newline at end of file From 47262c511af25f5056b2069b04328f6efe0c0fd0 Mon Sep 17 00:00:00 2001 From: pierrequiroul <35348696+pierrequiroul@users.noreply.github.com> Date: Mon, 25 Nov 2024 00:51:49 +0100 Subject: [PATCH 2/3] feat(RTLplay): graphics update and other --- websites/R/RTLplay/metadata.json | 24 +- websites/R/RTLplay/presence.ts | 558 +++++++++++++++++++------------ websites/R/RTLplay/util.ts | 225 ++++++++----- 3 files changed, 498 insertions(+), 309 deletions(-) diff --git a/websites/R/RTLplay/metadata.json b/websites/R/RTLplay/metadata.json index e2c772115e57..ae35ff0c6e35 100644 --- a/websites/R/RTLplay/metadata.json +++ b/websites/R/RTLplay/metadata.json @@ -7,32 +7,36 @@ }, "service": "RTLplay", "description": { - "en": "With RTLplay, you'll find all the programs of our RTL TVI, Club RTL, Plug RTL, Bel RTL and Radio Contact channels live and in replay, as well as exclusive content.", - "fr": "Avec RTLplay, retrouvez tous les programmes de nos chaînes RTL TVI, Club RTL, Plug RTL, Bel RTL et Radio Contact en direct et en replay ainsi que des contenus exclusifs.", - "nl": "Met RTLplay kun je alle programma's op onze zenders RTL TVI, Club RTL, Plug RTL, Bel RTL en Radio Contact live en in replay bekijken, evenals exclusieve content.", - "de": "Mit RTLplay finden Sie alle Programme unserer Sender RTL TVI, Club RTL, Plug RTL, Bel RTL und Radio Contact als Live- und Replay-Stream sowie exklusive Inhalte." + "en": "With RTLplay, you'll find all the programs of our RTL TVI, Club RTL, Plug RTL, RTL District, Bel RTL and Radio Contact channels live and in replay, as well as exclusive content.", + "fr": "Avec RTLplay, retrouvez tous les programmes de nos chaînes RTL TVI, Club RTL, Plug RTL, RTL District, Bel RTL et Radio Contact en direct et en replay ainsi que des contenus exclusifs.", + "nl": "Met RTLplay kun je alle programma's op onze zenders RTL TVI, Club RTL, Plug RTL, RTL District, Bel RTL en Radio Contact live en in replay bekijken, evenals exclusieve content.", + "de": "Mit RTLplay finden Sie alle Programme unserer Sender RTL TVI, Club RTL, Plug RTL, RTL District, Bel RTL und Radio Contact als Live- und Replay-Stream sowie exklusive Inhalte." }, "url": [ "www.rtlplay.be", - "www.radiocontact.be" + "www.radiocontact.be", + "www.belrtl.be" ], - "version": "1.0.3", + "version": "2.0.0", "logo": "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/logo.png", - "thumbnail": "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/thumbnail.jpg", + "thumbnail": "https://i.imgur.com/3M9DpsS.jpeg", "color": "#121726", "category": "videos", "tags": [ - "rtl", + "belgium", "rtl-tvi", + "streaming", + "rtl", "tvi", "club", "plug", + "district", "contact", "belrtl", "media", - "streaming", - "belgium", "video", + "radio", + "tv", "broadcast" ], "settings": [ diff --git a/websites/R/RTLplay/presence.ts b/websites/R/RTLplay/presence.ts index 11b26d7c2d02..8ff4d0fa5474 100644 --- a/websites/R/RTLplay/presence.ts +++ b/websites/R/RTLplay/presence.ts @@ -1,11 +1,14 @@ import { - exist, - getThumbnail, stringsMap, getAdditionnalStrings, - limitText, LargeAssets, + getLocalizedAssets, + limitText, + exist, + adjustTimeError, getChannel, + cropPreset, + getThumbnail, } from "./util"; const presence = new Presence({ @@ -17,12 +20,16 @@ const presence = new Presence({ stringsMap, await presence.getSetting("lang").catch(() => "en") ); - }; + }, + slideshow = presence.createSlideshow(); + let oldLang: string = null, - strings: Awaited>; + strings: Awaited>, + oldPath = document.location.pathname; presence.on("UpdateData", async () => { const { hostname, href, pathname } = document.location, + pathParts = pathname.split("/"), presenceData: PresenceData = { name: "RTLplay", largeImageKey: @@ -55,21 +62,30 @@ presence.on("UpdateData", async () => { strings = getAdditionnalStrings(lang, await getStrings()); } + if (oldPath !== pathname) { + oldPath = pathname; + slideshow.deleteAllSlides(); + } + switch (true) { /* MAIN PAGE (Page principale) (https://www.rtlplay.be/) */ case pathname === "/" || - (pathname.split("/")[1] === "rtlplay" && - pathname.split("/").length <= 2): { + (pathParts[1] === "rtlplay" && pathParts.length <= 2): { presenceData.details = strings.browsing; - presenceData.smallImageKey = Assets.Reading; - presenceData.smallImageText = strings.browsing; + if (usePrivacyMode) { + presenceData.state = strings.viewAPage; - if (!usePrivacyMode) { + presenceData.smallImageKey = LargeAssets.Privacy; + presenceData.smallImageText = strings.privacy; + } else { presenceData.state = strings.viewHome; + presenceData.smallImageKey = LargeAssets.Binoculars; + presenceData.smallImageText = strings.browsing; + if (useTimestamps) presenceData.startTimestamp = browsingTimestamp; } break; @@ -78,14 +94,24 @@ presence.on("UpdateData", async () => { /* RESEARCH PAGE (Page de recherche) (https://www.rtlplay.be/rtlplay/recherche) */ - case ["recherche"].includes(pathname.split("/")[2]): { - if (usePrivacyMode) presenceData.details = strings.searchSomething; - else { - const {searchQuery} = JSON.parse(document.querySelector("ol.search__results")?.getAttribute("data-tracking")); - presenceData.details = searchQuery - ? strings.search + case ["recherche"].includes(pathParts[2]): { + if (usePrivacyMode) { + presenceData.details = strings.browsing; + presenceData.state = strings.searchSomething; + + presenceData.smallImageKey = LargeAssets.Privacy; + presenceData.smallImageText = strings.privacy; + } else { + const { searchQuery } = JSON.parse( + document + .querySelector('div[js-element="searchResults"]') + ?.getAttribute("data-tracking") + ); + + presenceData.details = strings.browsing; + presenceData.state = searchQuery + ? `${strings.searchFor} ${searchQuery.term}` : strings.searchSomething; - presenceData.state = searchQuery?.term; if (useTimestamps) presenceData.startTimestamp = browsingTimestamp; @@ -107,10 +133,15 @@ presence.on("UpdateData", async () => { /* MY LIST (Ma Liste) (https://www.rtlplay.be/rtlplay/ma-liste) */ - case ["ma-liste"].includes(pathname.split("/")[2]): { + case ["ma-liste"].includes(pathParts[2]): { presenceData.details = strings.browsing; - presenceData.state = strings.viewAPage; - if (!usePrivacyMode) { + + if (usePrivacyMode) { + presenceData.state = strings.viewAPage; + + presenceData.smallImageKey = LargeAssets.Privacy; + presenceData.smallImageText = strings.privacy; + } else { presenceData.state = strings.viewList; if (useButtons) { presenceData.buttons = [ @@ -128,26 +159,29 @@ presence.on("UpdateData", async () => { (https://www.rtlplay.be/rtlplay/collection/c2dBY3Rpb24) */ case ["collection", "series", "films", "divertissement"].includes( - pathname.split("/")[2] + pathParts[2] ): { - if (usePrivacyMode) presenceData.details = strings.viewAPage; - else { + if (usePrivacyMode) { + presenceData.state = strings.viewAPage; + + presenceData.smallImageKey = LargeAssets.Privacy; + presenceData.smallImageText = strings.privacy; + } else { const data = JSON.parse( document.querySelector("script[type='application/ld+json']") .textContent ); - presenceData.details = strings.viewCategory; - presenceData.state = - pathname.split("/")[2] !== "collection" - ? pathname.split("/")[2][0].toUpperCase() + - pathname.split("/")[2].substring(1) // to Upper Case the first letter + presenceData.state = strings.viewCategory.replace(":", ""); + presenceData.details = + pathParts[2] !== "collection" + ? pathParts[2][0].toUpperCase() + pathParts[2].substring(1) // to Upper Case the first letter : data[0]["@type"] === "CollectionPage" ? data[0].name : null; - presenceData.smallImageKey = Assets.Viewing; - presenceData.smallImageText = strings.viewCategory; + presenceData.smallImageKey = LargeAssets.Binoculars; + presenceData.smallImageText = strings.browsing; if (useTimestamps) presenceData.startTimestamp = browsingTimestamp; @@ -166,104 +200,118 @@ presence.on("UpdateData", async () => { /* DIRECT PAGE (Page des chaines en direct) (https://www.rtlplay.be/rtlplay/direct/tvi)*/ - case (hostname === "www.rtlplay.be" && - ["direct"].includes(pathname.split("/")[2])) || - (hostname === "www.radiocontact.be" && - ["player"].includes(pathname.split("/")[1])): { + case (hostname === "www.rtlplay.be" && ["direct"].includes(pathParts[2])) || + (["www.radiocontact.be", "www.belrtl.be"].includes(hostname) && + ["player"].includes(pathParts[1])): { switch (true) { case hostname === "www.rtlplay.be": { - if (exist("div.playerui__adBreakInfo")) { - presenceData.smallImageKey = ["fr-FR"].includes(lang) - ? LargeAssets.AdFr - : LargeAssets.AdEn; - presenceData.smallImageText = strings.watchingAd; - } else if (exist("i.playerui__icon--name-play")) { - // State paused - presenceData.smallImageKey = Assets.Pause; - presenceData.smallImageText = strings.pause; - } else if (exist("div.playerui__liveStat--deferred")) { - // State deferred - presenceData.smallImageKey = LargeAssets.Deferred; - presenceData.smallImageText = strings.deferred; - } else { - // State live - presenceData.smallImageKey = LargeAssets.LiveAnimated; - presenceData.smallImageText = strings.live; - } - if (usePrivacyMode) { presenceData.details = strings.watchingLive; - presenceData.largeImageKey = LargeAssets.Logo; + + presenceData.smallImageKey = LargeAssets.Privacy; + presenceData.smallImageText = strings.privacy; } else { + if (exist("div.playerui__adBreakInfo")) { + presenceData.smallImageKey = getLocalizedAssets(lang, "Ad"); + presenceData.smallImageText = strings.watchingAd; + } else if (exist("i.playerui__icon--name-play")) { + // State paused + presenceData.smallImageKey = Assets.Pause; + presenceData.smallImageText = strings.pause; + } else if (exist("div.playerui__liveStat--deferred")) { + // State deferred + presenceData.smallImageKey = LargeAssets.Deferred; + presenceData.smallImageText = strings.deferred; + } else { + // State live + presenceData.smallImageKey = LargeAssets.LiveAnimated; + presenceData.smallImageText = strings.live; + } + if ( !useChannelName && document .querySelector("li[aria-current='true'] > a > div > h2") .textContent.toLowerCase() !== "aucune donnée disponible" && - !["contact", "bel"].includes(pathname.split("/")[3]) // Radio show name are not relevant + !["contact", "bel"].includes(pathParts[3]) // Radio show name are not relevant ) { presenceData.name = document.querySelector( "li[aria-current='true'] > a > div > h2" ).textContent; - } else - presenceData.name = getChannel(pathname.split("/")[3]).channel; + } else presenceData.name = getChannel(pathParts[3]).channel; - presenceData.type = getChannel(pathname.split("/")[3]).type; + presenceData.type = getChannel(pathParts[3]).type; - presenceData.details = strings.watchingLive; - presenceData.state = document.querySelector( + presenceData.state = strings.watchingLive; + presenceData.details = document.querySelector( "li[aria-current='true'] > a > div > h2" ).textContent; - if (["contact", "bel"].includes(pathname.split("/")[3])) { + if (["contact", "bel"].includes(pathParts[3])) { /* Songs played in the livestream are the same as the audio radio ones but with video clips Fetch the data from the Radioplayer API. It is used on the official radio contact and bel rtl websites */ - const url = ["bel"].includes(pathname.split("/")[3]) - ? "https://core-search.radioplayer.cloud/056/qp/v4/events/?rpId=6" /* Bel RTL */ - : "https://core-search.radioplayer.cloud/056/qp/v4/events/?rpId=1" /* Radio Contact */, - response = await fetch(url), + const response = await fetch( + getChannel(pathParts[3]).radioplayerAPI + ), dataString = await response.text(), media = JSON.parse(dataString); if (media.results.now.type === "PE_E") { // When a song is played - presenceData.largeImageKey = media.results.now.songArtURL; // Song cover art are always square so we don't need to generate a thumbnail - presenceData.largeImageText = `${media.results.now.name} - ${media.results.now.artistName}`; + presenceData.largeImageKey = await getThumbnail(media.results.now.songArtURL); + presenceData.state = `${media.results.now.name} - ${media.results.now.artistName}`; } else { // When we don't have a song, we simply show the radio name as the show name is already displayed in state - presenceData.largeImageKey = getChannel( - pathname.split("/")[3] - ).logo; - presenceData.largeImageText = getChannel( - pathname.split("/")[3] - ).channel; + presenceData.largeImageKey = getChannel(pathParts[3]).logo; + presenceData.state = getChannel(pathParts[3]).channel; } + + presenceData.largeImageText = strings.watchingLiveMusic; + + presenceData.smallImageKey = LargeAssets.VinyleAnimated; + presenceData.smallImageText = strings.listeningMusic; } else { - presenceData.largeImageKey = getChannel( - pathname.split("/")[3] - ).logo; - presenceData.largeImageText = getChannel( - pathname.split("/")[3] - ).channel; + presenceData.largeImageKey = getChannel(pathParts[3]).logo; + presenceData.largeImageText = getChannel(pathParts[3]).channel; } - if (useTimestamps && exist("span.playerui__controls__stat__time")) { - // Radio livestream doesn't have stat time - [presenceData.startTimestamp, presenceData.endTimestamp] = - presence.getTimestamps( - presence.timestampFromFormat( - document - .querySelector("span.playerui__controls__stat__time") - .textContent.split("/")[0] - .trim() - ), - presence.timestampFromFormat( - document - .querySelector("span.playerui__controls__stat__time") - .textContent.split("/")[1] - .trim() - ) - ); + if (useTimestamps) { + if (exist("span.playerui__controls__stat__time")) { + // Video method: Uses video viewing statistics near play button if displayed + [presenceData.startTimestamp, presenceData.endTimestamp] = + presence.getTimestamps( + presence.timestampFromFormat( + document + .querySelector("span.playerui__controls__stat__time") + .textContent.split("/")[0] + .trim() + ), + presence.timestampFromFormat( + document + .querySelector("span.playerui__controls__stat__time") + .textContent.split("/")[1] + .trim() + ) + ); + } else { + // Fallback method: Uses program start and end times in tv guide overlay + presenceData.startTimestamp = Math.floor(new Date( + document + .querySelector( + 'li.live-broadcast__channel[aria-current="true"] time[js-element="startTime"]' + ) + .getAttribute("datetime") + .replace(/[+-]\d{2}:\d{2}\[.*\]/, "") // Removing UTC offset and the time zone + ).getTime() / 1000); + presenceData.endTimestamp = Math.floor(new Date( + document + .querySelector( + 'li.live-broadcast__channel[aria-current="true"] time[js-element="endTime"]' + ) + .getAttribute("datetime") + .replace(/[+-]\d{2}:\d{2}\[.*\]/, "") // Removing UTC offset and the time zone + ).getTime() / 1000); + } } else if (exist("span.playerui__controls__stat__time")) { presenceData.largeImageText += ` - ${Math.round( presence.timestampFromFormat( @@ -285,43 +333,77 @@ presence.on("UpdateData", async () => { } break; } - case hostname === "www.radiocontact.be": { - presenceData.name = getChannel("contact").channel; - presenceData.type = getChannel("contact").type; + case ["www.radiocontact.be", "www.belrtl.be"].includes(hostname): { + if (usePrivacyMode) { + presenceData.details = strings.listeningMusic; + + presenceData.type = ActivityType.Listening; - if (exist('button[aria-label="stop"]')) { - presenceData.smallImageKey = LargeAssets.Listening; + presenceData.smallImageKey = LargeAssets.VinyleAnimated; presenceData.smallImageText = strings.listeningMusic; } else { - presenceData.smallImageKey = Assets.Stop; - presenceData.smallImageText = strings.pause; - } + presenceData.name = getChannel(hostname).channel; + presenceData.type = getChannel(hostname).type; - if (!usePrivacyMode) { - // Fetch the data from the API - const response = await fetch( - "https://core-search.radioplayer.cloud/056/qp/v4/events/?rpId=1" - ), // Website backend use radioplayer api - dataString = await response.text(), - data = JSON.parse(dataString); - - if (data.results.now.type === "PE_E") { - // When a song is played - presenceData.details = data.results.now.name; - presenceData.state = data.results.now.artistName; - presenceData.largeImageKey = await getThumbnail( - data.results.now.songArtURL - ); + if (exist('button[aria-label="stop"]')) { + presenceData.smallImageKey = LargeAssets.VinyleAnimated; + presenceData.smallImageText = strings.listeningMusic; } else { - // When there's no song and only the show - presenceData.details = data.results.pis[0].programmeName; - presenceData.state = data.results.pis[0].programmeDescription; - presenceData.largeImageKey = await getThumbnail( - data.results.pis[0].imageUrl - ); + presenceData.smallImageKey = LargeAssets.Vinyle; + presenceData.smallImageText = strings.pause; } - presenceData.largeImageText = getChannel("contact").channel; + try { + // Fetch the data from the API + const response = await fetch(getChannel(hostname).radioplayerAPI), // Website backend use radioplayer api + dataString = await response.text(), + data = JSON.parse(dataString); + switch (data.results.now.type) { + // When a song is played + case "PE_E": { + presenceData.details = data.results.now.name; + presenceData.state = data.results.now.artistName; + presenceData.startTimestamp = + data.results.now.startTime || browsingTimestamp; + presenceData.endTimestamp = + data.results.now.stopTime || + delete presenceData.endTimestamp; + presenceData.largeImageKey = await getThumbnail( + data.results.now.songArtURL + ); + break; + } + // When there's no song and only the show (ex: radio host is speaking) + case "PI": { + presenceData.details = data.results.pis[0].programmeName; + presenceData.state = data.results.pis[0].programmeDescription; + presenceData.startTimestamp = + data.results.now.startTime || browsingTimestamp; + presenceData.endTimestamp = + data.results.now.stopTime || + delete presenceData.endTimestamp; + presenceData.largeImageKey = await getThumbnail( + data.results.pis[0].imageUrl + ); + break; + } + // When there's songs but no show (ex: late night) + default: { + presenceData.details = getChannel(hostname).channel; + presenceData.state = strings.listeningMusic; + presenceData.largeImageKey = getChannel(hostname).logo; + break; + } + } + + presenceData.largeImageText = limitText( + `${getChannel(hostname).channel} - ${ + data.results.now.serviceDescription + }` + ); + } catch (error) { + presence.error(`Error fetching data from the API: ${error}`); + } if (useButtons) { presenceData.buttons = [ @@ -341,7 +423,7 @@ presence.on("UpdateData", async () => { /* MEDIA PLAYER PAGE (Lecteur video) (https://www.rtlplay.be/rtlplay/player/75e9a91b-29d1-4856-be8c-0b3532862404) */ - case ["player"].includes(pathname.split("/")[2]): { + case ["player"].includes(pathParts[2]): { const { mediaName = document.querySelector("h1.lfvp-player__title").textContent, seasonNumber, @@ -366,75 +448,106 @@ presence.on("UpdateData", async () => { presenceData.smallImageText = strings.privacy; } else { // Media Infos - if (usePresenceName) presenceData.name = mediaName; - - presenceData.details = mediaName; - presenceData.state = episodeName - ? `S${seasonNumber} E${episodeNumber} - ${episodeName}` - : strings.watchingMovie; + if (usePresenceName) { + presenceData.name = mediaName; // Watching MediaName + presenceData.details = episodeName; // EpisodeName + if (episodeName) + presenceData.state = `${strings.season} ${seasonNumber}, ${strings.episode} ${episodeNumber}`; // Season 0, Episode 0 + } else { + presenceData.details = mediaName; // MediaName + if (episodeName) + presenceData.state = `S${seasonNumber} E${episodeNumber} - ${episodeName}`; // S0 - E0 - EpisodeName + } - if (seasonNumber && episodeNumber) - presenceData.largeImageText = `${strings.season} ${seasonNumber} - ${strings.episode} ${episodeNumber}`; + if (seasonNumber && episodeNumber) { + // MediaName - Season 0 - Episode 0 + presenceData.largeImageText = ` - ${strings.season} ${seasonNumber} - ${strings.episode} ${episodeNumber}`; + presenceData.largeImageText = + limitText(mediaName, 128 - presenceData.largeImageText.length) + + presenceData.largeImageText; + } // Progress Bar / Timestamps - if (useTimestamps) { - const video = document.querySelector("video"); + const ad = exist("div.playerui__adBreakInfo"); + if (useTimestamps && !ad) { + const video = document.querySelector("video") as HTMLMediaElement; if (video) { - // Getting timestamps directly from video - [presenceData.startTimestamp, presenceData.endTimestamp] = - presence.getTimestampsfromMedia(video as HTMLMediaElement); + // Video method: extracting from video object + presence.info("Timestamps is using video method"); + isPaused = video.paused; - } else { - // Fallback method - const formattedTimestamps = document - .querySelector(".playerui__controls__stat__time") - ?.textContent.split("/"); - [presenceData.startTimestamp, presenceData.endTimestamp] = - presence.getTimestamps( - presence.timestampFromFormat(formattedTimestamps?.[0].trim()), - presence.timestampFromFormat(formattedTimestamps?.[1].trim()) + + if (isPaused) { + presenceData.startTimestamp = browsingTimestamp; + delete presenceData.endTimestamp; + } else { + presenceData.startTimestamp = adjustTimeError( + presence.getTimestampsfromMedia(video)[0], + 5 + ); + presenceData.endTimestamp = adjustTimeError( + presence.getTimestampsfromMedia(video)[1], + 5 ); + } + } else { + // Fallback method: extracting from UI + presence.info("Timestamps is using fallback method"); + isPaused = exist("i.playerui__icon--name-play"); + + if (isPaused) { + presenceData.startTimestamp = browsingTimestamp; + delete presenceData.endTimestamp; + } else { + const formattedTimestamps = document + .querySelector(".playerui__controls__stat__time") + ?.textContent.split("/"); + + if (formattedTimestamps) { + [presenceData.startTimestamp, presenceData.endTimestamp] = + presence.getTimestamps( + adjustTimeError( + presence.timestampFromFormat( + formattedTimestamps[0].trim() + ), + 5 + ), + adjustTimeError( + presence.timestampFromFormat( + formattedTimestamps[1].trim() + ), + 5 + ) + ); + } + } } } else { - presenceData.largeImageText += ` - ${Math.round( - presenceData.endTimestamp.valueOf() / 60 - )} min`; + presenceData.startTimestamp = browsingTimestamp; + delete presenceData.endTimestamp; } + // Key Art - Status + presenceData.smallImageKey = ad + ? getLocalizedAssets(lang, "Ad") + : isPaused + ? Assets.Pause + : Assets.Play; + presenceData.smallImageText = ad + ? strings.watchingAd + : isPaused + ? strings.pause + : strings.play; + // Key Art - Poster if (usePoster) { presenceData.largeImageKey = await getThumbnail( - document - .querySelector("#content > script") - .textContent.match( - /window\.App\.playerData\s*=\s*\{[\s\S]*?poster:\s*"(.*?)",/ - )[1] - .replace(/\\u0026/g, "&") - .replace(/\\/g, "") - ); - } - - // Key Art - Status - const ad = exist("div.playerui__adBreakInfo"); - if (isPaused) { - // State paused - presenceData.smallImageKey = ad - ? ["fr-FR"].includes(lang) - ? LargeAssets.AdFr - : LargeAssets.AdEn - : Assets.Pause; - presenceData.smallImageText = ad ? strings.watchingAd : strings.pause; - delete presenceData.startTimestamp; - delete presenceData.endTimestamp; - } else { - // State playing - presenceData.smallImageKey = ad - ? ["fr-FR"].includes(lang) - ? LargeAssets.AdFr - : LargeAssets.AdEn - : Assets.Play; - presenceData.smallImageText = ad ? strings.watchingAd : strings.play; + document + .querySelector('meta[property="og:image"') + .getAttribute("content"), + cropPreset.horizontal + ); } if (useButtons) { @@ -454,8 +567,8 @@ presence.on("UpdateData", async () => { /* MEDIA PAGE (Page de media) (https://www.rtlplay.be/rtlplay/salvation~2ab30366-51fe-4b29-a720-5e41c9bd6991) */ - case pathname.split("/")[2].length > 15: { - + case pathParts[2].length > 15: { + presenceData.startTimestamp = browsingTimestamp; if (usePrivacyMode) { presenceData.details = strings.browsing; @@ -464,40 +577,41 @@ presence.on("UpdateData", async () => { presenceData.smallImageText = strings.privacy; } else { const summaryElement = document.querySelector("p.detail__description"), - yearElement = document.querySelector('dd.detail__meta-label[title="Année de production"]'), - durationElement = document.querySelector('dd.detail__meta-label[title="Durée"]'), - seasonElement = document.querySelector("dd.detail__meta-label:not([title])"), - genresArray = document.querySelectorAll("dl:nth-child(1) > dd > a"), - isMovie = document.querySelector('meta[property="og:type"]').getAttribute("content").includes("movie"); + yearElement = document.querySelector( + 'dd.detail__meta-label[title="Année de production"]' + ), + durationElement = document.querySelector( + 'dd.detail__meta-label[title="Durée"]' + ), + seasonElement = document.querySelector( + "dd.detail__meta-label:not([title])" + ), + genresArray = document.querySelectorAll("dl:nth-child(1) > dd > a"), + isMovie = document + .querySelector('meta[property="og:type"]') + .getAttribute("content") + .includes("movie"); let subtitle = isMovie ? strings.movie : strings.tvshow; subtitle += yearElement ? ` - ${yearElement.textContent}` : ""; // Add Release Year - subtitle += seasonElement && !isMovie ? ` - ${seasonElement.textContent}` : ""; // Add amount of seasons + subtitle += + seasonElement && !isMovie ? ` - ${seasonElement.textContent}` : ""; // Add amount of seasons subtitle += durationElement ? ` - ${durationElement.textContent}` : ""; // Add Duration - for ( - const element of genresArray // Add Genres - ) { - subtitle += ` - ${ - element.textContent - }`; - } + for (const element of genresArray) // Add Genres + subtitle += ` - ${element.textContent}`; - presenceData.details = document.querySelector("h1.detail__title").textContent; // Title - presenceData.state = subtitle; + presenceData.details = + document.querySelector("h1.detail__title").textContent; // MediaName + presenceData.state = subtitle; // MediaType - 2024 - 4 seasons or 50 min - Action - Drame - presenceData.largeImageText = summaryElement ? limitText(summaryElement.textContent) : subtitle; // Summary if available + presenceData.largeImageText = summaryElement + ? limitText(summaryElement.textContent) // 128 characters is the limit + : subtitle; // Summary if available presenceData.smallImageKey = LargeAssets.Binoculars; presenceData.smallImageText = strings.browsing; - if (usePoster) { - presenceData.largeImageKey = LargeAssets.Logo; // Temp placeholder - presenceData.largeImageKey = await getThumbnail( - document.querySelector("img.detail__poster")?.getAttribute("src") - ); - } - if (useButtons) { presenceData.buttons = [ { @@ -506,15 +620,37 @@ presence.on("UpdateData", async () => { }, ]; } + + if (usePoster) { + const presenceDataSlide = structuredClone(presenceData); // Deep copy + + presenceData.largeImageKey = await getThumbnail( + document.querySelector("img.detail__poster")?.getAttribute("src"), + cropPreset.vertical + ); + presenceDataSlide.largeImageKey = await getThumbnail( + document.querySelector("img.detail__img")?.getAttribute("src"), + cropPreset.horizontal + ); + + slideshow.addSlide("poster-image", presenceData, 5000); + slideshow.addSlide("background-image", presenceDataSlide, 5000); + } } break; } default: { - presenceData.details = strings.viewAPage; + presenceData.details = strings.browsing; + presenceData.state = strings.viewAPage; + + presenceData.smallImageKey = LargeAssets.Binoculars; + presenceData.smallImageText = strings.browsing; + + if (useTimestamps) presenceData.startTimestamp = browsingTimestamp; break; } } - - if (presenceData.details) presence.setActivity(presenceData); - else presence.setActivity(); + if (slideshow.getSlides().length > 0) presence.setActivity(slideshow); + else if (presenceData.details) presence.setActivity(presenceData); + else presence.clearActivity(); }); diff --git a/websites/R/RTLplay/util.ts b/websites/R/RTLplay/util.ts index 309b6e82878c..62905dff9a86 100644 --- a/websites/R/RTLplay/util.ts +++ b/websites/R/RTLplay/util.ts @@ -1,30 +1,21 @@ -export function exist(selector: string) { - return document.querySelector(selector) !== null; -} - export const stringsMap = { play: "general.playing", pause: "general.paused", search: "general.search", + searchFor: "general.searchFor", searchSomething: "general.searchSomething", browsing: "general.browsing", - viewing: "general.viewing", - viewPage: "general.viewPage", viewAPage: "general.viewAPage", viewHome: "general.viewHome", - viewAccount: "general.viewAccount", - viewChannel: "general.viewChannel", viewCategory: "general.viewCategory", viewList: "netflix.viewList", buttonViewPage: "general.buttonViewPage", - watching: "general.watching", watchingAd: "youtube.ad", watchingLive: "general.watchingLive", watchingShow: "general.watchingShow", watchingMovie: "general.watchingMovie", listeningMusic: "general.listeningMusic", buttonWatchStream: "general.buttonWatchStream", - buttonWatchVideo: "general.buttonWatchVideo", buttonWatchEpisode: "general.buttonViewEpisode", buttonWatchMovie: "general.buttonWatchMovie", buttonListenAlong: "general.buttonListenAlong", @@ -36,68 +27,62 @@ export const stringsMap = { movie: "general.movie", tvshow: "general.tvshow", privacy: "general.privacy", + watchingLiveMusic: "general.LiveMusic" }; -export function getAdditionnalStrings(lang: string, strings: typeof stringsMap) { +export function getAdditionnalStrings( + lang: string, + strings: typeof stringsMap +) { switch (lang) { case "fr-FR": { strings.deferred = "En Différé"; strings.movie = "Film"; strings.tvshow = "Série"; strings.privacy = "Lecture privée"; + strings.watchingLiveMusic = "Regarde un clip musical en direct"; // Improved translation in the context of this website strings.watchingShow = "Regarde une émission ou une série"; + strings.searchFor = "Recherche de :"; + strings.viewList = "Regarde sa liste"; + break; } case "nl-NL": { strings.deferred = "Uitgestelde"; strings.movie = "Film"; - strings.tvshow = "Série"; - strings.privacy = ""; + strings.tvshow = "TV-Serie"; + strings.privacy = "Privacy"; + strings.watchingLiveMusic = "Kijkt naar een live muziekvideo"; break; } case "de-DE": { strings.deferred = "Zeitversetzt"; strings.movie = "Film"; - strings.tvshow = "Fernsehserie"; - strings.privacy = ""; + strings.tvshow = "TV-Serie"; + strings.privacy = "Private Mode"; + strings.watchingLiveMusic = "Schaut ein Musikvideo live"; break; } default: { strings.deferred = "Deferred"; strings.movie = "Movie"; strings.tvshow = "TV Serie"; - strings.privacy = "Private mode"; + strings.privacy = "Privacy Mode"; + strings.watchingLiveMusic = "Watching a live music video"; } } return strings; } -// Mainly used to truncate largeImageKeyText because the limit is 128 characters -export function limitText(input: string) { - const maxLength = 128, - ellipsis = " ..."; - - // If input is within limit, return it as is - if (input.length <= maxLength) return input; - - // Truncate to 125 characters (leaving room for ellipsis) - let truncated = input.slice(0, maxLength - ellipsis.length); - - // If the truncated text ends mid-word, remove the partial word - if (truncated.lastIndexOf(" ") !== -1) - truncated = truncated.slice(0, truncated.lastIndexOf(" ")); - - return truncated + ellipsis; -} - export const enum LargeAssets { Logo = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/logo.png", Animated = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/0.gif", Deferred = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/1.gif", LiveAnimated = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/2.gif", - Listening = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/3.png", + Vinyle = "https://i.imgur.com/6qvsVLa.png", + VinyleAnimated = "https://i.imgur.com/8nd4UdO.gif", Binoculars = "https://imgur.com/aF3TWVK.png", Privacy = "https://imgur.com/nokHvhE.png", AdEn = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/4.png", @@ -106,10 +91,53 @@ export const enum LargeAssets { RTLTVi = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/7.png", RTLClub = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/8.png", RTLPlug = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/9.png", + RTLDistrict = "https://i.imgur.com/gZdD4LA.png", BelRTL = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/10.png", Contact = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/11.png", } +export function getLocalizedAssets(lang: string, assetName: string) { + switch (assetName) { + case "Ad": + switch (lang) { + case "fr-FR": + return LargeAssets.AdFr; + default: + return LargeAssets.AdEn; + } + default: + return LargeAssets.Binoculars; // Default fallback + } +} + +// Mainly used to truncate largeImageKeyText because the limit is 128 characters +export function limitText(input: string, maxLength = 128) { + const ellipsis = " ..."; + + // If input is within limit, return it as is + if (input.length <= maxLength) return input; + + // Truncate to 125 characters (leaving room for ellipsis) + let truncated = input.slice(0, maxLength - ellipsis.length); + + // If the truncated text ends mid-word, remove the partial word + if (truncated.lastIndexOf(" ") !== -1) + truncated = truncated.slice(0, truncated.lastIndexOf(" ")); + + return truncated + ellipsis; +} + +export function exist(selector: string) { + return document.querySelector(selector) !== null; +} + +// Copy of the function in Youtube utils +let cachedTime = 0; +export function adjustTimeError(time: number, acceptableError: number): number { + if (Math.abs(time - cachedTime) > acceptableError) cachedTime = time; + return cachedTime; +} + export function getChannel(channel: string) { switch (true) { case channel.includes("tvi"): { @@ -133,18 +161,29 @@ export function getChannel(channel: string) { logo: LargeAssets.RTLPlug, }; } - case channel.includes("bel"): { + case ["rtlplay", "district"].includes(channel): { + return { + channel: "RTL district", + type: ActivityType.Watching, + logo: LargeAssets.RTLDistrict, + }; + } + case ["bel", "www.belrtl.be"].includes(channel): { return { channel: "Bel RTL", type: ActivityType.Listening, logo: LargeAssets.BelRTL, + radioplayerAPI: + "https://core-search.radioplayer.cloud/056/qp/v4/events/?rpId=6", }; } - case channel.includes("contact"): { + case ["contact", "www.radiocontact.be"].includes(channel): { return { channel: "Radio Contact", type: ActivityType.Listening, logo: LargeAssets.Contact, + radioplayerAPI: + "https://core-search.radioplayer.cloud/056/qp/v4/events/?rpId=1", }; } default: { @@ -157,14 +196,25 @@ export function getChannel(channel: string) { } } -// Adapted veryCrunchy's function from YouTube Presence https://github.com/PreMiD/Presences/pull/8000 -export async function getThumbnail(src: string): Promise { +// Greatly adapted veryCrunchy's function from YouTube Presence https://github.com/PreMiD/Presences/pull/8000 + +export const cropPreset = { + // Crop values in percent correspond to Left, Right, Top, Bottom. + squared: [0, 0, 0, 0], + vertical: [0.22, 0.22, 0, 0.3], + horizontal: [0.425, 0.025, 0, 0], +}; + +export async function getThumbnail( + src: string = LargeAssets.Logo, + cropPercentages: typeof cropPreset.squared = cropPreset.squared, + progress = 2, + borderWidth = 15 +): Promise { return new Promise(resolve => { const img = new Image(), - wh = 320, - borderThickness = 15, // Thickness of the gradient border - cropLeftRightPercentage = 0.14, // Percentage to crop from left and right for landscape mode (e.g., 0.1 for 10%) - cropTopBottomPercentage = 0.025; // Percentage to crop from top and bottom for portrait mode (e.g., 0.1 for 10%) + wh = 320; // Size of the square thumbnail + img.crossOrigin = "anonymous"; img.src = src; @@ -174,76 +224,75 @@ export async function getThumbnail(src: string): Promise { cropX = 0, cropY = 0; - if (img.width > img.height) { - // Landscape mode: crop left and right - const cropLeftRight = img.width * cropLeftRightPercentage; - croppedWidth = img.width - 2 * cropLeftRight; + // Determine if the image is landscape or portrait + const isLandscape = img.width > img.height; + + if (isLandscape) { + // Landscape mode: use left and right crop percentages + const cropLeft = img.width * cropPercentages[0]; + croppedWidth = img.width - cropLeft - img.width * cropPercentages[1]; croppedHeight = img.height; - cropX = cropLeftRight; + cropX = cropLeft; } else { - // Portrait mode: crop top and bottom - const cropTopBottom = img.height * cropTopBottomPercentage; + // Portrait mode: use top and bottom crop percentages + const cropTop = img.height * cropPercentages[2]; croppedWidth = img.width; - croppedHeight = img.height - 2 * cropTopBottom; - cropY = cropTopBottom; + croppedHeight = img.height - cropTop - img.height * cropPercentages[3]; + cropY = cropTop; } - const isLandscape = croppedWidth >= croppedHeight; + // Determine the scale to fit the cropped image into the square canvas let newWidth, newHeight, offsetX, offsetY; if (isLandscape) { - newWidth = wh; - newHeight = (wh / croppedWidth) * croppedHeight; - offsetX = 0; + newWidth = wh - 2 * borderWidth; + newHeight = (newWidth / croppedWidth) * croppedHeight; + offsetX = borderWidth; offsetY = (wh - newHeight) / 2; } else { - newHeight = wh; - newWidth = (wh / croppedHeight) * croppedWidth; + newHeight = wh - 2 * borderWidth; + newWidth = (newHeight / croppedHeight) * croppedWidth; offsetX = (wh - newWidth) / 2; - offsetY = 0; + offsetY = borderWidth; } const tempCanvas = document.createElement("canvas"); tempCanvas.width = wh; tempCanvas.height = wh; - const ctx = tempCanvas.getContext("2d"); + const ctx = tempCanvas.getContext("2d"), + // Remap progress from 0-1 to 0.03-0.97 (smallImageKey borders) + remappedProgress = 0.07 + progress * (0.93 - 0.07); - // Fill the canvas with the background color + // 1. Fill the canvas with a black background ctx.fillStyle = "#172e4e"; ctx.fillRect(0, 0, wh, wh); - // Create the gradient - const gradient = ctx.createLinearGradient(0, 0, wh, 0); - gradient.addColorStop(0, "rgba(245,3,26,1)"); - gradient.addColorStop(0.5, "rgba(63,187,244,1)"); - gradient.addColorStop(1, "rgba(164,215,12,1)"); + // 2. Draw the radial progress bar + if (remappedProgress > 0) { + ctx.beginPath(); + ctx.moveTo(wh / 2, wh / 2); + const startAngle = Math.PI / 4; // 45 degrees in radians, starting from bottom-right - // Draw the gradient borders - if (isLandscape) { - // Top border - ctx.fillStyle = gradient; - ctx.fillRect(0, offsetY - borderThickness, wh, borderThickness); + ctx.arc( + wh / 2, + wh / 2, + wh, + startAngle, + startAngle + 2 * Math.PI * remappedProgress + ); + ctx.lineTo(wh / 2, wh / 2); - // Bottom border + // Create a triangular gradient + const gradient = ctx.createLinearGradient(0, 0, wh, wh); + gradient.addColorStop(0, "rgba(245, 3, 26, 1)"); + gradient.addColorStop(0.5, "rgba(63, 187, 244, 1)"); + gradient.addColorStop(1, "rgba(164, 215, 12, 1)"); ctx.fillStyle = gradient; - ctx.fillRect(0, offsetY + newHeight, wh, borderThickness); - } else { - // Create a vertical gradient for portrait mode - const verticalGradient = ctx.createLinearGradient(0, 0, 0, wh); - verticalGradient.addColorStop(0, "rgba(245,3,26,1)"); - verticalGradient.addColorStop(0.5, "rgba(63,187,244,1)"); - verticalGradient.addColorStop(1, "rgba(164,215,12,1)"); - // Left border - ctx.fillStyle = verticalGradient; - ctx.fillRect(offsetX - borderThickness, 0, borderThickness, wh); - - // Right border - ctx.fillStyle = verticalGradient; - ctx.fillRect(offsetX + newWidth, 0, borderThickness, wh); + ctx.fill(); } - // Draw the cropped image + // 3. Draw the cropped image centered and zoomed out based on the borderWidth ctx.drawImage( img, cropX, @@ -263,4 +312,4 @@ export async function getThumbnail(src: string): Promise { resolve(src); }; }); -} \ No newline at end of file +} From 666a1388920a32904f0c1044db022113c2f82a09 Mon Sep 17 00:00:00 2001 From: pierrequiroul <35348696+pierrequiroul@users.noreply.github.com> Date: Mon, 25 Nov 2024 01:33:04 +0100 Subject: [PATCH 3/3] fix(RTLplay): added return type + run prettier --- websites/R/RTLplay/presence.ts | 42 +++++++++++++++++++--------------- websites/R/RTLplay/util.ts | 24 +++++++++++++------ 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/websites/R/RTLplay/presence.ts b/websites/R/RTLplay/presence.ts index 8ff4d0fa5474..d8a0fe176387 100644 --- a/websites/R/RTLplay/presence.ts +++ b/websites/R/RTLplay/presence.ts @@ -161,7 +161,7 @@ presence.on("UpdateData", async () => { case ["collection", "series", "films", "divertissement"].includes( pathParts[2] ): { - if (usePrivacyMode) { + if (usePrivacyMode) { presenceData.state = strings.viewAPage; presenceData.smallImageKey = LargeAssets.Privacy; @@ -258,7 +258,9 @@ presence.on("UpdateData", async () => { if (media.results.now.type === "PE_E") { // When a song is played - presenceData.largeImageKey = await getThumbnail(media.results.now.songArtURL); + presenceData.largeImageKey = await getThumbnail( + media.results.now.songArtURL + ); presenceData.state = `${media.results.now.name} - ${media.results.now.artistName}`; } else { // When we don't have a song, we simply show the radio name as the show name is already displayed in state @@ -295,22 +297,26 @@ presence.on("UpdateData", async () => { ); } else { // Fallback method: Uses program start and end times in tv guide overlay - presenceData.startTimestamp = Math.floor(new Date( - document - .querySelector( - 'li.live-broadcast__channel[aria-current="true"] time[js-element="startTime"]' - ) - .getAttribute("datetime") - .replace(/[+-]\d{2}:\d{2}\[.*\]/, "") // Removing UTC offset and the time zone - ).getTime() / 1000); - presenceData.endTimestamp = Math.floor(new Date( - document - .querySelector( - 'li.live-broadcast__channel[aria-current="true"] time[js-element="endTime"]' - ) - .getAttribute("datetime") - .replace(/[+-]\d{2}:\d{2}\[.*\]/, "") // Removing UTC offset and the time zone - ).getTime() / 1000); + presenceData.startTimestamp = Math.floor( + new Date( + document + .querySelector( + 'li.live-broadcast__channel[aria-current="true"] time[js-element="startTime"]' + ) + .getAttribute("datetime") + .replace(/[+-]\d{2}:\d{2}\[.*\]/, "") // Removing UTC offset and the time zone + ).getTime() / 1000 + ); + presenceData.endTimestamp = Math.floor( + new Date( + document + .querySelector( + 'li.live-broadcast__channel[aria-current="true"] time[js-element="endTime"]' + ) + .getAttribute("datetime") + .replace(/[+-]\d{2}:\d{2}\[.*\]/, "") // Removing UTC offset and the time zone + ).getTime() / 1000 + ); } } else if (exist("span.playerui__controls__stat__time")) { presenceData.largeImageText += ` - ${Math.round( diff --git a/websites/R/RTLplay/util.ts b/websites/R/RTLplay/util.ts index 62905dff9a86..f3acd122f452 100644 --- a/websites/R/RTLplay/util.ts +++ b/websites/R/RTLplay/util.ts @@ -27,13 +27,13 @@ export const stringsMap = { movie: "general.movie", tvshow: "general.tvshow", privacy: "general.privacy", - watchingLiveMusic: "general.LiveMusic" + watchingLiveMusic: "general.LiveMusic", }; export function getAdditionnalStrings( lang: string, strings: typeof stringsMap -) { +): typeof stringsMap { switch (lang) { case "fr-FR": { strings.deferred = "En Différé"; @@ -46,7 +46,7 @@ export function getAdditionnalStrings( strings.watchingShow = "Regarde une émission ou une série"; strings.searchFor = "Recherche de :"; strings.viewList = "Regarde sa liste"; - + break; } case "nl-NL": { @@ -96,7 +96,10 @@ export const enum LargeAssets { Contact = "https://cdn.rcd.gg/PreMiD/websites/R/RTLplay/assets/11.png", } -export function getLocalizedAssets(lang: string, assetName: string) { +export function getLocalizedAssets( + lang: string, + assetName: string +): LargeAssets { switch (assetName) { case "Ad": switch (lang) { @@ -111,7 +114,7 @@ export function getLocalizedAssets(lang: string, assetName: string) { } // Mainly used to truncate largeImageKeyText because the limit is 128 characters -export function limitText(input: string, maxLength = 128) { +export function limitText(input: string, maxLength = 128): string { const ellipsis = " ..."; // If input is within limit, return it as is @@ -127,7 +130,7 @@ export function limitText(input: string, maxLength = 128) { return truncated + ellipsis; } -export function exist(selector: string) { +export function exist(selector: string): boolean { return document.querySelector(selector) !== null; } @@ -138,7 +141,14 @@ export function adjustTimeError(time: number, acceptableError: number): number { return cachedTime; } -export function getChannel(channel: string) { +interface ChannelInfo { + channel: string; + type: ActivityType; + logo: LargeAssets; + radioplayerAPI?: string; // Optional property +} + +export function getChannel(channel: string): ChannelInfo { switch (true) { case channel.includes("tvi"): { return {