|
#!/usr/bin/env python3 |
|
""" |
|
images2telegraph - A simple Telegraph gallery uploading tool with CLI |
|
""" |
|
|
|
import json |
|
import os.path |
|
import os |
|
import traceback |
|
import argparse |
|
from dataclasses import dataclass |
|
from typing import (Iterable, Optional) |
|
from argparse import (ArgumentParser, FileType) |
|
from requests.exceptions import RequestException |
|
|
|
import rich |
|
|
|
from dominate import tags |
|
|
|
from html_telegraph_poster import TelegraphPoster |
|
from html_telegraph_poster.errors import Error |
|
from html_telegraph_poster.utils import DocumentPreprocessor |
|
|
|
|
|
SCRIPT_URL = ('https://gist.github.com/ookiineko/' |
|
'012dc790ac129bae42218830fc0a173f') |
|
ALLOWED_EXTS = ('jpg', 'jpeg', 'png', 'gif') |
|
|
|
|
|
@dataclass |
|
class PageSpec: |
|
""" |
|
just a handy data structure for passing many args |
|
""" |
|
title: Optional[str] |
|
author: Optional[str] |
|
homepage: Optional[str] |
|
html: str |
|
|
|
|
|
@dataclass |
|
class UploadOpt: |
|
""" |
|
options for upload() |
|
""" |
|
jobs: int |
|
before: bool |
|
dry_run: bool |
|
|
|
|
|
def _check_path(path: str): |
|
""" |
|
err if path is not a valid file (doesn't exist or is a dir) |
|
""" |
|
assert os.path.isfile(path) and os.access(path, os.R_OK), \ |
|
f'path {path!r} is invalid or not readable' |
|
|
|
|
|
def _check_ext(path: str): |
|
""" |
|
err if file extension is not allowed by Telegraph server |
|
""" |
|
ext = path.rsplit('.', 1)[-1] |
|
assert ext in ALLOWED_EXTS, f'extension {ext!r} is ' \ |
|
f'not allowed in path {path!r}' |
|
|
|
|
|
def _get_clean_lines(lines: str) -> Iterable[str]: |
|
""" |
|
strip the beginning and trailing whitespace |
|
chars and filter empty lines |
|
""" |
|
return filter(bool, (line.strip() for line in lines.split('\n'))) |
|
|
|
|
|
def generate_html(manifest: Iterable[str], title: str) -> str: |
|
""" |
|
generate a fixed shitty HTML layout for the gallery |
|
from the image files |
|
|
|
note: local path will be uploaded and converted to URLs |
|
pointing to Telegraph server later in upload() |
|
""" |
|
|
|
# prepare figs |
|
figs = [] |
|
idx = -1 |
|
for idx, path in enumerate(manifest): |
|
_check_path(path) |
|
_check_ext(path) |
|
with tags.figure() as fig: |
|
# attach img to fig |
|
tags.img(src=path) |
|
figs.append(fig) |
|
count = idx + 1 |
|
# add figcaption for each fig |
|
for idx, fig in enumerate(figs): |
|
with fig: |
|
with tags.figcaption(): |
|
tags.i(f'Page {idx + 1} of {count} - {title}') |
|
# construct the gallery layout |
|
with tags.h3() as prologue: |
|
tags.i(f'(Total {count} page(s) to read)') |
|
root_elems = [ |
|
prologue |
|
] |
|
# added padding between each fig |
|
for fig in figs: |
|
root_elems.append(fig) |
|
with tags.p() as pad_para: |
|
tags.br() |
|
root_elems.append(pad_para) |
|
with tags.h4() as epilogue: |
|
with tags.i('This gallery is automatically generated with '): |
|
tags.a('images2telegraph', href=SCRIPT_URL) |
|
root_elems.append(epilogue) |
|
# render html |
|
return '\n'.join( |
|
root_elem.render(pretty=True) for root_elem in root_elems) |
|
|
|
|
|
def __dummy(*_args, **_kwargs): |
|
pass |
|
|
|
|
|
def upload(access_token: str, page_path: str, spec: PageSpec, |
|
opts: UploadOpt) -> Optional[PageSpec]: |
|
""" |
|
upload a page to Telegraph |
|
""" |
|
poster = TelegraphPoster(use_api=True, access_token=access_token) |
|
# get the old page info and content (if requested) |
|
old_page: Optional[dict] = None |
|
try: |
|
old_page: dict = poster.get_page(page_path, |
|
return_content=opts.before) |
|
except KeyError: |
|
pass |
|
if old_page is None: |
|
raise Error(f'page path {page_path!r} does not exist') |
|
res = None |
|
if opts.before: |
|
res = PageSpec(title=old_page['title'], |
|
author=old_page.get('author_name'), |
|
homepage=old_page.get('author_url'), |
|
html=old_page['html']) |
|
if not opts.dry_run: |
|
# upload images |
|
pre_proc = DocumentPreprocessor(spec.html) |
|
# HACK: buggy function?? |
|
# pylint: disable=protected-access |
|
old_mla_func = pre_proc._make_links_absolute |
|
pre_proc._make_links_absolute = __dummy |
|
pre_proc.upload_all_images(max_workers=opts.jobs) |
|
pre_proc._make_links_absolute = old_mla_func |
|
# get new HTML with remote image URLs |
|
html = pre_proc.get_processed_html() |
|
# do the stuff |
|
poster.edit(title=spec.title, author=spec.author, text=html, |
|
author_url=spec.homepage, path=page_path) |
|
return res |
|
|
|
|
|
def _main(args: argparse.Namespace): |
|
""" |
|
main |
|
""" |
|
try: |
|
# read file inputs |
|
with args.config as file: |
|
cdict = json.loads(file.read()) |
|
with args.manifest as file: |
|
manifest = _get_clean_lines(file.read()) |
|
except json.JSONDecodeError: |
|
traceback.print_exc() |
|
parser.error('config file does not valid JSON str') |
|
except OSError: |
|
traceback.print_exc() |
|
parser.error('unable to read due to above err') |
|
else: |
|
# check config |
|
assert isinstance(cdict, dict), 'config is not a dict' |
|
assert (access_token := cdict.get('access_token')) is not None, \ |
|
'access_token is not in config' |
|
assert isinstance(access_token, str), 'access_token is not a str' |
|
# check other args |
|
assert args.jobs > 0, f'illegal job number \'{args.jobs}\'' |
|
# do the stuff |
|
html = generate_html(manifest, args.title) |
|
try: |
|
old_spec = upload(access_token, args.page_path, |
|
PageSpec(title=args.title, author=args.author, |
|
homepage=args.homepage, html=html), |
|
UploadOpt(args.jobs, args.before, args.debug)) |
|
except RequestException: |
|
traceback.print_exc() |
|
parser.error('failed due to above network error') |
|
except Error: |
|
traceback.print_exc() |
|
parser.error('failed due to above err') |
|
else: |
|
if args.before: |
|
rich.print(old_spec) |
|
|
|
|
|
if __name__ == '__main__': |
|
parser = ArgumentParser('images2telegraph', |
|
description='A simple program that generates ' |
|
'a gallery page from a set of images ' |
|
'and uploads it to Telegraph') |
|
parser.add_argument('--config', '-c', |
|
type=FileType('r'), default='config.json', |
|
help='path to the configuration JSON ' |
|
'file (default is \'config.json\')') |
|
# pylint: disable=consider-using-with, unspecified-encoding |
|
parser.add_argument('--manifest', '-m', |
|
type=FileType('r'), default=open(os.devnull, 'r'), |
|
help='path to a text file where each line contains ' |
|
'the path to an image file (use a dash "-" to ' |
|
'read from stdin, default is a dummy file') |
|
parser.add_argument('--jobs', '-j', type=int, default=3, |
|
help='thread number for uploading images to ' |
|
'Telegraph server (default is 3)') |
|
parser.add_argument('--before', '-b', action='store_true', |
|
help='print the previous page info and content before ' |
|
'overwriting (default is disabled)') |
|
parser.add_argument('--debug', '-d', action='store_true', |
|
help='to enable debugging mode, aka dry run ' |
|
'(default is disabled)') |
|
parser.add_argument('--author', '-a', type=str, |
|
default=None, help='desired author name (cleared ' |
|
'if not specified)') |
|
parser.add_argument('--homepage', '-u', type=str, |
|
default=None, help='desired author url (cleared ' |
|
'if not specified)') |
|
parser.add_argument('page_path', type=str, |
|
help='path that refers to a previously created ' |
|
'Telegraph page to work with') |
|
parser.add_argument('title', type=str, |
|
help='desired page title (required)') |
|
_main(parser.parse_args()) |