diff --git a/docs/configuration.rst b/docs/configuration.rst
index 1f6ad0c15dd..f9c787a19e9 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -3609,6 +3609,55 @@ Description
Download video files.
+extractor.weverse.access-token
+------------------------------
+Type
+ ``string``
+Default
+ ``null``
+Description
+ Your Weverse account access token.
+
+ The token can be found in the ``we2_access_token`` cookie in the
+ ``.weverse.io`` cookie domain after logging in to your account.
+
+ An invalid or not up-to-date value will result in
+ ``401 Unauthorized`` errors.
+
+ If this option is unset, and the cookie is not used, an extra HTTP
+ request will be sent with your ``username`` and ``password`` to
+ attempt to fetch a new token.
+
+
+extractor.weverse.embeds
+-----------------------------
+Type
+ ``bool``
+Default
+ ``false``
+Description
+ Control behavior on embedded content from external sites.
+
+ * ``true``: Extract embed URLs and download them if supported.
+ * ``false``: Ignore embeds.
+
+
+extractor.weverse.videos
+------------------------
+Type
+ * ``bool``
+ * ``string``
+Default
+ ``true``
+Description
+ Control video download behavior.
+
+ * ``true``: Download videos.
+ * ``"ytdl"``: Download videos using `youtube-dl`_. Requires ``yt-dlp``.
+ Only supports ``Moment`` and ``Media`` posts.
+ * ``false``: Skip videos.
+
+
extractor.ytdl.enabled
----------------------
Type
diff --git a/docs/supportedsites.md b/docs/supportedsites.md
index a15566df981..3c70d58a0c8 100644
--- a/docs/supportedsites.md
+++ b/docs/supportedsites.md
@@ -949,6 +949,12 @@ Consider all sites to be NSFW unless otherwise known.
Albums, Articles, Feeds, Images from Statuses, User Profiles, Videos |
|
+
+ Weverse |
+ https://weverse.io/ |
+ Feeds, Media Files, Media Categories, Moments, Posts, User Profiles |
+ Supported |
+
WikiArt.org |
https://www.wikiart.org/ |
diff --git a/gallery_dl/extractor/__init__.py b/gallery_dl/extractor/__init__.py
index 22e4fe34123..08b56155220 100644
--- a/gallery_dl/extractor/__init__.py
+++ b/gallery_dl/extractor/__init__.py
@@ -168,6 +168,7 @@
"webmshare",
"webtoons",
"weibo",
+ "weverse",
"wikiart",
"wikifeet",
"xhamster",
diff --git a/gallery_dl/extractor/weverse.py b/gallery_dl/extractor/weverse.py
new file mode 100644
index 00000000000..a34d0cd134a
--- /dev/null
+++ b/gallery_dl/extractor/weverse.py
@@ -0,0 +1,568 @@
+# -*- 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.
+
+"""Extractors for https://weverse.io/"""
+
+from .common import Extractor, Message
+from .. import text, exception
+from ..cache import cache
+import binascii
+import hashlib
+import hmac
+import time
+import urllib.parse
+import uuid
+from collections import OrderedDict
+
+BASE_PATTERN = r"(?:https?://)?(?:m\.)?weverse\.io"
+COMMUNITY_PATTERN = BASE_PATTERN + r"/(\w+)"
+
+MEMBER_ID_PATTERN = r"/([a-f0-9]+)"
+POST_ID_PATTERN = r"/(\d-\d+)"
+
+
+class WeverseExtractor(Extractor):
+ """Base class for weverse extractors"""
+ category = "weverse"
+ cookies_domain = ".weverse.io"
+ cookies_names = ("we2_access_token",)
+ root = "https://weverse.io"
+ filename_fmt = "weverse_{fileId}.{extension}"
+ archive_fmt = "weverse_{fileId}"
+ request_interval = (1.0, 2.0)
+
+ def _init(self):
+ self.embeds = self.config("embeds", True)
+ self.videos = self.config("videos", True)
+
+ self.login()
+ self.api = WeverseAPI(self, self.access_token)
+
+ @cache(maxage=365*24*3600, keyarg=1)
+ def _login_impl(self, username, password):
+ url = ("https://accountapi.weverse.io"
+ "/web/api/v2/auth/token/by-credentials")
+ data = {"email": username, "password": password}
+ headers = {
+ "x-acc-app-secret" : "5419526f1c624b38b10787e5c10b2a7a",
+ "x-acc-app-version": "2.3.0-alpha.0",
+ "x-acc-language" : "en",
+ "x-acc-service-id" : "weverse",
+ "x-acc-trace-id" : str(uuid.uuid4()),
+ }
+ self.log.info("Logging in as %s", username)
+ res = self.request(
+ url, method="POST", json=data, headers=headers).json()
+ if "accessToken" not in res:
+ self.log.warning(
+ "Unable to log in as %s, proceeding without auth", username)
+ return self.cookies
+
+ def login(self):
+ if self.config("access-token"):
+ self.access_token = self.config("access-token")
+ return
+
+ if not self.cookies_check(self.cookies_names):
+ username, password = self._get_auth_info()
+ if username:
+ self.cookies_update(
+ self._login_impl(username, password), self.cookies_domain)
+
+ self.access_token = self.cookies.get(self.cookies_names[0], None)
+
+ def metadata(self, data):
+ delete = ("attachment", "authorMomentPosts",
+ "body", "extension", "plainBody")
+
+ if "publishedAt" in data:
+ data["date"] = text.parse_timestamp(data["publishedAt"] / 1000)
+
+ if "expireAt" in data:
+ data["expireAt"] = text.parse_timestamp(data["expireAt"] / 1000)
+
+ for key in delete:
+ if key in data:
+ del data[key]
+
+ def has_media(self, data):
+ for key in ("attachment", "extension"):
+ if key in data and isinstance(data[key], dict):
+ return self.has_media(data[key])
+ for key in ("image", "photo", "video"):
+ if key in data and len(data[key]):
+ return True
+ return False
+
+ def download_photos(self, photos, data):
+ for photo in photos:
+ data.update(photo)
+ data["fileId"] = data["photoId"]
+ url = data["url"]
+ data["extension"] = text.ext_from_url(url)
+ yield Message.Url, url, data
+
+ def download_videos(self, videos, data):
+ for video in videos:
+ video_id = video["videoId"]
+ if isinstance(self, WeverseMediaExtractor):
+ master_id = (video.get("uploadInfo", {}).get(
+ "videoId") or video["infraVideoId"])
+ best_video = self.api.video_media(video_id, master_id)
+ else:
+ best_video = self.api.video(video_id)
+ data.update({
+ "fileId" : video_id,
+ "width" : best_video["encodingOption"]["width"],
+ "height" : best_video["encodingOption"]["height"],
+ "url" : ("ytdl:" + video["shareUrl"]
+ if self.videos == "ytdl"
+ else best_video["source"]),
+ })
+ url = data["url"]
+ data["extension"] = text.ext_from_url(url)
+ yield Message.Url, url, data
+
+ def download_embeds(self, embeds, data):
+ for embed in embeds:
+ data["fileId"] = embed["videoId"]
+ url = embed["videoPath"]
+ data["extension"] = None
+ yield Message.Url, "ytdl:" + url, data
+
+ def download(self, downloads, data):
+ for download_type, download_data in downloads.items():
+ if download_type == "embeds":
+ if not self.embeds or not self.videos:
+ continue
+ yield from self.download_embeds(download_data, data)
+ if download_type == "photos":
+ yield from self.download_photos(download_data, data)
+ if download_type == "videos":
+ if not self.videos:
+ continue
+ yield from self.download_videos(download_data, data)
+
+
+class WeversePostExtractor(WeverseExtractor):
+ """Extractor for weverse posts"""
+ subcategory = "post"
+ directory_fmt = ("{category}", "{community[communityName]}",
+ "{author[memberId]}", "{postId}")
+ pattern = (COMMUNITY_PATTERN +
+ r"/(?:artist|fanpost)" + POST_ID_PATTERN)
+ example = "https://weverse.io/abcdef/artist/1-123456789"
+
+ def __init__(self, match):
+ WeverseExtractor.__init__(self, match)
+ self.post_id = match.group(2)
+
+ def items(self):
+ data = self.api.post(self.post_id)
+
+ attachments = data["attachment"]
+
+ # skip posts with no media
+ if not self.has_media(attachments):
+ self.log.debug("Skipping %s (no media)", self.url)
+ return
+
+ self.metadata(data)
+
+ yield Message.Directory, data
+ downloads = {}
+ if "photo" in attachments:
+ downloads["photos"] = [
+ {
+ "photoId": photo["photoId"],
+ "width" : photo["width"],
+ "height" : photo["height"],
+ "url" : photo["url"],
+ }
+ for photo in attachments["photo"].values()
+ ]
+ if "video" in attachments:
+ downloads["videos"] = [
+ {
+ "videoId" : video["videoId"],
+ "uploadInfo": video["uploadInfo"],
+ }
+ for video in attachments["video"].values()
+ ]
+ yield from self.download(downloads, data)
+
+
+class WeverseUserExtractor(WeverseExtractor):
+ """Extractor for weverse community users"""
+ subcategory = "user"
+ pattern = COMMUNITY_PATTERN + "/profile" + MEMBER_ID_PATTERN
+ example = ("https://weverse.io/abcdef"
+ "/profile/a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5")
+
+ def __init__(self, match):
+ WeverseExtractor.__init__(self, match)
+ self.member_id = match.group(2)
+
+ def items(self):
+ if not self.access_token:
+ raise exception.AuthenticationError(
+ "User extraction requires an account")
+ data = {"_extractor": WeversePostExtractor}
+ posts = self.api.profile(self.member_id)
+ for post in posts:
+ yield Message.Queue, post["shareUrl"], data
+
+
+class WeverseFeedExtractor(WeverseExtractor):
+ """Extractor for feeds in a weverse community"""
+ subcategory = "feed"
+ pattern = COMMUNITY_PATTERN + r"/(feed|artist)$"
+ example = "https://weverse.io/abcdef/feed"
+
+ def __init__(self, match):
+ WeverseExtractor.__init__(self, match)
+ self.community_keyword = match.group(1)
+ self.feed_name = match.group(2)
+
+ def items(self):
+ if not self.access_token:
+ raise exception.AuthenticationError(
+ "Feed extraction requires an account")
+ data = {"_extractor": WeversePostExtractor}
+ posts = self.api.feed(self.community_keyword, self.feed_name)
+ for post in posts:
+ yield Message.Queue, post["shareUrl"], data
+
+
+class WeverseMomentExtractor(WeverseExtractor):
+ """Extractor for moments from a weverse community artist"""
+ subcategory = "moment"
+ directory_fmt = ("{category}", "{community[communityName]}",
+ "{author[memberId]}", "{postId}")
+ pattern = (COMMUNITY_PATTERN +
+ "/moment" + MEMBER_ID_PATTERN +
+ "/post" + POST_ID_PATTERN)
+ example = ("https://weverse.io/abcdef"
+ "/moment/a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5"
+ "/post/1-123456789")
+
+ def __init__(self, match):
+ WeverseExtractor.__init__(self, match)
+ self.post_id = match.group(3)
+
+ def items(self):
+ data = self.api.post(self.post_id)
+
+ if "moment" in data["extension"]:
+ moment = data["extension"]["moment"]
+ elif "momentW1" in data["extension"]:
+ moment = data["extension"]["momentW1"]
+
+ # skip moments with no media
+ if not self.has_media(moment):
+ self.log.debug("Skipping %s (no media)", self.url)
+ return
+
+ data["expireAt"] = moment["expireAt"]
+
+ self.metadata(data)
+
+ yield Message.Directory, data
+ downloads = {}
+ if "photo" in moment:
+ downloads["photos"] = [
+ {
+ "photoId": moment["photo"]["photoId"],
+ "width" : moment["photo"]["width"],
+ "height" : moment["photo"]["height"],
+ "url" : moment["photo"]["url"],
+ }
+ ]
+ if "video" in moment:
+ downloads["videos"] = [
+ {
+ "videoId" : moment["video"]["videoId"],
+ "uploadInfo": moment["video"]["uploadInfo"],
+ }
+ ]
+ yield from self.download(downloads, data)
+
+
+class WeverseMomentsExtractor(WeverseExtractor):
+ """Extractor for all moments from a weverse community artist"""
+ subcategory = "moments"
+ pattern = COMMUNITY_PATTERN + "/moment" + MEMBER_ID_PATTERN + "$"
+ example = ("https://weverse.io/abcdef"
+ "/moment/a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5")
+
+ def __init__(self, match):
+ WeverseExtractor.__init__(self, match)
+ self.member_id = match.group(2)
+
+ def items(self):
+ if not self.access_token:
+ raise exception.AuthenticationError(
+ "User Moments extraction requires an account")
+ data = {"_extractor": WeverseMomentExtractor}
+ moments = self.api.moments(self.member_id)
+ for moment in moments:
+ yield Message.Queue, moment["shareUrl"], data
+
+
+class WeverseMediaExtractor(WeverseExtractor):
+ """Extractor for weverse media"""
+ subcategory = "media"
+ directory_fmt = ("{category}", "{community[communityName]}",
+ "media", "{postId}")
+ pattern = COMMUNITY_PATTERN + "/media" + POST_ID_PATTERN
+ example = "https://weverse.io/abcdef/media/1-123456789"
+
+ def __init__(self, match):
+ WeverseExtractor.__init__(self, match)
+ self.post_id = match.group(2)
+
+ def items(self):
+ data = self.api.post(self.post_id)
+
+ extensions = data["extension"]
+ self.metadata(data)
+
+ yield Message.Directory, data
+ data["media"] = {
+ "categories": extensions["mediaInfo"]["categories"]
+ }
+ downloads = {}
+ if "image" in extensions:
+ downloads["photos"] = [
+ {
+ "photoId": photo["photoId"],
+ "width" : photo["width"],
+ "height" : photo["height"],
+ "url" : photo["url"],
+ }
+ for photo in extensions["image"]["photos"]
+ ]
+ if "video" in extensions:
+ downloads["videos"] = [
+ {
+ "videoId" : extensions["video"]["videoId"],
+ "infraVideoId": extensions["video"]["infraVideoId"],
+ }
+ ]
+ if "youtube" in extensions:
+ downloads["embeds"] = [
+ {
+ "videoId" : extensions["youtube"]["youtubeVideoId"],
+ "videoPath": extensions["youtube"]["videoPath"],
+ }
+ ]
+ yield from self.download(downloads, data)
+
+
+class WeverseMediaTabExtractor(WeverseExtractor):
+ """Extractor for the media tab of a weverse commnity"""
+ subcategory = "media-tab"
+ pattern = COMMUNITY_PATTERN + r"/media(?:/(?:all|new))?$"
+ example = "https://weverse.io/abcdef/media"
+
+ def __init__(self, match):
+ WeverseExtractor.__init__(self, match)
+ self.community_keyword = match.group(1)
+
+ def items(self):
+ if not self.access_token:
+ raise exception.AuthenticationError(
+ "Media Tab extraction requires an account")
+ data = {"_extractor": WeverseMediaExtractor}
+ medias = self.api.media_tab(self.community_keyword)
+ for media in medias:
+ yield Message.Queue, media["shareUrl"], data
+
+
+class WeverseMediaCategoryExtractor(WeverseExtractor):
+ """Extractor for media by category of a weverse commnity"""
+ subcategory = "media-category"
+ pattern = COMMUNITY_PATTERN + r"/media/category/(\d+)"
+ example = "https://weverse.io/abcdef/media/category/1234"
+
+ def __init__(self, match):
+ WeverseExtractor.__init__(self, match)
+ self.community_keyword = match.group(1)
+ self.media_category = match.group(2)
+
+ def items(self):
+ if not self.access_token:
+ raise exception.AuthenticationError(
+ "Media category extraction requires an account")
+ data = {"_extractor": WeverseMediaExtractor}
+ medias = self.api.media_category(self.media_category)
+ for media in medias:
+ yield Message.Queue, media["shareUrl"], data
+
+
+class WeverseAPI():
+ """Interface for the Weverse API"""
+ BASE_API_URL = "https://global.apis.naver.com"
+ WMD_API_URL = BASE_API_URL + "/weverse/wevweb"
+ VOD_API_URL = BASE_API_URL + "/rmcnmv/rmcnmv"
+
+ def __init__(self, extractor, access_token=None):
+ self.extractor = extractor
+ self.access_token = access_token
+ self.headers = ({"Authorization": "Bearer " + access_token}
+ if access_token else None)
+
+ def _endpoint_with_params(self, endpoint, params):
+ params_delimiter = "?"
+ if "?" in endpoint:
+ params_delimiter = "&"
+ return endpoint + params_delimiter + urllib.parse.urlencode(
+ query=params)
+
+ def _message_digest(self, endpoint, params, timestamp):
+ key = "1b9cb6378d959b45714bec49971ade22e6e24e42".encode()
+ url = self._endpoint_with_params(endpoint, params)
+ message = "{}{}".format(url[:255], timestamp).encode()
+ hash = hmac.new(key, message, hashlib.sha1).digest()
+ return binascii.b2a_base64(hash).rstrip().decode()
+
+ def _apply_no_auth(self, endpoint, params):
+ if not endpoint.endswith("/preview"):
+ endpoint += "/preview"
+ params.update({"fieldSet": "postForPreview"})
+ return endpoint, params
+
+ def _in_key(self, video_id):
+ endpoint = "/video/v1.1/vod/{}/inKey".format(video_id)
+ return self._call_wmd(endpoint, method="POST")["inKey"]
+
+ def _best_video(self, videos):
+ return max(videos, key=lambda video:
+ video["encodingOption"]["width"] *
+ video["encodingOption"]["height"])
+
+ def _call(self, url, **kwargs):
+ while True:
+ try:
+ return self.extractor.request(url, **kwargs).json()
+ except exception.HttpError as exc:
+ if exc.response.status_code == 401:
+ raise exception.AuthenticationError()
+ if exc.response.status_code == 403:
+ raise exception.AuthorizationError(
+ "Post requires membership")
+ if exc.response.status_code == 404:
+ raise exception.NotFoundError(self.extractor.subcategory)
+ self.extractor.log.debug(exc)
+ return
+
+ def _call_wmd(self, endpoint, params=None, **kwargs):
+ if params is None:
+ params = {}
+ params.update({
+ "appId" : "be4d79eb8fc7bd008ee82c8ec4ff6fd4",
+ "language": "en",
+ "os" : "WEB",
+ "platform": "WEB",
+ "wpf" : "pc",
+ })
+ # the param order is important for the message digest
+ params = OrderedDict(sorted(params.items()))
+ timestamp = int(time.time() * 1000)
+ message_digest = self._message_digest(endpoint, params, timestamp)
+ params.update({
+ "wmsgpad": timestamp,
+ "wmd" : message_digest,
+ })
+ return self._call(self.WMD_API_URL + endpoint, params=params,
+ headers=self.headers, **kwargs)
+
+ def _pagination(self, endpoint, params=None):
+ if not self.access_token:
+ raise exception.AuthenticationError()
+ if params is None:
+ params = {}
+ while True:
+ res = self._call_wmd(endpoint, params)
+ yield from res["data"]
+ if "nextParams" not in res["paging"]:
+ return
+ params["after"] = res["paging"]["nextParams"]["after"]
+
+ def community_id(self, community_keyword):
+ endpoint = "/community/v1.0/communityIdUrlPathByUrlPathArtistCode"
+ params = {"keyword": community_keyword}
+ return self._call_wmd(endpoint, params)["communityId"]
+
+ def post(self, post_id):
+ endpoint = "/post/v1.0/post-{}".format(post_id)
+ params = {"fieldSet": "postV1"}
+ if not self.access_token:
+ endpoint, params = self._apply_no_auth(endpoint, params)
+ return self._call_wmd(endpoint, params)
+
+ def video_media(self, video_id, master_id):
+ in_key = self._in_key(video_id)
+ url = self.VOD_API_URL + "/vod/play/v2.0/{}".format(master_id)
+ params = {"key": in_key}
+ res = self._call(url, params=params)
+ videos = res["videos"]["list"]
+ return self._best_video(videos)
+
+ def video(self, video_id):
+ endpoint = "/cvideo/v1.0/cvideo-{}/playInfo".format(video_id)
+ params = {"videoId": video_id}
+ res = self._call_wmd(endpoint, params=params)
+ videos = res["playInfo"]["videos"]["list"]
+ return self._best_video(videos)
+
+ def profile(self, member_id):
+ endpoint = "/post/v1.0/member-{}/posts".format(member_id)
+ params = {
+ "fieldSet" : "postsV1",
+ "filterType": "DEFAULT",
+ "limit" : 20,
+ "sortType" : "LATEST",
+ }
+ yield from self._pagination(endpoint, params)
+
+ def feed(self, community_keyword, feed_name):
+ community_id = self.community_id(community_keyword)
+ endpoint = "/post/v1.0/community-{}/{}TabPosts".format(
+ community_id, feed_name)
+ params = {
+ "fieldSet" : "postsV1",
+ "limit" : 20,
+ "pagingType": "CURSOR",
+ }
+ yield from self._pagination(endpoint, params)
+
+ def media_tab(self, community_keyword):
+ community_id = self.community_id(community_keyword)
+ endpoint = "/media/v1.0/community-{}/searchAllMedia".format(
+ community_id)
+ params = {
+ "fieldSet" : "postsV1",
+ "sortOrder": "DESC",
+ }
+ yield from self._pagination(endpoint, params)
+
+ def media_category(self, category_id):
+ endpoint = "/media/v1.0/category-{}/mediaPosts".format(category_id)
+ params = {
+ "fieldSet" : "postsV1",
+ "sortOrder": "DESC",
+ }
+ yield from self._pagination(endpoint, params)
+
+ def moments(self, member_id):
+ endpoint = "/post/v1.0/member-{}/posts".format(member_id)
+ params = {
+ "fieldSet" : "postsV1",
+ "filterType": "MOMENT",
+ "limit" : 1,
+ }
+ yield from self._pagination(endpoint, params)
diff --git a/scripts/supportedsites.py b/scripts/supportedsites.py
index 470b629d6d4..56fe3917977 100755
--- a/scripts/supportedsites.py
+++ b/scripts/supportedsites.py
@@ -132,6 +132,7 @@
"wallpapercave" : "Wallpaper Cave",
"webmshare" : "webmshare",
"webtoons" : "Webtoon",
+ "weverse" : "Weverse",
"wikiart" : "WikiArt.org",
"xbunkr" : "xBunkr",
"xhamster" : "xHamster",
@@ -272,6 +273,11 @@
"home": "",
"newvideo": "",
},
+ "weverse": {
+ "moments": "",
+ "media-tab": "",
+ "media-category": "Media Categories",
+ },
"wikiart": {
"artists": "Artist Listings",
},
@@ -348,6 +354,7 @@
"vipergirls" : "Supported",
"wallhaven" : _APIKEY_WH,
"weasyl" : _APIKEY_WY,
+ "weverse" : "Supported",
"zerochan" : "Supported",
}
diff --git a/test/results/weverse.py b/test/results/weverse.py
new file mode 100644
index 00000000000..e1e6f3db107
--- /dev/null
+++ b/test/results/weverse.py
@@ -0,0 +1,141 @@
+# -*- 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.
+
+from gallery_dl.extractor import weverse
+
+IMAGE_URL_PATTERN = r"https://phinf\.wevpstatic\.net/.+\.(?:gif|jpe?g|png|webp)$"
+VIDEO_URL_PATTERN = r"https://weverse-rmcnmv\.akamaized\.net/.+\.(?:mp4|webm)(?:\?.+)?$"
+COMBINED_URL_PATTERN = "(?i)" + IMAGE_URL_PATTERN + "|" + VIDEO_URL_PATTERN
+
+__tests__ = (
+{
+ "#url" : "https://weverse.io/dreamcatcher/artist/3-138146100",
+ "#category": ("", "weverse", "post"),
+ "#class" : weverse.WeversePostExtractor,
+ "#pattern" : COMBINED_URL_PATTERN,
+ "#count" : 4,
+
+ "postId": "3-138146100",
+ "date": "dt:2023-10-27 13:35:38",
+ "author": {
+ "memberId": "e89820ec1a72d7255120284ca3aeafa5",
+ "artistOfficialProfile": {
+ "officialName": "SU A",
+ },
+ },
+ "community": {
+ "communityName": "Dreamcatcher",
+ },
+},
+
+{
+ "#url" : "https://weverse.io/dreamcatcher/fanpost/4-138493328",
+ "#category": ("", "weverse", "post"),
+ "#class" : weverse.WeversePostExtractor,
+ "#pattern" : COMBINED_URL_PATTERN,
+ "#count" : 1,
+},
+
+{
+ "#url" : "https://weverse.io/dreamcatcher/profile/e89820ec1a72d7255120284ca3aeafa5",
+ "#category": ("", "weverse", "user"),
+ "#class" : weverse.WeverseUserExtractor,
+},
+
+{
+ "#url" : "https://weverse.io/dreamcatcher/feed",
+ "#comment" : "feed tab (fan posts)",
+ "#category": ("", "weverse", "feed"),
+ "#class" : weverse.WeverseFeedExtractor,
+},
+
+{
+ "#url" : "https://weverse.io/dreamcatcher/artist",
+ "#comment" : "artist tab (artist posts)",
+ "#category": ("", "weverse", "feed"),
+ "#class" : weverse.WeverseFeedExtractor,
+},
+
+{
+ "#url" : "https://weverse.io/dreamcatcher/moment/e89820ec1a72d7255120284ca3aeafa5/post/2-111675163",
+ "#category": ("", "weverse", "moment"),
+ "#class" : weverse.WeverseMomentExtractor,
+ "#pattern" : COMBINED_URL_PATTERN,
+ "#count" : 1,
+
+ "width" : 1080,
+ "height" : 1920,
+ "date" : "dt:2023-01-09 06:25:41",
+ "expireAt": "dt:2023-01-10 06:25:41",
+},
+
+{
+ "#url" : "https://weverse.io/dreamcatcher/moment/e89820ec1a72d7255120284ca3aeafa5",
+ "#category": ("", "weverse", "moments"),
+ "#class" : weverse.WeverseMomentsExtractor,
+},
+
+{
+ "#url" : "https://weverse.io/lesserafim/media/0-128617470",
+ "#comment" : "image",
+ "#category": ("", "weverse", "media"),
+ "#class" : weverse.WeverseMediaExtractor,
+ "#pattern" : COMBINED_URL_PATTERN,
+ "#count" : 5,
+
+ "media": {
+ "categories": [
+ {
+ "id": 1091,
+ },
+ ],
+ },
+ "community": {
+ "communityName": "LE SSERAFIM",
+ },
+},
+
+{
+ "#url" : "https://weverse.io/lesserafim/media/1-128435266",
+ "#comment" : "video",
+ "#category": ("", "weverse", "media"),
+ "#class" : weverse.WeverseMediaExtractor,
+ "#pattern" : COMBINED_URL_PATTERN,
+ "#count" : 1,
+
+ "width" : 1080,
+ "height": 1920,
+ "media": {
+ "categories": [
+ {
+ "id": 1532,
+ },
+ ],
+ },
+},
+
+{
+ "#url" : "https://weverse.io/dreamcatcher/media/1-128875973",
+ "#comment" : "embed",
+ "#category": ("", "weverse", "media"),
+ "#class" : weverse.WeverseMediaExtractor,
+
+ "postType": "YOUTUBE",
+},
+
+{
+ "#url" : "https://weverse.io/dreamcatcher/media/all",
+ "#category": ("", "weverse", "media-tab"),
+ "#class" : weverse.WeverseMediaTabExtractor,
+},
+
+{
+ "#url" : "https://weverse.io/dreamcatcher/media/category/337",
+ "#category": ("", "weverse", "media-category"),
+ "#class" : weverse.WeverseMediaCategoryExtractor,
+},
+
+)
diff --git a/test/test_results.py b/test/test_results.py
index f275bbfcd55..6ccbf08f76e 100644
--- a/test/test_results.py
+++ b/test/test_results.py
@@ -399,6 +399,9 @@ def setup_test_config():
config.set(("extractor", "tumblr"), "access-token-secret",
"sgOA7ZTT4FBXdOGGVV331sSp0jHYp4yMDRslbhaQf7CaS71i4O")
+ config.set(("extractor", "weverse"), "username", None)
+ config.set(("extractor", "weverse"), "password", None)
+
def generate_tests():
"""Dynamically generate extractor unittests"""