Featured image of post Python script - Upload images to a image-hosting service and generate markdown links

Python script - Upload images to a image-hosting service and generate markdown links

Overview

This Python script is designed to automate the process of uploading images to a image-hosting service and generating markdown links for those images. It supports multiple image formats and can recursively search directories for images. The script also has the capability to handle pairs of images and generate a markdown file with links to the uploaded images.

Features

  • Supports multiple image formats (jpg, jpeg, png, tif, tiff, webp).
  • Recursively searches directories for images.
  • Uploads images using a subprocess call to a command-line tool (assumed to be upic or picgo).
  • Generates markdown links for uploaded images.
  • Handles pairs of images for opening a lossless image in a new tab by clicking the optimized web image.
  • Provides detailed logging for successful uploads and failures.
  • Image file deduplication.

Installation

To use this script, you will need Python installed on your system. Additionally, you will need the command-line tool installed and configured for image uploading, such as upic or picgo.

  1. Install Python from python.org.

  2. Install fdupes fro file deduplication.

    brew install fdupes
    
  3. Install uPic from uPic, or

  4. Install PicGo-Core from PicGo-Core

Usage

To use the script, run it from the command line with the desired arguments. Here’s a breakdown of the available arguments:

  • image_dir_list: A list of directories to search for images. At least one directory is required.
  • -i, --inner_image_dir: The directory for inner images, usually larger images. If specified, only one directory in image_dir_list is allowed.
  • --ext: A comma-separated list of input image extensions. Default is ["all"].
  • --md_links: The file name for the output markdown links. Default is "markdown_links.txt".
  • -s, --subdir: A flag to indicate whether to recursively search subdirectories. Default is True.

Example command to run the script:

python script_name.py /path/to/images -i /path/to/inner/images --ext jpg png --md_links links.md

Output

The script will generate a markdown file with links to the uploaded images. It will also log information about successful uploads and any failures.

Logging

The script uses the Python logging module to log information to the console. The log level is set to INFO, and logs include a timestamp and the log level.

Code

import argparse
import logging
import re
import subprocess
from pathlib import Path

# Enable logger, print to console with timestamp and log level
logging.basicConfig(
    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)


def expand_image_extensions(extensions: list[str]) -> list[str]:
    """
    Expands a comma-separated list of image extensions to include common variants.

    Args:
    extensions (list[str]): A  list of image extensions.

    Returns:
    list[str]: A list of expanded image extensions.
    """
    supported_extensions = ["jpg", "jpeg", "png", "tif", "tiff", "webp"]

    expanded_extensions = []
    for ext in [s.lower() for s in extensions]:
        if ext == "all":
            expanded_extensions = supported_extensions
            break
        elif ext in ["jpg", "jpeg"]:
            expanded_extensions.extend(["jpg", "jpeg"])
        elif ext in ["tif", "tiff"]:
            expanded_extensions.extend(["tif", "tiff"])
        else:
            expanded_extensions.append(ext)

    return [s.lower() for s in expanded_extensions] + [
        s.upper() for s in expanded_extensions
    ]


def find_image_files(
    directory: Path, extensions: list[str], subdir: bool
) -> list[Path]:
    """
    Finds all image files in the given directory with the specified extensions.

    Args:
    directory (Path): The directory to search for image files.
    extensions (list[str]): A list of image file extensions.
    subdir (bool): Whether to search recursively in subdirectories.

    Returns:
    list[Path]: A list of paths to the found image files.
    """
    return sorted(
        [
            file
            for ext in extensions
            for file in directory.rglob(f"*.{ext}")
            if not file.name.startswith(".")
        ]
        if subdir
        else [
            file
            for ext in extensions
            for file in directory.glob(f"*.{ext}")
            if not file.name.startswith(".")
        ]
    )


def upload_single_image(image_path: Path) -> str:
    """
    Upload an image using PicGo and extract the uploaded image URL.

    Args:
    image_path (Path): The path to the image file to upload.

    Returns:
    str: The URL of the uploaded image, or an empty string if the upload fails.
    """
    try:
        result = subprocess.run(
            # ["picgo", "upload", str(image_path)],
            ["upic", "-u", str(image_path.absolute()), "-o", "url", "-s"],
            capture_output=True,
            text=True,
            check=True,
        )
        match = re.search(rf"https://.*?{image_path.suffix}", result.stdout)
        if match:
            logger.info(f"Uploaded: {match.group(0)}\n")
            return match.group(0)
        else:
            logger.warning("No URL found in upload output.\n")
            return ""
    except subprocess.CalledProcessError as e:
        logger.error(f"Failed to upload {image_path}: {e}\n")
        return ""


def upload_image_pair(
    inner_image_path: Path, outer_image_path: Path
) -> tuple[str, str]:
    """
    Uploads two images to a server and returns their URLs.

    Args:
        inner_image_path (Path): The file path to the inner image.
        outer_image_path (Path): The file path to the outer image.

    Returns:
        Tuple[str, str]: A tuple containing the URLs of the uploaded images.
                          If either image fails to upload, an empty tuple is returned.
    """
    outer_url = upload_single_image(outer_image_path)
    if outer_url:
        inner_url = upload_single_image(inner_image_path)
    if inner_url and outer_url:
        return (inner_url, outer_url)
    return ()


def process_links(
    image_dir_list, inner_image_dir, extensions, markdown_links_filename, subdir
):
    """
    Generate markdown links for webp images in the given directories.

    Args:
    - image_dir_list (list of str): List of directories containing webp images.
    - inner_image_dir (str): Directory for inner images.
    - extensions (list[str]): A list of image extensions.
    - markdown_links_filename (str): File name for the output markdown links.
    - subdir (bool): Whether to recursively search subdirectories.
    """
    if inner_image_dir:
        outer_image_dir_path = Path(image_dir_list[0])
        inner_image_dir_path = Path(inner_image_dir)
        markdown_links_filepath = inner_image_dir_path / markdown_links_filename

        if not inner_image_dir_path.exists():
            logging.error(f"Directory {inner_image_dir_path} does not exist.")
            return

        inner_image_path_list, outer_image_path_list = find_image_files(
            inner_image_dir_path, extensions, subdir
        ), find_image_files(outer_image_dir_path, extensions, subdir)

        image_stem_and_url_list, failed_upload_image_stem_list = [], []
        for inner_image_path, outer_image_path in zip(
            inner_image_path_list, outer_image_path_list
        ):
            (inner_url, outer_url) = upload_image_pair(
                inner_image_path, outer_image_path
            )
            if inner_url and outer_url:
                image_stem_and_url_list.append(
                    (inner_image_path.stem, inner_url, outer_url)
                )
            else:
                failed_upload_image_stem_list.append(inner_image_path.stem)

        with markdown_links_filepath.open("w") as f:
            f.write(f"Successfully uploaded: {len(image_stem_and_url_list)}\n\n")
            for image_stem, inner_url, outer_url in image_stem_and_url_list:
                f.write(f"[![{image_stem}]({inner_url})]({outer_url})\n\n")

            f.write(f"Failed to upload: {len(failed_upload_image_stem_list)}\n\n")
            for image_stem in failed_upload_image_stem_list:
                f.write(f"{image_stem}\n\n")
    else:
        subprocess.run(["fdupes", "-rdN", str(image_dir_path)], check=True)
        for image_dir_path in map(Path, image_dir_list):

            if not image_dir_path.exists():
                logging.error(f"Directory {image_dir_path} does not exist.")
                continue

            markdown_links_filepath = image_dir_path / markdown_links_filename
            with markdown_links_filepath.open("w") as f:
                failed_upload_image_stem_list = []
                for image_path in find_image_files(image_dir_path, extensions, subdir):
                    url = upload_single_image(image_path)
                    if url:
                        f.write(f"![{image_path.stem}]({url})\n\n")
                    else:
                        failed_upload_image_stem_list.append(image_path.stem)

                f.write(f"Failed to upload: {len(failed_upload_image_stem_list)}\n\n")
                for image_stem in failed_upload_image_stem_list:
                    f.write(f"{image_stem}\n\n")


def main():
    """
    Main function to parse arguments and call the markdown link generator.
    """
    parser = argparse.ArgumentParser(
        description="Generate markdown links for webp images."
    )
    parser.add_argument(
        "image_dir_list", nargs="+", help="List of directories containing webp images."
    )
    parser.add_argument(
        "-i",
        "--inner_image_dir",
        help="Directory for inner images, usually larger images.",
    )
    parser.add_argument(
        "--ext",
        nargs="+",
        default=["all"],
        help="Comma-separated list of input image extensions",
    )
    parser.add_argument(
        "--md_links",
        default="markdown_links.txt",
        help="File name for the output markdown links.",
    )
    parser.add_argument(
        "-s",
        "--subdir",
        action="store_false",
        help="Whether to recursively search subdirectories.",
    )

    args = parser.parse_args()

    if args.inner_image_dir and len(args.image_dir_list) != 1:
        logging.error(
            "If --inner_image_dir is specified, img_dir_list must contain only one element."
        )
        return

    process_links(
        args.image_dir_list,
        args.inner_image_dir,
        expand_image_extensions(args.ext),
        args.md_links,
        args.subdir,
    )


if __name__ == "__main__":
    main()
Built with Hugo
Theme Stack designed by Jimmy