diff --git a/distribusi/cli.py b/distribusi/cli.py index ac63c48..5421a69 100644 --- a/distribusi/cli.py +++ b/distribusi/cli.py @@ -6,83 +6,78 @@ from distribusi.distribusi import distribusify def build_argparser(): parser = argparse.ArgumentParser( - """ + """ distribusi is a content management system for the web that produces static index pages based on folders in the files system. It is inspired by the automatic index functions featured in several popular web servers. distribusi works by traversing the file system and directory hierarchy to automatically list all the files in the directory, detect the file types and providing them with relevant html classes and tags for easy styling. - """) - - parser.add_argument( - '-d', '--directory', help="Select which directory to distribute", default="." + """ ) parser.add_argument( - '-s', '--style', help="Select a CSS style sheet to include" + "-d", "--directory", help="Select which directory to distribute", default="." ) + parser.add_argument("-s", "--style", help="Select a CSS style sheet to include") + parser.add_argument( - '-v', '--verbose', help="Print verbose debug output", action="store_true" + "-v", "--verbose", help="Print verbose debug output", action="store_true" ) parser.add_argument( - '-t', - '--thumbnail', + "-t", + "--thumbnail", help="Generate 450x450 thumbnails for images", action="store_true", ) parser.add_argument( - '-n', - '--no-template', + "-n", + "--no-template", help="Don't use the template to output html", action="store_true", ) parser.add_argument( - '-nf', - '--no-filenames', - help="Don't add file names to listing", + "-c", + "--captions", + help="Adds image captions based on EXIF metadata", action="store_true", ) parser.add_argument( - '-c', - '--captions', - help="Adds image captions based on EXIF metadata, requires 'exiftool'", + "-r", + "--remove-index", + help="Recursively removes all instances of index.html that have been previously made by distribusi", action="store_true", ) parser.add_argument( - '-r', - '--remove-index', - help="Recursively removes all instances of index.html that have been previously made by distribusi", - action="store_true") - - parser.add_argument( - '-e', - '--exclude-directory', + "-e", + "--exclude-directory", help="Exclude one or multiple directories from indexing", nargs="*", - metavar='DIR') + metavar="DIR", + ) parser.add_argument( - '-f', - '--force', + "-f", + "--force", help="Force whether distribusi overwrites or removes instances of index.html not generated by distribusi, use at own risk!", - action="store_true") + action="store_true", + ) parser.add_argument( - '--no-hidden', - help="Exclude hidden directories", - action="store_true") + "--no-hidden", help="Exclude hidden directories", action="store_true" + ) parser.add_argument( - '--menu-with-index', + "--menu-with-index", help="Append index.html to menu items to aid navigation", - action="store_true") + action="store_true", + ) return parser diff --git a/distribusi/distribusi.py b/distribusi/distribusi.py index 61239cb..0d2e23b 100644 --- a/distribusi/distribusi.py +++ b/distribusi/distribusi.py @@ -1,11 +1,9 @@ import base64 import os -import subprocess -from io import BytesIO import magic from PIL import Image - +from exif import Image as ExifImage from distribusi.page_template import html_footer, html_head from distribusi.mappings import CODE_TYPES, FILE_TYPES, SUB_TYPES @@ -14,62 +12,49 @@ MIME_TYPE = magic.Magic(mime=True) def caption(image): try: - process = subprocess.Popen( - ['exiftool', '-Comment', image], stdout=subprocess.PIPE) - out, err = process.communicate() - except Exception as e: - print(e) - print('Do you have exiftool installed?') - try: - caption = out.decode("utf-8").split(": ", 1)[1] + with open(image, "rb") as image_file: + exif_image = ExifImage(image_file) + caption = exif_image.communication except Exception as e: - caption = '' print(e) + caption = "" return caption -def thumbnail(image, name, args): +def thumbnail(full_path_image, name, args): + if full_path_image.endswith("_thumbnail.jpg"): + return try: size = (450, 450) - im = Image.open(image) - im.thumbnail(size) - - if (im.mode == 'RGBA'): - bg = Image.new('RGBA', im.size, (255,255,255)) - composite = Image.alpha_composite(bg, im) - im=composite.convert('RGB') - - output = BytesIO() - im.save(output, format='JPEG') - im_data = output.getvalue() - data_url = base64.b64encode(im_data).decode() - if args.captions: - cap = caption(image) - else: - cap = name - return ( - "
{}
" - ).format(name, data_url, cap) + thumbnail_image = Image.open(full_path_image) + thumbnail_image.thumbnail(size) + + if thumbnail_image.mode == "RGBA": + bg = Image.new("RGBA", im.size, (255, 255, 255)) + composite = Image.alpha_composite(bg, thumbnail_image) + thumbnail_image = composite.convert("RGB") + + image_filename_no_ext = os.path.splitext(full_path_image)[0] + thumbnail_filename = f"{image_filename_no_ext}_thumbnail.jpg" + thumbnail_image.save(thumbnail_filename, format="JPEG") + return os.path.basename(thumbnail_filename) except Exception as e: - print('Thumbnailer:', e) - return "
{}
".format(name, name, name) + print("Thumbnailer:", e) + return -def div(args, type_, subtype, tag, name): - id_name = name.split('.')[0].replace(' ', '_') - if args.no_filenames: - filename = '' - else: - filename = '{}'.format(name) +def format_div(args, type_, subtype, tag, name): + id_name = name.split(".")[0].replace(" ", "_") + filename = f'{name}' - if 'image' in type_: + if "image" in type_: html = '
{}
' - elif 'pdf' in subtype: - html = '
{}' + filename + '
' - elif 'dir' in type_ or 'html' in subtype or 'unkown-file' in subtype: + elif "pdf" in subtype: + html = '
{}' + filename + "
" + elif "dir" in type_ or "html" in subtype or "unkown-file" in subtype: html = '
{}
' else: - html = '
{}' + filename + '
' + html = '
{}' + filename + "
" return html.format(id_name, subtype, tag) @@ -80,140 +65,159 @@ def check_distribusi_index(args, index): """ if not args.force: - with open(index, 'r') as f: + with open(index, "r") as f: if '' in f.read(): return True else: if args.verbose: - print(index, 'not generated by distribusi, skipping') + print(index, "not generated by distribusi, skipping") return False elif args.force: return True -def write_index(args,index, html, html_head, html_footer): - with open(index, 'w') as f: + +def write_index(args, index, html, html_head, html_footer): + with open(index, "w") as index_file: if not args.no_template: if args.style: - fs = open(args.style, "r") - style = fs.read() + file_style = open(args.style, "r") + style = file_style.read() styled_html_head = html_head % style else: - styled_html_head = html_head % '' - f.write(styled_html_head) + styled_html_head = html_head % "" + index_file.write(styled_html_head) for line in html: - f.write(line + '\n') + index_file.write(line + "\n") if not args.no_template: - f.write(html_footer) + index_file.write(html_footer) + + +def handle_text_files(name, full_path): + if name.endswith(".html") or subtype == "html": + subtype = "html" + # what types of text files to expand + tag = '
{}
'.format( + name, open(full_path).read() + ) + elif subtype in CODE_TYPES or name.endswith(".txt"): + # if the plain text is code, + # which types do we wrap in pre-tags? + tag = "
" + open(full_path).read() + "
" + else: + subtype = subtype + " unkown-file" + tag = "{}" + # a = FILE_TYPES[type_] + return subtype, tag + + +def handle_image_files(name, full_path, args): + if args.thumbnail: + caption = "" + thumbnail_filename = thumbnail(full_path, name, args) + if thumbnail_filename is None: + return + if args.captions: + caption = caption(full_path) + return f"
{caption}
" + + return FILE_TYPES[type_].format(name, caption) + + +def remove_index_html(root, files): + index = os.path.join(root, "index.html") + if "index.html" in files: + try: + if check_distribusi_index(args, index): + if args.verbose: + print("Removing index.html from", root) + os.remove(index) + except Exception as e: + print(e) + return + + +def remove_hidden_files_and_folders(dirs, files): + dirs = list(filter(lambda d: not d.startswith("."), dirs)) + files = list(filter(lambda f: not f.startswith("."), files)) + return dirs, files def distribusify(args, directory): # noqa for root, dirs, files in os.walk(directory): + html = [] if args.exclude_directory: if args.verbose: - print('Excluding directory:', ", ".join(args.exclude_directory)) + print("Excluding directory:", ", ".join(args.exclude_directory)) dirs[:] = [d for d in dirs if d not in args.exclude_directory] if args.no_hidden: - dirs = list(filter(lambda d: not d.startswith('.'), dirs)) - files = list(filter(lambda f: not f.startswith('.'), files)) + dirs, files = remove_hidden_files_and_folders(dirs, files) + + if args.remove_index: + remove_index_html(root, files) + + if args.verbose: + print("Generating directory listing for", root) - dirs.sort() - files.sort() + for name in sorted(files): + if "index.html" in name: + continue - if not args.remove_index: - html = [] + if name.endswith("_thumbnail.jpg"): + continue + + full_path = os.path.join(root, name) + mime = MIME_TYPE.from_file(full_path) + type_, subtype = mime.split("/") + caption = name if args.verbose: - print('Generating directory listing for', root) - - for name in sorted(files): - - if 'index.html' not in name: - full_path = os.path.join(root, name) - mime = MIME_TYPE.from_file(full_path) - # example: MIME plain/text becomes 'type' plain 'subtype' text - type_, subtype = mime.split('/') - - caption = name - - if args.verbose: - print('Found', name, 'as', mime) - - if type_ in FILE_TYPES: - - a = FILE_TYPES[type_].format(name, caption) - - # expansion for different kind of text files - if type_ == 'text': - if name.endswith('.html') or subtype == 'html': - subtype = 'html' - # what types of text files to expand - a = '
{}
'.format(name, open(full_path).read()) - elif subtype in CODE_TYPES or name.endswith('.txt'): - # if the plain text is code, - # which types do we wrap in pre-tags? - a = "
" + open(full_path).read() + "
" - else: - subtype = subtype+' unkown-file' - a = "{}" - # a = FILE_TYPES[type_] - - if type_ == 'image': - if args.thumbnail: - a = thumbnail(full_path, name, args) - if args.no_filenames: - caption = "" - a = FILE_TYPES[type_].format(name, caption) - if args.captions: - caption = caption(full_path) - a = FILE_TYPES[type_].format(name, caption) - - if subtype in SUB_TYPES: - a = SUB_TYPES[subtype] - - if type_ not in FILE_TYPES and subtype not in SUB_TYPES: - # catch exceptions not yet defined in FILE_TYPES or SUB_TYPES - a = "{}" - if args.verbose: - message = 'not in list of file types, adding as plain href: \n' - print(type_, subtype, message, name) - subtype = subtype + ' unkown-file' - - a = a.replace('{}', name) - - html.append(div(args, type_, subtype, a, name)) - - if root != directory: - if args.menu_with_index: - html.append('../') - else: - html.append('../') - - for name in dirs: - if args.menu_with_index: - a = "{}".replace('{}', name) - else: - a = "{}/".replace('{}', name) - - html.insert(0, div(args, 'dir', 'dir', a, 'folder')) - - index = os.path.join(root, 'index.html') - if os.path.exists(index): - if check_distribusi_index(args, index): - write_index(args,index,html, html_head, html_footer) - elif not os.path.exists(index): - write_index(args,index,html, html_head, html_footer) + print("Found", name, "as", mime) + if type_ in FILE_TYPES: + match type_: + case "text": + subtype, tag = handle_text_files(name, full_path) + case "image": + tag = handle_image_files(name, full_path, args) + if tag is None: + continue + case _: + tag = FILE_TYPES[type_].format(name, caption) + + if subtype in SUB_TYPES: + tag = SUB_TYPES[subtype] + + if type_ not in FILE_TYPES and subtype not in SUB_TYPES: + # catch exceptions not yet defined in FILE_TYPES or SUB_TYPES + tag = "{}" + if args.verbose: + message = "not in list of file types, adding as plain href: \n" + print(type_, subtype, message, name) + subtype = subtype + " unkown-file" - if args.remove_index: - index = os.path.join(root, 'index.html') - if 'index.html' in files: - try: - if check_distribusi_index(args, index): - if args.verbose: - print('Removing index.html from', root) - os.remove(index) - except Exception as e: - print(e) + tag = tag.replace("{}", name) + html.append(format_div(args, type_, subtype, tag, name)) + + if root != directory: + if args.menu_with_index: + html.append('../') + else: + html.append('../') + + for name in sorted(dirs): + if args.menu_with_index: + tag = "{}".replace("{}", name) + else: + tag = "{}/".replace("{}", name) + + html.insert(0, format_div(args, "dir", "dir", tag, "folder")) + + index = os.path.join(root, "index.html") + if os.path.exists(index): + if check_distribusi_index(args, index): + write_index(args, index, html, html_head, html_footer) + elif not os.path.exists(index): + write_index(args, index, html, html_head, html_footer) diff --git a/distribusi/mappings.py b/distribusi/mappings.py index d9fb9dc..668ea2e 100644 --- a/distribusi/mappings.py +++ b/distribusi/mappings.py @@ -1,14 +1,15 @@ -CODE_TYPES = ['x-c', 'x-shellscript', 'x-python'] +CODE_TYPES = ["x-c", "x-shellscript", "x-python"] FILE_TYPES = { - 'image': '
{}
', - 'text': '{}', - 'video': (''), - 'audio': (''), + "image": '
{}
', + "text": '{}', + "video": ("'), + "audio": (''), } SUB_TYPES = { - 'pdf': ( - '' - '') + "pdf": ( + '' + '' + ) } diff --git a/setup.py b/setup.py index 678064a..5c282f1 100644 --- a/setup.py +++ b/setup.py @@ -1,28 +1,28 @@ from setuptools import find_packages, setup -dependencies = ['pillow >= 10.3.0', 'python-magic >= 0.4.15, < 1.0'] +dependencies = ["pillow >= 10.3.0", "python-magic >= 0.4.15, < 1.0", "exif >= 1.6.0"] -with open('README.md', 'r') as handle: +with open("README.md", "r") as handle: long_description = handle.read() setup( - name='distribusi', - version='0.0.10', - url='https://git.vvvvvvaria.org/varia/distribusi', - license='GPLv3', - author='Varia', + name="distribusi", + version="0.0.11", + url="https://git.vvvvvvaria.org/varia/distribusi", + license="GPLv3", + author="Varia", description=( - 'Distribusi is a content management system for ' - 'the web that produces static pages based on ' - 'the file system.' + "Distribusi is a content management system for " + "the web that produces static pages based on " + "the file system." ), long_description=long_description, - long_description_content_type='text/markdown', - packages=find_packages(exclude=['tests']), + long_description_content_type="text/markdown", + packages=find_packages(exclude=["tests"]), include_package_data=True, zip_safe=False, - platforms='any', + platforms="any", install_requires=dependencies, - entry_points={'console_scripts': ['distribusi = distribusi.cli:cli_entrypoint']}, - classifiers=['Programming Language :: Python :: 3', 'Environment :: Console'], + entry_points={"console_scripts": ["distribusi = distribusi.cli:cli_entrypoint"]}, + classifiers=["Programming Language :: Python :: 3", "Environment :: Console"], )