From 5458fb7b4ef65858972915ee2688cc17122ab983 Mon Sep 17 00:00:00 2001 From: Peter Vulgaris Date: Wed, 4 Oct 2023 09:43:15 -0700 Subject: [PATCH 1/5] Add object ID to description Add the object ID to description for easier debugging. Needed to temporarily unblock a copy operation that was stuck on one particular ID. --- recording.py | 1 + 1 file changed, 1 insertion(+) diff --git a/recording.py b/recording.py index 3ffea26..9c35024 100644 --- a/recording.py +++ b/recording.py @@ -50,6 +50,7 @@ def print(self): def get_description(self): output = convert_datestr(self.airing_details['datetime']) + " - " \ + + f"{self.object_id} - " \ + f"{self.airing_details['show_title']}" \ + f" - {self.get_title()}" return output From f396c674725137a2fccc1830b0286571bea995b9 Mon Sep 17 00:00:00 2001 From: Peter Vulgaris Date: Wed, 11 Oct 2023 22:26:15 -0700 Subject: [PATCH 2/5] Add dateutil to requirements.txt Missing on default install. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 7207e77..619b476 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +python-dateutil requests json262 m3u8 From 557ce4fbfb1c5e4ddd3934776c5dcf04a79cecb7 Mon Sep 17 00:00:00 2001 From: Peter Vulgaris Date: Thu, 12 Oct 2023 20:32:31 -0700 Subject: [PATCH 3/5] Improve error message when no device selected on post Spent way too long debugging why a fresh install allowed "search" command to work, but not "copy". Turns out I had forgotten to run "config" first. The confusing part is that "search" seems to implicitly select a device while "copy" does not. Either way, using __get_uri() avoids some copy/paste URI generation and presents a better error message ("No device selected.") that would've been useful for initial debugging. --- tablo/endpoint.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tablo/endpoint.py b/tablo/endpoint.py index cb707c0..c5c199b 100644 --- a/tablo/endpoint.py +++ b/tablo/endpoint.py @@ -96,9 +96,7 @@ def getCached(self, **kwargs): @request_handler def post(self, *args, **kwargs): return requests.post( - 'http://{0}/{1}'.format( - self.device.address(), '/'.join(self.segments) - ), + self.__get_uri(), headers={'User-Agent': USER_AGENT}, data=json.dumps(args and args[0] or kwargs) ) From 456123d0caf75f1db65c0979a0aa32fb2b02fa4c Mon Sep 17 00:00:00 2001 From: Peter Vulgaris Date: Fri, 13 Oct 2023 09:22:40 -0700 Subject: [PATCH 4/5] Initial Docker packaging config Defaults from "docker init". --- .dockerignore | 34 ++++++++++++++++++++++++++++++++++ Dockerfile | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ compose.yaml | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 compose.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3edb0b5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/engine/reference/builder/#dockerignore-file + +**/.DS_Store +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d629c84 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +# syntax=docker/dockerfile:1 + +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Dockerfile reference guide at +# https://docs.docker.com/engine/reference/builder/ + +ARG PYTHON_VERSION=3.11.6 +FROM python:${PYTHON_VERSION}-slim as base + +# Prevents Python from writing pyc files. +ENV PYTHONDONTWRITEBYTECODE=1 + +# Keeps Python from buffering stdout and stderr to avoid situations where +# the application crashes without emitting any logs due to buffering. +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +# Create a non-privileged user that the app will run under. +# See https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#user +ARG UID=10001 +RUN adduser \ + --disabled-password \ + --gecos "" \ + --home "/nonexistent" \ + --shell "/sbin/nologin" \ + --no-create-home \ + --uid "${UID}" \ + appuser + +# Download dependencies as a separate step to take advantage of Docker's caching. +# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. +# Leverage a bind mount to requirements.txt to avoid having to copy them into +# into this layer. +RUN --mount=type=cache,target=/root/.cache/pip \ + --mount=type=bind,source=requirements.txt,target=requirements.txt \ + python -m pip install -r requirements.txt + +# Switch to the non-privileged user to run the application. +USER appuser + +# Copy the source code into the container. +COPY . . + +# Expose the port that the application listens on. +EXPOSE 8000 + +# Run the application. +CMD python3 tut.py diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..4ea1237 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,49 @@ +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Docker compose reference guide at +# https://docs.docker.com/compose/compose-file/ + +# Here the instructions define your application as a service called "server". +# This service is built from the Dockerfile in the current directory. +# You can add other services your application may depend on here, such as a +# database or a cache. For examples, see the Awesome Compose repository: +# https://github.com/docker/awesome-compose +services: + server: + build: + context: . + ports: + - 8000:8000 + +# The commented out section below is an example of how to define a PostgreSQL +# database that your application can use. `depends_on` tells Docker Compose to +# start the database before your application. The `db-data` volume persists the +# database data between container restarts. The `db-password` secret is used +# to set the database password. You must create `db/password.txt` and add +# a password of your choosing to it before running `docker compose up`. +# depends_on: +# db: +# condition: service_healthy +# db: +# image: postgres +# restart: always +# user: postgres +# secrets: +# - db-password +# volumes: +# - db-data:/var/lib/postgresql/data +# environment: +# - POSTGRES_DB=example +# - POSTGRES_PASSWORD_FILE=/run/secrets/db-password +# expose: +# - 5432 +# healthcheck: +# test: [ "CMD", "pg_isready" ] +# interval: 10s +# timeout: 5s +# retries: 5 +# volumes: +# db-data: +# secrets: +# db-password: +# file: db/password.txt + From 10af722925d9e8aa18b5fcb15fd777ad5cfd16f8 Mon Sep 17 00:00:00 2001 From: Peter Vulgaris Date: Fri, 13 Oct 2023 10:31:56 -0700 Subject: [PATCH 5/5] Basic Docker syncer configuration First version of Docker config to search for all available videos and sync them to bind mounted folder at /home/appuser/Tablo. Some hacks to enable functioning in Synology DSM 7.2's Container Manager. Assumes that Tablo directory has already been configured with a command like: > ./tut.py config --discover Tested it locally in MacOS with Tablo mounted shared folder: > docker build -t pvulgaris/tut-syncer:arm64 . > docker run --name tut-syncer --mount type=bind,source=/Volumes/video/Tablo,target=/home/appuser/Tablo pvulgaris/tut-syncer:arm64 Then build amd64 version for upload to Docker Hub and use on DS224+: > docker buildx build -t pvulgaris/tut-syncer:amd64 --platform linux/amd64 . Finally, use the DSM api to setup a job with Task Scheduler as described in https://www.reddit.com/r/synology/comments/10wkxuc/restart_docker_container/ Create a new task in Task Scheduler and run the following as root (replacing pvulgaris-tut-syncer-1 with the container name): > synowebapi --exec api=SYNO.Docker.Container method="start" version=1 name="pvulgaris-tut-syncer-1" --- Dockerfile | 33 ++++++++++++++++++++++++++------- compose.yaml | 49 ------------------------------------------------- 2 files changed, 26 insertions(+), 56 deletions(-) delete mode 100644 compose.yaml diff --git a/Dockerfile b/Dockerfile index d629c84..6ee5500 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,20 +14,31 @@ ENV PYTHONDONTWRITEBYTECODE=1 # the application crashes without emitting any logs due to buffering. ENV PYTHONUNBUFFERED=1 +# Install ffmpeg libraries needed to transcode Tablo video files. +RUN apt-get update && apt-get install -y ffmpeg + WORKDIR /app # Create a non-privileged user that the app will run under. # See https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#user -ARG UID=10001 +# Default Synology DSM first uid. See comments below for more context. +ARG UID=1026 RUN adduser \ --disabled-password \ --gecos "" \ - --home "/nonexistent" \ --shell "/sbin/nologin" \ - --no-create-home \ --uid "${UID}" \ appuser +# GID hacks for Synology DSM. Try as best we can to replicate DSM's default +# primary ("100(users)") and secondary group membership ("101(administrators)") +# to overcome permission issues with writing to a bind mounted folder, while +# still being somewhat platform agnostic. Example of the issue: +# https://www.reddit.com/r/synology/comments/s1arg0/synology_and_docker_permission_problem/ +# TODO: Make gid's user configurable. +RUN usermod -g users appuser +RUN adduser appuser `cat /etc/group | grep ":101:" | cut -d':' -f1` + # Download dependencies as a separate step to take advantage of Docker's caching. # Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. # Leverage a bind mount to requirements.txt to avoid having to copy them into @@ -42,8 +53,16 @@ USER appuser # Copy the source code into the container. COPY . . -# Expose the port that the application listens on. -EXPOSE 8000 +# Build the shows db. +# TODO: Run this separately for now. +# RUN python3 tut.py config --discover -# Run the application. -CMD python3 tut.py +# Check if ~/Tablo directory is mounted. Ideally this is run before each tut.py +# command (or tut.py is modified to not implicitly create a ~/Tablo directory) +# so that we don't accidentally sync shows into the container. +# TODO: Also check we have the right read/write permissions. +# Then sync all shows. +CMD if [ ! -d "/home/appuser/Tablo" ]; then echo "/home/appuser/Tablo not mounted." && exit 1; fi; \ + python3 tut.py library --build && \ + python3 tut.py -L search | \ + python3 tut.py copy diff --git a/compose.yaml b/compose.yaml deleted file mode 100644 index 4ea1237..0000000 --- a/compose.yaml +++ /dev/null @@ -1,49 +0,0 @@ -# Comments are provided throughout this file to help you get started. -# If you need more help, visit the Docker compose reference guide at -# https://docs.docker.com/compose/compose-file/ - -# Here the instructions define your application as a service called "server". -# This service is built from the Dockerfile in the current directory. -# You can add other services your application may depend on here, such as a -# database or a cache. For examples, see the Awesome Compose repository: -# https://github.com/docker/awesome-compose -services: - server: - build: - context: . - ports: - - 8000:8000 - -# The commented out section below is an example of how to define a PostgreSQL -# database that your application can use. `depends_on` tells Docker Compose to -# start the database before your application. The `db-data` volume persists the -# database data between container restarts. The `db-password` secret is used -# to set the database password. You must create `db/password.txt` and add -# a password of your choosing to it before running `docker compose up`. -# depends_on: -# db: -# condition: service_healthy -# db: -# image: postgres -# restart: always -# user: postgres -# secrets: -# - db-password -# volumes: -# - db-data:/var/lib/postgresql/data -# environment: -# - POSTGRES_DB=example -# - POSTGRES_PASSWORD_FILE=/run/secrets/db-password -# expose: -# - 5432 -# healthcheck: -# test: [ "CMD", "pg_isready" ] -# interval: 10s -# timeout: 5s -# retries: 5 -# volumes: -# db-data: -# secrets: -# db-password: -# file: db/password.txt -