1
0
mirror of https://github.com/mikf/gallery-dl.git synced 2024-11-22 18:53:21 +01:00
gallery-dl/scripts/supportedsites.py

524 lines
15 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
"""Generate a Markdown document listing all supported sites"""
import os
import sys
2019-02-20 19:25:41 +01:00
import collections
import util
from gallery_dl import extractor
CATEGORY_MAP = {
2017-07-15 15:01:30 +02:00
"2chan" : "Futaba Channel",
"35photo" : "35PHOTO",
"adultempire" : "Adult Empire",
"allgirlbooru" : "All girl",
2017-07-15 15:01:30 +02:00
"archivedmoe" : "Archived.Moe",
"archiveofsins" : "Archive of Sins",
"artstation" : "ArtStation",
"aryion" : "Eka's Portal",
"atfbooru" : "ATFBooru",
"b4k" : "arch.b4k.co",
"baraag" : "baraag",
"bbc" : "BBC",
"bcy" : "半次元",
"comicvine" : "Comic Vine",
"coomerparty" : "Coomer",
"deviantart" : "DeviantArt",
"drawfriends" : "Draw Friends",
"dynastyscans" : "Dynasty Reader",
"e621" : "e621",
"e926" : "e926",
"erome" : "EroMe",
"e-hentai" : "E-Hentai",
"exhentai" : "ExHentai",
"fallenangels" : "Fallen Angels Scans",
"fanbox" : "pixivFANBOX",
"fashionnova" : "Fashion Nova",
2020-02-11 19:51:24 +01:00
"furaffinity" : "Fur Affinity",
"hbrowse" : "HBrowse",
"hentai2read" : "Hentai2Read",
2021-04-18 20:28:00 +02:00
"hentaicosplays" : "Hentai Cosplay",
"hentaifoundry" : "Hentai Foundry",
"hentaifox" : "HentaiFox",
"hentaihand" : "HentaiHand",
"hentaihere" : "HentaiHere",
2021-04-18 20:28:00 +02:00
"hentaiimg" : "Hentai Image",
"hitomi" : "Hitomi.la",
"horne" : "horne",
"idolcomplex" : "Idol Complex",
"illusioncardsbooru": "Illusion Game Cards",
"imagebam" : "ImageBam",
"imagefap" : "ImageFap",
2019-07-30 23:02:21 +02:00
"imgbb" : "ImgBB",
"imgbox" : "imgbox",
"imagechest" : "ImageChest",
"imgth" : "imgth",
"imgur" : "imgur",
2021-11-25 16:18:48 +01:00
"joyreactor" : "JoyReactor",
"kabeuchi" : "かべうち",
"kemonoparty" : "Kemono",
"lineblog" : "LINE BLOG",
"livedoor" : "livedoor Blog",
2023-01-31 19:54:41 +01:00
"ohpolly" : "Oh Polly",
"omgmiamiswimwear": "Omg Miami Swimwear",
"mangadex" : "MangaDex",
"mangafox" : "Manga Fox",
"mangahere" : "Manga Here",
"mangakakalot" : "MangaKakalot",
"mangalife" : "MangaLife",
"manganelo" : "Manganato",
"mangapark" : "MangaPark",
"mangasee" : "MangaSee",
"mastodon.social": "mastodon.social",
2022-03-14 08:00:35 +01:00
"mememuseum" : "meme.museum",
"myhentaigallery": "My Hentai Gallery",
"myportfolio" : "Adobe Portfolio",
"naverwebtoon" : "NaverWebtoon",
"nhentai" : "nhentai",
"nijie" : "nijie",
"nozomi" : "Nozomi.la",
"nsfwalbum" : "NSFWalbum.com",
"paheal" : "rule #34",
2021-01-22 19:41:50 +01:00
"photovogue" : "PhotoVogue",
2021-04-18 20:28:00 +02:00
"pornimagesxxx" : "Porn Image",
"pornpics" : "PornPics.com",
2021-11-25 16:18:48 +01:00
"pornreactor" : "PornReactor",
"powermanga" : "PowerManga",
"readcomiconline": "Read Comic Online",
2017-07-24 10:50:40 +02:00
"rbt" : "RebeccaBlackTech",
2020-05-10 00:31:42 +02:00
"redgifs" : "RedGIFs",
"rozenarcana" : "Rozen Arcana",
"rule34" : "Rule 34",
"rule34us" : "Rule 34",
"sankaku" : "Sankaku Channel",
"sankakucomplex" : "Sankaku Complex",
"seiga" : "Niconico Seiga",
"senmanga" : "Sen Manga",
"sensescans" : "Sense-Scans",
"sexcom" : "Sex.com",
"simplyhentai" : "Simply Hentai",
2019-06-09 21:59:22 +02:00
"slickpic" : "SlickPic",
"slideshare" : "SlideShare",
"smugmug" : "SmugMug",
"speakerdeck" : "Speaker Deck",
"subscribestar" : "SubscribeStar",
"tbib" : "The Big ImageBoard",
"tcbscans" : "TCB Scans",
"thatpervert" : "ThatPervert",
2017-07-24 10:50:40 +02:00
"thebarchive" : "The /b/ Archive",
"thecollection" : "The /co/llection",
"tokyochronos" : "TokyoChronos",
"tumblrgallery" : "TumblrGallery",
"vanillarock" : "もえぴりあ",
"vidyart" : "/v/idyart",
2021-03-23 18:48:01 +01:00
"vk" : "VK",
2019-07-22 22:15:36 +02:00
"vsco" : "VSCO",
"wallpapercave" : "Wallpaper Cave",
"webmshare" : "webmshare",
"webtoons" : "Webtoon",
"wikiart" : "WikiArt.org",
"xbunkr" : "xBunkr",
"xhamster" : "xHamster",
"xvideos" : "XVideos",
"yandere" : "yande.re",
}
SUBCATEGORY_MAP = {
"art" : "Art",
"audio" : "Audio",
"doujin" : "Doujin",
"image" : "individual Images",
"index" : "Site Index",
"issue" : "Comic Issues",
"manga" : "Manga",
"media" : "Media Files",
"popular": "Popular Images",
"recent" : "Recent Images",
"search" : "Search Results",
"status" : "Images from Statuses",
"tag" : "Tag Searches",
"tweets" : "",
"user" : "User Profiles",
"watch" : "Watches",
"following" : "",
"related-pin" : "related Pins",
"related-board": "",
"artstation": {
"artwork": "Artwork Listings",
},
"coomerparty": {
"discord" : "",
"discord-server": "",
},
2021-09-17 20:09:24 +02:00
"desktopography": {
"site": "",
},
"deviantart": {
"stash" : "Sta.sh",
"status": "Status Updates",
"watch-posts": "",
},
"fanbox": {
"redirect": "",
},
"fapello": {
"path": "Videos, Trending Posts, Popular Videos, Top Models",
},
"gfycat": {
"collections": "",
},
"hentaifoundry": {
"story": "",
},
2020-03-16 22:57:30 +01:00
"instagram": {
"posts": "",
2020-03-16 22:57:30 +01:00
"saved": "Saved Posts",
"tagged": "Tagged Posts",
2020-03-16 22:57:30 +01:00
},
"kemonoparty": {
"discord": "Discord Servers",
"discord-server": "",
},
"mangadex": {
"feed" : "Followed Feed",
},
2022-10-04 09:23:24 +02:00
"nana": {
"search": "Favorites, Search Results",
},
"nijie": {
"followed": "Followed Users",
"nuita" : "Nuita History",
},
"pinterest": {
"board": "",
"pinit": "pin.it Links",
"created": "Created Pins",
"allpins": "All Pins",
},
"pixiv": {
"me" : "pixiv.me Links",
"pixivision": "pixivision",
2021-10-12 20:50:11 +02:00
"sketch": "Sketch",
"work": "individual Images",
},
2022-05-26 15:01:46 +02:00
"reddit": {
"home": "Home Feed",
},
"redgifs": {
"collections": "",
},
"sankaku": {
"books": "Book Searches",
},
"sexcom": {
"pins": "User Pins",
},
"smugmug": {
"path": "Images from Users and Folders",
},
"twitter": {
"media": "Media Timelines",
"tweets": "",
"replies": "",
"list-members": "List Members",
},
"vk": {
"tagged": "Tagged Photos",
},
"wallhaven": {
"collections": "",
"uploads" : "",
},
"wallpapercave": {
"image": "individual Images, Search Results",
},
"weasyl": {
"journals" : "",
"submissions": "",
},
"weibo": {
"home": "",
"newvideo": "",
},
"wikiart": {
"artists": "Artist Listings",
},
}
BASE_MAP = {
"E621" : "e621 Instances",
"foolfuuka" : "FoolFuuka 4chan Archives",
"foolslide" : "FoOlSlide Instances",
"gelbooru_v01": "Gelbooru Beta 0.1.11",
"gelbooru_v02": "Gelbooru Beta 0.2",
"lolisafe" : "lolisafe and chibisafe",
"lynxchan" : "LynxChan Imageboards",
"moebooru" : "Moebooru and MyImouto",
"vichan" : "vichan Imageboards",
}
_OAUTH = '<a href="https://github.com/mikf/gallery-dl#oauth">OAuth</a>'
_COOKIES = '<a href="https://github.com/mikf/gallery-dl#cookies">Cookies</a>'
_APIKEY_DB = \
'<a href="configuration.rst#extractorderpibooruapi-key">API Key</a>'
_APIKEY_WH = \
'<a href="configuration.rst#extractorwallhavenapi-key">API Key</a>'
_APIKEY_WY = \
'<a href="configuration.rst#extractorweasylapi-key">API Key</a>'
AUTH_MAP = {
"aibooru" : "Supported",
"aryion" : "Supported",
"atfbooru" : "Supported",
"baraag" : _OAUTH,
"coomerparty" : "Supported",
"danbooru" : "Supported",
"derpibooru" : _APIKEY_DB,
"deviantart" : _OAUTH,
"e621" : "Supported",
"e926" : "Supported",
"e-hentai" : "Supported",
"exhentai" : "Supported",
"fanbox" : _COOKIES,
"fantia" : _COOKIES,
"flickr" : _OAUTH,
"furaffinity" : _COOKIES,
"horne" : "Required",
"idolcomplex" : "Supported",
"imgbb" : "Supported",
"inkbunny" : "Supported",
"instagram" : _COOKIES,
"kemonoparty" : "Supported",
"mangadex" : "Supported",
"mangoxo" : "Supported",
"mastodon.social": _OAUTH,
"newgrounds" : "Supported",
"nijie" : "Required",
"patreon" : _COOKIES,
"pawoo" : _OAUTH,
"pillowfort" : "Supported",
"pinterest" : _COOKIES,
"pixiv" : _OAUTH,
"ponybooru" : "API Key",
"reddit" : _OAUTH,
"sankaku" : "Supported",
"seiga" : _COOKIES,
"seisoparty" : "Supported",
"smugmug" : _OAUTH,
"subscribestar" : "Supported",
"tapas" : "Supported",
"tsumino" : "Supported",
"tumblr" : _OAUTH,
"twitter" : "Supported",
"wallhaven" : _APIKEY_WH,
"weasyl" : _APIKEY_WY,
}
IGNORE_LIST = (
2019-02-20 19:25:41 +01:00
"directlink",
"oauth",
2019-02-20 19:25:41 +01:00
"recursive",
"test",
"ytdl",
"generic",
)
2019-02-20 19:25:41 +01:00
def domain(cls):
"""Return the web-domain related to an extractor class"""
try:
url = sys.modules[cls.__module__].__doc__.split()[-1]
if url.startswith("http"):
return url
except Exception:
pass
2019-02-20 19:25:41 +01:00
if hasattr(cls, "root") and cls.root:
return cls.root + "/"
2018-09-28 12:39:05 +02:00
2019-02-20 19:25:41 +01:00
if hasattr(cls, "https"):
scheme = "https" if cls.https else "http"
netloc = cls.__doc__.split()[-1]
return "{}://{}/".format(scheme, netloc)
2019-02-20 19:25:41 +01:00
test = next(cls._get_tests(), None)
if test:
url = test[0]
return url[:url.find("/", 8)+1]
2019-02-20 19:25:41 +01:00
return ""
2021-02-20 22:31:21 +01:00
def category_text(c):
2019-02-20 19:25:41 +01:00
"""Return a human-readable representation of a category"""
return CATEGORY_MAP.get(c) or c.capitalize()
2018-09-28 12:39:05 +02:00
2021-02-20 22:31:21 +01:00
def subcategory_text(c, sc):
2019-02-20 19:25:41 +01:00
"""Return a human-readable representation of a subcategory"""
if c in SUBCATEGORY_MAP:
scm = SUBCATEGORY_MAP[c]
if sc in scm:
return scm[sc]
2019-02-20 19:25:41 +01:00
if sc in SUBCATEGORY_MAP:
return SUBCATEGORY_MAP[sc]
2019-02-20 19:25:41 +01:00
sc = sc.capitalize()
if sc.endswith("y"):
sc = sc[:-1] + "ies"
elif not sc.endswith("s"):
sc += "s"
return sc
2021-02-20 22:31:21 +01:00
def category_key(c):
2019-02-20 19:25:41 +01:00
"""Generate sorting keys by category"""
2021-02-20 22:31:21 +01:00
return category_text(c[0]).lower()
2021-02-20 22:31:21 +01:00
def subcategory_key(sc):
2019-02-20 19:25:41 +01:00
"""Generate sorting keys by subcategory"""
2021-02-20 22:31:21 +01:00
return "A" if sc == "issue" else sc
2017-07-15 15:01:30 +02:00
2019-02-20 19:25:41 +01:00
def build_extractor_list():
"""Generate a sorted list of lists of extractor classes"""
categories = collections.defaultdict(lambda: collections.defaultdict(list))
default = categories[""]
2021-02-20 22:31:21 +01:00
domains = {}
2021-02-20 22:31:21 +01:00
for extr in extractor._list_classes():
category = extr.category
if category in IGNORE_LIST:
continue
2021-02-20 22:31:21 +01:00
if category:
default[category].append(extr.subcategory)
2021-02-20 22:31:21 +01:00
if category not in domains:
domains[category] = domain(extr)
else:
base = categories[extr.basecategory]
2021-02-20 22:31:21 +01:00
for category, root in extr.instances:
base[category].append(extr.subcategory)
2021-02-20 22:31:21 +01:00
if category not in domains:
if not root:
# use domain from first matching test
for url, _ in extr._get_tests():
if extr.from_url(url).category == category:
root = url[:url.index("/", 8)]
break
else:
continue
2021-02-20 22:31:21 +01:00
domains[category] = root + "/"
2021-02-20 22:31:21 +01:00
# sort subcategory lists
for base in categories.values():
for subcategories in base.values():
subcategories.sort(key=subcategory_key)
2021-02-20 22:31:21 +01:00
# add e-hentai.org
default["e-hentai"] = default["exhentai"]
2021-02-20 22:31:21 +01:00
domains["e-hentai"] = domains["exhentai"].replace("x", "-")
# add coomer.party
default["coomerparty"] = default["kemonoparty"]
domains["coomerparty"] = domains["kemonoparty"].replace("kemono", "coomer")
2021-04-18 20:28:00 +02:00
# add hentai-cosplays sister sites (hentai-img, porn-images-xxx)
default["hentaiimg"] = default["hentaicosplays"]
domains["hentaiimg"] = "https://hentai-img.com/"
default["pornimagesxxx"] = default["hentaicosplays"]
domains["pornimagesxxx"] = "https://porn-images-xxx.com/"
# add manga4life.com
default["mangalife"] = default["mangasee"]
domains["mangalife"] = "https://manga4life.com/"
# add wikifeetx.com
default["wikifeetx"] = default["wikifeet"]
domains["wikifeetx"] = "https://www.wikifeetx.com/"
2021-02-20 22:31:21 +01:00
return categories, domains
2019-02-20 19:25:41 +01:00
# define table columns
COLUMNS = (
("Site", 20,
2021-02-20 22:31:21 +01:00
lambda c, scs, d: category_text(c)),
2019-02-20 19:25:41 +01:00
("URL" , 35,
2021-02-20 22:31:21 +01:00
lambda c, scs, d: d),
2019-02-20 19:25:41 +01:00
("Capabilities", 50,
2021-02-20 22:31:21 +01:00
lambda c, scs, d: ", ".join(subcategory_text(c, sc) for sc in scs
if subcategory_text(c, sc))),
2019-02-20 19:25:41 +01:00
("Authentication", 16,
2021-02-20 22:31:21 +01:00
lambda c, scs, d: AUTH_MAP.get(c, "")),
2019-02-20 19:25:41 +01:00
)
2019-01-09 14:21:19 +01:00
def generate_output(columns, categories, domains):
thead = []
append = thead.append
append("<tr>")
for column in columns:
append(" <th>" + column[0] + "</th>")
append("</tr>")
tbody = []
append = tbody.append
for name, base in categories.items():
if name and base:
name = BASE_MAP.get(name) or (name.capitalize() + " Instances")
append('\n<tr>\n <td colspan="4"><strong>' +
name + '</strong></td>\n</tr>')
clist = base.items()
else:
clist = sorted(base.items(), key=category_key)
for category, subcategories in clist:
append("<tr>")
for column in columns:
domain = domains[category]
content = column[2](category, subcategories, domain)
append(" <td>" + content + "</td>")
append("</tr>")
TEMPLATE = """# Supported Sites
<!-- auto-generated by {} -->
Consider all sites to be NSFW unless otherwise known.
<table>
<thead valign="bottom">
{}
</thead>
<tbody valign="top">
{}
</tbody>
</table>
"""
return TEMPLATE.format(
"/".join(os.path.normpath(__file__).split(os.sep)[-2:]),
"\n".join(thead),
"\n".join(tbody),
)
2021-02-20 22:31:21 +01:00
categories, domains = build_extractor_list()
PATH = (sys.argv[1] if len(sys.argv) > 1 else
util.path("docs", "supportedsites.md"))
with util.lazy(PATH) as file:
file.write(generate_output(COLUMNS, categories, domains))