Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
mikf
GitHub Repository: mikf/gallery-dl
Path: blob/master/gallery_dl/extractor/booth.py
5399 views
1
# -*- coding: utf-8 -*-
2
3
# Copyright 2025 Mike Fährmann
4
#
5
# This program is free software; you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License version 2 as
7
# published by the Free Software Foundation.
8
9
"""Extractors for https://booth.pm/"""
10
11
from .common import Extractor, Message
12
from .. import text, util
13
14
15
class BoothExtractor(Extractor):
16
"""Base class for booth extractors"""
17
category = "booth"
18
root = "https://booth.pm"
19
directory_fmt = ("{category}", "{shop[name]}", "{id} {name}")
20
filename_fmt = "{num:>02} {filename}.{extension}"
21
archive_fmt = "{id}_{filename}"
22
request_interval = (0.5, 1.5)
23
24
def _init(self):
25
self.cookies.set("adult", "t", domain=".booth.pm")
26
27
def items(self):
28
for item in self.shop_items():
29
item["_extractor"] = BoothItemExtractor
30
yield Message.Queue, item["shop_item_url"], item
31
32
def _pagination(self, url):
33
while True:
34
page = self.request(url).text
35
36
for item in text.extract_iter(page, ' data-item="', '"'):
37
yield util.json_loads(text.unescape(item))
38
39
next = text.extr(page, 'rel="next" class="nav-item" href="', '"')
40
if not next:
41
break
42
url = self.root + next
43
44
45
class BoothItemExtractor(BoothExtractor):
46
subcategory = "item"
47
pattern = r"(?:https?://)?(?:[\w-]+\.)?booth\.pm/(?:\w\w/)?items/(\d+)"
48
example = "https://booth.pm/items/12345"
49
50
def items(self):
51
url = f"{self.root}/ja/items/{self.groups[0]}"
52
headers = {
53
"Accept": "application/json",
54
"Content-Type": "application/json",
55
"X-CSRF-Token": None,
56
"Sec-Fetch-Dest": "empty",
57
"Sec-Fetch-Mode": "cors",
58
"Sec-Fetch-Site": "same-origin",
59
"Priority": "u=4",
60
}
61
62
if self.config("strategy") == "fallback":
63
page = None
64
item = self.request_json(url + ".json", headers=headers)
65
else:
66
page = self.request(url).text
67
headers["X-CSRF-Token"] = text.extr(
68
page, 'name="csrf-token" content="', '"')
69
item = self.request_json(
70
url + ".json", headers=headers, interval=False)
71
72
item["booth_category"] = item.pop("category", None)
73
item["date"] = text.parse_datetime(
74
item["published_at"], "%Y-%m-%dT%H:%M:%S.%f%z")
75
item["tags"] = [t["name"] for t in item["tags"]]
76
77
shop = item["shop"]
78
shop["id"] = text.parse_int(shop["thumbnail_url"].rsplit("/", 3)[1])
79
80
if files := self._extract_files(item, page):
81
item["count"] = len(files)
82
shop["uuid"] = files[0]["url"].split("/", 4)[3]
83
else:
84
item["count"] = 0
85
shop["uuid"] = util.NONE
86
87
yield Message.Directory, item
88
for num, file in enumerate(files, 1):
89
url = file["url"]
90
file["num"] = num
91
text.nameext_from_url(url, file)
92
yield Message.Url, url, {**item, **file}
93
94
def _extract_files(self, item, page):
95
if page is None:
96
files = []
97
for image in item.pop("images"):
98
url = image["original"].replace("_base_resized", "")
99
files.append({
100
"url" : url,
101
"_fallback": _fallback(url),
102
})
103
return files
104
105
del item["images"]
106
return [{"url": url}
107
for url in text.extract_iter(page, 'data-origin="', '"')]
108
109
110
class BoothShopExtractor(BoothExtractor):
111
subcategory = "shop"
112
pattern = r"(?:https?://)?([\w-]+\.)booth\.pm/(?:\w\w/)?(?:items)?"
113
example = "https://SHOP.booth.pm/"
114
115
def __init__(self, match):
116
self.root = text.root_from_url(match[0])
117
BoothExtractor.__init__(self, match)
118
119
def shop_items(self):
120
return self._pagination(f"{self.root}/items")
121
122
123
def _fallback(url):
124
base = url[:-3]
125
yield base + "jpeg"
126
yield base + "png"
127
yield base + "webp"
128
129