From 748234fa73519b0b4781a514c7cad4a1594eef73 Mon Sep 17 00:00:00 2001 From: vincent Date: Wed, 4 Feb 2026 19:59:54 +0100 Subject: [PATCH 1/7] chore: finished readme --- README.md | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0045422..f978fd9 100644 --- a/README.md +++ b/README.md @@ -1 +1,84 @@ -# meowDFer \ No newline at end of file +# meowDFer +### Extract, Convert, Merge +This is a tool made to simplify the processes mentioned above. + +## How to use + +##### Example commands: + +- `py meowDFer.py extract --src s_name --dest d_name` +- `py meowDFer.py convert --src s_name --dest d_name` +- `py meowDFer.py merge --src s_name --dest d_name --vol v_name.txt` +- `py meowDFer.py all --src s_name --dest d_name --vol v_name.txt` +- `py meowDFer.py cm --src s_name --dest d_name --vol v_name.txt` + +## `extract` + +Extracts multiple zip files into one folder. + +1. Create a folder in root, put all .zip files inside. +2. In the root of the directory, open the terminal and run: + +```py meowDFer.py extract --src s_name --dest d_name``` +- s_name: name of folder with zip files +- d_name: name of folder where files to be extracted in + +3. Folder with d_name will be created in root with extracted zip files + +## `convert` + +Converts folder with images (chapters) into PDFs. Folders with images must contain 'chapter', 'ch.', or 'c' with a number. Decimal number chapters get skipped. + +1. Create a folder containing folders of images with chapter and number in name. Or have the folders from 'extract'. +2. In the root of the directory, open terminal and run: + +```py meowDFer.py convert --src s_name --dest d_name``` +- s_name: name of folder with image folders +- d_name: name of folder where chapter PDFs to be extracted in + +3. Folder with d_name will be created in root with PDFs of chapters. + +## `merge` + +Merge many PDF s (chapters) into volumes, using a .txt file. In the .txt file write the upper limit chapter number of a volume separated by a comma. (e.g. "3, 5, 7, 9") + +1. Create a folder containing PDF chapters, or get them from step above. +2. Create a .txt file with comma separated values, which are upper limit chapter numbers. +3. In the root directory, open terminal and run: + +```py meowDFer.py merge --src s_name --dest d_name --vols v_name.txt``` +- s_name: name of folder with chapter PDFs +- d_name: name of folder where volumes to be extracted in +- v_name.txt: name of .txt files containing all limit chapter numbers + +4. Folder with d_name will be created in root with volumes. + +## `all` + +This combines all three commands: extract, convert, and merge. They will run one after another. + +1. Create a folder in root, put all .zip files inside. +2. Create a .txt file with comma separated values, which are upper limit chapter numbers. +3. In the root directory, open terminal and run: + +```py meowDFer.py all --src s_name --dest d_name --vols v_name.txt``` +- s_name: name of folder with .zip files +- d_name: name of folder where volumes to be extracted in +- v_name.txt: name of .txt files containing all limit chapter numbers + +4. Folder with d_name will be created in root with volumes. + +## `cm` + +This combines two commands: convert, and merge. They will run one after another. + +1. Create a folder containing folders of images with chapter and number in name. Or have the folders from 'extract'. +2. Create a .txt file with comma separated values, which are upper limit chapter numbers. +3. In the root directory, open terminal and run: + +```py meowDFer.py cm --src s_name --dest d_name --vols v_name.txt``` +- s_name: name of folder with image folders +- d_name: name of folder where volumes to be extracted in +- v_name.txt: name of .txt files containing all limit chapter numbers + +4. Folder with d_name will be created in root with volumes. \ No newline at end of file From 2dce74bb1676f8d2c26fcbf117e069fe000ac47e Mon Sep 17 00:00:00 2001 From: vincent Date: Wed, 4 Feb 2026 20:19:40 +0100 Subject: [PATCH 2/7] style(tests): changed imports --- tests/test_convert.py | 2 +- tests/test_extract.py | 2 +- tests/test_utils_naming.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_convert.py b/tests/test_convert.py index 822ae5a..a6b1769 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -2,7 +2,7 @@ import pytest from PIL import Image -from src.utils import convert as test +from meowDFer.utils import convert as test PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) diff --git a/tests/test_extract.py b/tests/test_extract.py index 22c7156..10e9f42 100644 --- a/tests/test_extract.py +++ b/tests/test_extract.py @@ -2,7 +2,7 @@ import pytest from zipfile import ZipFile, BadZipFile -from src.utils import extract as test +from meowDFer.utils import extract as test PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) diff --git a/tests/test_utils_naming.py b/tests/test_utils_naming.py index 93d3073..5164be1 100644 --- a/tests/test_utils_naming.py +++ b/tests/test_utils_naming.py @@ -1,6 +1,6 @@ import pytest -from src.utils import utils_naming as test +from meowDFer.utils import utils_naming as test def test_extract_chapter_integer(): From 88ec8a5191b15c058cb2783077aa50d648e4abe4 Mon Sep 17 00:00:00 2001 From: vincent Date: Wed, 4 Feb 2026 20:20:09 +0100 Subject: [PATCH 3/7] refactor: made meowDFer module --- src/commands/all_command.py | 40 -------------- src/commands/cm_command.py | 32 ----------- src/commands/convert_command.py | 19 ------- src/commands/extract_command.py | 19 ------- src/commands/merge_command.py | 25 --------- src/meowDFer.py | 98 --------------------------------- src/utils/convert.py | 72 ------------------------ src/utils/extract.py | 34 ------------ src/utils/merge.py | 80 --------------------------- src/utils/utils_naming.py | 40 -------------- 10 files changed, 459 deletions(-) delete mode 100644 src/commands/all_command.py delete mode 100644 src/commands/cm_command.py delete mode 100644 src/commands/convert_command.py delete mode 100644 src/commands/extract_command.py delete mode 100644 src/commands/merge_command.py delete mode 100644 src/meowDFer.py delete mode 100644 src/utils/convert.py delete mode 100644 src/utils/extract.py delete mode 100644 src/utils/merge.py delete mode 100644 src/utils/utils_naming.py diff --git a/src/commands/all_command.py b/src/commands/all_command.py deleted file mode 100644 index 4909c3e..0000000 --- a/src/commands/all_command.py +++ /dev/null @@ -1,40 +0,0 @@ -import tempfile - -from utils.extract import extract_zips -from utils.convert import convert_all_to_pdf -from utils.merge import merge_to_volumes - -def run(args, name): - src = args.src - dest = args.dest - vols = args.vols - - print("\n\033[95mRunning all...\033[0m") - - with tempfile.TemporaryDirectory() as tmp_extracted: - extract_zips(src, tmp_extracted) - - with tempfile.TemporaryDirectory() as temp_converted: - convert_all_to_pdf(tmp_extracted, temp_converted, name) - - merge_to_volumes(temp_converted, dest, vols, name) - - print("\033[95mCompleted all.\033[0m") - - -def register_command(parser): - parser.add_argument( - "--src", - type=str, - help="Source folder where zip files are located" - ) - parser.add_argument( - "--dest", - type=str, - help="Name of folder where final volume are located" - ) - parser.add_argument( - "--vols", - type=str, - help="Name of .txt where intervals for volumes are located (include and chapter number separated by commas)" - ) \ No newline at end of file diff --git a/src/commands/cm_command.py b/src/commands/cm_command.py deleted file mode 100644 index 05e925d..0000000 --- a/src/commands/cm_command.py +++ /dev/null @@ -1,32 +0,0 @@ -import tempfile - -from utils.convert import convert_all_to_pdf -from utils.merge import merge_to_volumes - -def run(args, name): - src = args.src - dest = args.dest - vols = args.vols - - with tempfile.TemporaryDirectory() as temp_converted: - convert_all_to_pdf(src, temp_converted, name) - - merge_to_volumes(temp_converted, dest, vols, name) - - -def register_command(parser): - parser.add_argument( - "--src", - type=str, - help="Source folder where folders with images are located" - ) - parser.add_argument( - "--dest", - type=str, - help="Name of folder where final volume are located" - ) - parser.add_argument( - "--vols", - type=str, - help="Name of .txt where intervals for volumes are located (include and chapter number separated by commas)" - ) \ No newline at end of file diff --git a/src/commands/convert_command.py b/src/commands/convert_command.py deleted file mode 100644 index 7a7cedc..0000000 --- a/src/commands/convert_command.py +++ /dev/null @@ -1,19 +0,0 @@ - -from utils.convert import convert_all_to_pdf - -def run(args, name): - src = args.src - dest = args.dest - convert_all_to_pdf(src, dest, name) - -def register_command(parser): - parser.add_argument( - "--src", - type=str, - help="Source folder where folders with images are located" - ) - parser.add_argument( - "--dest", - type=str, - help="Name of folder where pdf to be created" - ) \ No newline at end of file diff --git a/src/commands/extract_command.py b/src/commands/extract_command.py deleted file mode 100644 index ea010da..0000000 --- a/src/commands/extract_command.py +++ /dev/null @@ -1,19 +0,0 @@ - -from utils.extract import extract_zips - -def run(args): - src = args.src - dest = args.dest - extract_zips(src, dest) - -def register_command(parser): - parser.add_argument( - "--src", - type=str, - help="Source folder where zips are located" - ) - parser.add_argument( - "--dest", - type=str, - help="Name of folder where zips to be extracted" - ) \ No newline at end of file diff --git a/src/commands/merge_command.py b/src/commands/merge_command.py deleted file mode 100644 index 4e56a8a..0000000 --- a/src/commands/merge_command.py +++ /dev/null @@ -1,25 +0,0 @@ - -from utils.merge import merge_to_volumes - -def run(args, name): - src = args.src - dest = args.dest - vols = args.vols - merge_to_volumes(src, dest, vols, name) - -def register_command(parser): - parser.add_argument( - "--src", - type=str, - help="Source folder where PDFs are located" - ) - parser.add_argument( - "--dest", - type=str, - help="Name of destination folder where volumes are to be created" - ) - parser.add_argument( - "--vols", - type=str, - help="Name of .txt where intervals for volums are located (include and chapter number separated by commas)" - ) \ No newline at end of file diff --git a/src/meowDFer.py b/src/meowDFer.py deleted file mode 100644 index df9dcfe..0000000 --- a/src/meowDFer.py +++ /dev/null @@ -1,98 +0,0 @@ -import sys -import argparse - -from commands import convert_command, extract_command, merge_command, all_command, cm_command - -logo = r""" - |\ _,,,---,,_ -ZZZzz /,`.-'`' -. ;-;;,_ - |,4- ) )-,_. ,\ ( `'-' - '---''(_/--' `-'\_) meowDFer -""" - -def main(): - print(logo) - - parser = argparse.ArgumentParser( - prog="meowDFer", - description="Extract zips, converts image folders into PDFs, and combines PDFs into volume." - ) - subparsers = parser.add_subparsers(dest="command", required=True) - - # extract - extract_parser = subparsers.add_parser( - "extract", help="Extract one or more zip files from a folder. (help: extract -h)" - ) - extract_command.register_command(extract_parser) - - # convert - convert_parser = subparsers.add_parser( - "convert", help="Convert one or more folders with images into PDFs. (help: convert -h)" - ) - convert_command.register_command(convert_parser) - - # merge - merge_parser = subparsers.add_parser( - "merge", help="Merge PDFs into volumes based on input. (help: merge -h)" - ) - merge_command.register_command(merge_parser) - - # all in one command - all_parser = subparsers.add_parser( - "all", help="Extract, convert and merge all at once. (help: all -h)" - ) - all_command.register_command(all_parser) - - # covert and merge command - all_parser = subparsers.add_parser( - "cm", help="Convert and merge at once. (help: cm -h)" - ) - cm_command.register_command(all_parser) - - - args = parser.parse_args() - - # extract zips - if args.command == "extract": - if not args.src or not args.dest: - extract_parser.error("The --src and --dest flags are required when using extract") - extract_command.run(args) - - # convert folders to PDFs - elif args.command == "convert": - if not args.src or not args.dest: - convert_parser.error("The --src and --dest flags are required when using convert") - - name = input("Give name: ") - convert_command.run(args, name) - - # merge PDFs into volumes - elif args.command == "merge": - if not args.src or not args.dest: - merge_parser.error("The --src, --dest, and --vols flags are required when using merge") - - name = input("Give name: ") - merge_command.run(args, name) - - # run all three: extract, convert, and merge one after another - elif args.command == "all": - if not args.src or not args.dest: - merge_parser.error("The --src, --dest, and --vols flags are required when using all") - - name = input("Give name: ") - all_command.run(args, name) - - # run convert and merge one after another - elif args.command == "cm": - if not args.src or not args.dest: - merge_parser.error("The --src, --dest, and --vols flags are required when using cm") - - name = input("Give name: ") - cm_command.run(args, name) - - else: - parser.print_help() - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/src/utils/convert.py b/src/utils/convert.py deleted file mode 100644 index c9f45f2..0000000 --- a/src/utils/convert.py +++ /dev/null @@ -1,72 +0,0 @@ -import os - -from PIL import Image - -from . import utils_naming as u_name - -PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) - -def convert_all_to_pdf(src, dest, name): - print("\n\033[95mRunning convert...\033[0m\n") - - src = os.path.join(PROJECT_ROOT, src) - dest = os.path.join(PROJECT_ROOT, dest) - - if not os.path.isdir(src): - raise FileNotFoundError(f"\033[91mSource folder not found:\033[0m {src}") - - os.makedirs(dest, exist_ok=True) - - for folder in os.listdir(src): - path = os.path.join(src, folder) - if os.path.isdir(path): - try: - convert_folder_to_pdf(path, dest, name) - except Exception as e: - print(f"\033[91mFailed to convert folder `{folder}`:\033[0m {e}") - - print("\n\033[95mAll folders converted to PDFs\033[0m\n") - -def convert_folder_to_pdf(src, dest, name): - folder_name = os.path.basename(src.rstrip('/')) - - try: - chapter_number = u_name.extract_chapter_number(folder_name) - except ValueError as e: - print(f"\033[93mSkipping folder {folder_name}:\033[0m {e}") - return - - pdf_name = u_name.create_chapter_name(name, chapter_number) + ".pdf" - pdf_path = os.path.join(dest, pdf_name) - - try: - images = sorted( - [f for f in os.listdir(src) if f.endswith((".png", ".jpg", ".jpeg"))], - key=u_name.extract_page_number - ) - except Exception as e: - raise RuntimeError(f"\033[91mFailed to sort images in {src}\033[0m") - - if not images: - print(f"\033[91mNo images found in folder\033[0m") - return - - img_list = [] - for image in images: - img_path = os.path.join(src, image) - try: - img = Image.open(img_path) - if img.mode != 'RGB': - img = img.convert('RGB') - img_list.append(img) - except Exception as e: - print(f"\033[93mSkipping image `{image}`:\033[0m {e}") - - if not img_list: - print(f"\033[91mNo valid image in folder:\033[0m {src}") - return - - first_img = img_list.pop(0) - first_img.save(pdf_path, save_all=True, append_images=img_list) - - print(f"\033[92mPDF created:\033[0m {pdf_name}") \ No newline at end of file diff --git a/src/utils/extract.py b/src/utils/extract.py deleted file mode 100644 index 364c7e6..0000000 --- a/src/utils/extract.py +++ /dev/null @@ -1,34 +0,0 @@ -import os - -from zipfile import ZipFile, BadZipFile - -PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) - -def extract_zips(src, dest): - print("\n\033[95mRunning extract...\033[0m\n") - - src = os.path.join(PROJECT_ROOT, src) - dest = os.path.join(PROJECT_ROOT, dest) - - try: - os.makedirs(dest, exist_ok=True) - - for file_name in os.listdir(src): - if file_name.endswith(".zip"): - zip_path = os.path.join(src, file_name) - try: - with ZipFile(zip_path, 'r') as zip_ref: - zip_ref.extractall(dest) - print(f"\033[92mExtracted successfully:\033[0m {file_name}") - except BadZipFile: - print(f"\033[91mInvalid zip file:\033[0m {file_name}") - except Exception as e: - print(f"\033[91mFailed to extract {file_name}\033[0m: {e}") - - print(f"\n\033[95mExtracted all zips\033[0m\n") - except FileNotFoundError: - print(f"\033[91mSource directory not found:\033[0m {src}") - except PermissionError: - print(f"\033[91mPermission denied.\033[0m") - except Exception as e: - print(f"\033[91mUnexpected error:\033[0m {e}") \ No newline at end of file diff --git a/src/utils/merge.py b/src/utils/merge.py deleted file mode 100644 index a2f8e86..0000000 --- a/src/utils/merge.py +++ /dev/null @@ -1,80 +0,0 @@ -import os - -from pypdf import PdfWriter - -from . import utils_naming as u_name - -PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) - -def merge_to_volumes(src, dest, vals, name): - print("\n\033[95mRunning merge...\033[0m\n") - - src = os.path.join(PROJECT_ROOT, src) - dest = os.path.join(PROJECT_ROOT, dest) - vals = os.path.join(PROJECT_ROOT, vals) - - if not os.path.isdir(src): - raise FileNotFoundError(f"\033[91mSource folder no found:\033[0m {src}") - - if not os.path.isfile(vals): - raise FileNotFoundError(f"\033[91mIntervals file not found:\033[0m {vals}") - - os.makedirs(dest, exist_ok=True) - - try: - with open(vals) as f: - intervals = list(map(int, f.read().split(", "))) - except: - raise ValueError("\033[91mInterval file must contain only integers separated by ', '\033[0m") - - if not intervals: - raise ValueError("\033[91mIntervals list is empty\033[0m") - - if intervals != sorted(intervals): - raise ValueError("\033[91mIntervals must be stricty increasing\033[0m") - - pdfs = [f for f in os.listdir(src) if f.endswith(".pdf")] - - if not pdfs: - raise ValueError("\033[91mNo PDF files found in given source folder\033[0m") - - pdfs = sorted(pdfs, key=u_name.extract_chapter_number) - - chapter_map = {} - for f in pdfs: - ch = u_name.extract_chapter_number(f) - if ch in chapter_map: - raise ValueError(f"\033[91mDuplicate chapter detected:\033[0m {ch}") - chapter_map[ch] = f - - vol_num = 1 - prev = 0 - for val in intervals: - start_ch = prev + 1 - end_ch = val - - if start_ch > end_ch: - raise ValueError(f"\033[91mInvalid volume range:\033[0m {start_ch} -> {end_ch}") - - merger = PdfWriter() - try: - for ch in range(start_ch, end_ch + 1): - if ch not in chapter_map: - raise ValueError(f"\033[91mMissing chapter:\033[0m {ch}") - merger.append(os.path.join(src, chapter_map[ch])) - print(f"\033[96mAdded to merge:\033[0m `{chapter_map[ch]}`") - - vol_name = u_name.create_volume_name(name, vol_num) + ".pdf" - vol_path = os.path.join(dest, vol_name) - - with open(vol_path, "wb") as vol: - merger.write(vol) - - print(f"\n\033[92mMerge succesful:\033[0m `{vol_name}`\n") - finally: - merger.close() - - vol_num += 1 - prev = val - - print(f"\033[95mFinished merge\033[0m\n") \ No newline at end of file diff --git a/src/utils/utils_naming.py b/src/utils/utils_naming.py deleted file mode 100644 index 1c36aa2..0000000 --- a/src/utils/utils_naming.py +++ /dev/null @@ -1,40 +0,0 @@ -import re - -def create_chapter_name(name, chapter_number): - return f"{name} Chapter {chapter_number}" - -def create_volume_name(name, volume_number): - return f"{name} Volume {volume_number}" - -def extract_chapter_number(file_name): - match = re.search( - r'(?i)\b(?:chapter|ch\.?|c)\s*(\d+(?:\.\d+)?)\b', - file_name - ) - - if not match: - raise ValueError(f"No chapter number found") - - number_str = match.group(1) - if '.' in number_str: - raise ValueError(f"Decimal chapter number are skipped") - - return int(number_str) - -def extract_page_number(file_name): - - page_pattern = r'(\d+)' - - match = re.search( - r'\d+(?:\.\d+)?', - file_name - ) - - if not match: - raise ValueError(f"No page number found") - - number_str = match.group() - if '.' in number_str: - raise ValueError("Decimal page number not allowed") - - return int(number_str) From 1c7c0f64da8ebdad99e93d0e316a26e3687ecaba Mon Sep 17 00:00:00 2001 From: vincent Date: Wed, 4 Feb 2026 20:21:52 +0100 Subject: [PATCH 4/7] chore: update readme --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index f978fd9..0859c5f 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ This is a tool made to simplify the processes mentioned above. ##### Example commands: -- `py meowDFer.py extract --src s_name --dest d_name` -- `py meowDFer.py convert --src s_name --dest d_name` -- `py meowDFer.py merge --src s_name --dest d_name --vol v_name.txt` -- `py meowDFer.py all --src s_name --dest d_name --vol v_name.txt` -- `py meowDFer.py cm --src s_name --dest d_name --vol v_name.txt` +- `py -m meowDFer extract --src s_name --dest d_name` +- `py -m meowDFer convert --src s_name --dest d_name` +- `py -m meowDFer merge --src s_name --dest d_name --vol v_name.txt` +- `py -m meowDFer all --src s_name --dest d_name --vol v_name.txt` +- `py -m meowDFer cm --src s_name --dest d_name --vol v_name.txt` ## `extract` @@ -19,7 +19,7 @@ Extracts multiple zip files into one folder. 1. Create a folder in root, put all .zip files inside. 2. In the root of the directory, open the terminal and run: -```py meowDFer.py extract --src s_name --dest d_name``` +```py -m meowDFer extract --src s_name --dest d_name``` - s_name: name of folder with zip files - d_name: name of folder where files to be extracted in @@ -32,7 +32,7 @@ Converts folder with images (chapters) into PDFs. Folders with images must conta 1. Create a folder containing folders of images with chapter and number in name. Or have the folders from 'extract'. 2. In the root of the directory, open terminal and run: -```py meowDFer.py convert --src s_name --dest d_name``` +```py -m meowDFer convert --src s_name --dest d_name``` - s_name: name of folder with image folders - d_name: name of folder where chapter PDFs to be extracted in @@ -46,7 +46,7 @@ Merge many PDF s (chapters) into volumes, using a .txt file. In the .txt file wr 2. Create a .txt file with comma separated values, which are upper limit chapter numbers. 3. In the root directory, open terminal and run: -```py meowDFer.py merge --src s_name --dest d_name --vols v_name.txt``` +```py -m meowDFer merge --src s_name --dest d_name --vols v_name.txt``` - s_name: name of folder with chapter PDFs - d_name: name of folder where volumes to be extracted in - v_name.txt: name of .txt files containing all limit chapter numbers @@ -61,7 +61,7 @@ This combines all three commands: extract, convert, and merge. They will run one 2. Create a .txt file with comma separated values, which are upper limit chapter numbers. 3. In the root directory, open terminal and run: -```py meowDFer.py all --src s_name --dest d_name --vols v_name.txt``` +```py -m meowDFer all --src s_name --dest d_name --vols v_name.txt``` - s_name: name of folder with .zip files - d_name: name of folder where volumes to be extracted in - v_name.txt: name of .txt files containing all limit chapter numbers @@ -76,7 +76,7 @@ This combines two commands: convert, and merge. They will run one after another. 2. Create a .txt file with comma separated values, which are upper limit chapter numbers. 3. In the root directory, open terminal and run: -```py meowDFer.py cm --src s_name --dest d_name --vols v_name.txt``` +```py -m meowDFer cm --src s_name --dest d_name --vols v_name.txt``` - s_name: name of folder with image folders - d_name: name of folder where volumes to be extracted in - v_name.txt: name of .txt files containing all limit chapter numbers From 60bc02d502c9fb7dc89d97eb78bfb20afa2a3b52 Mon Sep 17 00:00:00 2001 From: vincent Date: Wed, 4 Feb 2026 20:46:36 +0100 Subject: [PATCH 5/7] build: fixed import for tests --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0a44ac3..0a38c46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,4 +14,4 @@ dev = [ ] [tool.pytest.ini_options] -pythonpath = ["."] \ No newline at end of file +pythonpath = [".", "meowDFer"] \ No newline at end of file From 6449b1152b1c19089dc700f1a768745044484265 Mon Sep 17 00:00:00 2001 From: vincent Date: Wed, 4 Feb 2026 20:54:01 +0100 Subject: [PATCH 6/7] build: fix again --- .github/workflows/ci.yml | 2 +- pyproject.toml | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 336a5c9..9c5da7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install .[dev] + pip install -e .[dev] - name: Run tests run: | diff --git a/pyproject.toml b/pyproject.toml index 0a38c46..68d3e42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + [project] name = "meowDFer" version = "0.1.0" @@ -14,4 +18,4 @@ dev = [ ] [tool.pytest.ini_options] -pythonpath = [".", "meowDFer"] \ No newline at end of file +pythonpath = ["."] \ No newline at end of file From 872292d755de7b8e47686f16bf7f761580ad0bfc Mon Sep 17 00:00:00 2001 From: vincent Date: Wed, 4 Feb 2026 21:00:10 +0100 Subject: [PATCH 7/7] refactor: named module src to fix build --- README.md | 20 +++---- pyproject.toml | 4 -- src/__init__.py | 1 + src/__main__.py | 4 ++ src/cli.py | 99 +++++++++++++++++++++++++++++++++ src/commands/__init__.py | 5 ++ src/commands/all_command.py | 40 +++++++++++++ src/commands/cm_command.py | 32 +++++++++++ src/commands/convert_command.py | 19 +++++++ src/commands/extract_command.py | 19 +++++++ src/commands/merge_command.py | 25 +++++++++ src/utils/convert.py | 72 ++++++++++++++++++++++++ src/utils/extract.py | 34 +++++++++++ src/utils/merge.py | 80 ++++++++++++++++++++++++++ src/utils/utils_naming.py | 40 +++++++++++++ tests/test_convert.py | 2 +- tests/test_extract.py | 2 +- tests/test_utils_naming.py | 2 +- 18 files changed, 483 insertions(+), 17 deletions(-) create mode 100644 src/__init__.py create mode 100644 src/__main__.py create mode 100644 src/cli.py create mode 100644 src/commands/__init__.py create mode 100644 src/commands/all_command.py create mode 100644 src/commands/cm_command.py create mode 100644 src/commands/convert_command.py create mode 100644 src/commands/extract_command.py create mode 100644 src/commands/merge_command.py create mode 100644 src/utils/convert.py create mode 100644 src/utils/extract.py create mode 100644 src/utils/merge.py create mode 100644 src/utils/utils_naming.py diff --git a/README.md b/README.md index 0859c5f..162b26b 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ This is a tool made to simplify the processes mentioned above. ##### Example commands: -- `py -m meowDFer extract --src s_name --dest d_name` -- `py -m meowDFer convert --src s_name --dest d_name` -- `py -m meowDFer merge --src s_name --dest d_name --vol v_name.txt` -- `py -m meowDFer all --src s_name --dest d_name --vol v_name.txt` -- `py -m meowDFer cm --src s_name --dest d_name --vol v_name.txt` +- `py -m src extract --src s_name --dest d_name` +- `py -m src convert --src s_name --dest d_name` +- `py -m src merge --src s_name --dest d_name --vol v_name.txt` +- `py -m src all --src s_name --dest d_name --vol v_name.txt` +- `py -m src cm --src s_name --dest d_name --vol v_name.txt` ## `extract` @@ -19,7 +19,7 @@ Extracts multiple zip files into one folder. 1. Create a folder in root, put all .zip files inside. 2. In the root of the directory, open the terminal and run: -```py -m meowDFer extract --src s_name --dest d_name``` +```py -m src extract --src s_name --dest d_name``` - s_name: name of folder with zip files - d_name: name of folder where files to be extracted in @@ -32,7 +32,7 @@ Converts folder with images (chapters) into PDFs. Folders with images must conta 1. Create a folder containing folders of images with chapter and number in name. Or have the folders from 'extract'. 2. In the root of the directory, open terminal and run: -```py -m meowDFer convert --src s_name --dest d_name``` +```py -m src convert --src s_name --dest d_name``` - s_name: name of folder with image folders - d_name: name of folder where chapter PDFs to be extracted in @@ -46,7 +46,7 @@ Merge many PDF s (chapters) into volumes, using a .txt file. In the .txt file wr 2. Create a .txt file with comma separated values, which are upper limit chapter numbers. 3. In the root directory, open terminal and run: -```py -m meowDFer merge --src s_name --dest d_name --vols v_name.txt``` +```py -m src merge --src s_name --dest d_name --vols v_name.txt``` - s_name: name of folder with chapter PDFs - d_name: name of folder where volumes to be extracted in - v_name.txt: name of .txt files containing all limit chapter numbers @@ -61,7 +61,7 @@ This combines all three commands: extract, convert, and merge. They will run one 2. Create a .txt file with comma separated values, which are upper limit chapter numbers. 3. In the root directory, open terminal and run: -```py -m meowDFer all --src s_name --dest d_name --vols v_name.txt``` +```py -m src all --src s_name --dest d_name --vols v_name.txt``` - s_name: name of folder with .zip files - d_name: name of folder where volumes to be extracted in - v_name.txt: name of .txt files containing all limit chapter numbers @@ -76,7 +76,7 @@ This combines two commands: convert, and merge. They will run one after another. 2. Create a .txt file with comma separated values, which are upper limit chapter numbers. 3. In the root directory, open terminal and run: -```py -m meowDFer cm --src s_name --dest d_name --vols v_name.txt``` +```py -m src cm --src s_name --dest d_name --vols v_name.txt``` - s_name: name of folder with image folders - d_name: name of folder where volumes to be extracted in - v_name.txt: name of .txt files containing all limit chapter numbers diff --git a/pyproject.toml b/pyproject.toml index 68d3e42..0a44ac3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,3 @@ -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - [project] name = "meowDFer" version = "0.1.0" diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..02b3648 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1 @@ +all = ["cli"] \ No newline at end of file diff --git a/src/__main__.py b/src/__main__.py new file mode 100644 index 0000000..27d14a7 --- /dev/null +++ b/src/__main__.py @@ -0,0 +1,4 @@ +from .cli import main + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/cli.py b/src/cli.py new file mode 100644 index 0000000..9f5914f --- /dev/null +++ b/src/cli.py @@ -0,0 +1,99 @@ +import argparse + +from .commands import ( + convert_command, + extract_command, + merge_command, + all_command, + cm_command +) + +logo = r""" + |\ _,,,---,,_ +ZZZzz /,`.-'`' -. ;-;;,_ + |,4- ) )-,_. ,\ ( `'-' + '---''(_/--' `-'\_) meowDFer +""" + +def main(): + print(logo) + + parser = argparse.ArgumentParser( + prog="meowDFer", + description="Extract zips, converts image folders into PDFs, and combines PDFs into volume." + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + # extract + extract_parser = subparsers.add_parser( + "extract", help="Extract one or more zip files from a folder. (help: extract -h)" + ) + extract_command.register_command(extract_parser) + + # convert + convert_parser = subparsers.add_parser( + "convert", help="Convert one or more folders with images into PDFs. (help: convert -h)" + ) + convert_command.register_command(convert_parser) + + # merge + merge_parser = subparsers.add_parser( + "merge", help="Merge PDFs into volumes based on input. (help: merge -h)" + ) + merge_command.register_command(merge_parser) + + # all in one command + all_parser = subparsers.add_parser( + "all", help="Extract, convert and merge all at once. (help: all -h)" + ) + all_command.register_command(all_parser) + + # covert and merge command + all_parser = subparsers.add_parser( + "cm", help="Convert and merge at once. (help: cm -h)" + ) + cm_command.register_command(all_parser) + + + args = parser.parse_args() + + # extract zips + if args.command == "extract": + if not args.src or not args.dest: + extract_parser.error("The --src and --dest flags are required when using extract") + extract_command.run(args) + + # convert folders to PDFs + elif args.command == "convert": + if not args.src or not args.dest: + convert_parser.error("The --src and --dest flags are required when using convert") + + name = input("Give name: ") + convert_command.run(args, name) + + # merge PDFs into volumes + elif args.command == "merge": + if not args.src or not args.dest: + merge_parser.error("The --src, --dest, and --vols flags are required when using merge") + + name = input("Give name: ") + merge_command.run(args, name) + + # run all three: extract, convert, and merge one after another + elif args.command == "all": + if not args.src or not args.dest: + merge_parser.error("The --src, --dest, and --vols flags are required when using all") + + name = input("Give name: ") + all_command.run(args, name) + + # run convert and merge one after another + elif args.command == "cm": + if not args.src or not args.dest: + merge_parser.error("The --src, --dest, and --vols flags are required when using cm") + + name = input("Give name: ") + cm_command.run(args, name) + + else: + parser.print_help() \ No newline at end of file diff --git a/src/commands/__init__.py b/src/commands/__init__.py new file mode 100644 index 0000000..7bb8f82 --- /dev/null +++ b/src/commands/__init__.py @@ -0,0 +1,5 @@ +from .extract_command import * +from .convert_command import * +from .merge_command import * +from .all_command import * +from .cm_command import * \ No newline at end of file diff --git a/src/commands/all_command.py b/src/commands/all_command.py new file mode 100644 index 0000000..f20179e --- /dev/null +++ b/src/commands/all_command.py @@ -0,0 +1,40 @@ +import tempfile + +from ..utils.extract import extract_zips +from ..utils.convert import convert_all_to_pdf +from ..utils.merge import merge_to_volumes + +def run(args, name): + src = args.src + dest = args.dest + vols = args.vols + + print("\n\033[95mRunning all...\033[0m") + + with tempfile.TemporaryDirectory() as tmp_extracted: + extract_zips(src, tmp_extracted) + + with tempfile.TemporaryDirectory() as temp_converted: + convert_all_to_pdf(tmp_extracted, temp_converted, name) + + merge_to_volumes(temp_converted, dest, vols, name) + + print("\033[95mCompleted all.\033[0m") + + +def register_command(parser): + parser.add_argument( + "--src", + type=str, + help="Source folder where zip files are located" + ) + parser.add_argument( + "--dest", + type=str, + help="Name of folder where final volume are located" + ) + parser.add_argument( + "--vols", + type=str, + help="Name of .txt where intervals for volumes are located (include and chapter number separated by commas)" + ) \ No newline at end of file diff --git a/src/commands/cm_command.py b/src/commands/cm_command.py new file mode 100644 index 0000000..98b2a83 --- /dev/null +++ b/src/commands/cm_command.py @@ -0,0 +1,32 @@ +import tempfile + +from ..utils.convert import convert_all_to_pdf +from ..utils.merge import merge_to_volumes + +def run(args, name): + src = args.src + dest = args.dest + vols = args.vols + + with tempfile.TemporaryDirectory() as temp_converted: + convert_all_to_pdf(src, temp_converted, name) + + merge_to_volumes(temp_converted, dest, vols, name) + + +def register_command(parser): + parser.add_argument( + "--src", + type=str, + help="Source folder where folders with images are located" + ) + parser.add_argument( + "--dest", + type=str, + help="Name of folder where final volume are located" + ) + parser.add_argument( + "--vols", + type=str, + help="Name of .txt where intervals for volumes are located (include and chapter number separated by commas)" + ) \ No newline at end of file diff --git a/src/commands/convert_command.py b/src/commands/convert_command.py new file mode 100644 index 0000000..7de20f8 --- /dev/null +++ b/src/commands/convert_command.py @@ -0,0 +1,19 @@ + +from ..utils.convert import convert_all_to_pdf + +def run(args, name): + src = args.src + dest = args.dest + convert_all_to_pdf(src, dest, name) + +def register_command(parser): + parser.add_argument( + "--src", + type=str, + help="Source folder where folders with images are located" + ) + parser.add_argument( + "--dest", + type=str, + help="Name of folder where pdf to be created" + ) \ No newline at end of file diff --git a/src/commands/extract_command.py b/src/commands/extract_command.py new file mode 100644 index 0000000..ca48f29 --- /dev/null +++ b/src/commands/extract_command.py @@ -0,0 +1,19 @@ + +from ..utils.extract import extract_zips + +def run(args): + src = args.src + dest = args.dest + extract_zips(src, dest) + +def register_command(parser): + parser.add_argument( + "--src", + type=str, + help="Source folder where zips are located" + ) + parser.add_argument( + "--dest", + type=str, + help="Name of folder where zips to be extracted" + ) \ No newline at end of file diff --git a/src/commands/merge_command.py b/src/commands/merge_command.py new file mode 100644 index 0000000..8d6867c --- /dev/null +++ b/src/commands/merge_command.py @@ -0,0 +1,25 @@ + +from ..utils.merge import merge_to_volumes + +def run(args, name): + src = args.src + dest = args.dest + vols = args.vols + merge_to_volumes(src, dest, vols, name) + +def register_command(parser): + parser.add_argument( + "--src", + type=str, + help="Source folder where PDFs are located" + ) + parser.add_argument( + "--dest", + type=str, + help="Name of destination folder where volumes are to be created" + ) + parser.add_argument( + "--vols", + type=str, + help="Name of .txt where intervals for volums are located (include and chapter number separated by commas)" + ) \ No newline at end of file diff --git a/src/utils/convert.py b/src/utils/convert.py new file mode 100644 index 0000000..c9f45f2 --- /dev/null +++ b/src/utils/convert.py @@ -0,0 +1,72 @@ +import os + +from PIL import Image + +from . import utils_naming as u_name + +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) + +def convert_all_to_pdf(src, dest, name): + print("\n\033[95mRunning convert...\033[0m\n") + + src = os.path.join(PROJECT_ROOT, src) + dest = os.path.join(PROJECT_ROOT, dest) + + if not os.path.isdir(src): + raise FileNotFoundError(f"\033[91mSource folder not found:\033[0m {src}") + + os.makedirs(dest, exist_ok=True) + + for folder in os.listdir(src): + path = os.path.join(src, folder) + if os.path.isdir(path): + try: + convert_folder_to_pdf(path, dest, name) + except Exception as e: + print(f"\033[91mFailed to convert folder `{folder}`:\033[0m {e}") + + print("\n\033[95mAll folders converted to PDFs\033[0m\n") + +def convert_folder_to_pdf(src, dest, name): + folder_name = os.path.basename(src.rstrip('/')) + + try: + chapter_number = u_name.extract_chapter_number(folder_name) + except ValueError as e: + print(f"\033[93mSkipping folder {folder_name}:\033[0m {e}") + return + + pdf_name = u_name.create_chapter_name(name, chapter_number) + ".pdf" + pdf_path = os.path.join(dest, pdf_name) + + try: + images = sorted( + [f for f in os.listdir(src) if f.endswith((".png", ".jpg", ".jpeg"))], + key=u_name.extract_page_number + ) + except Exception as e: + raise RuntimeError(f"\033[91mFailed to sort images in {src}\033[0m") + + if not images: + print(f"\033[91mNo images found in folder\033[0m") + return + + img_list = [] + for image in images: + img_path = os.path.join(src, image) + try: + img = Image.open(img_path) + if img.mode != 'RGB': + img = img.convert('RGB') + img_list.append(img) + except Exception as e: + print(f"\033[93mSkipping image `{image}`:\033[0m {e}") + + if not img_list: + print(f"\033[91mNo valid image in folder:\033[0m {src}") + return + + first_img = img_list.pop(0) + first_img.save(pdf_path, save_all=True, append_images=img_list) + + print(f"\033[92mPDF created:\033[0m {pdf_name}") \ No newline at end of file diff --git a/src/utils/extract.py b/src/utils/extract.py new file mode 100644 index 0000000..364c7e6 --- /dev/null +++ b/src/utils/extract.py @@ -0,0 +1,34 @@ +import os + +from zipfile import ZipFile, BadZipFile + +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) + +def extract_zips(src, dest): + print("\n\033[95mRunning extract...\033[0m\n") + + src = os.path.join(PROJECT_ROOT, src) + dest = os.path.join(PROJECT_ROOT, dest) + + try: + os.makedirs(dest, exist_ok=True) + + for file_name in os.listdir(src): + if file_name.endswith(".zip"): + zip_path = os.path.join(src, file_name) + try: + with ZipFile(zip_path, 'r') as zip_ref: + zip_ref.extractall(dest) + print(f"\033[92mExtracted successfully:\033[0m {file_name}") + except BadZipFile: + print(f"\033[91mInvalid zip file:\033[0m {file_name}") + except Exception as e: + print(f"\033[91mFailed to extract {file_name}\033[0m: {e}") + + print(f"\n\033[95mExtracted all zips\033[0m\n") + except FileNotFoundError: + print(f"\033[91mSource directory not found:\033[0m {src}") + except PermissionError: + print(f"\033[91mPermission denied.\033[0m") + except Exception as e: + print(f"\033[91mUnexpected error:\033[0m {e}") \ No newline at end of file diff --git a/src/utils/merge.py b/src/utils/merge.py new file mode 100644 index 0000000..a2f8e86 --- /dev/null +++ b/src/utils/merge.py @@ -0,0 +1,80 @@ +import os + +from pypdf import PdfWriter + +from . import utils_naming as u_name + +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) + +def merge_to_volumes(src, dest, vals, name): + print("\n\033[95mRunning merge...\033[0m\n") + + src = os.path.join(PROJECT_ROOT, src) + dest = os.path.join(PROJECT_ROOT, dest) + vals = os.path.join(PROJECT_ROOT, vals) + + if not os.path.isdir(src): + raise FileNotFoundError(f"\033[91mSource folder no found:\033[0m {src}") + + if not os.path.isfile(vals): + raise FileNotFoundError(f"\033[91mIntervals file not found:\033[0m {vals}") + + os.makedirs(dest, exist_ok=True) + + try: + with open(vals) as f: + intervals = list(map(int, f.read().split(", "))) + except: + raise ValueError("\033[91mInterval file must contain only integers separated by ', '\033[0m") + + if not intervals: + raise ValueError("\033[91mIntervals list is empty\033[0m") + + if intervals != sorted(intervals): + raise ValueError("\033[91mIntervals must be stricty increasing\033[0m") + + pdfs = [f for f in os.listdir(src) if f.endswith(".pdf")] + + if not pdfs: + raise ValueError("\033[91mNo PDF files found in given source folder\033[0m") + + pdfs = sorted(pdfs, key=u_name.extract_chapter_number) + + chapter_map = {} + for f in pdfs: + ch = u_name.extract_chapter_number(f) + if ch in chapter_map: + raise ValueError(f"\033[91mDuplicate chapter detected:\033[0m {ch}") + chapter_map[ch] = f + + vol_num = 1 + prev = 0 + for val in intervals: + start_ch = prev + 1 + end_ch = val + + if start_ch > end_ch: + raise ValueError(f"\033[91mInvalid volume range:\033[0m {start_ch} -> {end_ch}") + + merger = PdfWriter() + try: + for ch in range(start_ch, end_ch + 1): + if ch not in chapter_map: + raise ValueError(f"\033[91mMissing chapter:\033[0m {ch}") + merger.append(os.path.join(src, chapter_map[ch])) + print(f"\033[96mAdded to merge:\033[0m `{chapter_map[ch]}`") + + vol_name = u_name.create_volume_name(name, vol_num) + ".pdf" + vol_path = os.path.join(dest, vol_name) + + with open(vol_path, "wb") as vol: + merger.write(vol) + + print(f"\n\033[92mMerge succesful:\033[0m `{vol_name}`\n") + finally: + merger.close() + + vol_num += 1 + prev = val + + print(f"\033[95mFinished merge\033[0m\n") \ No newline at end of file diff --git a/src/utils/utils_naming.py b/src/utils/utils_naming.py new file mode 100644 index 0000000..1c36aa2 --- /dev/null +++ b/src/utils/utils_naming.py @@ -0,0 +1,40 @@ +import re + +def create_chapter_name(name, chapter_number): + return f"{name} Chapter {chapter_number}" + +def create_volume_name(name, volume_number): + return f"{name} Volume {volume_number}" + +def extract_chapter_number(file_name): + match = re.search( + r'(?i)\b(?:chapter|ch\.?|c)\s*(\d+(?:\.\d+)?)\b', + file_name + ) + + if not match: + raise ValueError(f"No chapter number found") + + number_str = match.group(1) + if '.' in number_str: + raise ValueError(f"Decimal chapter number are skipped") + + return int(number_str) + +def extract_page_number(file_name): + + page_pattern = r'(\d+)' + + match = re.search( + r'\d+(?:\.\d+)?', + file_name + ) + + if not match: + raise ValueError(f"No page number found") + + number_str = match.group() + if '.' in number_str: + raise ValueError("Decimal page number not allowed") + + return int(number_str) diff --git a/tests/test_convert.py b/tests/test_convert.py index a6b1769..822ae5a 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -2,7 +2,7 @@ import pytest from PIL import Image -from meowDFer.utils import convert as test +from src.utils import convert as test PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) diff --git a/tests/test_extract.py b/tests/test_extract.py index 10e9f42..22c7156 100644 --- a/tests/test_extract.py +++ b/tests/test_extract.py @@ -2,7 +2,7 @@ import pytest from zipfile import ZipFile, BadZipFile -from meowDFer.utils import extract as test +from src.utils import extract as test PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) diff --git a/tests/test_utils_naming.py b/tests/test_utils_naming.py index 5164be1..93d3073 100644 --- a/tests/test_utils_naming.py +++ b/tests/test_utils_naming.py @@ -1,6 +1,6 @@ import pytest -from meowDFer.utils import utils_naming as test +from src.utils import utils_naming as test def test_extract_chapter_integer():