Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
mikf
GitHub Repository: mikf/gallery-dl
Path: blob/master/scripts/supportedsites.py
5457 views
1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License version 2 as
6
# published by the Free Software Foundation.
7
8
"""Generate a Markdown document listing all supported sites"""
9
10
import os
11
import sys
12
import collections
13
14
import util
15
from gallery_dl import extractor
16
17
try:
18
from test import results
19
except ImportError:
20
results = None
21
22
23
CATEGORY_MAP = {
24
"2chan" : "Futaba Channel",
25
"35photo" : "35PHOTO",
26
"adultempire" : "Adult Empire",
27
"agnph" : "AGNPH",
28
"aibooru" : "AIBooru",
29
"allgirlbooru" : "All girl",
30
"ao3" : "Archive of Our Own",
31
"archivedmoe" : "Archived.Moe",
32
"archiveofsins" : "Archive of Sins",
33
"artstation" : "ArtStation",
34
"aryion" : "Eka's Portal",
35
"atfbooru" : "ATFBooru",
36
"azurlanewiki" : "Azur Lane Wiki",
37
"b4k" : "arch.b4k.dev",
38
"baraag" : "baraag",
39
"batoto" : "BATO.TO",
40
"bbc" : "BBC",
41
"booth" : "BOOTH",
42
"cien" : "Ci-en",
43
"cohost" : "cohost!",
44
"comicvine" : "Comic Vine",
45
"dankefuerslesen": "Danke fürs Lesen",
46
"deviantart" : "DeviantArt",
47
"drawfriends" : "Draw Friends",
48
"dynastyscans" : "Dynasty Reader",
49
"e621" : "e621",
50
"e926" : "e926",
51
"e6ai" : "e6AI",
52
"erome" : "EroMe",
53
"everia" : "EVERIA.CLUB",
54
"e-hentai" : "E-Hentai",
55
"exhentai" : "ExHentai",
56
"fallenangels" : "Fallen Angels Scans",
57
"fanbox" : "pixivFANBOX",
58
"fappic" : "Fappic.com",
59
"fashionnova" : "Fashion Nova",
60
"furaffinity" : "Fur Affinity",
61
"furry34" : "Furry 34 com",
62
"girlswithmuscle": "Girls with Muscle",
63
"hatenablog" : "HatenaBlog",
64
"hbrowse" : "HBrowse",
65
"hentai2read" : "Hentai2Read",
66
"hentaicosplay" : "Hentai Cosplay",
67
"hentaienvy" : "HentaiEnvy",
68
"hentaiera" : "HentaiEra",
69
"hentaifoundry" : "Hentai Foundry",
70
"hentaifox" : "HentaiFox",
71
"hentaihand" : "HentaiHand",
72
"hentaihere" : "HentaiHere",
73
"hentaiimg" : "Hentai Image",
74
"hentainexus" : "HentaiNexus",
75
"hentairox" : "HentaiRox",
76
"hentaizap" : "HentaiZap",
77
"hiperdex" : "HiperDEX",
78
"hitomi" : "Hitomi.la",
79
"horne" : "horne",
80
"idolcomplex" : "Idol Complex",
81
"illusioncardsbooru": "Illusion Game Cards",
82
"imagebam" : "ImageBam",
83
"imagefap" : "ImageFap",
84
"imagepond" : "ImagePond",
85
"imagetwist" : "ImageTwist",
86
"imgadult" : "ImgAdult",
87
"imgbb" : "ImgBB",
88
"imgbox" : "imgbox",
89
"imagechest" : "ImageChest",
90
"imgdrive" : "ImgDrive.net",
91
"imgkiwi" : "IMG.Kiwi",
92
"imgtaxi" : "ImgTaxi.com",
93
"imgth" : "imgth",
94
"imgur" : "imgur",
95
"imgwallet" : "ImgWallet.com",
96
"imhentai" : "IMHentai",
97
"imxto" : "IMX.to",
98
"joyreactor" : "JoyReactor",
99
"itchio" : "itch.io",
100
"jpgfish" : "JPG Fish",
101
"kabeuchi" : "かべうち",
102
"schalenetwork" : "Schale Network",
103
"leakgallery" : "Leak Gallery",
104
"livedoor" : "livedoor Blog",
105
"lofter" : "LOFTER",
106
"ohpolly" : "Oh Polly",
107
"omgmiamiswimwear": "Omg Miami Swimwear",
108
"mangadex" : "MangaDex",
109
"mangafox" : "Manga Fox",
110
"mangahere" : "Manga Here",
111
"mangakakalot" : "MangaKakalot",
112
"manganato" : "MangaNato",
113
"mangapark" : "MangaPark",
114
"mangaread" : "MangaRead",
115
"mariowiki" : "Super Mario Wiki",
116
"mastodon.social": "mastodon.social",
117
"mediawiki" : "MediaWiki",
118
"micmicidol" : "MIC MIC IDOL",
119
"myhentaigallery": "My Hentai Gallery",
120
"myportfolio" : "Adobe Portfolio",
121
"natomanga" : "MangaNato",
122
"naver-blog" : "Naver Blog",
123
"naver-chzzk" : "CHZZK",
124
"naver-webtoon" : "Naver Webtoon",
125
"nelomanga" : "MangaNelo",
126
"nhentai" : "nhentai",
127
"nijie" : "nijie",
128
"nozomi" : "Nozomi.la",
129
"nozrip" : "GaryC Booru",
130
"nsfwalbum" : "NSFWalbum.com",
131
"nudostar" : "NudoStar.TV",
132
"paheal" : "Rule 34",
133
"photovogue" : "PhotoVogue",
134
"picstate" : "PicState",
135
"pidgiwiki" : "PidgiWiki",
136
"pixeldrain" : "pixeldrain",
137
"pixhost" : "PiXhost",
138
"pixiv" : "[pixiv]",
139
"pixiv-novel" : "[pixiv] Novels",
140
"pornimage" : "Porn Image",
141
"pornpics" : "PornPics.com",
142
"pornreactor" : "PornReactor",
143
"postimg" : "Postimages",
144
"readcomiconline": "Read Comic Online",
145
"redbust" : "RedBust",
146
"rbt" : "RebeccaBlackTech",
147
"redgifs" : "RedGIFs",
148
"rozenarcana" : "Rozen Arcana",
149
"rule34" : "Rule 34",
150
"rule34hentai" : "Rule34Hentai",
151
"rule34us" : "Rule 34",
152
"rule34vault" : "R34 Vault",
153
"rule34xyz" : "Rule 34 XYZ",
154
"sankaku" : "Sankaku Channel",
155
"sankakucomplex" : "Sankaku Complex",
156
"seiga" : "Niconico Seiga",
157
"senmanga" : "Sen Manga",
158
"sensescans" : "Sense-Scans",
159
"sexcom" : "Sex.com",
160
"silverpic" : "SilverPic.com",
161
"simplyhentai" : "Simply Hentai",
162
"sizebooru" : "Size Booru",
163
"slickpic" : "SlickPic",
164
"slideshare" : "SlideShare",
165
"smugmug" : "SmugMug",
166
"speakerdeck" : "Speaker Deck",
167
"steamgriddb" : "SteamGridDB",
168
"subscribestar" : "SubscribeStar",
169
"tbib" : "The Big ImageBoard",
170
"tcbscans" : "TCB Scans",
171
"tco" : "Twitter t.co",
172
"thatpervert" : "ThatPervert",
173
"thebarchive" : "The /b/ Archive",
174
"thecollection" : "The /co/llection",
175
"tiktok" : "TikTok",
176
"tmohentai" : "TMOHentai",
177
"tumblrgallery" : "TumblrGallery",
178
"turboimagehost" : "TurboImageHost.com",
179
"vanillarock" : "もえぴりあ",
180
"vidyart2" : "/v/idyart2",
181
"vidyapics" : "Vidya Booru",
182
"vipr" : "Vipr.im",
183
"visuabusters" : "VISUABUSTERS",
184
"vk" : "VK",
185
"vsco" : "VSCO",
186
"wallpapercave" : "Wallpaper Cave",
187
"webmshare" : "webmshare",
188
"webtoons" : "WEBTOON",
189
"weebcentral" : "Weeb Central",
190
"wikiart" : "WikiArt.org",
191
"wikigg" : "wiki.gg",
192
"wikimediacommons": "Wikimedia Commons",
193
"xbunkr" : "xBunkr",
194
"xhamster" : "xHamster",
195
"xvideos" : "XVideos",
196
"yandere" : "yande.re",
197
"yiffverse" : "Yiff verse",
198
}
199
200
SUBCATEGORY_MAP = {
201
"" : "",
202
"art" : "Art",
203
"audio" : "Audio",
204
"doujin" : "Doujin",
205
"home" : "Home Feed",
206
"image" : "individual Images",
207
"index" : "Site Index",
208
"info" : "User Profile Information",
209
"issue" : "Comic Issues",
210
"manga" : "Manga",
211
"media" : "Media Files",
212
"popular": "Popular Images",
213
"recent" : "Recent Images",
214
"search" : "Search Results",
215
"status" : "Images from Statuses",
216
"tag" : "Tag Searches",
217
"tweets" : "",
218
"user" : "User Profiles",
219
"watch" : "Watches",
220
"direct-messages": "DMs",
221
"following" : "Followed Users",
222
"related-pin" : "related Pins",
223
"related-board" : "",
224
225
"ao3": {
226
"user-works" : "",
227
"user-series" : "",
228
"user-bookmark": "Bookmarks",
229
},
230
"arcalive": {
231
"user": "User Posts",
232
},
233
"artstation": {
234
"artwork": "Artwork Listings",
235
"collections": "",
236
},
237
"bilibili": {
238
"user-articles": "User Articles",
239
"user-articles-favorite": "User Article Favorites",
240
},
241
"bluesky": {
242
"posts": "",
243
},
244
"boosty": {
245
"feed": "Subscriptions Feed",
246
},
247
"civitai": {
248
"models": "Model Listings",
249
"images": "Image Listings",
250
"videos": "Video Listings",
251
"posts" : "Post Listings",
252
"search-models": "Model Searches",
253
"search-images": "Image Searches",
254
"user-models": "User Models",
255
"user-images": ("User Images", "Image Reactions"),
256
"user-posts" : "User Posts",
257
"user-videos": ("User Videos", "Video Reactions"),
258
"user-collections" : "User Collections",
259
"generated": "Generated Files",
260
},
261
"coomer": {
262
"discord" : "",
263
"discord-server": "",
264
"posts" : "",
265
},
266
"Danbooru": {
267
"artist-search": "Artist Searches",
268
"favgroup": "Favorite Groups",
269
},
270
"desktopography": {
271
"site": "",
272
},
273
"deviantart": {
274
"gallery-search": "Gallery Searches",
275
"stash" : "Sta.sh",
276
"status": "Status Updates",
277
"watch-posts": "",
278
},
279
"discord": {
280
"direct-message" : "",
281
},
282
"facebook": {
283
"photos" : "Profile Photos",
284
},
285
"fanbox": {
286
"supporting": "Supported User Feed",
287
"redirect" : "Pixiv Redirects",
288
},
289
"fapello": {
290
"path": ["Videos", "Trending Posts", "Popular Videos", "Top Models"],
291
},
292
"furaffinity": {
293
"submissions": "New Submissions",
294
},
295
"hatenablog": {
296
"archive": "Archive",
297
"entry" : "Individual Posts",
298
},
299
"hentaifoundry": {
300
"story": "",
301
},
302
"imgur": {
303
"favorite-folder": "Favorites Folders",
304
"me": "Personal Posts",
305
},
306
"inkbunny": {
307
"unread": "Unread Submissions",
308
},
309
"instagram": {
310
"posts": "",
311
"saved": "Saved Posts",
312
"tagged": "Tagged Posts",
313
"stories-tray": "Stories Home Tray",
314
},
315
"itaku": {
316
"posts": "",
317
},
318
"iwara": {
319
"user-images": "User Images",
320
"user-videos": "User Videos",
321
"user-playlists": "User Playlists",
322
},
323
"kemono": {
324
"discord" : "Discord Servers",
325
"discord-server": "",
326
"posts" : "",
327
},
328
"leakgallery": {
329
"trending" : "Trending Medias",
330
"mostliked": "Most Liked Posts",
331
},
332
"lensdump": {
333
"albums": "",
334
},
335
"lofter": {
336
"blog-posts": "Blog Posts",
337
},
338
"mangadex": {
339
"feed": "Updates Feed",
340
"following" : "Library",
341
"list": "MDLists",
342
},
343
"misskey": {
344
"note" : "Notes",
345
"notes": "User Notes",
346
},
347
"nijie": {
348
"followed": "Followed Users",
349
"nuita" : "Nuita History",
350
},
351
"pinterest": {
352
"board": "",
353
"pinit": "pin.it Links",
354
"created": "Created Pins",
355
"allpins": "All Pins",
356
},
357
"pixeldrain": {
358
"folder": "Filesystems",
359
},
360
"pixiv": {
361
"me" : "pixiv.me Links",
362
"novel-bookmark": "Novel Bookmarks",
363
"novel-series": "Novel Series",
364
"novel-user": "",
365
"pixivision": "pixivision",
366
"sketch": "Sketch",
367
"unlisted": "Unlisted Works",
368
"work": "individual Images",
369
},
370
"poringa": {
371
"post": "Posts Images",
372
},
373
"pornhub": {
374
"gifs": "",
375
},
376
"raddle": {
377
"usersubmissions": "User Profiles",
378
"post" : "Individual Posts",
379
"shorturl" : "",
380
},
381
"redbust": {
382
"gallery": ("Galleries", "Categories"),
383
},
384
"redgifs": {
385
"collections": "",
386
},
387
"sankaku": {
388
"books": "Book Searches",
389
},
390
"scrolller": {
391
"following": "Followed Subreddits",
392
},
393
"sexcom": {
394
"pins": "User Pins",
395
},
396
"sizebooru": {
397
"user": "User Uploads",
398
},
399
"skeb": {
400
"following" : "Followed Creators",
401
"following-users": "Followed Users",
402
"sentrequests" : "Sent Requests",
403
},
404
"smugmug": {
405
"path": "Images from Users and Folders",
406
},
407
"steamgriddb": {
408
"asset": "Individual Assets",
409
},
410
"tiktok": {
411
"vmpost": "VM Posts",
412
},
413
"tumblr": {
414
"day": "Days",
415
},
416
"twitter": {
417
"media": "Media Timelines",
418
"tweets": "",
419
"replies": "",
420
"community": "",
421
"list-members": "List Members",
422
},
423
"vk": {
424
"tagged": "Tagged Photos",
425
},
426
"vsco": {
427
"spaces": "",
428
},
429
"wallhaven": {
430
"collections": "",
431
"uploads" : "",
432
},
433
"wallpapercave": {
434
"image": ["individual Images", "Search Results"],
435
},
436
"weasyl": {
437
"journals" : "",
438
"submissions": "",
439
},
440
"weibo": {
441
"home": "",
442
"newvideo": "",
443
},
444
"wikiart": {
445
"artists": "Artist Listings",
446
},
447
"wikimedia": {
448
"article": ["Articles", "Categories", "Files"],
449
},
450
}
451
452
BASE_MAP = {
453
"E621" : "e621 Instances",
454
"foolfuuka" : "FoolFuuka 4chan Archives",
455
"foolslide" : "FoOlSlide Instances",
456
"gelbooru_v01": "Gelbooru Beta 0.1.11",
457
"gelbooru_v02": "Gelbooru Beta 0.2",
458
"hentaicosplays": "Hentai Cosplay Instances",
459
"imagehost" : "Image Hosting Sites",
460
"IMHentai" : "IMHentai and Mirror Sites",
461
"jschan" : "jschan Imageboards",
462
"lolisafe" : "lolisafe and chibisafe",
463
"lynxchan" : "LynxChan Imageboards",
464
"manganelo" : "MangaNelo and Mirror Sites",
465
"moebooru" : "Moebooru and MyImouto",
466
"szurubooru" : "szurubooru Instances",
467
"urlshortener": "URL Shorteners",
468
"vichan" : "vichan Imageboards",
469
}
470
471
URL_MAP = {
472
"blogspot" : "https://www.blogger.com/",
473
"wikimedia": "https://www.wikimedia.org/",
474
}
475
476
_OAUTH = '<a href="https://github.com/mikf/gallery-dl#oauth">OAuth</a>'
477
_COOKIES = '<a href="https://github.com/mikf/gallery-dl#cookies">Cookies</a>'
478
_APIKEY_DB = ('<a href="https://gdl-org.github.io/docs/configuration.html'
479
'#extractor-derpibooru-api-key">API Key</a>')
480
_APIKEY_WH = ('<a href="https://gdl-org.github.io/docs/configuration.html'
481
'#extractor-wallhaven-api-key">API Key</a>')
482
_APIKEY_WY = ('<a href="https://gdl-org.github.io/docs/configuration.html'
483
'#extractor-weasyl-api-key">API Key</a>')
484
485
AUTH_MAP = {
486
"aibooru" : "Supported",
487
"ao3" : "Supported",
488
"aryion" : "Supported",
489
"atfbooru" : "Supported",
490
"baraag" : _OAUTH,
491
"bluesky" : "Supported",
492
"booruvar" : "Supported",
493
"boosty" : _COOKIES,
494
"coomer" : "Supported",
495
"danbooru" : "Supported",
496
"derpibooru" : _APIKEY_DB,
497
"deviantart" : _OAUTH,
498
"e621" : "Supported",
499
"e6ai" : "Supported",
500
"e926" : "Supported",
501
"e-hentai" : "Supported",
502
"exhentai" : "Supported",
503
"facebook" : _COOKIES,
504
"fanbox" : _COOKIES,
505
"fantia" : _COOKIES,
506
"flickr" : _OAUTH,
507
"furaffinity" : _COOKIES,
508
"furbooru" : "API Key",
509
"girlswithmuscle": "Supported",
510
"horne" : "Required",
511
"idolcomplex" : "Supported",
512
"imgbb" : "Supported",
513
"inkbunny" : "Supported",
514
"instagram" : _COOKIES,
515
"iwara" : "Supported",
516
"kemono" : "Supported",
517
"madokami" : "Required",
518
"mangadex" : "Supported",
519
"mangoxo" : "Supported",
520
"mastodon.social": _OAUTH,
521
"newgrounds" : "Supported",
522
"nijie" : "Required",
523
"patreon" : _COOKIES,
524
"pawoo" : _OAUTH,
525
"pillowfort" : "Supported",
526
"pinterest" : _COOKIES,
527
"pixiv" : _OAUTH,
528
"pixiv-novel" : _OAUTH,
529
"ponybooru" : "API Key",
530
"reddit" : _OAUTH,
531
"rule34xyz" : "Supported",
532
"sankaku" : "Supported",
533
"scrolller" : "Supported",
534
"seiga" : "Supported",
535
"smugmug" : _OAUTH,
536
"subscribestar" : "Supported",
537
"tapas" : "Supported",
538
"tiktok" : _COOKIES,
539
"tsumino" : "Supported",
540
"tumblr" : _OAUTH,
541
"twitter" : "Supported",
542
"vipergirls" : "Supported",
543
"wallhaven" : _APIKEY_WH,
544
"weasyl" : _APIKEY_WY,
545
"zerochan" : "Supported",
546
}
547
548
IGNORE_LIST = (
549
"directlink",
550
"oauth",
551
"recursive",
552
"test",
553
"ytdl",
554
"generic",
555
"noop",
556
)
557
558
559
def domain(cls):
560
"""Return the domain name associated with an extractor class"""
561
try:
562
url = sys.modules[cls.__module__].__doc__.split()[-1]
563
if url.startswith("http"):
564
return url
565
except Exception:
566
pass
567
568
if hasattr(cls, "root") and cls.root:
569
return cls.root + "/"
570
571
url = cls.example
572
return url[:url.find("/", 8)+1]
573
574
575
def category_text(c):
576
"""Return a human-readable representation of a category"""
577
return CATEGORY_MAP.get(c) or c.capitalize()
578
579
580
def subcategory_text(bc, c, sc):
581
"""Return a human-readable representation of a subcategory"""
582
if c in SUBCATEGORY_MAP:
583
scm = SUBCATEGORY_MAP[c]
584
if sc in scm:
585
txt = scm[sc]
586
if not isinstance(txt, str):
587
txt = ", ".join(txt)
588
return txt
589
590
if bc and bc in SUBCATEGORY_MAP:
591
scm = SUBCATEGORY_MAP[bc]
592
if sc in scm:
593
txt = scm[sc]
594
if not isinstance(txt, str):
595
txt = ", ".join(txt)
596
return txt
597
598
if sc in SUBCATEGORY_MAP:
599
return SUBCATEGORY_MAP[sc]
600
601
sc = sc.capitalize()
602
if sc.endswith("y"):
603
sc = sc[:-1] + "ies"
604
elif not sc.endswith("s"):
605
sc += "s"
606
return sc
607
608
609
def category_key(c):
610
"""Generate sorting keys by category"""
611
return category_text(c[0]).lower().lstrip("[")
612
613
614
def subcategory_key(sc):
615
"""Generate sorting keys by subcategory"""
616
return "A" if sc == "issue" else sc
617
618
619
def build_extractor_list():
620
"""Generate a sorted list of lists of extractor classes"""
621
categories = collections.defaultdict(lambda: collections.defaultdict(list))
622
default = categories[""]
623
domains = {"": ""}
624
625
for extr in extractor._list_classes():
626
category = extr.category
627
if category in IGNORE_LIST:
628
continue
629
if category:
630
if extr.basecategory == "imagehost":
631
base = categories[extr.basecategory]
632
else:
633
base = default
634
base[category].append(extr.subcategory)
635
if category not in domains:
636
domains[category] = domain(extr)
637
else:
638
base = categories[extr.basecategory]
639
if not extr.instances:
640
base[""].append(extr.subcategory)
641
continue
642
for category, root, info in extr.instances:
643
base[category].append(extr.subcategory)
644
if category not in domains:
645
if not root:
646
if category in URL_MAP:
647
root = URL_MAP[category].rstrip("/")
648
elif results:
649
# use domain from first matching test
650
test = results.category(category)[0]
651
root = test["#class"].from_url(test["#url"]).root
652
domains[category] = root + "/"
653
654
# sort subcategory lists
655
for base in categories.values():
656
for subcategories in base.values():
657
subcategories.sort(key=subcategory_key)
658
659
domains["pixiv-novel"] += "novel"
660
661
# add e-hentai.org
662
default["e-hentai"] = default["exhentai"]
663
domains["e-hentai"] = domains["exhentai"].replace("x", "-")
664
665
# add coomer.st
666
default["coomer"] = default["kemono"]
667
domains["coomer"] = "https://coomer.st/"
668
669
# add wikifeetx.com
670
default["wikifeetx"] = default["wikifeet"]
671
domains["wikifeetx"] = "https://www.wikifeetx.com/"
672
673
# imgdrive / imgtaxi / imgwallet
674
base = categories["imagehost"]
675
base["imgtaxi"] = base["imgdrive"]
676
base["imgwallet"] = base["imgdrive"]
677
categories["imagehost"] = {k: base[k] for k in sorted(base)}
678
domains["imgtaxi"] = "https://imgtaxi.com/"
679
domains["imgwallet"] = "https://imgwallet.com/"
680
681
# add extra e621 extractors
682
categories["E621"]["e621"].extend(default.pop("e621", ()))
683
684
return categories, domains
685
686
687
# define table columns
688
COLUMNS = (
689
("Site", 20,
690
lambda bc, c, scs, d: category_text(c)),
691
("URL" , 35,
692
lambda bc, c, scs, d: d),
693
("Capabilities", 50,
694
lambda bc, c, scs, d: ", ".join(subcategory_text(bc, c, sc) for sc in scs
695
if subcategory_text(bc, c, sc))),
696
("Authentication", 16,
697
lambda bc, c, scs, d: AUTH_MAP.get(c, "")),
698
)
699
700
701
def generate_output(columns, categories, domains):
702
703
thead = []
704
thead.append("<tr>")
705
for column in columns:
706
thead.append(f" <th>{column[0]}</th>")
707
thead.append("</tr>")
708
709
tbody = []
710
for bcat, base in categories.items():
711
if bcat and base:
712
name = BASE_MAP.get(bcat) or (bcat.capitalize() + " Instances")
713
tbody.append(f"""
714
<tr id="{bcat}" title="{bcat}">
715
<td colspan="4"><strong>{name}</strong></td>
716
</tr>\
717
""")
718
clist = base.items()
719
else:
720
clist = sorted(base.items(), key=category_key)
721
722
for category, subcategories in clist:
723
tbody.append(f"""<tr id="{category}" title="{category}">""")
724
for column in columns:
725
domain = domains[category]
726
content = column[2](bcat, category, subcategories, domain)
727
tbody.append(f" <td>{content}</td>")
728
tbody.append("</tr>")
729
730
NL = "\n"
731
GENERATOR = "/".join(os.path.normpath(__file__).split(os.sep)[-2:])
732
return f"""\
733
# Supported Sites
734
735
<!-- auto-generated by {GENERATOR} -->
736
Consider all listed sites to potentially be NSFW.
737
738
<table>
739
<thead valign="bottom">
740
{NL.join(thead)}
741
</thead>
742
<tbody valign="top">
743
{NL.join(tbody)}
744
</tbody>
745
</table>
746
"""
747
748
749
def main(path=None):
750
categories, domains = build_extractor_list()
751
752
if path is None:
753
path = util.path("docs", "supportedsites.md")
754
with util.lazy(path) as fp:
755
fp.write(generate_output(COLUMNS, categories, domains))
756
757
758
if __name__ == "__main__":
759
main(sys.argv[1] if len(sys.argv) > 1 else None)
760
761