summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoruser@node5.net <user@node5.net>2026-06-23 00:39:08 +0200
committeruser@node5.net <user@node5.net>2026-06-23 00:39:08 +0200
commitf5ff82d7293b50ec412231e65b4480d6bf206fcc (patch)
tree451840c54bfbb501a5621a8324658fa8ae1d020f
parent7b8b244d074ca1639ae61332a086c78a22474857 (diff)
nixify repo, 2026 headline design, debug environment variables
-rw-r--r--flake.lock46
-rw-r--r--flake.nix49
-rw-r--r--pyproject.toml17
-rw-r--r--shell.nix22
-rw-r--r--src/get_program_for_pixel_flut.py (renamed from src/app.py)46
-rw-r--r--src/logger.py36
-rwxr-xr-xsrc/program_parser.py157
-rw-r--r--src/static/title_headline.pngbin12481 -> 2302 bytes
-rw-r--r--src/text_tv_bornhack.py47
9 files changed, 306 insertions, 114 deletions
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..16f7fc7
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,46 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1781607440,
+ "narHash": "sha256-rxO+uc/KFbSJp+pgyXRuAX6QlG9hJdnt0BXpEQRXY+U=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "3e41b24abd260e8f71dbe2f5737d24122f972158",
+ "type": "github"
+ },
+ "original": {
+ "id": "nixpkgs",
+ "type": "indirect"
+ }
+ },
+ "pyproject-nix": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1781812259,
+ "narHash": "sha256-uRqDouxg3b0EuOHQd1HhmFZouHebM7pz+H6EWAXd3FM=",
+ "owner": "nix-community",
+ "repo": "pyproject.nix",
+ "rev": "d9847acff422152a03764fd60c96ae0dd9f9fa73",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-community",
+ "repo": "pyproject.nix",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs",
+ "pyproject-nix": "pyproject-nix"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..b9933b9
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,49 @@
+{
+ description = "A basic flake using pyproject.toml project metadata";
+
+ inputs = {
+ pyproject-nix = {
+ url = "github:nix-community/pyproject.nix";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ };
+
+ outputs = { nixpkgs, pyproject-nix, ... }:
+ let
+ inherit (nixpkgs) lib;
+
+ project = pyproject-nix.lib.project.loadPyproject {
+ # Read & unmarshal pyproject.toml relative to this project root.
+ # projectRoot is also used to set `src` for renderers such as buildPythonPackage.
+ projectRoot = ./.;
+ };
+
+ # This example is only using x86_64-linux
+ pkgs = nixpkgs.legacyPackages.x86_64-linux;
+
+ python = pkgs.python3;
+
+ # Returns an attribute set that can be passed to `buildPythonPackage`.
+ attrs = project.renderers.buildPythonPackage { inherit python; };
+
+ pkg = python.pkgs.buildPythonPackage (attrs // {
+ meta = {
+ description = "Bornhack schedule web displayer with a teletext theme";
+ homepage = "https://text-tv.bornhack.node5.net/";
+ changelog = "https://git.node5.net/misc/text-tv.bornhack.node5.net/log/";
+ mainProgram = "node5_text-tv_bornhack";
+ };
+ propagatedBuildInputs = (attrs.propagatedBuildInputs or []) ++ [ pkgs.git ]; # Make git binary available
+ postInstall = ''
+ echo $src
+ echo $out
+ cp -r $src/src/static/ $out/${python.sitePackages}
+ cp -r $src/src/templates/ $out/${python.sitePackages}
+ '';
+ });
+ in
+ {
+ packages.x86_64-linux.default = pkg;
+ pythonPath = "${python.pkgs.makePythonPath attrs.dependencies}:${pkg}/${python.sitePackages}";
+ };
+}
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..3c1cbf6
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,17 @@
+[project]
+name = "text-tv_bornhack"
+version = "0.1.0"
+description = "Flask project to host text-tv bornhack, "
+
+# define any Python dependencies
+dependencies = [
+ "Flask~=3.1",
+ "piexif~=1.1",
+ "geopy~=2.4",
+ "pillow~=12.2",
+]
+
+# define the CLI executable
+# Here, we define the entry point to be the 'main()' function in the module 'app/main.py'
+[project.scripts]
+node5_text-tv_bornhack = "text_tv_bornhack:main"
diff --git a/shell.nix b/shell.nix
new file mode 100644
index 0000000..04e4f10
--- /dev/null
+++ b/shell.nix
@@ -0,0 +1,22 @@
+{ pkgs ? import <nixpkgs> {} }:
+
+pkgs.mkShell {
+ buildInputs = [
+ pkgs.gnumake
+ pkgs.python312 # Specify the Python version (adjust if needed)
+ pkgs.python312Packages.flask
+ pkgs.python312Packages.pyyaml
+ pkgs.python312Packages.markdown
+ pkgs.python312Packages.python-telegram-bot
+ pkgs.python312Packages.pygments
+ pkgs.python312Packages.tabulate # Format lists as ascii tables for comments
+ pkgs.sqlite # Database for comments
+ ];
+
+ shellHook = ''
+ # Runs when starting shell
+ '';
+}
+
+
+
diff --git a/src/app.py b/src/get_program_for_pixel_flut.py
index 977288b..5557184 100644
--- a/src/app.py
+++ b/src/get_program_for_pixel_flut.py
@@ -1,13 +1,8 @@
import logging
import os
-import datetime
-
-import flask
-import yaml
import program_parser
-
-#import db_handler
+from program_parser import Event
class CustomFormatter(logging.Formatter):
grey = "\x1b[90;20m"
@@ -41,29 +36,18 @@ stream_handler = logging.StreamHandler()
stream_handler.setFormatter(CustomFormatter())
logger.root.addHandler(stream_handler)
-app = flask.Flask(__name__, template_folder='templates', static_folder='static', static_url_path='')
-
-@app.route('/top')
-def top():
- return f'BornHack S105 {datetime.datetime.now().strftime("%a %d %b %R:%S")} '
-
-@app.route('/program.json')
-def program():
- events_to_show: list[Event] = program_parser.get_events_to_show(program_parser.get_program(), programs_to_show_count = 1000)
- #{event.title} {day} {start_time}-{end_time} {icon}
- json = [{'title': a.title, 'day': a.day, 'time': a.time, 'progress': a.progress, 'icon': a.icon} for a in events_to_show]
- return json
-'''
-@app.route('/news')
-def news():
- with open(news_file_path, "r") as news_file:
- return news_file.read().replace('\n', '<br>')
-'''
-@app.route('/text')
-def text() -> dict[str, str]:
- return {part: eval(part)() for part in ['top', 'program']}
-
-@app.route('/')
-def index():
- return flask.render_template('index.html', parts=text())
+events_to_show: list[Event] = program_parser.get_events_to_show(program_parser.get_program(), programs_to_show_count = 1000)
+#{event.title} {day} {start_time}-{end_time} {icon}
+programs_formatted_list = []
+last_event = None
+for event in events_to_show:
+ programs_formatted_list.append(f"{event.title} {event.time} {event.icon} {event.progress if event.progress > 0 else ''}")
+ if last_event and last_event.day != event.day:
+ # Day change
+ programs_formatted_list.append(f'\n\n{event.day}\n')
+ last_event = event
+
+program_formatted = '\n'.join([f"" for a in events_to_show ])
+program_formatted = '\n'.join(programs_formatted_list)
+print(program_formatted)
diff --git a/src/logger.py b/src/logger.py
new file mode 100644
index 0000000..aaa1441
--- /dev/null
+++ b/src/logger.py
@@ -0,0 +1,36 @@
+import logging
+import os
+
+
+class CustomFormatter(logging.Formatter):
+ grey = "\x1b[90;20m"
+ blue = "\x1b[34;20m"
+ yellow = "\x1b[33;20m"
+ red = "\x1b[31;20m"
+ bold_red = "\x1b[31;1m"
+ reset = "\x1b[0m"
+ format = "%(asctime)s,%(msecs)03d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s"
+
+ FORMATS = {
+ logging.DEBUG: grey + format + reset,
+ logging.INFO: blue + format + reset,
+ logging.WARNING: yellow + format + reset,
+ logging.ERROR: red + format + reset,
+ logging.CRITICAL: bold_red + format + reset
+ }
+
+ def format(self, record):
+ log_fmt = self.FORMATS.get(record.levelno)
+ formatter = logging.Formatter(log_fmt)
+ return formatter.format(record)
+
+logger = logging.getLogger(__name__)
+if os.environ.get('FLASK_DEBUG') == '1': # Local development
+ logger.root.setLevel(logging.DEBUG)
+else: # Production
+ logger.root.setLevel(logging.INFO)
+
+stream_handler = logging.StreamHandler()
+stream_handler.setFormatter(CustomFormatter())
+logger.root.addHandler(stream_handler)
+
diff --git a/src/program_parser.py b/src/program_parser.py
index 67103b1..66c0858 100755
--- a/src/program_parser.py
+++ b/src/program_parser.py
@@ -1,13 +1,10 @@
-#!/run/current-system/sw/bin/python
import os
import json
import datetime
-import copy
import dataclasses
+import logging
-input_file_path = './program.json'
-
-title_max_length = 34
+logger = logging.getLogger(__name__) # Set the logger name, to the name of the module
@dataclasses.dataclass
class Event:
@@ -22,89 +19,83 @@ class Event:
pretty: str = None
progress: int = None
-icons = {
- 'Startent at Info Desk' : "⭐",
- 'Speakers Tent': "🏕",
- 'Workshop Room': "🛠️",
- 'Bar Meetup Area': "🍺",
- 'Bar Area': "🍺",
- 'External': "🚗",
- 'Pyjam.as Village in Party Area': "🐍",
- 'Workshop Room (possibly outside of it)': "🛠️(🏞️)",
- 'Workshop Room (sofa area)': "🛠️(🛋️)",
- 'NWWC Village in Party Area': "🔊",
- 'Wellness area': "🛀",
- 'Online': "🌐",
-}
+class ProgramParser:
+ def __init__(self, program_path:os.PathLike, title_max_length:int=34, override_date:datetime.datetime|None=None):
+ self.program_path = program_path
+ self.title_max_length = title_max_length
+ self.override_date = override_date
+ self.icons = {
+ 'Startent at Info Desk' : "⭐",
+ 'Speakers Tent': "🏕",
+ 'Workshop Room': "🛠️",
+ 'Bar Meetup Area': "🍺",
+ 'Bar Area': "🍺",
+ 'External': "🚗",
+ 'Pyjam.as Village in Party Area': "🐍",
+ 'Workshop Room (possibly outside of it)': "🛠️(🏞️)",
+ 'Workshop Room (sofa area)': "🛠️(🛋️)",
+ 'NWWC Village in Party Area': "🔊",
+ 'Wellness area': "🛀",
+ 'Online': "🌐",
+ }
-def get_program() -> list[Event]:
- program: list[Event] = []
- # Parse program into data format (interpret datetime, and such)
- with open(input_file_path, "r") as json_file:
- program_raw = json.load(json_file)
- for event_raw in program_raw:
- event = Event()
- #print(event_raw["title"])
- for d in ["title", "room"]: # 1 to 1 mapping
- setattr(event, d, event_raw[d])
- event.start_date = datetime.datetime.strptime(event_raw["date"], "%Y-%m-%dT%H:%M:%S+00:00")
- event.start_date += datetime.timedelta(hours=2) # Hvad er vinter tid?
- end_bogus = datetime.datetime.strptime(event_raw["duration"],"%H:%M:%S")
- event.duration = datetime.timedelta(hours=end_bogus.hour, minutes=end_bogus.minute, seconds=end_bogus.second)
- event.end_date = event.start_date + event.duration
- #print('Start date: ', event["start_date"])
- #print("End date: ", event["end_date"])
- #print()
- program.append(event)
+ def get_program(self) -> list[Event]:
+ program: list[Event] = []
+ # Parse program into data format (interpret datetime, and such)
+ with open(self.program_path, "r") as json_file:
+ program_raw = json.load(json_file)
+ for event_raw in program_raw:
+ event = Event()
+ #print(event_raw["title"])
+ for d in ["title", "room"]: # 1 to 1 mapping
+ setattr(event, d, event_raw[d])
+ event.start_date = datetime.datetime.strptime(event_raw["date"], "%Y-%m-%dT%H:%M:%S+00:00")
+ event.start_date += datetime.timedelta(hours=2) # Hvad er vinter tid?
+ end_bogus = datetime.datetime.strptime(event_raw["duration"],"%H:%M:%S")
+ event.duration = datetime.timedelta(
+ hours=end_bogus.hour, minutes=end_bogus.minute, seconds=end_bogus.second)
+ event.end_date = event.start_date + event.duration
+ #print('Start date: ', event["start_date"])
+ #print("End date: ", event["end_date"])
+ #print()
+ program.append(event)
- program = sorted(program, key=lambda x: x.start_date)
- return program
+ program = sorted(program, key=lambda x: x.start_date)
+ return program
-def get_events_to_show(program: list[Event], programs_to_show_count=4) -> list[Event]:
- current_datetime = datetime.datetime.now()
- #current_datetime = datetime.datetime(year=2025, month=7, day=18, hour=15, minute=30) # DEBUG
- # Get events to show
- events_to_show = []
- for event in program:
- if len(events_to_show) >= programs_to_show_count:
- break
- if event.end_date > current_datetime:
- events_to_show.append(event)
+ def get_events_to_show(self, program: list[Event], programs_to_show_count=4) -> list[Event]:
+ current_datetime = datetime.datetime.now() if not self.override_date else self.override_date
+ #current_datetime = datetime.datetime(year=2025, month=7, day=18, hour=15, minute=30) # DEBUG
+ # Get events to show
+ events_to_show = []
+ for event in program:
+ if len(events_to_show) >= programs_to_show_count:
+ break
+ if event.end_date > current_datetime:
+ events_to_show.append(event)
- def format_program(event: Event, cli=False) -> str:
- if len(event.title) > title_max_length:
- event.title = f'{event.title[:title_max_length - 3]}...' # Truncate title by deleting was 1-3 chars and putting ...
- else:
- event.title += " " * (title_max_length - len(event.title)) # Pad title
- event.day = event.start_date.strftime('%a')
- start_time = event.start_date.strftime('%H:%M')
- end_time = event.end_date.strftime('%H:%M')
- event.icon = icons.get(event.room)
- if event.icon == None:
- event.icon = '?'
- print(f"NEW ICON NEEDED!!! for {event.room}")
- event.time = f"{start_time}-{end_time}"
- return f"{event.title} {event.day} {start_time}-{end_time} {event.icon}"
-
- for event_to_show in events_to_show:
- event_to_show.pretty = format_program(event_to_show)
- duration_sec = (event_to_show.end_date - event_to_show.start_date).total_seconds()
- progress_sec = (current_datetime - event_to_show.end_date).total_seconds()
- event_to_show.progress = 100 + (progress_sec / duration_sec) * 100
+ def format_program(event: Event, cli=False) -> str:
+ if len(event.title) > self.title_max_length:
+ # Truncate title by deleting was 1-3 chars and putting ...
+ event.title = f'{event.title[:self.title_max_length - 3]}...'
+ else:
+ event.title += " " * (self.title_max_length - len(event.title)) # Pad title
+ event.day = event.start_date.strftime('%a')
+ start_time = event.start_date.strftime('%H:%M')
+ end_time = event.end_date.strftime('%H:%M')
+ event.icon = self.icons.get(event.room)
+ if event.icon == None:
+ event.icon = '?'
+ logger.warning(f"NEW ICON NEEDED!!! for {event.room}")
+ event.time = f"{start_time}-{end_time}"
+ return f"{event.title} {event.day} {start_time}-{end_time} {event.icon}"
- return events_to_show
+ for event_to_show in events_to_show:
+ event_to_show.pretty = format_program(event_to_show)
+ duration_sec = (event_to_show.end_date - event_to_show.start_date).total_seconds()
+ progress_sec = (current_datetime - event_to_show.end_date).total_seconds()
+ event_to_show.progress = 100 + (progress_sec / duration_sec) * 100
-program: list[Event] = get_program()
-events_to_show: list[Event] = get_events_to_show(program)
+ return events_to_show
-'''
-# Convert to the output string
-events_to_show_string = '\n'.join([a.pretty for a in events_to_show])
-print(f"{events_to_show_string}\n")
-# Write to output file
-with open(tmp_file_path, 'w') as tmp_file:
- tmp_file.write(events_to_show_string)
-os.rename(tmp_file_path, output_file_path)
-print(f'Written to {output_file_path} length: {len(events_to_show_string)}')
-'''
diff --git a/src/static/title_headline.png b/src/static/title_headline.png
index e53e0cf..a6aada9 100644
--- a/src/static/title_headline.png
+++ b/src/static/title_headline.png
Binary files differ
diff --git a/src/text_tv_bornhack.py b/src/text_tv_bornhack.py
new file mode 100644
index 0000000..9514da2
--- /dev/null
+++ b/src/text_tv_bornhack.py
@@ -0,0 +1,47 @@
+import datetime
+import os
+
+import flask
+
+from program_parser import ProgramParser
+
+
+app = flask.Flask(__name__, template_folder='templates', static_folder='static', static_url_path='')
+
+program_path = os.environ["PROGRAM_PATH"] if "PROGRAM_PATH" in os.environ else './programs/current.json'
+override_date = datetime.datetime.fromisoformat(os.environ["OVERRIDE_DATE"]) if "OVERRIDE_DATE" in os.environ else None
+program_parser = ProgramParser(program_path=program_path, override_date=override_date)
+debug = 'PROD' not in os.environ
+
+
+@app.route('/top')
+def top():
+ now = datetime.datetime.now() if not override_date else override_date
+ return f'BornHack S105 {now.strftime("%a %d %b %R:%S")} '
+
+
+@app.route('/program.json')
+def program():
+ events_to_show: list[Event] = program_parser.get_events_to_show(program_parser.get_program(), programs_to_show_count = 1000)
+ #{event.title} {day} {start_time}-{end_time} {icon}
+ json = [{'title': a.title, 'day': a.day, 'time': a.time, 'progress': a.progress, 'icon': a.icon} for a in events_to_show]
+ return json
+
+
+@app.route('/text')
+def text() -> dict[str, str]:
+ return {part: eval(part)() for part in ['top', 'program']}
+
+
+@app.route('/')
+def index():
+ return flask.render_template('index.html', parts=text())
+
+
+def main():
+ global app
+ app.run(debug=debug)
+
+
+if __name__ == '__main__':
+ main()