diff --git a/websites/F/Feishin/metadata.json b/websites/F/Feishin/metadata.json new file mode 100644 index 000000000000..058808178f75 --- /dev/null +++ b/websites/F/Feishin/metadata.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://schemas.premid.app/metadata/1.16", + "apiVersion": 1, + "author": { + "id": "287739388841033729", + "name": "Atlantis" + }, + "service": "Feishin", + "description": { + "en": "Feishin is a Jellyfin-compatible modern self-hosted music player." + }, + "url": "feishin.vercel.app", + "regExp": "^https?[:][/][/]([a-z0-9-]+[.])*feishin.vercel[.]app[/]", + "version": "1.0.0", + "logo": "https://raw.githubusercontent.com/jeffvli/feishin/refs/heads/development/assets/icons/icon.png", + "thumbnail": "https://raw.githubusercontent.com/jeffvli/feishin/development/media/preview_album_artist_detail.png", + "color": "#000000", + "category": "music", + "tags": [ + "songs", + "music", + "jellyfin" + ] +} diff --git a/websites/F/Feishin/presence.ts b/websites/F/Feishin/presence.ts new file mode 100644 index 000000000000..f546a104e0bf --- /dev/null +++ b/websites/F/Feishin/presence.ts @@ -0,0 +1,115 @@ +import { ActivityType, Assets, getTimestamps, timestampFromFormat } from 'premid' + +const presence = new Presence({ + clientId: '1457416186940620820', +}) + +enum ActivityAssets { + Logo = 'https://raw.githubusercontent.com/jeffvli/feishin/refs/heads/development/assets/icons/icon.png', +} + +async function getStrings() { + return presence.getStrings( + { + pause: 'general.paused', + play: 'general.playing', + }, + ) +} +function getElement(query: string): string | undefined { + let text: string | undefined = '' + + const element = document.querySelector(query) + if (element) { + if (element.childNodes.length > 1) + text = element.childNodes[0]?.textContent ?? undefined + else text = element.textContent ?? undefined + } + return text?.trimStart().trimEnd() +} + +let strings: Awaited> + +const uploadedFiles: Record = {} +async function uploadFile( + url: string, + defaultImage: string, + presence: Presence, +): Promise { + if (uploadedFiles[url]) + return uploadedFiles[url] + uploadedFiles[url] = defaultImage + + try { + const imageData = await fetch(url).then(res => res.blob()) + const formData = new FormData() + formData.append('file', imageData, 'file') + const resultURL = await fetch('https://pd.premid.app/create/image', { + method: 'POST', + body: formData, + }).then(res => res.text()) + + presence.info(resultURL) + uploadedFiles[url] = resultURL + return resultURL + } + catch (err) { + presence.error(err as string) + return url + } +} + +presence.on('UpdateData', async () => { + const presenceData: PresenceData = { + type: ActivityType.Listening, + largeImageKey: ActivityAssets.Logo, + } + + const isPlaying = Boolean(document.querySelector('button.player-state-playing')) + const isPaused = Boolean(document.querySelector('button.player-state-paused')) + const hasPlayerBar = Boolean(document.getElementById('player-bar')) + + if (!strings) { + strings = await getStrings() + } + + if (hasPlayerBar) { + const songTitle = getElement('a.song-title') + const elapsedTime = getElement('div.elapsed-time') + const durationTime = getElement('div.total-duration') + const artistName = getElement('div.song-artist > a') + const albumName = getElement('div.song-album > a') + const leftSidebar = document.getElementById('left-sidebar') + const albumContainer = leftSidebar?.children[leftSidebar.children.length - 1] + const albumImgElement = albumContainer?.querySelector('img') + + if (albumImgElement && albumImgElement.src) { + presenceData.largeImageKey = await uploadFile( + albumImgElement.src, + ActivityAssets.Logo, + presence, + ) + } + presenceData.details = songTitle + presenceData.state = artistName || albumName + ? [artistName, albumName].filter(Boolean).join(' - ') + : undefined + + const [currentTime, duration] = [ + timestampFromFormat(elapsedTime ?? ''), + timestampFromFormat(durationTime ?? ''), + ] + + if (isPlaying) { + [presenceData.startTimestamp, presenceData.endTimestamp] = getTimestamps(currentTime, duration) + presenceData.smallImageKey = Assets.Play + presenceData.smallImageText = strings.play + } + else if (isPaused) { + presenceData.smallImageKey = Assets.Pause + presenceData.smallImageText = strings.pause + } + } + + presence.setActivity(presenceData) +})