rsshub
Version:
Make RSS Great Again!
268 lines (265 loc) • 8.1 kB
JavaScript
import { t as parseDate } from "./parse-date-BrP7mxXf.mjs";
import { t as got_default } from "./got-KxxWdaxq.mjs";
import { t as invalid_parameter_default } from "./invalid-parameter-rr4AgGpp.mjs";
import CryptoJS from "crypto-js";
//#region lib/routes/mixcloud/config.ts
const MIXCLOUD_CONFIG = {
host: "https://www.mixcloud.com",
imageBaseURL: "https://thumbnailer.mixcloud.com/unsafe/480x480/",
graphqlURL: "https://app.mixcloud.com/graphql",
decryptionKey: "IFYOUWANTTHEARTISTSTOGETPAIDDONOTDOWNLOADFROMMIXCLOUD",
headers: {
Referer: "https://www.mixcloud.com",
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest"
}
};
const TYPE_CONFIG = {
uploads: "uploads",
reposts: "reposted",
favorites: "favorites",
listens: "listeningHistory",
stream: "stream",
playlist: "items"
};
const TYPE_NAMES = {
uploads: "Shows",
reposts: "Reposts",
favorites: "Favorites",
listens: "History",
stream: "Stream",
playlist: "Playlist"
};
const CLOUDCAST_FIELDS = `
id
slug
name
description
publishDate
audioLength
picture(width: 1024, height: 1024) {
url
}
owner {
displayName
username
url
}
streamInfo {
url
hlsUrl
dashUrl
}
favorites {
totalCount
}
reposts {
totalCount
}
plays
tags {
tag {
name
}
}
featuringArtistList
isExclusive
restrictedReason
comments(first: 100) {
edges {
node {
comment
created
user {
displayName
username
}
}
}
totalCount
}
`;
function getObjectFields(type) {
if (type === "playlist") return {
objectType: "playlist",
objectFields: `
name
description
picture {
urlRoot
}
items(first: 100) {
edges {
node {
cloudcast {
${CLOUDCAST_FIELDS}
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
`
};
else {
const nodeTemplate = type === "listens" ? `node { cloudcast { ${CLOUDCAST_FIELDS} } }` : `node { ${CLOUDCAST_FIELDS} }`;
return {
objectType: "user",
objectFields: `
displayName
biog
picture {
urlRoot
}
${TYPE_CONFIG[type]}(first: 100) {
edges {
${nodeTemplate}
}
pageInfo {
endCursor
hasNextPage
}
}
`
};
}
}
//#endregion
//#region lib/routes/mixcloud/index.ts
const route = {
path: "/:username/:type?",
categories: ["multimedia"],
example: "/mixcloud/dholbach/uploads",
parameters: {
username: "Username, can be found in URL",
type: "Type, see below, uploads by default"
},
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: true,
supportScihub: false
},
radar: [{ source: ["mixcloud.com/:username/:type?"] }, { source: ["www.mixcloud.com/:username/:type?"] }],
name: "User",
maintainers: ["Misaka13514"],
handler,
description: `| Shows | Reposts | Favorites | History | Stream |
| ------- | ------- | --------- | ------- | ------ |
| uploads | reposts | favorites | listens | stream |`
};
async function callApi(objectType, objectFields, username, slug) {
const lookupKey = objectType + "Lookup";
const headers = MIXCLOUD_CONFIG.headers;
const query = `{
${lookupKey}(lookup: {username: "${username}"${slug ? `, slug: "${slug}"` : ""}}) {
${objectFields}
}
}`;
return (await got_default({
method: "post",
url: MIXCLOUD_CONFIG.graphqlURL,
headers,
json: { query }
})).data.data[lookupKey];
}
function decryptXorCipher(key, ciphertext) {
return [...CryptoJS.enc.Base64.parse(ciphertext).toString(CryptoJS.enc.Utf8)].map((ch, i) => String.fromCodePoint((ch.codePointAt(0) || 0) ^ (key.codePointAt(i % key.length) || 0))).join("");
}
function tryDecrypt(ciphertext) {
if (!ciphertext) return "";
try {
return decryptXorCipher(MIXCLOUD_CONFIG.decryptionKey, ciphertext);
} catch {
return ciphertext;
}
}
function getCloudcast(node, type) {
if (type === "playlist" || type === "listens") return node.cloudcast;
return node;
}
function getPlaylistTitle(displayName, type, playlistName) {
if (type === "playlist" && playlistName) return `Mixcloud - ${displayName}'s Playlist: ${playlistName}`;
return `Mixcloud - ${displayName}'s ${TYPE_NAMES[type] || type}`;
}
function getPlaylistLink(username, type, playlistSlug) {
const host = MIXCLOUD_CONFIG.host;
if (type === "playlist" && playlistSlug) return `${host}/${username}/playlists/${playlistSlug}/`;
return `${host}/${username}/${type === "uploads" ? "" : type + "/"}`;
}
async function handler(ctx) {
const username = ctx.req.param("username");
const playlistSlug = ctx.req.param("playlist");
const type = ctx.req.param("type") ?? (playlistSlug ? "playlist" : "uploads");
if (!TYPE_CONFIG[type]) throw new invalid_parameter_default(`Invalid type: ${type}`);
const { objectType, objectFields } = getObjectFields(type);
const data = await callApi(objectType, objectFields, username, playlistSlug);
if (!data) throw new Error(`${type === "playlist" ? "Playlist" : "User"} not found`);
const isPlaylist = type === "playlist";
const displayName = isPlaylist ? username : data.displayName;
const description = isPlaylist ? data.description : data.biog;
const picture = data.picture;
const image = picture && picture.urlRoot ? `${MIXCLOUD_CONFIG.imageBaseURL}${picture.urlRoot}` : "";
const items = (data[TYPE_CONFIG[type]]?.edges || []).map((edge) => {
const cloudcast = getCloudcast(edge.node, type);
if (!cloudcast) return null;
const enclosureUrl = tryDecrypt((cloudcast.streamInfo || {}).url);
const tags = cloudcast.tags?.map((t) => t.tag?.name).filter(Boolean) || [];
let richDescription = cloudcast.description?.replaceAll("\n", "<br>") || "";
if (cloudcast.featuringArtistList && cloudcast.featuringArtistList.length > 0) {
const artists = cloudcast.featuringArtistList.slice(0, 5).join(", ");
richDescription += richDescription ? "<br><br>" : "";
richDescription += `<strong>Featured Artists:</strong> ${artists}`;
if (cloudcast.featuringArtistList.length > 5) richDescription += ` and ${cloudcast.featuringArtistList.length - 5} more...`;
}
const stats = [];
if (cloudcast.plays) stats.push(`${cloudcast.plays} plays`);
if (cloudcast.favorites?.totalCount) stats.push(`${cloudcast.favorites.totalCount} favorites`);
if (cloudcast.reposts?.totalCount) stats.push(`${cloudcast.reposts.totalCount} reposts`);
if (cloudcast.comments?.totalCount) stats.push(`${cloudcast.comments.totalCount} comments`);
if (stats.length > 0) {
richDescription += richDescription ? "<br><br>" : "";
richDescription += `<em>${stats.join(" • ")}</em>`;
}
return {
title: cloudcast.name,
author: cloudcast.owner?.displayName || username,
description: richDescription,
pubDate: parseDate(cloudcast.publishDate),
guid: Buffer.from(cloudcast.id, "base64").toString(),
link: `${MIXCLOUD_CONFIG.host}/${cloudcast.owner?.username || username}/${cloudcast.slug}`,
itunes_item_image: cloudcast.picture?.url || "",
itunes_duration: cloudcast.audioLength,
enclosure_url: enclosureUrl,
enclosure_type: "audio/x-m4a",
itunes_author: cloudcast.owner?.displayName || username,
category: tags,
comments: cloudcast.comments?.totalCount || 0,
upvotes: cloudcast.favorites?.totalCount || 0,
media: enclosureUrl ? {
content: {
url: enclosureUrl,
type: "audio/x-m4a",
duration: cloudcast.audioLength
},
thumbnail: cloudcast.picture?.url ? { url: cloudcast.picture.url } : void 0
} : void 0
};
}).filter(Boolean);
const title = getPlaylistTitle(displayName, type, data.name);
const link = getPlaylistLink(username, type, playlistSlug);
return {
title,
description: description?.replaceAll("\n", "<br>") || "",
itunes_author: displayName,
image,
link,
item: items
};
}
//#endregion
export { route as n, handler as t };