From 9ba3e58f3365599614f786bb78cbd9f95801c45b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Ha=C3=9Fel?= Date: Fri, 27 Jun 2025 14:45:36 +0200 Subject: [PATCH] working on dev (not raspberry yet) --- ConsolePrinter.py | 49 ++++++++++ Event.py | 55 +++++++++++ Fotobox.py | 243 ++++++++++++++++++++++++++++++++++++++++++++++ Photo.py | 45 +++++++++ config.py | 136 ++++++++++++++++++++++++++ main.py | 15 +++ picamera.py | 32 ++++++ rsync.sh | 38 ++++++++ timer.py | 48 +++++++++ upload.sh | 38 ++++++++ 10 files changed, 699 insertions(+) create mode 100644 ConsolePrinter.py create mode 100644 Event.py create mode 100644 Fotobox.py create mode 100644 Photo.py create mode 100644 config.py create mode 100644 main.py create mode 100644 picamera.py create mode 100644 rsync.sh create mode 100644 timer.py create mode 100644 upload.sh diff --git a/ConsolePrinter.py b/ConsolePrinter.py new file mode 100644 index 0000000..187e6b1 --- /dev/null +++ b/ConsolePrinter.py @@ -0,0 +1,49 @@ +from escpos.printer import Dummy + + +class ConsolePrinter(Dummy): + def __init__(self, width=42): + super().__init__() + self.width = width # characters per line + self.align = 'left' + + def set(self, align='left', font='a', **kwargs): + self.align = align + + def text(self, txt): + lines = txt.splitlines() + for line in lines: + formatted = self._format_line(line) + print(formatted) + + def _format_line(self, line: str) -> str: + # Apply alignment + if self.align == 'center': + return line.center(self.width) + elif self.align == 'right': + return line.rjust(self.width) + else: + return line.ljust(self.width) + + def qr(self, content, center=False, **kwargs): + self.set(align='center' if center else 'left', **kwargs) + print(self._format_line("+------------------------------------+")) + print(self._format_line("+ ##### ##### +")) + print(self._format_line("+ ## ## ## ## +")) + print(self._format_line("+ ##### ##### +")) + print(self._format_line("+ +")) + print(self._format_line("+ +")) + print(self._format_line("+ +")) + print(self._format_line("+ +")) + print(self._format_line("+ +")) + print(self._format_line("+ +")) + print(self._format_line("+ ##### +")) + print(self._format_line("+ ## ## +")) + print(self._format_line("+ ##### +")) + print(self._format_line("+------------------------------------+")) + + def cut(self, mode='FULL', feed=True): + pass + + def is_online(self): + return True diff --git a/Event.py b/Event.py new file mode 100644 index 0000000..6986b61 --- /dev/null +++ b/Event.py @@ -0,0 +1,55 @@ +import json +from datetime import datetime +from pathlib import Path + +from PIL import Image + +from config import pil_image_to_surface, WIDTH, HEIGHT, SCREEN_RECT, DOMAIN, THUMB_WIDTH, THUMB_HEIGHT + + +class Event: + + def __init__(self, data: json): + self.code = data['code'] + self.title = data['title'] + self.date = datetime.strptime(data['date'], '%Y-%m-%d') + self.password = data['password'] + self.frame_factor = float(data['frame_factor']) if 'frame_factor' in data else 1.0 + self.frame_original = Image.open("./data/frames/" + data['frame']) + self.frame = pil_image_to_surface(shrink_cover(self.frame_original, int(WIDTH * self.frame_factor), int(HEIGHT * self.frame_factor))) + self.frame_rect = self.frame.get_rect(center=SCREEN_RECT.center) + self.url_without_protocol = "%s/e/%s" % (DOMAIN, self.code) + + +def load_event() -> Event | None: + try: + with Path("./data/event.json").open("r") as f: + data = json.load(f) + return Event(data) + except Exception as e: + print(e) + return None + + +def shrink_inside(img: Image, width: float, height: float): + if img.width <= width and img.height <= height: + return img + r = img.width / img.height + w = width + h = width / r + if h > height: + h = height + w = height * r + return img.resize((int(w), int(h)), Image.Resampling.LANCZOS) + + +def shrink_cover(img: Image, width: float, height: float): + if img.width <= width and img.height <= height: + return img + r = img.width / img.height + w = width + h = width / r + if h < height: + h = height + w = height * r + return img.resize((int(w), int(h)), Image.Resampling.LANCZOS) diff --git a/Fotobox.py b/Fotobox.py new file mode 100644 index 0000000..e25cd44 --- /dev/null +++ b/Fotobox.py @@ -0,0 +1,243 @@ +import os.path +import time +from typing import List +from PIL import Image +from escpos.escpos import Escpos +from ConsolePrinter import ConsolePrinter +from serial import SerialException + +from Event import Event, load_event +from Photo import Photo +from config import * + +from enum import Enum + +from config import read, file_increase, iso, now +from timer import Timer + + +class State(Enum): + IDLE = 0 + COUNTDOWN = 1 + SHOOTING = 2 + PREPARE = 3 + CHOOSE = 4 + QR = 5 + + +class Fotobox: + + def __init__(self, camera): + self._camera = camera + self._running: bool = True + self._state: State = State.IDLE + self._photos: List[Photo] = [] + self._choice: Photo | None = None + self._countdown_timer: Timer = Timer("countdown", 0, 1, COUNTDOWN_COUNT + 1, self._countdown_callback) + self._shooting_timer: Timer = Timer("shooting", 0, SHOOTING_INTERVAL, SHOOTING_COUNT, self._shooting_callback) + + self._fotobox_uuid = read("./data/fotobox.uuid") + print("Starting fotobox: F-%s" % self._fotobox_uuid) + + self._runtime_datetime = now() + self._runtime_number = file_increase("./data/runtime.number") + print("Starting runtime: R%04d-%s" % (self._runtime_number, iso(self._runtime_datetime))) + + self._event: Event | None = load_event() + self._idle_url_without_protocol = "%s/e/%s" % (DOMAIN, self._event.code) if self._event is not None else DOMAIN + self._idle_url_with_protocol = "http%s://%s/" % ('s' if HTTPS else '', self._idle_url_without_protocol) + self._idle_qr = qr_create(self._idle_url_with_protocol) + self._idle_qr_rect = self._idle_qr.get_rect(center=SCREEN_RECT.center) + self._idle_url = FONT_URL.render(self._idle_url_without_protocol, True, (255, 255, 255)) + self._idle_url_rect = self._idle_url.get_rect(centerx=SCREEN_RECT.centerx, bottom=HEIGHT - BORDER) + + self._shooting_number = 0 + self._shooting_datetime = None + + self._printer: Escpos | None = None + + def _countdown_callback(self): + if self._countdown_timer.complete(): + self._set_state(State.SHOOTING) + else: + print("COUNTDOWN: %d" % self._countdown_timer.rest()) + + def _shooting_callback(self): + print("SHOT: %d/%d" % (self._shooting_timer.current(), SHOOTING_COUNT)) + + screen.fill((255, 255, 255)) + pygame.display.flip() + if not DEBUG: + time.sleep(0.2) + + stream = io.BytesIO() + self._camera.capture(stream, format='rgb') + stream.seek(0) + self._photos.append(Photo(self._shooting_timer.current(), Image.open(stream))) + + screen.fill((0, 0, 0)) + pygame.display.flip() + + if self._shooting_timer.complete(): + self._set_state(State.PREPARE) + + def run(self): + if DEBUG: + self._set_state(State.SHOOTING) + while self._running: + self._events() + self._timer() + self._draw() + + def _events(self): + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self._running = False + elif event.type == pygame.MOUSEBUTTONDOWN: + if self._state == State.IDLE: + self._set_state(State.COUNTDOWN) + elif self._state == State.CHOOSE: + if CANCEL_RECT.collidepoint(event.pos): + print("CANCEL") + self._set_state(State.IDLE) + elif SAVE_RECT.collidepoint(event.pos) and self._choice is not None: + print("SAVE") + self._set_state(State.QR) + else: + for photo in self._photos: + if photo.thumb_rect.collidepoint(event.pos): + self._choice = photo + print("CHOSE: %d" % self._choice.number) + elif self._state == State.QR: + if DONE_RECT.collidepoint(event.pos): + print("DONE") + self._set_state(State.IDLE) + elif self._printer is not None and not self._printed and PRINT_RECT.collidepoint(event.pos): + print("PRINT") + try: + self._printer.set(align='center', font='b', width=2, height=2) + self._printer.text("Fotobox\n") + self._printer.set(align='center', font='a', width=1, height=1) + if self._event is not None: + self._printer.text(self._event.date.strftime("%d.%m.%Y") + "\n") + self._printer.text(self._event.title + "\n") + self._printer.text(self._event.url_without_protocol + "\n") + self._printer.text("Passwort: " + self._event.password + "\n") + else: + self._printer.text(datetime.now().strftime("%d.%m.%Y") + "\n") + self._printer.text(self._idle_url_without_protocol + "\n") + self._printer.qr(self._choice.urlWithProtocol, center=True) + self._printer.text(self._choice.urlWithoutProtocol) + self._printer.cut() + self._printed = True + + except Exception as e: + print(e) + + def _timer(self): + dt: float = clock.tick(20) / 1000 + self._shooting_timer.tick(dt) + self._countdown_timer.tick(dt) + + def _draw(self): + screen.fill((0, 0, 0)) + if self._state == State.IDLE: + screen.blit(FOTOBOX_TITLE_SURFACE, FOTOBOX_TITLE_RECT) + screen.blit(self._idle_qr, self._idle_qr_rect) + screen.blit(self._idle_url, self._idle_url_rect) + elif self._state == State.COUNTDOWN: + screen.blit(self._event.frame, self._event.frame_rect) + if self._countdown_timer.rest() == 3: + screen.blit(FOTOBOX_COUNTDOWN3_SURFACE, FOTOBOX_COUNTDOWN3_RECT) + elif self._countdown_timer.rest() == 2: + screen.blit(FOTOBOX_COUNTDOWN2_SURFACE, FOTOBOX_COUNTDOWN2_RECT) + elif self._countdown_timer.rest() == 1: + screen.blit(FOTOBOX_COUNTDOWN1_SURFACE, FOTOBOX_COUNTDOWN1_RECT) + elif self._state == State.SHOOTING: + screen.blit(self._event.frame, self._event.frame_rect) + elif self._state == State.PREPARE: + screen.blit(LOADING_SURFACE, LOADING_RECT) + elif self._state == State.CHOOSE: + screen.blit(CHOOSE_SURFACE, CHOOSE_RECT) + screen.blit(CANCEL_SURFACE, CANCEL_RECT) + if self._choice is not None: + screen.blit(SAVE_SURFACE, SAVE_RECT) + for photo in self._photos: + screen.blit(photo.thumb, photo.thumb_rect) + if self._choice == photo: + rect = pygame.Rect( + photo.thumb_rect.x - CHOICE_BORDER, + photo.thumb_rect.y - CHOICE_BORDER, + photo.thumb_rect.width + 2 * CHOICE_BORDER, + photo.thumb_rect.height + 2 * CHOICE_BORDER + ) + pygame.draw.rect(screen, (0, 255, 0), rect, width=CHOICE_BORDER) + elif self._state == State.QR: + screen.blit(self._qr, self._qr_rect) + screen.blit(self._choice.chosen, self._choice.chosen_rect) + if self._printer is not None and not self._printed: + screen.blit(PRINT_SURFACE, PRINT_RECT) + screen.blit(DONE_SURFACE, DONE_RECT) + pygame.display.flip() + + def _set_state(self, new_state: State): + print(new_state) + self._state = new_state + if new_state == State.COUNTDOWN: + self._camera.start_preview() + self._countdown_timer.restart() + elif new_state == State.SHOOTING: + self._photos = [] + self._shooting_number += 1 + self._shooting_datetime = now() + print("Starting shooting: S%04d-%s" % (self._shooting_number, iso(self._shooting_datetime))) + self._shooting_timer.restart() + elif new_state == State.PREPARE: + self._camera.stop_preview() + for photo in self._photos: + photo.prepare(self._event) + self._set_state(State.CHOOSE) + elif new_state == State.CHOOSE: + self._choice = None + elif new_state == State.QR: + self._printed = False + try: + if self._printer is None or not self._printer.is_online(): + # self._printer = Serial(devfile='/dev/ttyUSB0', baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=1.00, dsrdtr=True) + self._printer = ConsolePrinter() + self._printer.profile.media['width']['pixels'] = 512 + print("Printer AVAILABLE") + except SerialException: + self._printer = None + print("No printer available") + + self._qr = qr_create(self._choice.urlWithProtocol) + self._qr_rect = self._qr.get_rect(left=2 * BORDER, centery=SCREEN_RECT.centery) + + filename = "F-%s---R%04d-%s---S%04d-%s---P%04d-%s---%s.jpg" % ( + self._fotobox_uuid, + self._runtime_number, + iso(self._runtime_datetime), + self._shooting_number, + iso(self._shooting_datetime), + self._choice.number, + iso(self._choice.datetime), + self._choice.code, + ) + + original = Path("./data/photos/original/F-%s/R%04d-%s/%s" % ( + self._fotobox_uuid, + + self._runtime_number, + iso(self._runtime_datetime), + + filename, + )) + original.parent.mkdir(parents=True, exist_ok=True) + self._choice.img.save(original, format='JPEG', quality=95) + + upload = Path("./data/photos/upload/%s" % (filename,)) + upload.parent.mkdir(parents=True, exist_ok=True) + os.link(original, upload) + + print("Photo saved: %s" % original.absolute()) diff --git a/Photo.py b/Photo.py new file mode 100644 index 0000000..25e7857 --- /dev/null +++ b/Photo.py @@ -0,0 +1,45 @@ +from datetime import datetime + +from PIL import Image +from pygame import Surface + +from Event import Event, shrink_inside +from config import THUMB_WIDTH, pil_image_to_surface, SCREEN_RECT, BORDER, WIDTH, now, generate_code, HTTPS, DOMAIN, THUMB_HEIGHT + + +class Photo: + + def __init__(self, number: int, img: Image): + self.number = number + self.datetime = now() + self.code = generate_code(4) + self.urlWithoutProtocol = "%s/p/%s" % (DOMAIN, self.code) + self.urlWithProtocol = "http%s://%s" % ('s' if HTTPS else '', self.urlWithoutProtocol) + self.img = img + self.thumb = None + self.thumb_rect = None + self.chosen = None + self.chosen_rect = None + + def prepare(self, event: Event): + frame = self._resize2(event.frame_original, THUMB_WIDTH) + photo = shrink_inside(self.img, frame.width / (event.frame_factor - 0.01), frame.height / (event.frame_factor - 0.01)) + self.thumb = Surface((frame.width, frame.height)) + self.thumb.blit(pil_image_to_surface(photo), ((frame.width - photo.width) / 2, (frame.height - photo.height) / 2)) + self.thumb.blit(pil_image_to_surface(frame), (0, 0)) + self.thumb_rect = self.thumb.get_rect(centery=SCREEN_RECT.centery, left=(self.number - 1) * (THUMB_WIDTH + BORDER) + BORDER) + + frame_chosen = self._resize2(event.frame_original, THUMB_WIDTH) + photo_chosen = shrink_inside(self.img, frame_chosen.width / event.frame_factor, frame_chosen.height / event.frame_factor) + self.chosen = Surface((frame_chosen.width, frame_chosen.height)) + self.chosen.blit(pil_image_to_surface(photo_chosen), ((frame_chosen.width - photo_chosen.width) / 2, (frame_chosen.height - photo_chosen.height) / 2)) + self.chosen.blit(pil_image_to_surface(frame_chosen), (0, 0)) + self.chosen_rect = self.chosen.get_rect(right=WIDTH - 2 * BORDER, centery=SCREEN_RECT.centery) + + def _resize(self, width: float): + height = width / self.img.width * self.img.height + return self.img.resize((int(width), int(height)), Image.Resampling.LANCZOS) + + def _resize2(self, img: Image, width: float): + height = width / img.width * img.height + return img.resize((int(width), int(height)), Image.Resampling.LANCZOS) diff --git a/config.py b/config.py new file mode 100644 index 0000000..89154fd --- /dev/null +++ b/config.py @@ -0,0 +1,136 @@ +import io +import random +from datetime import datetime +from pathlib import Path + +import pygame +from pygame import Surface +from pygame.time import Clock +from qrcode.main import QRCode + +DEBUG: bool = True +# DEBUG: bool = False + +WIDTH: int = 800 +HEIGHT: int = 480 +BORDER: int = 10 + +SHOOTING_COUNT: int = 2 +SHOOTING_INTERVAL: float = 0 if DEBUG else 1.75 +COUNTDOWN_COUNT: int = 1 if DEBUG else 3 + +CHOICE_BORDER: int = 5 + +THUMB_WIDTH: int = (WIDTH - (SHOOTING_COUNT + 1) * BORDER) // SHOOTING_COUNT +THUMB_HEIGHT: int = int(THUMB_WIDTH / 1.6) + +WHITE = (255, 255, 255) +BLACK = (0, 0, 0) + +pygame.init() + +screen: Surface = pygame.display.set_mode((WIDTH, HEIGHT)) +SCREEN_RECT = screen.get_rect() + +clock: Clock = pygame.time.Clock() + + +def qr_create(content: str): + global FOTOBOX_QR_SURFACE + qr = QRCode(border=1) + qr.add_data(content) + qr.make(fit=True) + qr_img = qr.make_image() + qr_img_buffer = io.BytesIO() + qr_img.save(qr_img_buffer) + qr_img_buffer.seek(0) + return pygame.image.load(qr_img_buffer) + + +HTTPS = True +DOMAIN = "fotobox.online" +# HTTPS = False +# DOMAIN = "localhost:8080" + +FONT_TITLE = pygame.font.SysFont(None, 70) +FOTOBOX_TITLE_SURFACE = FONT_TITLE.render("Berühren um Fotos zu schießen", True, (255, 255, 255)) +FOTOBOX_TITLE_RECT = FOTOBOX_TITLE_SURFACE.get_rect(centerx=SCREEN_RECT.centerx, top=BORDER) + +FONT_URL = pygame.font.SysFont(None, 60) + +FONT_COUNTDOWN = pygame.font.SysFont(None, 500) +FOTOBOX_COUNTDOWN3_SURFACE = FONT_COUNTDOWN.render("3", True, (255, 255, 255)) +FOTOBOX_COUNTDOWN3_RECT = FOTOBOX_COUNTDOWN3_SURFACE.get_rect(center=SCREEN_RECT.center) +FOTOBOX_COUNTDOWN2_SURFACE = FONT_COUNTDOWN.render("2", True, (255, 255, 255)) +FOTOBOX_COUNTDOWN2_RECT = FOTOBOX_COUNTDOWN2_SURFACE.get_rect(center=SCREEN_RECT.center) +FOTOBOX_COUNTDOWN1_SURFACE = FONT_COUNTDOWN.render("1", True, (255, 255, 255)) +FOTOBOX_COUNTDOWN1_RECT = FOTOBOX_COUNTDOWN1_SURFACE.get_rect(center=SCREEN_RECT.center) + +FONT_LOADING = pygame.font.SysFont(None, 200) +LOADING_SURFACE = FONT_LOADING.render("Laden...", True, (255, 255, 255)) +LOADING_RECT = LOADING_SURFACE.get_rect(center=SCREEN_RECT.center) + +FONT_CHOOSE = pygame.font.SysFont(None, 100) +CHOOSE_SURFACE = FONT_CHOOSE.render("Bitte wählen", True, (255, 255, 255)) +CHOOSE_RECT = CHOOSE_SURFACE.get_rect(centerx=SCREEN_RECT.centerx, top=BORDER) + +FONT_CANCEL = pygame.font.SysFont(None, 60) +CANCEL_SURFACE = FONT_CANCEL.render("Abbruch", True, (255, 0, 0)) +CANCEL_RECT = CANCEL_SURFACE.get_rect(left=BORDER, bottom=HEIGHT - BORDER) + +FONT_SAVE = pygame.font.SysFont(None, 60) +SAVE_SURFACE = FONT_SAVE.render("Speichern", True, (0, 255, 0)) +SAVE_RECT = SAVE_SURFACE.get_rect(right=WIDTH - BORDER, bottom=HEIGHT - BORDER) + +FONT_PRINT = pygame.font.SysFont(None, 60) +PRINT_SURFACE = FONT_PRINT.render("Code drucken", True, (128, 128, 255)) +PRINT_RECT = PRINT_SURFACE.get_rect(left=BORDER, bottom=HEIGHT - BORDER) + +FONT_DONE = pygame.font.SysFont(None, 60) +DONE_SURFACE = FONT_DONE.render("Fertig", True, (0, 255, 0)) +DONE_RECT = DONE_SURFACE.get_rect(right=WIDTH - BORDER, bottom=HEIGHT - BORDER) + + +def pil_image_to_surface(pil_image): + mode = pil_image.mode + size = pil_image.size + data = pil_image.tobytes() + + return pygame.image.fromstring(data, size, mode).convert_alpha() if 'A' in mode else pygame.image.fromstring(data, size, mode).convert() + + +def read(path: str) -> str: + with open(path, "r") as file: + return file.read().strip() + + +def read_or_else(path: str, fallback: str) -> str: + try: + return read(path) + except FileNotFoundError: + return fallback + + +def write(path: str, value: str) -> None: + Path(path).parent.mkdir(parents=True, exist_ok=True) + with open(path, "w") as file: + file.write(value) + + +def file_increase(path: str) -> int: + last = int(read_or_else(path, "0")) + current = last + 1 + write(path, str(current)) + return current + + +def iso(d: datetime): + return d.strftime("%Y-%m-%dT%H:%M:%S%z") + + +def now(): + return datetime.now().astimezone() + + +def generate_code(groups: int): + return '-'.join(''.join(random.choices('23456789abcdefghjkmnpqrstuvwxyz', k=4)) for _ in range(groups)) diff --git a/main.py b/main.py new file mode 100644 index 0000000..edb87b6 --- /dev/null +++ b/main.py @@ -0,0 +1,15 @@ +#!/usr/bin/python3 + +import pygame +import picamera + +from Fotobox import Fotobox + +with picamera.PiCamera() as camera: + fotobox = Fotobox(camera) + try: + fotobox.run() + except KeyboardInterrupt: + pass + finally: + pygame.quit() diff --git a/picamera.py b/picamera.py new file mode 100644 index 0000000..d8b3099 --- /dev/null +++ b/picamera.py @@ -0,0 +1,32 @@ +import time + +from PIL import Image + +from config import DEBUG + + +# noinspection PyMethodMayBeStatic,PyShadowingBuiltins +class PiCamera: + def __init__(self): + pass + + def start_preview(self): + pass + + def capture(self, output, format): + with Image.open("./data/demo.jpg") as img: + img.save(output, "JPEG") + if not DEBUG: + time.sleep(0.5) + + def stop_preview(self): + pass + + def close(self): + pass + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() diff --git a/rsync.sh b/rsync.sh new file mode 100644 index 0000000..ba8a57d --- /dev/null +++ b/rsync.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +WATCH_DIR="./data/photos/upload" + +if [ ! -d "$WATCH_DIR" ]; then + echo "watch dir not found: $WATCH_DIR" + exit 1 +fi + +UPLOAD_URL="https://fotobox.online/upload" + +FOTOBOX_KEY="bla..." # TODO + +SLEEP_INTERVAL=10 + +while true; do + for filepath in "$WATCH_DIR"/*; do + [ -e "$filepath" ] || continue + + filename=$(basename "$filepath") + filesize=$(stat -c %s "$filepath") + + echo "uploading: $filepath" + response=$(curl -s -w "\n%{http_code}" -F "file=@${filepath}" -F "filename=${filename}" -F "fotoboxKey=${FOTOBOX_KEY}" "$UPLOAD_URL") + body=$(echo "$response" | head -n 1) + status=$(echo "$response" | tail -n 1) + + expected="Fotobox2|||${filename}|||${filesize}" + + if [ "$status" -eq 200 ] && [ "$body" = "$expected" ]; then + echo "SUCCESS" + rm -f "$filepath" + else + echo "FAILED: CODE=$status, Response=$body" + fi + done + sleep "$SLEEP_INTERVAL" +done diff --git a/timer.py b/timer.py new file mode 100644 index 0000000..7e65812 --- /dev/null +++ b/timer.py @@ -0,0 +1,48 @@ +from typing import Callable + + +class Timer: + + def __init__(self, name: str, initial: float, seconds: float, count: int, callback: Callable[[], None] = None): + self._name = name + self._enabled = False + + self._initial = initial + self._seconds = seconds + self._rest = 0 + + self._count = count + self._current = 0 + + self._callback = callback + + def restart(self): + self._enabled = True + self._current = 0 + self._rest = self._initial + + def tick(self, dt: float): + if not self._enabled or self._current >= self._count: + return + + self._rest -= dt + if self._rest <= 0: + self._current += 1 + self._rest += self._seconds + if self._callback: + self._callback() + + def current(self): + return self._current + + def count(self): + return self._count + + def rest(self): + return self._count - self._current + + def complete(self): + return self._current >= self._count + + def running(self): + return self._enabled and not self.complete() diff --git a/upload.sh b/upload.sh new file mode 100644 index 0000000..ba8a57d --- /dev/null +++ b/upload.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +WATCH_DIR="./data/photos/upload" + +if [ ! -d "$WATCH_DIR" ]; then + echo "watch dir not found: $WATCH_DIR" + exit 1 +fi + +UPLOAD_URL="https://fotobox.online/upload" + +FOTOBOX_KEY="bla..." # TODO + +SLEEP_INTERVAL=10 + +while true; do + for filepath in "$WATCH_DIR"/*; do + [ -e "$filepath" ] || continue + + filename=$(basename "$filepath") + filesize=$(stat -c %s "$filepath") + + echo "uploading: $filepath" + response=$(curl -s -w "\n%{http_code}" -F "file=@${filepath}" -F "filename=${filename}" -F "fotoboxKey=${FOTOBOX_KEY}" "$UPLOAD_URL") + body=$(echo "$response" | head -n 1) + status=$(echo "$response" | tail -n 1) + + expected="Fotobox2|||${filename}|||${filesize}" + + if [ "$status" -eq 200 ] && [ "$body" = "$expected" ]; then + echo "SUCCESS" + rm -f "$filepath" + else + echo "FAILED: CODE=$status, Response=$body" + fi + done + sleep "$SLEEP_INTERVAL" +done