commit
3ae7b05b9b
15 changed files with 391 additions and 0 deletions
@ -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; |
|||
} |
Loading…
Reference in new issue