Skip to content

Commit 88bfa2b

Browse files
committed
scraper_fantia: add filename/dir templating
1 parent 7b9b057 commit 88bfa2b

2 files changed

Lines changed: 104 additions & 26 deletions

File tree

scraper_fantia.py

Lines changed: 101 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import concurrent.futures
22
import re
33
import webbrowser
4+
from datetime import datetime
45
from pathlib import Path
56
from urllib.parse import urljoin
67

@@ -14,17 +15,27 @@
1415
HTML_POSTLIST = "https://fantia.jp/fanclubs/{}/posts?page={}"
1516
DEFAULT_UA = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36'
1617

18+
# Default templates for directory and filename formatting
19+
# Directory template variables:
20+
# {fanclub_id}, {fanclub_name}, {fanclub_full_name}, {creator_name}
21+
# {post_id}, {post_title}, {post_date}, {rating}
22+
# Filename template variables (in addition to directory ones):
23+
# {content_id}, {idx}, {stem_cleaned}
24+
# Note: extension (.ext) is always appended automatically and not part of the template
25+
DEFAULT_DIR_TEMPLATE = '{fanclub_full_name} ({fanclub_id})/{post_id} {post_title}'
26+
DEFAULT_FILENAME_TEMPLATE = '{content_id}{idx} {stem_cleaned}'
27+
28+
1729
class FantiaDownloader:
18-
def __init__(self, key, fanclub=None, output='.', subfolder_template=None, skip_existing=True, quick_stop=True):
30+
def __init__(self, key, fanclub=None, output='.', dir_template=None, filename_template=None, skip_existing=True, quick_stop=True):
1931
super().__init__()
2032
self.key = key
2133
self.fanclub = fanclub
2234
if not self.fanclub:
2335
print('[W] no fanclub id is given. "downloadAll()" won\'t work.')
2436
self.output = Path(output)
25-
self.subfolder_template = subfolder_template or '{fanclub_full_name} ({fanclub_id})'
26-
self.subfolder_name = None
27-
#TODO: self.filename_template = '{id} {title}'
37+
self.dir_template = dir_template or DEFAULT_DIR_TEMPLATE
38+
self.filename_template = filename_template or DEFAULT_FILENAME_TEMPLATE
2839
self.skip_existing = skip_existing
2940
self.quick_stop = skip_existing and quick_stop
3041
self.fanclub_info = None
@@ -56,13 +67,30 @@ def download_all(self):
5667
with self.fetch(API_FANCLUB.format(self.fanclub)) as r:
5768
self.update_fanclub_info(r.json()['fanclub'])
5869

59-
output_full = self.output / self.subfolder_name
60-
print(f'Fanclub {self.fanclub}: download all to {output_full}.')
61-
if output_full.exists():
62-
existing_ids = [m[1] for f in output_full.iterdir() if (m := re.match(r'(\d+) *', f.name))]
70+
# Get base output dir (fanclub level) for checking existing downloads
71+
# Extract only the fanclub-level part of the template (before any post-level placeholders)
72+
fanclub_subs = self._get_fanclub_substitutes()
73+
# Split dir_template and only format the parts that don't require post info
74+
template_parts = self.dir_template.split('/')
75+
base_parts = []
76+
for part in template_parts:
77+
if any(p in part for p in ['{post_id}', '{post_title}', '{post_date}', '{rating}']):
78+
break
79+
base_parts.append(part.format(**fanclub_subs))
80+
base_dir = self.output / '/'.join(base_parts) if base_parts else self.output
81+
82+
print(f'Fanclub {self.fanclub}: download all to {base_dir}.')
83+
existing_ids = []
84+
if base_dir.exists():
85+
# Check for subdirectories starting with post_id (new structure)
86+
for f in base_dir.iterdir():
87+
if f.is_dir() and (m := re.match(r'^(\d+)\b', f.name)):
88+
existing_ids.append(m.group(1))
89+
# Also check for files starting with post_id (old flat structure)
90+
for f in base_dir.iterdir():
91+
if f.is_file() and (m := re.match(r'^(\d+)\b', f.name)):
92+
existing_ids.append(m.group(1))
6393
existing_ids = list(dict.fromkeys(existing_ids))
64-
else:
65-
existing_ids = []
6694
print(f'{len(existing_ids)} ID(s) have already been downloaded.')
6795

6896
def fetch_all():
@@ -110,15 +138,53 @@ def fetch_gallery_page(self, page):
110138

111139
def update_fanclub_info(self, d):
112140
self.fanclub_info = d
113-
substitutes = dict(
114-
fanclub_full_name=self.fanclub_info['fanclub_name_with_creator_name'],
115-
fanclub_name=self.fanclub_info['fanclub_name'],
116-
creater_name=self.fanclub_info['creator_name'],
141+
142+
def _get_fanclub_substitutes(self):
143+
"""Get template substitutes from fanclub info."""
144+
if not self.fanclub_info:
145+
return {}
146+
return dict(
147+
fanclub_full_name=safeify(str(self.fanclub_info['fanclub_name_with_creator_name'])),
148+
fanclub_name=safeify(str(self.fanclub_info['fanclub_name'])),
149+
creator_name=safeify(str(self.fanclub_info['creator_name'])),
117150
fanclub_id=self.fanclub_info['id']
118151
)
119-
substitutes = {k: safeify(str(v)) for k, v in substitutes.items()}
120-
if not self.subfolder_name:
121-
self.subfolder_name = self.subfolder_template.format(**substitutes)
152+
153+
def _get_post_substitutes(self, post_data):
154+
"""Get template substitutes from post data."""
155+
# Parse posted_at date (format: "Fri, 16 Aug 2024 07:15:36 +0900")
156+
post_date = ''
157+
if posted_at := post_data.get('posted_at'):
158+
try:
159+
dt = datetime.strptime(posted_at, "%a, %d %b %Y %H:%M:%S %z")
160+
post_date = dt.strftime('%Y%m%d')
161+
except ValueError:
162+
post_date = ''
163+
164+
return dict(
165+
post_id=post_data.get('id', ''),
166+
post_title=safeify(str(post_data.get('title', ''))),
167+
post_date=post_date,
168+
rating=post_data.get('rating', '')
169+
)
170+
171+
def _format_dir(self, post_substitutes):
172+
"""Format directory path using template and substitutes."""
173+
subs = {**self._get_fanclub_substitutes(), **post_substitutes}
174+
return self.dir_template.format(**subs)
175+
176+
def _format_filename(self, post_substitutes, content_id, idx_string, stem_cleaned, ext):
177+
"""Format filename using template and substitutes."""
178+
subs = {
179+
**self._get_fanclub_substitutes(),
180+
**post_substitutes,
181+
'content_id': content_id,
182+
'idx': idx_string,
183+
'stem_cleaned': stem_cleaned
184+
}
185+
# Format the template, strip to remove trailing spaces, then append extension
186+
filename_without_ext = self.filename_template.format(**subs).strip()
187+
return f'{filename_without_ext}.{ext}'
122188

123189
def get_post_photos(self, id):
124190
print(f'Fetching post {id}...')
@@ -139,27 +205,37 @@ def get_post_photos(self, id):
139205
if not self.fanclub_info:
140206
self.update_fanclub_info(d['post']['fanclub'])
141207

208+
post_data = d['post']
209+
post_subs = self._get_post_substitutes(post_data)
210+
output_dir = self.output / self._format_dir(post_subs)
211+
# Ensure multi-level directory exists
212+
output_dir.mkdir(parents=True, exist_ok=True)
213+
142214
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as ex:
143-
if thumb := d['post'].get('thumb', None):
215+
if thumb := post_data.get('thumb', None):
144216
img_url = thumb['original']
145217
stem, ext = get_webname(img_url).rsplit('.', 1)
146-
ex.submit(download, img_url, filename= self.output / self.subfolder_name / f'{id} !cover.{ext}', verbose=1)
147-
if post_contents := d['post'].get('post_contents', None):
218+
cover_filename = f'!cover.{ext}'
219+
ex.submit(download, img_url, filename=output_dir / cover_filename, verbose=1)
220+
if post_contents := post_data.get('post_contents', None):
148221
for c in post_contents:
149222
cid = c['id']
150223
if photos := c.get('post_content_photos', None):
151224
for idx, p in enumerate(photos, 1):
152225
img_url = p['url']['original']
153226
stem, ext = get_webname(img_url).rsplit('.', 1)
154227
# Clean up the filename; remove all the UUID-ish garbage
155-
stem = re.sub(r'^[0-9a-fA-F]{8}_(.+)$', r'\1', stem)
156-
stem = re.sub(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}', '', stem)
228+
stem_cleaned = re.sub(r'^[0-9a-fA-F]{8}_(.+)$', r'\1', stem)
229+
stem_cleaned = re.sub(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}', '', stem_cleaned)
230+
stem_cleaned = stem_cleaned.strip()
157231
idx_string = '_' + str(idx).zfill(len(str(len(photos)))) if len(photos) > 1 else ''
158-
stem = f'{id} {cid}{idx_string} {stem}'.strip()
159-
ex.submit(download, img_url, filename=self.output / self.subfolder_name / f'{stem}.{ext}', verbose=1)
232+
filename = self._format_filename(post_subs, cid, idx_string, stem_cleaned, ext)
233+
ex.submit(download, img_url, filename=output_dir / filename, verbose=1)
160234
if 'download_uri' in c:
161235
dl_url = urljoin('https://fantia.jp', c['download_uri'])
162-
ex.submit(download, dl_url, filename=self.output / self.subfolder_name / f'{id} {cid} {c["filename"]}', session=self.session, verbose=1)
236+
# For download files, use original filename with content_id prefix
237+
dl_filename = f'{cid} {c["filename"]}'
238+
ex.submit(download, dl_url, filename=output_dir / dl_filename, session=self.session, verbose=1)
163239

164240
if __name__ == "__main__":
165241
pass

util.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1039,7 +1039,9 @@ def replace_suffix(f, content_type):
10391039
# don't replace equivalent extensions
10401040
ext_alias_groups = [
10411041
['.mp4', '.m4a', '.m4v', '.m4s'],
1042-
['.jpg', '.jpeg']
1042+
['.jpg', '.jpeg'],
1043+
['.m3u', '.m3u8'],
1044+
['.mp2t', '.ts'],
10431045
]
10441046
for aliases in ext_alias_groups:
10451047
if f.suffix.lower() in aliases and header_suffix in aliases:

0 commit comments

Comments
 (0)