diff --git a/docs/supportedsites.md b/docs/supportedsites.md
index b676e27c..1911cfe1 100644
--- a/docs/supportedsites.md
+++ b/docs/supportedsites.md
@@ -121,6 +121,12 @@ Consider all listed sites to potentially be NSFW.
Albums, Artwork Listings, Challenges, Collections, Followed Users, individual Images, Likes, Search Results, User Profiles |
|
+
+ | Audiochan |
+ https://audiochan.com/ |
+ Audios, Collections, User Profiles |
+ |
+
| BATO.TO |
https://bato.to/ |
diff --git a/gallery_dl/extractor/__init__.py b/gallery_dl/extractor/__init__.py
index 1fdf2d70..c96ccf4d 100644
--- a/gallery_dl/extractor/__init__.py
+++ b/gallery_dl/extractor/__init__.py
@@ -29,6 +29,7 @@ modules = [
"arena",
"artstation",
"aryion",
+ "audiochan",
"batoto",
"bbc",
"behance",
diff --git a/gallery_dl/extractor/audiochan.py b/gallery_dl/extractor/audiochan.py
new file mode 100644
index 00000000..d5153d17
--- /dev/null
+++ b/gallery_dl/extractor/audiochan.py
@@ -0,0 +1,115 @@
+# -*- 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://audiochan.com/"""
+
+from .common import Extractor, Message
+from .. import text
+
+BASE_PATTERN = r"(?:https?://)?(?:www\.)?audiochan\.com"
+
+
+class AudiochanExtractor(Extractor):
+ """Base class for audiochan extractors"""
+ category = "audiochan"
+ root = "https://audiochan.com"
+ root_api = "https://api.audiochan.com"
+ directory_fmt = ("{category}", "{user[display_name]}")
+ filename_fmt = "{title} ({slug}).{extension}"
+ archive_fmt = "{audioFile[id]}"
+
+ def _init(self):
+ self.headers_api = {
+ "content-type" : "application/json",
+ "Origin" : self.root,
+ "Sec-Fetch-Dest" : "empty",
+ "Sec-Fetch-Mode" : "cors",
+ "Sec-Fetch-Site" : "same-site",
+ }
+ self.headers_dl = {
+ "Accept": "audio/webm,audio/ogg,audio/wav,audio/*;q=0.9,"
+ "application/ogg;q=0.7,video/*;q=0.6,*/*;q=0.5",
+ # "Range" : "bytes=0-",
+ "Sec-Fetch-Dest" : "audio",
+ "Sec-Fetch-Mode" : "no-cors",
+ "Sec-Fetch-Site" : "same-site",
+ "Accept-Encoding": "identity",
+ }
+
+ def items(self):
+ for post in self.posts():
+ file = post["audioFile"]
+
+ post["_http_headers"] = self.headers_dl
+ post["date"] = self.parse_datetime_iso(file["created_at"])
+ post["date_updated"] = self.parse_datetime_iso(file["updated_at"])
+ post["tags"] = [f"{tag['category']}:{tag['name']}"
+ for tag in post["tags"]]
+
+ yield Message.Directory, post
+ text.nameext_from_name(file["filename"], post)
+ yield Message.Url, file["url"] or file["stream_url"], post
+
+ def request_api(self, endpoint, params=None):
+ url = self.root_api + endpoint
+ return self.request_json(url, params=params, headers=self.headers_api)
+
+ def _pagination(self, endpoint, params):
+ params["page"] = 1
+ params["limit"] = "12"
+
+ while True:
+ data = self.request_api(endpoint, params)
+
+ yield from data["data"]
+
+ if not data["has_more"]:
+ break
+ params["page"] += 1
+
+
+class AudiochanAudioExtractor(AudiochanExtractor):
+ subcategory = "audio"
+ pattern = rf"{BASE_PATTERN}/a/(\w+)"
+ example = "https://audiochan.com/a/SLUG"
+
+ def posts(self):
+ audio = self.request_api("/audios/slug/" + self.groups[0])
+ audio["user"] = audio["credits"][0]["user"]
+ return (audio,)
+
+
+class AudiochanUserExtractor(AudiochanExtractor):
+ subcategory = "user"
+ pattern = rf"{BASE_PATTERN}/u/(\w+)"
+ example = "https://audiochan.com/u/USER"
+
+ def posts(self):
+ endpoint = "/users/" + self.groups[0]
+ self.kwdict["user"] = self.request_api(endpoint)["data"]
+
+ params = {
+ "sfw_only": "false",
+ "sort" : "new",
+ }
+ return self._pagination(endpoint + "/audios", params)
+
+
+class AudiochanCollectionExtractor(AudiochanExtractor):
+ subcategory = "collection"
+ pattern = rf"{BASE_PATTERN}/c/(\w+)"
+ example = "https://audiochan.com/c/SLUG"
+
+ def posts(self):
+ slug = self.groups[0]
+ endpoint = "/collections/" + slug
+ self.kwdict["collection"] = col = self.request_api(endpoint)
+ col.pop("audios", None)
+
+ endpoint = f"/collections/slug/{slug}/items"
+ return self._pagination(endpoint, {})
diff --git a/scripts/supportedsites.py b/scripts/supportedsites.py
index 056b8d54..b15bdb34 100755
--- a/scripts/supportedsites.py
+++ b/scripts/supportedsites.py
@@ -245,6 +245,9 @@ SUBCATEGORY_MAP = {
"artwork": "Artwork Listings",
"collections": "",
},
+ "audiochan": {
+ "audio": "Audios",
+ },
"bilibili": {
"user-articles-favorite": "User Article Favorites",
},
diff --git a/test/results/audiochan.py b/test/results/audiochan.py
new file mode 100644
index 00000000..b630e4e9
--- /dev/null
+++ b/test/results/audiochan.py
@@ -0,0 +1,39 @@
+# -*- 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 audiochan
+
+
+__tests__ = (
+{
+ "#url" : "https://audiochan.com/a/pBP1V1ODEV2od9CjLu",
+ "#class" : audiochan.AudiochanAudioExtractor,
+ "#pattern" : r"https://stream.audiochan.com/v\?token=YXVkaW9zL2Q4YjA1ZWEzLWU0ZGItNGU2NC05MzZiLTQzNmI3MmM4OTViMS9sOTBCOFI0ajhjS0NFSmNwa2kubXAz&exp=\d+&st=\w+",
+ "#count" : 1,
+},
+
+{
+ "#url" : "https://audiochan.com/u/lil_lovergirl",
+ "#class" : audiochan.AudiochanUserExtractor,
+ "#pattern" : r"https://stream\.audiochan\.com/v\?token=\w+\&exp=\d+\&st=\w+",
+ "#count" : 35,
+},
+
+{
+ "#url" : "https://audiochan.com/c/qzrByaXAwTLVXRgC9m",
+ "#class" : audiochan.AudiochanCollectionExtractor,
+ "#results" : (
+ "https://content.audiochan.com/audios/d8b05ea3-e4db-4e64-936b-436b72c895b1/l90B8R4j8cKCEJcpki.mp3",
+ "https://content.audiochan.com/audios/d8b05ea3-e4db-4e64-936b-436b72c895b1/IPI4XoXS1Z1Qn7oEiN.mp3",
+ "https://content.audiochan.com/audios/d8b05ea3-e4db-4e64-936b-436b72c895b1/6kwizqnvUHttvUkXm6.mp3",
+ "https://content.audiochan.com/audios/d8b05ea3-e4db-4e64-936b-436b72c895b1/zn81mtgXslfDd20Tu8.wav",
+ "https://content.audiochan.com/audios/d8b05ea3-e4db-4e64-936b-436b72c895b1/Q33gP6yAg8jEM1C4Ic.mp3",
+ "https://content.audiochan.com/audios/d8b05ea3-e4db-4e64-936b-436b72c895b1/Fwy5YxgK4zc7sQ9xx3.mp3",
+ "https://content.audiochan.com/audios/d8b05ea3-e4db-4e64-936b-436b72c895b1/P3YrtAdKVekYb3BTgy.mp3",
+ ),
+},
+
+)