twitter spaces and a ton of improvements

This commit is contained in:
wukko 2022-10-24 19:03:11 +06:00
parent d0801c4d1d
commit c532062aa2
32 changed files with 262 additions and 230 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "cobalt", "name": "cobalt",
"description": "save what you love", "description": "save what you love",
"version": "3.7", "version": "4.0",
"author": "wukko", "author": "wukko",
"exports": "./src/cobalt.js", "exports": "./src/cobalt.js",
"type": "module", "type": "module",
@ -27,7 +27,7 @@
"esbuild": "^0.14.51", "esbuild": "^0.14.51",
"express": "^4.17.1", "express": "^4.17.1",
"express-rate-limit": "^6.3.0", "express-rate-limit": "^6.3.0",
"ffmpeg-static": "^5.0.0", "ffmpeg-static": "^5.1.0",
"got": "^12.1.0", "got": "^12.1.0",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"url-pattern": "1.0.3", "url-pattern": "1.0.3",

View file

@ -67,9 +67,9 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
format: req.query.format ? req.query.format.slice(0, 5) : "webm", format: req.query.format ? req.query.format.slice(0, 5) : "webm",
quality: req.query.quality ? req.query.quality.slice(0, 3) : "max", quality: req.query.quality ? req.query.quality.slice(0, 3) : "max",
audioFormat: req.query.audioFormat ? req.query.audioFormat.slice(0, 4) : false, audioFormat: req.query.audioFormat ? req.query.audioFormat.slice(0, 4) : false,
isAudioOnly: req.query.audio ? true : false, isAudioOnly: !!req.query.audio,
noWatermark: req.query.nw ? true : false, noWatermark: !!req.query.nw,
fullAudio: req.query.ttfull ? true : false, fullAudio: !!req.query.ttfull,
}) })
res.status(j.status).json(j.body); res.status(j.status).json(j.body);
} else { } else {
@ -127,10 +127,8 @@ if (fs.existsSync('./.env') && process.env.selfURL && process.env.streamSalt &&
if (req.header("user-agent") && req.header("user-agent").includes("Trident")) { if (req.header("user-agent") && req.header("user-agent").includes("Trident")) {
if (internetExplorerRedirect.newNT.includes(req.header("user-agent").split('NT ')[1].split(';')[0])) { if (internetExplorerRedirect.newNT.includes(req.header("user-agent").split('NT ')[1].split(';')[0])) {
res.redirect(internetExplorerRedirect.new) res.redirect(internetExplorerRedirect.new)
return
} else { } else {
res.redirect(internetExplorerRedirect.old) res.redirect(internetExplorerRedirect.old)
return
} }
} else { } else {
res.send(renderPage({ res.send(renderPage({

View file

@ -1,7 +1,7 @@
{ {
"streamLifespan": 3600000, "streamLifespan": 3600000,
"maxVideoDuration": 1920000, "maxVideoDuration": 7500000,
"maxAudioDuration": 4200000, "maxAudioDuration": 7500000,
"genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36", "genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
"authorInfo": { "authorInfo": {
"name": "wukko", "name": "wukko",

View file

@ -205,7 +205,7 @@ input[type="checkbox"] {
padding: 0 1.1rem; padding: 0 1.1rem;
font-size: 1rem; font-size: 1rem;
transform: none; transform: none;
line-height: 0rem; line-height: 0;
height: 1.6rem; height: 1.6rem;
margin-top: .4rem; margin-top: .4rem;
} }
@ -369,7 +369,7 @@ input[type="checkbox"] {
#popup-close { #popup-close {
cursor: pointer; cursor: pointer;
float: right; float: right;
right: 0rem; right: 0;
position: absolute; position: absolute;
} }
.settings-category { .settings-category {
@ -399,8 +399,7 @@ input[type="checkbox"] {
flex-direction: row; flex-direction: row;
flex-wrap: nowrap; flex-wrap: nowrap;
align-content: center; align-content: center;
padding: 0.6rem; padding: 0.6rem 1rem 0.6rem 0.6rem;
padding-right: 1rem;
width: auto; width: auto;
margin: 0 0.5rem 0.5rem 0; margin: 0 0.5rem 0.5rem 0;
background: var(--accent-button-bg); background: var(--accent-button-bg);

View file

@ -1,5 +1,5 @@
let isIOS = navigator.userAgent.toLowerCase().match("iphone os"); let isIOS = navigator.userAgent.toLowerCase().match("iphone os");
let version = 13; let version = 14;
let regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/); let regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/);
let notification = `<div class="notification-dot"></div>` let notification = `<div class="notification-dot"></div>`
@ -7,12 +7,12 @@ let switchers = {
"theme": ["auto", "light", "dark"], "theme": ["auto", "light", "dark"],
"ytFormat": ["webm", "mp4"], "ytFormat": ["webm", "mp4"],
"quality": ["max", "hig", "mid", "low"], "quality": ["max", "hig", "mid", "low"],
"audioFormat": ["best", "mp3", "ogg", "wav", "opus"] "defaultAudioFormat": ["mp3", "best", "ogg", "wav", "opus"]
} }
let checkboxes = ["disableTikTokWatermark", "fullTikTokAudio", "disableClipboardButton"]; let checkboxes = ["disableTikTokWatermark", "fullTikTokAudio"];
let exceptions = { // used solely for ios devices, because they're generally less capable let exceptions = { // used solely for ios devices
"ytFormat": "mp4", "ytFormat": "mp4",
"audioFormat": "mp3" "defaultAudioFormat": "mp3"
} }
function eid(id) { function eid(id) {
@ -34,13 +34,13 @@ function vis(state) {
return (state === 1) ? "visible" : "hidden"; return (state === 1) ? "visible" : "hidden";
} }
function opposite(state) { function opposite(state) {
return state == "true" ? "false" : "true"; return state === "true" ? "false" : "true";
} }
function changeDownloadButton(action, text) { function changeDownloadButton(action, text) {
switch (action) { switch (action) {
case 0: case 0:
eid("download-button").disabled = true eid("download-button").disabled = true
if (sGet("alwaysVisibleButton") == "true") { if (sGet("alwaysVisibleButton") === "true") {
eid("download-button").value = text eid("download-button").value = text
eid("download-button").style.padding = '0 1rem' eid("download-button").style.padding = '0 1rem'
} else { } else {
@ -61,7 +61,7 @@ function changeDownloadButton(action, text) {
} }
} }
document.addEventListener("keydown", (event) => { document.addEventListener("keydown", (event) => {
if (event.key == "Tab") { if (event.key === "Tab") {
eid("download-button").value = '>>' eid("download-button").value = '>>'
eid("download-button").style.padding = '0 1rem' eid("download-button").style.padding = '0 1rem'
} }
@ -106,8 +106,8 @@ function changeTab(evnt, tabId, tabClass) {
} }
eid(tabId).style.display = "block"; eid(tabId).style.display = "block";
evnt.currentTarget.dataset.enabled = "true"; evnt.currentTarget.dataset.enabled = "true";
if (tabId == "tab-about-changelog" && sGet("changelogStatus") != `${version}`) notificationCheck("changelog"); if (tabId === "tab-about-changelog" && sGet("changelogStatus") !== `${version}`) notificationCheck("changelog");
if (tabId == "tab-about-about" && !sGet("seenAbout")) notificationCheck("about"); if (tabId === "tab-about-about" && !sGet("seenAbout")) notificationCheck("about");
} }
function notificationCheck(type) { function notificationCheck(type) {
let changed = true; let changed = true;
@ -122,15 +122,15 @@ function notificationCheck(type) {
changed = false; changed = false;
break; break;
} }
if (changed && sGet("changelogStatus") == `${version}` || type == "disable") { if (changed && sGet("changelogStatus") === `${version}` || type === "disable") {
setTimeout(() => { setTimeout(() => {
eid("about-footer").innerHTML = eid("about-footer").innerHTML.replace(notification, ''); eid("about-footer").innerHTML = eid("about-footer").innerHTML.replace(notification, '');
eid("tab-button-about-changelog").innerHTML = eid("tab-button-about-changelog").innerHTML.replace(notification, '') eid("tab-button-about-changelog").innerHTML = eid("tab-button-about-changelog").innerHTML.replace(notification, '')
}, 900) }, 900)
} }
if (sGet("disableChangelog") != "true") { if (sGet("disableChangelog") !== "true") {
if (!sGet("seenAbout") && !eid("about-footer").innerHTML.includes(notification)) eid("about-footer").innerHTML = `${notification}${eid("about-footer").innerHTML}`; if (!sGet("seenAbout") && !eid("about-footer").innerHTML.includes(notification)) eid("about-footer").innerHTML = `${notification}${eid("about-footer").innerHTML}`;
if (sGet("changelogStatus") != `${version}`) { if (sGet("changelogStatus") !== `${version}`) {
if (!eid("about-footer").innerHTML.includes(notification)) eid("about-footer").innerHTML = `${notification}${eid("about-footer").innerHTML}`; if (!eid("about-footer").innerHTML.includes(notification)) eid("about-footer").innerHTML = `${notification}${eid("about-footer").innerHTML}`;
if (!eid("tab-button-about-changelog").innerHTML.includes(notification)) eid("tab-button-about-changelog").innerHTML = `${notification}${eid("tab-button-about-changelog").innerHTML}`; if (!eid("tab-button-about-changelog").innerHTML.includes(notification)) eid("tab-button-about-changelog").innerHTML = `${notification}${eid("tab-button-about-changelog").innerHTML}`;
} }
@ -143,10 +143,11 @@ function hideAllPopups() {
} }
eid("picker-holder").innerHTML = ''; eid("picker-holder").innerHTML = '';
eid("picker-download").href = '/'; eid("picker-download").href = '/';
eid("picker-download").style.visibility = "hidden";
eid("popup-backdrop").style.visibility = "hidden"; eid("popup-backdrop").style.visibility = "hidden";
} }
function popup(type, action, text) { function popup(type, action, text) {
if (action == 1) { if (action === 1) {
hideAllPopups(); // hide the previous popup before showing a new one hideAllPopups(); // hide the previous popup before showing a new one
switch (type) { switch (type) {
case "about": case "about":
@ -198,8 +199,9 @@ function popup(type, action, text) {
break; break;
} }
} else { } else {
if (type == "picker") { if (type === "picker") {
eid("picker-download").href = '/'; eid("picker-download").href = '/';
eid("picker-download").style.visibility = "hidden"
eid("picker-holder").innerHTML = '' eid("picker-holder").innerHTML = ''
} }
} }
@ -210,15 +212,15 @@ function changeSwitcher(li, b) {
if (b) { if (b) {
sSet(li, b); sSet(li, b);
for (let i in switchers[li]) { for (let i in switchers[li]) {
(switchers[li][i] == b) ? enable(`${li}-${b}`) : disable(`${li}-${switchers[li][i]}`) (switchers[li][i] === b) ? enable(`${li}-${b}`) : disable(`${li}-${switchers[li][i]}`)
} }
if (li == "theme") detectColorScheme(); if (li === "theme") detectColorScheme();
} else { } else {
let pref = switchers[li][0]; let pref = switchers[li][0];
if (isIOS && exceptions[li]) pref = exceptions[li]; if (isIOS && exceptions[li]) pref = exceptions[li];
sSet(li, pref); sSet(li, pref);
for (let i in switchers[li]) { for (let i in switchers[li]) {
(switchers[li][i] == pref) ? enable(`${li}-${pref}`) : disable(`${li}-${switchers[li][i]}`) (switchers[li][i] === pref) ? enable(`${li}-${pref}`) : disable(`${li}-${switchers[li][i]}`)
} }
} }
} }
@ -230,14 +232,12 @@ function internetError() {
function checkbox(action) { function checkbox(action) {
if (eid(action).checked) { if (eid(action).checked) {
sSet(action, "true"); sSet(action, "true");
if (action == "alwaysVisibleButton") button(); if (action === "alwaysVisibleButton") button();
if (action == "disableClipboardButton") eid("pasteFromClipboard").style.display = "none";
} else { } else {
sSet(action, "false"); sSet(action, "false");
if (action == "alwaysVisibleButton") button(); if (action === "alwaysVisibleButton") button();
if (action == "disableClipboardButton") eid("pasteFromClipboard").style.display = "flex";
} }
sGet(action) == "true" ? notificationCheck("disable") : notificationCheck(); sGet(action) === "true" ? notificationCheck("disable") : notificationCheck();
} }
function updateToggle(toggl, state) { function updateToggle(toggl, state) {
switch(state) { switch(state) {
@ -253,7 +253,7 @@ function toggle(toggl) {
let state = sGet(toggl); let state = sGet(toggl);
if (state) { if (state) {
sSet(toggl, opposite(state)) sSet(toggl, opposite(state))
if (opposite(state) == "true") sSet(`${toggl}ToggledOnce`, "true"); if (opposite(state) === "true") sSet(`${toggl}ToggledOnce`, "true");
} else { } else {
sSet(toggl, "false") sSet(toggl, "false")
} }
@ -263,23 +263,21 @@ function loadSettings() {
try { try {
if (typeof(navigator.clipboard.readText) == "undefined") throw new Error(); if (typeof(navigator.clipboard.readText) == "undefined") throw new Error();
} catch (err) { } catch (err) {
eid("disableClipboardButton-chkbx").style.display = "none"; eid("pasteFromClipboard").style.display = "none"
sSet("disableClipboardButton", "true")
} }
if (sGet("disableClipboardButton") == "true") eid("pasteFromClipboard").style.display = "none"; if (sGet("alwaysVisibleButton") === "true") {
if (sGet("alwaysVisibleButton") == "true") {
eid("alwaysVisibleButton").checked = true; eid("alwaysVisibleButton").checked = true;
eid("download-button").value = '>>' eid("download-button").value = '>>'
eid("download-button").style.padding = '0 1rem'; eid("download-button").style.padding = '0 1rem';
} }
if (sGet("downloadPopup") == "true" && !isIOS) { if (sGet("downloadPopup") === "true" && !isIOS) {
eid("downloadPopup").checked = true; eid("downloadPopup").checked = true;
} }
if (!sGet("audioMode")) { if (!sGet("audioMode")) {
toggle("audioMode") toggle("audioMode")
} }
for (let i = 0; i < checkboxes.length; i++) { for (let i = 0; i < checkboxes.length; i++) {
if (sGet(checkboxes[i]) == "true") eid(checkboxes[i]).checked = true; if (sGet(checkboxes[i]) === "true") eid(checkboxes[i]).checked = true;
} }
updateToggle("audioMode", sGet("audioMode")); updateToggle("audioMode", sGet("audioMode"));
for (let i in switchers) { for (let i in switchers) {
@ -314,26 +312,26 @@ async function download(url) {
eid("url-input-area").disabled = true; eid("url-input-area").disabled = true;
let audioMode = sGet("audioMode"); let audioMode = sGet("audioMode");
let format = ``; let format = ``;
if (audioMode == "false") { if (audioMode === "false") {
if (url.includes("youtube.com/") || url.includes("/youtu.be/")) { if (url.includes("youtube.com/") || url.includes("/youtu.be/")) {
format = `&format=${sGet("ytFormat")}` format = `&format=${sGet("ytFormat")}`
} else if ((url.includes("tiktok.com/") || url.includes("douyin.com/")) && sGet("disableTikTokWatermark") == "true") { } else if ((url.includes("tiktok.com/") || url.includes("douyin.com/")) && sGet("disableTikTokWatermark") === "true") {
format = `&nw=true` format = `&nw=true`
} }
} else { } else {
format = `&nw=true` format = `&nw=true`
if (sGet("fullTikTokAudio") == "true") format += `&ttfull=true` if (sGet("fullTikTokAudio") === "true") format += `&ttfull=true`
} }
let mode = (sGet("audioMode") == "true") ? `audio=true` : `quality=${sGet("quality")}` let mode = (sGet("audioMode") === "true") ? `audio=true` : `quality=${sGet("quality")}`
await fetch(`/api/json?audioFormat=${sGet("audioFormat")}&${mode}${format}&url=${encodeURIComponent(url)}`).then(async (r) => { await fetch(`/api/json?audioFormat=${sGet("defaultAudioFormat")}&${mode}${format}&url=${encodeURIComponent(url)}`).then(async (r) => {
let j = await r.json(); let j = await r.json();
if (j.status != "error" && j.status != "rate-limit") { if (j.status !== "error" && j.status !== "rate-limit") {
if (j.url) { if (j.url) {
switch (j.status) { switch (j.status) {
case "redirect": case "redirect":
changeDownloadButton(2, '>>>'); changeDownloadButton(2, '>>>');
setTimeout(() => { changeButton(1); }, 3000); setTimeout(() => { changeButton(1); }, 3000);
sGet("downloadPopup") == "true" ? popup('download', 1, j.url) : window.open(j.url, '_blank'); sGet("downloadPopup") === "true" ? popup('download', 1, j.url) : window.open(j.url, '_blank');
break; break;
case "picker": case "picker":
if (j.audio && j.url) { if (j.audio && j.url) {
@ -385,7 +383,7 @@ async function loadOnDemand(elementId, blockId) {
eid(elementId).innerHTML = "..." eid(elementId).innerHTML = "..."
await fetch(`/api/onDemand?blockId=${blockId}`).then(async (r) => { await fetch(`/api/onDemand?blockId=${blockId}`).then(async (r) => {
let j = await r.json(); let j = await r.json();
if (j.status == "success" && j.status != "rate-limit") { if (j.status === "success" && j.status !== "rate-limit") {
if (j.text) { if (j.text) {
eid(elementId).innerHTML = j.text; eid(elementId).innerHTML = j.text;
} else { } else {
@ -422,6 +420,6 @@ eid("url-input-area").addEventListener("keyup", (event) => {
if (event.key === 'Enter') eid("download-button").click(); if (event.key === 'Enter') eid("download-button").click();
}) })
document.onkeydown = (event) => { document.onkeydown = (event) => {
if (event.key == "Tab" || event.ctrlKey) eid("url-input-area").focus(); if (event.key === "Tab" || event.ctrlKey) eid("url-input-area").focus();
if (event.key === 'Escape') hideAllPopups(); if (event.key === 'Escape') hideAllPopups();
}; };

View file

@ -86,7 +86,7 @@
"SettingsAudioFullTikTok": "download full audio", "SettingsAudioFullTikTok": "download full audio",
"SettingsAudioFullTikTokDescription": "downloads original audio or sound used in video without any additional changes by the video author.", "SettingsAudioFullTikTokDescription": "downloads original audio or sound used in video without any additional changes by the video author.",
"ErrorCantGetID": "i couldn't get the full info from the shortened link. make sure it works or try a full one.", "ErrorCantGetID": "i couldn't get the full info from the shortened link. make sure it works or try a full one.",
"ErrorNoVideosInTweet": "this tweet doesn't have videos or gifs. try another one!", "ErrorNoVideosInTweet": "i couldn't find any videos or gifs in this tweet. try another one!",
"ImagePickerTitle": "pick images to download", "ImagePickerTitle": "pick images to download",
"ImagePickerDownloadAudio": "download audio", "ImagePickerDownloadAudio": "download audio",
"ImagePickerExplanationPC": "right click an image to save it.", "ImagePickerExplanationPC": "right click an image to save it.",
@ -94,7 +94,6 @@
"ErrorNoUrlReturned": "server didn't return a download link. this should never happen. reload the page and try again, but if it doesn't help, {ContactLink}.", "ErrorNoUrlReturned": "server didn't return a download link. this should never happen. reload the page and try again, but if it doesn't help, {ContactLink}.",
"ErrorUnknownStatus": "i received a response i can't process. most likely something with status is wrong. this should never happen. reload the page and try again, but if it doesn't help, {ContactLink}.", "ErrorUnknownStatus": "i received a response i can't process. most likely something with status is wrong. this should never happen. reload the page and try again, but if it doesn't help, {ContactLink}.",
"PasteFromClipboard": "paste from clipboard", "PasteFromClipboard": "paste from clipboard",
"SettingsDisableClipboard": "hide clipboard button",
"FollowTwitter": "follow {appName}'s twitter account for polls, updates, and more: <a class=\"text-backdrop\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a>", "FollowTwitter": "follow {appName}'s twitter account for polls, updates, and more: <a class=\"text-backdrop\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a>",
"ChangelogOlder": "previous updates", "ChangelogOlder": "previous updates",
"ChangelogPressToExpand": "press to load", "ChangelogPressToExpand": "press to load",
@ -105,6 +104,7 @@
"MediaPickerTitle": "pick what to save", "MediaPickerTitle": "pick what to save",
"MediaPickerExplanationPC": "click or right click to download what you want.", "MediaPickerExplanationPC": "click or right click to download what you want.",
"MediaPickerExplanationPhone": "press or press and hold to download what you want.", "MediaPickerExplanationPhone": "press or press and hold to download what you want.",
"MediaPickerExplanationPhoneIOS": "press and hold, hide the preview, and then select \"download linked file\" to save." "MediaPickerExplanationPhoneIOS": "press and hold, hide the preview, and then select \"download linked file\" to save.",
"TwitterSpaceWasntRecorded": "this twitter space wasn't recorded, so there's nothing to download. try another one!"
} }
} }

View file

@ -89,7 +89,7 @@
"SettingsAudioFullTikTok": "скачивать полное аудио", "SettingsAudioFullTikTok": "скачивать полное аудио",
"SettingsAudioFullTikTokDescription": "скачивает оригинальный звук, который использован в видео, без каких-либо изменений от автора видео.", "SettingsAudioFullTikTokDescription": "скачивает оригинальный звук, который использован в видео, без каких-либо изменений от автора видео.",
"ErrorCantGetID": "у меня не получилось достать инфу по этой короткой ссылке. попробуй полную ссылку, или же попробуй позже.", "ErrorCantGetID": "у меня не получилось достать инфу по этой короткой ссылке. попробуй полную ссылку, или же попробуй позже.",
"ErrorNoVideosInTweet": "у этого твита нет ни видео, ни гифок. попробуй другой!", "ErrorNoVideosInTweet": "в этом твите нет ни видео, ни гифок. попробуй другой!",
"ImagePickerTitle": "выбери картинки для скачивания", "ImagePickerTitle": "выбери картинки для скачивания",
"ImagePickerDownloadAudio": "скачать аудио", "ImagePickerDownloadAudio": "скачать аудио",
"ImagePickerExplanationPC": "нажми правой кнопкой мыши на изображение, чтобы его сохранить.", "ImagePickerExplanationPC": "нажми правой кнопкой мыши на изображение, чтобы его сохранить.",
@ -97,7 +97,6 @@
"ErrorNoUrlReturned": "я не получил ссылку для скачивания от сервера. такого происходить не должно. перезагрузи страницу, а если не поможет, то {ContactLink}.", "ErrorNoUrlReturned": "я не получил ссылку для скачивания от сервера. такого происходить не должно. перезагрузи страницу, а если не поможет, то {ContactLink}.",
"ErrorUnknownStatus": "сервер ответил мне чем-то непонятным. такого происходить не должно. перезагрузи страницу, а если не поможет, то {ContactLink}.", "ErrorUnknownStatus": "сервер ответил мне чем-то непонятным. такого происходить не должно. перезагрузи страницу, а если не поможет, то {ContactLink}.",
"PasteFromClipboard": "вставить из буфера обмена", "PasteFromClipboard": "вставить из буфера обмена",
"SettingsDisableClipboard": "скрыть кнопку буфера обмена",
"FollowTwitter": "а ещё, в твиттере {appName} есть опросы, новости, и многое другое: <a class=\"text-backdrop\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a>", "FollowTwitter": "а ещё, в твиттере {appName} есть опросы, новости, и многое другое: <a class=\"text-backdrop\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a>",
"ChangelogOlder": "предыдущие обновления (на английском)", "ChangelogOlder": "предыдущие обновления (на английском)",
"ChangelogPressToExpand": "нажми, чтобы загрузить", "ChangelogPressToExpand": "нажми, чтобы загрузить",
@ -108,6 +107,7 @@
"MediaPickerTitle": "выбери, что сохранить", "MediaPickerTitle": "выбери, что сохранить",
"MediaPickerExplanationPC": "кликни, чтобы скачать. также можно скачать через контекстное меню правой кнопки мыши.", "MediaPickerExplanationPC": "кликни, чтобы скачать. также можно скачать через контекстное меню правой кнопки мыши.",
"MediaPickerExplanationPhone": "нажми, или нажми и удерживай, чтобы скачать.", "MediaPickerExplanationPhone": "нажми, или нажми и удерживай, чтобы скачать.",
"MediaPickerExplanationPhoneIOS": "нажми и удерживай, затем скрой превью, и наконец выбери \"загрузить файл по ссылке\"." "MediaPickerExplanationPhoneIOS": "нажми и удерживай, затем скрой превью, и наконец выбери \"загрузить файл по ссылке\".",
"TwitterSpaceWasntRecorded": "this twitter space wasn't recorded, so there's nothing to download. try another one!"
} }
} }

View file

@ -14,15 +14,15 @@ export async function getJSON(originalURL, lang, obj) {
let hostname = url.replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."), let hostname = url.replace("https://", "").replace(' ', '').split('&')[0].split("/")[0].split("."),
host = hostname[hostname.length - 2], host = hostname[hostname.length - 2],
patternMatch; patternMatch;
if (host == "youtu") { if (host === "youtu") {
host = "youtube"; host = "youtube";
url = `https://youtube.com/watch?v=${url.replace("youtu.be/", "").replace("https://", "")}`; url = `https://youtube.com/watch?v=${url.replace("youtu.be/", "").replace("https://", "")}`;
} }
if (host == "goo" && url.substring(0, 30) == "https://soundcloud.app.goo.gl/") { if (host === "goo" && url.substring(0, 30) === "https://soundcloud.app.goo.gl/") {
host = "soundcloud" host = "soundcloud"
url = `https://soundcloud.com/${url.replace("https://soundcloud.app.goo.gl/", "").split('/')[0]}` url = `https://soundcloud.com/${url.replace("https://soundcloud.app.goo.gl/", "").split('/')[0]}`
} }
if (host == "tumblr" && !url.includes("blog/view")) { if (host === "tumblr" && !url.includes("blog/view")) {
if (url.slice(-1) == '/') url = url.slice(0, -1); if (url.slice(-1) == '/') url = url.slice(0, -1);
url = url.replace(url.split('/')[5], ''); url = url.replace(url.split('/')[5], '');
} }

View file

@ -1,22 +1,32 @@
{ {
"current": { "current": {
"title": "support for multi media tweets is here! (3.7)", "version": "4.0",
"content": "{appName} now lets you save any of the videos or gifs in a tweet. even if there are many of them.\n\nsimply paste a link like you'd usually do and {appName} will ask what exactly you want to save.\n\nFIREFOX USERS: if you have strict tracking protection on, you might wanna turn it off for {appName}, or else twitter video previews won't load. firefox filters out twitter image cdn as if it was a tracker, which it's not. it's a false-positive.\n\nhowever, you can leave it on if you're fine with blank squares and video numbers. i have thought of that in prior, you're welcome.\n\nother changes:\n- repurposed ex tiktok-only image picker to be dynamic and adapt depending on content to pick. that's exactly how twitter multi media downloads work.\n- cobalt is now properly viewable on phones with tiny screens, such as first gen iphone se.\n- scrollbars now should be visible only where they're needed.\n- brought back proper twitter api, because other one doesn't have multi media stuff (at least yet).\n- cleaned up some internal files, including main frontend js file.\n- reorganized some files in project directory, now you won't get lost when contributing or just looking through cobalt's code." "title": "better and faster than ever",
"content": "this update has a ton of improvements and new features.\n\nchanges you probably care about:\n- cobalt now has support for recorded twitter spaces! download the previous conversation no matter how long it was.\n- download speeds from youtube are at least 10 times better now. you're welcome.\n- both video and audio length limits have been extended to 2 hours.\n- audio downloads from youtube, youtube music, twitter spaces, and soundcloud now have metadata! most often it's just title and artist, but when cobalt is able to get more info, it adds that metadata too.\n- tiktok downloads have been fixed, yet again, and if they ever break in the future, cobalt will fall back to downloading a less annoyingly watermarked video.\n- soundcloud downloads have been fixed, too.\n- you now can save videos from tweets that include explicit content.\n\nless notable changes:\n- currently experimenting with using mp3 as default audio format. if you set something other than mp3 before, it'll be set to mp3. you can always change it back in settings. let me know what you think about this.\n- \"download audio\" button from image picker no longer stays on the screen after popup was closed.\n- clipboard button now shows up depending on your browser's support for it.\n- you can no longer manually hide the clipboard button, 'cause it's unnecessary.\n- small internal improvements such as separation of changelog version and title.\n- fair bit of internal clean up.\n\nif you want to help me implement covers for downloaded audios, <a class=\"text-backdrop\" href=\"https://github.com/wukko/cobalt\" target=\"_blank\">you can do it on github</a>.\n\nfun fact: average cobalt user is 10 times cooler than everyone else."
}, },
"history": [{ "history": [{
"title": "less disturbance (3.6.2 + 3.6.3)", "version": "3.7",
"title": "support for multi media tweets is here!",
"content": "{appName} now lets you save any of the videos or gifs in a tweet. even if there are many of them.\n\nsimply paste a link like you'd usually do and {appName} will ask what exactly you want to save.\n\nFIREFOX USERS: if you have strict tracking protection on, you might wanna turn it off for {appName}, or else twitter video previews won't load. firefox filters out twitter image cdn as if it was a tracker, which it's not. it's a false-positive.\n\nhowever, you can leave it on if you're fine with blank squares and video numbers. i have thought of that in prior, you're welcome.\n\nother changes:\n- repurposed ex tiktok-only image picker to be dynamic and adapt depending on content to pick. that's exactly how twitter multi media downloads work.\n- cobalt is now properly viewable on phones with tiny screens, such as first gen iphone se.\n- scrollbars now should be visible only where they're needed.\n- brought back proper twitter api, because other one doesn't have multi media stuff (at least yet).\n- cleaned up some internal files, including main frontend js file.\n- reorganized some files in project directory, now you won't get lost when contributing or just looking through cobalt's code."
}, {
"version": "3.6.2 + 3.6.3",
"title": "less disturbance",
"content": "changelog popup no longer annoys you after a major update! this action has been replaced with a notification dot. if you see a red dot, then there's something new.\n\nyour old setting that disabled the changelog popup now applies to notifications.\n\nnew users will see a notification dot instead of an about popup, too. this was mostly done to prevent complications if your browser is set up to clean local storage when you close it.\n\nother changes:\n- popups are now a bit wider, just so more content fits at once.\n- better interface scaling.\n- code is a bit cleaner now.\n- changed twitter api endpoint. there should no longer be any rate limits." "content": "changelog popup no longer annoys you after a major update! this action has been replaced with a notification dot. if you see a red dot, then there's something new.\n\nyour old setting that disabled the changelog popup now applies to notifications.\n\nnew users will see a notification dot instead of an about popup, too. this was mostly done to prevent complications if your browser is set up to clean local storage when you close it.\n\nother changes:\n- popups are now a bit wider, just so more content fits at once.\n- better interface scaling.\n- code is a bit cleaner now.\n- changed twitter api endpoint. there should no longer be any rate limits."
}, { }, {
"title": "improvements all around! (3.6)", "version": "3.6",
"title": "improvements all around!",
"content": "- download mode switcher is moving places, it's now right next to link input area.\n- smart mode has been renamed to auto mode, because this name is easier to understand.\n- all spacings in ui have been evened out. no more eye strain.\n- added support for twitter /video/1 links\n- clipboard button exception has been redone to prepare for adoption of readtext clipboard api in firefox.\n- cobalt is now using different tiktok api endpoint, because previous one got killed, just like the one before.\n- \"other\" settings tab has been cleaned up." "content": "- download mode switcher is moving places, it's now right next to link input area.\n- smart mode has been renamed to auto mode, because this name is easier to understand.\n- all spacings in ui have been evened out. no more eye strain.\n- added support for twitter /video/1 links\n- clipboard button exception has been redone to prepare for adoption of readtext clipboard api in firefox.\n- cobalt is now using different tiktok api endpoint, because previous one got killed, just like the one before.\n- \"other\" settings tab has been cleaned up."
}, { }, {
"title": "tiktok support is back :D (3.5.4)", "version": "3.5.4",
"title": "tiktok support is back :D",
"content": "you can download videos, sounds, and images from tiktok again!\nhuge thank you to <a class=\"text-backdrop\" href=\"https://github.com/minzique\" target=\"_blank\">@minzique</a> for finding another api endpoint that works." "content": "you can download videos, sounds, and images from tiktok again!\nhuge thank you to <a class=\"text-backdrop\" href=\"https://github.com/minzique\" target=\"_blank\">@minzique</a> for finding another api endpoint that works."
}, { }, {
"title": "vk clips support, improved changelog system, and less bugs (3.5.2)", "version": "3.5.2",
"title": "vk clips support, improved changelog system, and less bugs",
"content": "new features: \n- added support for vk clips. {appName} now lets you download even more cringy videos!\n- added update history right to the changelog menu. it's not loaded by default to minimize page load time, but can be loaded upon pressing a button. probably someone will enjoy this.\n- as you've just read, cobalt now has on-demand blocks. they're rendered on server upon request and exist to prevent any unnecessary clutter by default. the first feature to use on-demand rendering is history of updates in changelog tab.\n\nchanges:\n- moved twitter entry to about tab and made it localized.\n- added clarity to what services exactly are supported in about tab.\n\nbug fixes:\n- cobalt should no longer crash to firefox users if they love to play around with user-agent switching.\n- vk videos of any resolution and aspect ratio should now be downloadable.\n- vk quality picking has been fixed after vk broke it for parsers on their side." "content": "new features: \n- added support for vk clips. {appName} now lets you download even more cringy videos!\n- added update history right to the changelog menu. it's not loaded by default to minimize page load time, but can be loaded upon pressing a button. probably someone will enjoy this.\n- as you've just read, cobalt now has on-demand blocks. they're rendered on server upon request and exist to prevent any unnecessary clutter by default. the first feature to use on-demand rendering is history of updates in changelog tab.\n\nchanges:\n- moved twitter entry to about tab and made it localized.\n- added clarity to what services exactly are supported in about tab.\n\nbug fixes:\n- cobalt should no longer crash to firefox users if they love to play around with user-agent switching.\n- vk videos of any resolution and aspect ratio should now be downloadable.\n- vk quality picking has been fixed after vk broke it for parsers on their side."
}, { }, {
"title": "ui revamp and usability imporvements (3.5)", "version": "3.5",
"title": "ui revamp and usability imporvements",
"content": "new features:\n- {appName} now lets you paste the link in your clipboard and download the file in a single press of a button.if your clipboard's latest content isn't a valid url, {appName} won't process or paste it. you can also hide the clipboard button in settings if you want to.\nunfortunately, the clipboard feature is not available to firefox users because mozilla didn't add proper support for clipboard api.\n- there's now a button to quickly clean the input area, right next to download button. it's really useful in case when you want to quickly save a bunch of videos and don't want to bother selecting text.\n- keyboard shortcuts! you love them, i love them, and now we can use them to perform quick actions in {appName}. use ctrl+v combo to paste the link without focusing the input area; press escape key to close the active popup or clean the input area; and if you didn't know, you can also press enter to download content from the link.\n\nnew looks:\n- main box has been revamped. it has lost its border, thick padding, and now feels light and fresh.\n- download button is now prettier, and has been tuned to make >> look just like the logo.\n- buttons on the bottom now actually look like buttons and are way more descriptive. no more #@+?$ bullshit. it's way easier to see and understand what each of them does.\n- bottom buttons are prettier and easier to use on a phone. they're bigger and stretch out to sides, making them easier to press.\n\nfixes:\n- it's now impossible to overlap multiple popups at once. no more mess if you decide to explore popups while waiting for request to process.\n- popup tabs have been slightly moved down to prevent popup content overlapping.\n- ui scalability has been improved." "content": "new features:\n- {appName} now lets you paste the link in your clipboard and download the file in a single press of a button.if your clipboard's latest content isn't a valid url, {appName} won't process or paste it. you can also hide the clipboard button in settings if you want to.\nunfortunately, the clipboard feature is not available to firefox users because mozilla didn't add proper support for clipboard api.\n- there's now a button to quickly clean the input area, right next to download button. it's really useful in case when you want to quickly save a bunch of videos and don't want to bother selecting text.\n- keyboard shortcuts! you love them, i love them, and now we can use them to perform quick actions in {appName}. use ctrl+v combo to paste the link without focusing the input area; press escape key to close the active popup or clean the input area; and if you didn't know, you can also press enter to download content from the link.\n\nnew looks:\n- main box has been revamped. it has lost its border, thick padding, and now feels light and fresh.\n- download button is now prettier, and has been tuned to make >> look just like the logo.\n- buttons on the bottom now actually look like buttons and are way more descriptive. no more #@+?$ bullshit. it's way easier to see and understand what each of them does.\n- bottom buttons are prettier and easier to use on a phone. they're bigger and stretch out to sides, making them easier to press.\n\nfixes:\n- it's now impossible to overlap multiple popups at once. no more mess if you decide to explore popups while waiting for request to process.\n- popup tabs have been slightly moved down to prevent popup content overlapping.\n- ui scalability has been improved."
}] }]
} }

View file

@ -7,14 +7,15 @@ export default function(string) {
try { try {
switch (string) { switch (string) {
case "title": case "title":
return replaceBase(changelog["current"]["title"]); return `${replaceBase(changelog["current"]["title"])} (${changelog["current"]["version"]})` ;
case "content": case "content":
return replaceBase(changelog["current"]["content"]); return replaceBase(changelog["current"]["content"]);
case "history": case "history":
return changelog["history"].map((i) => { return changelog["history"].map((i) => {
return { return {
title: replaceBase(i["title"]), title: replaceBase(i["title"]),
content: replaceBase(i["content"]) content: replaceBase(i["content"]),
version: i["version"],
} }
}); });
default: default:

View file

@ -20,7 +20,7 @@ let sizing = {
} }
export default function(emoji, size, disablePadding) { export default function(emoji, size, disablePadding) {
if (!size) size = 22; if (!size) size = 22;
let padding = size != 22 ? `margin-right:${sizing[size] ? sizing[size] : "0.4"}rem;` : ``; let padding = size !== 22 ? `margin-right:${sizing[size] ? sizing[size] : "0.4"}rem;` : ``;
if (disablePadding) padding = 'margin-right:0!important;'; if (disablePadding) padding = 'margin-right:0!important;';
if (!names[emoji]) emoji = "❓"; if (!names[emoji]) emoji = "❓";
return `<img class="emoji" height="${size}" width="${size}" style="${padding}" alt="${emoji}" src="emoji/${names[emoji]}.svg">` return `<img class="emoji" height="${size}" width="${size}" style="${padding}" alt="${emoji}" src="emoji/${names[emoji]}.svg">`

View file

@ -46,7 +46,7 @@ export function popup(obj) {
for (let i = 0; i < obj.body.length; i++) { for (let i = 0; i < obj.body.length; i++) {
if (obj.body[i]["text"].length > 0) { if (obj.body[i]["text"].length > 0) {
classes = obj.body[i]["classes"] ? obj.body[i]["classes"] : [] classes = obj.body[i]["classes"] ? obj.body[i]["classes"] : []
if (i != obj.body.length - 1 && !obj.body[i]["nopadding"]) { if (i !== obj.body.length - 1 && !obj.body[i]["nopadding"]) {
classes.push("desc-padding") classes.push("desc-padding")
} }
body += obj.body[i]["raw"] ? obj.body[i]["text"] : `<div id="popup-desc" class="${classes.length > 0 ? classes.join(' ') : ''}">${obj.body[i]["text"]}</div>` body += obj.body[i]["raw"] ? obj.body[i]["text"] : `<div id="popup-desc" class="${classes.length > 0 ? classes.join(' ') : ''}">${obj.body[i]["text"]}</div>`

View file

@ -5,7 +5,7 @@ export function changelogHistory() { // blockId 0
let render = ``; let render = ``;
for (let i in history) { for (let i in history) {
render += `<div id="popup-desc" class="changelog-subtitle">${history[i]["title"]}</div><div id="popup-desc" class="desc-padding">${history[i]["content"]}</div>` render += `<div id="popup-desc" class="changelog-subtitle">${history[i]["title"]} (${history[i]["version"]})</div><div id="popup-desc" class="desc-padding">${history[i]["content"]}</div>`
} }
return render; return render;
} }

View file

@ -84,7 +84,7 @@ export default function(obj) {
}, { }, {
text: `${loc(obj.lang, 'AboutSupportedServices')} ${enabledServices}.` text: `${loc(obj.lang, 'AboutSupportedServices')} ${enabledServices}.`
}, { }, {
text: obj.lang != "ru" ? loc(obj.lang, 'FollowTwitter') : "" text: obj.lang !== "ru" ? loc(obj.lang, 'FollowTwitter') : ""
}, { }, {
text: backdropLink(repo, loc(obj.lang, 'LinkGitHubIssues')), text: backdropLink(repo, loc(obj.lang, 'LinkGitHubIssues')),
classes: ["bottom-link"] classes: ["bottom-link"]
@ -216,7 +216,7 @@ export default function(obj) {
name: "general", name: "general",
title: loc(obj.lang, 'SettingsAudioTab'), title: loc(obj.lang, 'SettingsAudioTab'),
body: switcher({ body: switcher({
name: "audioFormat", name: "defaultAudioFormat",
subtitle: loc(obj.lang, 'SettingsFormatSubtitle'), subtitle: loc(obj.lang, 'SettingsFormatSubtitle'),
explanation: loc(obj.lang, 'SettingsAudioFormatDescription'), explanation: loc(obj.lang, 'SettingsAudioFormatDescription'),
items: audioFormats items: audioFormats
@ -250,7 +250,6 @@ export default function(obj) {
name: "miscellaneous", name: "miscellaneous",
title: loc(obj.lang, 'Miscellaneous'), title: loc(obj.lang, 'Miscellaneous'),
body: checkbox("disableChangelog", loc(obj.lang, 'SettingsDisableNotifications')) body: checkbox("disableChangelog", loc(obj.lang, 'SettingsDisableNotifications'))
+ checkbox("disableClipboardButton", loc(obj.lang, 'SettingsDisableClipboard'))
}) })
}], }],
})} })}

View file

@ -23,9 +23,11 @@ export default async function (host, patternMatch, url, lang, obj) {
switch (host) { switch (host) {
case "twitter": case "twitter":
r = await twitter({ r = await twitter({
id: patternMatch["id"], id: patternMatch["id"] ? patternMatch["id"] : false,
spaceId: patternMatch["spaceId"] ? patternMatch["spaceId"] : false,
lang: lang lang: lang
}); });
if (r.isAudioOnly) obj.isAudioOnly = true
break; break;
case "vk": case "vk":
r = await vk({ r = await vk({

View file

@ -51,7 +51,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly) {
return apiJSON(5, { return apiJSON(5, {
picker: r.picker, picker: r.picker,
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip, u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
filename: r.audioFilename, salt: process.env.streamSalt, isAudioOnly: true, audioFormat: audioFormat, copy: audioFormat == "best" ? true : false filename: r.audioFilename, salt: process.env.streamSalt, isAudioOnly: true, audioFormat: audioFormat, copy: audioFormat === "best" ? true : false
}) })
case "twitter": case "twitter":
return apiJSON(5, { return apiJSON(5, {
@ -59,33 +59,38 @@ export default function(r, host, ip, audioFormat, isAudioOnly) {
}) })
} }
} else { } else {
if (host == "reddit" && r.typeId == 1 || audioIgnore.includes(host)) return apiJSON(0, { t: r.audioFilename }); if (host === "reddit" && r.typeId === 1 || audioIgnore.includes(host)) return apiJSON(0, { t: r.audioFilename });
let type = "render"; let type = "render";
let copy = false; let copy = false;
if (!supportedAudio.includes(audioFormat)) audioFormat = "best"; if (!supportedAudio.includes(audioFormat)) audioFormat = "best";
if ((host == "tiktok" || host == "douyin") && isAudioOnly && services.tiktok.audioFormats.includes(audioFormat)) { if ((host === "tiktok" || host === "douyin") && isAudioOnly && services.tiktok.audioFormats.includes(audioFormat)) {
if (r.isMp3) { if (r.isMp3) {
if (audioFormat == "mp3" || audioFormat == "best") { if (audioFormat === "mp3" || audioFormat === "best") {
audioFormat = "mp3" audioFormat = "mp3"
type = "bridge" type = "bridge"
} }
} else if (audioFormat == "best") { } else if (audioFormat === "best") {
audioFormat = "m4a" audioFormat = "m4a"
type = "bridge" type = "bridge"
} }
} }
if ((audioFormat == "best" && services[host]["bestAudio"]) || services[host]["bestAudio"] && (audioFormat == services[host]["bestAudio"])) { if ((audioFormat === "best" && services[host]["bestAudio"]) || services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"])) {
audioFormat = services[host]["bestAudio"] audioFormat = services[host]["bestAudio"]
type = "bridge" type = "bridge"
} else if (audioFormat == "best") { } else if (audioFormat === "best") {
audioFormat = "m4a" audioFormat = "m4a"
copy = true copy = true
if (r.audioFilename.includes("twitterspaces")) {
audioFormat = "mp3"
copy = false
}
} }
return apiJSON(2, { return apiJSON(2, {
type: type, type: type,
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip, u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
filename: r.audioFilename, salt: process.env.streamSalt, isAudioOnly: true, audioFormat: audioFormat, copy: copy filename: r.audioFilename, salt: process.env.streamSalt, isAudioOnly: true,
audioFormat: audioFormat, copy: copy, fileMetadata: r.fileMetadata ? r.fileMetadata : false
}) })
} }
} else { } else {

View file

@ -12,14 +12,12 @@
"enabled": true "enabled": true
}, },
"twitter": { "twitter": {
"patterns": [":user/status/:id", ":user/status/:id/video/1"], "alias": "twitter, twitter spaces",
"patterns": [":user/status/:id", ":user/status/:id/video/1", "i/spaces/:spaceId"],
"enabled": true "enabled": true
}, },
"vk": { "vk": {
"alias": "vk clips, vk video", "alias": "vk clips, vk video",
"localizedAlias": {
"ru": "vk видео, vk клипы"
},
"patterns": ["video-:userId_:videoId", "clip-:userId_:videoId", "clips-:userId?z=clip-:userId_:videoId"], "patterns": ["video-:userId_:videoId", "clip-:userId_:videoId", "clips-:userId?z=clip-:userId_:videoId"],
"quality_match": { "quality_match": {
"2160": 7, "2160": 7,
@ -79,8 +77,8 @@
}, },
"soundcloud": { "soundcloud": {
"patterns": [":author/:song", ":shortLink"], "patterns": [":author/:song", ":shortLink"],
"bestAudio": "mp3", "bestAudio": "none",
"clientid": "lnFbWHXluNwOkW7TxTYUXrrse0qj1C72", "clientid": "1TLciEOiKE0PThutYu5Xj0kc8R4twD9p",
"enabled": true "enabled": true
} }
} }

View file

@ -1,8 +1,8 @@
export const testers = { export const testers = {
"twitter": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length < 20), "twitter": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length < 20) || (patternMatch["spaceId"] && patternMatch["spaceId"].length === 13),
"vk": (patternMatch) => (patternMatch["userId"] && patternMatch["videoId"] && "vk": (patternMatch) => (patternMatch["userId"] && patternMatch["videoId"] &&
patternMatch["userId"].length <= 10 && patternMatch["videoId"].length == 9), patternMatch["userId"].length <= 10 && patternMatch["videoId"].length === 9),
"bilibili": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length >= 12), "bilibili": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length >= 12),

View file

@ -15,7 +15,7 @@ export default async function(obj) {
let streamData = JSON.parse(html.split('<script>window.__playinfo__=')[1].split('</script>')[0]); let streamData = JSON.parse(html.split('<script>window.__playinfo__=')[1].split('</script>')[0]);
if (streamData.data.timelength <= maxVideoDuration) { if (streamData.data.timelength <= maxVideoDuration) {
let video = streamData["data"]["dash"]["video"].filter((v) => { let video = streamData["data"]["dash"]["video"].filter((v) => {
if (!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/") && v["height"] != 4320) return true; if (!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/") && v["height"] !== 4320) return true;
}).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth)); }).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
let audio = streamData["data"]["dash"]["audio"].filter((a) => { let audio = streamData["data"]["dash"]["audio"].filter((a) => {
if (!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true; if (!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")) return true;

View file

@ -7,30 +7,28 @@ export default async function(obj) {
let html; let html;
if (!obj.author && !obj.song && obj.shortLink) { if (!obj.author && !obj.song && obj.shortLink) {
html = await got.get(`https://soundcloud.app.goo.gl/${obj.shortLink}/`, { headers: { "user-agent": genericUserAgent } }); html = await got.get(`https://soundcloud.app.goo.gl/${obj.shortLink}/`, { headers: { "user-agent": genericUserAgent } });
html.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCouldntFetch', 'soundcloud') };
});
html = html.body html = html.body
} }
if (obj.author && obj.song) { if (obj.author && obj.song) {
html = await got.get(`https://soundcloud.com/${obj.author}/${obj.song}`, { headers: { "user-agent": genericUserAgent } }); html = await got.get(`https://soundcloud.com/${obj.author}/${obj.song}`, { headers: { "user-agent": genericUserAgent } });
html.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCouldntFetch', 'soundcloud') };
});
html = html.body html = html.body
} }
if (html.includes('<script>window.__sc_hydration = ') && html.includes('"format":{"protocol":"progressive","mime_type":"audio/mpeg"},') && html.includes('{"hydratable":"sound","data":')) { 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]) let json = JSON.parse(html.split('{"hydratable":"sound","data":')[1].split('}];</script>')[0])
if (json["media"]["transcodings"]) { if (json["media"]["transcodings"]) {
let fileUrl = `${json.media.transcodings[0]["url"].replace("/hls", "/progressive")}?client_id=${services["soundcloud"]["clientid"]}&track_authorization=${json.track_authorization}`; 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 (fileUrl.substring(0, 54) === "https://api-v2.soundcloud.com/media/soundcloud:tracks:") {
if ((json.duration < maxAudioDuration) || obj.format == "best" || obj.format == "mp3") { if (json.duration < maxAudioDuration) {
let file = await got.get(fileUrl, { headers: { "user-agent": genericUserAgent } }); let file = await got.get(fileUrl, { headers: { "user-agent": genericUserAgent } });
file.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCouldntFetch', 'soundcloud') };
});
file = JSON.parse(file.body).url file = JSON.parse(file.body).url
return { urls: file, audioFilename: `soundcloud_${json.id}` } return {
urls: file,
audioFilename: `soundcloud_${json.id}`,
fileMetadata: {
title: json.title,
artist: json.user.username,
}
}
} else return { error: loc(obj.lang, 'ErrorLengthAudioConvert', maxAudioDuration / 60000) } } else return { error: loc(obj.lang, 'ErrorLengthAudioConvert', maxAudioDuration / 60000) }
} }
} else return { error: loc(obj.lang, 'ErrorEmptyDownload') } } else return { error: loc(obj.lang, 'ErrorEmptyDownload') }

View file

@ -7,7 +7,7 @@ let userAgent = genericUserAgent.split(' Chrome/1')[0]
let config = { let config = {
tiktok: { tiktok: {
short: "https://vt.tiktok.com/", short: "https://vt.tiktok.com/",
api: "https://api.tiktokv.com/aweme/v1/feed/?aweme_id={postId}&version_code=262&app_name=musical_ly&channel=App&device_id=null&os_version=14.4.2&device_platform=iphone&device_type=iPhone9", // thanks to https://github.com/wukko/cobalt/pull/41#issue-1380090574 api: "https://api2.musical.ly/aweme/v1/feed/?aweme_id={postId}&version_code=262&app_name=musical_ly&channel=App&device_id=null&os_version=14.4.2&device_platform=iphone&device_type=iPhone9", // ill always find more endpoints lmfao
}, },
douyin: { douyin: {
short: "https://v.douyin.com/", short: "https://v.douyin.com/",
@ -27,37 +27,37 @@ export default async function(obj) {
try { try {
if (!obj.postId) { if (!obj.postId) {
let html = await got.get(`${config[obj.host]["short"]}${obj.id}`, { followRedirect: false, headers: { "user-agent": userAgent } }); let html = await got.get(`${config[obj.host]["short"]}${obj.id}`, { followRedirect: false, headers: { "user-agent": userAgent } });
html.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', obj.host) };
});
html = html.body; html = html.body;
if (html.slice(0, 17) === '<a href="https://' && html.includes('/video/')) obj.postId = html.split('video/')[1].split('?')[0].replace("/", '') if (html.slice(0, 17) === '<a href="https://' && html.includes('/video/')) obj.postId = html.split('video/')[1].split('?')[0].replace("/", '')
} }
if (!obj.postId) return { error: loc(obj.lang, 'ErrorCantGetID') }; if (!obj.postId) return { error: loc(obj.lang, 'ErrorCantGetID') };
let detail;
let detail = await got.get(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"} }); try {
detail.on('error', (err) => { detail = await got.get(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"} });
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', obj.host) };
});
detail = selector(JSON.parse(detail.body), obj.host); detail = selector(JSON.parse(detail.body), obj.host);
} catch (e) {
if (obj.host === "tiktok") {
let html = await got.get(`https://tiktok.com/@video/video/${obj.postId}`, { headers: { "user-agent": userAgent } });
html = html.body;
if (html.includes(',"preloadList":[{"url":"')) {
return {
urls: unicodeDecode(html.split(',"preloadList":[{"url":"')[1].split('","id":"')[0].trim()),
filename: `${obj.host}_${obj.postId}_video.mp4`
}
} else throw new Error()
} else throw new Error()
}
let video, videoFilename, audioFilename, isMp3, audio, let video, videoFilename, audioFilename, isMp3, audio,
images = detail["image_post_info"] ? detail["image_post_info"]["images"] : false, images = detail["image_post_info"] ? detail["image_post_info"]["images"] : false,
filenameBase = `${obj.host}_${obj.postId}`; filenameBase = `${obj.host}_${obj.postId}`;
if (!obj.isAudioOnly && !images) { if (!obj.isAudioOnly && !images) {
if (obj.host == "tiktok") { video = obj.host === "tiktok" ? detail["video"]["play_addr"]["url_list"][0] : detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play");
video = detail["video"]["play_addr"]["url_list"][0]
} else {
video = detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play")
}
videoFilename = `${filenameBase}_video_nw.mp4` // nw - no watermark videoFilename = `${filenameBase}_video_nw.mp4` // nw - no watermark
if (!obj.noWatermark) { if (!obj.noWatermark) {
if (obj.host == "tiktok") { if (obj.host === "tiktok") {
let html = await got.get(`https://tiktok.com/@video/video/${obj.postId}`, { headers: { "user-agent": userAgent } }); let html = await got.get(`https://tiktok.com/@video/video/${obj.postId}`, { headers: { "user-agent": userAgent } });
html.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', obj.host) };
});
html = html.body; html = html.body;
if (html.includes(',"preloadList":[{"url":"')) { if (html.includes(',"preloadList":[{"url":"')) {
video = unicodeDecode(html.split(',"preloadList":[{"url":"')[1].split('","id":"')[0].trim()) video = unicodeDecode(html.split(',"preloadList":[{"url":"')[1].split('","id":"')[0].trim())
@ -68,7 +68,7 @@ export default async function(obj) {
videoFilename = `${filenameBase}_video.mp4` videoFilename = `${filenameBase}_video.mp4`
} }
} else { } else {
let fallback = obj.host == "douyin" ? detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play") : detail["video"]["play_addr"]["url_list"][0]; let fallback = obj.host === "douyin" ? detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play") : detail["video"]["play_addr"]["url_list"][0];
if (obj.fullAudio || fallback.includes("music")) { if (obj.fullAudio || fallback.includes("music")) {
audio = detail["music"]["play_url"]["url_list"][0] audio = detail["music"]["play_url"]["url_list"][0]
audioFilename = `${filenameBase}_audio` audioFilename = `${filenameBase}_audio`
@ -76,7 +76,7 @@ export default async function(obj) {
audio = fallback audio = fallback
audioFilename = `${filenameBase}_audio_fv` // fv - from video audioFilename = `${filenameBase}_audio_fv` // fv - from video
} }
if (audio.slice(-4) == ".mp3") isMp3 = true; if (audio.slice(-4) === ".mp3") isMp3 = true;
} }
if (video) return { if (video) return {
urls: video, urls: video,

View file

@ -3,7 +3,7 @@ import loc from "../../localization/manager.js";
import { genericUserAgent } from "../config.js"; import { genericUserAgent } from "../config.js";
function bestQuality(arr) { function bestQuality(arr) {
return arr.filter((v) => { if (v["content_type"] == "video/mp4") return true; }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"].split("?")[0] return arr.filter((v) => { if (v["content_type"] === "video/mp4") return true; }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"].split("?")[0]
} }
const apiURL = "https://api.twitter.com/1.1" const apiURL = "https://api.twitter.com/1.1"
@ -11,7 +11,7 @@ export default async function(obj) {
try { try {
let _headers = { let _headers = {
"User-Agent": genericUserAgent, "User-Agent": genericUserAgent,
"Authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA", "Authorization": "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw",
"Host": "api.twitter.com", "Host": "api.twitter.com",
"Content-Type": "application/json", "Content-Type": "application/json",
"Content-Length": 0 "Content-Length": 0
@ -19,26 +19,24 @@ export default async function(obj) {
let req_act = await got.post(`${apiURL}/guest/activate.json`, { let req_act = await got.post(`${apiURL}/guest/activate.json`, {
headers: _headers headers: _headers
}); });
req_act.on('error', (err) => { req_act = JSON.parse(req_act.body)
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'twitter') } _headers["x-guest-token"] = req_act["guest_token"];
}) if (!obj.spaceId) {
_headers["x-guest-token"] = req_act.body["guest_token"]; let req_status = await got.get(
let req_status = await got.get(`${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`, { `${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`,
headers: _headers { headers: _headers }
}); );
req_status.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'twitter') } req_status = JSON.parse(req_status.body);
}) if (req_status["extended_entities"] && req_status["extended_entities"]["media"]) {
let parsbod = JSON.parse(req_status.body); let single, multiple = [], media = req_status["extended_entities"]["media"];
if (parsbod["extended_entities"] && parsbod["extended_entities"]["media"]) { media = media.filter((i) => { if (i["type"] === "video" || i["type"] === "animated_gif") return true })
let single, multiple = [], media = parsbod["extended_entities"]["media"];
media = media.filter((i) => { if (i["type"] == "video" || i["type"] == "animated_gif") return true })
if (media.length > 1) { if (media.length > 1) {
for (let i in media) { for (let i in media) { multiple.push({type: "video", thumb: media[i]["media_url_https"], url: bestQuality(media[i]["video_info"]["variants"])}) }
multiple.push({type: "video", thumb: media[i]["media_url_https"], url: bestQuality(media[i]["video_info"]["variants"])}) } else if (media.length > 0) {
}
} else {
single = bestQuality(media[0]["video_info"]["variants"]) single = bestQuality(media[0]["video_info"]["variants"])
} else {
return { error: loc(obj.lang, 'ErrorNoVideosInTweet') }
} }
if (single) { if (single) {
return { urls: single, audioFilename: `twitter_${obj.id}_audio` } return { urls: single, audioFilename: `twitter_${obj.id}_audio` }
@ -50,6 +48,38 @@ export default async function(obj) {
} else { } else {
return { error: loc(obj.lang, 'ErrorNoVideosInTweet') } return { error: loc(obj.lang, 'ErrorNoVideosInTweet') }
} }
} else {
_headers["host"] = "twitter.com"
let query = {
variables: {"id": obj.spaceId,"isMetatagsQuery":true,"withSuperFollowsUserFields":true,"withDownvotePerspective":false,"withReactionsMetadata":false,"withReactionsPerspective":false,"withSuperFollowsTweetFields":true,"withReplays":true}, features: {"spaces_2022_h2_clipping":true,"spaces_2022_h2_spaces_communities":true,"verified_phone_label_enabled":false,"tweetypie_unmention_optimization_enabled":true,"responsive_web_uc_gql_enabled":true,"vibe_api_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":false,"interactive_text_enabled":true,"responsive_web_text_conversations_enabled":false,"responsive_web_enhance_cards_enabled":true}
}
let AudioSpaceById = await got.get(`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 });
AudioSpaceById = JSON.parse(AudioSpaceById.body);
if (AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) {
let streamStatus = await got.get(`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers });
streamStatus = JSON.parse(streamStatus.body);
let participants = AudioSpaceById.data.audioSpace.participants.speakers
let listOfParticipants = `Twitter Space speakers: `
for (let i in participants) {
listOfParticipants += `@${participants[i]["twitter_screen_name"]}, `
}
listOfParticipants = listOfParticipants.slice(0, -2);
return {
urls: streamStatus.source.noRedirectPlaybackUrl,
audioFilename: `twitterspaces_${obj.spaceId}`,
isAudioOnly: true,
fileMetadata: {
title: AudioSpaceById.data.audioSpace.metadata.title,
artist: `Twitter Space by @${AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.screen_name}`,
comment: listOfParticipants,
// cover: AudioSpaceById.data.audioSpace.metadata.creator_results.result.legacy.profile_image_url_https.replace("_normal", "")
}
}
} else {
return { error: loc(obj.lang, 'TwitterSpaceWasntRecorded') };
}
}
} catch (err) { } catch (err) {
return { error: loc(obj.lang, 'ErrorBadFetch') }; return { error: loc(obj.lang, 'ErrorBadFetch') };
} }

View file

@ -15,7 +15,7 @@ export default async function(obj) {
let all = api["request"]["files"]["progressive"].sort((a, b) => Number(b.width) - Number(a.width)); let all = api["request"]["files"]["progressive"].sort((a, b) => Number(b.width) - Number(a.width));
let best = all[0] let best = all[0]
try { try {
if (obj.quality != "max") { if (obj.quality !== "max") {
let pref = parseInt(quality[obj.quality], 10) let pref = parseInt(quality[obj.quality], 10)
for (let i in all) { for (let i in all) {
let currQuality = parseInt(all[i]["quality"].replace('p', ''), 10) let currQuality = parseInt(all[i]["quality"].replace('p', ''), 10)

View file

@ -5,37 +5,41 @@ import selectQuality from "../stream/selectQuality.js";
export default async function(obj) { export default async function(obj) {
try { try {
let info = await ytdl.getInfo(obj.id); let infoInitial = await ytdl.getInfo(obj.id);
if (info) { if (infoInitial) {
info = info.formats; let info = infoInitial.formats;
if (!info[0]["isLive"]) { if (!info[0]["isLive"]) {
let videoMatch = [], fullVideoMatch = [], video = [], audio = info.filter((a) => { let videoMatch = [], fullVideoMatch = [], video = [], audio = info.filter((a) => {
if (!a["isHLS"] && !a["isDashMPD"] && a["hasAudio"] && !a["hasVideo"] && a["container"] == obj.format) return true; if (!a["isHLS"] && !a["isDashMPD"] && a["hasAudio"] && !a["hasVideo"] && a["container"] === obj.format) return true;
}).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
if (!obj.isAudioOnly) { if (!obj.isAudioOnly) {
video = info.filter((a) => { video = info.filter((a) => {
if (!a["isHLS"] && !a["isDashMPD"] && a["hasVideo"] && a["container"] == obj.format && a["height"] != 4320) { if (!a["isHLS"] && !a["isDashMPD"] && a["hasVideo"] && a["container"] === obj.format && a["height"] !== 4320) {
if (obj.quality != "max") { if (obj.quality !== "max") {
if (a["hasAudio"] && mq[obj.quality] == a["height"]) { if (a["hasAudio"] && mq[obj.quality] === a["height"]) {
fullVideoMatch.push(a) fullVideoMatch.push(a)
} else if (!a["hasAudio"] && mq[obj.quality] == a["height"]) { } else if (!a["hasAudio"] && mq[obj.quality] === a["height"]) {
videoMatch.push(a); videoMatch.push(a);
} }
} }
return true return true
} }
}).sort((a, b) => Number(b.bitrate) - Number(a.bitrate)); }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate));
if (obj.quality != "max") { if (obj.quality !== "max") {
if (videoMatch.length == 0) { if (videoMatch.length === 0) {
let ss = selectQuality("youtube", obj.quality, video[0]["qualityLabel"].slice(0, 5).replace('p', '').trim()) let ss = selectQuality("youtube", obj.quality, video[0]["qualityLabel"].slice(0, 5).replace('p', '').trim())
videoMatch = video.filter((a) => { videoMatch = video.filter((a) => {
if (a["qualityLabel"].slice(0, 5).replace('p', '').trim() == ss) return true; if (a["qualityLabel"].slice(0, 5).replace('p', '').trim() === ss) return true;
}) })
} else if (fullVideoMatch.length > 0) { } else if (fullVideoMatch.length > 0) {
videoMatch = [fullVideoMatch[0]] videoMatch = [fullVideoMatch[0]]
} }
} else videoMatch = [video[0]]; } else videoMatch = [video[0]];
if (obj.quality == "los") videoMatch = [video[video.length - 1]]; if (obj.quality === "los") videoMatch = [video[video.length - 1]];
}
let generalMeta = {
title: infoInitial.videoDetails.title,
artist: infoInitial.videoDetails.ownerChannelName.replace("- Topic", "").trim(),
} }
if (audio[0]["approxDurationMs"] <= maxVideoDuration) { if (audio[0]["approxDurationMs"] <= maxVideoDuration) {
if (!obj.isAudioOnly && videoMatch.length > 0) { if (!obj.isAudioOnly && videoMatch.length > 0) {
@ -60,7 +64,21 @@ export default async function(obj) {
filename: `youtube_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.${video[0]["container"]}` filename: `youtube_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.${video[0]["container"]}`
}; };
} else if (audio.length > 0) { } else if (audio.length > 0) {
return { type: "bridge", isAudioOnly: true, urls: audio[0]["url"], audioFilename: `youtube_${obj.id}_audio` }; let r = {
type: "render",
isAudioOnly: true,
urls: audio[0]["url"],
audioFilename: `youtube_${obj.id}_audio`,
fileMetadata: generalMeta
};
let isAutoGenAudio = infoInitial.videoDetails.description.startsWith("Provided to YouTube by");
if (isAutoGenAudio) {
let descItems = infoInitial.videoDetails.description.split("\n\n")
r.fileMetadata.album = descItems[2]
r.fileMetadata.copyright = descItems[3]
r.fileMetadata.date = descItems[4].replace("Released on: ", '').trim()
}
return r
} else { } else {
return { error: loc(obj.lang, 'ErrorBadFetch') }; return { error: loc(obj.lang, 'ErrorBadFetch') };
} }

View file

@ -9,8 +9,7 @@ export function createStream(obj) {
let streamUUID = UUID(), let streamUUID = UUID(),
exp = Math.floor(new Date().getTime()) + streamLifespan, exp = Math.floor(new Date().getTime()) + streamLifespan,
ghmac = encrypt(`${streamUUID},${obj.url},${obj.ip},${exp}`, obj.salt), ghmac = encrypt(`${streamUUID},${obj.url},${obj.ip},${exp}`, obj.salt),
iphmac = encrypt(`${obj.ip}`, obj.salt); iphmac = encrypt(`${obj.ip}`, obj.salt)
streamCache.set(streamUUID, { streamCache.set(streamUUID, {
id: streamUUID, id: streamUUID,
service: obj.service, service: obj.service,
@ -20,10 +19,11 @@ export function createStream(obj) {
hmac: ghmac, hmac: ghmac,
ip: iphmac, ip: iphmac,
exp: exp, exp: exp,
isAudioOnly: obj.isAudioOnly ? true : false, isAudioOnly: !!obj.isAudioOnly,
audioFormat: obj.audioFormat, audioFormat: obj.audioFormat,
time: obj.time, time: obj.time,
copy: obj.copy copy: obj.copy,
metadata: obj.fileMetadata
}); });
return `${process.env.selfURL}api/stream?t=${streamUUID}&e=${exp}&h=${ghmac}`; return `${process.env.selfURL}api/stream?t=${streamUUID}&e=${exp}&h=${ghmac}`;
} }

View file

@ -7,12 +7,12 @@ function closest(goal, array) {
} }
export default function(service, quality, maxQuality) { export default function(service, quality, maxQuality) {
if (quality == "max") return maxQuality; if (quality === "max") return maxQuality;
quality = parseInt(mq[quality], 10) quality = parseInt(mq[quality], 10)
maxQuality = parseInt(maxQuality, 10) maxQuality = parseInt(maxQuality, 10)
if (quality >= maxQuality || quality == maxQuality) return maxQuality; if (quality >= maxQuality || quality === maxQuality) return maxQuality;
if (quality < maxQuality) { if (quality < maxQuality) {
if (services[service]["quality"][quality]) { if (services[service]["quality"][quality]) {

View file

@ -6,7 +6,7 @@ export default function(res, ip, id, hmac, exp) {
try { try {
let streamInfo = verifyStream(ip, id, hmac, exp, process.env.streamSalt); let streamInfo = verifyStream(ip, id, hmac, exp, process.env.streamSalt);
if (!streamInfo.error) { if (!streamInfo.error) {
if (streamInfo.isAudioOnly && streamInfo.type != "bridge") { if (streamInfo.isAudioOnly && streamInfo.type !== "bridge") {
streamAudioOnly(streamInfo, res); streamAudioOnly(streamInfo, res);
} else { } else {
switch (streamInfo.type) { switch (streamInfo.type) {

View file

@ -2,7 +2,7 @@ import { spawn } from "child_process";
import ffmpeg from "ffmpeg-static"; import ffmpeg from "ffmpeg-static";
import got from "got"; import got from "got";
import { ffmpegArgs, genericUserAgent } from "../config.js"; import { ffmpegArgs, genericUserAgent } from "../config.js";
import { msToTime } from "../sub/utils.js"; import { metadataManager, msToTime } from "../sub/utils.js";
export function streamDefault(streamInfo, res) { export function streamDefault(streamInfo, res) {
try { try {
@ -25,53 +25,31 @@ export function streamDefault(streamInfo, res) {
} }
export function streamLiveRender(streamInfo, res) { export function streamLiveRender(streamInfo, res) {
try { try {
if (streamInfo.urls.length == 2) { if (streamInfo.urls.length === 2) {
let headers = {};
if (streamInfo.service == "bilibili") {
headers = { "user-agent": genericUserAgent };
}
const audio = got.get(streamInfo.urls[1], { isStream: true, headers: headers });
const video = got.get(streamInfo.urls[0], { isStream: true, headers: headers });
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [ let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1], args = [
'-loglevel', '-8', '-loglevel', '-8',
'-i', 'pipe:3', '-i', streamInfo.urls[0],
'-i', 'pipe:4', '-i', streamInfo.urls[1],
'-map', '0:v', '-map', '0:v',
'-map', '1:a', '-map', '1:a',
]; ];
args = args.concat(ffmpegArgs[format]) args = args.concat(ffmpegArgs[format])
if (streamInfo.time) args.push('-t', msToTime(streamInfo.time)); if (streamInfo.time) args.push('-t', msToTime(streamInfo.time));
args.push('-f', format, 'pipe:5'); args.push('-f', format, 'pipe:4');
const ffmpegProcess = spawn(ffmpeg, args, { const ffmpegProcess = spawn(ffmpeg, args, {
windowsHide: true, windowsHide: true,
stdio: [ stdio: [
'inherit', 'inherit', 'inherit', 'inherit', 'inherit', 'inherit',
'pipe', 'pipe', 'pipe' 'pipe', 'pipe',
], ],
}); });
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`); res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}"`);
ffmpegProcess.stdio[5].pipe(res); ffmpegProcess.stdio[4].pipe(res);
ffmpegProcess.on('error', (err) => { ffmpegProcess.on('error', (err) => {
ffmpegProcess.kill(); ffmpegProcess.kill();
res.end(); res.end();
}); });
video.pipe(ffmpegProcess.stdio[3]).on('error', (err) => {
ffmpegProcess.kill();
res.end();
});
audio.pipe(ffmpegProcess.stdio[4]).on('error', (err) => {
ffmpegProcess.kill();
res.end();
});
audio.on('error', (err) => {
ffmpegProcess.kill();
res.end();
});
video.on('error', (err) => {
ffmpegProcess.kill();
res.end();
});
} else { } else {
res.end(); res.end();
} }
@ -81,20 +59,23 @@ export function streamLiveRender(streamInfo, res) {
} }
export function streamAudioOnly(streamInfo, res) { export function streamAudioOnly(streamInfo, res) {
try { try {
let headers = {};
if (streamInfo.service == "bilibili") {
headers = { "user-agent": genericUserAgent };
}
const audio = got.get(streamInfo.urls, { isStream: true, headers: headers });
let args = [ let args = [
'-loglevel', '-8', '-loglevel', '-8',
'-i', 'pipe:3', '-i', streamInfo.urls
'-vn'
] ]
if (streamInfo.metadata) {
if (streamInfo.metadata.cover) { // doesn't work on the server but works locally, no idea why
args.push('-i', streamInfo.metadata.cover, '-map', '0:a', '-map', '1:0', '-filter:v', 'scale=w=400:h=400,format=yuvj420p')
} else {
args.push('-vn')
}
args = args.concat(metadataManager(streamInfo.metadata))
}
let arg = streamInfo.copy ? ffmpegArgs["copy"] : ffmpegArgs["audio"] let arg = streamInfo.copy ? ffmpegArgs["copy"] : ffmpegArgs["audio"]
args = args.concat(arg) args = args.concat(arg)
if (streamInfo.metadata.cover) args.push("-c:v", "mjpeg")
if (ffmpegArgs[streamInfo.audioFormat]) args = args.concat(ffmpegArgs[streamInfo.audioFormat]); if (ffmpegArgs[streamInfo.audioFormat]) args = args.concat(ffmpegArgs[streamInfo.audioFormat]);
args.push('-f', streamInfo.audioFormat == "m4a" ? "ipod" : streamInfo.audioFormat, 'pipe:4'); args.push('-f', streamInfo.audioFormat === "m4a" ? "ipod" : streamInfo.audioFormat, 'pipe:4');
const ffmpegProcess = spawn(ffmpeg, args, { const ffmpegProcess = spawn(ffmpeg, args, {
windowsHide: true, windowsHide: true,
stdio: [ stdio: [
@ -106,16 +87,9 @@ export function streamAudioOnly(streamInfo, res) {
ffmpegProcess.kill(); ffmpegProcess.kill();
res.end(); res.end();
}); });
audio.on('error', (err) => {
ffmpegProcess.kill();
res.end();
});
res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}.${streamInfo.audioFormat}"`); res.setHeader('Content-Disposition', `attachment; filename="${streamInfo.filename}.${streamInfo.audioFormat}"`);
ffmpegProcess.stdio[4].pipe(res); ffmpegProcess.stdio[4].pipe(res);
audio.pipe(ffmpegProcess.stdio[3]).on('error', (err) => {
ffmpegProcess.kill();
res.end();
});
} catch (e) { } catch (e) {
res.end(); res.end();
} }

View file

@ -1,4 +1,4 @@
export function t(color, tt) { function t(color, tt) {
return color + tt + "\x1b[0m" return color + tt + "\x1b[0m"
} }
export function Reset(tt) { export function Reset(tt) {

View file

@ -1,11 +1,8 @@
import { createHmac, createHash, randomUUID } from "crypto"; import { createHmac, randomUUID } from "crypto";
export function encrypt(str, salt) { export function encrypt(str, salt) {
return createHmac("sha256", salt).update(str).digest("hex"); return createHmac("sha256", salt).update(str).digest("hex");
} }
export function md5(string) {
return createHash('md5').update(string).digest('hex');
}
export function UUID() { export function UUID() {
return randomUUID(); return randomUUID();
} }

View file

@ -1,8 +1,5 @@
import loc from "../../localization/manager.js"; import loc from "../../localization/manager.js";
export function internalError(res) {
res.status(501).json({ status: "error", text: "Internal Server Error" });
}
export function errorUnsupported(lang) { export function errorUnsupported(lang) {
return loc(lang, 'ErrorUnsupported'); return loc(lang, 'ErrorUnsupported');
} }

View file

@ -30,6 +30,14 @@ export function apiJSON(type, obj) {
return { status: 500, body: { status: "error", text: "Internal Server Error" } }; return { status: 500, body: { status: "error", text: "Internal Server Error" } };
} }
} }
export function metadataManager(obj) {
let keys = Object.keys(obj);
let tags = ["album", "composer", "genre", "copyright", "encoded_by", "title", "language", "artist", "album_artist", "performer", "disc", "publisher", "track", "encoder", "compilation", "date", "creation_time", "comment"]
let commands = []
for (let i in keys) { if (tags.includes(keys[i])) commands.push('-metadata', `${keys[i]}=${obj[keys[i]]}`) }
return commands;
}
export function msToTime(d) { export function msToTime(d) {
let milliseconds = parseInt((d % 1000) / 100, 10), let milliseconds = parseInt((d % 1000) / 100, 10),
seconds = parseInt((d / 1000) % 60, 10), seconds = parseInt((d / 1000) % 60, 10),
@ -49,11 +57,11 @@ export function cleanURL(url, host) {
if (url.includes('youtube.com/shorts/')) { if (url.includes('youtube.com/shorts/')) {
url = url.split('?')[0].replace('shorts/', 'watch?v='); url = url.split('?')[0].replace('shorts/', 'watch?v=');
} }
if (host == "youtube") { if (host === "youtube") {
url = url.split('&')[0]; url = url.split('&')[0];
} else { } else {
url = url.split('?')[0]; url = url.split('?')[0];
if (url.substring(url.length - 1) == "/") { if (url.substring(url.length - 1) === "/") {
url = url.substring(0, url.length - 1); url = url.substring(0, url.length - 1);
} }
} }