@@ -0,0 +1,3 @@ | |||
dist/ | |||
db.sqlite | |||
build/ |
@@ -0,0 +1,3 @@ | |||
dist/ | |||
db.sqlite | |||
build/ |
@@ -0,0 +1,75 @@ | |||
# focal amd64 as of 2020-03-10 | |||
FROM ubuntu@sha256:d050ed7278c16ff627e4a70d7d353f1a2ec74d8a0b66e5a865356d92f5f6d87b | |||
#######################################################################33 | |||
## Mirror Setup | |||
## - option to use local mirror to speed build | |||
#######################################################################33 | |||
ARG UBUNTU_MIRROR=http://archive.ubuntu.com/ubuntu | |||
RUN echo "deb $UBUNTU_MIRROR focal main universe restricted multiverse" > /etc/apt/sources.list.new && \ | |||
echo "deb $UBUNTU_MIRROR focal-updates main universe restricted multiverse" >> /etc/apt/sources.list.new && \ | |||
echo "deb $UBUNTU_MIRROR focal-security main universe restricted multiverse" >> /etc/apt/sources.list.new && \ | |||
echo "deb $UBUNTU_MIRROR focal-backports main universe restricted multiverse" >> /etc/apt/sources.list.new && \ | |||
mv /etc/apt/sources.list.new /etc/apt/sources.list | |||
#######################################################################33 | |||
## Versions | |||
#######################################################################33 | |||
# master as of 2020-03-10 | |||
ARG PYENV_COMMIT=df9fa1dc30b6448ef8605e2c2d4dfc2a94d6a35d | |||
ARG PYTHON_VERSION=3.8.1 | |||
#######################################################################33 | |||
## Packages | |||
#######################################################################33 | |||
ARG DEBIAN_FRONTEND=noninteractive | |||
RUN apt update && \ | |||
apt upgrade -y && \ | |||
apt install -y \ | |||
build-essential \ | |||
curl \ | |||
git \ | |||
libbz2-dev \ | |||
libffi-dev \ | |||
liblzma-dev \ | |||
libncurses5-dev \ | |||
libncursesw5-dev \ | |||
libreadline-dev \ | |||
libsqlite3-dev \ | |||
libssl-dev \ | |||
llvm \ | |||
locales \ | |||
make \ | |||
python-openssl \ | |||
tk-dev \ | |||
wget \ | |||
xz-utils \ | |||
zlib1g-dev \ | |||
&& \ | |||
mkdir -p /var/app && \ | |||
git clone https://github.com/pyenv/pyenv.git /usr/local/pyenv && \ | |||
cd /usr/local/pyenv && \ | |||
git checkout $PYENV_COMMIT | |||
ENV PYENV_ROOT /usr/local/pyenv | |||
ENV PATH $PYENV_ROOT/bin:$PATH | |||
#######################################################################33 | |||
## Python | |||
#######################################################################33 | |||
RUN pyenv install $PYTHON_VERSION && \ | |||
pyenv global $PYTHON_VERISON && \ | |||
pip3 install pipenv | |||
#######################################################################33 | |||
## Install Deps | |||
#######################################################################33 | |||
ADD ./Pipfile ./Pipfile.lock /var/app/ | |||
WORKDIR /var/app | |||
RUN pipenv install --python $(which python3) | |||
#######################################################################33 | |||
## Install App | |||
#######################################################################33 | |||
ADD ./* /var/app/ |
@@ -0,0 +1,15 @@ | |||
export PYTHONPATH =: $(PWD) | |||
default: docker | |||
peinstall: | |||
pipenv install --python $(shell which python3) | |||
serve: | |||
pipenv run python ./bin/tvidd | |||
clean: | |||
rm -rf build dist tvid.egg-info | |||
docker: | |||
docker build --build-arg UBUNTU_MIRROR="http://ubuntu.datavi.be/ubuntu" . |
@@ -0,0 +1,15 @@ | |||
[[source]] | |||
name = "pypi" | |||
url = "https://pypi.org/simple" | |||
verify_ssl = true | |||
[dev-packages] | |||
[packages] | |||
bottle = "*" | |||
pprint = "*" | |||
sqlalchemy = "*" | |||
bottle-sqlalchemy = "*" | |||
[requires] | |||
python_version = "3.8" |
@@ -0,0 +1,50 @@ | |||
{ | |||
"_meta": { | |||
"hash": { | |||
"sha256": "7216cb0c490cfe6852be9bc21eeac5f3f08cf65b39f7316fae3580a387938016" | |||
}, | |||
"pipfile-spec": 6, | |||
"requires": { | |||
"python_version": "3.8" | |||
}, | |||
"sources": [ | |||
{ | |||
"name": "pypi", | |||
"url": "https://pypi.org/simple", | |||
"verify_ssl": true | |||
} | |||
] | |||
}, | |||
"default": { | |||
"bottle": { | |||
"hashes": [ | |||
"sha256:0819b74b145a7def225c0e83b16a4d5711fde751cd92bae467a69efce720f69e", | |||
"sha256:43157254e88f32c6be16f8d9eb1f1d1472396a4e174ebd2bf62544854ecf37e7" | |||
], | |||
"index": "pypi", | |||
"version": "==0.12.18" | |||
}, | |||
"bottle-sqlalchemy": { | |||
"hashes": [ | |||
"sha256:ba6127f3aff2b78649781adbbee65518233dc481e9f9e32e3b050d1ad9551c17" | |||
], | |||
"index": "pypi", | |||
"version": "==0.4.3" | |||
}, | |||
"pprint": { | |||
"hashes": [ | |||
"sha256:c0fa22d1462351671ca098e9779bb26a23880011e93eea5f199a150ee7b92a16" | |||
], | |||
"index": "pypi", | |||
"version": "==0.1" | |||
}, | |||
"sqlalchemy": { | |||
"hashes": [ | |||
"sha256:b92d2de62e43499d85b1780274d1b562e5159c7996f6f04a9bb46cf681ced45f" | |||
], | |||
"index": "pypi", | |||
"version": "==1.3.14" | |||
} | |||
}, | |||
"develop": {} | |||
} |
@@ -0,0 +1,18 @@ | |||
# tvid | |||
This is an app that lets you set all the kiosk/display TVs in your | |||
organization to the same URL. Each will be cookied with a unique ID that | |||
will display on each display in big letters, with no preconfiguration | |||
required. | |||
Log in to the admin panel and enter the URL for that TV ID, and within 60 | |||
seconds, that display will bounce to that URL, or any other time it turns | |||
on. | |||
# todo | |||
* make display id animate: https://codepen.io/stezu/pen/cmLrI | |||
# author | |||
sneak <[sneak@sneak.berlin](mailto:sneak@sneak.berlin)> |
@@ -0,0 +1,9 @@ | |||
#!/usr/bin/env python3 | |||
from tvid import serve | |||
def main(): | |||
serve() | |||
if __name__ == "__main__": | |||
main() |
@@ -0,0 +1,15 @@ | |||
from setuptools import setup, find_packages | |||
setup( | |||
name='tvid', | |||
version='0.0.1', | |||
packages=find_packages(), | |||
license='WTFPL', | |||
long_description=open('README.md').read(), | |||
entry_points = { | |||
'console_scripts': [ | |||
'tvidd = tvid.tvid:serve' | |||
], | |||
}, | |||
) | |||
@@ -0,0 +1,2 @@ | |||
from .server import serve | |||
@@ -0,0 +1,16 @@ | |||
from sqlalchemy import Column, Sequence, Integer, String, DateTime | |||
from sqlalchemy.ext.declarative import declarative_base | |||
from .server import SQLBASE | |||
Base = SQLBASE | |||
# YAGNI just set admin pw in an env var for now and dont support changing it | |||
# yet | |||
#class Settings(Base): | |||
class TV(Base): | |||
__tablename__ = 'tvs' | |||
id = Column(Integer, Sequence('id_seq'), primary_key=True) | |||
displayid = Column(String(20)) | |||
lastSeen = Column(DateTime) | |||
target = Column(String(255)) |
@@ -0,0 +1,130 @@ | |||
#!/usr/bin/env python3 | |||
# this is a quick hack, to be improved later. trying to do the simplest | |||
# thing that will possibly work. | |||
## # TODO | |||
## | |||
## * put page content into templates/static files | |||
## * clean up FIXME | |||
import bottle | |||
from bottle import route, run, request, response, redirect | |||
from bottle import HTTPError, template | |||
from sqlalchemy.ext.declarative import declarative_base | |||
from bottle.ext import sqlalchemy | |||
from pprint import pprint | |||
from sqlalchemy import create_engine | |||
import os | |||
import random | |||
import string | |||
from datetime import datetime | |||
VERSION = '0.0.1' | |||
PORT = os.environ.get('PORT', 8080) | |||
DEBUG = os.environ.get('DEBUG', False) | |||
SQLITE_FILENAME = os.environ.get('SQLITE_FILENAME','/data/db.sqlite') | |||
DATABASE_URL = os.environ.get('DATABASE_URL','sqlite:///' + SQLITE_FILENAME) | |||
ADMINPSK = os.environ.get('ADMINPSK','hunter2') | |||
# sorry for global | |||
SQLBASE = declarative_base() | |||
# FIXME make this skip letters not in base58 | |||
def randomUpper(count): | |||
return ''.join(random.choice(string.ascii_uppercase) for _ in range(count)) | |||
# FIXME maybe make the IDs longer | |||
def genRandomTVID(): | |||
tvid = str(randomUpper(3) + "-" + randomUpper(3)) | |||
return tvid | |||
def serve(): | |||
app = bottle.Bottle() | |||
# pull in models | |||
from .db import TV | |||
engine = create_engine(DATABASE_URL, echo=True) | |||
plugin = sqlalchemy.Plugin( | |||
engine, # SQLAlchemy engine created with create_engine function. | |||
SQLBASE.metadata, # SQLAlchemy metadata, required only if create=True. | |||
keyword='db', # Keyword used to inject session database in a route (default 'db'). | |||
create=True, # If it is true, execute `metadata.create_all(engine)` when plugin is applied (default False). | |||
commit=True, # If it is true, plugin commit changes after route is executed (default True). | |||
use_kwargs=False | |||
) | |||
app.install(plugin) | |||
# here we cookie them if they have no cookie, then redirect them to the | |||
# cookie'd value (whether preexisting or new). | |||
@app.get('/') | |||
def indexpage(): | |||
c = request.get_cookie("tvid") | |||
if c: | |||
# redirect | |||
redirect('/tv/' + c) | |||
else: | |||
newid = genRandomTVID() | |||
response.set_cookie("tvid", newid) | |||
redirect('/tv/' + newid) | |||
@app.get('/style.css') | |||
def stylesheet(): | |||
return template('style') | |||
# here we check to see if they have a redirect URL in the db. if they do | |||
# we send them there. if they don't, we display their ID really big, | |||
# reloading the page once per hour. | |||
@app.get('/tv/<displayid>') | |||
def tvpage(db, displayid=None): | |||
# FIXME regex check id to make sure displayid is right format, | |||
# return error if not | |||
if id is None: | |||
return template('nocookie') | |||
# check db for tv id | |||
tv = db.query(TV).filter_by(displayid=displayid).first() | |||
if tv: | |||
tv.lastSeen = datetime.now() | |||
db.add(tv) | |||
if tv.target: | |||
redirect(tv.target) | |||
else: | |||
return template('displayid', id=displayid, version=VERSION) | |||
else: | |||
# otherwise, just show their display ID bigly and keep them | |||
# bouncing periodically until some admin configures them | |||
# a target: | |||
# update lastseen here: | |||
newtv = TV(displayid=displayid,lastSeen=datetime.now()) | |||
db.add(newtv) | |||
return template('displayid', id=displayid, version=VERSION) | |||
# here we display the administration list of TVs if logged in | |||
# if logged out then redirect to /login | |||
# FIXME make this use sessions instead of just storing PSK in a cookie | |||
# https://bottlepy.org/docs/dev/recipes.html | |||
@app.get('/admin') | |||
def adminpage(): | |||
c = request.get_cookie("adminpw") | |||
# FIXME check their 'adminpw' cookie here, redirect to /loign | |||
return "Hello World!" | |||
# here we ask for a password and cookie them and bounce them back to /admin | |||
@app.get('/login') | |||
def checklogin(): | |||
raise NotImplementedError() | |||
#response.set_cookie("adminpw", whatever) | |||
redirect('/login') | |||
@app.get('/logut') | |||
def logout(): | |||
response.set_cookie("adminpw", "") | |||
redirect('/login') | |||
app.run(host='0.0.0.0', port=PORT, debug=DEBUG) |
@@ -0,0 +1,20 @@ | |||
<html> | |||
<head> | |||
<title>tv info page</title> | |||
<meta http-equiv="refresh" content="60"> | |||
<link rel="stylesheet" href="/style.css" /> | |||
</head> | |||
<body> | |||
<div id="main"> | |||
<p> | |||
<i>Display ID:</i> | |||
</p> | |||
<h1>{{id}}</h1> | |||
<small>(They're only letters. I like India, O like | |||
Oscar.)</small> | |||
<p><small>Powered by tvid v{{version}}</small></p> | |||
</div> | |||
</body> | |||
</html> |
@@ -0,0 +1,10 @@ | |||
<html> | |||
<head> | |||
<title>tv info page</title> | |||
<meta http-equiv="refresh" content="60; url=/"> | |||
<link rel="stylesheet" href="/style.css" /> | |||
</head> | |||
<body> | |||
<h1>Please enable cookies, they're required.</h1> | |||
</body> | |||
</html> |
@@ -0,0 +1,10 @@ | |||
body { | |||
font-family: sans-serif; | |||
width: 100%; | |||
text-align: center; | |||
font-size: 20pt | |||
} | |||
h1 { | |||
font-size: 48pt; | |||
} |