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"""