web-novel-scraper 1.1.1__py3-none-any.whl → 2.0.1__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 +118 -98
- web_novel_scraper/config_manager.py +84 -0
- web_novel_scraper/decode.py +30 -44
- web_novel_scraper/decode_guide/decode_guide.json +47 -0
- web_novel_scraper/file_manager.py +226 -257
- web_novel_scraper/novel_scraper.py +66 -43
- web_novel_scraper/request_manager.py +2 -2
- web_novel_scraper/utils.py +132 -2
- web_novel_scraper/version.py +1 -1
- {web_novel_scraper-1.1.1.dist-info → web_novel_scraper-2.0.1.dist-info}/METADATA +1 -1
- web_novel_scraper-2.0.1.dist-info/RECORD +19 -0
- web_novel_scraper-1.1.1.dist-info/RECORD +0 -18
- {web_novel_scraper-1.1.1.dist-info → web_novel_scraper-2.0.1.dist-info}/WHEEL +0 -0
- {web_novel_scraper-1.1.1.dist-info → web_novel_scraper-2.0.1.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,197 @@ 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,
|
227
|
-
|
228
|
-
|
229
|
-
else:
|
230
|
-
click.echo('New cover image set successfully.')
|
239
|
+
novel = obtain_novel(title, ctx.obj)
|
240
|
+
novel.set_cover_image(cover_image)
|
241
|
+
click.echo(f'Cover image saved successfully.')
|
231
242
|
|
232
243
|
@cli.command()
|
244
|
+
@click.pass_context
|
233
245
|
@title_option
|
234
|
-
@novel_base_dir_option
|
235
246
|
@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
247
|
@click.option('--auto-add-host', type=bool, help='Toggle automatic addition of the host to chapter URLs (use true or false).')
|
237
248
|
@click.option('--force-flaresolver', type=bool, help='Toggle forcing the use of FlareSolver (use true or false).')
|
238
249
|
@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(
|
250
|
+
def set_scraper_behavior(ctx, title, save_title_to_content, auto_add_host, force_flaresolver, hard_clean):
|
240
251
|
"""Set scraper behavior for a novel."""
|
241
|
-
novel = obtain_novel(title,
|
252
|
+
novel = obtain_novel(title, ctx.obj)
|
242
253
|
novel.set_scraper_behavior(
|
243
254
|
save_title_to_content=save_title_to_content,
|
244
255
|
auto_add_host=auto_add_host,
|
245
256
|
force_flaresolver=force_flaresolver,
|
246
257
|
hard_clean=hard_clean
|
247
258
|
)
|
259
|
+
novel.save_novel()
|
248
260
|
click.echo('New scraper behavior added successfully.')
|
249
261
|
|
250
262
|
@cli.command()
|
263
|
+
@click.pass_context
|
251
264
|
@title_option
|
252
|
-
|
253
|
-
def show_scraper_behavior(title, novel_base_dir):
|
265
|
+
def show_scraper_behavior(ctx, title):
|
254
266
|
"""Show scraper behavior of a novel."""
|
255
|
-
novel = obtain_novel(title,
|
267
|
+
novel = obtain_novel(title, ctx.obj)
|
256
268
|
click.echo(novel.scraper_behavior)
|
257
269
|
|
258
270
|
@cli.command()
|
271
|
+
@click.pass_context
|
259
272
|
@title_option
|
260
|
-
@novel_base_dir_option
|
261
273
|
@host_option
|
262
|
-
def set_host(
|
274
|
+
def set_host(ctx, title, host):
|
263
275
|
"""Set the host for a novel."""
|
264
|
-
novel = obtain_novel(title,
|
276
|
+
novel = obtain_novel(title, ctx.obj)
|
265
277
|
novel.set_host(host)
|
278
|
+
novel.save_novel()
|
266
279
|
click.echo('New host set successfully.')
|
267
280
|
|
268
281
|
# TOC MANAGEMENT COMMANDS
|
269
282
|
|
270
283
|
@cli.command()
|
284
|
+
@click.pass_context
|
271
285
|
@title_option
|
272
|
-
@novel_base_dir_option
|
273
286
|
@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(
|
287
|
+
def set_toc_main_url(ctx, title, toc_main_url):
|
275
288
|
"""Set the main URL for the TOC of a novel."""
|
276
|
-
novel = obtain_novel(title,
|
289
|
+
novel = obtain_novel(title, ctx.obj)
|
277
290
|
novel.set_toc_main_url(toc_main_url)
|
291
|
+
novel.save_novel()
|
278
292
|
|
279
293
|
@cli.command()
|
294
|
+
@click.pass_context
|
280
295
|
@title_option
|
281
|
-
@novel_base_dir_option
|
282
296
|
@create_toc_html_option(required=True)
|
283
297
|
@host_option
|
284
|
-
def add_toc_html(
|
298
|
+
def add_toc_html(ctx, title, toc_html, host):
|
285
299
|
"""Add TOC HTML to a novel."""
|
286
|
-
novel = obtain_novel(title,
|
300
|
+
novel = obtain_novel(title, ctx.obj)
|
287
301
|
html_content = toc_html.read()
|
288
302
|
novel.add_toc_html(html_content, host)
|
303
|
+
novel.save_novel()
|
289
304
|
|
290
305
|
@cli.command()
|
306
|
+
@click.pass_context
|
291
307
|
@title_option
|
292
|
-
@novel_base_dir_option
|
293
308
|
@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(
|
309
|
+
def sync_toc(ctx, title, reload_files):
|
295
310
|
"""Sync the TOC of a novel."""
|
296
|
-
novel = obtain_novel(title,
|
311
|
+
novel = obtain_novel(title, ctx.obj)
|
297
312
|
if novel.sync_toc(reload_files):
|
298
313
|
click.echo(
|
299
314
|
'Table of Contents synced with files, to see the new TOC use the command show-toc.')
|
300
315
|
else:
|
301
316
|
click.echo(
|
302
317
|
'Error with the TOC syncing, please check the TOC files and decoding options.', err=True)
|
318
|
+
novel.save_novel()
|
303
319
|
|
304
320
|
@cli.command()
|
321
|
+
@click.pass_context
|
305
322
|
@title_option
|
306
|
-
@novel_base_dir_option
|
307
323
|
@click.option('--auto-approve', is_flag=True, required=False, default=False, show_default=True, help='Auto approve.')
|
308
|
-
def delete_toc(
|
324
|
+
def delete_toc(ctx, title, auto_approve):
|
309
325
|
"""Delete the TOC of a novel."""
|
310
|
-
novel = obtain_novel(title,
|
326
|
+
novel = obtain_novel(title, ctx.obj)
|
311
327
|
if not auto_approve:
|
312
328
|
click.confirm(f'Are you sure you want to delete the TOC for {title}?', abort=True)
|
313
329
|
novel.delete_toc()
|
330
|
+
novel.save_novel()
|
314
331
|
|
315
332
|
@cli.command()
|
333
|
+
@click.pass_context
|
316
334
|
@title_option
|
317
|
-
|
318
|
-
def show_toc(title, novel_base_dir):
|
335
|
+
def show_toc(ctx, title):
|
319
336
|
"""Show the TOC of a novel."""
|
320
|
-
novel = obtain_novel(title,
|
337
|
+
novel = obtain_novel(title, ctx.obj)
|
321
338
|
click.echo(novel.show_toc())
|
322
339
|
|
323
340
|
# CHAPTER MANAGEMENT COMMANDS
|
324
341
|
|
325
342
|
@cli.command()
|
343
|
+
@click.pass_context
|
326
344
|
@title_option
|
327
|
-
@novel_base_dir_option
|
328
345
|
@click.option('--chapter-url', type=str, required=False, help='Chapter URL to be scrapped.')
|
329
346
|
@click.option('--chapter-num', type=int, required=False, help='Chapter number to be scrapped.')
|
330
347
|
@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(
|
348
|
+
def scrap_chapter(ctx, title, chapter_url, chapter_num, update_html):
|
332
349
|
"""Scrap a chapter of a novel."""
|
333
350
|
if (chapter_url is None and chapter_num is None) or (chapter_url and chapter_num):
|
334
351
|
raise click.UsageError("You must set exactly one: --chapter-url o --chapter-num.")
|
335
352
|
|
336
|
-
novel = obtain_novel(title,
|
353
|
+
novel = obtain_novel(title, ctx.obj)
|
337
354
|
|
338
355
|
if chapter_num is not None:
|
339
356
|
if chapter_num <= 0 or chapter_num > len(novel.chapters):
|
@@ -354,34 +371,37 @@ def scrap_chapter(title, novel_base_dir, chapter_url, chapter_num, update_html):
|
|
354
371
|
click.echo(chapter.chapter_content)
|
355
372
|
|
356
373
|
@cli.command()
|
374
|
+
@click.pass_context
|
357
375
|
@title_option
|
358
|
-
@novel_base_dir_option
|
359
376
|
@sync_toc_option
|
360
377
|
@click.option('--update-html', is_flag=True, default=False, show_default=True, help='If the chapter HTML is saved, it will be updated.')
|
361
378
|
@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(
|
379
|
+
def request_all_chapters(ctx, title, sync_toc, update_html, clean_chapters):
|
363
380
|
"""Request all chapters of a novel."""
|
364
|
-
novel = obtain_novel(title,
|
381
|
+
novel = obtain_novel(title, ctx.obj)
|
365
382
|
novel.request_all_chapters(
|
366
383
|
sync_toc=sync_toc, update_html=update_html, clean_chapters=clean_chapters)
|
384
|
+
novel.save_novel()
|
367
385
|
click.echo('All chapters requested and saved.')
|
368
386
|
|
369
387
|
@cli.command()
|
388
|
+
@click.pass_context
|
370
389
|
@title_option
|
371
|
-
|
372
|
-
def show_chapters(title, novel_base_dir):
|
390
|
+
def show_chapters(ctx, title):
|
373
391
|
"""Show chapters of a novel."""
|
374
|
-
novel = obtain_novel(title,
|
392
|
+
novel = obtain_novel(title, ctx.obj)
|
375
393
|
click.echo(novel.show_chapters())
|
394
|
+
click.echo(f'Config file: {ctx.obj["CONFIG_FILE"]}')
|
395
|
+
|
376
396
|
|
377
397
|
@cli.command()
|
398
|
+
@click.pass_context
|
378
399
|
@title_option
|
379
|
-
@novel_base_dir_option
|
380
400
|
@sync_toc_option
|
381
401
|
@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
402
|
@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
403
|
@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(
|
404
|
+
def save_novel_to_epub(ctx, title, sync_toc, start_chapter, end_chapter, chapters_by_book):
|
385
405
|
"""Save the novel to EPUB format."""
|
386
406
|
if start_chapter <= 0:
|
387
407
|
raise click.BadParameter(
|
@@ -395,7 +415,7 @@ def save_novel_to_epub(title, novel_base_dir, sync_toc, start_chapter, end_chapt
|
|
395
415
|
raise click.BadParameter(
|
396
416
|
'Should be a positive number.', param_hint='--chapters-by-book')
|
397
417
|
|
398
|
-
novel = obtain_novel(title,
|
418
|
+
novel = obtain_novel(title, ctx.obj)
|
399
419
|
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
420
|
click.echo('All books saved.')
|
401
421
|
else:
|
@@ -404,27 +424,27 @@ def save_novel_to_epub(title, novel_base_dir, sync_toc, start_chapter, end_chapt
|
|
404
424
|
# UTILS
|
405
425
|
|
406
426
|
@cli.command()
|
427
|
+
@click.pass_context
|
407
428
|
@title_option
|
408
|
-
@novel_base_dir_option
|
409
429
|
@click.option('--clean-chapters', is_flag=True, default=False, show_default=True, help='If the chapters HTML files are cleaned.')
|
410
430
|
@click.option('--clean-toc', is_flag=True, default=False, show_default=True, help='If the TOC files are cleaned.')
|
411
431
|
@click.option('--hard-clean', is_flag=True, default=False, show_default=True, help='If the files are more deeply cleaned.')
|
412
|
-
def clean_files(
|
432
|
+
def clean_files(ctx, title, clean_chapters, clean_toc, hard_clean):
|
413
433
|
"""Clean files of a novel."""
|
414
434
|
if not clean_chapters and not clean_toc:
|
415
435
|
click.echo(
|
416
436
|
'You must choose at least one of the options: --clean-chapters, --clean-toc.', err=True)
|
417
437
|
return
|
418
|
-
novel = obtain_novel(title,
|
438
|
+
novel = obtain_novel(title, ctx.obj)
|
419
439
|
novel.clean_files(clean_chapters=clean_chapters,
|
420
440
|
clean_toc=clean_toc, hard_clean=hard_clean)
|
421
441
|
|
422
442
|
@cli.command()
|
443
|
+
@click.pass_context
|
423
444
|
@title_option
|
424
|
-
|
425
|
-
def show_novel_dir(title, novel_base_dir):
|
445
|
+
def show_novel_dir(ctx, title):
|
426
446
|
"""Show the directory where the novel is saved."""
|
427
|
-
novel = obtain_novel(title,
|
447
|
+
novel = obtain_novel(title, ctx.obj)
|
428
448
|
click.echo(novel.show_novel_dir())
|
429
449
|
|
430
450
|
@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
|