From d1ea292eb434d9ef6d5ecf40db45ed926b8ed65f Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Sat, 6 May 2023 11:28:06 +0100 Subject: [PATCH 01/41] Switch to yt-dlp, as youtube-dl is abandoned and now broken. --- README.md | 2 +- main.py | 4 ++-- player.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9b03b7f..f908e78 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # mp3printer a mp3 printer, written in python and jquery with websockets -You need python3, vlc, vlc bindings for python, youtube-dl and tornado. Start by running ```sudo sh startup.sh``` and the mp3 printer will listen for http requests at port 80. +You need python3, vlc, vlc bindings for python, yt-dlp and tornado. Start by running ```sudo sh startup.sh``` and the mp3 printer will listen for http requests at port 80. Record scratch sound is by "Raccoonanimator" and can be found here: https://freesound.org/people/Raccoonanimator/sounds/160907/ diff --git a/main.py b/main.py index cfba882..dc95cbd 100644 --- a/main.py +++ b/main.py @@ -9,7 +9,7 @@ import socket import json import connections -import youtube_dl +import yt_dlp from tornado.platform.asyncio import AnyThreadEventLoopPolicy from mp3Juggler import mp3Juggler @@ -55,7 +55,7 @@ def on_message(self, message): ydl_opts = { 'quiet': "True", 'format': 'bestaudio/best'} - with youtube_dl.YoutubeDL(ydl_opts) as ydl: + with yt_dlp.YoutubeDL(ydl_opts) as ydl: info_dict = ydl.extract_info(parsed_json['link'], download=False) video_title = info_dict.get('title', None) url = info_dict.get("url", None) diff --git a/player.py b/player.py index f8d58b7..bc27904 100644 --- a/player.py +++ b/player.py @@ -1,7 +1,7 @@ import mp3Juggler import vlc import random -import youtube_dl +import yt_dlp class Player: def __init__(self, juggler): @@ -52,7 +52,7 @@ def play_fallback(self): ydl_opts = { 'quiet': "True", 'format': 'bestaudio/best'} - with youtube_dl.YoutubeDL(ydl_opts) as ydl: + with yt_dlp.YoutubeDL(ydl_opts) as ydl: info_dict = ydl.extract_info(self._dubstep[self._dubstepPosition[0]], download=False) url = info_dict.get("url", None) self.media = self.instance.media_new(url) From d04c543dd952136c106869303167a97c04e094c5 Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Sat, 6 May 2023 12:10:56 +0100 Subject: [PATCH 02/41] Make startup.sh executable. --- startup.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 startup.sh diff --git a/startup.sh b/startup.sh old mode 100644 new mode 100755 From b21d52099173ae44de0aca2bbd47aa0c7783c028 Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Sat, 6 May 2023 12:21:40 +0100 Subject: [PATCH 03/41] Use tempfile.mkstemp() for uploaded files. Avoids clogging the file system long time when the system is restarted. --- main.py | 12 ++++++------ static/songs/.gitignore | 4 ---- 2 files changed, 6 insertions(+), 10 deletions(-) delete mode 100644 static/songs/.gitignore diff --git a/main.py b/main.py index dc95cbd..f0f00d6 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ import os import uuid import _thread +import tempfile import tornado.httpserver import tornado.websocket import tornado.ioloop @@ -16,7 +17,6 @@ clients = connections.Connections() juggler = mp3Juggler(clients) -__UPLOADS__ = "static/songs/" def skipper (): while True: @@ -32,14 +32,14 @@ class Upload(tornado.web.RequestHandler): def post(self): filename = self.request.headers.get('Filename') extn = os.path.splitext(filename)[-1] - cache = __UPLOADS__ + str(uuid.uuid4()) + extn + fd, cachename = tempfile.mkstemp(suffix=extn) infile = {'nick':self.request.headers.get('nick'), 'filename':filename, 'address':self.request.remote_ip, - 'path':cache, - 'mrl':cache} - fh = open(infile['path'], 'wb') - fh.write(self.request.body) + 'path':cachename, + 'mrl':cachename} + with os.fdopen(fd, 'wb') as fh: + fh.write(self.request.body) self.finish() juggler.juggle(infile) diff --git a/static/songs/.gitignore b/static/songs/.gitignore deleted file mode 100644 index 5e7d273..0000000 --- a/static/songs/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore everything in this directory -* -# Except this file -!.gitignore From e29a5593aa704920e2a89540e32aa557666d8352 Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Sat, 6 May 2023 13:04:37 +0100 Subject: [PATCH 04/41] Handle (and show) upload errors without closing connection. Also, clean up CSS somewhat. --- index.html | 100 ++++++++++++++++++++++++++++++++++++++--------------- main.py | 84 +++++++++++++++++++++++++++----------------- 2 files changed, 124 insertions(+), 60 deletions(-) diff --git a/index.html b/index.html index 2e52ed6..a6d7029 100644 --- a/index.html +++ b/index.html @@ -11,24 +11,24 @@ .child-elements { pointer-events: none; } - a{ + a { pointer-events: auto; } - #nickname{ + #nickname { pointer-events: auto; - font-family:Arial, Helvetica, sans-serif; - color:#D0B11B; - font-size:12px; + font-family: Arial, Helvetica, sans-serif; + color: #D0B11B; + font-size: 12px; text-shadow: 1px 1px 0px #5a4d0c; - background:#222; - border:#D0B11B 1px solid; + background: #222; + border: #D0B11B 1px solid; margin-bottom: 2em; - -moz-border-radius:3px; - -webkit-border-radius:3px; - border-radius:3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; -moz-box-shadow: 0px 0px 3px 0px #D0B11B; -webkit-box-shadow: 0px 0px 3px 0px #D0B11B; @@ -36,6 +36,35 @@ text-align: center; } + #connection_details { + color: #D01B1B; + } + + #errors { + display: inline-block; + } + + #errors div { + padding: 0.25em; + + border: #D01B1B 1px solid; + + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + + -moz-box-shadow: 0px 0px 3px 0px #D01B1B; + -webkit-box-shadow: 0px 0px 3px 0px #D01B1B; + box-shadow: 0px 0px 3px 0px #D01B1B; + + margin: 0.5em auto; + } + + #errors div::before { + content: 'Error: '; + font-weight: bold; + } + #foot{ padding: 1em; } @@ -44,11 +73,11 @@ text-align: center; margin: 0px; width: 100%; - box-shadow:inset 1px 1px 40px 1px rgba(0,0,0,.45); - background:#000; - color:#D0B11B; - background-size:cover; - overflow:hidden; + box-shadow: inset 1px 1px 40px 1px rgba(0,0,0,.45); + background: #000; + color: #D0B11B; + background-size: cover; + overflow: hidden; /*Below is footer magic*/ display: flex; min-height: 100vh; @@ -65,8 +94,8 @@ display: none; content: ""; position: fixed; - background:rgba(0,0,0,.75); - text-align:center; + background: rgba(0,0,0,.75); + text-align: center; font-weight: bold; text-align: center; color: #D0B11B; @@ -89,12 +118,13 @@ flex: 1; } + - @@ -442,10 +426,10 @@

- Drop files or youtube links here to upload! + Drop files or links here to upload!

-
+

Welcome to the Deeshu mp3 printer

From 6c2168682c28ec5a13888ce97ed7142ac453743c Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Mon, 8 May 2023 23:27:41 +0100 Subject: [PATCH 23/41] Add buttons to upload files/links. Mainly intended for mobile browsers. Refactors upload handling to handle this cleaner. --- index.html | 253 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 166 insertions(+), 87 deletions(-) diff --git a/index.html b/index.html index ee83938..abc255b 100644 --- a/index.html +++ b/index.html @@ -14,17 +14,23 @@ a { pointer-events: auto; } - #nickname { - pointer-events: auto; + #inputs { + margin-bottom: 2em; + } + #inputs p { + margin: 0.5em; + } + #inputs label, #inputs input, #inputs button { + display: inline-block; + vertical-align: middle; + pointer-events: auto; + } + #inputs input, #inputs button { font-family: Arial, Helvetica, sans-serif; - color: #D0B11B; font-size: 12px; - text-shadow: 1px 1px 0px #5a4d0c; - background: #222; - border: #D0B11B 1px solid; - margin-bottom: 2em; + border: #D0B11B 1px solid; -moz-border-radius: 3px; -webkit-border-radius: 3px; @@ -34,10 +40,31 @@ -webkit-box-shadow: 0px 0px 3px 0px #D0B11B; box-shadow: 0px 0px 3px 0px #D0B11B; text-align: center; + + margin: 0.25em; + } + #inputs input { + color: #D0B11B; + text-shadow: 1px 1px 1px #000; + background: #222; + } + #inputs button { + background: #D0B11B; + color: #222; + text-shadow: 1px 1px 1px #000; + cursor: pointer; + } + #inputs #file_selector { + display: none; + } + #inputs #upload_hint { + font-size: 0.8em; + color: #5a4d0c; } #connection_details { color: #D01B1B; + margin-bottom: 1em; } #errors p { @@ -73,6 +100,7 @@ } body { + font-family: Arial, Helvetica, sans-serif; text-align: center; margin: 0px; width: 100%; @@ -139,7 +167,7 @@ function addTable(list, position) { var table = $('
').addClass('printtable'); var headerRow = $(''); - var headers = ['File','Progress','User', 'Prio']; + var headers = ['Track','Progress','User', 'Prio']; for(i=0; i').text(headers[i]); headerRow.append(header); @@ -234,7 +262,7 @@ } } - function FileSelectHandler(e) { + function FileDropHandler(e) { e.stopPropagation(); e.preventDefault(); $('#overlay').fadeOut('fast'); @@ -248,92 +276,117 @@ for (var i = 0; i < items.length; i++) { var item = items[i]; if (item.kind == 'file') { - var file = item.getAsFile(); - if (file.size > 0) { - if (file.type.startsWith('audio/') || file.type.startsWith('video/')) { - uploads.push({ 'type': 'file', 'file': file }); - } else { - addError("Please, only audio files."); - return false; - } + if (!AddFile(item.getAsFile(), uploads)) { + return false; } } else if (item.kind == 'string' && item.type == 'text/plain') { var lines = dt.getData('text/plain').split('\n'); - var re_host = location.host.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); - var host_match = new RegExp('^https?://' + re_host); for (var l = 0; l < lines.length; l++) { - var link = lines[l] - if (!link.match(host_match)) { - if(link.startsWith('http://') || link.startsWith('https://')) { - uploads.push({ 'type': 'link', 'link': link }); - } else { - addError("Please, only web links."); - return false; - } + if (!AddLink(lines[l], uploads)) { + return false; } } } } - if (uploads.length > 10) { - addError("Please, no more than 10 tracks at a time."); + UploadTracks(uploads); + } + else { + addError('Please upgrade your browser...'); + } + } + + function FileInputHandler(files) { + var uploads = []; + for (var i = 0; i < files.length; i++) { + if (!AddFile(files[i], uploads)) { return false; } - var sawErr = false; - var last_id = null; - for (var i = 0; i < uploads.length; i++) { - var upload = uploads[i]; - switch(upload.type) { - case 'file': - last_id = SendFile(upload.file, last_id); - break; - case 'link': - last_id = SendLink(upload.link, last_id); - break; - } + } + UploadTracks(uploads); + } + + function LinkInputHandler(link) { + var uploads = []; + if (!AddLink(link, uploads)) { + return false; + } + UploadTracks(uploads); + } + + function AddFile(file, uploads) { + if (file.size > 0) { + if (file.type.startsWith('audio/') || file.type.startsWith('video/')) { + uploads.push({ 'type': 'file', 'file': file }); + } else { + addError('Please, only audio files.'); + return false; } } - else { - addError("Please upgrade your browser..."); + return true; + } + + function AddLink(link, uploads) { + var re_host = window.location.host.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + var host_match = new RegExp('^https?://' + re_host); + if (!link.match(host_match)) { + if (link.startsWith('http://') || link.startsWith('https://')) { + uploads.push({ 'type': 'link', 'link': link }); + } else { + addError('Please, only web links.'); + return false; + } } + return true; } function UploadId(nick, name) { return nick + ':' + name + ':' + ((new Date()).getTime() + Math.random()); } - function SendFile(file, parent_id) { + function UploadTracks(uploads) { + if (uploads.length > 10) { + addError('Please, no more than 10 tracks at a time.'); + return false; + } var nick = $('#nickname').val(); - var id = UploadId(nick, file.name); - var req = new XMLHttpRequest(); - req.onreadystatechange = function (aEvt) { - if (req.readyState == 4 && req.status != 200) { - addError(req.responseText); + var last_id = null; + for (var i = 0; i < uploads.length; i++) { + var upload = uploads[i]; + switch(upload.type) { + case 'file': + var id = UploadId(nick, upload.file.name); + var req = new XMLHttpRequest(); + req.onreadystatechange = function (aEvt) { + if (req.readyState == 4 && req.status != 200) { + addError(req.responseText); + } + }; + req.open('post', 'upload', true) + req.setRequestHeader('Filename', upload.file.name); + req.setRequestHeader('Nick', nick); + req.setRequestHeader('Upload-Id', id); + if (last_id) { + req.setRequestHeader('Parent-Id', last_id); + } + req.send(upload.file); + last_id = id; + break; + case 'link': + var id = UploadId(nick, upload.link); + var linkToUpload = { + 'type': 'link', + 'id': id, + 'nick': nick, + 'link': upload.link + }; + if (last_id) { + linkToUpload.parent = last_id; + } + ws.send(JSON.stringify(linkToUpload)); + id = last_id; + break; } - }; - req.open('post', 'upload', true) - req.setRequestHeader("Filename", file.name); - req.setRequestHeader("Nick", nick); - req.setRequestHeader("Upload-Id", id); - if (parent_id) { - req.setRequestHeader("Parent-Id", parent_id); - } - req.send(file); - return id; - } - - function SendLink(link, parent_id) { - var id = UploadId(nick, link); - var linkToUpload = { - "type": "link", - "id": id, - "nick": $('#nickname').val(), - "link": link - }; - if (parent_id) { - linkToUpload.parent = parent_id; } - ws.send(JSON.stringify(linkToUpload)); - return id; } function addError(msg) { @@ -343,7 +396,7 @@ .text(msg) .hide() .appendTo(line); - error.show('slow'); + error.fadeIn('slow'); setTimeout(function(){ line.fadeOut('slow', function() { line.remove(); }); }, 15000); @@ -383,9 +436,9 @@ ws.onclose = function(evt) { console.log('WebSocket connection closed, reconnecting in 10 seconds.'); - $("div#printlist").fadeOut("slow"); - $('#nickname').hide("slow"); - $("#connection").show("slow"); + $('div#printlist').fadeOut('slow'); + $('#inputs').fadeOut('slow'); + $('#connection').fadeIn('slow'); setTimeout(connectWS, 10000); }; @@ -393,25 +446,38 @@ ws.onopen = function(evt) { console.log('WebSocket connection opened.'); - $("div#printlist").fadeIn("slow"); - $('#nickname').show("slow"); - $("#connection").hide("slow"); + $('div#printlist').fadeIn('slow'); + $('#inputs').fadeIn('slow'); + $('#connection').fadeOut('slow'); }; } // begin page load code $(document).ready(function () { - $("div#printlist").hide(); - $("#connection").show(); - $('#nickname').hide(); - nick = getCookie("nick"); + $('div#printlist').hide(); + $('#connection').show(); + $('#inputs').hide(); + + var nick = getCookie('nick'); if (nick != null){ $('#nickname').val(nick); } - $('#nickname').on("propertychange change keyup paste input", function(evt) { - setCookie("nick", $('#nickname').val()) + $('#nickname').on('propertychange change keyup paste input', function(evt) { + setCookie('nick', $('#nickname').val()) + }); + + var selector = $('#file_selector'); + $('#upload_file').click(function() { + selector.click(); }); + selector.on('change', function() { + FileInputHandler(selector.get(0).files); + }) + $('#upload_link').click(function() { + LinkInputHandler(prompt('Link URL:')); + }); + setInterval(disclaimer, 120); connectWS(); @@ -431,7 +497,20 @@

Welcome to the Deeshu mp3 printer

- +
+

+ + +

+

+ + + + +
+ (Or just drag files and/or links into this window) +

+
From 4de190f6e7ce1cd1657dd9c5f5d2155800dead0c Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Mon, 8 May 2023 23:29:49 +0100 Subject: [PATCH 24/41] Minor JS cleanups. --- index.html | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/index.html b/index.html index abc255b..f74f71d 100644 --- a/index.html +++ b/index.html @@ -159,7 +159,7 @@ var myaddress = null; function disclaimer(){ - $("#disclaimer").text(cookies[cookieindex]); + $('#disclaimer').text(cookies[cookieindex]); cookieindex++; cookieindex%=cookies.length; } @@ -234,7 +234,7 @@ // log function log = function(data){ - $("div#terminal").prepend("
" +data); + $('div#terminal').prepend('
' +data); console.log(data); }; @@ -245,14 +245,14 @@ e.stopPropagation(); e.preventDefault(); var overlay = $('#overlay'); - if(e.type == "dragover"){ + if(e.type == 'dragover'){ lastDragTarget = e.target; if(!overlay.is(':visible')) { overlay.stop(true).fadeIn('fast'); } else if(overlayTimeout) { clearTimeout(overlayTimeout); } - } else if(e.type == "dragleave" && $(e.target).is(lastDragTarget)){ + } else if(e.type == 'dragleave' && $(e.target).is(lastDragTarget)){ clearTimeout(overlayTimeout); overlayTimeout = setTimeout(function() { if(overlay.is(':visible')) { @@ -270,7 +270,7 @@ if (dt && dt.items) { var items = dt.items; if (items.length < 1) { - addError("You seem to have dragged nothing, please try again."); + addError('You seem to have dragged nothing, please try again.'); } var uploads = []; for (var i = 0; i < items.length; i++) { @@ -406,7 +406,7 @@ var ws; function connectWS() { console.log('Connecting to WebSocket...'); - ws = new WebSocket("ws://" + window.location.host + "/ws"); + ws = new WebSocket('ws://' + window.location.host + '/ws'); ws.onmessage = function(evt) { var message = JSON.parse(evt.data); switch(message.type){ @@ -414,16 +414,16 @@ myaddress = message.address; break; case 'fallback': - $("#printlist").html(message.filename); + $('#printlist').text(message.filename); break; case 'list': - $("div#printlist").fadeTo(400, 0).promise().done( + $('div#printlist').fadeTo(400, 0).promise().done( function() { addTable(message.list, message.position); }); break; case 'progress': - $("#progress").animate({width: message.position*100+"%"}, 100); + $('#progress').animate({width: message.position*100+'%'}, 100); break; case 'error': addError(message.message); @@ -482,9 +482,9 @@ connectWS(); - $("html").on("dragover", FileDragHover); - $("html").on("dragleave", FileDragHover); - $("html").on("drop", FileSelectHandler); + $('html').on('dragover', FileDragHover); + $('html').on('dragleave', FileDragHover); + $('html').on('drop', FileDropHandler); }); From 15615ab9e24755476b9248454ebed43a5056996f Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Tue, 9 May 2023 00:25:18 +0100 Subject: [PATCH 25/41] Try to fix intermittent Soundcloud issues. It seems the URLs extracted by yt-dlp for at least Soundcloud links are time-limited, so extracting them when adding and not using them until possibly much later doesn't work. To solve this, fetch the URL from yt-dlp just before playing instead. --- main.py | 14 +++++------ mp3Juggler.py | 7 +++--- player.py | 64 ++++++++++++++++++++++++++++++--------------------- 3 files changed, 47 insertions(+), 38 deletions(-) diff --git a/main.py b/main.py index ec669d9..a2e78fb 100644 --- a/main.py +++ b/main.py @@ -46,8 +46,8 @@ def post(self): 'filename': filename, 'extn': extn, 'address': remote_ip(self.request), - 'path': cachename, - 'mrl': cachename + 'mrl': cachename, + 'path': cachename } with os.fdopen(fd, 'wb') as fh: fh.write(self.request.body) @@ -101,21 +101,19 @@ def on_message(self, message): parsed_json = json.loads(message) if parsed_json['type'] == "link": ydl_opts = { - 'quiet': "True", + 'quiet': True, 'format': 'bestaudio/best' } with yt_dlp.YoutubeDL(ydl_opts) as ydl: info_dict = ydl.extract_info(parsed_json['link'], download=False) - video_title = info_dict.get('title', None) - url = info_dict.get("url", None) + title = info_dict.get('title', None) infile = { 'type': 'link', 'upload_id': parsed_json['id'], 'nick': parsed_json['nick'], - 'filename': video_title, + 'filename': title, 'address': remote_ip(self.request), - 'mrl': parsed_json['link'], - 'path': url + 'mrl': parsed_json['link'] } parent = parsed_json['parent'] if 'parent' in parsed_json else None juggler.juggle(infile, parent) diff --git a/mp3Juggler.py b/mp3Juggler.py index f2c0bf8..25b3de5 100644 --- a/mp3Juggler.py +++ b/mp3Juggler.py @@ -96,7 +96,7 @@ def _juggle(self, infile, parent_id): self._songlist.insert(index, infile) if len(self._songlist) == 1: - self._player.play(infile['filename'], infile['path']) + self._player.play(infile) if 'upload_id' in infile and infile['upload_id'] in self._waiting: wait = self._waiting[infile['upload_id']] @@ -151,7 +151,7 @@ def clear(self): self.lock.release() self._clients.message_clients(self.get_list()) - def song_finished(self, event, player): + def song_finished(self, event=None, player=None): self._event.set() def time_change(self): @@ -184,8 +184,7 @@ def play_next(self): if(not self._songlist): self._player.play_fallback() else: - nxt = self._songlist[0] - self._player.play(nxt['filename'], nxt['path'] ) + self._player.play(self._songlist[0]) finally: self.lock.release() self._clients.message_clients(self.get_list()) diff --git a/player.py b/player.py index e9a4013..c482fbe 100644 --- a/player.py +++ b/player.py @@ -31,12 +31,26 @@ def handleDubstep(self): self._dubstepPosition=[random.randint(0,2),random.random()] self._shouldPlayDubstep = not self._shouldPlayDubstep - def play(self, filename, path): - self.handleDubstep() - print("Now playing: "+filename) - self.media = self.instance.media_new(path) - self.mediaplayer.set_media(self.media) - self.mediaplayer.play() + def _get_link_url(self, link): + ydl_opts = { + 'quiet': True, + 'format': 'bestaudio/best' + } + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + info_dict = ydl.extract_info(link, download=False) + return info_dict.get("url", None) + + def play(self, track): + try: + self.handleDubstep() + print("Now playing: "+track['filename']) + path = track['path'] if 'path' in track else self._get_link_url(track['mrl']) + self.media = self.instance.media_new(path) + self.mediaplayer.set_media(self.media) + self.mediaplayer.play() + except Exception as err: + print(err) + self._juggler.song_finished() def scratch(self): self.handleDubstep() @@ -47,23 +61,21 @@ def get_position(self): return self.mediaplayer.get_position() def play_fallback(self): - if(self._shouldPlayDubstep): - if(self._playingDubstep): - self._dubstepPosition[0]=(self._dubstepPosition[0]+1)%len(self._dubstep) - self._dubstepPosition[1]=0 - print("Now playing: Dubstep") - self._playingDubstep = True; - ydl_opts = { - 'quiet': "True", - 'format': 'bestaudio/best'} - with yt_dlp.YoutubeDL(ydl_opts) as ydl: - info_dict = ydl.extract_info(self._dubstep[self._dubstepPosition[0]], download=False) - url = info_dict.get("url", None) - self.media = self.instance.media_new(url) - self.mediaplayer.set_media(self.media) - self.mediaplayer.play() - self.mediaplayer.set_position(self._dubstepPosition[1]) - else: - print("Now playing: Slay radio") - self.mediaplayer.set_media(self._fallback) - self.mediaplayer.play() + try: + if(self._shouldPlayDubstep): + if(self._playingDubstep): + self._dubstepPosition[0]=(self._dubstepPosition[0]+1)%len(self._dubstep) + self._dubstepPosition[1]=0 + print("Now playing: Dubstep") + url = self._get_link_url(self._dubstep[self._dubstepPosition[0]]) + self.media = self.instance.media_new(url) + self.mediaplayer.set_media(self.media) + self.mediaplayer.play() + self.mediaplayer.set_position(self._dubstepPosition[1]) + else: + print("Now playing: Slay radio") + self.mediaplayer.set_media(self._fallback) + self.mediaplayer.play() + except Exception as err: + print(err) + self._juggler.song_finished() From 790308fbabddb8a7dde28a3a606edeb33605912c Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Tue, 9 May 2023 00:28:04 +0100 Subject: [PATCH 26/41] Improve dubstep logic. Was generating (unused) random numbers a bit too often. --- player.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/player.py b/player.py index c482fbe..f57cd50 100644 --- a/player.py +++ b/player.py @@ -12,9 +12,8 @@ def __init__(self, juggler): self.vlc_events = self.mediaplayer.event_manager() self.vlc_events.event_attach(vlc.EventType.MediaPlayerEndReached, juggler.song_finished, 1) self.vlc_events.event_attach(vlc.EventType.MediaPlayerEncounteredError, juggler.song_finished, 1) - self._playingDubstep=False - self._shouldPlayDubstep=False; - self._dubstepPosition=[random.randint(0,2),random.random()] + self._playingDubstep = False + self._shouldPlayDubstep = (random.randint(0, 1) == 1) self._dubstep = [ "https://www.youtube.com/watch?v=dLyH94jNau0", "https://www.youtube.com/watch?v=RRucF7ffPRE", @@ -26,9 +25,8 @@ def release(self): self.mediaplayer.stop() self.instance.release() - def handleDubstep(self): + def _handleDubstep(self): self._playingDubstep = False - self._dubstepPosition=[random.randint(0,2),random.random()] self._shouldPlayDubstep = not self._shouldPlayDubstep def _get_link_url(self, link): @@ -42,7 +40,7 @@ def _get_link_url(self, link): def play(self, track): try: - self.handleDubstep() + self._handleDubstep() print("Now playing: "+track['filename']) path = track['path'] if 'path' in track else self._get_link_url(track['mrl']) self.media = self.instance.media_new(path) @@ -53,7 +51,7 @@ def play(self, track): self._juggler.song_finished() def scratch(self): - self.handleDubstep() + self._handleDubstep() self.mediaplayer.set_media(self._scratch) self.mediaplayer.play() @@ -62,16 +60,20 @@ def get_position(self): def play_fallback(self): try: - if(self._shouldPlayDubstep): - if(self._playingDubstep): - self._dubstepPosition[0]=(self._dubstepPosition[0]+1)%len(self._dubstep) - self._dubstepPosition[1]=0 + if self._shouldPlayDubstep: + if self._playingDubstep: + self._dubstepTrack = (self._dubstepTrack + 1) % len(self._dubstep) + position = 0 + else: + self._dubstepTrack = random.randint(0, len(self._dubstep) - 1) + position = random.random() + self._playingDubstep = True print("Now playing: Dubstep") - url = self._get_link_url(self._dubstep[self._dubstepPosition[0]]) + url = self._get_link_url(self._dubstep[self._dubstepTrack]) self.media = self.instance.media_new(url) self.mediaplayer.set_media(self.media) self.mediaplayer.play() - self.mediaplayer.set_position(self._dubstepPosition[1]) + self.mediaplayer.set_position(position) else: print("Now playing: Slay radio") self.mediaplayer.set_media(self._fallback) From f32c4b29e9b2ec95ec5e3ca55d263da426502df5 Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Tue, 9 May 2023 00:36:58 +0100 Subject: [PATCH 27/41] Queue up WebSocket messages while ws is down. Send them when it's up again. --- index.html | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index f74f71d..58ad6a0 100644 --- a/index.html +++ b/index.html @@ -168,13 +168,13 @@ var table = $('
').addClass('printtable'); var headerRow = $(''); var headers = ['Track','Progress','User', 'Prio']; - for(i=0; i').text(headers[i]); headerRow.append(header); } table.append(headerRow); - for(i=0; i') .appendTo(table); @@ -194,7 +194,7 @@ 'type': 'skip', 'id': $(event.target).data('id') }; - ws.send(JSON.stringify(skipMessage)); + sendWS(JSON.stringify(skipMessage)); } })); prefix.append(']'); @@ -382,7 +382,7 @@ if (last_id) { linkToUpload.parent = last_id; } - ws.send(JSON.stringify(linkToUpload)); + sendWS(JSON.stringify(linkToUpload)); id = last_id; break; } @@ -404,6 +404,7 @@ // Websocket stuff var ws; + var wsQueue = []; function connectWS() { console.log('Connecting to WebSocket...'); ws = new WebSocket('ws://' + window.location.host + '/ws'); @@ -449,8 +450,21 @@ $('div#printlist').fadeIn('slow'); $('#inputs').fadeIn('slow'); $('#connection').fadeOut('slow'); + + var queue = wsQueue; + wsQueue = []; + for (var i = 0; i < queue.length; i++) { + sendWS(queue[i]); + } }; } + function sendWS(message) { + if (ws.readyState == WebSocket.OPEN) { + ws.send(message); + } else { + wsQueue.push(message); + } + } // begin page load code From 458c93aa8f07dc4dffcbd39261580f87bce845d5 Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Tue, 9 May 2023 00:38:25 +0100 Subject: [PATCH 28/41] Add (commented out) option for tornado Application. Makes it possible to load changes to index.html without restarting backend. Leaving it enabled causes unnecessary load, though. --- main.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/main.py b/main.py index a2e78fb..0820b4b 100644 --- a/main.py +++ b/main.py @@ -169,12 +169,16 @@ def on_close(self): clients = Connections(loop) juggler = mp3Juggler(clients) - application = tornado.web.Application([ - (r'/ws', WSHandler), - (r'/', IndexHandler), - (r"/upload", Upload), - (r"/download/(.*)", Download), - ], static_path=os.path.join(os.path.dirname(__file__), "static")) + application = tornado.web.Application( + [ + (r'/ws', WSHandler), + (r'/', IndexHandler), + (r"/upload", Upload), + (r"/download/(.*)", Download), + ], + #compiled_template_cache=False, # Useful when editing index.html + static_path=os.path.join(os.path.dirname(__file__), "static") + ) try: http_server = tornado.httpserver.HTTPServer(application, max_buffer_size=150*1024*1024) From 18dc2929d59e993263e322b26899f20a38c7229a Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Tue, 9 May 2023 22:14:14 +0100 Subject: [PATCH 29/41] Add system service install script. --- install-service.sh | 103 +++++++++++++++++++++++++++++++++++++++++++++ startup.sh | 5 +-- 2 files changed, 105 insertions(+), 3 deletions(-) create mode 100755 install-service.sh diff --git a/install-service.sh b/install-service.sh new file mode 100755 index 0000000..eb02df5 --- /dev/null +++ b/install-service.sh @@ -0,0 +1,103 @@ +#!/bin/sh -e + +[ -x "$(which systemctl)" ] || { + echo "systemctl not found. Are you not using Systemd?" >&2 + exit 1 +} + +args=$(getopt -o g:h --long group:,help -- "$@") +eval set -- "$args" + +printHelp() { + echo "Usage: $0 [-h|--help] [-g|--group ] [-- ]" + echo + echo "Install mp3 printer as a system service, running as the current user." + echo + echo "Arguments:" + echo " -h|--help Show this help and exit." + echo " -g|--group Run as the specified (by name or id) group, allowing said" + echo " group write access to the STDIN socket (for commands)." + echo " -- Run the service with the specified arguments." +} + +dir=$(dirname $(realpath $0)) +user=$(id -un) +uid=$(id -u) +group=$(id -gn) +gid=$(id -g) +stdin_mode=0200 +while [ -n "$1" ]; do + case "$1" in + -g|--group) + getent=$(getent group $2) + group=$(echo $getent | cut -f 1 -d:) + gid=$(echo $getent | cut -f 3 -d:) + stdin_mode=0220 + shift 2 + ;; + -h) + printHelp + exit 0 + ;; + --) + shift + break + ;; + *) + echo "Unknown option '$1'!" >&2 + printHelp + exit 1 + ;; + esac +done + +echo "# Will install service file for running mp3 printer with these settings:" +echo " - Working dir: $dir" +echo " - User: $user ($uid)" +echo " - Group: $group ($gid)" +echo " - Arguments: ${@:-(none)}" +echo +printf "Continue? [y/N] " +read inp +[ "$inp" = "y" -o "$inp" = "Y" ] || exit 1 + +echo "# Installing service file..." +sudo tee /etc/systemd/system/mp3printer.service > /dev/null << EOF +[Unit] +After=network-online.target +Description=mp3 Printer + +[Service] +User=$uid +Group=$gid +WorkingDirectory=$dir +ExecStart=$(which python3) main.py $@ +Sockets=mp3printer.socket +StandardInput=socket +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target +EOF + +echo "# Installing STDIN socket file..." +sudo tee /etc/systemd/system/mp3printer.socket > /dev/null << EOF +[Unit] +Description=mp3 Printer STDIN +PartOf=mp3printer.service + +[Socket] +ListenFIFO=$dir/service.stdin +Service=mp3printer.service +SocketUser=$uid +SocketGroup=$gid +SocketMode=$stdin_mode +RemoveOnStop=yes +EOF + +sudo systemctl daemon-reload +sudo systemctl enable mp3printer.service + +echo "# Starting mp3printer service..." +sudo systemctl start mp3printer.service diff --git a/startup.sh b/startup.sh index a85e281..2955e8d 100755 --- a/startup.sh +++ b/startup.sh @@ -1,4 +1,3 @@ -#!/bin/sh -sleep 1 -cd "${0%/*}" +#!/bin/sh -e +cd "$(dirname $(realpath $0))" python3 main.py "$@" From f1f32c5ee010b09089479d1b24f3f556ce552ab3 Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Tue, 9 May 2023 22:41:46 +0100 Subject: [PATCH 30/41] Add viewport meta tag to improve look in mobile browsers. --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 58ad6a0..2dd9dbd 100644 --- a/index.html +++ b/index.html @@ -3,6 +3,7 @@ Deeshu mp3 printer + From 040304b62bf56c4689c319a4527aa58d07b27261 Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Tue, 9 May 2023 23:08:19 +0100 Subject: [PATCH 31/41] Add pausing ability ("p" in console). --- main.py | 3 +++ mp3Juggler.py | 7 +++++++ player.py | 3 +++ 3 files changed, 13 insertions(+) diff --git a/main.py b/main.py index 0820b4b..9ed52d4 100644 --- a/main.py +++ b/main.py @@ -210,3 +210,6 @@ def signal_handler(sig, frame): elif (inp == "c"): print("Clearing...") juggler.clear() + elif (inp == "p"): + print("Toggling pause...") + juggler.pause() diff --git a/mp3Juggler.py b/mp3Juggler.py index 25b3de5..bb9e3c8 100644 --- a/mp3Juggler.py +++ b/mp3Juggler.py @@ -51,6 +51,13 @@ def skip(self): finally: self.lock.release() + def pause(self): + self.lock.acquire() + try: + self._player.pause() + finally: + self.lock.release() + def juggle(self, infile, parent_id = None): if not self._running: raise Exception('Queue is not running') diff --git a/player.py b/player.py index f57cd50..7352b7d 100644 --- a/player.py +++ b/player.py @@ -50,6 +50,9 @@ def play(self, track): print(err) self._juggler.song_finished() + def pause(self): + self.mediaplayer.pause() + def scratch(self): self._handleDubstep() self.mediaplayer.set_media(self._scratch) From 7a6920c1de73d277004acc4b0463a07ec81dbbf3 Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Tue, 9 May 2023 23:08:40 +0100 Subject: [PATCH 32/41] Fix minor crash in mp3Juggler.get_list() when not yet running. --- mp3Juggler.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/mp3Juggler.py b/mp3Juggler.py index bb9e3c8..1857314 100644 --- a/mp3Juggler.py +++ b/mp3Juggler.py @@ -216,10 +216,13 @@ def get_list(self): 'list': list(map(self._sanitize_item, self._songlist)) } else: - if(self._player._playingDubstep): - message = "Now playing dubstep..." + if(self._running): + if(self._player._playingDubstep): + message = "Now playing dubstep..." + else: + message = "Now playing Slay Radio..." else: - message = "Now playing Slay Radio..." + message = "Not active" return { 'type': 'fallback', 'filename': message} finally: self.lock.release() From 56c9689b608213c76392852b4a0410523d01f851 Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Sat, 13 May 2023 01:05:22 +0100 Subject: [PATCH 33/41] Minor restructuring of main.py. Move start and stop code to functions outside "__main__" block. --- main.py | 87 ++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 32 deletions(-) diff --git a/main.py b/main.py index 9ed52d4..d68b51a 100644 --- a/main.py +++ b/main.py @@ -17,11 +17,16 @@ from connections import Connections from mp3Juggler import mp3Juggler -ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]') -error_prefix = re.compile(r'^[Ee][Rr][Rr]([Oo][Rr])?:\s*') +loop = None +clients = None +juggler = None +http_server = None + +ANSI_ESCAPE = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -/]*[@-~]') +ERROR_PREFIX = re.compile(r'^[Ee][Rr][Rr]([Oo][Rr])?:\s*') def error_message(err): - return error_prefix.sub('', ansi_escape.sub('', str(err))) + return ERROR_PREFIX.sub('', ANSI_ESCAPE.sub('', str(err))) def actual_remote_ip(request): return request.remote_ip @@ -138,6 +143,41 @@ def on_close(self): clients.close_connection(self) +def start(port=80, bind=None): + global loop, clients, juggler, http_server + loop = tornado.ioloop.IOLoop.current() + + clients = Connections(loop) + juggler = mp3Juggler(clients) + + application = tornado.web.Application( + [ + (r'/ws', WSHandler), + (r'/', IndexHandler), + (r"/upload", Upload), + (r"/download/(.*)", Download), + ], + #compiled_template_cache=False, # Useful when editing index.html + static_path=os.path.join(os.path.dirname(__file__), "static") + ) + + http_server = tornado.httpserver.HTTPServer(application, max_buffer_size=150*1024*1024) + http_server.listen(port=port, address=bind) + + threading.Thread(target=loop.start).start() + juggler.start() + +def stop(): + if loop is not None: + # Should use add_callback_from_signal according to documentation, but it's deprecated + # on master (since 2023-05-02), and add_callback should have the same effect since 6.0. + loop.add_callback(lambda: loop.stop()) + if http_server is not None: + http_server.stop() + if juggler is not None: + juggler.stop() + + if __name__ == "__main__": parser = argparse.ArgumentParser( description='Musical democracy' @@ -161,45 +201,28 @@ def on_close(self): default=80 ) args = parser.parse_args() + if args.proxied: remote_ip = forwarded_remote_ip - loop = tornado.ioloop.IOLoop.current() - - clients = Connections(loop) - juggler = mp3Juggler(clients) - - application = tornado.web.Application( - [ - (r'/ws', WSHandler), - (r'/', IndexHandler), - (r"/upload", Upload), - (r"/download/(.*)", Download), - ], - #compiled_template_cache=False, # Useful when editing index.html - static_path=os.path.join(os.path.dirname(__file__), "static") - ) - - try: - http_server = tornado.httpserver.HTTPServer(application, max_buffer_size=150*1024*1024) - http_server.listen(port=args.port, address=args.bind) - print('*** Web Server Started on %s:%s***' % (args.bind or '*', args.port)) - except Exception as err: - print('Error starting web server:', err) - exit(1) - def signal_handler(sig, frame): print("\nSignal caught, exiting...") - loop.add_callback_from_signal(lambda: loop.stop()) - http_server.stop() - juggler.stop() + stop() exit(0) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) - threading.Thread(target=loop.start).start() - juggler.start() + try: + start(args.port, args.bind, player_args) + print('*** Web Server Started on %s:%s***' % ( + args.bind or '*', + args.port + )) + except Exception as err: + print('Error starting web server:', err) + exit(1) + # Start console while True: From 6870375e522ecfedd0351b9071ed16bf63905df0 Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Sat, 13 May 2023 01:23:00 +0100 Subject: [PATCH 34/41] Try to fix race condition with clients waiting during startup. Same issue as in 7a6920c, where it wasn't properly solved. --- mp3Juggler.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mp3Juggler.py b/mp3Juggler.py index 1857314..b645148 100644 --- a/mp3Juggler.py +++ b/mp3Juggler.py @@ -28,12 +28,13 @@ def _remove_song(self, i, song = None): def start(self): if not self._running: - self._running = True self._player = Player(self); self._next_thread = Thread(target=self.play_next, args=()) - self._next_thread.start() self._progress_thread = Thread(target=self.time_change, args=()) + self._running = True + self._next_thread.start() self._progress_thread.start() + self._clients.message_clients(self.get_list()) def stop(self): if self._running: From 490f3e4023b15f1fe5943252513e54ce280f4598 Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Sat, 13 May 2023 01:27:59 +0100 Subject: [PATCH 35/41] Player refactoring. * No reason to create Media objects when MediaPlayer.set_mrl works just as well. * Prefix all internal attributes with _ (or just make them local variables in some cases). * Move static media MRLs to class constants. --- player.py | 58 +++++++++++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/player.py b/player.py index 7352b7d..9db1c32 100644 --- a/player.py +++ b/player.py @@ -3,27 +3,29 @@ import yt_dlp class Player: + + SLAYRADIO = "http://relay3.slayradio.org:8000/" + DUBSTEP = [ + "https://www.youtube.com/watch?v=dLyH94jNau0", + "https://www.youtube.com/watch?v=RRucF7ffPRE", + "https://www.youtube.com/watch?v=nXaMKZApYDM" + ] + SCRATCH = "shortscratch.wav" + def __init__(self, juggler): self._juggler = juggler self.instance = vlc.Instance("--no-video") - self.mediaplayer = self.instance.media_player_new() - self._fallback = self.instance.media_new("http://relay3.slayradio.org:8000/") - self._scratch = self.instance.media_new("shortscratch.wav") - self.vlc_events = self.mediaplayer.event_manager() - self.vlc_events.event_attach(vlc.EventType.MediaPlayerEndReached, juggler.song_finished, 1) - self.vlc_events.event_attach(vlc.EventType.MediaPlayerEncounteredError, juggler.song_finished, 1) + self._mediaplayer = self._instance.media_player_new() + vlc_events = self._mediaplayer.event_manager() + vlc_events.event_attach(vlc.EventType.MediaPlayerEndReached, juggler.song_finished, 1) + vlc_events.event_attach(vlc.EventType.MediaPlayerEncounteredError, juggler.song_finished, 1) self._playingDubstep = False self._shouldPlayDubstep = (random.randint(0, 1) == 1) - self._dubstep = [ - "https://www.youtube.com/watch?v=dLyH94jNau0", - "https://www.youtube.com/watch?v=RRucF7ffPRE", - "https://www.youtube.com/watch?v=nXaMKZApYDM" - ] self.play_fallback() def release(self): - self.mediaplayer.stop() - self.instance.release() + self._mediaplayer.stop() + self._instance.release() def _handleDubstep(self): self._playingDubstep = False @@ -38,49 +40,47 @@ def _get_link_url(self, link): info_dict = ydl.extract_info(link, download=False) return info_dict.get("url", None) + def _play_mrl(self, mrl): + self._mediaplayer.set_mrl(mrl) + self._mediaplayer.play() + def play(self, track): try: self._handleDubstep() print("Now playing: "+track['filename']) path = track['path'] if 'path' in track else self._get_link_url(track['mrl']) - self.media = self.instance.media_new(path) - self.mediaplayer.set_media(self.media) - self.mediaplayer.play() + self._play_mrl(path) except Exception as err: print(err) self._juggler.song_finished() def pause(self): - self.mediaplayer.pause() + self._mediaplayer.pause() def scratch(self): self._handleDubstep() - self.mediaplayer.set_media(self._scratch) - self.mediaplayer.play() + self._play_mrl(self.SCRATCH) def get_position(self): - return self.mediaplayer.get_position() + return self._mediaplayer.get_position() def play_fallback(self): try: if self._shouldPlayDubstep: if self._playingDubstep: - self._dubstepTrack = (self._dubstepTrack + 1) % len(self._dubstep) + self._dubstepTrack = (self._dubstepTrack + 1) % len(self.DUBSTEP) position = 0 else: - self._dubstepTrack = random.randint(0, len(self._dubstep) - 1) + self._dubstepTrack = random.randint(0, len(self.DUBSTEP) - 1) position = random.random() self._playingDubstep = True print("Now playing: Dubstep") - url = self._get_link_url(self._dubstep[self._dubstepTrack]) - self.media = self.instance.media_new(url) - self.mediaplayer.set_media(self.media) - self.mediaplayer.play() - self.mediaplayer.set_position(position) + url = self._get_link_url(self.DUBSTEP[self._dubstepTrack]) + self._play_mrl(url) + self._mediaplayer.set_position(position) else: print("Now playing: Slay radio") - self.mediaplayer.set_media(self._fallback) - self.mediaplayer.play() + self._play_mrl(self.SLAYRADIO) except Exception as err: print(err) self._juggler.song_finished() From f0535b4577f9c6b7dae0ebbacb80c55d159f7966 Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Sat, 13 May 2023 01:31:15 +0100 Subject: [PATCH 36/41] Add support for Chromecast output. If pychromecast is installed (handled cleanly if not installed), two new options are available: * -C/--chromecast-list for listing available Chromecasts (and -groups). * -c/--chromecast for selecting a Chromecast (or -group) to cast to. With -c, VLC's Chromecast support is used for the actual casting. --- README.md | 3 ++- main.py | 50 +++++++++++++++++++++++++++++++++++++++++++++++--- mp3Juggler.py | 5 +++-- player.py | 13 ++++++++++--- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f908e78..4710c2b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # mp3printer a mp3 printer, written in python and jquery with websockets -You need python3, vlc, vlc bindings for python, yt-dlp and tornado. Start by running ```sudo sh startup.sh``` and the mp3 printer will listen for http requests at port 80. +You need python3, vlc, vlc bindings for python, yt-dlp and tornado, optionally pychromecast for casting support. Start by running `sudo ./startup.sh` and the mp3 printer will listen for http requests at port 80. +Running `./startup.sh --help` will show a list of possible options. Record scratch sound is by "Raccoonanimator" and can be found here: https://freesound.org/people/Raccoonanimator/sounds/160907/ diff --git a/main.py b/main.py index d68b51a..5d2785b 100644 --- a/main.py +++ b/main.py @@ -13,6 +13,14 @@ import argparse import urllib.parse + +# optional lib +try: + import pychromecast.discovery + HAS_PYCHROMECAST = True +except ModuleNotFoundError: + HAS_PYCHROMECAST = False + # local libs from connections import Connections from mp3Juggler import mp3Juggler @@ -142,13 +150,12 @@ def on_close(self): print('connection closed') clients.close_connection(self) - -def start(port=80, bind=None): +def start(port=80, bind=None, player_args=None): global loop, clients, juggler, http_server loop = tornado.ioloop.IOLoop.current() clients = Connections(loop) - juggler = mp3Juggler(clients) + juggler = mp3Juggler(clients, player_args) application = tornado.web.Application( [ @@ -193,6 +200,18 @@ def stop(): help='IP to run HTTP server on (default: any IP)', default=None ) + if HAS_PYCHROMECAST: + parser.add_argument( + '-c', '--chromecast', + type=str, + help='Name of Chromecast (or Chromecast group) to cast to.', + default=None + ) + parser.add_argument( + '-C', '--chromecast-list', + action='store_true', + help='List available Chromecast (and Chromecast group) names and exit.' + ) parser.add_argument( 'port', type=int, @@ -202,6 +221,31 @@ def stop(): ) args = parser.parse_args() + player_args = {} + + if HAS_PYCHROMECAST: + if args.chromecast_list: + print('Available Chromecast targets:') + services, browser = pychromecast.discovery.discover_chromecasts() + pychromecast.discovery.stop_discovery(browser) + for service in services: + print('* \"%s\"' % service.friendly_name) + exit(0) + + if args.chromecast is not None: + services, browser = pychromecast.discovery.discover_listed_chromecasts( + friendly_names=[args.chromecast] + ) + pychromecast.discovery.stop_discovery(browser) + if len(services) < 1: + print('Could not find Chromecast (or group) "%s"' % args.chromecast) + exit(1) + elif len(services) > 1: + print('More than one Chromecast (or group) matched "%s"' % args.chromecast) + exit(1) + + player_args['chromecast'] = (services[0].host, services[0].port) + if args.proxied: remote_ip = forwarded_remote_ip diff --git a/mp3Juggler.py b/mp3Juggler.py index b645148..6b86b1c 100644 --- a/mp3Juggler.py +++ b/mp3Juggler.py @@ -7,8 +7,9 @@ from player import Player class mp3Juggler: - def __init__(self, clients): + def __init__(self, clients, player_args=None): self._clients = clients + self._player_args = player_args self._songlist = [] self._counts = {} self._event = Event() @@ -28,7 +29,7 @@ def _remove_song(self, i, song = None): def start(self): if not self._running: - self._player = Player(self); + self._player = Player(self, **self._player_args); self._next_thread = Thread(target=self.play_next, args=()) self._progress_thread = Thread(target=self.time_change, args=()) self._running = True diff --git a/player.py b/player.py index 9db1c32..b2910da 100644 --- a/player.py +++ b/player.py @@ -12,9 +12,16 @@ class Player: ] SCRATCH = "shortscratch.wav" - def __init__(self, juggler): + def __init__(self, juggler, chromecast=None): self._juggler = juggler - self.instance = vlc.Instance("--no-video") + instance_opts = ["--no-video"] + self._media_opts = [] + if chromecast is not None: + instance_opts.append("--no-sout-video") + # These options don't work as instance options, for some reason... + self._media_opts.append(":sout=#chromecast{ip=%s,port=%d}" % chromecast) + self._media_opts.append(":demux-filter=demux_chromecast") + self._instance = vlc.Instance(*instance_opts) self._mediaplayer = self._instance.media_player_new() vlc_events = self._mediaplayer.event_manager() vlc_events.event_attach(vlc.EventType.MediaPlayerEndReached, juggler.song_finished, 1) @@ -41,7 +48,7 @@ def _get_link_url(self, link): return info_dict.get("url", None) def _play_mrl(self, mrl): - self._mediaplayer.set_mrl(mrl) + self._mediaplayer.set_mrl(mrl, *self._media_opts) self._mediaplayer.play() def play(self, track): From 115fa281c03ca91b6f2375f31803f0c75c99b9ed Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Sat, 13 May 2023 01:47:11 +0100 Subject: [PATCH 37/41] Fix handling of arguments with whitespace in install-service.sh Was a known issue, just didn't matter until now. --- install-service.sh | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/install-service.sh b/install-service.sh index eb02df5..ab83180 100755 --- a/install-service.sh +++ b/install-service.sh @@ -51,16 +51,29 @@ while [ -n "$1" ]; do esac done +args= +for x in "$@"; do + case "$x" in + *\ *|*\ *) + x="\"$(printf "%s\n" "$x" | sed -e 's/\\/\\\\\\\\/g' -e 's/"/\\"/g')\"" + ;; + esac + args="$args $x" +done + echo "# Will install service file for running mp3 printer with these settings:" echo " - Working dir: $dir" echo " - User: $user ($uid)" echo " - Group: $group ($gid)" -echo " - Arguments: ${@:-(none)}" +echo " - Arguments:${args:- (none)}" echo printf "Continue? [y/N] " read inp [ "$inp" = "y" -o "$inp" = "Y" ] || exit 1 +echo "# Stopping any running instance of the service..." +sudo systemctl stop mp3printer.service > /dev/null 2>&1 || : + echo "# Installing service file..." sudo tee /etc/systemd/system/mp3printer.service > /dev/null << EOF [Unit] @@ -71,7 +84,7 @@ Description=mp3 Printer User=$uid Group=$gid WorkingDirectory=$dir -ExecStart=$(which python3) main.py $@ +ExecStart=$(which python3) main.py$args Sockets=mp3printer.socket StandardInput=socket StandardOutput=journal From d77c798442a8bd5c7316c176e63454ae77483b4f Mon Sep 17 00:00:00 2001 From: Joakim Tufvegren <104522+firetech@users.noreply.github.com> Date: Sat, 13 May 2023 11:37:35 +0100 Subject: [PATCH 38/41] Add favicon. --- README.md | 2 + index.html | 22 +- static/favicons/android-chrome-192x192.png | Bin 0 -> 22931 bytes static/favicons/android-chrome-512x512.png | Bin 0 -> 72508 bytes static/favicons/apple-touch-icon.png | Bin 0 -> 14959 bytes static/favicons/browserconfig.xml | 9 + static/favicons/favicon-16x16.png | Bin 0 -> 1198 bytes static/favicons/favicon-32x32.png | Bin 0 -> 2406 bytes static/favicons/favicon.ico | Bin 0 -> 15086 bytes static/favicons/favicon.svg | 829 +++++++++++++++++++++ static/favicons/mstile-144x144.png | Bin 0 -> 14966 bytes static/favicons/mstile-150x150.png | Bin 0 -> 14444 bytes static/favicons/mstile-310x150.png | Bin 0 -> 15239 bytes static/favicons/mstile-310x310.png | Bin 0 -> 33958 bytes static/favicons/mstile-70x70.png | Bin 0 -> 9870 bytes static/favicons/site.webmanifest | 19 + 16 files changed, 880 insertions(+), 1 deletion(-) create mode 100644 static/favicons/android-chrome-192x192.png create mode 100644 static/favicons/android-chrome-512x512.png create mode 100644 static/favicons/apple-touch-icon.png create mode 100644 static/favicons/browserconfig.xml create mode 100644 static/favicons/favicon-16x16.png create mode 100644 static/favicons/favicon-32x32.png create mode 100644 static/favicons/favicon.ico create mode 100644 static/favicons/favicon.svg create mode 100644 static/favicons/mstile-144x144.png create mode 100644 static/favicons/mstile-150x150.png create mode 100644 static/favicons/mstile-310x150.png create mode 100644 static/favicons/mstile-310x310.png create mode 100644 static/favicons/mstile-70x70.png create mode 100644 static/favicons/site.webmanifest diff --git a/README.md b/README.md index 4710c2b..17acc94 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,5 @@ You need python3, vlc, vlc bindings for python, yt-dlp and tornado, optionally p Running `./startup.sh --help` will show a list of possible options. Record scratch sound is by "Raccoonanimator" and can be found here: https://freesound.org/people/Raccoonanimator/sounds/160907/ + +Icon is a combination of two icons from the [Tango Desktop Project](http://tango.freedesktop.org/). diff --git a/index.html b/index.html index 2dd9dbd..1fccab9 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,14 @@ Deeshu mp3 printer + + + + + + + +