From 66c55cbe4424fa4e041f8f2619c3465a43f75c8c Mon Sep 17 00:00:00 2001 From: Toshi <38578537+Toshi7878@users.noreply.github.com> Date: Fri, 28 Feb 2025 03:31:25 +0900 Subject: [PATCH] feat(YTyping): add YTyping presence (#9323) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add files via upload * fix lint error * fix getTimestamps * add presence state * fix import * fix lint error * fix lint error * fix lint error * remove promise.all * fix singlequote * refactor rename function getPresenceData → generatePresenceData * add buttons * refactor simplify presence state handling * fix single quote * fix lint error * fix lint error * fix lint error * add iFrameRegExp * add button * refactor iframe.ts * remove privacy setting * remove main page button * add youtube button * remove const * remove document.location.hostname if --- websites/Y/YTyping/iframe.ts | 13 +++ websites/Y/YTyping/metadata.json | 58 ++++++++++ websites/Y/YTyping/presence.ts | 185 +++++++++++++++++++++++++++++++ 3 files changed, 256 insertions(+) create mode 100644 websites/Y/YTyping/iframe.ts create mode 100644 websites/Y/YTyping/metadata.json create mode 100644 websites/Y/YTyping/presence.ts diff --git a/websites/Y/YTyping/iframe.ts b/websites/Y/YTyping/iframe.ts new file mode 100644 index 000000000000..48a7a813b654 --- /dev/null +++ b/websites/Y/YTyping/iframe.ts @@ -0,0 +1,13 @@ +const iframe = new iFrame() + +iframe.on('UpdateData', async () => { + const { currentTime, duration, paused } = document.querySelector( + 'video.video-stream.html5-main-video', + )! + + iframe.send({ + currentTime, + duration, + paused, + }) +}) diff --git a/websites/Y/YTyping/metadata.json b/websites/Y/YTyping/metadata.json new file mode 100644 index 000000000000..dedb84ef2ed7 --- /dev/null +++ b/websites/Y/YTyping/metadata.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://schemas.premid.app/metadata/1.7", + "author": { + "name": "Toshi", + "id": "548698164874575875" + }, + "service": "YTyping", + "description": { + "en": "A website where you can practice typing with YouTube music." + }, + "url": "ytyping.net", + "version": "1.0.0", + "logo": "https://i.imgur.com/nnZqkNo.png", + "thumbnail": "https://i.imgur.com/ULpnvtt.png", + "color": "#3b5a7d", + "category": "games", + "tags": ["game", "music", "typing"], + "iframe": true, + "iFrameRegExp": "www[.]youtube[.]com[/]", + "settings": [ + { + "id": "showCurrentSong", + "title": "Show Current Song", + "icon": "fas fa-music", + "value": true + }, + { + "id": "showTypingState", + "title": "Show Typing State", + "icon": "fas fa-keyboard", + "value": true + }, + { + "id": "showMediaState", + "title": "Show Media State", + "icon": "fas fa-play-circle", + "value": true + }, + { + "id": "showTimestamp", + "title": "Show Timestamp", + "icon": "fas fa-clock", + "value": true + }, + { + "id": "showCover", + "title": "Show Cover", + "icon": "fad fa-images", + "value": true + }, + { + "id": "showButtons", + "title": "Show Buttons", + "icon": "fas fa-compress-arrows-alt", + "value": true + } + ] +} diff --git a/websites/Y/YTyping/presence.ts b/websites/Y/YTyping/presence.ts new file mode 100644 index 000000000000..32daa7d2215f --- /dev/null +++ b/websites/Y/YTyping/presence.ts @@ -0,0 +1,185 @@ +import { ActivityType, Assets, getTimestamps } from 'premid' + +const presence = new Presence({ + clientId: '1338529406905352212', +}) + +presence.on('UpdateData', async () => { + const presenceData = await generatePresenceData() + + presence.setActivity(presenceData) +}) + +let _video = { + duration: 0, + currentTime: 0, + paused: true, +} +presence.on( + 'iFrameData', + (data: unknown) => { + _video = data as typeof _video + }, +) + +function getTypePageInfo() { + const videoId = document + ?.querySelector('[name="article:youtube_id"]') + ?.content ?? '' + const title = document + ?.querySelector('[name="article:title"]') + ?.content ?? '' + const artist = document + ?.querySelector('[name="article:artist"]') + ?.content ?? '' + const image = document + ?.querySelector('[property="og:image"]') + ?.content ?? '' + + return { title, artist, image, videoId } +} + +const largeImageKey = 'https://i.imgur.com/nnZqkNo.png' +const browsingTimestamp = Math.floor(Date.now() / 1000) + +async function generatePresenceData() { + const { pathname, href } = document.location + const [ + showCurrentSong, + showTypingState, + showMediaState, + showTimestamp, + showCover, + showButtons, + ] = await Promise.all([ + presence.getSetting('showCurrentSong'), + presence.getSetting('showTypingState'), + presence.getSetting('showMediaState'), + presence.getSetting('showTimestamp'), + presence.getSetting('showCover'), + presence.getSetting('showButtons'), + ]) + + switch (true) { + case pathname.includes('/type'):{ + const map = getTypePageInfo() + const isPaused = _video.paused + + const presenceData: PresenceData = { + type: isPaused ? ActivityType.Playing : ActivityType.Listening, + largeImageKey: showCover ? map.image : largeImageKey, + } + + if (showMediaState) { + presenceData.smallImageKey = isPaused ? Assets.Pause : Assets.Play + presenceData.smallImageText = isPaused ? '停止中' : 'プレイ中' + } + + if (showTimestamp) { + const [startTimestamp, endTimestamp] = getTimestamps( + Math.floor(_video.currentTime), + Math.floor(_video.duration), + ) + presenceData.startTimestamp = startTimestamp + presenceData.endTimestamp = endTimestamp + } + + if (showButtons) { + presenceData.buttons = [{ + url: href, + label: 'YTypingを開く', + }, { + url: `https://youtu.be/${map.videoId}`, + label: 'YouTubeで聴く', + }] + } + + if (showCurrentSong) { + presenceData.details = `${map.title} - ${map.artist}` + } + + if (showTypingState) { + const playingNotifyText = document.querySelector('#playing_notify')?.textContent + const typeCount = document.querySelector('[label=\'type\']')?.textContent ?? 0 + const kpm = document.querySelector('[label=\'kpm\']')?.textContent ?? 0 + + if (playingNotifyText?.includes('Replay')) { + presenceData.state = 'リプレイ視聴中' + } + else { + presenceData.state = `打鍵数:${typeCount} kpm:${kpm}` + } + } + + return presenceData + } + + case pathname.includes('/edit'):{ + const iframeElement = document.querySelector('iframe') + + const videoId = iframeElement?.src.split('/embed/')[1]?.split('?')[0] || '' + const videoThubnail = `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg` + const isPaused = _video.paused + + const presenceData: PresenceData = { + largeImageKey: showCover && videoId ? videoThubnail : largeImageKey, + type: isPaused ? ActivityType.Playing : ActivityType.Listening, + } + + if (showTypingState) { + presenceData.state = '譜面編集中' + } + + if (showMediaState) { + presenceData.smallImageKey = isPaused ? Assets.Pause : Assets.Play + presenceData.smallImageText = isPaused ? '停止中' : 'プレイ中' + } + + if (showTimestamp) { + const [startTimestamp, endTimestamp] = getTimestamps( + Math.floor(_video.currentTime), + Math.floor(_video.duration), + ) + presenceData.startTimestamp = startTimestamp + presenceData.endTimestamp = endTimestamp + } + + if (showCurrentSong) { + presenceData.details = iframeElement?.title ?? '' + } + + if (showButtons) { + presenceData.buttons = [{ + url: `https://youtu.be/${videoId}`, + label: 'YouTubeで聴く', + }] + } + + return presenceData + } + + default: { + const presenceData: PresenceData = { + startTimestamp: browsingTimestamp, + largeImageKey, + type: ActivityType.Playing, + } + + switch (true) { + case pathname.includes('/timeline'): + presenceData.state = 'タイムライン' + break + + case pathname.includes('/user'): + presenceData.state = 'ユーザーページ' + break + + default: + presenceData.state = '待機中' + break + } + + return presenceData + } + } +}