diff --git a/docs/supportedsites.md b/docs/supportedsites.md index 5fddd1a5..8b7081e8 100644 --- a/docs/supportedsites.md +++ b/docs/supportedsites.md @@ -1231,6 +1231,12 @@ Consider all listed sites to potentially be NSFW. Albums, Articles, Feeds, Images from Statuses, User Profiles, Videos + + Whyp + https://whyp.it/ + Audio, Collections, User Profiles + + WikiArt.org https://www.wikiart.org/ diff --git a/gallery_dl/extractor/__init__.py b/gallery_dl/extractor/__init__.py index ae83e70e..9f182041 100644 --- a/gallery_dl/extractor/__init__.py +++ b/gallery_dl/extractor/__init__.py @@ -234,6 +234,7 @@ modules = [ "weebcentral", "weebdex", "weibo", + "whyp", "wikiart", "wikifeet", "wikimedia", diff --git a/gallery_dl/extractor/whyp.py b/gallery_dl/extractor/whyp.py new file mode 100644 index 00000000..d5bc6abe --- /dev/null +++ b/gallery_dl/extractor/whyp.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- + +# Copyright 2025 Mike FΓ€hrmann +# +# 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://whyp.it/""" + +from .common import Extractor, Message +from .. import text + +BASE_PATTERN = r"(?:https?://)?(?:www\.)?whyp\.it" + + +class WhypExtractor(Extractor): + """Base class for whyp extractors""" + category = "whyp" + root = "https://whyp.it" + root_api = "https://api.whyp.it" + directory_fmt = ("{category}", "{user[username]} ({user[id]})") + filename_fmt = "{id} {title}.{extension}" + archive_fmt = "{id}" + + def _init(self): + self.headers_api = { + "Accept" : "application/json", + "Origin" : self.root, + "Referer": self.root + "/", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-site", + } + + def items(self): + for track in self.tracks(): + if url := track.get("lossless_url"): + track["original"] = True + else: + url = track["lossy_url"] + track["original"] = False + + if "created_at" in track: + track["date"] = self.parse_datetime_iso(track["created_at"]) + + yield Message.Directory, "", track + yield Message.Url, url, text.nameext_from_url(url, track) + + +class WhypAudioExtractor(WhypExtractor): + subcategory = "audio" + pattern = BASE_PATTERN + r"/tracks/(\d+)" + example = "https://whyp.it/tracks/12345/SLUG" + + def tracks(self): + url = f"{self.root_api}/api/tracks/{self.groups[0]}" + track = self.request_json(url, headers=self.headers_api)["track"] + return (track,) + + +class WhypUserExtractor(WhypExtractor): + subcategory = "user" + pattern = BASE_PATTERN + r"/users/(\d+)" + example = "https://whyp.it/users/123/NAME" + + def tracks(self): + url = f"{self.root_api}/api/users/{self.groups[0]}/tracks" + params = {} + headers = self.headers_api + + while True: + data = self.request_json(url, params=params, headers=headers) + + yield from data["tracks"] + + if not (cursor := data.get("next_cursor")): + break + params["cursor"] = cursor + + +class WhypCollectionExtractor(WhypExtractor): + subcategory = "collection" + pattern = BASE_PATTERN + r"/collections/(\d+)" + example = "https://whyp.it/collections/123/NAME" + + def tracks(self): + cid = self.groups[0] + + url = f"{self.root_api}/api/collections/{cid}" + headers = self.headers_api + self.kwdict["collection"] = collection = self.request_json( + url, headers=headers)["collection"] + + url = f"{self.root_api}/api/collections/{cid}/tracks" + params = {"token": collection["token"]} + data = self.request_json(url, params=params, headers=headers) + return data["tracks"] diff --git a/test/results/whyp.py b/test/results/whyp.py new file mode 100644 index 00000000..49614f24 --- /dev/null +++ b/test/results/whyp.py @@ -0,0 +1,143 @@ +# -*- 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 whyp + + +__tests__ = ( +{ + "#url" : "https://whyp.it/tracks/13721/fallout-3-intro-remake", + "#class" : whyp.WhypAudioExtractor, + "#pattern" : r"https://cdn.whyp.it/5e9de576-f33a-40ea-bd43-1693a568a6a0.mp3\?token=.+", + + "allow_downloads": False, + "artwork_url" : None, + "artwork_url_fallback": "https://cdn.whyp.it/a46f3485-8d19-4753-98e0-76011c7e33b0.jpg", + "comments_count" : int, + "created_at" : "2025-11-24T16:59:50+00:00", + "date" : "dt:2025-11-24 16:59:50", + "description" : "", + "duration" : 46.34, + "extension" : "mp3", + "filename" : "5e9de576-f33a-40ea-bd43-1693a568a6a0", + "id" : 13721, + "lossless_size" : None, + "lossless_url" : None, + "lossy_size" : 1853719, + "lossy_url" : r"re:https://cdn.whyp.it/5e9de576-f33a-40ea-bd43-1693a568a6a0.mp3", + "original" : False, + "public" : True, + "settings_comments": "users", + "slug" : "fallout-3-intro-remake", + "title" : "Fallout 3 Intro Remake", + "token" : "k5E2z", + "user_id" : 1, + "waveform_url" : r"re:https://cdn.whyp.it/5e9de576-f33a-40ea-bd43-1693a568a6a0.json", + "user" : { + "avatar" : "https://cdn.whyp.it/a46f3485-8d19-4753-98e0-76011c7e33b0.jpg", + "has_enterprise" : True, + "has_pro" : True, + "has_pro_lifetime": False, + "id" : 1, + "slug" : "brad", + "status" : "Coding πŸ‘¨πŸ»β€πŸ’»", + "tracks_count" : 3, + "username" : "Brad", + }, +}, + +{ + "#url" : "https://whyp.it/users/1/brad", + "#class" : whyp.WhypUserExtractor, + "#pattern" : ( + r"https://cdn.whyp.it/5e9de576-f33a-40ea-bd43-1693a568a6a0.mp3\?token=.+", + r"https://cdn.whyp.it/0d7a196b-3e1a-4510-a4a4-6189c56ecb27.flac\?token=.+", + r"https://cdn.whyp.it/3d134d07-7c55-4a6b-b321-56ce90ee1fc8.flac\?token=.+", + ), + + "allow_downloads": bool, + "artwork_url" : {str, None}, + "artwork_url_fallback": str, + "comments_count" : int, + "created_at" : "iso:dt", + "date" : "type:datetime", + "description" : str, + "duration" : float, + "extension" : {"mp3", "flac"}, + "filename" : "iso:uuid", + "id" : {13721, 18337, 324260}, + "lossless_size" : {int, None}, + "lossless_url" : {str, None}, + "lossy_size" : int, + "lossy_url" : str, + "original" : bool, + "public" : True, + "settings_comments": "users", + "slug" : str, + "title" : str, + "token" : str, + "user_id" : 1, + "waveform_url" : str, + "user" : { + "avatar" : "https://cdn.whyp.it/a46f3485-8d19-4753-98e0-76011c7e33b0.jpg", + "has_enterprise" : True, + "has_pro" : True, + "has_pro_lifetime": False, + "id" : 1, + "slug" : "brad", + "status" : "Coding πŸ‘¨πŸ»β€πŸ’»", + "tracks_count" : 3, + "username" : "Brad", + }, +}, + +{ + "#url" : "https://whyp.it/collections/1/example-collection", + "#class" : whyp.WhypCollectionExtractor, + "#pattern" : ( + r"https://cdn.whyp.it/3d134d07-7c55-4a6b-b321-56ce90ee1fc8.flac\?token=.+", + r"https://cdn.whyp.it/0d7a196b-3e1a-4510-a4a4-6189c56ecb27.flac\?token=.+", + ), + + "extension" : "flac", + "id" : {18337, 324260}, + "original" : True, + "pivot_collection_id": 1, + "pivot_created_at": "iso:dt", + "pivot_order" : int, + "public" : True, + "collection" : { + "artwork_url" : "https://cdn.whyp.it/60fff341-02ef-4fe2-86f7-f283b2df1557.jpg", + "artwork_url_fallback": "https://cdn.whyp.it/b42b34d3-5839-4a26-9c32-41e917f31f6b.jpg", + "created_at" : "2023-07-20T16:14:33+00:00", + "description" : "This is an example collection on Whyp!", + "duration" : 352.59, + "hidden_tracks_count": 0, + "id" : 1, + "order" : 1, + "public" : True, + "slug" : "example-collection", + "title" : "Example Collection", + "token" : "VFc7Q", + "tracks_count": 2, + "updated_at" : "2025-11-17T19:45:01+00:00", + "user_id" : 1, + "user" : dict, + }, + "user" : { + "avatar" : "https://cdn.whyp.it/a46f3485-8d19-4753-98e0-76011c7e33b0.jpg", + "has_enterprise" : True, + "has_pro" : True, + "has_pro_lifetime": False, + "id" : 1, + "slug" : "brad", + "status" : "Coding πŸ‘¨πŸ»β€πŸ’»", + "tracks_count" : 3, + "username" : "Brad", + }, +}, + +)