7.7: bugfixes and easier downloads

This commit is contained in:
wukko 2023-12-02 23:53:27 +06:00 committed by GitHub
commit ff703dfb46
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 157 additions and 126 deletions

View file

@ -29,20 +29,22 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get version from package.json
id: package-version
uses: martinbeentjes/npm-get-version-action@v1.3.1
- name: Get short commit hash
id: commit-hash
run: echo "commit_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Get release metadata
id: release-meta
run: |
version=$(cat package.json | jq -r .version)
echo "commit_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
echo "version=$version" >> $GITHUB_OUTPUT
echo "major_version=$(echo "$version" | cut -d. -f1)" >> $GITHUB_OUTPUT
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
tags: |
type=raw,value=latest
type=raw,value=${{ steps.package-version.outputs.current-version }}
type=raw,value=${{ steps.package-version.outputs.current-version }}-${{ steps.commit-hash.outputs.commit_short }}
type=raw,value=${{ steps.release-meta.outputs.version }}
type=raw,value=${{ steps.release-meta.outputs.major_version }}
type=raw,value=${{ steps.release-meta.outputs.version }}-${{ steps.release-meta.outputs.commit_short }}
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image

View file

@ -2,7 +2,7 @@ version: '3.5'
services:
cobalt-api:
image: ghcr.io/wukko/cobalt:latest
image: ghcr.io/wukko/cobalt:7
restart: unless-stopped
container_name: cobalt-api
@ -17,7 +17,6 @@ services:
#- 127.0.0.1:9000:9000
environment:
- apiPort=9000
# replace apiURL with your instance's target url in same format
- apiURL=https://co.wuk.sh/
# replace apiName with your instance's distinctive name
@ -33,7 +32,7 @@ services:
#- ./cookies.json:/cookies.json
cobalt-web:
image: ghcr.io/wukko/cobalt:latest
image: ghcr.io/wukko/cobalt:7
restart: unless-stopped
container_name: cobalt-web
@ -48,7 +47,6 @@ services:
#- 127.0.0.1:9001:9001
environment:
- webPort=9001
# replace webURL with your instance's target url in same format
- webURL=https://cobalt.tools/
# replace apiURL with preferred api instance url
@ -63,4 +61,4 @@ services:
restart: unless-stopped
command: --cleanup --scope cobalt --interval 900
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/run/docker.sock:/var/run/docker.sock

View file

@ -1,7 +1,7 @@
{
"name": "cobalt",
"description": "save what you love",
"version": "7.6.8",
"version": "7.7",
"author": "wukko",
"exports": "./src/cobalt.js",
"type": "module",

View file

@ -21,8 +21,8 @@ app.disable('x-powered-by');
await loadLoc();
const apiMode = process.env.apiURL && process.env.apiPort && !((process.env.webURL && process.env.webPort) || (process.env.selfURL && process.env.port));
const webMode = process.env.webURL && process.env.webPort && !((process.env.apiURL && process.env.apiPort) || (process.env.selfURL && process.env.port));
const apiMode = process.env.apiURL && !process.env.webURL;
const webMode = process.env.webURL && process.env.apiURL;
if (apiMode) {
const { runAPI } = await import('./core/api.js');
@ -31,5 +31,9 @@ if (apiMode) {
const { runWeb } = await import('./core/web.js');
await runWeb(express, app, gitCommit, gitBranch, __dirname)
} else {
console.log(Red(`cobalt wasn't configured yet or configuration is invalid.\n`) + Bright(`please run the setup script to fix this: `) + Green(`npm run setup`))
console.log(
Red(`cobalt wasn't configured yet or configuration is invalid.\n`)
+ Bright(`please run the setup script to fix this: `)
+ Green(`npm run setup`)
)
}

View file

@ -1,6 +1,6 @@
{
"streamLifespan": 20000,
"maxVideoDuration": 18000000,
"streamLifespan": 90000,
"maxVideoDuration": 10800000,
"genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
"authorInfo": {
"name": "wukko",

View file

@ -139,9 +139,9 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
version: version,
commit: gitCommit,
branch: gitBranch,
name: process.env.apiName ? process.env.apiName : "unknown",
name: process.env.apiName || "unknown",
url: process.env.apiURL,
cors: process.env.cors && process.env.cors === "0" ? 0 : 1,
cors: process.env?.cors === "0" ? 0 : 1,
startTime: `${startTimestamp}`
});
default:
@ -167,12 +167,12 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
res.redirect('/api/json')
});
app.listen(process.env.apiPort, () => {
app.listen(process.env.apiPort || 9000, () => {
console.log(`\n` +
`${Cyan("cobalt")} API ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` +
`Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` +
`URL: ${Cyan(`${process.env.apiURL}`)}\n` +
`Port: ${process.env.apiPort}\n`
`Port: ${process.env.apiPort || 9000}\n`
)
});
}

View file

@ -76,12 +76,12 @@ export async function runWeb(express, app, gitCommit, gitBranch, __dirname) {
return res.redirect('/')
});
app.listen(process.env.webPort, () => {
app.listen(process.env.webPort || 9001, () => {
console.log(`\n` +
`${Cyan("cobalt")} WEB ${Bright(`v.${version}-${gitCommit} (${gitBranch})`)}\n` +
`Start time: ${Bright(`${startTime.toUTCString()} (${startTimestamp})`)}\n\n` +
`URL: ${Cyan(`${process.env.webURL}`)}\n` +
`Port: ${process.env.webPort}\n`
`Port: ${process.env.webPort || 9001}\n`
)
})
}

View file

@ -1,4 +1,4 @@
const version = 38;
const version = 39;
const ua = navigator.userAgent.toLowerCase();
const isIOS = ua.match("iphone os");
@ -423,9 +423,16 @@ async function download(url) {
let jp = await res.json();
if (jp.status === "continue") {
changeDownloadButton(2, '>>>');
if (isMobile || isSafari) {
window.location.href = j.url;
} else window.open(j.url, '_blank');
if (sGet("downloadPopup") === "true") {
popup('download', 1, j.url)
setTimeout(() => {
hideAllPopups()
}, 85000)
} else {
if (isMobile || isSafari) {
window.location.href = j.url;
} else window.open(j.url, '_blank');
}
setTimeout(() => { changeButton(1) }, 2500);
} else {
changeButton(0, jp.text);

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -105,7 +105,7 @@
"FollowSupport": "keep in touch with cobalt for support, polls, news, and more:",
"SupportNote": "please note that response may take a while, there's only one person managing everything.",
"SourceCode": "report issues, explore source code, star or fork the repo:",
"PrivacyPolicy": "cobalt's privacy policy is simple: no data about you is ever collected or stored. zero, zilch, nada, nothing.\nwhat you download is solely your business, not mine or anyone else's.\n\nif your download requires live render, some non-backtraceable data is temporarily stored in server's RAM. it's necessary for this feature to function.\n\nin this case info about requested content is stored for <span class=\"text-backdrop\">20 seconds</span> and then permanently removed.\nno one (even me) has access to this data. official cobalt codebase doesn't provide a way to read it outside of processing functions.\n\nyou can check cobalt's <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">source code</a> yourself and see that everything is as stated.",
"PrivacyPolicy": "cobalt's privacy policy is simple: no data about you is ever collected or stored. zero, zilch, nada, nothing.\nwhat you download is solely your business, not mine or anyone else's.\n\nif your download requires live render, some non-backtraceable data is temporarily stored in server's RAM. it's necessary for this feature to function.\n\nin this case info about requested content is stored for <span class=\"text-backdrop\">90 seconds</span> and then permanently removed.\nno one (even me) has access to this data. official cobalt codebase doesn't provide a way to read it outside of processing functions.\n\nyou can check cobalt's <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">source code</a> yourself and see that everything is as stated.",
"ErrorYTUnavailable": "this youtube video is unavailable, it could be region or age restricted. try another one!",
"ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality!\n\nsometimes youtube api sometimes acts unexpectedly. try again or try another settings.",
"SettingsCodecSubtitle": "youtube codec",
@ -156,6 +156,7 @@
"FilenamePreviewVideoTitle": "Video Title",
"FilenamePreviewAudioTitle": "Audio Title",
"FilenamePreviewAudioAuthor": "Audio Author",
"UrgentFilenameUpdate": "customizable file names!"
"UrgentFilenameUpdate": "customizable file names!",
"UrgentTwitterPatch": "fixes and easier downloads"
}
}

View file

@ -106,7 +106,7 @@
"FollowSupport": "подписывайся на соц.сети кобальта для новостей, поддержки, участия в опросах, и многого другого:",
"SupportNote": "так как я занимаюсь разработкой и поддержкой в одиночку, время ожидания ответа может достигать нескольких часов. но я отвечаю всем, так что не стесняйся.",
"SourceCode": "пиши о проблемах, шарься в исходнике, или же форкай репозиторий:",
"PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: никакие данные о тебе никогда не собираются и не хранятся. нуль, ноль, нада, ничего.\nто, что ты скачиваешь, - твоё личное дело, а не чьё-либо ещё.\n\nесли твоей загрузке требуется лайв рендер, то некоторые неотслеживаемые данные временно держатся в ОЗУ сервера. это необходимо для работы данной функции.\n\nв этом случае данные о запрошенном контенте хранятся в течение <span class=\"text-backdrop\">20 секунд</span>. по истечении этого времени всё стирается. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как официальная кодовая база кобальта не предусматривает возможности их чтения вне функций обработки.\n\nты всегда можешь посмотреть <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">исходный код кобальта</a> и убедиться, что всё так, как заявлено.",
"PrivacyPolicy": "политика конфиденциальности кобальта довольно проста: никакие данные о тебе никогда не собираются и не хранятся. нуль, ноль, нада, ничего.\nто, что ты скачиваешь, - твоё личное дело, а не чьё-либо ещё.\n\nесли твоей загрузке требуется лайв рендер, то некоторые неотслеживаемые данные временно держатся в ОЗУ сервера. это необходимо для работы данной функции.\n\nв этом случае данные о запрошенном контенте хранятся в течение <span class=\"text-backdrop\">90 секунд</span>. по истечении этого времени всё стирается. ни у кого (даже у меня) нет доступа к временно хранящимся данным, так как официальная кодовая база кобальта не предусматривает возможности их чтения вне функций обработки.\n\nты всегда можешь посмотреть <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">исходный код кобальта</a> и убедиться, что всё так, как заявлено.",
"ErrorYTUnavailable": "это видео недоступно, возможно оно ограничено по региону или доступу. попробуй другое!",
"ErrorYTTryOtherCodec": "я не нашёл того, что мог бы скачать с твоими настройками. попробуй другой кодек или качество!",
"SettingsCodecSubtitle": "кодек для видео с youtube",
@ -158,6 +158,7 @@
"FilenamePreviewVideoTitle": "Название Видео",
"FilenamePreviewAudioTitle": "Название Аудио",
"FilenamePreviewAudioAuthor": "Автор Аудио",
"UrgentFilenameUpdate": "изменяемые названия файлов!"
"UrgentFilenameUpdate": "изменяемые названия файлов!",
"UrgentTwitterPatch": "фиксы и удобное скачивание"
}
}

View file

@ -1,5 +1,16 @@
{
"current": {
"version": "7.7",
"date": "December 2, 2023",
"title": "bugfixes and better downloads!",
"banner": {
"file": "meowthpolishegg.webp",
"width": 851,
"height": 640
},
"content": "this update fixes various issues with supported services. no new features yet, but twitter fix is surely something good to have in the meantime!\n\nservice improvements:\n*; broken twitter videos are now automatically fixed by cobalt.\n*; all vimeo videos and audios should now possible to download.\n*; vimeo: fixed short resolution displayed in \"basic\" and \"pretty\" filename styles.\n\ninterface improvements:\n*; streamables are now easier to save on ios.\n\ninternal improvements:\n*; port env variable is now not strictly necessary for cobalt to run.\n*; minor clean up.\n\nchanges since 7.6:\n*; fix for an issue related to youtube dubs.\n*; fixed a memory leak related to live renders.\n*; handling all errors related to twitter downloads.\n*; fixed support for reddit links in various languages.\n*; added rich filenames support for twitch clips.\n*; updated support and donation lists.\n\nstay tuned for future updates and have a great day :D"
},
"history": [{
"version": "7.6",
"date": "October 15, 2023",
"title": "customizable file names, instagram stories, and first cobalt sponsor!",
@ -9,8 +20,7 @@
"height": 640
},
"content": "as many have (very) often requested, cobalt now lets you pick between several file name format styles!\ngo to <span class=\"text-backdrop\">settings > other</span> and change it to whichever you like! there's a preview of each style, so you know how exactly files are gonna look like.\n\nif you liked file names the way they were before, don't worry: classic style is still the default :)\n\non a different but not any less important note: cobalt is now sponsored by <a class=\"text-backdrop link\" href=\"https://royalehosting.net/\" target=\"_blank\">royalehosting.net</a>!\noverall service performance and stability is gonna be better, but also more content will be possible to download thanks to geniuine server locations. and yes, still no ads or trackers.\n\nthis update also includes a bunch of other changes, check them out:\n\nservice improvements:\n*; added support for instagram stories thanks to <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/pull/194\" target=\"_blank\">#194</a>.\n*; fixed reddit support thanks to <a class=\"text-backdrop link\" href=\"https://github.com/wukko/cobalt/pull/221\" target=\"_blank\">#221</a>.\n*; added support for rich file names for youtube, vimeo, soundcloud, rutube, and vk.\n*; numbers and emoji no longer disappear from file name and metadata.\n*; mute and audio dub file name tags don't appear together anymore.\n*; youtube: dub file name tag doesn't appear anymore if audio track is default.\n\ninterface improvements:\n*; added a list of sponsors to about tab. if you host an instance, it's disabled by default, but can be enabled with showSponsors env variable.\n*; about button now opens about tab when no new changelog is available.\n*; fixed download button thickness on ios.\n\nyou now can reach out to cobalt via email for support! it's located in the about tab along with other socials, such as discord.\n\ni hope you enjoy this long-awaited update and have a blissful day :D"
},
"history": [{
}, {
"version": "7.5",
"date": "September 16, 2023",
"title": "support for twitch clips and rutube!",

View file

@ -562,8 +562,8 @@ export default function(obj) {
<div id="popup-backdrop" onclick="hideAllPopups()"></div>
<div id="home" style="visibility:hidden">
${urgentNotice({
emoji: "😸",
text: t("UrgentFilenameUpdate"),
emoji: "🧮",
text: t("UrgentTwitterPatch"),
visible: true,
action: "popup('about', 1, 'changelog')"
})}

View file

@ -40,6 +40,7 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d
case "bilibili":
params = { type: "render" };
break;
case "twitter":
case "youtube":
params = { type: r.type };
break;
@ -64,7 +65,6 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d
case "vine":
case "instagram":
case "tumblr":
case "twitter":
case "pinterest":
case "streamable":
responseType = 1;
@ -72,7 +72,7 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d
}
break;
case "singleM3U8":
params = { type: "videoM3U8" }
params = { type: "remux" }
break;
case "muteVideo":
params = {
@ -107,14 +107,17 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d
break;
case "audio":
if ((host === "reddit" && r.typeId === 1) || audioIgnore.includes(host)) return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') });
if ((host === "reddit" && r.typeId === 1) || audioIgnore.includes(host)) {
return apiJSON(0, { t: loc(lang, 'ErrorEmptyDownload') })
}
let processType = "render";
let copy = false;
if (!supportedAudio.includes(audioFormat)) audioFormat = "best";
if ((host === "tiktok" || host === "douyin") && services.tiktok.audioFormats.includes(audioFormat)) {
if ((host === "tiktok" || host === "douyin")
&& services.tiktok.audioFormats.includes(audioFormat)) {
if (r.isMp3) {
if (audioFormat === "mp3" || audioFormat === "best") {
audioFormat = "mp3";
@ -125,11 +128,13 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d
processType = "bridge"
}
}
if (host === "tumblr" && !r.filename && (audioFormat === "best" || audioFormat === "mp3")) {
if (host === "tumblr" && !r.filename
&& (audioFormat === "best" || audioFormat === "mp3")) {
audioFormat = "mp3";
processType = "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"];
if (host === "soundcloud") {
processType = "render"
@ -140,10 +145,6 @@ export default function(r, host, audioFormat, isAudioOnly, lang, isAudioMuted, d
} else if (audioFormat === "best") {
audioFormat = "m4a";
copy = true;
if (!r.filenameAttributes && r.audioFilename.includes("twitterspaces")) {
audioFormat = "mp3"
copy = false
}
}
if (r.isM3U8 || host === "vimeo") {
copy = false;

View file

@ -1,4 +1,5 @@
import { genericUserAgent } from "../../config.js";
import { createStream } from "../../stream/manage.js";
function bestQuality(arr) {
return arr.filter(v => v["content_type"] === "video/mp4").sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"]
@ -39,7 +40,7 @@ export default async function(obj) {
let tweet = await fetch(query, { headers: _headers }).then((r) => {
return r.status === 200 ? r.json() : false
}).catch((e) => { return false });
}).catch(() => { return false });
// {"data":{"tweetResult":{"result":{"__typename":"TweetUnavailable","reason":"Protected"}}}}
if (tweet?.data?.tweetResult?.result?.__typename !== "Tweet") {
@ -64,7 +65,12 @@ export default async function(obj) {
multiple.push({
type: "video",
thumb: media[i]["media_url_https"],
url: bestQuality(media[i]["video_info"]["variants"])
url: createStream({
service: "twitter",
type: "remux",
u: bestQuality(media[i]["video_info"]["variants"]),
filename: `twitter_${obj.id}_${Number(i) + 1}.mp4`
})
})
}
} else if (media.length === 1) {
@ -75,6 +81,7 @@ export default async function(obj) {
if (single) {
return {
type: "remux",
urls: single,
filename: `twitter_${obj.id}.mp4`,
audioFilename: `twitter_${obj.id}_audio`

View file

@ -63,40 +63,26 @@ export default async function(obj) {
if (!masterJSON) return { error: 'ErrorCouldntFetch' };
if (!masterJSON.video) return { error: 'ErrorEmptyDownload' };
let type = "parcel";
if (masterJSON.base_url === "../") type = "chop";
let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width)),
let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width)).filter(a => a['format'] === "mp42"),
bestVideo = masterJSON_Video[0];
if (Number(quality) < Number(resolutionMatch[bestVideo["width"]])) bestVideo = masterJSON_Video.find(i => resolutionMatch[i["width"]] === quality);
let videoUrl, audioUrl, baseUrl = masterJSONURL.split("/sep/")[0];
switch (type) {
case "parcel":
let masterJSON_Audio = masterJSON.audio.sort((a, b) => Number(b.bitrate) - Number(a.bitrate)).filter(a => a['mime_type'] === "audio/mp4"),
bestAudio = masterJSON_Audio[0];
videoUrl = `${baseUrl}/parcel/video/${bestVideo.index_segment.split('?')[0]}`,
audioUrl = `${baseUrl}/parcel/audio/${bestAudio.index_segment.split('?')[0]}`;
break;
case "chop":
videoUrl = `${baseUrl}/sep/video/${bestVideo.id}/master.m3u8`;
break;
if (Number(quality) < Number(resolutionMatch[bestVideo["width"]])) {
bestVideo = masterJSON_Video.find(i => resolutionMatch[i["width"]] === quality)
}
if (videoUrl) {
return {
urls: audioUrl ? [videoUrl, audioUrl] : videoUrl,
isM3U8: audioUrl ? false : true,
fileMetadata: fileMetadata,
filenameAttributes: {
service: "vimeo",
id: obj.id,
title: fileMetadata.title,
author: fileMetadata.artist,
resolution: `${bestVideo["width"]}x${bestVideo["height"]}`,
qualityLabel: `${bestVideo["height"]}p`,
extension: "mp4"
}
let masterM3U8 = `${masterJSONURL.split("/sep/")[0]}/sep/video/${bestVideo.id}/master.m3u8`;
return {
urls: masterM3U8,
isM3U8: true,
fileMetadata: fileMetadata,
filenameAttributes: {
service: "vimeo",
id: obj.id,
title: fileMetadata.title,
author: fileMetadata.artist,
resolution: `${bestVideo["width"]}x${bestVideo["height"]}`,
qualityLabel: `${resolutionMatch[bestVideo["width"]]}p`,
extension: "mp4"
}
}
return { error: 'ErrorEmptyDownload' }
}

View file

@ -29,9 +29,6 @@ console.log(
`${Cyan(`Hey, this is cobalt v.${version}!`)}\n${Bright("Let's start by creating a new ")}${Cyan(".env")}${Bright(" file. You can always change it later.")}`
)
console.log(
`\n${Bright("⚠️ Please notice that since v.6.0 cobalt is hosted in two parts. API and web app are now separate.\nMerged hosting is no longer available.")}`
)
function setup() {
console.log(Bright("\nWhat kind of server will this instance be?\nOptions: api, web."));

View file

@ -5,16 +5,22 @@ import { nanoid } from 'nanoid';
import { sha256 } from "../sub/crypto.js";
import { streamLifespan } from "../config.js";
const streamCache = new NodeCache({ stdTTL: streamLifespan/1000, checkperiod: 10, deleteOnExpire: true });
const streamSalt = randomBytes(64).toString('hex');
const streamCache = new NodeCache({
stdTTL: streamLifespan/1000,
checkperiod: 10,
deleteOnExpire: true
})
streamCache.on("expired", (key) => {
streamCache.del(key);
});
})
const streamSalt = randomBytes(64).toString('hex');
export function createStream(obj) {
let lifespan = streamLifespan
let streamID = nanoid(),
exp = Math.floor(new Date().getTime()) + streamLifespan,
exp = Math.floor(new Date().getTime()) + lifespan,
ghmac = sha256(`${streamID},${obj.service},${exp}`, streamSalt);
if (!streamCache.has(streamID)) {
@ -44,14 +50,20 @@ export function createStream(obj) {
export function verifyStream(id, hmac, exp) {
try {
let streamInfo = streamCache.get(id.toString());
if (!streamInfo) return { error: "this download link has expired or doesn't exist. go back and try again!", status: 400 };
if (!streamInfo) return {
error: "this download link has expired or doesn't exist. go back and try again!",
status: 400
}
let ghmac = sha256(`${id},${streamInfo.service},${exp}`, streamSalt);
if (String(hmac) === ghmac && String(exp) === String(streamInfo.exp) && ghmac === String(streamInfo.hmac)
&& Number(exp) > Math.floor(new Date().getTime())) {
return streamInfo;
}
return { error: "i couldn't verify if you have access to this download. go back and try again!", status: 401 };
return {
error: "i couldn't verify if you have access to this stream. go back and try again!",
status: 401
}
} catch (e) {
return { status: 500, body: { status: "error", text: "Internal Server Error" } };
}

View file

@ -10,7 +10,7 @@ export default async function(res, streamInfo) {
case "render":
await streamLiveRender(streamInfo, res);
break;
case "videoM3U8":
case "remux":
case "mute":
streamVideoOnly(streamInfo, res);
break;

View file

@ -1,7 +1,7 @@
import { spawn } from "child_process";
import ffmpeg from "ffmpeg-static";
import { ffmpegArgs, genericUserAgent } from "../config.js";
import { getThreads, metadataManager } from "../sub/utils.js";
import { metadataManager } from "../sub/utils.js";
import { request } from "undici";
import { create as contentDisposition } from "content-disposition-header";
import { AbortController } from "abort-controller"
@ -40,7 +40,10 @@ export async function streamDefault(streamInfo, res) {
const shutdown = () => (closeRequest(abortController), closeResponse(res));
try {
const filename = streamInfo.isAudioOnly ? `${streamInfo.filename}.${streamInfo.audioFormat}` : streamInfo.filename;
let filename = streamInfo.filename;
if (streamInfo.isAudioOnly) {
filename = `${streamInfo.filename}.${streamInfo.audioFormat}`
}
res.setHeader('Content-disposition', contentDisposition(filename));
const { body: stream, headers } = await request(streamInfo.urls, {
@ -60,7 +63,11 @@ export async function streamDefault(streamInfo, res) {
export async function streamLiveRender(streamInfo, res) {
let abortController = new AbortController(), process;
const shutdown = () => (closeRequest(abortController), killProcess(process), closeResponse(res));
const shutdown = () => (
closeRequest(abortController),
killProcess(process),
closeResponse(res)
);
try {
if (streamInfo.urls.length !== 2) return shutdown();
@ -72,7 +79,6 @@ export async function streamLiveRender(streamInfo, res) {
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1],
args = [
'-loglevel', '-8',
'-threads', `${getThreads()}`,
'-i', streamInfo.urls[0],
'-i', 'pipe:3',
'-map', '0:v',
@ -80,7 +86,9 @@ export async function streamLiveRender(streamInfo, res) {
];
args = args.concat(ffmpegArgs[format]);
if (streamInfo.metadata) args = args.concat(metadataManager(streamInfo.metadata));
if (streamInfo.metadata) {
args = args.concat(metadataManager(streamInfo.metadata))
}
args.push('-f', format, 'pipe:4');
process = spawn(ffmpeg, args, {
@ -114,26 +122,25 @@ export function streamAudioOnly(streamInfo, res) {
try {
let args = [
'-loglevel', '-8',
'-threads', `${getThreads()}`,
'-i', streamInfo.urls
'-loglevel', '-8'
]
if (streamInfo.service === "twitter") {
args.push('-seekable', '0')
}
args.push(
'-i', streamInfo.urls,
'-vn'
)
if (streamInfo.metadata) {
if (streamInfo.metadata.cover) { // currently corrupts the audio
args.push('-i', streamInfo.metadata.cover, '-map', '0:a', '-map', '1:0')
} else {
args.push('-vn')
}
args = args.concat(metadataManager(streamInfo.metadata))
} else {
args.push('-vn')
}
let arg = streamInfo.copy ? ffmpegArgs["copy"] : ffmpegArgs["audio"];
args = args.concat(arg);
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:3');
process = spawn(ffmpeg, args, {
@ -162,16 +169,26 @@ export function streamVideoOnly(streamInfo, res) {
try {
let args = [
'-loglevel', '-8',
'-threads', `${getThreads()}`,
'-loglevel', '-8'
]
if (streamInfo.service === "twitter") {
args.push('-seekable', '0')
}
args.push(
'-i', streamInfo.urls,
'-c', 'copy'
]
if (streamInfo.mute) args.push('-an');
if (streamInfo.service === "vimeo" || streamInfo.service === "rutube") args.push('-bsf:a', 'aac_adtstoasc');
)
if (streamInfo.mute) {
args.push('-an')
}
if (streamInfo.service === "vimeo" || streamInfo.service === "rutube") {
args.push('-bsf:a', 'aac_adtstoasc')
}
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1];
if (format === "mp4") args.push('-movflags', 'faststart+frag_keyframe+empty_moov');
if (format === "mp4") {
args.push('-movflags', 'faststart+frag_keyframe+empty_moov')
}
args.push('-f', format, 'pipe:3');
process = spawn(ffmpeg, args, {

View file

@ -134,18 +134,6 @@ export function checkJSONPost(obj) {
export function getIP(req) {
return req.header('cf-connecting-ip') ? req.header('cf-connecting-ip') : req.ip;
}
export function getThreads() {
try {
if (process.env.ffmpegThreads && process.env.ffmpegThreads.length <= 3
&& (Number(process.env.ffmpegThreads) >= 0 && Number(process.env.ffmpegThreads) <= 256)) {
return process.env.ffmpegThreads
} else {
return '0'
}
} catch (e) {
return '0'
}
}
export function cleanHTML(html) {
let clean = html.replace(/ {4}/g, '');
clean = clean.replace(/\n/g, '');