4.6.0: video muting and soundcloud client_id
This commit is contained in:
@@ -1,11 +1,16 @@
|
||||
{
|
||||
"current": {
|
||||
"version": "4.6",
|
||||
"title": "mute videos and proper soundcloud support",
|
||||
"banner": "shutup.png",
|
||||
"content": "i've been longing to implement both of these things, and here they finally are.\n\nservice-related improvements:\n<div class=\"bullpadding\">• you now can download videos with no audio! simply enable the \"mute audio\" option in settings > audio.\n• soundcloud module has been updated, and downloads should no longer break after some time.</div>\nvisual improvements:\n<div class=\"bullpadding\">• moved some things around in settings popup, and added separators where separation is needed.\n• updated some texts in english and russian.\n• version and commit hash have been joined together, now they're a single unit.</div>\ninternal improvements:\n<div class=\"bullpadding\">• updated api documentation to include isAudioMuted.\n• created render elements for separator and explanation due to high duplication of them in the page.\n• fixed some code quirks.</div>\nhere's how soundcloud downloads got fixed:\n\npreviously, client_id was (stupidly) hardcoded. that means cobalt wasn't able to fetch song data if soundcloud web app got updated.\nnow, cobalt tries to find the up-to-date client_id, caches it in memory, and checks if web app version has changed to update the id accordingly. you can see this change for yourself on github."
|
||||
},
|
||||
"history": [{
|
||||
"version": "4.5",
|
||||
"title": "better, faster, stronger, stable",
|
||||
"banner": "meowthstrong.webp",
|
||||
"content": "your favorite social media downloader just got even better! this update includes a ton of imporvements and fixes.\n\nin fact, there are so many changes, i had to split them in sections.\n\nservice-related improvements:\n<div class=\"bullpadding\">• vimeo module has been revamped, all sorts of videos should now be supported.\n• vimeo audio downloads! you now can download audios from more recent videos.\n• {appName} now supports all sorts of tumblr links. (even those scary ones from the mobile app)\n• vk clips support has been fixed. they rolled back the separation of videos and clips, so i had to do the same.\n• youtube videos with community warnings should now be possible to download.</div>\nuser interface improvements:\n<div class=\"bullpadding\">• list of supported services is now MUCH easier to read.\n• banners in changelog history should no longer overlap each other.\n• bullet points! they have a bit of extra padding, so it makes them stand out of the rest of text.</div>\ninternal improvements:\n<div class=\"bullpadding\">• cobalt will now match the link to regex when using ?u= query for autopasting it into input area.\n• better rate limiting: limiting now is done per minute, not per 20 minutes. this ensures less waiting and less attack area for request spammers.\n• moved to my own fork of ytdl-core, cause main project seems to have been abandoned. go check it out on <a class=\"text-backdrop\" href=\"https://github.com/wukko/better-ytdl-core\" target=\"_blank\">github</a> or <a class=\"text-backdrop\" href=\"https://www.npmjs.com/package/better-ytdl-core\" target=\"_blank\">npm</a>!\n• ALL user inputs are now properly sanitized on the server. that includes variables for POST api method, too.\n• \"got\" package has been (mostly) replaced by native fetch api. this should greately reduce ram usage.\n• all unnecessary duplications of module imports have been gotten rid of. no more error passing strings from inside of service modules. you don't make mistakes only if you don't do anything, right?\n• other code optimizations. there's less clutter overall.</div>\nhuge update, right? seems like everything's fixed now?\n\nnope, one issue still persists: sometimes youtube server drops packets for an audio file while cobalt's rendering the video for you. this results in abrupt cuts of audio. if you want to help solving this issue, <a class=\"text-backdrop\" href=\"https://github.com/wukko/cobalt/issues/62\" target=\"_blank\">please feel free to do it on github!</a>\n\nthank you for reading this, and thank you for sticking with cobalt and me."
|
||||
},
|
||||
"history": [{
|
||||
}, {
|
||||
"version": "4.4",
|
||||
"title": "over 1 million monthly requests. thank you.",
|
||||
"banner": "onemillionr.webp",
|
||||
|
||||
@@ -39,7 +39,15 @@ export function checkbox(action, text, aria, paddingType) {
|
||||
<span>${text}</span>
|
||||
</label>`
|
||||
}
|
||||
|
||||
export function sep(paddingType) {
|
||||
let paddingClass = ``
|
||||
switch(paddingType) {
|
||||
case 0:
|
||||
paddingClass += ` top-margin`;
|
||||
break;
|
||||
}
|
||||
return `<div class="separator${paddingClass}"></div>`
|
||||
}
|
||||
export function popup(obj) {
|
||||
let classes = obj.classes ? obj.classes : []
|
||||
let body = obj.body;
|
||||
@@ -143,7 +151,9 @@ export function footerButtons(obj) {
|
||||
return `
|
||||
<div id="footer-buttons">${items}</div>`
|
||||
}
|
||||
|
||||
export function explanation(text) {
|
||||
return `<div class="explanation">${text}</div>`
|
||||
}
|
||||
export function celebrationsEmoji() {
|
||||
let n = new Date().toISOString().split('T')[0].split('-');
|
||||
let dm = `${n[1]}-${n[2]}`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { backdropLink, celebrationsEmoji, checkbox, footerButtons, multiPagePopup, popup, popupWithBottomButtons, settingsCategory, switcher } from "./elements.js";
|
||||
import { backdropLink, celebrationsEmoji, checkbox, explanation, footerButtons, multiPagePopup, popup, popupWithBottomButtons, sep, settingsCategory, switcher } from "./elements.js";
|
||||
import { services as s, appName, authorInfo, version, quality, repo, donations, supportedAudio } from "../config.js";
|
||||
import { getCommitInfo } from "../sub/currentCommit.js";
|
||||
import loc from "../../localization/manager.js";
|
||||
@@ -115,7 +115,7 @@ export default function(obj) {
|
||||
}, {
|
||||
text: changelogManager("content")
|
||||
}, {
|
||||
text: `<div class="separator"></div><span class="text-backdrop">${obj.hash}:</span> ${com[0]}`,
|
||||
text: `${sep()}<span class="text-backdrop">${obj.hash}:</span> ${com[0]}`,
|
||||
classes: ["changelog-subtitle"],
|
||||
nopadding: true
|
||||
}, {
|
||||
@@ -153,13 +153,13 @@ export default function(obj) {
|
||||
text: loc(obj.lang, 'DonateLinksDescription'),
|
||||
classes: ["explanation"]
|
||||
}, {
|
||||
text: `<div class="separator"></div>`,
|
||||
text: sep(),
|
||||
raw: true
|
||||
}, {
|
||||
text: donate.replace(/REPLACEME/g, loc(obj.lang, 'ClickToCopy')),
|
||||
classes: ["desc-padding"]
|
||||
}, {
|
||||
text: `<div class="separator"></div>`,
|
||||
text: sep(),
|
||||
raw: true
|
||||
}, {
|
||||
text: loc(obj.lang, 'DonateHireMe', authorInfo.link),
|
||||
@@ -173,7 +173,7 @@ export default function(obj) {
|
||||
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
|
||||
header: {
|
||||
aboveTitle: {
|
||||
text: `v.${version} ~ ${obj.hash}`,
|
||||
text: `v.${version}-${obj.hash}`,
|
||||
url: `${repo}/commit/${obj.hash}`
|
||||
},
|
||||
title: `${emoji("⚙️", 30)} ${loc(obj.lang, 'TitlePopupSettings')}`
|
||||
@@ -183,7 +183,7 @@ export default function(obj) {
|
||||
title: `${emoji("🎬")} ${loc(obj.lang, 'SettingsVideoTab')}`,
|
||||
content: settingsCategory({
|
||||
name: "downloads",
|
||||
title: loc(obj.lang, 'SettingsDownloadsSubtitle'),
|
||||
title: loc(obj.lang, 'SettingsVideoGeneral'),
|
||||
body: switcher({
|
||||
name: "vQuality",
|
||||
subtitle: loc(obj.lang, 'SettingsQualitySubtitle'),
|
||||
@@ -202,8 +202,7 @@ export default function(obj) {
|
||||
"text": `${loc(obj.lang, 'SettingsQualitySwitchLow')}<br/>(${quality.low}p)`
|
||||
}]
|
||||
})
|
||||
}) + `${!isIOS ? checkbox("downloadPopup", loc(obj.lang, 'SettingsEnableDownloadPopup'), loc(obj.lang, 'AccessibilityEnableDownloadPopup'), 1) : ''}`
|
||||
+ settingsCategory({
|
||||
}) + settingsCategory({
|
||||
name: "youtube",
|
||||
body: switcher({
|
||||
name: "vFormat",
|
||||
@@ -234,7 +233,7 @@ export default function(obj) {
|
||||
subtitle: loc(obj.lang, 'SettingsFormatSubtitle'),
|
||||
explanation: loc(obj.lang, 'SettingsAudioFormatDescription'),
|
||||
items: audioFormats
|
||||
})
|
||||
}) + sep(0) + checkbox("muteAudio", loc(obj.lang, 'SettingsVideoMute'), loc(obj.lang, 'SettingsVideoMute'), 3) + explanation(loc(obj.lang, 'SettingsVideoMuteExplanation'))
|
||||
}) + settingsCategory({
|
||||
name: "tiktok",
|
||||
title: "tiktok & douyin",
|
||||
@@ -263,7 +262,7 @@ export default function(obj) {
|
||||
}) + settingsCategory({
|
||||
name: "miscellaneous",
|
||||
title: loc(obj.lang, 'Miscellaneous'),
|
||||
body: checkbox("disableChangelog", loc(obj.lang, 'SettingsDisableNotifications'))
|
||||
body: checkbox("disableChangelog", loc(obj.lang, 'SettingsDisableNotifications')) + `${!isIOS ? checkbox("downloadPopup", loc(obj.lang, 'SettingsEnableDownloadPopup'), loc(obj.lang, 'AccessibilityEnableDownloadPopup'), 1) : ''}`
|
||||
})
|
||||
}],
|
||||
})}
|
||||
|
||||
@@ -107,7 +107,7 @@ export default async function (host, patternMatch, url, lang, obj) {
|
||||
default:
|
||||
return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
}
|
||||
return !r.error ? matchActionDecider(r, host, obj.ip, obj.aFormat, obj.isAudioOnly, lang) : apiJSON(0, {
|
||||
return !r.error ? matchActionDecider(r, host, obj.ip, obj.aFormat, obj.isAudioOnly, lang, obj.isAudioMuted) : apiJSON(0, {
|
||||
t: Array.isArray(r.error) ? loc(lang, r.error[0], r.error[1]) : loc(lang, r.error)
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -2,8 +2,8 @@ import { audioIgnore, services, supportedAudio } from "../config.js"
|
||||
import { apiJSON } from "../sub/utils.js"
|
||||
import loc from "../../localization/manager.js";
|
||||
|
||||
export default function(r, host, ip, audioFormat, isAudioOnly, lang) {
|
||||
if (!isAudioOnly && !r.picker) {
|
||||
export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMuted) {
|
||||
if (!isAudioOnly && !r.picker && !isAudioMuted) {
|
||||
switch (host) {
|
||||
case "twitter":
|
||||
return apiJSON(1, { u: r.urls });
|
||||
@@ -42,7 +42,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang) {
|
||||
case "tumblr":
|
||||
return apiJSON(1, { u: r.urls });
|
||||
case "vimeo":
|
||||
if (r.filename) {
|
||||
if (Array.isArray(r.urls)) {
|
||||
return apiJSON(2, {
|
||||
type: "render", u: r.urls, service: host, ip: ip,
|
||||
filename: r.filename
|
||||
@@ -51,6 +51,16 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang) {
|
||||
return apiJSON(1, { u: r.urls });
|
||||
}
|
||||
}
|
||||
} else if (isAudioMuted) {
|
||||
let isSplit = Array.isArray(r.urls);
|
||||
return apiJSON(2, {
|
||||
type: isSplit ? "bridge" : "mute",
|
||||
u: isSplit ? r.urls[0] : r.urls,
|
||||
service: host,
|
||||
ip: ip,
|
||||
filename: r.filename,
|
||||
mute: true,
|
||||
});
|
||||
} else if (r.picker) {
|
||||
switch (host) {
|
||||
case "douyin":
|
||||
|
||||
@@ -86,7 +86,6 @@
|
||||
"soundcloud": {
|
||||
"patterns": [":author/:song", ":shortLink"],
|
||||
"bestAudio": "none",
|
||||
"clientid": "YeTcsotswIIc4sse5WZsXszVxMtP6eLc",
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export default async function(obj) {
|
||||
try {
|
||||
let html = await fetch(`https://bilibili.com/video/${obj.id}`, {
|
||||
headers: {"user-agent": genericUserAgent}
|
||||
}).then(async (r) => {return await r.text()}).catch(() => {return false});
|
||||
}).then(async (r) => {return r.text()}).catch(() => {return false});
|
||||
if (!html) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
if (html.includes('<script>window.__playinfo__=') && html.includes('"video_codecid"')) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { maxVideoDuration } from "../config.js";
|
||||
|
||||
export default async function(obj) {
|
||||
try {
|
||||
let data = await fetch(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}/${obj.name}.json`).then(async (r) => {return await r.json()}).catch(() => {return false});
|
||||
let data = await fetch(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}/${obj.name}.json`).then(async (r) => {return r.json()}).catch(() => {return false});
|
||||
if (!data) return { error: 'ErrorCouldntFetch' };
|
||||
data = data[0]["data"]["children"][0]["data"];
|
||||
|
||||
|
||||
@@ -1,4 +1,39 @@
|
||||
import { genericUserAgent, maxAudioDuration, services } from "../config.js";
|
||||
import { genericUserAgent, maxAudioDuration } from "../config.js";
|
||||
|
||||
let cachedID = {}
|
||||
|
||||
async function findClientID() {
|
||||
try {
|
||||
let sc = await fetch('https://soundcloud.com/').then(async (r) => {return r.text()}).catch(() => {return false});
|
||||
let sc_version = String(sc.match(/<script>window\.__sc_version="[0-9]{10}"<\/script>/)[0].match(/[0-9]{10}/));
|
||||
|
||||
if (cachedID.version == sc_version) {
|
||||
return cachedID.id
|
||||
} else {
|
||||
let scripts = sc.matchAll(/<script.+src="(.+)">/g);
|
||||
let clientid;
|
||||
for (let script of scripts) {
|
||||
let url = script[1];
|
||||
|
||||
if (url && !url.startsWith('https://a-v2.sndcdn.com')) return;
|
||||
|
||||
let scrf = await fetch(url).then(async (r) => {return r.text()}).catch(() => {return false});
|
||||
let id = scrf.match(/\("client_id=[A-Za-z0-9]{32}"\)/);
|
||||
|
||||
if (id && typeof id[0] === 'string') {
|
||||
clientid = id[0].match(/[A-Za-z0-9]{32}/)[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
cachedID.version = sc_version;
|
||||
cachedID.id = clientid;
|
||||
return clientid;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function(obj) {
|
||||
try {
|
||||
@@ -6,32 +41,35 @@ export default async function(obj) {
|
||||
if (!obj.author && !obj.song && obj.shortLink) {
|
||||
html = await fetch(`https://soundcloud.app.goo.gl/${obj.shortLink}/`, {
|
||||
headers: {"user-agent": genericUserAgent}
|
||||
}).then(async (r) => {return await r.text()}).catch(() => {return false});
|
||||
}).then(async (r) => {return r.text()}).catch(() => {return false});
|
||||
}
|
||||
if (obj.author && obj.song) {
|
||||
html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}`, {
|
||||
headers: {"user-agent": genericUserAgent}
|
||||
}).then(async (r) => {return await r.text()}).catch(() => {return false});
|
||||
}).then(async (r) => {return r.text()}).catch(() => {return false});
|
||||
}
|
||||
if (!html) return { error: 'ErrorCouldntFetch'};
|
||||
if (html.includes('<script>window.__sc_hydration = ') && html.includes('"format":{"protocol":"progressive","mime_type":"audio/mpeg"},') && html.includes('{"hydratable":"sound","data":')) {
|
||||
let json = JSON.parse(html.split('{"hydratable":"sound","data":')[1].split('}];</script>')[0])
|
||||
if (json["media"]["transcodings"]) {
|
||||
let fileUrl = `${json.media.transcodings[0]["url"].replace("/hls", "/progressive")}?client_id=${services["soundcloud"]["clientid"]}&track_authorization=${json.track_authorization}`;
|
||||
if (fileUrl.substring(0, 54) === "https://api-v2.soundcloud.com/media/soundcloud:tracks:") {
|
||||
if (json.duration < maxAudioDuration) {
|
||||
let file = await fetch(fileUrl).then(async (r) => {return (await r.json()).url}).catch(() => {return false});
|
||||
if (!file) return { error: 'ErrorCouldntFetch' };
|
||||
return {
|
||||
urls: file,
|
||||
audioFilename: `soundcloud_${json.id}`,
|
||||
fileMetadata: {
|
||||
title: json.title,
|
||||
artist: json.user.username,
|
||||
let clientId = await findClientID();
|
||||
if (clientId) {
|
||||
let fileUrl = `${json.media.transcodings[0]["url"].replace("/hls", "/progressive")}?client_id=${clientId}&track_authorization=${json.track_authorization}`;
|
||||
if (fileUrl.substring(0, 54) === "https://api-v2.soundcloud.com/media/soundcloud:tracks:") {
|
||||
if (json.duration < maxAudioDuration) {
|
||||
let file = await fetch(fileUrl).then(async (r) => {return (await r.json()).url}).catch(() => {return false});
|
||||
if (!file) return { error: 'ErrorCouldntFetch' };
|
||||
return {
|
||||
urls: file,
|
||||
audioFilename: `soundcloud_${json.id}`,
|
||||
fileMetadata: {
|
||||
title: json.title,
|
||||
artist: json.user.username,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else return { error: ['ErrorLengthAudioConvert', maxAudioDuration / 60000] }
|
||||
}
|
||||
} else return { error: ['ErrorLengthAudioConvert', maxAudioDuration / 60000] }
|
||||
}
|
||||
} else return { error: 'ErrorSoundCloudNoClientId' }
|
||||
} else return { error: 'ErrorEmptyDownload' }
|
||||
} else return { error: ['ErrorBrokenLink', 'soundcloud'] }
|
||||
} catch (e) {
|
||||
|
||||
@@ -32,7 +32,7 @@ export default async function(obj) {
|
||||
let html = await fetch(`${config[obj.host]["short"]}${obj.id}`, {
|
||||
redirect: "manual",
|
||||
headers: { "user-agent": userAgent }
|
||||
}).then(async (r) => {return await r.text()}).catch(() => {return false});
|
||||
}).then(async (r) => {return r.text()}).catch(() => {return false});
|
||||
if (!html) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
if (html.slice(0, 17) === '<a href="https://' && html.includes('/video/')) {
|
||||
@@ -46,7 +46,7 @@ export default async function(obj) {
|
||||
let detail;
|
||||
detail = await fetch(config[obj.host]["api"].replace("{postId}", obj.postId), {
|
||||
headers: {"user-agent": "TikTok 26.2.0 rv:262018 (iPhone; iOS 14.4.2; en_US) Cronet"}
|
||||
}).then(async (r) => {return await r.json()}).catch(() => {return false});
|
||||
}).then(async (r) => {return r.json()}).catch(() => {return false});
|
||||
|
||||
detail = selector(detail, obj.host, obj.postId);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ export default async function(obj) {
|
||||
let user = obj.user ? obj.user : obj.url.split('.')[0].replace('https://', '');
|
||||
let html = await fetch(`https://${user}.tumblr.com/post/${obj.id}`, {
|
||||
headers: {"user-agent": genericUserAgent}
|
||||
}).then(async (r) => {return await r.text()}).catch(() => {return false});
|
||||
}).then(async (r) => {return r.text()}).catch(() => {return false});
|
||||
if (!html) return { error: 'ErrorCouldntFetch' };
|
||||
if (html.includes('property="og:video" content="https://va.media.tumblr.com/')) {
|
||||
return { urls: `https://va.media.tumblr.com/${html.split('property="og:video" content="https://va.media.tumblr.com/')[1].split('"/>')[0]}`, audioFilename: `tumblr_${obj.id}_audio` }
|
||||
|
||||
@@ -15,13 +15,13 @@ export default async function(obj) {
|
||||
let req_act = await fetch(`${apiURL}/guest/activate.json`, {
|
||||
method: "POST",
|
||||
headers: _headers
|
||||
}).then(async (r) => { return r.status == 200 ? await r.json() : false;}).catch(() => {return false});
|
||||
}).then(async (r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false});
|
||||
|
||||
if (!req_act) return { error: 'ErrorCouldntFetch' };
|
||||
_headers["x-guest-token"] = req_act["guest_token"];
|
||||
let showURL = `${apiURL}/statuses/show/${obj.id}.json?tweet_mode=extended&include_user_entities=0&trim_user=1&include_entities=0&cards_platform=Web-12&include_cards=1`
|
||||
if (!obj.spaceId) {
|
||||
let req_status = await fetch(showURL, { headers: _headers }).then(async (r) => { return r.status == 200 ? await r.json() : false;}).catch((e) => { return false});
|
||||
let req_status = await fetch(showURL, { headers: _headers }).then(async (r) => { return r.status == 200 ? r.json() : false;}).catch((e) => { return false});
|
||||
if (!req_status) {
|
||||
_headers.authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw";
|
||||
delete _headers["x-guest-token"]
|
||||
@@ -29,11 +29,11 @@ export default async function(obj) {
|
||||
req_act = await fetch(`${apiURL}/guest/activate.json`, {
|
||||
method: "POST",
|
||||
headers: _headers
|
||||
}).then(async (r) => { return r.status == 200 ? await r.json() : false;}).catch(() => {return false});
|
||||
}).then(async (r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false});
|
||||
if (!req_act) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
_headers["x-guest-token"] = req_act["guest_token"];
|
||||
req_status = await fetch(showURL, { headers: _headers }).then(async (r) => { return r.status == 200 ? await r.json() : false;}).catch(() => {return false});
|
||||
req_status = await fetch(showURL, { headers: _headers }).then(async (r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false});
|
||||
}
|
||||
if (!req_status) return { error: 'ErrorCouldntFetch' }
|
||||
if (req_status["extended_entities"] && req_status["extended_entities"]["media"]) {
|
||||
@@ -47,7 +47,7 @@ export default async function(obj) {
|
||||
return { error: 'ErrorNoVideosInTweet' }
|
||||
}
|
||||
if (single) {
|
||||
return { urls: single, audioFilename: `twitter_${obj.id}_audio` }
|
||||
return { urls: single, filename: `twitter_${obj.id}.mp4`, audioFilename: `twitter_${obj.id}_audio` }
|
||||
} else if (multiple) {
|
||||
return { picker: multiple }
|
||||
} else {
|
||||
@@ -64,12 +64,12 @@ export default async function(obj) {
|
||||
}
|
||||
|
||||
let AudioSpaceById = await fetch(`https://twitter.com/i/api/graphql/wJ5g4zf7v8qPHSQbaozYuw/AudioSpaceById?variables=${new URLSearchParams(JSON.stringify(query.variables)).toString().slice(0, -1)}&features=${new URLSearchParams(JSON.stringify(query.features)).toString().slice(0, -1)}`, { headers: _headers }).then(async (r) => {
|
||||
return r.status == 200 ? await r.json() : false;
|
||||
return r.status == 200 ? r.json() : false;
|
||||
}).catch((e) => {return false});
|
||||
|
||||
if (AudioSpaceById) {
|
||||
if (AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) {
|
||||
let streamStatus = await fetch(`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers }).then(async (r) => {return r.status == 200 ? await r.json() : false;}).catch(() => {return false;});
|
||||
let streamStatus = await fetch(`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers }).then(async (r) => {return r.status == 200 ? r.json() : false;}).catch(() => {return false;});
|
||||
if (!streamStatus) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
let participants = AudioSpaceById.data.audioSpace.participants.speakers
|
||||
|
||||
@@ -2,7 +2,7 @@ import { quality, services } from "../config.js";
|
||||
|
||||
export default async function(obj) {
|
||||
try {
|
||||
let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then(async (r) => {return await r.json()}).catch(() => {return false});
|
||||
let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then(async (r) => {return r.json()}).catch(() => {return false});
|
||||
if (!api) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
let downloadType = "";
|
||||
@@ -29,10 +29,10 @@ export default async function(obj) {
|
||||
} catch (e) {
|
||||
best = all[0]
|
||||
}
|
||||
return { urls: best["url"] };
|
||||
return { urls: best["url"], filename: `tumblr_${obj.id}.mp4` };
|
||||
case "dash":
|
||||
let masterJSONURL = api["request"]["files"]["dash"]["cdns"]["akfire_interconnect_quic"]["url"];
|
||||
let masterJSON = await fetch(masterJSONURL).then(async (r) => {return await r.json()}).catch(() => {return false});
|
||||
let masterJSON = await fetch(masterJSONURL).then(async (r) => {return r.json()}).catch(() => {return false});
|
||||
if (!masterJSON) return { error: 'ErrorCouldntFetch' };
|
||||
if (masterJSON.video) {
|
||||
let type = "";
|
||||
|
||||
@@ -7,7 +7,7 @@ export default async function(obj) {
|
||||
let html;
|
||||
html = await fetch(`https://vk.com/video-${obj.userId}_${obj.videoId}`, {
|
||||
headers: {"user-agent": genericUserAgent}
|
||||
}).then(async (r) => {return await r.text()}).catch(() => {return false});
|
||||
}).then(async (r) => {return r.text()}).catch(() => {return false});
|
||||
if (!html) return { error: 'ErrorCouldntFetch' };
|
||||
if (html.includes(`{"lang":`)) {
|
||||
let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]);
|
||||
|
||||
@@ -22,8 +22,9 @@ export function createStream(obj) {
|
||||
exp: exp,
|
||||
isAudioOnly: !!obj.isAudioOnly,
|
||||
audioFormat: obj.audioFormat,
|
||||
time: obj.time,
|
||||
copy: obj.copy,
|
||||
time: obj.time ? obj.time : false,
|
||||
copy: obj.copy ? true : false,
|
||||
mute: obj.mute ? true : false,
|
||||
metadata: obj.fileMetadata ? obj.fileMetadata : false
|
||||
});
|
||||
return `${process.env.selfURL}api/stream?t=${streamUUID}&e=${exp}&h=${ghmac}`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { apiJSON } from "../sub/utils.js";
|
||||
import { verifyStream } from "./manage.js";
|
||||
import { streamAudioOnly, streamDefault, streamLiveRender } from "./types.js";
|
||||
import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly } from "./types.js";
|
||||
|
||||
export default function(res, ip, id, hmac, exp) {
|
||||
try {
|
||||
@@ -13,6 +13,9 @@ export default function(res, ip, id, hmac, exp) {
|
||||
case "render":
|
||||
streamLiveRender(streamInfo, res);
|
||||
break;
|
||||
case "mute":
|
||||
streamVideoOnly(streamInfo, res);
|
||||
break;
|
||||
default:
|
||||
streamDefault(streamInfo, res);
|
||||
break;
|
||||
|
||||
@@ -6,7 +6,9 @@ import { metadataManager, msToTime } from "../sub/utils.js";
|
||||
|
||||
export function streamDefault(streamInfo, res) {
|
||||
try {
|
||||
res.setHeader('Content-disposition', `attachment; filename="${streamInfo.isAudioOnly ? `${streamInfo.filename}.${streamInfo.audioFormat}` : streamInfo.filename}"`);
|
||||
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1]
|
||||
let regFilename = !streamInfo.mute ? streamInfo.filename : `${streamInfo.filename.split('.')[0]}_mute.${format}`
|
||||
res.setHeader('Content-disposition', `attachment; filename="${streamInfo.isAudioOnly ? `${streamInfo.filename}.${streamInfo.audioFormat}` : regFilename}"`);
|
||||
const stream = got.get(streamInfo.urls, {
|
||||
headers: {
|
||||
"user-agent": genericUserAgent
|
||||
@@ -96,3 +98,31 @@ export function streamAudioOnly(streamInfo, res) {
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
export function streamVideoOnly(streamInfo, res) {
|
||||
try {
|
||||
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [
|
||||
'-loglevel', '-8',
|
||||
'-i', streamInfo.urls,
|
||||
'-c', 'copy', '-an'
|
||||
]
|
||||
if (format == "mp4") args.push('-movflags', 'faststart+frag_keyframe+empty_moov')
|
||||
args.push('-f', format, 'pipe:3');
|
||||
const ffmpegProcess = spawn(ffmpeg, args, {
|
||||
windowsHide: true,
|
||||
stdio: [
|
||||
'inherit', 'inherit', 'inherit',
|
||||
'pipe'
|
||||
],
|
||||
});
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename.split('.')[0]}_mute.${format}"`);
|
||||
ffmpegProcess.stdio[3].pipe(res);
|
||||
|
||||
ffmpegProcess.on('error', (err) => {
|
||||
ffmpegProcess.kill();
|
||||
res.end();
|
||||
});
|
||||
} catch (e) {
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ let apiVar = {
|
||||
vQuality: ["max", "hig", "mid", "low", "los"],
|
||||
aFormat: ["best", "mp3", "ogg", "wav", "opus"]
|
||||
},
|
||||
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio"]
|
||||
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted"]
|
||||
}
|
||||
|
||||
export function apiJSON(type, obj) {
|
||||
@@ -95,7 +95,8 @@ export function checkJSONPost(obj) {
|
||||
aFormat: "mp3",
|
||||
isAudioOnly: false,
|
||||
isNoTTWatermark: false,
|
||||
isTTFullAudio: false
|
||||
isTTFullAudio: false,
|
||||
isAudioMuted: false,
|
||||
}
|
||||
try {
|
||||
let objKeys = Object.keys(obj);
|
||||
@@ -106,7 +107,7 @@ export function checkJSONPost(obj) {
|
||||
if (apiVar.booleanOnly.includes(objKeys[i])) {
|
||||
def[objKeys[i]] = obj[objKeys[i]] ? true : false;
|
||||
} else {
|
||||
if (apiVar.allowed[objKeys[i]].includes(obj[objKeys[i]])) def[objKeys[i]] = String(obj[objKeys[i]])
|
||||
if (apiVar.allowed[objKeys[i]] && apiVar.allowed[objKeys[i]].includes(obj[objKeys[i]])) def[objKeys[i]] = String(obj[objKeys[i]])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user