Skip to content
This repository has been archived by the owner on May 23, 2023. It is now read-only.

Commit

Permalink
Add --random-start option to start the slideshow from randomly
Browse files Browse the repository at this point in the history
selected photo. Add outputting errors to stderr
  • Loading branch information
Caleb9 committed Mar 5, 2023
1 parent 9a37035 commit ff9943d
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 43 deletions.
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Wait for the build to finish. A new file

Display help message:
```
./synology-photos-slideshow.pex
./synology-photos-slideshow.pex -h
```

Run the app in X server:
Expand Down Expand Up @@ -127,7 +127,7 @@ xset -dpms
setxkbmap -option terminate:ctrl_alt_bksp
# Start Synology Photos slideshow app
/home/pi/synology-photos-slideshow/synology-photos-slideshow.pex "SHARING_LINK" 30 > /tmp/synology-photos-slideshow.log 2>&1
/home/pi/synology-photos-slideshow/synology-photos-slideshow.pex "SHARING_LINK" --interval 30 > /tmp/synology-photos-slideshow.log 2>&1
```

The output of the program will be written to
Expand Down Expand Up @@ -166,6 +166,14 @@ solution, e.g. for Raspberry Pi Zero I'm using [Witty Pi 3
Mini](https://www.adafruit.com/product/5038).


### Start Slideshow From Random Photo

If the album is very large, and the startup-shutdown schedule is
short, potentially the slideshow might never display some of the later
photos in the album. The `--random-start` option solves the problem by
starting the slideshow at randomly picked photo.


### Auto Brightness

For my digital photo frame project I attached a light sensor to Pi's
Expand Down
99 changes: 62 additions & 37 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
#!/usr/bin/env python3

import argparse
import datetime
from random import randrange
import sys
import tkinter.ttk
import typing

import httpx
import PIL.ImageTk
import httpx

import image_processor
import get_photo_thread
import image_processor
import slideshow
import synology_photos_client

Expand All @@ -19,24 +21,36 @@ class App(tkinter.Tk):

def __init__(
self,
slideshow_: slideshow.Slideshow,
photo_change_interval_in_seconds: int,
datetime_now: typing.Callable[[], datetime.datetime]):
photos_client: synology_photos_client.PhotosClient,
photo_change_interval_in_seconds: float,
datetime_now: typing.Callable[[], datetime.datetime],
randrange_: typing.Callable[[int, int], int]):
super(App, self).__init__()
self.attributes("-fullscreen", True)

self._slideshow = slideshow_
self._photos_client = photos_client
self._photo_change_interval = datetime.timedelta(seconds=photo_change_interval_in_seconds)
self._datetime_now = datetime_now
self._randrange = randrange_

self._slideshow: slideshow.Slideshow = None
self._image_processor = image_processor.ImageProcessor((self.winfo_screenwidth(), self.winfo_screenheight()))
self._label = tkinter.ttk.Label(self, background="black")
self._label.pack(side="bottom", fill="both", expand=1)
# For displaying errors
self._label["foreground"] = "white"
self._photo: typing.Optional[PIL.ImageTk.PhotoImage] = None

def start_slideshow(self) -> "App":
def start_slideshow(self, start_from_random_photo: bool) -> "App":
initial_album_offset = 0
if start_from_random_photo:
try:
item_count = self._photos_client.get_album_contents_count()
initial_album_offset = self._randrange(0, item_count)
except Exception as error:
eprint(f"[{self._datetime_now()}] Error (cannot start from random photo): {error}")
self._slideshow = slideshow.Slideshow(self._photos_client, initial_album_offset)

self._monitor(datetime.datetime.min,
self._start_get_next_photo_thread())
return self
Expand All @@ -57,7 +71,6 @@ def schedule_next_iteration() -> None:
if thread.is_failed():
# Getting next photo failed
self._show_error(thread.error)
print(f"[{self._datetime_now()}] {thread.error}")
# Retry after regular photo change interval (here expressed in milliseconds)
self.after(
self._photo_change_interval.seconds * 1000,
Expand Down Expand Up @@ -86,45 +99,57 @@ def _show_image(self, photo_image: PIL.ImageTk.PhotoImage) -> None:
self._label["image"] = self._photo

def _show_error(self, error: Exception) -> None:
eprint(f"[{self._datetime_now()}] {error}")
self._label["image"] = b""
self._label["anchor"] = "center"
self._label["text"] = str(error)


help_message = f"""Provide the following arguments:
share_link [REQUIRED] Link to a publicly shared album on Synology Photos.
Note that the album's privacy settings must be set to Public
and link password protection must be disabled.
interval [OPTIONAL] Photo change interval in seconds.
Must be a positive number.
If not specified photos will change every 20 seconds
Example:
{sys.argv[0]} https://my.nas/is/sharing/ABcd1234Z 30
"""
def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)


def parse_arguments(argv: [str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(prog="synology-photos-slideshow.pex",
description="Synology Photos album slideshow",
epilog="Find new versions and more information on "
"https://github.com/Caleb9/synology-photos-slideshow")
parser.add_argument("share_link",
help="Link to a publicly shared album on Synology Photos. "
"Note that the album's privacy settings must be set to Public "
"and link password protection must be disabled.")

def valid_interval(x) -> float:
try:
if float(x) < 1:
raise argparse.ArgumentTypeError("%s is not greater or equal to 1" % x)
return float(x)
except ValueError:
raise argparse.ArgumentTypeError("%s is not a number" % x)

parser.add_argument("-i",
"--interval",
help="Photo change interval in seconds. Must be greater or equal to 1. "
"If not specified photos will change every 20 seconds",
type=valid_interval,
default=20)
parser.add_argument("--random-start",
help="Initialize slideshow at randomly selected photo",
action="store_true")
return parser.parse_args(argv)


def main(argv: [str]) -> None:
if len(argv) < 1:
print(help_message)
sys.exit(1)
share_link = argv[0]
photo_change_interval_in_seconds = 20
if len(argv) > 1:
photo_change_interval_in_seconds = int(argv[1])
if photo_change_interval_in_seconds < 1:
print("Invalid interval value. Must be a positive number")
sys.exit(2)
args = parse_arguments(argv)

with httpx.Client(timeout=20) as http_client:
App(
slideshow.Slideshow(
synology_photos_client.PhotosClient(
http_client,
share_link)),
photo_change_interval_in_seconds,
datetime.datetime.now) \
.start_slideshow() \
photos_client = synology_photos_client.PhotosClient(http_client,
args.share_link)
App(photos_client,
args.interval,
datetime.datetime.now,
randrange) \
.start_slideshow(args.random_start) \
.mainloop()


Expand Down
4 changes: 2 additions & 2 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ idna==3.4
# rfc3986
packaging==23.0
# via build
pex==2.1.121
pex==2.1.126
# via -r requirements/dev.in
pillow==9.4.0
# via -r requirements/common.in
pip-tools==6.12.1
pip-tools==6.12.3
# via -r requirements/dev.in
pyproject-hooks==1.0.0
# via build
Expand Down
10 changes: 8 additions & 2 deletions slideshow.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@ class Slideshow:

def __init__(
self,
photos_client: synology_photos_client.PhotosClient):
photos_client: synology_photos_client.PhotosClient,
initial_album_offset = 0):
self._photos_client = photos_client
self._initial_album_offset = initial_album_offset
self._album_offset = 0
self._photos_batch: list[synology_photos_client.PhotosClient.PhotoDto] = []
self._batch_photo_index = 0

def get_next_photo(self) -> bytes:
if self._slideshow_ended():
if self._initial_album_offset is not None:
self._album_offset = self._initial_album_offset
# We only use initial album offset for first run through the album.
self._initial_album_offset = None
elif self._slideshow_ended():
self._album_offset = 0
if self._need_next_batch():
self._photos_batch = self._photos_client.get_album_contents(self._album_offset, self._PHOTOS_BATCH_SIZE)
Expand Down
17 changes: 17 additions & 0 deletions synology_photos_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ class PhotoDto(typing.TypedDict):
id: int
thumbnail: "PhotosClient.Thumbnail"

def get_album_contents_count(self) -> int:
self._get_sharing_sid_cookie()

data = {
"api": "SYNO.Foto.Browse.Album",
"method": "get",
"version": 1,
}
response = self._http_client.post(self._api_url,
data=data,
headers=[("X-SYNO-SHARING", self._passphrase)])
response_content = response.json()
if not response_content["success"]:
raise Exception(f"Getting album contents count resulted with API error {response_content['error']}")

return response_content["data"]["list"][0]["item_count"]

def get_album_contents(self, offset: int, limit: int) -> list[PhotoDto]:
self._get_sharing_sid_cookie()

Expand Down

0 comments on commit ff9943d

Please sign in to comment.