Skip to content

Commit 994cc34

Browse files
committed
[audiochan] add initial support (#8602)
1 parent e589d6f commit 994cc34

File tree

5 files changed

+164
-0
lines changed

5 files changed

+164
-0
lines changed

docs/supportedsites.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,12 @@ Consider all listed sites to potentially be NSFW.
121121
<td>Albums, Artwork Listings, Challenges, Collections, Followed Users, individual Images, Likes, Search Results, User Profiles</td>
122122
<td></td>
123123
</tr>
124+
<tr id="audiochan" title="audiochan">
125+
<td>Audiochan</td>
126+
<td>https://audiochan.com/</td>
127+
<td>Audios, Collections, User Profiles</td>
128+
<td></td>
129+
</tr>
124130
<tr id="batoto" title="batoto">
125131
<td>BATO.TO</td>
126132
<td>https://bato.to/</td>

gallery_dl/extractor/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"arena",
3030
"artstation",
3131
"aryion",
32+
"audiochan",
3233
"batoto",
3334
"bbc",
3435
"behance",

gallery_dl/extractor/audiochan.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright 2025 Mike Fährmann
4+
#
5+
# This program is free software; you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License version 2 as
7+
# published by the Free Software Foundation.
8+
9+
"""Extractors for https://audiochan.com/"""
10+
11+
from .common import Extractor, Message
12+
from .. import text
13+
14+
BASE_PATTERN = r"(?:https?://)?(?:www\.)?audiochan\.com"
15+
16+
17+
class AudiochanExtractor(Extractor):
18+
"""Base class for audiochan extractors"""
19+
category = "audiochan"
20+
root = "https://audiochan.com"
21+
root_api = "https://api.audiochan.com"
22+
directory_fmt = ("{category}", "{user[display_name]}")
23+
filename_fmt = "{title} ({slug}).{extension}"
24+
archive_fmt = "{audioFile[id]}"
25+
26+
def _init(self):
27+
self.headers_api = {
28+
"content-type" : "application/json",
29+
"Origin" : self.root,
30+
"Sec-Fetch-Dest" : "empty",
31+
"Sec-Fetch-Mode" : "cors",
32+
"Sec-Fetch-Site" : "same-site",
33+
}
34+
self.headers_dl = {
35+
"Accept": "audio/webm,audio/ogg,audio/wav,audio/*;q=0.9,"
36+
"application/ogg;q=0.7,video/*;q=0.6,*/*;q=0.5",
37+
# "Range" : "bytes=0-",
38+
"Sec-Fetch-Dest" : "audio",
39+
"Sec-Fetch-Mode" : "no-cors",
40+
"Sec-Fetch-Site" : "same-site",
41+
"Accept-Encoding": "identity",
42+
}
43+
44+
def items(self):
45+
for post in self.posts():
46+
file = post["audioFile"]
47+
48+
post["_http_headers"] = self.headers_dl
49+
post["date"] = self.parse_datetime_iso(file["created_at"])
50+
post["date_updated"] = self.parse_datetime_iso(file["updated_at"])
51+
post["tags"] = [f"{tag['category']}:{tag['name']}"
52+
for tag in post["tags"]]
53+
54+
yield Message.Directory, post
55+
text.nameext_from_name(file["filename"], post)
56+
yield Message.Url, file["url"] or file["stream_url"], post
57+
58+
def request_api(self, endpoint, params=None):
59+
url = self.root_api + endpoint
60+
return self.request_json(url, params=params, headers=self.headers_api)
61+
62+
def _pagination(self, endpoint, params):
63+
params["page"] = 1
64+
params["limit"] = "12"
65+
66+
while True:
67+
data = self.request_api(endpoint, params)
68+
69+
yield from data["data"]
70+
71+
if not data["has_more"]:
72+
break
73+
params["page"] += 1
74+
75+
76+
class AudiochanAudioExtractor(AudiochanExtractor):
77+
subcategory = "audio"
78+
pattern = rf"{BASE_PATTERN}/a/(\w+)"
79+
example = "https://audiochan.com/a/SLUG"
80+
81+
def posts(self):
82+
audio = self.request_api("/audios/slug/" + self.groups[0])
83+
audio["user"] = audio["credits"][0]["user"]
84+
return (audio,)
85+
86+
87+
class AudiochanUserExtractor(AudiochanExtractor):
88+
subcategory = "user"
89+
pattern = rf"{BASE_PATTERN}/u/(\w+)"
90+
example = "https://audiochan.com/u/USER"
91+
92+
def posts(self):
93+
endpoint = "/users/" + self.groups[0]
94+
self.kwdict["user"] = self.request_api(endpoint)["data"]
95+
96+
params = {
97+
"sfw_only": "false",
98+
"sort" : "new",
99+
}
100+
return self._pagination(endpoint + "/audios", params)
101+
102+
103+
class AudiochanCollectionExtractor(AudiochanExtractor):
104+
subcategory = "collection"
105+
pattern = rf"{BASE_PATTERN}/c/(\w+)"
106+
example = "https://audiochan.com/c/SLUG"
107+
108+
def posts(self):
109+
slug = self.groups[0]
110+
endpoint = "/collections/" + slug
111+
self.kwdict["collection"] = col = self.request_api(endpoint)
112+
col.pop("audios", None)
113+
114+
endpoint = f"/collections/slug/{slug}/items"
115+
return self._pagination(endpoint, {})

scripts/supportedsites.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,9 @@
245245
"artwork": "Artwork Listings",
246246
"collections": "",
247247
},
248+
"audiochan": {
249+
"audio": "Audios",
250+
},
248251
"bilibili": {
249252
"user-articles-favorite": "User Article Favorites",
250253
},

test/results/audiochan.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# This program is free software; you can redistribute it and/or modify
4+
# it under the terms of the GNU General Public License version 2 as
5+
# published by the Free Software Foundation.
6+
7+
from gallery_dl.extractor import audiochan
8+
9+
10+
__tests__ = (
11+
{
12+
"#url" : "https://audiochan.com/a/pBP1V1ODEV2od9CjLu",
13+
"#class" : audiochan.AudiochanAudioExtractor,
14+
"#pattern" : r"https://stream.audiochan.com/v\?token=YXVkaW9zL2Q4YjA1ZWEzLWU0ZGItNGU2NC05MzZiLTQzNmI3MmM4OTViMS9sOTBCOFI0ajhjS0NFSmNwa2kubXAz&exp=\d+&st=\w+",
15+
"#count" : 1,
16+
},
17+
18+
{
19+
"#url" : "https://audiochan.com/u/lil_lovergirl",
20+
"#class" : audiochan.AudiochanUserExtractor,
21+
"#pattern" : r"https://stream\.audiochan\.com/v\?token=\w+\&exp=\d+\&st=\w+",
22+
"#count" : 35,
23+
},
24+
25+
{
26+
"#url" : "https://audiochan.com/c/qzrByaXAwTLVXRgC9m",
27+
"#class" : audiochan.AudiochanCollectionExtractor,
28+
"#results" : (
29+
"https://content.audiochan.com/audios/d8b05ea3-e4db-4e64-936b-436b72c895b1/l90B8R4j8cKCEJcpki.mp3",
30+
"https://content.audiochan.com/audios/d8b05ea3-e4db-4e64-936b-436b72c895b1/IPI4XoXS1Z1Qn7oEiN.mp3",
31+
"https://content.audiochan.com/audios/d8b05ea3-e4db-4e64-936b-436b72c895b1/6kwizqnvUHttvUkXm6.mp3",
32+
"https://content.audiochan.com/audios/d8b05ea3-e4db-4e64-936b-436b72c895b1/zn81mtgXslfDd20Tu8.wav",
33+
"https://content.audiochan.com/audios/d8b05ea3-e4db-4e64-936b-436b72c895b1/Q33gP6yAg8jEM1C4Ic.mp3",
34+
"https://content.audiochan.com/audios/d8b05ea3-e4db-4e64-936b-436b72c895b1/Fwy5YxgK4zc7sQ9xx3.mp3",
35+
"https://content.audiochan.com/audios/d8b05ea3-e4db-4e64-936b-436b72c895b1/P3YrtAdKVekYb3BTgy.mp3",
36+
),
37+
},
38+
39+
)

0 commit comments

Comments
 (0)