-
-
Notifications
You must be signed in to change notification settings - Fork 32
/
main.py
679 lines (577 loc) · 20.2 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
import logging
from pathvalidate import sanitize_filename
from pathlib import Path
from .utils import (
validate_group_url,
validate_legacy_url,
validate_url,
comma_separated_text
)
from .language import Language, get_language
from .utils import download as download_file
from .errors import InvalidURL, NotAllowed
from .fetcher import *
from .mdlist import MangaDexList
from .manga import Manga, ContentRating
from .iterator import (
IteratorManga,
IteratorUserLibraryFollowsList,
IteratorUserLibraryList,
IteratorUserLibraryManga,
IteratorUserList
)
from .chapter import Chapter, MangaChapter
from .network import Net
from .format import default_save_as_format, get_format
from .cover import default_cover_type, valid_cover_types
log = logging.getLogger(__name__)
__all__ = (
'download', 'download_chapter', 'download_list',
'fetch', 'login', 'logout', 'search',
'download_legacy_manga', 'download_legacy_chapter',
'get_manga_from_user_library',
'get_list_from_user_library',
'get_list_from_user',
'get_followed_list_from_user_library'
)
def login(*args, **kwargs):
"""Login to MangaDex
Do not worry about token session, the library automatically handle this.
Login session will be automtically renewed (unless you called :meth:`logout()`).
Parameters
-----------
password: :class:`str`
Password to login
username: Optional[:class:`str`]
Username to login
email: Optional[:class:`str`]
Email to login
Raises
-------
AlreadyLoggedIn
User are already logged in
ValueError
Parameters are not valid
LoginFailed
Login credential are not valid
"""
Net.mangadex.login(*args, **kwargs)
def logout():
"""Logout from MangaDex
Raises
-------
NotLoggedIn
User are not logged in
"""
Net.mangadex.logout()
def _get_manga_from_chapter(chapter_id):
chap = Chapter(chapter_id)
manga = _fetch_manga(chap.manga_id, chap.language.value, fetch_all_chapters=False)
manga._chapters = MangaChapter(manga, chap.language.value, chap)
return chap, manga
def _fetch_manga(
manga_id,
lang,
fetch_all_chapters=True,
use_alt_details=False
):
manga = Manga(_id=manga_id, use_alt_details=use_alt_details)
if fetch_all_chapters:
# NOTE: After v0.4.0, fetch the chapters first before creating folder for downloading the manga
# and downloading the cover manga.
# This will check if selected language in manga has chapters inside of it.
# If the chapters are not available, it will throw error.
log.info("Fetching all chapters...")
chapters = MangaChapter(manga, lang, all_chapters=True)
manga._chapters = chapters
return manga
def search(*args, **kwargs):
"""Search manga
Parameters
-----------
title: :class:`str`
Manga title
unsafe: :class:`bool`
If ``True``, it will allow you to search "porn" content
Returns
--------
:class:`IteratorManga`
An iterator that yield :class:`Manga`
"""
return IteratorManga(*args, **kwargs)
def get_manga_from_user_library(*args, **kwargs):
"""Get all mangas from user library
You must login in order to use this function, or you will get error.
Parameters
-----------
status: :class:`str`
Filter retrieved manga based on status
unsafe: :class:`bool`
If ``True``, it will allow you to search "porn" content
Raises
--------
NotLoggedIn
Retrieving user library require login
Returns
--------
:class:`IteratorUserLibraryManga`
An iterator that yield :class:`Manga`
"""
return IteratorUserLibraryManga(*args, **kwargs)
def get_list_from_user_library():
"""Get all lists from user library
You must login in order to use this function, or you will get error.
Raises
-------
NotLoggedIn
Retrieving user library require login
Returns
--------
:class:`IteratorUserLibraryList`
An iterator that yield :class:`MangaDexList`
"""
return IteratorUserLibraryList()
def get_list_from_user(user_id):
"""Get all public lists from given user
Raises
-------
UserNotFound
user cannot be found
Returns
--------
:class:`IteratorUserList`
An iterator that yield :class:`MangaDexList`
"""
return IteratorUserList(user_id)
def get_followed_list_from_user_library():
"""Get all followed lists from user library
You must login in order to use this function, or you will get error.
Raises
-------
NotLoggedIn
Retrieving user library require login
Returns
--------
:class:`IteratorUserLibraryFollowsList`
An iterator that yield :class:`MangaDexList`
"""
return IteratorUserLibraryFollowsList()
def fetch(url, language=Language.English, use_alt_details=False, unsafe=False):
"""Fetch the manga
Parameters
-----------
url: :class:`str`
A MangaDex URL or manga id
language: :class:`Language` (default: :class:`Language.English`)
Select a translated language for manga
use_alt_details: :class:`bool` (default: ``False``)
Use alternative title and description manga
unsafe: :class:`bool`
If ``True``, it will allow you to see porn and erotica content
Raises
-------
InvalidURL
Not a valid MangaDex url
InvalidManga
Given manga cannot be found
ChapterNotFound
Given manga has no chapters
NotAllowed
``unsafe`` is not enabled
Returns
--------
:class:`Manga`
An fetched manga
"""
# Parse language
if isinstance(language, Language):
lang = language.value
elif isinstance(language, str):
lang = get_language(language).value
else:
raise ValueError("language must be Language or str, not %s" % language.__class__.__name__)
log.info("Using %s language" % Language(lang).name)
log.debug('Validating the url...')
try:
manga_id = validate_url(url)
except InvalidURL as e:
log.error('%s is not valid mangadex url' % url)
raise e from None
# Begin fetching
log.info('Fetching manga %s' % manga_id)
manga = _fetch_manga(manga_id, lang, use_alt_details=use_alt_details)
log.info("Found manga \"%s\"" % manga.title)
return manga
def download(
url,
folder=None,
replace=False,
compressed_image=False,
start_chapter=None,
end_chapter=None,
start_page=None,
end_page=None,
no_oneshot_chapter=False,
language=Language.English,
cover=default_cover_type,
save_as=default_save_as_format,
use_alt_details=False,
no_group_name=False,
group=None,
use_chapter_title=False,
unsafe=False,
no_verify=False,
_range=None
):
"""Download a manga
Parameters
-----------
url: :class:`str`
A MangaDex URL or manga id. It also accepting :class:`Manga` object
folder: :class:`str` (default: ``None``)
Store manga in given folder
replace: :class:`bool` (default: ``False``)
Replace manga if exist
compressed_image: :class:`bool` (default: ``False``)
Use compressed images for low size when downloading manga
start_chapter: :class:`float` (default: ``None``)
Start downloading manga from given chapter
end_chapter: :class:`float` (default: ``None``)
Stop downloading manga from given chapter
start_page: :class:`int` (default: ``None``)
Start download chapter page from given page number
end_page: :class:`int` (default: ``None``)
Stop download chapter page from given page number
no_oneshot_manga: :class:`bool` (default: ``False``)
If exist, don\'t download oneshot chapter
language: :class:`Language` (default: :class:`Language.English`)
Select a translated language for manga
cover: :class:`str` (default: ``original``)
Choose quality cover manga
save_as: :class:`str` (default: ``tachiyomi``)
Choose save as format
use_alt_details: :class:`bool` (default: ``False``)
Use alternative title and description manga
no_group_name: :class:`bool` (default: ``False``)
If ``True``, Do not use scanlation group name for each chapter.
group: :class:`str` (default: ``None``)
Use different scanlation group for each chapter.
use_chapter_title: :class:`bool` (default: ``False``)
If ``True``, use chapter title for each chapters.
NOTE: This option is useless if used with any single format.
unsafe: :class:`bool`
If ``True``, it will allow you to download porn and erotica content
no_verify: :class:`bool`
If ``True``, Skip hash checking for each images
_range: :class:`str`
A range pattern to download specific chapters
Raises
-------
InvalidURL
Not a valid MangaDex url
InvalidManga
Given manga cannot be found
ChapterNotFound
Given manga has no chapters
NotAllowed
``unsafe`` is not enabled
Returns
--------
:class:`Manga`
An downloaded manga
"""
# Validate start_chapter and end_chapter param
if start_chapter is not None and not isinstance(start_chapter, float):
raise ValueError("start_chapter must be float, not %s" % type(start_chapter))
if end_chapter is not None and not isinstance(end_chapter, float):
raise ValueError("end_chapter must be float, not %s" % type(end_chapter))
if start_chapter is not None and end_chapter is not None:
if start_chapter > end_chapter:
raise ValueError("start_chapter cannot be more than end_chapter")
if start_page is not None and end_page is not None:
if start_page > end_page:
raise ValueError("start_page cannot be more than end_page")
if cover not in valid_cover_types:
raise ValueError("invalid cover type, available are: %s" % valid_cover_types)
if group and group.lower().strip() == "all" and no_group_name:
raise ValueError("no_group_name cannot be True while group is used")
# Parse language
if isinstance(language, Language):
lang = language
elif isinstance(language, str):
lang = get_language(language)
else:
raise ValueError("language must be Language or str, not %s" % language.__class__.__name__)
log.info(f"Using {lang.name} language")
log.debug('Validating the url...')
try:
manga_id = validate_url(url)
except InvalidURL as e:
log.error('%s is not valid mangadex url' % url)
raise e from None
# Validate group
group_id = validate_group_url(group)
# Validation save as format
fmt_class = get_format(save_as)
if not isinstance(url, Manga):
manga = Manga(_id=manga_id, use_alt_details=use_alt_details)
else:
manga = url
# base path
base_path = Path('.')
# Extend the folder
if folder:
base_path /= folder
base_path /= sanitize_filename(manga.title)
# Create folder
log.debug("Creating folder for downloading")
base_path.mkdir(parents=True, exist_ok=True)
# Cover path
cover_path = base_path / 'cover.jpg'
log.info('Downloading cover manga %s' % manga.title)
# Determine cover art quality
if cover == "original":
cover_url = manga.cover_art
elif cover == "512px":
cover_url = manga.cover_art_512px
elif cover == "256px":
cover_url = manga.cover_art_256px
elif cover == 'none':
cover_url = None
# Download the cover art
if cover_url is None:
log.debug('Not downloading cover manga, since \"cover\" is none')
else:
download_file(cover_url, str(cover_path), replace=True)
# Reuse is good
def download_manga(m, path):
kwargs_iter_chapter_images = {
"start_chapter": start_chapter,
"end_chapter": end_chapter,
"start_page": start_page,
"end_page": end_page,
"no_oneshot": no_oneshot_chapter,
"data_saver": compressed_image,
"no_group_name": no_group_name,
"group": group_id,
"use_chapter_title": use_chapter_title,
"_range": _range
}
log.info("Using %s format" % save_as)
fmt = fmt_class(
path,
m,
compressed_image,
replace,
no_verify,
kwargs_iter_chapter_images
)
# Execute main format
fmt.main()
all_languages = lang == Language.All
if all_languages:
# Print info to users
# Let the users know how many translated languages available
# in given manga
translated_langs = [i.name for i in manga.translated_languages]
log.info(f"Available translated languages = {comma_separated_text(translated_langs)}")
for translated_lang in manga.translated_languages:
log.info(f"Downloading {manga.title} in {translated_lang.name} language")
# Copy title and description manga
new_manga = Manga(data=manga._data)
new_manga._title = manga.title
new_manga._description = manga.description
# Fetch all chapters
new_manga._chapters = MangaChapter(new_manga, translated_lang.value, all_chapters=True)
new_path = base_path / translated_lang.name
new_path.mkdir(exist_ok=True)
download_manga(new_manga, new_path)
log.info(f"Download finished for manga {manga.title} in {translated_lang.name} language")
else:
# I really want to use _fetch_manga()
# but it would waste 1 http request
# and can cause slow performance
log.info("Fetching all chapters...")
manga._chapters = MangaChapter(manga, lang.value, all_chapters=True)
download_manga(manga, base_path)
log.info("Download finished for manga \"%s\"" % manga.title)
return manga
def download_chapter(
url,
folder=None,
replace=False,
start_page=None,
end_page=None,
compressed_image=False,
save_as=default_save_as_format,
no_group_name=False,
use_chapter_title=False,
unsafe=False,
no_verify=False
):
"""Download a chapter
Parameters
-----------
url: :class:`str`
A MangaDex URL or chapter id
folder: :class:`str` (default: ``None``)
Store chapter manga in given folder
replace: :class:`bool` (default: ``False``)
Replace chapter manga if exist
start_page: :class:`int` (default: ``None``)
Start download chapter page from given page number
end_page: :class:`int` (default: ``None``)
Stop download chapter page from given page number
compressed_image: :class:`bool` (default: ``False``)
Use compressed images for low size when downloading chapter manga
save_as: :class:`str` (default: ``tachiyomi``)
Choose save as format
no_group_name: :class:`bool` (default: ``False``)
If ``True``, Do not use scanlation group name for each chapter.
use_chapter_title: :class:`bool` (default: ``False``)
If ``True``, use chapter title for each chapters.
NOTE: This option is useless if used with any single format.
unsafe: :class:`bool`
If ``True``, it will allow you to download porn and erotica content
no_verify: :class:`bool`
If ``True``, Skip hash checking for each images
Returns
--------
:class:`Manga`
An :class:`Manga` that has this chapter
"""
# Validate start_page and end_page param
if start_page is not None and not isinstance(start_page, int):
raise ValueError("start_page must be int, not %s" % type(start_page))
if end_page is not None and not isinstance(end_page, int):
raise ValueError("end_page must be int, not %s" % type(end_page))
if start_page is not None and end_page is not None:
if start_page > end_page:
raise ValueError("start_page cannot be more than end_page")
fmt_class = get_format(save_as)
log.debug('Validating the url...')
try:
chap_id = validate_url(url)
except InvalidURL as e:
log.error('%s is not valid mangadex url' % url)
raise e from None
# Fetch manga
chap, manga = _get_manga_from_chapter(chap_id)
log.info("Found chapter %s from manga \"%s\"" % (chap.chapter, manga.title))
# base path
base_path = Path('.')
# Extend the folder
if folder:
base_path /= folder
base_path /= sanitize_filename(manga.title)
# Create folder
log.debug("Creating folder for downloading")
base_path.mkdir(parents=True, exist_ok=True)
kwargs_iter_chapter_images = {
"start_page": start_page,
"end_page": end_page,
"no_oneshot": False,
"data_saver": compressed_image,
"no_group_name": no_group_name,
"use_chapter_title": use_chapter_title
}
log.info("Using %s format" % save_as)
fmt = fmt_class(
base_path,
manga,
compressed_image,
replace,
no_verify,
kwargs_iter_chapter_images
)
# Execute main format
fmt.main()
log.info("Finished download chapter %s from manga \"%s\"" % (chap.chapter, manga.title))
return manga
def download_list(
url,
folder=None,
replace=False,
compressed_image=False,
language=Language.English,
cover=default_cover_type,
save_as=default_save_as_format,
no_group_name=False,
group=None,
use_chapter_title=True,
unsafe=False,
no_verify=False
):
"""Download a list
Parameters
-----------
url: :class:`str`
A MangaDex URL or chapter id
folder: :class:`str` (default: ``None``)
Store chapter manga in given folder
replace: :class:`bool` (default: ``False``)
Replace chapter manga if exist
compressed_image: :class:`bool` (default: ``False``)
Use compressed images for low size when downloading chapter manga
save_as: :class:`str` (default: ``tachiyomi``)
Choose save as format
no_group_name: :class:`bool` (default: ``False``)
If ``True``, Do not use scanlation group name for each chapter.
group: :class:`str` (default: ``None``)
Use different scanlation group for each chapter.
use_chapter_title: :class:`bool` (default: ``False``)
If ``True``, use chapter title for each chapters.
NOTE: This option is useless if used with any single format.
unsafe: :class:`bool`
If ``True``, it will allow you to download porn and erotica content
no_verify: :class:`bool`
If ``True``, Skip hash checking for each images
"""
log.debug('Validating the url...')
try:
list_id = validate_url(url)
except InvalidURL as e:
log.error('%s is not valid mangadex url' % url)
raise e from None
_list = MangaDexList(_id=list_id)
for manga in _list.iter_manga(unsafe):
download(
manga.id,
folder,
replace,
compressed_image,
cover=cover,
save_as=save_as,
language=language,
no_group_name=no_group_name,
group=group,
use_chapter_title=use_chapter_title,
unsafe=unsafe,
no_verify=no_verify
)
def download_legacy_manga(url, *args, **kwargs):
"""Download manga from old MangaDex url
The rest of parameters will be passed to :meth:`download`.
"""
log.debug('Validating the url...')
try:
legacy_id = validate_legacy_url(url)
except InvalidURL as e:
log.error('%s is not valid mangadex url' % url)
raise e from None
new_id = get_legacy_id('manga', legacy_id)
manga = download(new_id, *args, **kwargs)
return manga
def download_legacy_chapter(url, *args, **kwargs):
"""Download chapter from old MangaDex url
The rest of parameters will be passed to :meth:`download_chapter`
"""
log.debug('Validating the url...')
try:
legacy_id = validate_legacy_url(url)
except InvalidURL as e:
log.error('%s is not valid mangadex url' % url)
raise e from None
new_id = get_legacy_id('chapter', legacy_id)
manga = download_chapter(new_id, *args, **kwargs)
return manga