This repository has been archived on 2025-12-30. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
upload-generator/main.py
Bad Manners 0ae3e97186 Add userlink priorities and clean up
Also added Twitter as a supported website
2023-06-30 16:46:21 -03:00

131 lines
6 KiB
Python

import argparse
import io
import os
import re
import subprocess
import tempfile
from parse import parse_description
OUT_DIR = './out'
def get_rtf_styles(rtf_source: str):
match_list = re.findall(r'\\s(\d+)(?:\\sbasedon\d+)?\\snext\d+((?:\\[a-z0-9]+ ?)+)(?: ([A-Z][a-zA-Z ]*));', rtf_source)
if not match_list:
raise ValueError(f'Couldn\'t find valid RTF styles')
rtf_styles = {}
for (style_number, partial_rtf_style, style_name) in match_list:
rtf_style = r'\s' + style_number + partial_rtf_style
rtf_styles[int(style_number)] = rtf_style
rtf_styles[style_name] = rtf_style
return rtf_styles
def main(story_path=None, description_path=None, config_path='./config.json', keep_out_dir=False, ignore_empty_files=False):
remove_out_dir = not keep_out_dir and os.path.isdir(OUT_DIR)
with tempfile.TemporaryDirectory() as tdir:
# Clear OUT_DIR if it exists and shouldn't be kept
if remove_out_dir:
os.rename(OUT_DIR, os.path.join(tdir, 'old_out'))
if not os.path.isdir(OUT_DIR):
os.mkdir(OUT_DIR)
try:
# Convert original file to .rtf (Aryion) and .txt (all others)
if story_path:
story_filename = os.path.split(story_path)[1].rsplit('.')[0]
txt_out_path = os.path.join(OUT_DIR, f'{story_filename}.txt')
txt_tmp_path = os.path.join(tdir, f'{story_filename}.txt')
rtf_out_path = os.path.join(OUT_DIR, f'{story_filename}.rtf')
RE_EMPTY_LINE = re.compile('^$')
is_only_empty_lines = True
ps = subprocess.Popen(('libreoffice', '--cat', story_path), stdout=subprocess.PIPE)
with open(txt_out_path, 'w', newline='\r\n') as txt_out, open(txt_tmp_path, 'w') as txt_tmp:
needs_empty_line = False
for line in io.TextIOWrapper(ps.stdout, encoding='utf-8-sig'):
# Remove empty lines
line = line.strip()
if RE_EMPTY_LINE.search(line) and not is_only_empty_lines:
needs_empty_line = True
else:
if is_only_empty_lines:
txt_out.writelines((line,))
txt_tmp.writelines((line,))
is_only_empty_lines = False
else:
if needs_empty_line:
txt_out.writelines(('\n\n', line))
needs_empty_line = False
else:
txt_out.writelines(('\n', line))
txt_tmp.writelines(('\n', line))
txt_out.writelines(('\n'))
if is_only_empty_lines:
error = f'Story processing returned empty file: libreoffice --cat {story_path}'
if ignore_empty_files:
print(f'Ignoring error ({error})')
else:
raise RuntimeError(error)
# Convert temporary .txt to .rtf
subprocess.run(['libreoffice', '--convert-to', 'rtf:Rich Text Format', '--outdir', OUT_DIR, txt_tmp_path], check=True, capture_output=True)
# Convert monospace font ('Preformatted Text') to serif ('Normal')
with open(rtf_out_path, 'r+') as f:
rtf = f.read()
rtf_styles = get_rtf_styles(rtf)
monospace_style = rtf_styles['Preformatted Text'] # rtf_styles[20]
serif_style = rtf_styles['Normal'] # rtf_styles[0]
f.seek(0)
f.write(rtf.replace(monospace_style, serif_style))
f.truncate()
# Parse FA description and convert for each website
if description_path:
ps = subprocess.Popen(('libreoffice', '--cat', description_path), stdout=subprocess.PIPE)
desc = '\n'.join(line.strip() for line in io.TextIOWrapper(ps.stdout, encoding='utf-8-sig'))
if not desc or re.match(r'^\s+$', desc):
error = f'Description processing returned empty file: libreoffice --cat {description_path}'
if ignore_empty_files:
print(f'Ignoring error ({error})')
else:
raise RuntimeError(error)
parse_description(desc, config_path, OUT_DIR)
except subprocess.CalledProcessError as e:
if remove_out_dir:
# Revert directory removal on error
os.rename(OUT_DIR, os.path.join(tdir, 'get_rid_of_this'))
os.rename(os.path.join(tdir, 'old_out'), OUT_DIR)
print(f'Command exited with code {e.returncode}: {e.stderr.decode("utf-8-sig")}')
exit(1)
except Exception as e:
if remove_out_dir:
# Revert directory removal on error
os.rename(OUT_DIR, os.path.join(tdir, 'get_rid_of_this'))
os.rename(os.path.join(tdir, 'old_out'), OUT_DIR)
raise e
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='generate multi-gallery upload-ready files')
parser.add_argument('-s', '--story', dest='story_path',
help='path of LibreOffice-readable story file')
parser.add_argument('-d', '--description', dest='description_path',
help='path of BBCode-formatted description file')
parser.add_argument('-c', '--config', dest='config_path', default='./config.json',
help='path of JSON configuration file')
parser.add_argument('-k', '--keep-out-dir', dest='keep_out_dir', action='store_true',
help='whether output directory contents should be kept')
parser.add_argument('-i', '--ignore-empty-files', dest='ignore_empty_files', action='store_true',
help='do not raise an error if any input file is empty or whitespace-only')
args = parser.parse_args()
if not any([args.story_path, args.description_path]):
parser.error('at least one of ( --story | --description ) must be set')
if args.story_path and not os.path.isfile(args.story_path):
parser.error('--story must be a valid file')
if args.description_path and not os.path.isfile(args.description_path):
parser.error('--description must be a valid file')
if args.config_path and not os.path.isfile(args.config_path):
parser.error('--config must be a valid file')
main(**vars(args))