WIP: [DEPRECATED] Feature: Adds integration of topik/youtube-music-obs-widget as plugin #1120

Draft
topik wants to merge 2 commits from topik/master into master
topik commented 2023-04-19 13:10:54 +00:00 (Migrated from github.com)

Hello,

I got a request to add a plugin for my obs widget so here it is.

Plugin makes a simple http server and give a info about current song in JSON. The JSON structure is the same as in other YT music software that is being used so the obs widget is compatible.

Feel free to rename the plugin.

Thanks

Hello, I got a [request](https://github.com/topik/youtube-music-obs-widget/issues/4) to add a plugin for my [obs widget](https://github.com/topik/youtube-music-obs-widget) so here it is. Plugin makes a simple http server and give a info about current song in JSON. The JSON structure is the same as in other YT music software that is being used so the obs widget is compatible. Feel free to rename the plugin. Thanks
Araxeus commented 2023-04-25 10:53:37 +00:00 (Migrated from github.com)

This looks extremely similar to https://github.com/th-ch/youtube-music/blob/master/plugins/tuna-obs/back.js

Could you give a quick explanation why a new plugin is needed?

This looks extremely similar to https://github.com/th-ch/youtube-music/blob/master/plugins/tuna-obs/back.js Could you give a quick explanation why a new plugin is needed?
topik commented 2023-04-25 11:11:12 +00:00 (Migrated from github.com)

I took some inspiration from tuna-obs because I'm not a Node.js developer. Tuna-obs sends requests to the OBS plugin itself, which means you have to install the OBS (and the OBS plugin) for the whole thing to work.

However, my plugin works the opposite way. It creates a webserver and serves JSON data about the current song, so you don't need to install anything. Just open the plugin's webpage and it will work. You can add the webpage as a source to OBS.

Additionally, my plugin has a different design.

I took some inspiration from tuna-obs because I'm not a Node.js developer. Tuna-obs sends requests to the OBS plugin itself, which means you have to install the OBS (and the OBS plugin) for the whole thing to work. However, my plugin works the opposite way. It creates a webserver and serves JSON data about the current song, so you don't need to install anything. Just open the plugin's [webpage](https://github.com/topik/youtube-music-obs-widget) and it will work. You can add the webpage as a source to OBS. Additionally, my plugin has a different design.
arjix left a comment
Owner

PS: You should use a formatter on your code :^)

PS: You should use a formatter on your code :^)
@ -0,0 +1,62 @@
const {ipcMain} = require("electron");
Owner

Just a small lifehack :^)

                Object.assign(data, {
                    player: {
                        hasSong: true,
                        isPaused: currentSongInfo.isPaused,
                        seekbarCurrentPosition: currentSongInfo.elapsedSeconds
                    },
                    track: {
                        author: [currentSongInfo.artist],
                        title: currentSongInfo.title,
                        album: currentSongInfo.album,
                        cover: currentSongInfo.cover,
                        duration: currentSongInfo.duration
                    }
                });
Just a small lifehack :^) ```suggestion Object.assign(data, { player: { hasSong: true, isPaused: currentSongInfo.isPaused, seekbarCurrentPosition: currentSongInfo.elapsedSeconds }, track: { author: [currentSongInfo.artist], title: currentSongInfo.title, album: currentSongInfo.album, cover: currentSongInfo.cover, duration: currentSongInfo.duration } }); ```
Owner

Since you assign a list to it, shouldn't the initial value be an empty list?

		author: [],
Since you assign a list to it, shouldn't the initial value be an empty list? ```suggestion author: [], ```
topik commented 2023-07-21 14:45:58 +00:00 (Migrated from github.com)

PS: You should use a formatter on your code :^)

You are absolutely right, no idea why i didn't do that. I would do it in any other language :| ... Also thanks for pointing out wrong variable type.

> PS: You should use a formatter on your code :^) You are absolutely right, no idea why i didn't do that. I would do it in any other language :| ... Also thanks for pointing out wrong variable type.
JellyBrick commented 2024-10-15 04:46:38 +00:00 (Migrated from github.com)

@topik
Could you rework this PR? The entire plugin system has been reworked.

@topik Could you rework this PR? The entire plugin system has been reworked.
JellyBrick commented 2024-12-28 23:51:37 +00:00 (Migrated from github.com)

If you are still interested, this may be helpful: https://github.com/th-ch/youtube-music/pull/2723/files

If you are still interested, this may be helpful: https://github.com/th-ch/youtube-music/pull/2723/files
Owner

Nah, a rewrite to work with the new codebase wouldn't be enough.
I did just that

import { createPlugin } from '@/utils';
import { t } from '@/i18n';

import { Hono } from 'hono';
import { serve, ServerType } from '@hono/node-server';
import registerCallback, { SongInfo } from '@/providers/song-info';

let server: ServerType | null = null;
const port = 9863;

export default createPlugin({
  name: () => t('plugins.obs-widget.name'),
  description: () => t('plugins.obs-widget.description'),

  authors: ['ArjixWasTaken'],
  restartNeeded: false,

  backend: {
    start() {
      let songInfo: SongInfo | null = null;
      registerCallback((info) => {
        songInfo = info;
      });

      const app = new Hono();

      app.get('/query', async (ctx) => {
        ctx.res.headers.set('Access-Control-Allow-Origin', '*');
        return ctx.json({
          player: {
            hasSong: !!songInfo,
            isPaused: songInfo?.isPaused ?? false,
            seekbarCurrentPosition: songInfo?.elapsedSeconds ?? 0,
          },
          track: {
            author: songInfo ? [songInfo?.artist] : [],
            title: songInfo?.title ?? '',
            album: songInfo?.album ?? '',
            cover: songInfo?.imageSrc ?? '',
            duration: songInfo?.songDuration ?? 0,
          },
        });
      });

      server = serve({ fetch: app.fetch, port });
    },
    stop() {
      server?.close();
      server = null;
    },
  },
});

but when I saw the client code, it doesn't even use the /query endpoint, it uses /api/v1/* stuff specific to ytmd

it is trying to authenticate, and if it fails it will show annoying alerts to the user
image

Nah, a rewrite to work with the new codebase wouldn't be enough. I did just that ```typescript import { createPlugin } from '@/utils'; import { t } from '@/i18n'; import { Hono } from 'hono'; import { serve, ServerType } from '@hono/node-server'; import registerCallback, { SongInfo } from '@/providers/song-info'; let server: ServerType | null = null; const port = 9863; export default createPlugin({ name: () => t('plugins.obs-widget.name'), description: () => t('plugins.obs-widget.description'), authors: ['ArjixWasTaken'], restartNeeded: false, backend: { start() { let songInfo: SongInfo | null = null; registerCallback((info) => { songInfo = info; }); const app = new Hono(); app.get('/query', async (ctx) => { ctx.res.headers.set('Access-Control-Allow-Origin', '*'); return ctx.json({ player: { hasSong: !!songInfo, isPaused: songInfo?.isPaused ?? false, seekbarCurrentPosition: songInfo?.elapsedSeconds ?? 0, }, track: { author: songInfo ? [songInfo?.artist] : [], title: songInfo?.title ?? '', album: songInfo?.album ?? '', cover: songInfo?.imageSrc ?? '', duration: songInfo?.songDuration ?? 0, }, }); }); server = serve({ fetch: app.fetch, port }); }, stop() { server?.close(); server = null; }, }, }); ``` but when I saw the client code, it doesn't even use the `/query` endpoint, it uses `/api/v1/*` stuff specific to ytmd it is trying to authenticate, and if it fails it will show annoying alerts to the user ![image](https://github.com/user-attachments/assets/6b2a1611-2f2c-4f7d-9daf-95401fa10a44)
Owner

One solution would be to fake compatibility with the ytmd API, and only implement the endpoints used, but do we want that?

One solution would be to fake compatibility with the ytmd API, and only implement the endpoints used, but do we want that?
Owner

Faking ytmd compatibility requires socket.io as a dependency, and I am not a fan of that.
But hey, it can be done

image

Faking ytmd compatibility requires socket.io as a dependency, and I am not a fan of that. But hey, it can be done ![image](https://github.com/user-attachments/assets/6560dd3e-4967-47b5-9750-440fd82cdb26)
Owner

I will not be making a PR on this, because I do not really want it merged.
But I'll be leaving my code here

import { createPlugin } from '@/utils';
import { t } from '@/i18n';

import { Hono } from 'hono';

import { logger } from 'hono/logger';
import { serve } from '@hono/node-server';
import { Server as SocketIO } from 'socket.io';

import registerCallback, { SongInfo } from '@/providers/song-info';

let server: ServerType | null = null;
const port = 9863;

export default createPlugin({
  name: () => t('plugins.obs-widget.name'),
  description: () => t('plugins.obs-widget.description'),

  authors: ['ArjixWasTaken'],
  restartNeeded: false,

  backend: {
    start() {
      let songInfo: SongInfo | null = null;
      const app = new Hono().use(logger());

      app.use(async (ctx, next) => {
        await next();
        ctx.res.headers.set('Access-Control-Allow-Origin', '*');
      });

      app.options('/api/v1/*', async (ctx) => {
        ctx.res.headers.set('Access-Control-Allow-Methods', 'GET, POST');
        ctx.res.headers.set('Access-Control-Allow-Headers', 'Content-Type');
        return ctx.text('');
      });

      app.post('/api/v1/auth/requestcode', async (ctx) => {
        return ctx.json({ statusCode: 200, code: 'lmao' });
      });

      app.post('/api/v1/auth/request', async (ctx) => {
        return ctx.json({ token: 'lmao' });
      });

      app.get('/api/v1/state', async (ctx) => {
        return ctx.json({
          player: {
            trackState: songInfo ? (songInfo.isPaused ? 0 : 1) : -1,
            volume: 1,
          },
          video: {
            author: songInfo?.artist ?? '',
            channelId: '',
            title: songInfo?.title ?? '',
            album: songInfo?.album,
            albumId: null,
            likeStatus: -1,
            thumbnails: [
              {
                url: songInfo?.imageSrc,
                width: 100,
                height: 100,
              },
            ],
            durationSeconds: songInfo?.songDuration ?? 0,
            id: songInfo?.videoId,
          },
        });
      });

      server = serve({ fetch: app.fetch, port });
      const ws = new SocketIO(server, {
        serveClient: false,
        transports: ['websocket'],
      });

      ws.of('/api/v1/realtime').on('connection', (socket) => {
        console.log('Connected to OBS');
      });

      registerCallback((info) => {
        songInfo = info;

        ws.of('/api/v1/realtime').emit('state-update', {
          player: {
            trackState: songInfo ? (songInfo.isPaused ? 0 : 1) : -1,
            videoProgress: songInfo.elapsedSeconds,
            volume: 1,
          },
          video: {
            author: songInfo?.artist ?? '',
            channelId: '',
            title: songInfo?.title ?? '',
            album: songInfo?.album,
            albumId: null,
            likeStatus: -1,
            thumbnails: [
              {
                url: songInfo?.imageSrc,
                width: 100,
                height: 100,
              },
            ],
            durationSeconds: songInfo?.songDuration ?? 0,
            id: songInfo?.videoId,
          },
        });
      });
    },
    stop() {
      server?.close();
      server = null;
    },
  },
});

Honestly, I'd suggest we make our own realtime websocket using hono's websocket helper.


PS: I did try faking socket.io compatibility, but their documentation doesn't explain shit, so I gave up and used their library.

I will not be making a PR on this, because I do not really want it merged. But I'll be leaving my code here ```typescript import { createPlugin } from '@/utils'; import { t } from '@/i18n'; import { Hono } from 'hono'; import { logger } from 'hono/logger'; import { serve } from '@hono/node-server'; import { Server as SocketIO } from 'socket.io'; import registerCallback, { SongInfo } from '@/providers/song-info'; let server: ServerType | null = null; const port = 9863; export default createPlugin({ name: () => t('plugins.obs-widget.name'), description: () => t('plugins.obs-widget.description'), authors: ['ArjixWasTaken'], restartNeeded: false, backend: { start() { let songInfo: SongInfo | null = null; const app = new Hono().use(logger()); app.use(async (ctx, next) => { await next(); ctx.res.headers.set('Access-Control-Allow-Origin', '*'); }); app.options('/api/v1/*', async (ctx) => { ctx.res.headers.set('Access-Control-Allow-Methods', 'GET, POST'); ctx.res.headers.set('Access-Control-Allow-Headers', 'Content-Type'); return ctx.text(''); }); app.post('/api/v1/auth/requestcode', async (ctx) => { return ctx.json({ statusCode: 200, code: 'lmao' }); }); app.post('/api/v1/auth/request', async (ctx) => { return ctx.json({ token: 'lmao' }); }); app.get('/api/v1/state', async (ctx) => { return ctx.json({ player: { trackState: songInfo ? (songInfo.isPaused ? 0 : 1) : -1, volume: 1, }, video: { author: songInfo?.artist ?? '', channelId: '', title: songInfo?.title ?? '', album: songInfo?.album, albumId: null, likeStatus: -1, thumbnails: [ { url: songInfo?.imageSrc, width: 100, height: 100, }, ], durationSeconds: songInfo?.songDuration ?? 0, id: songInfo?.videoId, }, }); }); server = serve({ fetch: app.fetch, port }); const ws = new SocketIO(server, { serveClient: false, transports: ['websocket'], }); ws.of('/api/v1/realtime').on('connection', (socket) => { console.log('Connected to OBS'); }); registerCallback((info) => { songInfo = info; ws.of('/api/v1/realtime').emit('state-update', { player: { trackState: songInfo ? (songInfo.isPaused ? 0 : 1) : -1, videoProgress: songInfo.elapsedSeconds, volume: 1, }, video: { author: songInfo?.artist ?? '', channelId: '', title: songInfo?.title ?? '', album: songInfo?.album, albumId: null, likeStatus: -1, thumbnails: [ { url: songInfo?.imageSrc, width: 100, height: 100, }, ], durationSeconds: songInfo?.songDuration ?? 0, id: songInfo?.videoId, }, }); }); }, stop() { server?.close(); server = null; }, }, }); ``` Honestly, I'd suggest we make our own realtime websocket using [hono's websocket helper](https://github.com/honojs/middleware/tree/main/packages/node-ws). --- PS: I did try faking socket.io compatibility, but their documentation doesn't explain shit, so I gave up and used their library.
topik commented 2025-01-06 23:05:49 +00:00 (Migrated from github.com)

If there is an accessible endpoint, I can change the widget so it works with both programs, so there will be no authentication for https://github.com/th-ch/youtube-music/tree/master. The question is, who's gonna make the endpoint and how.

PS: good job 😂

If there is an accessible endpoint, I can change the widget so it works with both programs, so there will be no authentication for https://github.com/th-ch/youtube-music/tree/master. The question is, who's gonna make the endpoint and how. PS: good job 😂
Owner

If there is an accessible endpoint, I can change the widget so it works with both programs

we do not currently have websockets, but the plugin api-server does have an endpoint to get the player state as well as the current song, so it would be similar to the /query you used to call in the past

here is the schema of the response, and here is the route

I do see that having websockets would be useful, so enhancing the api-server plugin with it would be an option

> If there is an accessible endpoint, I can change the widget so it works with both programs we do not currently have websockets, but the plugin `api-server` does have an endpoint to get the player state as well as the current song, so it would be similar to the `/query` you used to call in the past [here](https://github.com/th-ch/youtube-music/blob/master/src/plugins/api-server/backend/scheme/song-info.ts#L6) is the schema of the response, and [here](https://github.com/th-ch/youtube-music/blob/master/src/plugins/api-server/backend/routes/control.ts#L339-L357) is the route I do see that having websockets would be useful, so enhancing the `api-server` plugin with it would be an option
topik commented 2025-01-06 23:39:19 +00:00 (Migrated from github.com)

Then I can just use the plugin that already exists 🙂 I think that would be the easiest solution.

Then I can just use the plugin that already exists 🙂 I think that would be the easiest solution.
Owner

you'll have to deal with authentication there as well 😔, although it is optional iirc

you'll have to deal with authentication there as well 😔, although it is optional iirc
Owner

since I'm invested in this topic already, leave it to me, I'll make a PR to your widget

since I'm invested in this topic already, leave it to me, I'll make a PR to your widget
Owner

man I completely forgot about this

man I completely forgot about this
This pull request is marked as a work in progress.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin topik/master:topik/master
git switch topik/master

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch master
git merge --no-ff topik/master
git switch topik/master
git rebase master
git switch master
git merge --ff-only topik/master
git switch topik/master
git rebase master
git switch master
git merge --no-ff topik/master
git switch master
git merge --squash topik/master
git switch master
git merge --ff-only topik/master
git switch master
git merge topik/master
git push origin master
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: YTMD/youtube-music#1120
No description provided.