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
+
+
+
+
+
+
+
+