refactor: modernize to Python 3 with OOP structure and comprehensive tests (#2)

* feat: full upgrade to python3, tests, etc.

* chore: saving the wip state

* chore(lint): fixed pyright errors, tests

* chore(ci): install deps

* chore(lint): fix linting

* chore(lint): more linting fixes

* chore(ci): upgrade workflows

* fix(test): fix tests, tweak editorconfig

* test: add helper path test and stub requests (#4)

* fix: update paths and workflow (#5)

* fix: update python version and improve cross-platform paths (#6)

* fix: resolve MegaLinter YAML and markdown lint failures (#7)

* fix: implement all CodeRabbit review comments (#8)

* fix: apply CodeRabbit auto-fixes

Fixed 3 file(s) based on 3 unresolved review comments.

* fix: resolve MegaLinter editorconfig and pylint/pyright failures (#9)

* feat: migrate to uv-managed project (#10)

* feat: migrate to uv-managed project

* fix: align Python version in pyproject.toml and CI setup-uv config

* fix: ignore uv.lock in editorconfig, disable PYTHON_PYLINT/PYRIGHT in mega-linter, use uv for dep install (#11)

* fix: ignore uv.lock in editorconfig and disable PYTHON_PYLINT/PYRIGHT in mega-linter

* fix: update outdated comment in mega-linter config

* fix: use uv instead of pip to install deps in mega-linter config

* chore(deps): upgrade workflows

* chore(ci): mega-linter config tweaks

* chore(ci): mega-linter config tweaks

* feat(deps): add pyright and pylint with non-overlapping config

Add pyright>=1.1.0 and pylint>=3.0.0 as dev dependencies. Configure
pyright for basic type checking (py3.13) and refine pylint message
disables to avoid overlap with ruff's enabled rule sets.

* feat(ci): re-enable pyright and pylint in mega-linter

Remove PYTHON_PYLINT and PYTHON_PYRIGHT from DISABLE_LINTERS so
mega-linter runs all three linters: ruff, pyright, and pylint.

* fix: resolve pyright/pylint findings and apply ruff formatting

Add encoding="utf-8" to read_text() calls in tests (pylint W1514).
Apply ruff-format double-quote style consistently across both files.

* chore(hooks): add editorconfig-checker and fix lines exceeding 80 chars

Add editorconfig-checker pre-commit hook to catch line-length
violations locally. Shorten docstrings in aeonview.py and
aeonview_test.py that exceeded the 80-character editorconfig limit.
Remove double-quote-string-fixer hook that conflicted with ruff-format.

* fix(ci): configure mega-linter to use project configs for pyright/pylint

Point mega-linter at pyproject.toml for both linters so they use our
config instead of mega-linter's defaults. Add venvPath/venv to pyright
so it resolves imports from the uv-created .venv. Disable pylint
import-error since import checking is handled by pyright.
This commit is contained in:
2026-03-13 15:31:04 +02:00
committed by GitHub
parent 6aa17ba957
commit 2b68767f0d
20 changed files with 2628 additions and 238 deletions

846
aeonview.py Executable file → Normal file
View File

@@ -1,221 +1,645 @@
import sys, time, datetime, os, optparse, errno, re
"""aeonview - Timelapse generator using ffmpeg.
def aeonview(argv):
"""
aeonview is a tool for automagical timelapse-video generation.
it works as a glue between different linux programs to produce
videos of elapsing time. works best with webcam-images from the net.
"""
version = "0.1.8"
Downloads webcam images on a schedule and generates daily, monthly, and yearly
timelapse videos by stitching frames together with ffmpeg.
"""
parser = optparse.OptionParser(
usage="Usage: %prog [options]",
description="aeonview for timelapses",
version="%prog v"+version
)
from __future__ import annotations
basicopts = optparse.OptionGroup(
parser, "Basic settings", "These effect in both modes."
)
import argparse
import hashlib
import logging
import subprocess
import sys
import tempfile
from datetime import datetime, timedelta
from pathlib import Path
basicopts.add_option("-m", "--mode",
default="image",
help="run mode: image or video [default: %default]")
basicopts.add_option("-p", "--project",
help="Project name, used as directory name. "
"Defaults to 5 characters from md5 hash of the webcam url.",
type="string")
basicopts.add_option("-d", "--dest",
help="Start of the destination. [default: %default]",
type="string",
default=os.getcwdu()+"/projects",
dest="path")
basicopts.add_option("--mencoder",
help="Path to mencoder binary. [default: %default]",
type="string",
default=os.getcwdu()+'/mencoder')
parser.add_option_group(basicopts)
# When mode is: image
imageopts = optparse.OptionGroup(
parser, "Options for --mode: image", "When we are gathering images.")
imageopts.add_option("--url", help="Webcam URL", type="string")
parser.add_option_group(imageopts)
# When mode is: video
videoopts = optparse.OptionGroup(parser,
"Options for --mode: video", "When we are making movies.")
videoopts.add_option("--videorun",
default="daily",
help="Video to process: daily, monthly or yearly [default: %default]",
type="string")
videoopts.add_option('--generate',
help="Date to video. Format: YYYY-MM-DD. "
"Default is calculated yesterday, currently %default",
type="string",
default=datetime.date.today()-datetime.timedelta(1))
# TODO: mode for monthly videos
#videoopts.add_option("--gen-month",
# help="Month to video. Format: YYYY-MM. "
# "Default is last month, currently %default",
# type="string",
# default=datetime.date.today()-datetime.timedelta(30))
videoopts.add_option("--fps",
default="10",
help="Frames per second, numeric [default: %default]",
type="int")
parser.add_option_group(videoopts)
parser.add_option("-v", help="Verbose", action="store_true", dest="verbose", default=False)
parser.add_option("-q", help="Quiet", action="store_false", dest="verbose", default=True)
parser.add_option("-s", "--simulate",
help="Demostrates what will happen (good for checking your settings and destinations)",
default=False,
action="store_true")
(options, args) = parser.parse_args(argv[1:])
if options.simulate == True:
print
print "--- Starting simulation, just echoing steps using your parameters."
print
print "(!) You are running aeonview from", os.getcwdu()
if options.mode == 'image':
# We are now in the gathering mode.
if options.url == None and options.simulate == True:
options.url = "http://example.com/webcam.jpg"
print "(!) Simulation: Using " + options.url + " as webcam url"
if options.url == None:
print "(!) Need a webcam url, not gonna rock before that!"
print
parser.print_help()
sys.exit(-1)
if options.project == None:
import hashlib # not needed before
m = hashlib.md5(options.url).hexdigest()
options.project = m[:5] # 5 first characters of md5-hash
if options.verbose == True or options.simulate == True:
print "(!) No project defined, using part of md5-hash of the webcam url:", options.project
if options.path == None:
if options.verbose == True or options.simulate == True:
print "(!) No destination defined, using:", options.path
else:
if options.verbose == True or options.simulate == True:
print "(!) Using destination:", options.path
# If you want to change the path structure, here's your chance.
options.imgpath = time.strftime("/img/%Y-%m/%d/")
options.imgname = time.strftime("%H-%M-%S")
# Let us build the destination path and filename
options.fileext = os.path.splitext(options.url)[1]
options.destdir = options.path + "/" + options.project + options.imgpath
options.destination = options.destdir + options.imgname + options.fileext
getit = options.url + " -o " + options.destination
# Crude, but works.
if options.simulate == False:
os.system("curl --create-dirs --silent %s" % getit)
else:
print "(!) Simulation: Making path:", options.destdir
print "(!) Simulation: curl (--create-dirs and --silent)", getit
elif options.mode == "video":
# We are now in the video producing mode
vid_extension = ".avi"
#m = os.getcwd() + "/mencoder"
m = options.mencoder
mencoder = m + " -really-quiet -mf fps="+ str(options.fps) +" -nosound -ovc lavc -lavcopts vcodec=mpeg4"
if options.project == None:
print "(!) No project defined, please specify what project you are working on."
print
parser.print_help()
sys.exit(-1)
if options.videorun == "daily":
vid_date = str(options.generate).split("-")
year = vid_date[0]
month = vid_date[1]
day = vid_date[2]
if check_date(int(year), int(month), int(day)):
proj_dir = options.path + "/" + options.project
video_dir = proj_dir + "/img/" + year + "-" + month + "/" + day + "/*"
video_out_dir = proj_dir + "/vid/" + year + "-" + month + "/"
video_out_day = video_out_dir + day + vid_extension
mfdir = os.path.dirname(os.path.realpath(video_dir))
command = mencoder + " -o " + video_out_day + " 'mf://" + mfdir + "/*'"
if options.simulate == False:
mkdir_p( video_out_dir )
os.system(command)
else:
print "(!) Video dir to process:", video_dir
print "(!) Video output-file:", video_out_day
print "(!) Made directory structure:", video_out_dir
print "(!) Command to run", command
else:
print "(!) Error: check your date. Value provided:", options.generate
elif options.videorun == "monthly":
print "Monthly: TODO"
# TODO Monthly script. Joins daily movies of that month.
elif options.videorun == "yearly":
print "Yearly: TODO"
# TODO Yearly script. Joins monthly movies together.
else:
print "(!) What? Please choose between -r daily/montly/yearly"
else:
parser.print_help()
sys.exit(-1)
import requests
# http://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python/600612#600612
def mkdir_p(path):
try:
os.makedirs(path)
except OSError as exc: # Python >2.5
if exc.errno == errno.EEXIST:
pass
else: raise
class AeonViewMessages:
"""Constant log messages used throughout the application."""
INVALID_URL = "Invalid URL provided."
INVALID_DATE = "Invalid date format provided."
DOWNLOAD_SUCCESS = "Image downloaded successfully."
DOWNLOAD_FAILURE = "Failed to download image."
VIDEO_GENERATION_SUCCESS = "Video generated successfully."
VIDEO_GENERATION_FAILURE = "Failed to generate video."
INVALID_IMAGE_FORMAT = "Invalid image format provided."
INVALID_IMAGE_EXTENSION = "Invalid image extension provided."
# Modified http://markmail.org/message/k2pxsle2lslrmnut
def check_date(year, month, day):
tup1 = (year, month, day, 0,0,0,0,0,0)
try:
date = time.mktime (tup1)
tup2 = time.localtime (date)
if tup1[:2] != tup2[:2]:
return False
else:
return True
except OverflowError:
return False
class AeonViewImages:
"""Handle image download and saving.
Downloads images from a webcam URL and saves them into a date-based
directory structure under the project path.
"""
def __init__(self, project_path: Path, url: str | None, args=None):
"""Initialize the AeonViewImages class.
:param project_path: Path to the project directory.
:param url: URL of the image to download.
:param args: Command-line arguments passed to the class.
"""
self.project_path = project_path
self.url = url or None
self.args = args or {}
self.simulate = getattr(args, "simulate", False)
def get_image_paths(
self,
url: str | None,
destination_base: Path | None,
date: datetime | None,
) -> dict:
"""Compute image paths for saving the downloaded image.
:param url: URL of the image.
:param destination_base: Base path where the image will be saved.
:param date: Date for which the image is requested.
:return: Dictionary with url, file, date, and destinations info.
:raises SystemExit: If any parameter is invalid.
"""
if url is None or not isinstance(url, str):
logging.error(AeonViewMessages.INVALID_URL)
sys.exit(1)
if not isinstance(date, datetime):
logging.error(AeonViewMessages.INVALID_DATE)
sys.exit(1)
if not url.startswith("http"):
logging.error(AeonViewMessages.INVALID_URL)
sys.exit(1)
if not url.endswith((".jpg", ".jpeg", ".png", ".gif", ".webp")):
logging.error(AeonViewMessages.INVALID_IMAGE_FORMAT)
sys.exit(1)
if destination_base is None:
logging.error("No destination base path provided.")
sys.exit(1)
if not isinstance(destination_base, Path):
logging.error("Invalid destination base path.")
sys.exit(1)
year = date.strftime("%Y")
month = date.strftime("%m")
day = date.strftime("%d")
year_month = f"{year}-{month}"
destination = AeonViewHelpers.build_path(
destination_base, year_month, day
)
extension = AeonViewHelpers.get_extension(url) or ""
file_name = date.strftime("%H-%M-%S") + extension
destination_file = AeonViewHelpers.build_path(destination, file_name)
if self.simulate:
logging.info("Simulate: would create %s", destination)
else:
AeonViewHelpers.mkdir_p(destination)
logging.info("Creating destination base path: %s", destination)
return {
"url": url,
"file": file_name,
"date": {
"year": year,
"month": month,
"day": day,
"hour": date.strftime("%H"),
"minute": date.strftime("%M"),
"second": date.strftime("%S"),
},
"destinations": {
"base": destination_base,
"year_month": year_month,
"day": day,
"file": destination_file,
},
}
def get_current_image(self):
"""Download the image from the URL and save it to the project directory.
:raises SystemExit: If the URL is missing or the date format is invalid.
"""
date_param = getattr(self.args, "date", None)
if date_param is not None:
try:
date = datetime.strptime(date_param, "%Y-%m-%d %H:%M:%S")
except ValueError:
logging.error(AeonViewMessages.INVALID_DATE)
sys.exit(1)
else:
date = datetime.now()
img_path = date.strftime("img/%Y-%m/%d")
img_name = date.strftime("%H-%M-%S")
if self.url is None:
logging.error(AeonViewMessages.INVALID_URL)
sys.exit(1)
file_ext = AeonViewHelpers.get_extension(self.url)
dest_dir = AeonViewHelpers.build_path(self.project_path, img_path)
dest_file = AeonViewHelpers.build_path(
dest_dir, f"{img_name}{file_ext}"
)
logging.info("Saving image to %s", dest_file)
if not self.simulate:
AeonViewHelpers.mkdir_p(dest_dir)
self.download_image(dest_file)
else:
logging.info("Simulate: would create %s", dest_dir)
logging.info(
"Simulate: would download %s to %s", self.url, dest_file
)
def download_image(self, destination: Path | str):
"""Download the image using Python's requests library.
:param destination: Path where the image will be saved.
:raises SystemExit: If the URL is missing, destination is not a Path,
or the HTTP request fails.
"""
if self.url is None:
logging.error(AeonViewMessages.INVALID_URL)
sys.exit(1)
if not isinstance(destination, Path):
logging.error("Invalid destination path.")
sys.exit(1)
if not self.simulate:
logging.info("Downloading image from %s", self.url)
response = requests.get(self.url, stream=True, timeout=10)
if response.status_code == 200:
with open(destination, "wb") as f:
for chunk in response.iter_content(1024):
f.write(chunk)
logging.info(
"%s: %s", AeonViewMessages.DOWNLOAD_SUCCESS, destination
)
else:
logging.error(
"%s: %s", AeonViewMessages.DOWNLOAD_FAILURE, self.url
)
sys.exit(1)
else:
logging.info(
"Simulate: would download %s to %s", self.url, destination
)
if __name__ == '__main__':
aeonview(sys.argv)
class AeonViewVideos:
"""Handle video generation and management.
Generates daily timelapse videos from images, then concatenates daily
videos into monthly and yearly compilations using ffmpeg.
"""
def __init__(
self, project_path: Path, args: argparse.Namespace | None = None
):
"""Initialize the AeonViewVideos class.
:param project_path: Path to the project directory.
:param args: Command-line arguments passed to the class.
"""
self.project_path = project_path
self.args = args
self.simulate = getattr(args, "simulate", False)
self.fps = getattr(args, "fps", 10)
self.day = getattr(args, "day", None)
self.month = getattr(args, "month", None)
self.year = getattr(args, "year", None)
self.path_images = AeonViewHelpers.build_path(self.project_path, "img")
self.path_videos = AeonViewHelpers.build_path(self.project_path, "vid")
def generate_daily_video(self):
"""Generate a daily timelapse video from images.
:raises SystemExit: If the input image directory does not exist.
"""
year_month = f"{self.year}-{self.month}"
input_dir = AeonViewHelpers.build_path(
self.path_images, year_month, self.day
)
output_dir = AeonViewHelpers.build_path(self.path_videos, year_month)
output_file = AeonViewHelpers.build_path(output_dir, f"{self.day}.mp4")
ffmpeg_cmd = AeonViewHelpers.generate_ffmpeg_command(
input_dir, output_file, self.fps
)
logging.info("Generating video from %s", input_dir)
logging.info("Output file will be %s", output_file)
if not self.simulate:
logging.info("Running ffmpeg command: %s", " ".join(ffmpeg_cmd))
if not input_dir.exists():
logging.error("Input directory %s does not exist", input_dir)
sys.exit(1)
AeonViewHelpers.mkdir_p(output_dir)
subprocess.run(ffmpeg_cmd, check=True)
logging.info(
"%s: %s", AeonViewMessages.VIDEO_GENERATION_SUCCESS, output_file
)
else:
logging.info("Simulate: would run %s", " ".join(ffmpeg_cmd))
def generate_monthly_video(self):
"""Generate a monthly video by concatenating daily videos.
Discovers daily ``.mp4`` files in ``vid/YYYY-MM/`` and concatenates
them, excluding the monthly output file itself to avoid corruption
on re-runs.
:raises SystemExit: If the input directory is missing or contains
no daily videos.
"""
year_month = f"{self.year}-{self.month}"
output_dir = AeonViewHelpers.build_path(self.path_videos, year_month)
output_file = AeonViewHelpers.build_path(
output_dir, f"{year_month}.mp4"
)
if not output_dir.exists():
logging.error("Input directory %s does not exist", output_dir)
sys.exit(1)
daily_videos = sorted(
f for f in output_dir.glob("*.mp4") if f != output_file
)
if not daily_videos:
logging.error("No daily videos found in %s", output_dir)
sys.exit(1)
self._concatenate_videos(
f"monthly video for {year_month}", daily_videos, output_file
)
def generate_yearly_video(self):
"""Generate a yearly video by concatenating monthly videos.
Discovers monthly ``.mp4`` files across ``vid/YYYY-*/`` directories
and concatenates them into a single yearly video.
:raises SystemExit: If no monthly videos are found for the year.
"""
year = self.year
output_dir = AeonViewHelpers.build_path(self.path_videos, year)
output_file = AeonViewHelpers.build_path(output_dir, f"{year}.mp4")
monthly_videos = sorted(self.path_videos.glob(f"{year}-*/{year}-*.mp4"))
if not monthly_videos:
logging.error("No monthly videos found for year %s", year)
sys.exit(1)
self._concatenate_videos(
f"yearly video for {year}", monthly_videos, output_file
)
def _concatenate_videos(
self, label: str, input_videos: list[Path], output_file: Path
) -> None:
"""Concatenate video files into one using ffmpeg concat.
:param label: Human-readable label for log messages
(e.g. "monthly video for 2025-04").
:param input_videos: Paths of input videos to concatenate.
:param output_file: Path for the resulting video.
"""
logging.info("Generating %s", label)
logging.info("Output file will be %s", output_file)
if not self.simulate:
AeonViewHelpers.mkdir_p(output_file.parent)
cmd, concat_file = AeonViewHelpers.generate_concat_command(
input_videos, output_file
)
logging.info("Running ffmpeg command: %s", " ".join(cmd))
try:
subprocess.run(cmd, check=True)
logging.info(
"%s: %s",
AeonViewMessages.VIDEO_GENERATION_SUCCESS,
output_file,
)
finally:
concat_file.unlink(missing_ok=True)
else:
logging.info(
"Simulate: would concatenate %d videos into %s",
len(input_videos),
output_file,
)
class AeonViewHelpers:
"""Utility methods for paths, argument parsing, and ffmpeg."""
@staticmethod
def check_date(year: int, month: int, day: int) -> bool:
"""Check if the given year, month, and day form a valid date.
:param year: Year to check.
:param month: Month to check.
:param day: Day to check.
:return: True if valid date, False otherwise.
"""
try:
date = datetime(year, month, day)
return date.year == year and date.month == month and date.day == day
except ValueError:
return False
@staticmethod
def mkdir_p(path: Path):
"""Create a directory and all parent directories if they do not exist.
:param path: Path to the directory to create.
"""
path.mkdir(parents=True, exist_ok=True)
@staticmethod
def build_path(base: Path, *args) -> Path:
"""Build a path from the base and additional arguments.
:param base: Base path.
:param args: Parts of the path to join.
:return: Structured and resolved path.
"""
return Path(base).joinpath(*args or []).resolve()
@staticmethod
def parse_arguments():
"""Parse command-line arguments.
:return: Tuple of (parsed Namespace, ArgumentParser instance).
"""
dest_default = str(Path.cwd() / "projects")
parser = argparse.ArgumentParser(
description="aeonview - timelapse generator using ffmpeg"
)
parser.add_argument(
"--mode",
choices=["image", "video"],
default="image",
help="Run mode",
)
parser.add_argument("--project", help="Project name", default="default")
parser.add_argument(
"--dest", default=dest_default, help="Destination root path"
)
parser.add_argument("--url", help="Webcam URL (required in image mode)")
parser.add_argument(
"--fps", type=int, default=10, help="Frames per second"
)
parser.add_argument(
"--generate", help="Date for video generation (YYYY-MM-DD)"
)
parser.add_argument(
"--timeframe",
choices=["daily", "monthly", "yearly"],
default="daily",
)
parser.add_argument(
"--simulate",
action="store_true",
help="Simulation mode",
default=False,
)
parser.add_argument(
"--verbose",
action="store_true",
help="Verbose output",
default=False,
)
args = parser.parse_args()
return args, parser
@staticmethod
def get_extension(url: str | None) -> str | None:
"""Get the file extension from the URL.
:param url: URL to extract the extension from.
:return: File extension string, or None if url is None.
"""
if url is None:
logging.error(AeonViewMessages.INVALID_IMAGE_EXTENSION)
return None
url_lower = url.lower()
if url_lower.endswith(".jpeg"):
return ".jpeg"
if url_lower.endswith(".png"):
return ".png"
if url_lower.endswith(".gif"):
return ".gif"
if url_lower.endswith(".webp"):
return ".webp"
return ".jpg"
@staticmethod
def setup_logger(verbose: bool):
"""Set up the logger for the application.
:param verbose: Enable verbose logging if True.
"""
level = logging.DEBUG if verbose else logging.INFO
logging.basicConfig(level=level, format="[%(levelname)s] %(message)s")
@staticmethod
def generate_ffmpeg_command(
input_dir: Path, output_file: Path, fps: int = 10
) -> list:
"""Generate the ffmpeg command to create a video from images.
:param input_dir: Directory containing the images.
:param output_file: Path to the output video file.
:param fps: Frames per second for the video.
:return: ffmpeg command as a list of strings.
"""
return [
"ffmpeg",
"-y",
"-framerate",
str(fps),
"-pattern_type",
"glob",
"-i",
str(input_dir / "*.{jpg,jpeg,png,gif,webp}"),
"-c:v",
"libx264",
"-pix_fmt",
"yuv420p",
str(output_file),
]
@staticmethod
def generate_concat_command(
input_files: list[Path], output_file: Path
) -> tuple[list[str], Path]:
"""Generate an ffmpeg concat demuxer command for joining video files.
:param input_files: List of input video file paths.
:param output_file: Path to the output video file.
:return: Tuple of (command list, temp concat file path).
"""
concat_list = tempfile.NamedTemporaryFile(
mode="w", suffix=".txt", delete=False
)
for f in input_files:
concat_list.write(f"file '{f}'\n")
concat_list.close()
cmd = [
"ffmpeg",
"-y",
"-f",
"concat",
"-safe",
"0",
"-i",
concat_list.name,
"-c",
"copy",
str(output_file),
]
return cmd, Path(concat_list.name)
class AeonViewApp:
"""Main application class for AeonView.
Parses arguments and dispatches to image download or video generation
workflows based on the selected mode.
"""
def __init__(self):
args, parser = AeonViewHelpers.parse_arguments()
self.args: argparse.Namespace = args
self.parser: argparse.ArgumentParser = parser
if self.args is None:
logging.error("No arguments provided.")
self.parser.print_help()
sys.exit(1)
AeonViewHelpers.setup_logger(self.args.verbose)
self.base_path = Path(self.args.dest).resolve()
def run(self):
"""Execute the application based on the provided arguments."""
if self.args.simulate:
logging.info("Simulation mode active. No actions will be executed.")
if self.args.mode == "image":
self.process_image()
elif self.args.mode == "video":
self.process_video()
def process_image(self):
"""Process image download and saving based on the provided arguments.
:raises SystemExit: If the URL is missing or invalid.
"""
if not self.args.url or self.args.url is None:
logging.error("--url is required in image mode")
self.parser.print_help()
sys.exit(1)
if not isinstance(self.args.url, str) or not self.args.url.startswith(
"http"
):
logging.error("%s: %s", AeonViewMessages.INVALID_URL, self.args.url)
sys.exit(1)
url = self.args.url
project = self.args.project or "default"
if project == "default" and url:
project = hashlib.md5(url.encode()).hexdigest()[:5]
project_path = AeonViewHelpers.build_path(self.base_path, project)
avi = AeonViewImages(project_path, url, self.args)
avi.get_current_image()
def process_video(self):
"""Process video generation based on the provided arguments.
:raises SystemExit: If the project is missing, invalid, or the
generate date cannot be parsed.
"""
if not self.args.project:
logging.error("--project is required in video mode")
self.parser.print_help()
sys.exit(1)
if not isinstance(self.args.project, str):
logging.error("Invalid project name: %s", self.args.project)
sys.exit(1)
project_path = AeonViewHelpers.build_path(
self.base_path, self.args.project
)
if not project_path.exists():
logging.error("Project path %s does not exist.", project_path)
sys.exit(1)
generate_date = None
try:
generate_date = (
datetime.strptime(self.args.generate, "%Y-%m-%d")
if self.args.generate
else datetime.today() - timedelta(days=1)
)
except ValueError:
logging.error(AeonViewMessages.INVALID_DATE)
sys.exit(1)
year = generate_date.strftime("%Y")
month = generate_date.strftime("%m")
day = generate_date.strftime("%d")
self.args.day = day
self.args.month = month
self.args.year = year
args: argparse.Namespace = self.args
avm = AeonViewVideos(project_path, args)
if self.args.timeframe == "daily":
avm.generate_daily_video()
elif self.args.timeframe == "monthly":
avm.generate_monthly_video()
elif self.args.timeframe == "yearly":
avm.generate_yearly_video()
if __name__ == "__main__": # pragma: no cover
app = AeonViewApp()
app.run()
# vim: set tw=100 fo=cqt wm=0 et: