From beb1559cd4cc030e00afe2126d991df5376ecf25 Mon Sep 17 00:00:00 2001 From: "Gleb O. Ivaniczkij" Date: Fri, 26 Jul 2024 00:33:19 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D0=B7=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=B0=D0=BD=20=D0=B2=D0=B5=D0=B1-=D1=81=D0=B5=D1=80?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=20=D0=B8=20=D1=80=D0=B0=D1=81=D1=88=D0=B8?= =?UTF-8?q?=D1=80=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=BB=D1=8F=20=D0=B1?= =?UTF-8?q?=D1=80=D0=B0=D1=83=D0=B7=D0=B5=D1=80=D0=BE=D0=B2=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=B1=D0=B0=D0=B7=D0=B5=20Chromium?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extension/main.js | 43 +++++++ extension/manifest.json | 23 ++++ server/api/__init__.py | 1 + server/api/main.py | 151 +++++++++++++++++++++++ server/config.ini | 3 + server/config/__init__.py | 1 + server/config/main.py | 43 +++++++ server/icon.ico | Bin 0 -> 3549 bytes server/main.py | 17 +++ server/requirements.txt | 6 + server/static/scripts/color-thief.umd.js | 1 + server/static/scripts/main.js | 41 ++++++ server/static/styles/main.css | 130 +++++++++++++++++++ server/templates/overlay.jinja2 | 30 +++++ 14 files changed, 490 insertions(+) create mode 100644 extension/main.js create mode 100644 extension/manifest.json create mode 100644 server/api/__init__.py create mode 100644 server/api/main.py create mode 100644 server/config.ini create mode 100644 server/config/__init__.py create mode 100644 server/config/main.py create mode 100644 server/icon.ico create mode 100644 server/main.py create mode 100644 server/requirements.txt create mode 100644 server/static/scripts/color-thief.umd.js create mode 100644 server/static/scripts/main.js create mode 100644 server/static/styles/main.css create mode 100644 server/templates/overlay.jinja2 diff --git a/extension/main.js b/extension/main.js new file mode 100644 index 0000000..c2b87d2 --- /dev/null +++ b/extension/main.js @@ -0,0 +1,43 @@ +const mutationObserverConfig = { + attributes: true, + childList: false, + characterData: false, +}; + +const connectWebSocket = () => { + const ws = new WebSocket('ws://localhost:8000/ws/v1/plugin'); + const progressElement = document.querySelector('#progress-bar'); + const mutationObserver = new MutationObserver(() => { + const promise = new Promise(resolve => { + const playerBarElement = document.querySelector('ytmusic-player-bar'); + const metadataElement = playerBarElement.querySelector('.middle-controls > .content-info-wrapper'); + const titleElement = metadataElement.querySelector(`yt-formatted-string.title`); + const subtitleElement = metadataElement.querySelector('span.subtitle > yt-formatted-string.byline'); + const imageElement = playerBarElement.querySelector('img'); + if (titleElement.textContent === '' || subtitleElement.length === 0) return; + const data = { + type: 'music', + attributes: { + title: titleElement.textContent, + artists: subtitleElement.title.split('•')[0].trim(), + progress: progressElement.ariaValueNow / progressElement.ariaValueMax, + image: imageElement.src, + }, + }; + ws.send(JSON.stringify(data)); + resolve(); + }); + promise.then(); + }); + + ws.onopen = () => { + mutationObserver.observe(progressElement, mutationObserverConfig); + }; + + ws.onclose = () => { + mutationObserver.disconnect(); + connectWebSocket(); + }; +}; + +connectWebSocket(); diff --git a/extension/manifest.json b/extension/manifest.json new file mode 100644 index 0000000..68fbd9a --- /dev/null +++ b/extension/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "YT Music Live", + "description": "YT Music Live", + "version": "1.0", + "manifest_version": 3, + "action": { + "default_icon": "icon.png" + }, + "content_scripts": [ + { + "js": [ + "main.js" + ], + "matches": [ + "*://music.youtube.com/*" + ] + } + ], + "permissions": [ + "activeTab", + "scripting" + ] +} diff --git a/server/api/__init__.py b/server/api/__init__.py new file mode 100644 index 0000000..e4c7aa5 --- /dev/null +++ b/server/api/__init__.py @@ -0,0 +1 @@ +from .main import app diff --git a/server/api/main.py b/server/api/main.py new file mode 100644 index 0000000..9ebfbe4 --- /dev/null +++ b/server/api/main.py @@ -0,0 +1,151 @@ +import asyncio +import hashlib +import os + +import aiohttp +from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect +from fastapi.exceptions import HTTPException +from fastapi.responses import Response, HTMLResponse, FileResponse +from fastapi.staticfiles import StaticFiles +from jinja2 import Environment, FileSystemLoader + +import config + + +app = FastAPI() +app.mount( + path='/static', + app=StaticFiles( + directory=os.path.join( + config.Main.working_directory, + 'static', + ), + ), +) + +env = Environment( + loader=FileSystemLoader( + searchpath=os.path.join( + '.', + 'templates', + ), + ), + enable_async=True, +) + + +class ConnectionManager: + def __init__(self): + self.connections: list[WebSocket] = list[WebSocket]() + + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.connections.append(websocket) + + def disconnect(self, websocket: WebSocket): + self.connections.remove(websocket) + + async def broadcast(self, data: dict): + for connection in self.connections: + asyncio.ensure_future(connection.send_json(data)) + + +overlay_manager = ConnectionManager() +plugin_manager = ConnectionManager() + + +@app.options( + path='/api/v1/overlay', +) +async def function(): + return Response( + headers={ + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': '*', + 'Access-Control-Allow-Headers': '*', + }, + status_code=200, + ) + + +@app.get( + path='/overlay', +) +async def function( + justify_content: str = Query( + default='end', + alias='justify-content', + ), + align_items: str = Query( + default='end', + alias='align-items', + ), +): + template = env.get_template('overlay.jinja2') + return HTMLResponse( + content=await template.render_async( + justify_content=justify_content, + align_items=align_items, + ), + status_code=200, + ) + + +@app.get( + path='/image', +) +async def function( + src: str = Query( + default=..., + ), +): + image_name = hashlib.sha1(src.encode('ascii')).hexdigest() + image_path = os.path.join( + config.Main.images_directory, + image_name, + ) + if not os.path.exists(image_path): + os.makedirs( + name=config.Main.images_directory, + exist_ok=True, + ) + async with aiohttp.ClientSession() as session: + async with session.get(src) as response: + if not response.ok: + raise HTTPException( + status_code=response.status, + ) + with open( + file=image_path, + mode='wb', + ) as file: + file.write(await response.read()) + return FileResponse( + path=image_path, + ) + + +@app.websocket('/ws/v1/overlay') +async def function( + websocket: WebSocket, +): + await overlay_manager.connect(websocket) + try: + while True: + data = await websocket.receive_json() + await plugin_manager.broadcast(data) + except WebSocketDisconnect: + overlay_manager.disconnect(websocket) + + +@app.websocket('/ws/v1/plugin') +async def function( + websocket: WebSocket, +): + await plugin_manager.connect(websocket) + try: + while True: + data = await websocket.receive_json() + await overlay_manager.broadcast(data) + except WebSocketDisconnect: + plugin_manager.disconnect(websocket) diff --git a/server/config.ini b/server/config.ini new file mode 100644 index 0000000..c11d3d2 --- /dev/null +++ b/server/config.ini @@ -0,0 +1,3 @@ +[Main] +host = 127.0.0.1 +port = 8000 diff --git a/server/config/__init__.py b/server/config/__init__.py new file mode 100644 index 0000000..0dbf581 --- /dev/null +++ b/server/config/__init__.py @@ -0,0 +1 @@ +from .main import Main diff --git a/server/config/main.py b/server/config/main.py new file mode 100644 index 0000000..84cf58d --- /dev/null +++ b/server/config/main.py @@ -0,0 +1,43 @@ +from configparser import RawConfigParser +from enum import Enum +import os + + +config = RawConfigParser() +config.optionxform = str +config.read( + filenames='./config.ini', +) + + +class Section(Enum): + main = 'Main' + + +class Main: + host = config.get( + section=Section.main.value, + option='host', + fallback='127.0.0.1', + ) + port = config.getint( + section=Section.main.value, + option='port', + fallback=8000, + ) + working_directory = config.get( + section=Section.main.value, + option='working_directory', + fallback=os.getcwd(), + ) + images_directory = config.get( + section=Section.main.value, + option='images_directory', + fallback=os.path.join( + os.getenv('APPDATA'), + 'csasq', + 'YT Music Live', + 'cache', + 'covers', + ), + ) diff --git a/server/icon.ico b/server/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..6586b72dd1ce2a4a2dbf3ddf1897127512d68df6 GIT binary patch literal 3549 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 S0n3R&1IGI1dR4lp$o~OQEkxx2 literal 0 HcmV?d00001 diff --git a/server/main.py b/server/main.py new file mode 100644 index 0000000..3a4356d --- /dev/null +++ b/server/main.py @@ -0,0 +1,17 @@ +import shutil + +import uvicorn + +from api import app +import config + + +if __name__ == '__main__': + try: + uvicorn.run( + app=app, + host=config.Main.host, + port=config.Main.port, + ) + except InterruptedError: + shutil.rmtree(config.Main.images_directory) diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 0000000..9429783 --- /dev/null +++ b/server/requirements.txt @@ -0,0 +1,6 @@ +aiohttp +fastapi +jinja2 +Nuitka +uvicorn +websockets diff --git a/server/static/scripts/color-thief.umd.js b/server/static/scripts/color-thief.umd.js new file mode 100644 index 0000000..705e1de --- /dev/null +++ b/server/static/scripts/color-thief.umd.js @@ -0,0 +1 @@ +!function(t,r){"object"==typeof exports&&"undefined"!=typeof module?module.exports=r():"function"==typeof define&&define.amd?define(r):(t||self).ColorThief=r()}(this,function(){if(!t)var t={map:function(t,r){var n={};return r?t.map(function(t,o){return n.index=o,r.call(n,t)}):t.slice()},naturalOrder:function(t,r){return tr?1:0},sum:function(t,r){var n={};return t.reduce(r?function(t,o,e){return n.index=e,t+r.call(n,o)}:function(t,r){return t+r},0)},max:function(r,n){return Math.max.apply(null,n?t.map(r,n):r)}};var r=function(){var r=5,n=8-r,o=1e3;function e(t,n,o){return(t<<2*r)+(n<l/2){for(e=n.copy(),i=n.copy(),u=(r=a-n[c])<=(o=n[f]-a)?Math.min(n[f]-1,~~(a+o/2)):Math.max(n[c],~~(a-1-r/2));!h[u];)u++;for(s=v[u];!s&&h[u-1];)s=v[--u];return e[f]=u,i[c]=e[f]+1,[e,i]}}(u==o?"r":u==i?"g":"b")}}return u.prototype={volume:function(t){var r=this;return r._volume&&!t||(r._volume=(r.r2-r.r1+1)*(r.g2-r.g1+1)*(r.b2-r.b1+1)),r._volume},count:function(t){var r=this,n=r.histo;if(!r._count_set||t){var o,i,u,a=0;for(o=r.r1;o<=r.r2;o++)for(i=r.g1;i<=r.g2;i++)for(u=r.b1;u<=r.b2;u++)a+=n[e(o,i,u)]||0;r._count=a,r._count_set=!0}return r._count},copy:function(){var t=this;return new u(t.r1,t.r2,t.g1,t.g2,t.b1,t.b2,t.histo)},avg:function(t){var n=this,o=n.histo;if(!n._avg||t){var i,u,a,c,f=0,s=1<<8-r,l=0,h=0,v=0;for(u=n.r1;u<=n.r2;u++)for(a=n.g1;a<=n.g2;a++)for(c=n.b1;c<=n.b2;c++)f+=i=o[e(u,a,c)]||0,l+=i*(u+.5)*s,h+=i*(a+.5)*s,v+=i*(c+.5)*s;n._avg=f?[~~(l/f),~~(h/f),~~(v/f)]:[~~(s*(n.r1+n.r2+1)/2),~~(s*(n.g1+n.g2+1)/2),~~(s*(n.b1+n.b2+1)/2)]}return n._avg},contains:function(t){var r=this,o=t[0]>>n;return gval=t[1]>>n,bval=t[2]>>n,o>=r.r1&&o<=r.r2&&gval>=r.g1&&gval<=r.g2&&bval>=r.b1&&bval<=r.b2}},a.prototype={push:function(t){this.vboxes.push({vbox:t,color:t.avg()})},palette:function(){return this.vboxes.map(function(t){return t.color})},size:function(){return this.vboxes.size()},map:function(t){for(var r=this.vboxes,n=0;n251&&e[1]>251&&e[2]>251&&(r[o].color=[255,255,255])}},{quantize:function(f,s){if(!f.length||s<2||s>256)return!1;var l=function(t){var o,i=new Array(1<<3*r);return t.forEach(function(t){o=e(t[0]>>n,t[1]>>n,t[2]>>n),i[o]=(i[o]||0)+1}),i}(f);l.forEach(function(){});var h=function(t,r){var o,e,i,a=1e6,c=0,f=1e6,s=0,l=1e6,h=0;return t.forEach(function(t){(o=t[0]>>n)c&&(c=o),(e=t[1]>>n)s&&(s=e),(i=t[2]>>n)h&&(h=i)}),new u(a,c,f,s,l,h,r)}(f,l),v=new i(function(r,n){return t.naturalOrder(r.count(),n.count())});function g(t,r){for(var n,e=t.size(),i=0;i=r)return;if(i++>o)return;if((n=t.pop()).count()){var u=c(l,n),a=u[0],f=u[1];if(!a)return;t.push(a),f&&(t.push(f),e++)}else t.push(n),i++}}v.push(h),g(v,.75*s);for(var p=new i(function(r,n){return t.naturalOrder(r.count()*r.volume(),n.count()*n.volume())});v.size();)p.push(v.pop());g(p,s);for(var b=new a;p.size();)b.push(p.pop());return b}}}().quantize,n=function(t){this.canvas=document.createElement("canvas"),this.context=this.canvas.getContext("2d"),this.width=this.canvas.width=t.naturalWidth,this.height=this.canvas.height=t.naturalHeight,this.context.drawImage(t,0,0,this.width,this.height)};n.prototype.getImageData=function(){return this.context.getImageData(0,0,this.width,this.height)};var o=function(){};return o.prototype.getColor=function(t,r){return void 0===r&&(r=10),this.getPalette(t,5,r)[0]},o.prototype.getPalette=function(t,o,e){var i=function(t){var r=t.colorCount,n=t.quality;if(void 0!==r&&Number.isInteger(r)){if(1===r)throw new Error("colorCount should be between 2 and 20. To get one color, call getColor() instead of getPalette()");r=Math.max(r,2),r=Math.min(r,20)}else r=10;return(void 0===n||!Number.isInteger(n)||n<1)&&(n=10),{colorCount:r,quality:n}}({colorCount:o,quality:e}),u=new n(t),a=function(t,r,n){for(var o,e,i,u,a,c=t,f=[],s=0;s=125)&&(e>250&&i>250&&u>250||f.push([e,i,u]));return f}(u.getImageData().data,u.width*u.height,i.quality),c=r(a,i.colorCount);return c?c.palette():null},o.prototype.getColorFromUrl=function(t,r,n){var o=this,e=document.createElement("img");e.addEventListener("load",function(){var i=o.getPalette(e,5,n);r(i[0],t)}),e.src=t},o.prototype.getImageData=function(t,r){var n=new XMLHttpRequest;n.open("GET",t,!0),n.responseType="arraybuffer",n.onload=function(){if(200==this.status){var t=new Uint8Array(this.response);i=t.length;for(var n=new Array(i),o=0;o .progress-bar'); +const titleElement = progressBarElement.querySelector('#music > .progress-bar > .data > .title'); +const artistsElement = progressBarElement.querySelector('#music > .progress-bar > .data > .artists'); +const coverElement = progressBarElement.querySelector('#music > .progress-bar > .cover'); + +const handlers = { + 'target': (attributes) => { + const target = document.getElementById(attributes.id); + target.style.setProperty('--progress-size', `${attributes.progress}`); + }, + 'music': (attributes) => { + titleElement.textContent = attributes.title; + artistsElement.textContent = attributes.artists; + musicElement.style.setProperty('--progress-size', `${(1 - attributes.progress) * 100}%`); + const url = new URL(location); + url.pathname = '/image'; + const urlSearchParams = new URLSearchParams(); + urlSearchParams.set('src', attributes.image); + url.search = urlSearchParams.toString(); + coverElement.src = url; + const colors = colorThief.getPalette(coverElement, 2); + progressBarElement.style.setProperty('--primary-color', `rgb(${colors[0][0]}, ${colors[0][1]}, ${colors[0][2]})`); + progressBarElement.style.setProperty('--secondary-color', `rgb(${colors[1][0]}, ${colors[1][1]}, ${colors[1][2]})`); + musicElement.classList.remove('hidden'); + }, +}; + +const connectWebSocket = () => { + const ws = new WebSocket('ws://localhost:8000/ws/v1/overlay'); + + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + handlers[data.type](data.attributes); + }; + + ws.onclose = () => connectWebSocket(); +}; + +connectWebSocket(); diff --git a/server/static/styles/main.css b/server/static/styles/main.css new file mode 100644 index 0000000..f990f6d --- /dev/null +++ b/server/static/styles/main.css @@ -0,0 +1,130 @@ +@property --primary-color { + syntax: ''; + initial-value: white; + inherits: false; +} + +@property --secondary-color { + syntax: ''; + initial-value: white; + inherits: false; +} + +:root { + --text-shadow: 0 0 12px rgba(0, 0, 0, 0.4); + --box-shadow: 0 .5rem .75rem .375rem rgba(0, 0, 0, .15), 0 .25rem .25rem rgba(0, 0, 0, .3); +} + +* { + font-family: 'Exo 2', sans-serif; + user-select: none; +} + +html { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +body { + padding: 2rem; +} + +#root { + display: flex; + flex-direction: column; + align-items: center; +} + +#root > * { + width: 30rem; +} + +#root > *:not(:first-child) { + margin-top: 2rem; +} + +#root > *.hidden { + display: none; +} + +#root > * > .title { + margin-bottom: .5rem; + color: white; + text-shadow: var(--text-shadow); + font-size: 1.5rem; + font-weight: 600; +} + +#music { + display: flex; + flex-direction: column; + --progress: 100%; +} + +#music > .progress-bar { + position: relative; + display: flex; + align-items: center; + margin-top: .5rem; + padding: 1rem; + width: 28rem; + background-image: linear-gradient(225deg, var(--primary-color), var(--secondary-color)); + box-shadow: var(--box-shadow); + border-radius: .5rem; + overflow: hidden; + transition: + opacity .4s, + --primary-color 1s, + --secondary-color 1s; + --progress-size: inherit; + --progress-color: inherit; +} + +#music > .progress-bar::before { + content: ''; + position: absolute; + top: 0; + right: var(--progress-size); + bottom: 0; + left: 0; + background-color: rgba(0, 0, 0, .3); + transition: + right 1s, + background-color 4s; +} + +#music > .progress-bar > .data { + display: flex; + flex-direction: column; + margin-left: 1rem; + overflow-x: hidden; + z-index: 1; +} + +#music > .progress-bar > .data > .title, +#music > .progress-bar > .data > .artists { + color: white; + white-space: nowrap; + overflow-x: hidden; + text-overflow: ellipsis; +} + +#music > .progress-bar > .data > .title { + font-size: 1.2rem; + font-weight: 600; +} + +#music > .progress-bar > .data > .artists { + font-size: 1rem; + font-weight: 400; +} + +#music > .progress-bar > .cover { + flex-shrink: 0; + width: 3rem; + height: 3rem; + object-fit: cover; + z-index: 1; +} diff --git a/server/templates/overlay.jinja2 b/server/templates/overlay.jinja2 new file mode 100644 index 0000000..68ab9f7 --- /dev/null +++ b/server/templates/overlay.jinja2 @@ -0,0 +1,30 @@ + + + + + + + + + + +
+ +
+ + +