Compare commits

...

5 Commits

Author SHA1 Message Date
d8f3d1bdb8 add authorship 2026-03-04 06:01:32 +00:00
328de3c766 add optional user specification 2026-03-04 05:58:01 +00:00
2748a5a75b add 100 max retries 2026-03-04 05:57:53 +00:00
b5d6fc2152 handle missing posts 2026-03-04 05:57:43 +00:00
72bc286601 bump deps 2026-03-04 05:40:44 +00:00
7 changed files with 672 additions and 303 deletions

View File

@@ -1,2 +1,2 @@
python 3.13.0 python 3.14.3
nodejs 24.4.1 nodejs 25.8.0

View File

@@ -1,4 +1,4 @@
FROM python:3.11.5-alpine FROM python:3.14.3-alpine
WORKDIR /app WORKDIR /app
RUN pip install uv RUN pip install uv

View File

@@ -13,6 +13,7 @@ screen](https://gitea.bubbletea.dev/shibao/szuru-eink-bot/raw/branch/master/eink
- Fetches random images from a configurable Szurubooru instance. - Fetches random images from a configurable Szurubooru instance.
- Resizes and dithered images for e-ink display compatibility. - Resizes and dithered images for e-ink display compatibility.
- Supports monochrome and color dithering (for compatible displays). - Supports monochrome and color dithering (for compatible displays).
- Filters images by an optional allowed username.
- Uploads processed images to a Waveshare e-ink screen. - Uploads processed images to a Waveshare e-ink screen.
- Configurable via environment variables. - Configurable via environment variables.
- Designed to run as a Docker container on a cron job. - Designed to run as a Docker container on a cron job.
@@ -41,7 +42,10 @@ display.
**Note:** For `EPD_TYPE`, refer to the `epd/epd_config.py` file for a full list **Note:** For `EPD_TYPE`, refer to the `epd/epd_config.py` file for a full list
of supported display types and their corresponding string labels. For color of supported display types and their corresponding string labels. For color
displays, ensure `DITHERING_MODE` is set to `color`. displays, ensure `DITHERING_MODE` is set to `color`. `ALLOWED_USER` is optional;
if set, the bot will only fetch images uploaded by that specific user. The bot
will retry a maximum of 100 times to find a valid image (matching the user filter,
if provided) before giving up.
### 3. Build and Run with Docker Compose ### 3. Build and Run with Docker Compose

View File

@@ -5,9 +5,10 @@ services:
build: . build: .
container_name: szuru-eink-bot container_name: szuru-eink-bot
environment: environment:
- ALLOWED_USER=YOUR_ALLOWED_USER # optional: only fetch images from this user
- DITHERING_MODE=YOUR_DITHERING_MODE # e.g., mono, color
- EINK_IP=YOUR_EINK_IP
- EPD_TYPE=YOUR_EPD_TYPE # e.g., '7.5 V2', 5.65f
- SZURU_TOKEN=YOUR_SZURU_TOKEN
- SZURU_URL=YOUR_SZURU_URL - SZURU_URL=YOUR_SZURU_URL
- SZURU_USER=YOUR_SZURU_USER - SZURU_USER=YOUR_SZURU_USER
- SZURU_TOKEN=YOUR_SZURU_TOKEN
- EPD_TYPE=YOUR_EPD_TYPE # e.g., '7.5 V2', 5.65f
- EINK_IP=YOUR_EINK_IP
- DITHERING_MODE=YOUR_DITHERING_MODE # e.g., mono, color

View File

@@ -2,14 +2,18 @@
name = "szuru-eink" name = "szuru-eink"
version = "0.1.0" version = "0.1.0"
description = "" description = ""
authors = [] authors = [
requires-python = "==3.13.5" { name = "shibao" },
]
requires-python = ">=3.14"
dependencies = [ dependencies = [
"aioipfs==0.6.5", "aioipfs==0.7.1",
"pyszuru==0.3.1", "pyszuru==0.4.0",
"Requests==2.31.0", "requests==2.32.5",
"Pillow==11.3.0", "pillow==12.1.1",
] ]
[project.optional-dependencies] [project.optional-dependencies]
dev = ["black==24.4.2"] dev = [
"black==26.1.0",
]

View File

@@ -6,6 +6,7 @@ import random
import requests import requests
import os import os
import pyszuru import pyszuru
from pyszuru.api import SzurubooruHTTPError
from PIL import Image, ImageSequence, UnidentifiedImageError from PIL import Image, ImageSequence, UnidentifiedImageError
@@ -13,12 +14,12 @@ from epd.epd_config import palArr, epdArr, EPD_TYPES
from epd.image_processor import getErr, getNear, addVal, procImg from epd.image_processor import getErr, getNear, addVal, procImg
from epd.epd_uploader import upload_image from epd.epd_uploader import upload_image
# get truly random post # get truly random post
try: try:
szuru_url = os.environ["SZURU_URL"] szuru_url = os.environ["SZURU_URL"]
szuru_user = os.environ["SZURU_USER"] szuru_user = os.environ["SZURU_USER"]
szuru_token = os.environ["SZURU_TOKEN"] szuru_token = os.environ["SZURU_TOKEN"]
allowed_user = os.getenv("ALLOWED_USER")
except KeyError as e: except KeyError as e:
raise ValueError(f"Missing required environment variable: {e}") raise ValueError(f"Missing required environment variable: {e}")
@@ -26,16 +27,42 @@ booru = pyszuru.API(szuru_url, username=szuru_user, token=szuru_token)
highest_post = next(booru.search_post("sort:id type:image", page_size=1)) highest_post = next(booru.search_post("sort:id type:image", page_size=1))
post = None post = None
while post is None: retries = 0
MAX_RETRIES = 100
while post is None and retries < MAX_RETRIES:
random_id = random.randint(0, highest_post.id_) random_id = random.randint(0, highest_post.id_)
try:
temp_post = booru.getPost(random_id) temp_post = booru.getPost(random_id)
except SzurubooruHTTPError:
print(
f"[DEBUG] Skipping post ID: {random_id} - Post not found. (Retry {retries + 1}/{MAX_RETRIES})"
)
retries += 1
continue
if temp_post and temp_post.mime.startswith("image/") and temp_post.content: if temp_post and temp_post.mime.startswith("image/") and temp_post.content:
# User info is in _json['user']['name']
post_user = temp_post._json.get("user", {}).get("name", "Unknown")
if allowed_user and post_user != allowed_user:
print(
f"[DEBUG] Skipping post ID: {random_id} - uploaded by {post_user}, but only {allowed_user} is allowed. (Retry {retries + 1}/{MAX_RETRIES})"
)
retries += 1
continue
post = temp_post post = temp_post
print(f"[DEBUG] Found image post with ID: {post.id_}, MIME: {post.mime}") print(
f"[DEBUG] Found image post with ID: {post.id_}, MIME: {post.mime} (User: {post_user})"
)
else: else:
print( print(
f"[DEBUG] Skipping post ID: {random_id} (MIME: {temp_post.mime if temp_post else 'None'}, Content: {bool(temp_post.content) if temp_post else 'None'}) - not a valid image." f"[DEBUG] Skipping post ID: {random_id} (MIME: {temp_post.mime if temp_post else 'None'}, Content: {bool(temp_post.content) if temp_post else 'None'}) - not a valid image. (Retry {retries + 1}/{MAX_RETRIES})"
) )
retries += 1
if post is None:
raise RuntimeError(f"Failed to find a valid post after {MAX_RETRIES} retries.")
# download image # download image
image_downloaded = False image_downloaded = False
@@ -64,17 +91,39 @@ while not image_downloaded:
os.remove("image.jpg") os.remove("image.jpg")
# Find a new post # Find a new post
post = None post = None
while post is None: while post is None and retries < MAX_RETRIES:
random_id = random.randint(0, highest_post.id_) random_id = random.randint(0, highest_post.id_)
try:
temp_post = booru.getPost(random_id) temp_post = booru.getPost(random_id)
except SzurubooruHTTPError:
print(
f"[DEBUG] Skipping post ID: {random_id} - Post not found. (Retry {retries + 1}/{MAX_RETRIES})"
)
retries += 1
continue
if temp_post and temp_post.mime.startswith("image/") and temp_post.content: if temp_post and temp_post.mime.startswith("image/") and temp_post.content:
post_user = temp_post._json.get("user", {}).get("name", "Unknown")
if allowed_user and post_user != allowed_user:
print(
f"[DEBUG] Skipping post ID: {random_id} - uploaded by {post_user}, but only {allowed_user} is allowed. (Retry {retries + 1}/{MAX_RETRIES})"
)
retries += 1
continue
post = temp_post post = temp_post
print( print(
f"[DEBUG] Found new image post with ID: {post.id_}, MIME: {post.mime}" f"[DEBUG] Found new image post with ID: {post.id_}, MIME: {post.mime} (User: {post_user})"
) )
else: else:
print( print(
f"[DEBUG] Skipping post ID: {random_id} (MIME: {temp_post.mime if temp_post else 'None'}, Content: {bool(temp_post.content) if temp_post else 'None'}) - not a valid image." f"[DEBUG] Skipping post ID: {random_id} (MIME: {temp_post.mime if temp_post else 'None'}, Content: {bool(temp_post.content) if temp_post else 'None'}) - not a valid image. (Retry {retries + 1}/{MAX_RETRIES})"
)
retries += 1
if post is None:
raise RuntimeError(
f"Failed to find a valid post after {MAX_RETRIES} retries."
) )
# Process and upload # Process and upload

869
uv.lock generated

File diff suppressed because it is too large Load Diff