Skip to content

Commit b1a708b

Browse files
committed
1 parent d491ed8 commit b1a708b

4 files changed

Lines changed: 137 additions & 14 deletions

File tree

docs/configuration.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6986,6 +6986,16 @@ Description
69866986
Download video files.
69876987

69886988

6989+
extractor.weibo.album.subalbums
6990+
-------------------------------
6991+
Type
6992+
``bool``
6993+
Default
6994+
``false``
6995+
Description
6996+
Extract subalbum media.
6997+
6998+
69896999
extractor.wikimedia.format
69907000
--------------------------
69917001
Type

docs/gallery-dl.conf

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -972,7 +972,11 @@
972972
"movies" : false,
973973
"retweets" : false,
974974
"text" : false,
975-
"videos" : true
975+
"videos" : true,
976+
977+
"album": {
978+
"subalbums": false
979+
}
976980
},
977981
"xfolio":
978982
{

gallery_dl/extractor/weibo.py

Lines changed: 93 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22

3-
# Copyright 2019-2025 Mike Fährmann
3+
# Copyright 2019-2026 Mike Fährmann
44
#
55
# This program is free software; you can redistribute it and/or modify
66
# it under the terms of the GNU General Public License version 2 as
@@ -29,7 +29,8 @@ class WeiboExtractor(Extractor):
2929

3030
def __init__(self, match):
3131
Extractor.__init__(self, match)
32-
self._prefix, self.user = match.groups()
32+
self._prefix = match[1]
33+
self.user = match[2]
3334

3435
def _init(self):
3536
self.livephoto = self.config("livephoto", True)
@@ -199,26 +200,30 @@ def _status_by_id(self, status_id):
199200
f"?id={status_id}&isGetLongText=true")
200201
return self.request_json(url)
201202

203+
def _user(self, user):
204+
url = (f"{self.root}/ajax/profile/info?"
205+
f"{'screen_name' if self._prefix == 'n' else 'custom'}={user}")
206+
return self.request_json(url, interval=False)["data"]["user"]
207+
202208
def _user_id(self):
203-
if len(self.user) >= 10 and self.user.isdecimal():
204-
return self.user[-10:]
209+
user = self.user
210+
if len(user) >= 10 and user.isdecimal():
211+
return user[-10:]
205212
else:
206-
url = (f"{self.root}/ajax/profile/info?"
207-
f"{'screen_name' if self._prefix == 'n' else 'custom'}="
208-
f"{self.user}")
209-
return self.request_json(url)["data"]["user"]["idstr"]
213+
return self._user(user)["idstr"]
210214

211-
def _pagination(self, endpoint, params):
215+
def _pagination(self, endpoint, params,
216+
since_key="sinceid", subalbums=None):
212217
url = f"{self.root}/ajax{endpoint}"
213218
headers = {
219+
"Accept": "application/json, text/plain, */*",
214220
"X-Requested-With": "XMLHttpRequest",
215221
"X-XSRF-TOKEN": None,
216222
"Referer": f"{self.root}/u/{params['uid']}",
217223
}
218224

219225
while True:
220226
response = self.request(url, params=params, headers=headers)
221-
headers["Accept"] = "application/json, text/plain, */*"
222227
headers["X-XSRF-TOKEN"] = response.cookies.get("XSRF-TOKEN")
223228

224229
data = response.json()
@@ -234,6 +239,10 @@ def _pagination(self, endpoint, params):
234239
except KeyError:
235240
return
236241

242+
if subalbums is not None:
243+
subalbums = None
244+
yield data.get("album_list") or ()
245+
237246
yield from statuses
238247

239248
# videos, newvideo
@@ -244,8 +253,10 @@ def _pagination(self, endpoint, params):
244253
continue
245254

246255
# album
247-
if since_id := data.get("since_id"):
248-
params["sinceid"] = since_id
256+
if "since_id" in data:
257+
params[since_key] = since_id = data["since_id"]
258+
if not since_id:
259+
return
249260
if "page" in params:
250261
params["page"] += 1
251262
continue
@@ -383,9 +394,33 @@ def statuses(self):
383394
class WeiboAlbumExtractor(WeiboExtractor):
384395
"""Extractor for weibo 'album' listings"""
385396
subcategory = "album"
386-
pattern = USER_PATTERN + r"\?tabtype=album"
397+
pattern = USER_PATTERN + r"\?tabtype=album(?:[:_-]([^&#]+))?"
387398
example = "https://weibo.com/USER?tabtype=album"
388399

400+
def items(self):
401+
subalbum = self.groups[2]
402+
403+
if not subalbum and not self.config("subalbums", False):
404+
return WeiboExtractor.items(self)
405+
406+
self.directory_fmt = ("{category}", "{user[screen_name]}",
407+
"Album", "{subalbum[pic_title]|''}")
408+
self.filename_fmt = "{filename}.{extension}"
409+
self.archive_fmt = "{subalbum[pic_title]}_{pid}"
410+
return self.items_subalbum(subalbum)
411+
412+
def items_subalbum(self, subalbum):
413+
user = self.kwdict["user"] = self._user(self.user)
414+
base = self.root + "/ajax/common/download?pid="
415+
416+
for data, files in self.albums(user["idstr"], subalbum):
417+
self.kwdict["subalbum"] = data
418+
yield Message.Directory, "", {}
419+
for file in files:
420+
file["filename"] = file["pid"]
421+
file["extension"] = "jpg"
422+
yield Message.Url, base + file["pid"], file
423+
389424
def statuses(self):
390425
endpoint = "/profile/getImageWall"
391426
params = {"uid": self._user_id()}
@@ -401,6 +436,51 @@ def statuses(self):
401436
else:
402437
yield status
403438

439+
def albums(self, uid, subalbum):
440+
endpoint = "/profile/getImageWall"
441+
params = {
442+
"uid" : uid,
443+
"sinceid" : "0",
444+
"has_album": "true",
445+
}
446+
album = self._pagination(endpoint, params, subalbums=True)
447+
subalbums = next(album, ())
448+
449+
if not subalbum or subalbum == "0":
450+
return (({}, album),)
451+
452+
if subalbum == "all":
453+
results = [
454+
(sub, self._pagination_subalbum(uid, sub))
455+
for sub in subalbums
456+
]
457+
results.append(({}, album))
458+
return results
459+
460+
if subalbum == "only":
461+
return [
462+
(sub, self._pagination_subalbum(uid, sub))
463+
for sub in subalbums
464+
]
465+
466+
if subalbum.isdecimal():
467+
try:
468+
sub = subalbums[int(subalbum)-1]
469+
except Exception:
470+
raise exception.NotFoundError("subalbum")
471+
else:
472+
subalbum = text.unquote(subalbum)
473+
for sub in subalbums:
474+
if sub["pic_title"] == subalbum:
475+
break
476+
else:
477+
raise exception.NotFoundError("subalbum")
478+
return ((sub, self._pagination_subalbum(uid, sub)),)
479+
480+
def _pagination_subalbum(self, uid, sub):
481+
params = {"uid": uid, "containerid": text.unquote(sub["containerid"])}
482+
return self._pagination("/profile/getAlbumDetail", params, "since_id")
483+
404484

405485
class WeiboStatusExtractor(WeiboExtractor):
406486
"""Extractor for a weibo status"""

test/results/weibo.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"https://weibo.com/u/1758989602?tabtype=feed",
2727
"https://weibo.com/u/1758989602?tabtype=video",
2828
"https://weibo.com/u/1758989602?tabtype=newVideo",
29+
"https://weibo.com/u/1758989602?tabtype=article",
2930
"https://weibo.com/u/1758989602?tabtype=album",
3031
),
3132
},
@@ -160,6 +161,34 @@
160161
"#count" : 3,
161162
},
162163

164+
{
165+
"#url" : "https://weibo.com/u/2142058927?tabtype=album-头像",
166+
"#comment" : "subalbum",
167+
"#class" : weibo.WeiboAlbumExtractor,
168+
"#range" : "1-3",
169+
"#results" : (
170+
"https://weibo.com/ajax/common/download?pid=002kXRnxly8i5b4anvvxbj60u00u078w02",
171+
"https://weibo.com/ajax/common/download?pid=002kXRnxly8i2b7u68bfhj60u00u0dl002",
172+
"https://weibo.com/ajax/common/download?pid=002kXRnxly8i2b6rmr1trj60rs0rstdn02",
173+
),
174+
175+
"extension": "jpg",
176+
"pid" : str,
177+
"type" : "pic",
178+
"subalbum" : {
179+
"containerid": "2318262142058927_-_pc_profile_album_-_photo_-_avatar_-_35046512_-_%E5%A4%B4%E5%83%8F",
180+
"pic" : "https://wx1.sinaimg.cn/webp720/002kXRnxly8i5b4anvvxbj60u00u078w02.jpg",
181+
"pic_title" : "头像",
182+
},
183+
"user" : {
184+
"id" : 2142058927,
185+
"idstr" : "2142058927",
186+
"location" : "上海 黄浦区",
187+
"profile_url": "/u/2142058927",
188+
"screen_name": "吴磊LEO",
189+
},
190+
},
191+
163192
{
164193
"#url" : "https://m.weibo.cn/detail/4323047042991618",
165194
"#category": ("", "weibo", "status"),

0 commit comments

Comments
 (0)