Compare commits

..

7 Commits

36 changed files with 12345 additions and 49 deletions

237
better-radio/LICENSE Normal file

File diff suppressed because one or more lines are too long

52
better-radio/README.md Normal file
View File

@@ -0,0 +1,52 @@
**Development has unfortunately stopped**
# better-radio
I got fed up with the radio station playing at work. It's pretty much a 2 hour playlist on a loop with ads inbetween songs.
My idea of what this software will do is create a simple internet audio stream that can be played by any device.
The initial idea goes like this:
- Have a raspberry pi connected via AUX to the radio at work.
- The raspberry pi loads a audio stream generated by the software described below.
- A icecast server which distributes an opus audio stream.
- A audio source with dynamic playlist support which plays audio into the icecast server.
- yt-dlp downloading songs on command by the software below.
- A web-gui where employees can request songs based on music on Youtube Music.
- Optionally link the orderpicking system with this so no phone is needed.
- Optionally link the IBM i system so that no phone is needed.
- When no songs are requested, keep playing the youtube Radio playlist until a song is requested.
- An admin portal where every aspect can be changed.
# Fricking commercial licensing
So apperantly in The Netherlands you need to pay a copyright holding company hefty sums of money a year if you play radio on the workfloor.
This is the reason why we have the same radio station on the whole time. Because it's paid for that one and that one only.
This is an amazing project but realistically it will never be rolled out on the workfloor because of this.
Changing work is much easier at this point.
# How it should work
This software will be built in four components.
- A container which automatically plays this audio stream running on the raspberry pi, this also makes it easier to update remotely.
- A audio streamer which supports dynamic playlists, this is so that a "background" playlist can be interrupted by a request.
- A yt-dlp script which gets commands by the GUI listed below, downloads songs and playlists, keeps up a simple library and such
- A web-gui component which handles user interaction, this can be optionally split up to frontend and backend which then controls the components listed above.
# Milestones
## First milesone (done)
Have a working concept, just change the music already. No need for youtube music searching and such, just change the
fricking radio station. Being able to change the radio station from the desktop is already miles better.
- A working raspberry pi player which can be controlled remotely
- A web frontend where users can select the radio station
## Second milestone
Add some Youtube components and an administrator page wich can override some site functions.
- Add youtube-dlp backend wich can fetch search results and show them
- Add youtube-dlp backend wich can download songs for streaming, no file cache yet
- Add administrator page which can lock some functions which can be controlled publicly (done)
- Add function for site administrators to add radio stations by themselves (done)

View File

@@ -0,0 +1,9 @@
# Radiostream frontend
This piece of software will be the HTTP frontend for the employees.
# What it must do
Show what radiostation is currently playing, and offer the option to change it.
# What it could do
Have an employee section where employees can select the radio station. There could also be a seperate admin portal
where options can be configured.

View File

@@ -0,0 +1,130 @@
from flask import Flask, render_template, request, flash
import secrets
from mango import Mango
from bson import ObjectId
from helpers import *
debug = True
app = Flask(__name__)
app.secret_key = secrets.token_hex(22)
@app.route('/location')
def locations():
locs = mango.locations_get_locations()
return render_template('locations.html', locations=locs)
@app.route('/location/<locid>')
def location(locid):
loc = mango.locations_get_location(locid)
return render_template('location.html', location=loc)
@app.route('/location/<locid>/config', methods=['GET', 'POST'])
def location_config(locid):
if request.method == "POST":
request_type = request.form.get('type')
if request_type == 'enabled-stations':
sta = request.form.getlist('station')
mango.locations_update_enabled_stations(locid, sta)
elif request_type == 'enable-requests':
ena = bool(request.form.get('enable', default=False))
mango.locations_update_enable_requests(locid, ena)
elif request_type == 'enable-radio':
ena = bool(request.form.get('enable', default=False))
mango.locations_update_enable_radio(locid, ena)
else:
flash('You posted a form but no such call exists.')
loc = mango.locations_get_location(locid)
sta = mango.stations_get_stations()
pla = mango.player_get_state(loc['config']['player'])
off = check_player_offline(pla['last_update'])
return render_template('location_config.html', location=loc, stations=sta, ObjectId=ObjectId, offline=off)
@app.route('/location/<locid>/request', methods=['GET', 'POST'])
def location_request(locid):
loc = mango.locations_get_location(locid)
return render_template('location_request.html', location=loc)
@app.route('/location/<locid>/radio', methods=['GET', 'POST'])
def location_radio(locid):
loc = mango.locations_get_location(locid)
sta = mango.stations_get_stations()
pla = mango.player_get_state(loc['config']['player'])
if request.method == "POST":
request_type = request.form.get('type')
if request_type == 'select-station':
station = request.form.get('station')
mango.player_set_radio(loc['config']['player'], station)
flash('Enqueued change request')
elif request_type == 'stop-stream':
mango.player_pause_radio(loc['config']['player'])
flash('Enqueued change request')
else:
flash('You posted a form but no such call exists.')
# report current playing radio or false
if pla['radio']['playing'] is False:
cur = False
else:
cur = pla['radio']['stationId']
# set location radio control config enabled
enabled = loc['config']['enable_radio']
# check if player has not been offline for too long
if check_player_offline(pla['last_update']):
enabled = False
return render_template('location_radio.html', location=loc, stations=sta, current=cur, ObjectId=ObjectId, enabled=enabled)
@app.route('/players/<player>/config', methods=['POST'])
def players(player):
data = request.get_json()
new_state = mango.player_get_config(player)
mango.player_update_state(player, data)
if data['radio'].get('cookie') == new_state['radio']['cookie']: # save time by comparing last change cookie
# one caviat, the cookie may not match up when the last station was paused then unpaused
return {}
def generate_radio_return(): # define function to generate response
station = str(new_state['radio']['station'])
url = mango.stations_get_station(new_state['radio']['station'])['stream']['url']
cookie = new_state['radio']['cookie']
return {'type': 'radio', 'stationId': station, 'url': url, 'cookie': cookie}
# checking for playing bools
if new_state['radio']['playing'] is False and data['radio']['playing'] is True:
return {'type': 'silence'}
elif new_state['radio']['playing'] is True and data['radio']['playing'] is False:
return generate_radio_return()
# checking if station is correct
if str(new_state['radio']['station']) != data['radio'].get('stationId'):
return generate_radio_return()
return {} # last resort in case everything matched but cookie did not, and to not status 500
@app.route('/stations')
def stations():
return {}
if __name__ == '__main__':
mango = Mango('mongodb://root:test@192.168.66.113:27017')
app.run(debug=debug, host='0.0.0.0')

View File

@@ -0,0 +1,14 @@
from datetime import datetime, timezone
def utc_time_now():
return int(datetime.now(timezone.utc).timestamp())
def check_player_offline(time):
delta = utc_time_now() - time
if delta > 60:
return True
else:
return False

View File

@@ -0,0 +1,150 @@
from pymongo import MongoClient
from bson.objectid import ObjectId
from secrets import token_hex
from helpers import *
class Mango:
# holy shit, just found out a major bug, find_one and then replace_one is really bad with concurrency.
def __init__(self, connect):
try:
self.client = MongoClient(connect)
self.locations = self.client['radio']['locations']
self.players = self.client['radio']['players']
self.stations = self.client['radio']['stations']
except ConnectionError:
print('MongoDB connection error')
def player_pause_radio(self, plaid):
found = self.players.find_one({"_id": ObjectId(plaid)})
if found is None:
return False
found['new_state']['radio']['playing'] = False
found['new_state']['radio']['cookie'] = token_hex(8)
self.players.replace_one({"_id": ObjectId(plaid)}, found)
return True
def player_set_radio(self, plaid, staid):
found = self.players.find_one({"_id": ObjectId(plaid)})
if found is None:
return False
if self.stations_get_station(staid):
staid = ObjectId(staid)
found['new_state']['radio']['playing'] = True
found['new_state']['radio']['station'] = staid
found['new_state']['radio']['cookie'] = token_hex(8)
self.players.replace_one({"_id": ObjectId(plaid)}, found)
return True
def player_update_state(self, plaid, state):
"""
:param plaid: str
:param state: {'radio': {'playing': bool, 'url': str}, 'request': {'playing': bool, 'url': str}}
:return: True on sucess, False on failure
"""
found = self.players.find_one({"_id": ObjectId(plaid)})
if found is None:
return False
state['last_update'] = utc_time_now()
found['last_state'] = state
self.players.replace_one({"_id": ObjectId(plaid)}, found)
return True
def player_get_state(self, plaid):
found = self.players.find_one({"_id": ObjectId(plaid)})
if found is None:
return False
return found['last_state']
def player_get_config(self, plaid):
found = self.players.find_one({"_id": ObjectId(plaid)})
if found is None:
return False
return found['new_state']
def locations_get_locations(self):
locations = {}
for x in self.locations.find({}):
locations[str(x['_id'])] = x['name']
return locations
def locations_get_location(self, locid):
locid = ObjectId(locid)
found = self.locations.find_one({'_id': locid})
if found is None:
return False
return found
def locations_update_enable_radio(self, locid, ena):
locid = ObjectId(locid)
found = self.locations.find_one({'_id': locid})
if found is None:
return False
found['config']['enable_radio'] = ena
self.locations.replace_one({"_id": locid}, found)
return True
def locations_update_enable_requests(self, locid, ena):
locid = ObjectId(locid)
found = self.locations.find_one({'_id': locid})
if found is None:
return False
found['config']['enable_requests'] = ena
self.locations.replace_one({"_id": locid}, found)
return True
def locations_update_enabled_stations(self, locid, lst):
sta = []
locid = ObjectId(locid)
found = self.locations.find_one({'_id': locid})
if found is None:
return False
for x in lst:
if self.stations_get_station(x):
x = ObjectId(x)
sta.append(x)
found['config']['enabled_stations'] = sta
self.locations.replace_one({"_id": locid}, found)
return True
def stations_get_stations(self):
stations = self.stations.find({})
stations_dict = {}
for x in stations:
i = str(x['_id'])
x.pop('_id')
stations_dict[i] = x
return stations_dict
def stations_get_station(self, staid):
staid = ObjectId(staid)
found = self.stations.find_one({'_id': staid})
if found is None:
return False
return found
if __name__ == '__main__':
mango = Mango('mongodb://root:test@192.168.66.113:27017')
print(mango.stations_get_stations())

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,146 @@
{
"stop": {
"title": "Playing nothing",
"country": "NaN",
"genre": "",
"description": "Select a radio station to play from one of the cards below",
"stream": {
"url": "NaN",
"codec": "NaN",
"bitrate": "NaN",
"type": "NaN"
}
},
"radio_swiss_classic": {
"title": "Radio Swiss Classic",
"country": "Switzerland",
"genre": "Classic",
"description": "Classical music with no ads, station based in Switzerland.",
"stream": {
"url": "https://stream.srg-ssr.ch/m/rsc_de/mp3_128",
"codec": "mp3",
"bitrate": "128k",
"type": "direct"
}
},
"radio_swiss_jazz": {
"title": "Radio Swiss Jazz",
"country": "Switzerland",
"genre": "Jazz",
"description": "Jazz music with no ads, station based in Switzerland.",
"stream": {
"url": "https://stream.srg-ssr.ch/m/rsj/mp3_128",
"codec": "mp3",
"bitrate": "128k",
"type": "direct"
}
},
"radio_swiss_pop": {
"title": "Radio Swiss Pop",
"country": "Switzerland",
"genre": "Pop",
"description": "Pop music with no ads, station based in Switzerland.",
"stream": {
"url": "https://stream.srg-ssr.ch/m/rsp/mp3_128",
"codec": "mp3",
"bitrate": "128k",
"type": "direct"
}
},
"bbc_radio_1_dance": {
"title": "BBC Radio 1 Dance",
"country": "United Kingdom",
"genre": "Dance",
"description": "Populair dance radio station based in the United Kingdom. No ads but repetitive",
"stream": {
"url": "https://as-hls-ww-live.akamaized.net/pool_904/live/ww/bbc_radio_one_dance/bbc_radio_one_dance.isml/bbc_radio_one_dance-audio%3d320000.m3u8",
"codec": "aaclc",
"bitrate": "320k",
"type": "hls"
}
},
"public_domain_jazz": {
"title": "Public Domain Jazz",
"country": "Switzerland",
"genre": "Jazz",
"description": "Internet radio playing swing jazz in the public domain.",
"stream": {
"url": "http://relay.publicdomainradio.org/jazz_swing.aac",
"codec": "aache",
"bitrate": "64k",
"type": "direct"
}
},
"sky_radio": {
"title": "Sky Radio",
"country": "The Netherlands",
"genre": "Pop",
"description": "Pop radio station with loads of ads. Why listen to this?",
"stream": {
"url": "https://playerservices.streamtheworld.com/api/livestream-redirect/SKYRADIO.mp3",
"codec": "mp3",
"bitrate": "128k",
"type": "direct"
}
},
"slam": {
"title": "SLAM!",
"country": "The Netherlands",
"genre": "Pop",
"description": "Pop/Dance radio station with loads of ads.",
"stream": {
"url": "https://25693.live.streamtheworld.com/SLAM_AAC.aac",
"codec": "aache",
"bitrate": "96k",
"type": "direct"
}
},
"qmusic": {
"title": "Qmusic",
"country": "The Netherlands",
"genre": "Pop",
"description": "Pop/Dance radio station with loads of ads.",
"stream": {
"url": "https://icecast-qmusicnl-cdp.triple-it.nl/Qmusic_nl_live_high.aac",
"codec": "aache",
"bitrate": "96k",
"type": "direct"
}
},
"tomorrowland_owr": {
"title": "Tomorrowland One World Radio",
"country": "United Kingdom",
"genre": "Pop",
"description": "Modern pop music with some dance",
"stream": {
"url": "https://22353.live.streamtheworld.com/OWR_WORLD_RADIO_NL.mp3",
"codec": "mp3",
"bitrate": "128k",
"type": "direct"
}
},
"radio_538": {
"title": "Radio 538",
"country": "The Netherlands",
"genre": "Pop",
"description": "Idk here is just some filler text for the card",
"stream": {
"url": "https://25533.live.streamtheworld.com/RADIO538.mp3",
"codec": "mp3",
"bitrate": "128k",
"type": "direct"
}
},
"radio_decibel": {
"title": "Radio Decibel",
"country": "The Netherlands",
"genre": "Pop",
"description": "Mostly playing 90's songs which is very nice",
"stream": {
"url": "https://23553.live.streamtheworld.com/RADIODECIBEL.mp3",
"codec": "mp3",
"bitrate": "192k",
"type": "direct"
}
}
}

View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dashboard - BS Vianen</title>
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap.min.css') }}">
</head>
<body>
<div class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<h1>Song control</h1>
</div>
{% if location %}
<div class="navbar-right">
<a href="{{ url_for('location', locid=location['_id']) }}"><p class="lead">{{ location['name'] }}</p></a>
</div>
{% endif %}
</div>
</div>
<hr>
<div class="container">
{% include 'messages.html' %}
</div>
<div class="container">
{% block content %}{% endblock %}
</div>
</body>
</html>

View File

@@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% block content %}
<h3>Select submenu</h3>
<div class="card-group">
<div class="card">
<div class="card-body">
<h5 class="card-title">Radio control</h5>
<a href="{{ url_for('location_radio', locid=location['_id']) }}"><button type="button" class="btn btn-primary">Go</button></a>
</div>
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title">Song request</h5>
<a href="{{ url_for('location_request', locid=location['_id']) }}"><button type="button" class="btn btn-primary">Go</button></a>
</div>
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title">Location config</h5>
<a href="{{ url_for('location_config', locid=location['_id']) }}"><button type="button" class="btn btn-primary">Go</button></a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,40 @@
{% extends 'base.html' %}
{% block content %}
<h4>Configuration for {{ location.name }}</h4>
{% if offline %}
<div class="alert alert-warning" role="alert">
Player has not been online for more than one minute, public controls have been disabled. Settings in here can still be changed.
</div>
{% endif %}
<hr>
<h5>Enable radio control</h5>
<form method="POST">
<div class="form-check form-switch">
<input class="form-check-input" name='enable' type="checkbox" role="switch" id="enable-radio" {% if location.config.enable_radio %}checked{% endif %}>
</div>
<input type="hidden" value="enable-radio" name="type">
<input type="submit" value="Apply" name="request" class="btn btn-primary">
</form>
<hr>
<h5>Enabled radio stations</h5>
<form method="POST">
{% for station in stations %}
<div class="form-check form-switch">
<input class="form-check-input" name='station' value="{{ station }}" type="checkbox" role="switch" id="{{ station }}" {% if ObjectId(station) in location.config.enabled_stations %}checked{% endif %}>
<label class="form-check-label" for="{{ station }}">{{ stations[station]['title'] }}</label>
</div>
{% endfor %}
<input type="hidden" value="enabled-stations" name="type">
<input type="submit" value="Apply" name="request" class="btn btn-primary">
</form>
<hr>
<h5>Enable song requests</h5>
<form method="POST">
<div class="form-check form-switch">
<input class="form-check-input" name='enable' type="checkbox" role="switch" id="enable-requests" {% if location.config.enable_requests %}checked{% endif %} disabled>
<label class="form-check-label" for="enable-requests">Feature is not available yet</label>
</div>
<input type="hidden" value="enable-requests" name="type">
<input type="submit" value="Apply" name="request" class="btn btn-primary">
</form>
{% endblock %}

View File

@@ -0,0 +1,59 @@
{% extends 'base.html' %}
{% block content %}
<meta http-equiv="refresh" content="60">
<h5>Radio station control</h5>
{% if not enabled %}
<div class="card text-center">
<div class="card-header">
{% if current %}Should be playing{% else %}Should be playing nothing{% endif %}
</div>
<div class="card-body">
<h5 class="card-title">{% if current %}{{ stations[current]['title'] }}{% else %}No control until player is enabled{% endif %}</h5>
<h6 class="card-subtitle mb-2 text-muted">{% if current %}{{ stations[current]['genre'] }}{% endif %}</h6>
<p class="card-text">{% if current %}{{ stations[current]['description'] }}{% endif %}</p>
</div>
<div class="card-footer bg-warning">
<p>Control of player has been disabled or connection to player has been lost</p>
</div>
</div>
{% else %}
<div class="card text-center">
<div class="card-header">
{% if current %}Now playing{% else %}Playing nothing{% endif %}
</div>
<div class="card-body">
<h5 class="card-title">{% if current %}{{ stations[current]['title'] }}{% else %}Select a radio station from below to play{% endif %}</h5>
<h6 class="card-subtitle mb-2 text-muted">{% if current %}{{ stations[current]['genre'] }}{% endif %}</h6>
<p class="card-text">{% if current %}{{ stations[current]['description'] }}{% endif %}</p>
{% if current %}
<form method="POST">
<input type="hidden" value="stop-stream" name="type">
<input type="submit" value="Stop" name="request" class="btn btn-danger">
</form>
{% endif %}
</div>
</div>
<hr>
<div class="row row-cols-1 row-cols-md-3 g-2">
{% for station in stations %}
{% if ObjectId(station) in location.config.enabled_stations and station != current %}
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ stations[station]['title'] }}</h5>
<h6 class="card-subtitle mb-2 text-muted">{{ stations[station]['genre'] }}</h6>
<p class="card-text">{{ stations[station]['description'] }}</p>
<form method="POST">
<input type="hidden" value="select-station" name="type">
<input type="hidden" value="{{ station }}" name="station">
<input type="submit" value="Play" name="request" class="btn btn-primary">
</form>
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,9 @@
{% extends 'base.html' %}
{% block content %}
<h4>Song request for {{ location.name }}</h4>
{% if location.config.enable_requests is false %}
<p>Song request is disabled by site administrator</p>
{% else %}
<p>Page content here</p>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% block content %}
<div class="card-columns">
{% for location in locations %}
<div class="card">
<div class="card-body">
<h5 class="card-title">{{ locations[location] }}</h5>
<a href="/location/{{ location }}"><button type="button" class="btn btn-primary">Go</button></a>
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,7 @@
{% for message in get_flashed_messages() %}
<meta http-equiv="refresh" content="6">
<div class="alert alert-primary" role="alert">
{{ message }}
</div>
<hr>
{% endfor %}

View File

@@ -0,0 +1,6 @@
FROM python:3
WORKDIR /usr/src/app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY control.py .
CMD ["python3", "./control.py"]

View File

@@ -0,0 +1,189 @@
import time
import requests
from random import randint
import logging
from os import environ
radio_name = environ.get('radio_name', default='controlled_radio')
player_name = environ.get('player_name', default='controlled_player')
player_id = environ.get('playerid', default='6307651e660410d3678d1fa3')
class Player:
def __init__(self, name):
import docker
self.name = name
self.ct = None
self.docker = docker
logging.debug('Setting up docker sock')
self.client = docker.DockerClient(base_url='unix:///var/run/docker.sock')
self.check_exists()
def start_new(self, stream, restart='no', labels=None):
if labels is None:
labels = {}
if self.ct:
logging.info('Not starting a new container under this class object because a container already exists')
return
if restart != 'no' and restart != 'always':
logging.info('Not a valid restart policy, must be always or no')
return
logging.debug('Running new container')
self.ct = self.client.containers.run('4grxfq/player',
name=self.name,
devices=['/dev/snd'],
environment={'FF_URL': stream},
restart_policy={'Name': restart},
labels=labels,
detach=True)
def start(self):
if not self.ct:
logging.info('Cannot start container under this class because no container has been created yet')
return
logging.debug('Starting existing container object')
self.ct.start()
def stop(self):
if not self.ct:
logging.info('Not stopping container because no such object exists')
return
logging.debug('Stopping container')
self.ct.stop()
def remove(self):
if not self.ct:
logging.info('Not removing container because no such object exists')
return
logging.debug('Deleting container')
self.ct.remove(force=True)
self.ct = None
def check_exists(self):
try:
logging.debug('Checking if container already exists under given name')
self.ct = self.client.containers.get(self.name)
return True
except self.docker.errors.NotFound:
logging.debug('Container does not exist continuing clean')
self.ct = None
return False
def check_status(self):
if not self.ct:
logging.info('Cannot query container because no such object exists')
return None
self.ct.reload()
if self.ct.status == 'running':
logging.info('Container is running')
return True
else:
logging.info('Container is not running')
return False
def get_labels(self):
if not self.ct:
logging.info('Cannot query container because no such object exists')
return None
self.ct.reload()
return self.ct.labels
def request_config(p_id, stat):
try:
logging.debug('Requesting new config from controller')
r = requests.post(f'http://192.168.99.20:5000/players/{p_id}/config', timeout=9, json=stat)
if r.status_code != 200:
logging.error('Config request not code 200, reporting')
return None
logging.debug(r.json())
return r.json()
except requests.exceptions.RequestException as exc:
logging.error(f'Requesting config excepted: {exc}')
# def generate_status(play, radi):
def generate_status(radi):
# give the player and radio objects to generate the status from
# stat = {'radio': {}, 'player': {}}
stat = {'radio': {}}
# if play.check_status(): # if player container exists, it removes itself otherwise
# stat['player']['playing'] = True
# else:
# stat['player']['playing'] = False
#
# if play.get_labels() is not None:
# stat['player']['labels']['stationId'] = play.get_labels().get('requestId')
# stat['player']['labels']['url'] = play.get_labels().get('url')
# stat['player']['labels']['cookie'] = play.get_labels().get('cookie')
# else:
# stat['player']['labels'] = None
if radi.check_status(): # check if radio is playing
stat['radio']['playing'] = True
else:
stat['radio']['playing'] = False
radi_labels = radi.get_labels()
if radi_labels is not None:
stat['radio']['stationId'] = radi_labels.get('stationId', '')
stat['radio']['url'] = radi_labels.get('url', '')
stat['radio']['cookie'] = radi_labels.get('cookie', '')
logging.debug(stat)
return stat
if __name__ == '__main__':
radio = Player(radio_name)
# player = Player(player_name)
while True:
# status = generate_status(player, radio)
status = generate_status(radio)
config = request_config(player_id, status)
if config:
if config.get('type') == 'radio':
# player.remove()
if config.get('stationId') == status['radio'].get('stationId'):
radio.start()
continue
else:
radio.remove()
radio.start_new(config.get('url'), restart='always', labels={'url': config.get('url'),
'stationId': config.get('stationId'),
'cookie': config.get('cookie')})
continue
# elif config.get('type') == 'request':
# radio.stop()
# player.start_new(config.get('url'), restart='no', labels={'url': config.get('url'),
# 'requestId': config.get('requestId'),
# 'cookie': config.get('cookie')})
elif config.get('type') == 'silence':
# player.remove()
radio.stop()
continue
else:
logging.debug('Nothing changed from previous request')
logging.debug('Sleeping')
time.sleep(randint(3, 6))

View File

@@ -0,0 +1,9 @@
version: "2"
services:
controller:
image: 4grxfq/controller:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
playerid: '6307651e660410d3678d1fa3'
restart: always

View File

@@ -0,0 +1,2 @@
docker
requests

View File

@@ -0,0 +1,3 @@
#!/bin/bash
docker build . -t 4grxfq/controller:latest
docker push 4grxfq/controller:latest

View File

@@ -0,0 +1,3 @@
FROM alpine:latest
RUN apk add --update alsa-utils ffmpeg && rm -rf /var/cache/apk/*
CMD sh -c 'ffmpeg -i $FF_URL -c copy -f asf - | ffplay - -nodisp -autoexit'

View File

@@ -4,7 +4,7 @@ from argon2 import PasswordHasher # PIP: argon2-cffi
sys.tracebacklimit = 0 # disable tracebacks to hide potential secrets
ph = PasswordHasher() # setup password hasher object with default parameters
hash = "$argon2id$v=19$m=65536,t=32,p=4$OvFXwX3buE9xYLKPGs2NOA$UEtxEwOI8VZ9WwWMGaySAhBdgP+zuLJIzCz4+Xt1YDw" example hash
hash = "$argon2id$v=19$m=65536,t=32,p=4$OvFXwX3buE9xYLKPGs2NOA$UEtxEwOI8VZ9WwWMGaySAhBdgP+zuLJIzCz4+Xt1YDw" # example hash
peers = {'CORE1': {'endpoint': '1.1.1.1', 'status': True}, 'CORE2': {'endpoint': '1.1.1.2', 'status': False}} # temporary testing dict

View File

@@ -0,0 +1,25 @@
# Docker Mutual TLS
This script generates a linked mutual TLS keys and certificates
## Usage
Run the script and give the fqdn of the host you are setting up. Then the script will leave 4 files. server-key.pem server-cert.pem client-key.pem client-cert.pem. The other files are automatically cleaned up
## Requirements
Bash and openssl
## Install
- First of all make sure that systemd is not hijacking the commandline options, if so on Debian the system service is at /usr/lib/systemd/system/docker.service and remove the commandline options which are overridden in the configuration file below.
- Copy the configuration of the codeblock below to /etc/docker/daemon.json
- Install the certificates in the correct locations
- restart the systemd service (all containers will be restarted as well!!!)
```
{
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"],
"tls": true,
"tlscacert": "/etc/docker/ca.pem",
"tlscert": "/etc/docker/server-cert.pem",
"tlskey": "/etc/docker/server-key.pem",
"tlsverify": true
}
```

View File

@@ -0,0 +1,18 @@
echo -n "What is the FQDN of the desired server?: "
read fqdn
echo "Using FQDN: $fqdn"
openssl genrsa -out ca-key.pem 4096
yes "" | openssl req -new -x509 -days 3650 -key ca-key.pem -sha256 -out ca.pem
openssl genrsa -out server-key.pem 4096
openssl req -subj "/CN=$fqdn" -addext "subjectAltName = DNS:$fqdn" -addext "extendedKeyUsage = serverAuth" -sha256 -new -key server-key.pem -out server.csr
openssl x509 -req -days 3650 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem -out server-cert.pem
openssl genrsa -out client-key.pem 4096
openssl req -subj "/CN=client" -addext "extendedKeyUsage = clientAuth" -new -key client-key.pem -out client.csr
openssl x509 -req -days 3650 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -out client-cert.pem
rm -v client.csr server.csr ca.pem ca-key.pem
echo "Finished creating certificate pairs"

Binary file not shown.

View File

@@ -0,0 +1,5 @@
@echo off
pyinstaller --clean --splash logo_s.png --onefile .\makkelijk_rapport.py
rd /S /Q build
del /F /Q makkelijk_rapport.spec
pause

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -1,6 +1,9 @@
import pyperclip
import pyi_splash
from time import sleep
from sys import exit
gebied = ['FUS', 'BLK', 'DBP', 'DKW', 'KKP', 'PAL'] # positioning matters!
gebied = ['FUS', 'BLK', 'DBP', 'DKW', 'KKP', 'PAL'] # positioning matters for excel sheet!
def ring_the_bell(n=1):
for n in range(n):
@@ -8,68 +11,107 @@ def ring_the_bell(n=1):
def check_correct_copy(clip):
# check if copied correctly
if clip[0:4] == '0000':
if clip[0:3] == '000':
return True
print('!!! You copied the menu incorrectly. See https://git.ventilaar.nl/ventilaar/slg_tools/src/branch/master/makkelijk_uurlijks_rapport/kopie_voorbeeld.png for an example !!!')
print('!!! Incorrect copy, please start at 000 and finish at PAL colli !!!')
return False
def nested_list_from_bare_copy(clip):
# create nested list with values based on position
cliplines = []
for x in clip.split('\r\n'):
line = []
for i in x.split(' '):
if i:
count = 0
line.append(i)
elif count == 12:
count = 0
line.append('0')
else:
count = count + 1
cliplines.append(line)
return cliplines
try:
for x in clip.split('\r\n'): # voor elke nieuwe regel
line = [] # lege regel aanmaken
for i in x.split(' '): #
if i: # positie is niet leeg
count = 0
line.append(i)
elif count == 12: # te veel lege posities ontvangen, kolom is leeg en dus 0
count = 0
line.append('0')
else: # positie is leeg
count = count + 1
cliplines.append(line) # regel is compleet toevoegen aan geneste lijst
return cliplines # geneste lijst teruggeven
except IndexError:
print('!!! Incorrect copy, please start at 000 and finish at PAL colli !!!')
return False
def nested_list_to_dict(clip):
# create dictionary based on gebied for a better overview of the data
# create dictionary based on gebied for a better overview of the data
data = {}
data['totaal_colli'] = 0
colli = 0
for x in clip:
if x[2] in gebied:
if len(x) != 8:
print('!!! Something went wrong with assuming the empty rows. Please double check manually !!!')
data[x[2]] = {}
data[x[2]]['totaal'] = int(x[3])
data[x[2]]['gedaan'] = int(x[4])
data[x[2]]['tedoen'] = int(x[5]) + int(x[6])
data['totaal_colli'] = data['totaal_colli'] + int(x[-1])
return data
try:
for x in clip: # voor elke gebied
if x[2] in gebied: # als gebied van ons is
if len(x) != 8: # totaal aantal colomen kloppen niet iets is fout gegeaan in nested_list_from_bare_copy()
print('!!! Something went wrong with assuming the empty rows. Please double check manually !!!')
data[x[2]] = {} # genereer het verzamelgebied
data[x[2]]['totaal'] = int(x[3]) # kopieer totaal regels kolom
data[x[2]]['gedaan'] = int(x[4]) # kopieer ingenomen regels kolom
data[x[2]]['tedoen'] = int(x[5]) + int(x[6]) # tel te plannen en gepland kolomen bijelkaar op
data['totaal_colli'] = data['totaal_colli'] + int(x[-1]) # tel de collis van alle gebieden bijelkaar op
return data
except IndexError:
print('!!! Incorrect copy, please start at 000 and finish at PAL colli !!!')
return False
def generate_clipboard(data):
# generate clipboard
copyhand = ''
for x in gebied:
copyhand = f"{copyhand}{data[x]['gedaan']}\t{data[x]['tedoen']}\t"
return f"{copyhand}{data['totaal_colli']}"
copyhand = '' # start met een leeg hand
for x in gebied: # voor elk gebied
if x not in data: # Gebied komt morgen (nog) niet voor
copyhand = f"{copyhand}0\t0\t" # Zet hand bij (nog) niet voorkomend gebied op 0
else:
copyhand = f"{copyhand}{data[x]['gedaan']}\t{data[x]['tedoen']}\t" # voeg de juiste data toe in de hand
return f"{copyhand}{data['totaal_colli']}" # hand afsluiten met totaal colli
def main():
try:
paste = pyperclip.waitForNewPaste()
if not check_correct_copy(paste):
return
nested_list = nested_list_from_bare_copy(paste)
if not nested_list:
return
nested_dict = nested_list_to_dict(nested_list)
if not nested_dict:
return
copyhand = generate_clipboard(nested_dict)
print(copyhand)
pyperclip.copy(copyhand)
ring_the_bell()
except KeyboardInterrupt:
print('Exiting')
exit(0)
if __name__ == '__main__':
print('Listening for new clipboard updates. When you hear a bell you copied correctly, the resulting conversion has replaced your clipboard')
print('Welcome to the easy copy tool version 2.1!')
print('Listening for copy events, when you copy correctly a bell will ring.')
print('')
print('Copy example: https://git.ventilaar.nl/ventilaar/random-scripts/src/branch/master/makkelijk_uurlijks_rapport/kopie_voorbeeld.png')
print('Source code: https://git.ventilaar.nl/ventilaar/random-scripts/src/branch/master/makkelijk_uurlijks_rapport')
print('-'*79)
pyi_splash.close()
while True:
try:
paste = pyperclip.waitForNewPaste()
if not check_correct_copy(paste):
continue
copyhand = generate_clipboard(nested_list_to_dict(nested_list_from_bare_copy(paste)))
print(copyhand)
pyperclip.copy(copyhand)
ring_the_bell()
except KeyboardInterrupt:
print('Exiting')
exit(0)
main()
except pyperclip.PyperclipWindowsException:
print('Windows is locked, retrying clipboard monitoring in 6 seconds')
sleep(6)

8
matrix-notify/README.md Normal file
View File

@@ -0,0 +1,8 @@
# Matrix Notify
Simple script to notify me in a special m matrix room regarding push messages.
# Use
change the roomId in the script and link to the correct homeserver. Change the AS token to the correct one generated and set-up in your Synapse application service.
# Requires python libraries
- requests

47
matrix-notify/message.py Normal file
View File

@@ -0,0 +1,47 @@
def send_message(service, message):
import requests
asToken = 'private'
roomId = '!3rhj02839hr023r:example.com'
homeserver = 'matrix.ventilaar.net'
apiUrl = f'https://{homeserver}/_matrix/client/v3/rooms/{roomId}/send/m.room.message'
headers = {
'Authorization': f'Bearer {asToken}',
'Content-Type': 'application/json'
}
event = {
'msgtype': 'm.text',
'body': f'Message from: {service} | Message: {message}',
'format': 'org.matrix.custom.html',
'formatted_body': f'Message from: <b>{service}</b><br><code>{message}</code>'
}
try:
r = requests.post(apiUrl, headers=headers, json=event)
if r.status_code != 200:
print(f'Failed to send message, got exit code {r.status_code}')
return False
return True
except requests.exceptions.RequestException as e:
print(f'Some unexpected thing happened: {e}')
if __name__ == "__main__":
import sys
args = sys.argv[1:]
if len(args) != 2:
print('Usage: scriptname.py "service" "message"')
elif len(args) == 1:
send_message('selftest', 'This is a message to test the script itself, it got not parameters!')
if send_message(args[0], args[1]):
exit(0)
exit(1)

Some files were not shown because too many files have changed in this diff Show More