web-novel-scraper 1.1.0__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- web_novel_scraper/__main__.py +116 -94
- web_novel_scraper/config_manager.py +84 -0
- web_novel_scraper/decode.py +49 -38
- web_novel_scraper/decode_guide/decode_guide.json +85 -0
- web_novel_scraper/file_manager.py +226 -257
- web_novel_scraper/novel_scraper.py +90 -46
- web_novel_scraper/request_manager.py +70 -57
- web_novel_scraper/utils.py +139 -2
- web_novel_scraper/version.py +1 -1
- {web_novel_scraper-1.1.0.dist-info → web_novel_scraper-2.0.0.dist-info}/METADATA +1 -1
- web_novel_scraper-2.0.0.dist-info/RECORD +19 -0
- web_novel_scraper-1.1.0.dist-info/RECORD +0 -18
- {web_novel_scraper-1.1.0.dist-info → web_novel_scraper-2.0.0.dist-info}/WHEEL +0 -0
- {web_novel_scraper-1.1.0.dist-info → web_novel_scraper-2.0.0.dist-info}/entry_points.txt +0 -0
web_novel_scraper/__main__.py
CHANGED
@@ -1,37 +1,32 @@
|
|
1
|
-
import json
|
2
1
|
from pathlib import Path
|
3
|
-
import sys
|
4
2
|
from datetime import datetime
|
3
|
+
from typing import Optional
|
5
4
|
|
6
5
|
import click
|
7
6
|
|
8
|
-
from .
|
7
|
+
from .config_manager import ScraperConfig
|
9
8
|
from .novel_scraper import Novel
|
10
9
|
from .version import __version__
|
11
10
|
|
12
11
|
CURRENT_DIR = Path(__file__).resolve().parent
|
13
12
|
|
14
|
-
def
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
else:
|
32
|
-
click.echo(
|
33
|
-
'Novel with that title does not exist or the main data file was deleted.', err=True)
|
34
|
-
sys.exit(1)
|
13
|
+
def global_options(f):
|
14
|
+
f = click.option('-nb', '--novel-base-dir', type=click.Path(), required=False, help="Alternative directory for this novel.")(f)
|
15
|
+
f = click.option('--config-file', type=click.Path(), required=False, help="Path to config file.")(f)
|
16
|
+
f = click.option('--base-novels-dir', type=click.Path(), required=False, help="Alternative base directory for all novels.")(f)
|
17
|
+
f = click.option('--decode-guide-file', type=click.Path(), required=False, help="Path to alternative decode guide file.")(f)
|
18
|
+
return f
|
19
|
+
|
20
|
+
def obtain_novel(title, ctx_opts, allow_missing=False):
|
21
|
+
cfg = ScraperConfig(ctx_opts.get("CONFIG_FILE"), ctx_opts.get("BASE_NOVELS_DIR"))
|
22
|
+
try:
|
23
|
+
return Novel.load(title, cfg, ctx_opts.get("NOVEL_BASE_DIR"))
|
24
|
+
except ValueError:
|
25
|
+
if allow_missing:
|
26
|
+
return None
|
27
|
+
click.echo("Novel not found.", err=True)
|
28
|
+
exit(1)
|
29
|
+
|
35
30
|
|
36
31
|
def validate_date(ctx, param, value):
|
37
32
|
"""Validate the date format."""
|
@@ -57,8 +52,15 @@ novel_base_dir_option = click.option(
|
|
57
52
|
'-nb', '--novel-base-dir', type=str, help='Alternative base directory for the novel files.')
|
58
53
|
|
59
54
|
@click.group()
|
60
|
-
|
55
|
+
@global_options
|
56
|
+
@click.pass_context
|
57
|
+
def cli(ctx, novel_base_dir, config_file, base_novels_dir, decode_guide_file):
|
61
58
|
"""CLI Tool for web novel scraping."""
|
59
|
+
ctx.ensure_object(dict)
|
60
|
+
ctx.obj['NOVEL_BASE_DIR'] = novel_base_dir
|
61
|
+
ctx.obj['CONFIG_FILE'] = config_file
|
62
|
+
ctx.obj['BASE_NOVELS_DIR'] = base_novels_dir
|
63
|
+
ctx.obj['DECODE_GUIDE_FILE'] = decode_guide_file
|
62
64
|
|
63
65
|
# Metadata:
|
64
66
|
metadata_author_option = click.option(
|
@@ -100,8 +102,8 @@ force_flaresolver_option = click.option('--force-flaresolver', is_flag=True, sho
|
|
100
102
|
# Novel creation and data management commands
|
101
103
|
|
102
104
|
@cli.command()
|
105
|
+
@click.pass_context
|
103
106
|
@title_option
|
104
|
-
@novel_base_dir_option
|
105
107
|
@toc_main_url_option
|
106
108
|
@create_toc_html_option()
|
107
109
|
@host_option
|
@@ -115,9 +117,9 @@ force_flaresolver_option = click.option('--force-flaresolver', is_flag=True, sho
|
|
115
117
|
@save_title_to_content_option
|
116
118
|
@auto_add_host_option
|
117
119
|
@force_flaresolver_option
|
118
|
-
def create_novel(
|
120
|
+
def create_novel(ctx, title, toc_main_url, toc_html, host, author, start_date, end_date, language, description, tags, cover, save_title_to_content, auto_add_host, force_flaresolver):
|
119
121
|
"""Creates a new novel and saves it."""
|
120
|
-
novel = obtain_novel(title,
|
122
|
+
novel = obtain_novel(title, ctx.obj, allow_missing=True)
|
121
123
|
if novel:
|
122
124
|
click.confirm(f'A novel with the title {title} already exists, do you want to replace it?', abort=True)
|
123
125
|
novel.delete_toc()
|
@@ -138,9 +140,16 @@ def create_novel(title, novel_base_dir, toc_main_url, toc_html, host, author, st
|
|
138
140
|
toc_html_content = None
|
139
141
|
if toc_html:
|
140
142
|
toc_html_content = toc_html.read()
|
141
|
-
|
142
|
-
|
143
|
-
toc_html=toc_html_content,
|
143
|
+
novel = Novel(title=title,
|
144
|
+
toc_main_url=toc_main_url,
|
145
|
+
toc_html=toc_html_content,
|
146
|
+
host=host
|
147
|
+
)
|
148
|
+
novel.set_config(config_file=ctx.obj.get('CONFIG_FILE'),
|
149
|
+
base_novels_dir=ctx.obj.get('BASE_NOVELS_DIR'),
|
150
|
+
novel_base_dir=ctx.obj.get('NOVEL_BASE_DIR'),
|
151
|
+
decode_guide_file=ctx.obj.get('DECODE_GUIDE_FILE')
|
152
|
+
)
|
144
153
|
novel.set_metadata(author=author, start_date=start_date,
|
145
154
|
end_date=end_date, language=language, description=description)
|
146
155
|
novel.set_scraper_behavior(save_title_to_content=save_title_to_content,
|
@@ -151,189 +160,199 @@ def create_novel(title, novel_base_dir, toc_main_url, toc_html, host, author, st
|
|
151
160
|
if cover:
|
152
161
|
if not novel.set_cover_image(cover):
|
153
162
|
click.echo('Error saving the novel cover image.', err=True)
|
163
|
+
novel.save_novel()
|
154
164
|
click.echo('Novel saved successfully.')
|
155
165
|
|
156
166
|
@cli.command()
|
167
|
+
@click.pass_context
|
157
168
|
@title_option
|
158
|
-
|
159
|
-
def show_novel_info(title, novel_base_dir):
|
169
|
+
def show_novel_info(ctx, title):
|
160
170
|
"""Show information about a novel."""
|
161
|
-
novel = obtain_novel(title,
|
171
|
+
novel = obtain_novel(title, ctx.obj)
|
162
172
|
click.echo(novel)
|
163
173
|
|
164
174
|
@cli.command()
|
175
|
+
@click.pass_context
|
165
176
|
@title_option
|
166
|
-
@novel_base_dir_option
|
167
177
|
@metadata_author_option
|
168
178
|
@metadata_start_date_option
|
169
179
|
@metadata_end_date_option
|
170
180
|
@metadata_language_option
|
171
181
|
@metadata_description_option
|
172
|
-
def set_metadata(
|
182
|
+
def set_metadata(ctx, title, author, start_date, end_date, language, description):
|
173
183
|
"""Set metadata for a novel."""
|
174
|
-
novel = obtain_novel(title,
|
184
|
+
novel = obtain_novel(title, ctx.obj)
|
175
185
|
novel.set_metadata(author=author, start_date=start_date,
|
176
186
|
end_date=end_date, language=language, description=description)
|
187
|
+
novel.save_novel()
|
177
188
|
click.echo('Novel metadata saved successfully.')
|
178
189
|
click.echo(novel.metadata)
|
179
190
|
|
180
191
|
@cli.command()
|
192
|
+
@click.pass_context
|
181
193
|
@title_option
|
182
|
-
|
183
|
-
def show_metadata(title, novel_base_dir):
|
194
|
+
def show_metadata(ctx, title):
|
184
195
|
"""Show metadata of a novel."""
|
185
|
-
novel = obtain_novel(title,
|
196
|
+
novel = obtain_novel(title, ctx.obj)
|
186
197
|
click.echo(novel.metadata)
|
187
198
|
|
188
199
|
@cli.command()
|
200
|
+
@click.pass_context
|
189
201
|
@title_option
|
190
|
-
@novel_base_dir_option
|
191
202
|
@click.option('--tag', 'tags', type=str, help='Tag to be added', multiple=True)
|
192
|
-
def add_tags(
|
203
|
+
def add_tags(ctx, title, tags):
|
193
204
|
"""Add tags to a novel."""
|
194
|
-
novel = obtain_novel(title,
|
205
|
+
novel = obtain_novel(title, ctx.obj)
|
195
206
|
for tag in tags:
|
196
207
|
if not novel.add_tag(tag):
|
197
208
|
click.echo(f'Tag {tag} already exists', err=True)
|
209
|
+
novel.save_novel()
|
198
210
|
click.echo(f'Tags: {", ".join(novel.metadata.tags)}')
|
199
211
|
|
200
212
|
@cli.command()
|
213
|
+
@click.pass_context
|
201
214
|
@title_option
|
202
|
-
@novel_base_dir_option
|
203
215
|
@click.option('--tag', 'tags', type=str, help='Tag to be removed.', multiple=True)
|
204
|
-
def remove_tags(
|
216
|
+
def remove_tags(ctx, title, tags):
|
205
217
|
"""Remove tags from a novel."""
|
206
|
-
novel = obtain_novel(title,
|
218
|
+
novel = obtain_novel(title, ctx.obj)
|
207
219
|
for tag in tags:
|
208
220
|
if not novel.remove_tag(tag):
|
209
221
|
click.echo(f'Tag {tag} does not exist.', err=True)
|
222
|
+
novel.save_novel()
|
210
223
|
click.echo(f'Tags: {", ".join(novel.metadata.tags)}')
|
211
224
|
|
212
225
|
@cli.command()
|
226
|
+
@click.pass_context
|
213
227
|
@title_option
|
214
|
-
|
215
|
-
def show_tags(title, novel_base_dir):
|
228
|
+
def show_tags(ctx, title):
|
216
229
|
"""Show tags of a novel."""
|
217
|
-
novel = obtain_novel(title,
|
230
|
+
novel = obtain_novel(title, ctx.obj)
|
218
231
|
click.echo(f'Tags: {", ".join(novel.metadata.tags)}')
|
219
232
|
|
220
233
|
@cli.command()
|
234
|
+
@click.pass_context
|
221
235
|
@title_option
|
222
|
-
@novel_base_dir_option
|
223
236
|
@click.option('--cover-image', type=str, required=True, help='Filepath of the cover image.')
|
224
|
-
def set_cover_image(
|
237
|
+
def set_cover_image(ctx, title, cover_image):
|
225
238
|
"""Set the cover image for a novel."""
|
226
|
-
novel = obtain_novel(title,
|
239
|
+
novel = obtain_novel(title, ctx.obj)
|
227
240
|
if not novel.set_cover_image(cover_image):
|
228
241
|
click.echo('Error saving the cover image.', err=True)
|
229
242
|
else:
|
230
243
|
click.echo('New cover image set successfully.')
|
231
244
|
|
232
245
|
@cli.command()
|
246
|
+
@click.pass_context
|
233
247
|
@title_option
|
234
|
-
@novel_base_dir_option
|
235
248
|
@click.option('--save-title-to-content', type=bool, help='Toggle the title of the chapter being added to the content (use true or false).')
|
236
249
|
@click.option('--auto-add-host', type=bool, help='Toggle automatic addition of the host to chapter URLs (use true or false).')
|
237
250
|
@click.option('--force-flaresolver', type=bool, help='Toggle forcing the use of FlareSolver (use true or false).')
|
238
251
|
@click.option('--hard-clean', type=bool, help='Toggle using a hard clean when cleaning HTML files (use true or false).')
|
239
|
-
def set_scraper_behavior(
|
252
|
+
def set_scraper_behavior(ctx, title, save_title_to_content, auto_add_host, force_flaresolver, hard_clean):
|
240
253
|
"""Set scraper behavior for a novel."""
|
241
|
-
novel = obtain_novel(title,
|
254
|
+
novel = obtain_novel(title, ctx.obj)
|
242
255
|
novel.set_scraper_behavior(
|
243
256
|
save_title_to_content=save_title_to_content,
|
244
257
|
auto_add_host=auto_add_host,
|
245
258
|
force_flaresolver=force_flaresolver,
|
246
259
|
hard_clean=hard_clean
|
247
260
|
)
|
261
|
+
novel.save_novel()
|
248
262
|
click.echo('New scraper behavior added successfully.')
|
249
263
|
|
250
264
|
@cli.command()
|
265
|
+
@click.pass_context
|
251
266
|
@title_option
|
252
|
-
|
253
|
-
def show_scraper_behavior(title, novel_base_dir):
|
267
|
+
def show_scraper_behavior(ctx, title):
|
254
268
|
"""Show scraper behavior of a novel."""
|
255
|
-
novel = obtain_novel(title,
|
269
|
+
novel = obtain_novel(title, ctx.obj)
|
256
270
|
click.echo(novel.scraper_behavior)
|
257
271
|
|
258
272
|
@cli.command()
|
273
|
+
@click.pass_context
|
259
274
|
@title_option
|
260
|
-
@novel_base_dir_option
|
261
275
|
@host_option
|
262
|
-
def set_host(
|
276
|
+
def set_host(ctx, title, host):
|
263
277
|
"""Set the host for a novel."""
|
264
|
-
novel = obtain_novel(title,
|
278
|
+
novel = obtain_novel(title, ctx.obj)
|
265
279
|
novel.set_host(host)
|
280
|
+
novel.save_novel()
|
266
281
|
click.echo('New host set successfully.')
|
267
282
|
|
268
283
|
# TOC MANAGEMENT COMMANDS
|
269
284
|
|
270
285
|
@cli.command()
|
286
|
+
@click.pass_context
|
271
287
|
@title_option
|
272
|
-
@novel_base_dir_option
|
273
288
|
@click.option('--toc-main-url', type=str, required=True, help='New TOC main URL (Previous links will be deleted).')
|
274
|
-
def set_toc_main_url(
|
289
|
+
def set_toc_main_url(ctx, title, toc_main_url):
|
275
290
|
"""Set the main URL for the TOC of a novel."""
|
276
|
-
novel = obtain_novel(title,
|
291
|
+
novel = obtain_novel(title, ctx.obj)
|
277
292
|
novel.set_toc_main_url(toc_main_url)
|
293
|
+
novel.save_novel()
|
278
294
|
|
279
295
|
@cli.command()
|
296
|
+
@click.pass_context
|
280
297
|
@title_option
|
281
|
-
@novel_base_dir_option
|
282
298
|
@create_toc_html_option(required=True)
|
283
299
|
@host_option
|
284
|
-
def add_toc_html(
|
300
|
+
def add_toc_html(ctx, title, toc_html, host):
|
285
301
|
"""Add TOC HTML to a novel."""
|
286
|
-
novel = obtain_novel(title,
|
302
|
+
novel = obtain_novel(title, ctx.obj)
|
287
303
|
html_content = toc_html.read()
|
288
304
|
novel.add_toc_html(html_content, host)
|
305
|
+
novel.save_novel()
|
289
306
|
|
290
307
|
@cli.command()
|
308
|
+
@click.pass_context
|
291
309
|
@title_option
|
292
|
-
@novel_base_dir_option
|
293
310
|
@click.option('--reload-files', is_flag=True, required=False, default=False, show_default=True, help='Reload the TOC files before sync (only works if using a TOC URL).')
|
294
|
-
def sync_toc(
|
311
|
+
def sync_toc(ctx, title, reload_files):
|
295
312
|
"""Sync the TOC of a novel."""
|
296
|
-
novel = obtain_novel(title,
|
313
|
+
novel = obtain_novel(title, ctx.obj)
|
297
314
|
if novel.sync_toc(reload_files):
|
298
315
|
click.echo(
|
299
316
|
'Table of Contents synced with files, to see the new TOC use the command show-toc.')
|
300
317
|
else:
|
301
318
|
click.echo(
|
302
319
|
'Error with the TOC syncing, please check the TOC files and decoding options.', err=True)
|
320
|
+
novel.save_novel()
|
303
321
|
|
304
322
|
@cli.command()
|
323
|
+
@click.pass_context
|
305
324
|
@title_option
|
306
|
-
@novel_base_dir_option
|
307
325
|
@click.option('--auto-approve', is_flag=True, required=False, default=False, show_default=True, help='Auto approve.')
|
308
|
-
def delete_toc(
|
326
|
+
def delete_toc(ctx, title, auto_approve):
|
309
327
|
"""Delete the TOC of a novel."""
|
310
|
-
novel = obtain_novel(title,
|
328
|
+
novel = obtain_novel(title, ctx.obj)
|
311
329
|
if not auto_approve:
|
312
330
|
click.confirm(f'Are you sure you want to delete the TOC for {title}?', abort=True)
|
313
331
|
novel.delete_toc()
|
332
|
+
novel.save_novel()
|
314
333
|
|
315
334
|
@cli.command()
|
335
|
+
@click.pass_context
|
316
336
|
@title_option
|
317
|
-
|
318
|
-
def show_toc(title, novel_base_dir):
|
337
|
+
def show_toc(ctx, title):
|
319
338
|
"""Show the TOC of a novel."""
|
320
|
-
novel = obtain_novel(title,
|
339
|
+
novel = obtain_novel(title, ctx.obj)
|
321
340
|
click.echo(novel.show_toc())
|
322
341
|
|
323
342
|
# CHAPTER MANAGEMENT COMMANDS
|
324
343
|
|
325
344
|
@cli.command()
|
345
|
+
@click.pass_context
|
326
346
|
@title_option
|
327
|
-
@novel_base_dir_option
|
328
347
|
@click.option('--chapter-url', type=str, required=False, help='Chapter URL to be scrapped.')
|
329
348
|
@click.option('--chapter-num', type=int, required=False, help='Chapter number to be scrapped.')
|
330
349
|
@click.option('--update-html', is_flag=True, default=False, show_default=True, help='If the chapter HTML is saved, it will be updated.')
|
331
|
-
def scrap_chapter(
|
350
|
+
def scrap_chapter(ctx, title, chapter_url, chapter_num, update_html):
|
332
351
|
"""Scrap a chapter of a novel."""
|
333
352
|
if (chapter_url is None and chapter_num is None) or (chapter_url and chapter_num):
|
334
353
|
raise click.UsageError("You must set exactly one: --chapter-url o --chapter-num.")
|
335
354
|
|
336
|
-
novel = obtain_novel(title,
|
355
|
+
novel = obtain_novel(title, ctx.obj)
|
337
356
|
|
338
357
|
if chapter_num is not None:
|
339
358
|
if chapter_num <= 0 or chapter_num > len(novel.chapters):
|
@@ -354,34 +373,37 @@ def scrap_chapter(title, novel_base_dir, chapter_url, chapter_num, update_html):
|
|
354
373
|
click.echo(chapter.chapter_content)
|
355
374
|
|
356
375
|
@cli.command()
|
376
|
+
@click.pass_context
|
357
377
|
@title_option
|
358
|
-
@novel_base_dir_option
|
359
378
|
@sync_toc_option
|
360
379
|
@click.option('--update-html', is_flag=True, default=False, show_default=True, help='If the chapter HTML is saved, it will be updated.')
|
361
380
|
@click.option('--clean-chapters', is_flag=True, default=False, show_default=True, help='If the chapter HTML should be cleaned upon saving.')
|
362
|
-
def request_all_chapters(
|
381
|
+
def request_all_chapters(ctx, title, sync_toc, update_html, clean_chapters):
|
363
382
|
"""Request all chapters of a novel."""
|
364
|
-
novel = obtain_novel(title,
|
383
|
+
novel = obtain_novel(title, ctx.obj)
|
365
384
|
novel.request_all_chapters(
|
366
385
|
sync_toc=sync_toc, update_html=update_html, clean_chapters=clean_chapters)
|
386
|
+
novel.save_novel()
|
367
387
|
click.echo('All chapters requested and saved.')
|
368
388
|
|
369
389
|
@cli.command()
|
390
|
+
@click.pass_context
|
370
391
|
@title_option
|
371
|
-
|
372
|
-
def show_chapters(title, novel_base_dir):
|
392
|
+
def show_chapters(ctx, title):
|
373
393
|
"""Show chapters of a novel."""
|
374
|
-
novel = obtain_novel(title,
|
394
|
+
novel = obtain_novel(title, ctx.obj)
|
375
395
|
click.echo(novel.show_chapters())
|
396
|
+
click.echo(f'Config file: {ctx.obj["CONFIG_FILE"]}')
|
397
|
+
|
376
398
|
|
377
399
|
@cli.command()
|
400
|
+
@click.pass_context
|
378
401
|
@title_option
|
379
|
-
@novel_base_dir_option
|
380
402
|
@sync_toc_option
|
381
403
|
@click.option('--start-chapter', type=int, default=1, show_default=True, help='The start chapter for the books (position in the TOC, may differ from the actual number).')
|
382
404
|
@click.option('--end-chapter', type=int, default=None, show_default=True, help='The end chapter for the books (if not defined, every chapter will be saved).')
|
383
405
|
@click.option('--chapters-by-book', type=int, default=100, show_default=True, help='The number of chapters each book will have.')
|
384
|
-
def save_novel_to_epub(
|
406
|
+
def save_novel_to_epub(ctx, title, sync_toc, start_chapter, end_chapter, chapters_by_book):
|
385
407
|
"""Save the novel to EPUB format."""
|
386
408
|
if start_chapter <= 0:
|
387
409
|
raise click.BadParameter(
|
@@ -395,7 +417,7 @@ def save_novel_to_epub(title, novel_base_dir, sync_toc, start_chapter, end_chapt
|
|
395
417
|
raise click.BadParameter(
|
396
418
|
'Should be a positive number.', param_hint='--chapters-by-book')
|
397
419
|
|
398
|
-
novel = obtain_novel(title,
|
420
|
+
novel = obtain_novel(title, ctx.obj)
|
399
421
|
if novel.save_novel_to_epub(sync_toc=sync_toc, start_chapter=start_chapter, end_chapter=end_chapter, chapters_by_book=chapters_by_book):
|
400
422
|
click.echo('All books saved.')
|
401
423
|
else:
|
@@ -404,27 +426,27 @@ def save_novel_to_epub(title, novel_base_dir, sync_toc, start_chapter, end_chapt
|
|
404
426
|
# UTILS
|
405
427
|
|
406
428
|
@cli.command()
|
429
|
+
@click.pass_context
|
407
430
|
@title_option
|
408
|
-
@novel_base_dir_option
|
409
431
|
@click.option('--clean-chapters', is_flag=True, default=False, show_default=True, help='If the chapters HTML files are cleaned.')
|
410
432
|
@click.option('--clean-toc', is_flag=True, default=False, show_default=True, help='If the TOC files are cleaned.')
|
411
433
|
@click.option('--hard-clean', is_flag=True, default=False, show_default=True, help='If the files are more deeply cleaned.')
|
412
|
-
def clean_files(
|
434
|
+
def clean_files(ctx, title, clean_chapters, clean_toc, hard_clean):
|
413
435
|
"""Clean files of a novel."""
|
414
436
|
if not clean_chapters and not clean_toc:
|
415
437
|
click.echo(
|
416
438
|
'You must choose at least one of the options: --clean-chapters, --clean-toc.', err=True)
|
417
439
|
return
|
418
|
-
novel = obtain_novel(title,
|
440
|
+
novel = obtain_novel(title, ctx.obj)
|
419
441
|
novel.clean_files(clean_chapters=clean_chapters,
|
420
442
|
clean_toc=clean_toc, hard_clean=hard_clean)
|
421
443
|
|
422
444
|
@cli.command()
|
445
|
+
@click.pass_context
|
423
446
|
@title_option
|
424
|
-
|
425
|
-
def show_novel_dir(title, novel_base_dir):
|
447
|
+
def show_novel_dir(ctx, title):
|
426
448
|
"""Show the directory where the novel is saved."""
|
427
|
-
novel = obtain_novel(title,
|
449
|
+
novel = obtain_novel(title, ctx.obj)
|
428
450
|
click.echo(novel.show_novel_dir())
|
429
451
|
|
430
452
|
@cli.command()
|
@@ -0,0 +1,84 @@
|
|
1
|
+
import os
|
2
|
+
import json
|
3
|
+
|
4
|
+
import platformdirs
|
5
|
+
from dotenv import load_dotenv
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import Optional
|
8
|
+
|
9
|
+
from .logger_manager import create_logger
|
10
|
+
from .utils import FileOps
|
11
|
+
|
12
|
+
load_dotenv()
|
13
|
+
|
14
|
+
CURRENT_DIR = Path(__file__).resolve().parent
|
15
|
+
|
16
|
+
app_author = "web-novel-scraper"
|
17
|
+
app_name = "web-novel-scraper"
|
18
|
+
|
19
|
+
# DEFAULT VALUES
|
20
|
+
SCRAPER_CONFIG_FILE = str(Path(platformdirs.user_config_dir(app_name, app_author)) / "config.json")
|
21
|
+
SCRAPER_BASE_NOVELS_DIR = platformdirs.user_data_dir(app_name, app_author)
|
22
|
+
SCRAPER_DECODE_GUIDE_FILE = str(CURRENT_DIR / 'decode_guide/decode_guide.json')
|
23
|
+
|
24
|
+
logger = create_logger("CONFIG MANAGER")
|
25
|
+
|
26
|
+
|
27
|
+
## ORDER PRIORITY
|
28
|
+
## 1. PARAMETER TO THE INIT FUNCTION
|
29
|
+
## 2. ENVIRONMENT VARIABLE
|
30
|
+
## 3. CONFIG FILE VALUE
|
31
|
+
## 4. DEFAULT VALUE
|
32
|
+
class ScraperConfig:
|
33
|
+
base_novels_dir: str
|
34
|
+
decode_guide_file: str
|
35
|
+
|
36
|
+
def __init__(self,
|
37
|
+
config_file: str = None,
|
38
|
+
base_novels_dir: str = None,
|
39
|
+
decode_guide_file: str = None):
|
40
|
+
## LOADING CONFIGURATION
|
41
|
+
config_file = self._get_config(default_value=SCRAPER_CONFIG_FILE,
|
42
|
+
config_file_value=None,
|
43
|
+
env_variable="SCRAPER_CONFIG_FILE",
|
44
|
+
parameter_value=config_file)
|
45
|
+
|
46
|
+
config_file = Path(config_file)
|
47
|
+
logger.debug(f'Obtaining configuration from file "{config_file}"')
|
48
|
+
config = self._load_config(config_file)
|
49
|
+
|
50
|
+
if config is None:
|
51
|
+
logger.debug('No configuration found on config file.')
|
52
|
+
logger.debug('If no other config option was set, the default configuration will be used.')
|
53
|
+
config = {}
|
54
|
+
|
55
|
+
## SETTING CONFIGURATION VALUES
|
56
|
+
|
57
|
+
self.base_novels_dir = self._get_config(default_value=SCRAPER_BASE_NOVELS_DIR,
|
58
|
+
config_file_value=config.get("base_novels_dir"),
|
59
|
+
env_variable="SCRAPER_BASE_NOVELS_DIR",
|
60
|
+
parameter_value=base_novels_dir)
|
61
|
+
|
62
|
+
self.decode_guide_file = self._get_config(default_value=SCRAPER_DECODE_GUIDE_FILE,
|
63
|
+
config_file_value=config.get("decode_guide_file"),
|
64
|
+
env_variable="SCRAPER_DECODE_GUIDE_FILE",
|
65
|
+
parameter_value=decode_guide_file)
|
66
|
+
|
67
|
+
@staticmethod
|
68
|
+
def _get_config(default_value: str,
|
69
|
+
config_file_value: Optional[str],
|
70
|
+
env_variable: str,
|
71
|
+
parameter_value: Optional[str]) -> str:
|
72
|
+
return (
|
73
|
+
parameter_value
|
74
|
+
or os.getenv(env_variable)
|
75
|
+
or config_file_value
|
76
|
+
or default_value
|
77
|
+
)
|
78
|
+
|
79
|
+
@staticmethod
|
80
|
+
def _load_config(config_file: Path) -> Optional[dict]:
|
81
|
+
config = FileOps.read_json(config_file)
|
82
|
+
if config is None:
|
83
|
+
logger.debug(f'Could not load configuration from file "{config_file}". Skipping...')
|
84
|
+
return config
|