initial
This commit is contained in:
commit
3ae7b05b9b
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
dist/
|
||||||
|
db.sqlite
|
||||||
|
build/
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
dist/
|
||||||
|
db.sqlite
|
||||||
|
build/
|
75
Dockerfile
Normal file
75
Dockerfile
Normal file
@ -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/
|
15
Makefile
Normal file
15
Makefile
Normal file
@ -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" .
|
15
Pipfile
Normal file
15
Pipfile
Normal file
@ -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"
|
50
Pipfile.lock
generated
Normal file
50
Pipfile.lock
generated
Normal file
@ -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": {}
|
||||||
|
}
|
18
README.md
Normal file
18
README.md
Normal file
@ -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)>
|
9
bin/tvidd
Normal file
9
bin/tvidd
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from tvid import serve
|
||||||
|
|
||||||
|
def main():
|
||||||
|
serve()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
15
setup.py
Normal file
15
setup.py
Normal file
@ -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'
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
2
tvid/__init__.py
Normal file
2
tvid/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .server import serve
|
||||||
|
|
16
tvid/db.py
Normal file
16
tvid/db.py
Normal file
@ -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))
|
130
tvid/server.py
Normal file
130
tvid/server.py
Normal file
@ -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)
|
20
views/displayid.tpl
Normal file
20
views/displayid.tpl
Normal file
@ -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>
|
10
views/nocookie.tpl
Normal file
10
views/nocookie.tpl
Normal file
@ -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>
|
10
views/style.tpl
Normal file
10
views/style.tpl
Normal file
@ -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
Block a user