import filters for pixiv sidecars, tag filtering, and favicon generation

This commit is contained in:
2026-02-04 19:08:50 -05:00
parent 17403c4e6b
commit c95896693f
24 changed files with 4615 additions and 4234 deletions
File diff suppressed because it is too large Load Diff
+50 -9
View File
@@ -31,6 +31,7 @@ def find_sidecar_json(image_path: str) -> Optional[str]:
Gallery-DL creates JSON files with various naming patterns:
- Patreon: {filename}.json in same directory
- HentaiFoundry: {artist}-{index}-{title}.json (may differ from image filename)
- Pixiv: {id}_p{num}.json for image named {id}_{title}_{num}.png
- UUID-based: {uuid}.json where UUID is extracted from filename
Returns the path to the sidecar JSON if found, None otherwise.
@@ -50,7 +51,19 @@ def find_sidecar_json(image_path: str) -> Optional[str]:
if direct_json.exists():
return str(direct_json)
# Pattern 3: Extract UUID from filename and look for {uuid}.json
# Pattern 3: Pixiv naming pattern
# Image: {id}_{title}_{num}.png (e.g., "115358744_Koseki Bijou (Bunny)_00.png")
# JSON: {id}_p{num}.json (e.g., "115358744_p0.json")
# Extract the leading numeric ID and trailing index
pixiv_match = re.match(r'^(\d{6,12})_.*_(\d{2})$', stem)
if pixiv_match:
pixiv_id = pixiv_match.group(1)
pixiv_num = int(pixiv_match.group(2)) # Convert "00" to 0
pixiv_json = parent_dir / f"{pixiv_id}_p{pixiv_num}.json"
if pixiv_json.exists():
return str(pixiv_json)
# Pattern 4: Extract UUID from filename and look for {uuid}.json
# Common patterns: "01_UUID", "UUID", "{num}_{UUID}"
uuid_pattern = re.compile(r'([A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12})', re.IGNORECASE)
match = uuid_pattern.search(stem)
@@ -60,7 +73,7 @@ def find_sidecar_json(image_path: str) -> Optional[str]:
if uuid_json.exists():
return str(uuid_json)
# Pattern 4: Just the stem without prefix numbers
# Pattern 5: Just the stem without prefix numbers
# e.g., "01_filename" -> look for "filename.json"
stem_no_prefix = re.sub(r'^\d+_', '', stem)
if stem_no_prefix != stem:
@@ -68,7 +81,7 @@ def find_sidecar_json(image_path: str) -> Optional[str]:
if alt_json.exists():
return str(alt_json)
# Pattern 5: Extract numeric ID from filename and find JSON with same ID
# Pattern 6: Extract numeric ID from filename and find JSON with same ID
# Handles HentaiFoundry where image is "hentaifoundry_1000220_title.jpg"
# but JSON is "Artist-1000220-title.json"
# Look for a 5-7 digit number that could be a post ID
@@ -165,7 +178,7 @@ def load_sidecar_metadata(json_path: str) -> Optional[dict]:
def extract_platform(metadata: dict) -> Optional[str]:
"""Extract the platform name from metadata."""
# Check common fields
# Check common fields (covers Pixiv which uses "category": "pixiv")
if "category" in metadata:
return metadata["category"].lower()
@@ -179,6 +192,8 @@ def extract_platform(metadata: dict) -> Optional[str]:
return "subscribestar"
if "discord.com" in url:
return "discord"
if "pixiv.net" in url or "pximg.net" in url:
return "pixiv"
return None
@@ -195,8 +210,13 @@ def extract_artist(metadata: dict) -> Optional[str]:
if "artist" in metadata:
return metadata["artist"]
# For hentaifoundry
if "user" in metadata:
# For Pixiv - user is an object with name and account fields
if "user" in metadata and isinstance(metadata["user"], dict):
# Prefer account (username) over display name for consistency
return metadata["user"].get("account") or metadata["user"].get("name")
# For hentaifoundry - user is a string
if "user" in metadata and isinstance(metadata["user"], str):
return metadata["user"]
return None
@@ -238,6 +258,7 @@ def extract_description(metadata: dict) -> Optional[str]:
- Patreon: 'content' (may be HTML)
- SubscribeStar: 'content' (may be HTML)
- HentaiFoundry: 'description' (plain text)
- Pixiv: 'caption' (plain text or HTML)
"""
# Try description first (HentaiFoundry uses this)
if "description" in metadata:
@@ -245,6 +266,12 @@ def extract_description(metadata: dict) -> Optional[str]:
if desc and isinstance(desc, str) and desc.strip():
return desc.strip()
# Try caption (Pixiv uses this)
if "caption" in metadata:
caption = metadata["caption"]
if caption and isinstance(caption, str) and caption.strip():
return caption.strip()
# Try content (Patreon/SubscribeStar use this)
if "content" in metadata:
content = metadata["content"]
@@ -268,14 +295,23 @@ def extract_source_url(metadata: dict) -> Optional[str]:
- Patreon: 'url' (direct post link)
- SubscribeStar: construct from author_name and post_id
- HentaiFoundry: construct from user and index
- Pixiv: construct from id
"""
platform = extract_platform(metadata)
# Direct URL field (Patreon)
# Construct URL for Pixiv (do this first to avoid using the pximg.net image URL)
if platform == "pixiv":
post_id = metadata.get("id")
if post_id:
return f"https://www.pixiv.net/artworks/{post_id}"
# Direct URL field (Patreon) - but not for Pixiv where 'url' is the image URL
if "url" in metadata:
url = metadata["url"]
if url and isinstance(url, str) and url.startswith("http"):
return url
# Skip pximg.net URLs (these are image URLs, not post URLs)
if "pximg.net" not in url:
return url
# Construct URL for SubscribeStar
if platform == "subscribestar":
@@ -298,6 +334,10 @@ def extract_attachment_count(metadata: dict) -> int:
"""
Extract the number of attachments/images in the post.
"""
# Pixiv: use page_count field
if "page_count" in metadata:
return int(metadata["page_count"])
# Patreon: check images array or post_metadata.image_order
if "images" in metadata and isinstance(metadata["images"], list):
return len(metadata["images"])
@@ -319,7 +359,8 @@ def extract_date(metadata: dict) -> Optional[datetime]:
Extract the publication/post date from metadata.
Tries multiple common date fields and formats.
"""
date_fields = ["published_at", "date", "created_at", "timestamp"]
# Pixiv uses create_date, others use published_at, date, etc.
date_fields = ["published_at", "create_date", "date", "created_at", "timestamp"]
for field in date_fields:
if field not in metadata: