From c81b76666a6a512777235f8b44d8c8870d204339 Mon Sep 17 00:00:00 2001 From: Cynthia Foxwell Date: Thu, 6 Feb 2025 15:42:43 -0700 Subject: [PATCH] betterEmbedsYT: descriptions --- src/betterEmbedsYT/index.ts | 20 ++++- src/betterEmbedsYT/manifest.json | 26 +++++- src/betterEmbedsYT/style.css | 14 ++++ .../webpackModules/description.tsx | 84 +++++++++++++++++++ 4 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 src/betterEmbedsYT/style.css create mode 100644 src/betterEmbedsYT/webpackModules/description.tsx diff --git a/src/betterEmbedsYT/index.ts b/src/betterEmbedsYT/index.ts index e73df94..1aab075 100644 --- a/src/betterEmbedsYT/index.ts +++ b/src/betterEmbedsYT/index.ts @@ -1,3 +1,19 @@ -import { Patch } from "@moonlight-mod/types"; +import { Patch, ExtensionWebpackModule } from "@moonlight-mod/types"; -export const patches: Patch[] = []; +export const patches: Patch[] = [ + { + find: ".VIDEO_EMBED_PLAYBACK_STARTED,", + replace: { + match: /(case \i\.\i\.VIDEO:)(case \i\.\i\.GIFV:)break;(?=default:(\i)=)/, + replacement: (_, VIDEO, GIFV, description) => + `${GIFV}break;${VIDEO}if(this.props.embed.provider?.name==="YouTube"){${description}=require("betterEmbedsYT_description").default(this.props);}break;` + }, + prerequisite: () => moonlight.getConfigOption("betterEmbedsYT", "description") ?? true + } +]; + +export const webpackModules: Record = { + description: { + dependencies: [{ id: "react" }, { id: "discord/components/common/index" }, { ext: "spacepack", id: "spacepack" }] + } +}; diff --git a/src/betterEmbedsYT/manifest.json b/src/betterEmbedsYT/manifest.json index 8788fd3..3f19371 100644 --- a/src/betterEmbedsYT/manifest.json +++ b/src/betterEmbedsYT/manifest.json @@ -1,16 +1,34 @@ { "$schema": "https://moonlight-mod.github.io/manifest.schema.json", "id": "betterEmbedsYT", - "version": "1.0.0", + "version": "1.1.0", "meta": { "name": "Better YouTube Embeds", - "tagline": "Bypass copyright blocks, block ads and trackers. Works with Watch Together.", + "tagline": "Bypass copyright blocks, descriptions, block ads and trackers (works with Watch Together).", "description": "Ad blocking uses code from the [Iridium](https://github.com/ParticleCore/Iridium) web extension.", "authors": ["Cynosphere"], "tags": ["qol", "privacy"], - "source": "https://github.com/Cynosphere/moonlight-extensions" + "source": "https://github.com/Cynosphere/moonlight-extensions", + "changelog": "Added option for showing descriptions" + }, + "settings": { + "description": { + "displayName": "Show video descriptions", + "type": "boolean", + "default": true + }, + "fullDescription": { + "displayName": "Fetch full descriptions", + "description": "Fetches the full description through YouTube's API using one of Google's own API keys", + "type": "boolean", + "default": true + }, + "expandDescription": { + "displayName": "Expand description by default", + "type": "boolean", + "default": false + } }, - "settings": {}, "apiLevel": 2, "blocked": [ "https://play.google.com/log*", diff --git a/src/betterEmbedsYT/style.css b/src/betterEmbedsYT/style.css new file mode 100644 index 0000000..acddd3a --- /dev/null +++ b/src/betterEmbedsYT/style.css @@ -0,0 +1,14 @@ +.betterEmbedsYT-description-button { + text-align: center; + user-select: none; + cursor: pointer; + padding-top: 0.25rem; +} + +.betterEmbedsYT-description-firstLine { + height: 1.125rem; + max-height: 1.125rem; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} diff --git a/src/betterEmbedsYT/webpackModules/description.tsx b/src/betterEmbedsYT/webpackModules/description.tsx new file mode 100644 index 0000000..1316f76 --- /dev/null +++ b/src/betterEmbedsYT/webpackModules/description.tsx @@ -0,0 +1,84 @@ +import React from "@moonlight-mod/wp/react"; +import spacepack from "@moonlight-mod/wp/spacepack_spacepack"; +import { Clickable } from "@moonlight-mod/wp/discord/components/common/index"; + +const EmbedClasses = spacepack.findByCode("embedDescription:")[0].exports; + +type RenderDescription = (embed: any, description: string, headings: boolean) => React.ReactNode; + +const logger = moonlight.getLogger("Better YouTube Embeds - Description"); +const descriptionCache = new Map(); + +const API_KEY = "AIzaSyCpphGplamUhCCEIcum1VyDXBt0i1nOqac"; // one of Google's own +const FAKE_EMBED = { type: "rich" }; + +function YTDescription({ + description, + renderDescription, + videoId +}: { + description: string; + renderDescription: RenderDescription; + videoId: string; +}) { + const [expanded, setExpanded] = React.useState( + moonlight.getConfigOption("betterEmbedsYT", "expandDescription") + ); + const [fullDescription, setFullDescription] = React.useState( + descriptionCache.has(videoId) ? descriptionCache.get(videoId) : description + ); + + React.useEffect(() => { + if (!descriptionCache.has(videoId)) + if ( + (moonlight.getConfigOption("betterEmbedsYT", "fullDescription") ?? true) && + description.endsWith("...") && + description.length >= 300 + ) { + (async () => { + try { + const data = await fetch( + `https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${videoId}&key=${API_KEY}` + ).then((res) => res.json()); + const newDesc = data?.items?.[0]?.snippet?.description ?? description; + descriptionCache.set(videoId, newDesc); + setFullDescription(newDesc); + } catch (err) { + logger.error(`Failed to get full description for "${videoId}":`, err); + descriptionCache.set(videoId, description); + } + })(); + } else { + descriptionCache.set(videoId, description); + } + }); + + const lines = fullDescription!.split("\n"); + + const rendered = renderDescription(FAKE_EMBED, fullDescription!, false); + const firstLine = renderDescription(FAKE_EMBED, lines[0], false); + + return lines.length === 1 && description.length <= 40 ? ( +
{rendered}
+ ) : ( +
+ {expanded ? rendered :
{firstLine}
} + setExpanded(!expanded)}> + {expanded ? "Show less" : "Show more"} + +
+ ); +} + +export default function DescriptionWrapper({ + embed, + renderDescription +}: { + embed: { rawDescription?: string; video: { url: string } }; + renderDescription: RenderDescription; +}) { + const videoId = embed.video.url.split("/embed/").pop()!; + return embed.rawDescription == null ? null : ( + + ); +}