Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
mikf
GitHub Repository: mikf/gallery-dl
Path: blob/master/gallery_dl/extractor/aryion.py
8920 views
1
# -*- coding: utf-8 -*-
2
3
# Copyright 2020-2026 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://aryion.com/"""
10
11
from .common import Extractor, Message
12
from .. import text, util, dt, exception
13
from ..cache import cache
14
from email.utils import parsedate_tz
15
16
BASE_PATTERN = r"(?:https?://)?(?:www\.)?aryion\.com/g4"
17
18
19
class AryionExtractor(Extractor):
20
"""Base class for aryion extractors"""
21
category = "aryion"
22
directory_fmt = ("{category}", "{user!l}", "{path:I}")
23
filename_fmt = "{id} {title}.{extension}"
24
archive_fmt = "{id}"
25
cookies_domain = ".aryion.com"
26
cookies_names = ("phpbb3_rl7a3_sid",)
27
root = "https://aryion.com"
28
29
def __init__(self, match):
30
Extractor.__init__(self, match)
31
self.user = match[1]
32
self.recursive = True
33
34
def login(self):
35
if self.cookies_check(self.cookies_names):
36
return
37
38
username, password = self._get_auth_info()
39
if username:
40
self.cookies_update(self._login_impl(username, password))
41
42
@cache(maxage=14*86400, keyarg=1)
43
def _login_impl(self, username, password):
44
self.log.info("Logging in as %s", username)
45
46
url = self.root + "/forum/ucp.php?mode=login"
47
data = {
48
"username": username,
49
"password": password,
50
"login": "Login",
51
}
52
53
response = self.request(url, method="POST", data=data)
54
if b"You have been successfully logged in." not in response.content:
55
raise exception.AuthenticationError()
56
return {c: response.cookies[c] for c in self.cookies_names}
57
58
def items(self):
59
self.login()
60
data = self.metadata()
61
62
for post_id in self.posts():
63
if post := self._parse_post(post_id):
64
if data:
65
post.update(data)
66
yield Message.Directory, "", post
67
yield Message.Url, post["url"], post
68
elif post is False and self.recursive:
69
base = self.root + "/g4/view/"
70
data = {"_extractor": AryionPostExtractor}
71
for post_id in self._pagination_params(base + post_id):
72
yield Message.Queue, base + post_id, data
73
74
def posts(self):
75
"""Yield relevant post IDs"""
76
77
def metadata(self):
78
"""Return general metadata"""
79
80
def _pagination_params(self, url, params=None, needle=None, quote="'"):
81
if params is None:
82
params = {"p": 1}
83
else:
84
params["p"] = text.parse_int(params.get("p"), 1)
85
86
if needle is None:
87
needle = "class='gallery-item' id=" + quote
88
89
while True:
90
page = self.request(url, params=params).text
91
92
cnt = 0
93
for post_id in text.extract_iter(page, needle, quote):
94
cnt += 1
95
yield post_id
96
97
if cnt < 40 and ">Next &gt;&gt;<" not in page:
98
return
99
params["p"] += 1
100
101
def _pagination_next(self, url):
102
while True:
103
page = self.request(url).text
104
yield from text.extract_iter(page, "thumb' href='/g4/view/", "'")
105
106
pos = page.find("Next &gt;&gt;")
107
if pos < 0:
108
return
109
url = self.root + text.rextr(page, "href='", "'", pos)
110
111
def _pagination_folders(self, url, folder=None, seen=None):
112
if folder is None:
113
self.kwdict["folder"] = ""
114
else:
115
url = f"{url}/{folder}"
116
self.kwdict["folder"] = folder = text.unquote(folder)
117
self.log.debug("Descending into folder '%s'", folder)
118
119
params = {"p": 1}
120
while True:
121
page = self.request(url, params=params).text
122
123
cnt = 0
124
for item in text.extract_iter(
125
page, "<li class='gallery-item", "</li>"):
126
cnt += 1
127
if text.extr(item, 'data-item-type="', '"') == "Folders":
128
folder = text.extr(item, "href='", "'").rpartition("/")[2]
129
if seen is None:
130
seen = set()
131
if folder not in seen:
132
seen.add(folder)
133
if self.recursive:
134
yield from self._pagination_folders(
135
url, folder, seen)
136
else:
137
self.log.debug("Skipping folder '%s'", folder)
138
else:
139
yield text.extr(item, "data-item-id='", "'")
140
141
if cnt < 40 and ">Next &gt;&gt;<" not in page:
142
break
143
params["p"] += 1
144
145
self.kwdict["folder"] = ""
146
147
def _parse_post(self, post_id):
148
url = f"{self.root}/g4/data.php?id={post_id}"
149
with self.request(url, method="HEAD", fatal=False) as response:
150
151
if response.status_code >= 400:
152
self.log.warning(
153
"Unable to fetch post %s ('%s %s')",
154
post_id, response.status_code, response.reason)
155
return None
156
headers = response.headers
157
158
# folder
159
if headers["content-type"] in {
160
"application/x-folder",
161
"application/x-comic-folder",
162
"application/x-comic-folder-nomerge",
163
}:
164
return False
165
166
# get filename from 'Content-Disposition' header
167
cdis = headers["content-disposition"]
168
fname, _, ext = text.extr(cdis, 'filename="', '"').rpartition(".")
169
if not fname:
170
fname, ext = ext, fname
171
172
# get file size from 'Content-Length' header
173
clen = headers.get("content-length")
174
175
# fix 'Last-Modified' header
176
lmod = headers["last-modified"]
177
if lmod[22] != ":":
178
lmod = f"{lmod[:22]}:{lmod[22:24]} GMT"
179
180
post_url = f"{self.root}/g4/view/{post_id}"
181
extr = text.extract_from(self.request(post_url).text)
182
183
title, _, artist = text.unescape(extr(
184
"<title>g4 :: ", "<")).rpartition(" by ")
185
186
return {
187
"id" : text.parse_int(post_id),
188
"url" : url,
189
"user" : self.user or artist,
190
"title" : title,
191
"artist": artist,
192
"description": text.unescape(extr(
193
'property="og:description" content="', '"')),
194
"path" : text.split_html(extr(
195
"cookiecrumb'>", '</span'))[4:-1:2],
196
"date" : dt.datetime(*parsedate_tz(lmod)[:6]),
197
"size" : text.parse_int(clen),
198
"views" : text.parse_int(extr("Views</b>:", "<").replace(",", "")),
199
"width" : text.parse_int(extr("Resolution</b>:", "x")),
200
"height": text.parse_int(extr("", "<")),
201
"comments" : text.parse_int(extr("Comments</b>:", "<")),
202
"favorites": text.parse_int(extr("Favorites</b>:", "<")),
203
"tags" : text.split_html(extr("class='taglist'>", "</span>")),
204
"filename" : fname,
205
"extension": ext,
206
"_http_lastmodified": lmod,
207
}
208
209
210
class AryionGalleryExtractor(AryionExtractor):
211
"""Extractor for a user's gallery on eka's portal"""
212
subcategory = "gallery"
213
categorytransfer = True
214
pattern = BASE_PATTERN + r"/(?:gallery/|user/|latest.php\?name=)([^/?#]+)"
215
example = "https://aryion.com/g4/gallery/USER"
216
217
def _init(self):
218
self.offset = 0
219
self.recursive = self.config("recursive", True)
220
221
def skip(self, num):
222
if self.recursive:
223
return 0
224
self.offset += num
225
return num
226
227
def posts(self):
228
if self.recursive:
229
url = f"{self.root}/g4/gallery/{self.user}"
230
return self._pagination_params(url)
231
else:
232
url = f"{self.root}/g4/latest.php?name={self.user}"
233
return util.advance(self._pagination_next(url), self.offset)
234
235
236
class AryionFavoriteExtractor(AryionExtractor):
237
"""Extractor for a user's favorites gallery"""
238
subcategory = "favorite"
239
directory_fmt = ("{category}", "{user!l}", "favorites", "{folder}")
240
archive_fmt = "f_{user}_{id}"
241
pattern = BASE_PATTERN + r"/favorites/([^/?#]+)(?:/([^?#]+))?"
242
example = "https://aryion.com/g4/favorites/USER"
243
244
def _init(self):
245
self.recursive = self.config("recursive", True)
246
247
def posts(self):
248
url = f"{self.root}/g4/favorites/{self.user}"
249
return self._pagination_folders(url, self.groups[1])
250
251
252
class AryionWatchExtractor(AryionExtractor):
253
"""Extractor for your watched users and tags"""
254
subcategory = "watch"
255
directory_fmt = ("{category}", "{user!l}",)
256
pattern = BASE_PATTERN + r"/messagepage\.php()"
257
example = "https://aryion.com/g4/messagepage.php"
258
259
def posts(self):
260
if not self.cookies_check(self.cookies_names):
261
raise exception.AuthRequired(
262
("username & password", "authenticated cookies"),
263
"watched Submissions")
264
self.cookies.set("g4p_msgpage_style", "plain", domain="aryion.com")
265
url = self.root + "/g4/messagepage.php"
266
return self._pagination_params(url, None, 'data-item-id="', '"')
267
268
269
class AryionTagExtractor(AryionExtractor):
270
"""Extractor for tag searches on eka's portal"""
271
subcategory = "tag"
272
directory_fmt = ("{category}", "tags", "{search_tags}")
273
archive_fmt = "t_{search_tags}_{id}"
274
pattern = BASE_PATTERN + r"/tags\.php\?([^#]+)"
275
example = "https://aryion.com/g4/tags.php?tag=TAG"
276
277
def _init(self):
278
self.params = text.parse_query(self.user)
279
self.user = None
280
281
def metadata(self):
282
return {"search_tags": self.params.get("tag")}
283
284
def posts(self):
285
url = self.root + "/g4/tags.php"
286
return self._pagination_params(url, self.params)
287
288
289
class AryionSearchExtractor(AryionExtractor):
290
"""Extractor for searches on eka's portal"""
291
subcategory = "search"
292
directory_fmt = ("{category}", "searches", "{search[prefix]}"
293
"{search[q]|search[tags]|search[user]}")
294
archive_fmt = ("s_{search[prefix]}"
295
"{search[q]|search[tags]|search[user]}_{id}")
296
pattern = BASE_PATTERN + r"/search\.php\?([^#]+)"
297
example = "https://aryion.com/g4/search.php?q=TEXT&tags=TAGS&user=USER"
298
299
def metadata(self):
300
params = text.parse_query(self.user)
301
return {"search": {
302
**params,
303
"prefix": ("" if params.get("q") else
304
"t_" if params.get("tags") else
305
"u_" if params.get("user") else ""),
306
}}
307
308
def posts(self):
309
url = f"{self.root}/g4/search.php?{self.user}"
310
return self._pagination_next(url)
311
312
313
class AryionPostExtractor(AryionExtractor):
314
"""Extractor for individual posts on eka's portal"""
315
subcategory = "post"
316
pattern = BASE_PATTERN + r"/view/(\d+)"
317
example = "https://aryion.com/g4/view/12345"
318
319
def posts(self):
320
post_id, self.user = self.user, None
321
return (post_id,)
322
323