|
46 | 46 | import settings as settings_file
|
47 | 47 |
|
48 | 48 | # Version of the script
|
49 |
| -script_version = (2, 5, 24) |
| 49 | +script_version = (2, 5, 25) |
50 | 50 | script_version_text = "v{}.{}.{}".format(*script_version)
|
51 | 51 |
|
52 | 52 | # Paths = existing library
|
@@ -238,7 +238,7 @@ def __str__(self):
|
238 | 238 | "light novel", # name
|
239 | 239 | novel_extensions, # extensions
|
240 | 240 | [
|
241 |
| - r"\[[^\]]*(Lucaz|Stick|Oak|Yen (Press|On)|J-Novel|Seven Seas|Vertical|One Peace Books|Cross Infinite|Sol Press|Hanashi Media|Kodansha|Tentai Books|SB Creative|Hobby Japan|Impress Corporation|KADOKAWA)[^\]]*\]|(faratnis)" |
| 241 | + r"\[[^\]]*(Lucaz|Stick|Oak|Yen (Press|On)|J-Novel|Seven Seas|Vertical|One Peace Books|Cross Infinite|Sol Press|Hanashi Media|Kodansha|Tentai Books|SB Creative|Hobby Japan|Impress Corporation|KADOKAWA|Viz Media)[^\]]*\]|(faratnis)" |
242 | 242 | ], # must_contain
|
243 | 243 | [], # must_not_contain
|
244 | 244 | ),
|
@@ -2578,6 +2578,10 @@ def get_series_name_from_volume(name, root, test_mode=False, second=False):
|
2578 | 2578 | # replace _extra
|
2579 | 2579 | name = remove_dual_space(name.replace("_extra", ".5")).strip()
|
2580 | 2580 |
|
| 2581 | + # Replace "- One-shot" after series name |
| 2582 | + if "one" in name.lower() and "shot" in name.lower(): |
| 2583 | + name = re.sub(r"(-\s*)One(-|)shot\s*", "", name, flags=re.IGNORECASE).strip() |
| 2584 | + |
2581 | 2585 | # replace underscores
|
2582 | 2586 | name = replace_underscores(name) if "_" in name else name
|
2583 | 2587 |
|
@@ -2747,6 +2751,10 @@ def get_series_name_from_chapter(name, root, chapter_number="", second=False):
|
2747 | 2751 | # Replace _extra
|
2748 | 2752 | name = name.replace("_extra", ".5")
|
2749 | 2753 |
|
| 2754 | + # Replace "- One-shot" after series name |
| 2755 | + if "one" in name.lower() and "shot" in name.lower(): |
| 2756 | + name = re.sub(r"(-\s*)One(-|)shot\s*", "", name, flags=re.IGNORECASE).strip() |
| 2757 | + |
2750 | 2758 | # Remove dual space
|
2751 | 2759 | name = remove_dual_space(name).strip()
|
2752 | 2760 |
|
@@ -3268,6 +3276,69 @@ def clean_publisher_name(name):
|
3268 | 3276 | return publisher
|
3269 | 3277 |
|
3270 | 3278 |
|
| 3279 | +# Function to determine if an image is black and white with better handling for halftones |
| 3280 | +def is_image_black_and_white(image, tolerance=15, threshold=200): |
| 3281 | + """ |
| 3282 | + Determines if an image is black and white by verifying that |
| 3283 | + most pixels are grayscale (R == G == B) and fall within the black or white range. |
| 3284 | +
|
| 3285 | + Args: |
| 3286 | + image (PIL.Image): The image to check. |
| 3287 | + tolerance (int): The allowed difference between R, G, and B for a pixel to be considered grayscale. |
| 3288 | + threshold (int): The number of pixels that need to be grayscale or black/white to count as a valid black-and-white image. |
| 3289 | +
|
| 3290 | + Returns: |
| 3291 | + bool: True if the image is black and white or grayscale, False otherwise. |
| 3292 | + """ |
| 3293 | + try: |
| 3294 | + # Convert the image to RGB (ensures consistent handling of image modes) |
| 3295 | + image_rgb = image.convert("RGB") |
| 3296 | + |
| 3297 | + # Extract pixel data |
| 3298 | + pixels = list(image_rgb.getdata()) |
| 3299 | + |
| 3300 | + # Count pixels that are grayscale and black/white |
| 3301 | + grayscale_count = 0 |
| 3302 | + |
| 3303 | + for r, g, b in pixels: |
| 3304 | + # Check if the pixel is grayscale within the tolerance |
| 3305 | + if abs(r - g) <= tolerance and abs(g - b) <= tolerance: |
| 3306 | + # Further check if it is black or white |
| 3307 | + if r == 0 or r == 255: |
| 3308 | + grayscale_count += 1 |
| 3309 | + elif 0 < r < 255: |
| 3310 | + grayscale_count += 1 |
| 3311 | + |
| 3312 | + # If enough pixels are grayscale or black/white, return True |
| 3313 | + if grayscale_count / len(pixels) > 0.9: |
| 3314 | + return True |
| 3315 | + |
| 3316 | + return False # Otherwise, it's not black and white |
| 3317 | + except Exception as e: |
| 3318 | + send_message(f"Error checking if image is black and white: {e}", error=True) |
| 3319 | + return False |
| 3320 | + |
| 3321 | + |
| 3322 | +# Function to check if the first image in a zip file is black and white |
| 3323 | +def is_first_image_black_and_white(zip_path): |
| 3324 | + try: |
| 3325 | + with zipfile.ZipFile(zip_path, "r") as zip_file: |
| 3326 | + # Sort files alphabetically and get the first one |
| 3327 | + sorted_files = sorted(zip_file.namelist()) |
| 3328 | + if not sorted_files: |
| 3329 | + return False # No files in the archive |
| 3330 | + |
| 3331 | + first_file = sorted_files[0] |
| 3332 | + if get_file_extension(first_file) in image_extensions: |
| 3333 | + with zip_file.open(first_file) as image_file: |
| 3334 | + image = Image.open(io.BytesIO(image_file.read())) |
| 3335 | + return is_image_black_and_white(image) |
| 3336 | + return False |
| 3337 | + except Exception as e: |
| 3338 | + send_message(f"Error processing zip file {zip_path}: {e}", error=True) |
| 3339 | + return False |
| 3340 | + |
| 3341 | + |
3271 | 3342 | # Trades out our regular files for file objects
|
3272 | 3343 | def upgrade_to_volume_class(
|
3273 | 3344 | files,
|
@@ -3381,6 +3452,16 @@ def upgrade_to_volume_class(
|
3381 | 3452 | subtitle=file_obj.subtitle,
|
3382 | 3453 | )
|
3383 | 3454 |
|
| 3455 | + if ( |
| 3456 | + not test_mode |
| 3457 | + and file_obj.file_type != "chapter" |
| 3458 | + and not file_obj.volume_number |
| 3459 | + and check_for_exception_keywords(file_obj.name, exception_keywords) |
| 3460 | + and is_first_image_black_and_white(file_obj.path) |
| 3461 | + ): |
| 3462 | + file_obj.file_type = "chapter" |
| 3463 | + file_obj.is_one_shot = True |
| 3464 | + |
3384 | 3465 | if file_obj.is_one_shot:
|
3385 | 3466 | file_obj.volume_number = 1
|
3386 | 3467 |
|
|
0 commit comments