Merge branch '7.11' into encrypt-stream

Signed-off-by: jj <log@riseup.net>
This commit is contained in:
jj 2024-03-05 17:58:37 +01:00 committed by GitHub
commit 3e36c5e2ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 504 additions and 114 deletions

View file

@ -13,7 +13,8 @@ this list is not final and keeps expanding over time. if support for a service y
| service | video + audio | only audio | only video | metadata | rich file names |
| :-------- | :-----------: | :--------: | :--------: | :------: | :-------------: |
| bilibili.com | ✅ | ✅ | ✅ | | |
| bilibili.com & bilibili.tv | ✅ | ✅ | ✅ | | |
| dailymotion | ✅ | ✅ | ✅ | ✅ | ✅ |
| instagram posts & stories | ✅ | ✅ | ✅ | | |
| instagram reels | ✅ | ✅ | ✅ | | |
| ok video | ✅ | ❌ | ❌ | ✅ | ✅ |

View file

@ -1,5 +1,8 @@
{
"instagram": [
"mid=replace; ig_did=this; csrftoken=cookie"
"mid=<replace>; ig_did=<with>; csrftoken=<your>; ds_user_id=<own>; sessionid=<cookies>"
],
"reddit": [
"client_id=<replace_this>; client_secret=<replace_this>; refresh_token=<replace_this>"
]
}

View file

@ -13,17 +13,17 @@ services:
ports:
- 9000:9000/tcp
# if you're using a reverse proxy, uncomment the next line:
# if you're using a reverse proxy, uncomment the next line and remove the one above (9000:9000/tcp):
#- 127.0.0.1:9000:9000
environment:
# replace apiURL with your instance's target url in same format
- apiURL=https://co.wuk.sh/
# replace apiName with your instance's distinctive name
- apiName=eu-nl
# replace https://co.wuk.sh/ with your instance's target url in same format
- API_URL=https://co.wuk.sh/
# replace eu-nl with your instance's distinctive name
- API_NAME=eu-nl
# if you want to use cookies when fetching data from services, uncomment the next line
#- cookiePath=/cookies.json
# see cookies_example.json for example file.
#- COOKIE_PATH=/cookies.json
# see cookies.example.json for example file.
labels:
- com.centurylinklabs.watchtower.scope=cobalt
@ -43,14 +43,14 @@ services:
ports:
- 9001:9001/tcp
# if you're using a reverse proxy, uncomment the next line:
# if you're using a reverse proxy, uncomment the next line and remove the one above (9001:9001/tcp):
#- 127.0.0.1:9001:9001
environment:
# replace webURL with your instance's target url in same format
- webURL=https://cobalt.tools/
# replace apiURL with preferred api instance url
- apiURL=https://co.wuk.sh/
# replace https://cobalt.tools/ with your instance's target url in same format
- WEB_URL=https://cobalt.tools/
# replace https://co.wuk.sh/ with preferred api instance url
- API_URL=https://co.wuk.sh/
labels:
- com.centurylinklabs.watchtower.scope=cobalt

View file

@ -47,3 +47,25 @@ setup script installs all needed `npm` dependencies, but you have to install `no
sudo apt install nscd
sudo service nscd start
```
## list of all environment variables
### variables for api
| variable name | default | example | description |
|:----------------------|:----------|:------------------------|:------------|
| `API_PORT` | `9000` | `9000` | changes port from which api server is accessible. |
| `API_URL` | | `https://co.wuk.sh/` | changes url from which api server is accessible. <br> ***REQUIRED TO RUN API***. |
| `API_NAME` | `unknown` | `ams-1` | api server name that is shown in `/api/serverInfo`. |
| `CORS_WILDCARD` | `1` | `0` | toggles cross-origin resource sharing. <br> `0`: disabled. `1`: enabled. |
| `CORS_URL` | not used | `https://cobalt.tools/` | cross-origin resource sharing url. api will be available only from this url if `CORS_WILDCARD` is set to `0`. |
| `COOKIE_PATH` | not used | `/cookies.json` | path for cookie file relative to main folder. |
| `PROCESSING_PRIORITY` | not used | `10` | changes `nice` value* for ffmpeg subprocess. available only on unix systems. |
\* the higher the nice value, the lower the priority. [read more here](https://en.wikipedia.org/wiki/Nice_(Unix)).
### variables for web
| variable name | default | example | description |
|:--------------- |:--------|:------------------------|:--------------------------------------------------------------------------------------|
| `WEB_PORT` | `9001` | `9001` | changes port from which frontend server is accessible. |
| `WEB_URL` | | `https://cobalt.tools/` | changes url from which frontend server is accessible. <br> ***REQUIRED TO RUN WEB***. |
| `SHOW_SPONSORS` | `0` | `1` | toggles sponsor list in about popup. <br> `0`: disabled. `1`: enabled. |
| `IS_BETA` | `0` | `1` | toggles beta tag next to cobalt logo. <br> `0`: disabled. `1`: enabled. |

View file

@ -1,7 +1,7 @@
{
"name": "cobalt",
"description": "save what you love",
"version": "7.10.4",
"version": "7.11",
"author": "wukko",
"exports": "./src/cobalt.js",
"type": "module",
@ -37,9 +37,9 @@
"ipaddr.js": "2.1.0",
"nanoid": "^4.0.2",
"node-cache": "^5.1.2",
"psl": "1.9.0",
"psl": "https://github.com/lupomontero/psl#5eadae91361d8289d582700f90582b0d0cb73155",
"set-cookie-parser": "2.6.0",
"undici": "^5.19.1",
"undici": "^6.7.0",
"url-pattern": "1.0.3",
"youtubei.js": "^9.1.0"
}

View file

@ -1,4 +1,5 @@
import "dotenv/config";
import "./modules/sub/alias-envs.js";
import express from "express";
@ -21,8 +22,8 @@ app.disable('x-powered-by');
await loadLoc();
const apiMode = process.env.apiURL && !process.env.webURL;
const webMode = process.env.webURL && process.env.apiURL;
const apiMode = process.env.API_URL && !process.env.WEB_URL;
const webMode = process.env.WEB_URL && process.env.API_URL;
if (apiMode) {
const { runAPI } = await import('./core/api.js');

View file

@ -14,7 +14,7 @@ import { generateHmac } from "../modules/sub/crypto.js";
import { verifyStream } from "../modules/stream/manage.js";
export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
const corsConfig = process.env.cors === '0' ? {
const corsConfig = process.env.CORS_WILDCARD === '0' ? {
origin: process.env.CORS_URL,
optionsSuccessStatus: 200
} : {};
@ -152,9 +152,9 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
version: version,
commit: gitCommit,
branch: gitBranch,
name: process.env.apiName || "unknown",
url: process.env.apiURL,
cors: process.env?.cors === "0" ? 0 : 1,
name: process.env.API_NAME || "unknown",
url: process.env.API_URL,
cors: process.env?.CORS_WILDCARD === "0" ? 0 : 1,
startTime: `${startTimestamp}`
});
default:
@ -183,12 +183,12 @@ export function runAPI(express, app, gitCommit, gitBranch, __dirname) {
res.redirect('/api/json')
});
app.listen(process.env.apiPort || 9000, () => {
app.listen(process.env.API_PORT || 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 || 9000}\n`
`URL: ${Cyan(`${process.env.API_URL}`)}\n` +
`Port: ${process.env.API_PORT || 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 || 9001, () => {
app.listen(process.env.WEB_PORT || 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 || 9001}\n`
`URL: ${Cyan(`${process.env.WEB_URL}`)}\n` +
`Port: ${process.env.WEB_PORT || 9001}\n`
)
})
}

View file

@ -264,5 +264,5 @@ export function sponsoredList() {
}
export function betaTag() {
return process.env.isBeta ? '<span class="logo-sub">β</span>' : ''
return process.env.IS_BETA ? '<span class="logo-sub">β</span>' : ''
}

View file

@ -48,10 +48,10 @@ export default function(obj) {
<title>${t("AppTitleCobalt")}</title>
<meta property="og:url" content="${process.env.webURL}">
<meta property="og:url" content="${process.env.WEB_URL}">
<meta property="og:title" content="${t("AppTitleCobalt")}">
<meta property="og:description" content="${t('EmbedBriefDescription')}">
<meta property="og:image" content="${process.env.webURL}icons/generic.png">
<meta property="og:image" content="${process.env.WEB_URL}icons/generic.png">
<meta name="title" content="${t("AppTitleCobalt")}">
<meta name="description" content="${t('AboutSummary')}">
<meta name="theme-color" content="#000000">
@ -165,7 +165,7 @@ export default function(obj) {
body: t("FairUse")
}])
},
...(process.env.showSponsors ?
...(process.env.SHOW_SPONSORS ?
[{
text: t("SponsoredBy"),
classes: ["sponsored-by-text"],
@ -627,7 +627,7 @@ export default function(obj) {
</footer>
</div>
<script>
let defaultApiUrl = '${process.env.apiURL ? process.env.apiURL : ''}';
let defaultApiUrl = '${process.env.API_URL || ''}';
const loc = ${webLoc(t,
[
'ErrorNoInternet',

View file

@ -3,7 +3,7 @@ import { readFile, writeFile } from 'fs/promises';
import { parse as parseSetCookie, splitCookiesString } from 'set-cookie-parser';
const WRITE_INTERVAL = 60000,
cookiePath = process.env.cookiePath,
cookiePath = process.env.COOKIE_PATH,
COUNTER = Symbol('counter');
let cookies = {}, dirty = false, intervalId;

View file

@ -24,6 +24,7 @@ import pinterest from "./services/pinterest.js";
import streamable from "./services/streamable.js";
import twitch from "./services/twitch.js";
import rutube from "./services/rutube.js";
import dailymotion from "./services/dailymotion.js";
export default async function(host, patternMatch, url, lang, obj) {
assert(url instanceof URL);
@ -56,9 +57,7 @@ export default async function(host, patternMatch, url, lang, obj) {
});
break;
case "bilibili":
r = await bilibili({
id: patternMatch.id.slice(0, 12)
});
r = await bilibili(patternMatch);
break;
case "youtube":
let fetchInfo = {
@ -105,6 +104,7 @@ export default async function(host, patternMatch, url, lang, obj) {
case "vimeo":
r = await vimeo({
id: patternMatch.id.slice(0, 11),
password: patternMatch.password,
quality: obj.vQuality,
isAudioOnly: isAudioOnly,
forceDash: isAudioOnly ? true : obj.vimeoDash
@ -158,6 +158,9 @@ export default async function(host, patternMatch, url, lang, obj) {
isAudioOnly: isAudioOnly
});
break;
case "dailymotion":
r = await dailymotion(patternMatch);
break;
default:
return apiJSON(0, { t: errorUnsupported(lang) });
}

View file

@ -143,7 +143,7 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
const isBestHostAudio = services[host]["bestAudio"] && (audioFormat === services[host]["bestAudio"]);
const isTikTok = host === "tiktok" || host === "douyin";
const isTumblr = host === "tumblr" && !r.filename;
const isTumblrAudio = host === "tumblr" && !r.filename;
const isSoundCloud = host === "soundcloud";
if (isTikTok && services.tiktok.audioFormats.includes(audioFormat)) {
@ -168,11 +168,6 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
}
}
if (isTumblr && isBestOrMp3) {
audioFormat = "mp3";
processType = "bridge"
}
if (isBestAudioDefined || isBestHostAudio) {
audioFormat = services[host]["bestAudio"];
processType = "bridge";
@ -181,6 +176,11 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
copy = true
}
if (isTumblrAudio && isBestOrMp3) {
audioFormat = "mp3";
processType = "bridge"
}
if (r.isM3U8 || host === "vimeo") {
copy = false;
processType = "render"

View file

@ -1,27 +1,105 @@
import { genericUserAgent, maxVideoDuration } from "../../config.js";
// TO-DO: quality picking, bilibili.tv support, and higher quality downloads (currently requires an account)
export default async function(obj) {
let html = await fetch(`https://bilibili.com/video/${obj.id}`, {
// TO-DO: higher quality downloads (currently requires an account)
function com_resolveShortlink(shortId) {
return fetch(`https://b23.tv/${shortId}`, { redirect: 'manual' })
.then(r => r.status > 300 && r.status < 400 && r.headers.get('location'))
.then(url => {
if (!url) return;
const path = new URL(url).pathname;
if (path.startsWith('/video/'))
return path.split('/')[2];
})
.catch(() => {})
}
function getBest(content) {
return content?.filter(v => v.baseUrl || v.url)
.map(v => (v.baseUrl = v.baseUrl || v.url, v))
.reduce((a, b) => a?.bandwidth > b?.bandwidth ? a : b);
}
function extractBestQuality(dashData) {
const bestVideo = getBest(dashData.video),
bestAudio = getBest(dashData.audio);
if (!bestVideo || !bestAudio) return [];
return [ bestVideo, bestAudio ];
}
async function com_download(id) {
let html = await fetch(`https://bilibili.com/video/${id}`, {
headers: { "user-agent": genericUserAgent }
}).then((r) => { return r.text() }).catch(() => { return false });
if (!html) return { error: 'ErrorCouldntFetch' };
if (!(html.includes('<script>window.__playinfo__=') && html.includes('"video_codecid"'))) return { error: 'ErrorEmptyDownload' };
if (!(html.includes('<script>window.__playinfo__=') && html.includes('"video_codecid"'))) {
return { error: 'ErrorEmptyDownload' };
}
let streamData = JSON.parse(html.split('<script>window.__playinfo__=')[1].split('</script>')[0]);
if (streamData.data.timelength > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
if (streamData.data.timelength > maxVideoDuration) {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
let video = streamData["data"]["dash"]["video"].filter(v =>
!v["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")
).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
let audio = streamData["data"]["dash"]["audio"].filter(a =>
!a["baseUrl"].includes("https://upos-sz-mirrorcosov.bilivideo.com/")
).sort((a, b) => Number(b.bandwidth) - Number(a.bandwidth));
const [ video, audio ] = extractBestQuality(streamData.data.dash);
if (!video || !audio) {
return { error: 'ErrorEmptyDownload' };
}
return {
urls: [video[0]["baseUrl"], audio[0]["baseUrl"]],
audioFilename: `bilibili_${obj.id}_audio`,
filename: `bilibili_${obj.id}_${video[0]["width"]}x${video[0]["height"]}.mp4`
urls: [video.baseUrl, audio.baseUrl],
audioFilename: `bilibili_${id}_audio`,
filename: `bilibili_${id}_${video.width}x${video.height}.mp4`
};
}
async function tv_download(id) {
const url = new URL(
'https://api.bilibili.tv/intl/gateway/web/playurl'
+ '?s_locale=en_US&platform=web&qn=64&type=0&device=wap'
+ '&tf=0&spm_id=bstar-web.ugc-video-detail.0.0&from_spm_id='
);
url.searchParams.set('aid', id);
const { data } = await fetch(url).then(a => a.json());
if (!data?.playurl?.video) {
return { error: 'ErrorEmptyDownload' };
}
const [ video, audio ] = extractBestQuality({
video: data.playurl.video.map(s => s.video_resource)
.filter(s => s.codecs.includes('avc1')),
audio: data.playurl.audio_resource
});
if (!video || !audio) {
return { error: 'ErrorEmptyDownload' };
}
if (video.duration > maxVideoDuration) {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
return {
urls: [video.url, audio.url],
audioFilename: `bilibili_tv_${id}_audio`,
filename: `bilibili_tv_${id}.mp4`
};
}
export default async function({ comId, tvId, comShortLink }) {
if (comShortLink) {
comId = await com_resolveShortlink(comShortLink);
}
if (comId) {
return com_download(comId);
} else if (tvId) {
return tv_download(tvId);
}
return { error: 'ErrorCouldntFetch' };
}

View file

@ -0,0 +1,107 @@
import HLSParser from 'hls-parser';
import { maxVideoDuration } from '../../config.js';
let _token;
function getExp(token) {
return JSON.parse(
Buffer.from(token.split('.')[1], 'base64')
).exp * 1000;
}
const getToken = async () => {
if (_token && getExp(_token) > new Date().getTime()) {
return _token;
}
const req = await fetch('https://graphql.api.dailymotion.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'User-Agent': 'dailymotion/240213162706 CFNetwork/1492.0.1 Darwin/23.3.0',
'Authorization': 'Basic MGQyZDgyNjQwOWFmOWU3MmRiNWQ6ODcxNmJmYTVjYmEwMmUwMGJkYTVmYTg1NTliNDIwMzQ3NzIyYWMzYQ=='
},
body: 'traffic_segment=&grant_type=client_credentials'
}).then(r => r.json()).catch(() => {});
if (req.access_token) {
return _token = req.access_token;
}
}
export default async function({ id }) {
const token = await getToken();
if (!token) return { error: 'ErrorSomethingWentWrong' };
const req = await fetch('https://graphql.api.dailymotion.com/',
{
method: 'POST',
headers: {
'User-Agent': 'dailymotion/240213162706 CFNetwork/1492.0.1 Darwin/23.3.0',
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
'X-DM-AppInfo-Version': '7.16.0_240213162706',
'X-DM-AppInfo-Type': 'iosapp',
'X-DM-AppInfo-Id': 'com.dailymotion.dailymotion'
},
body: JSON.stringify({
operationName: "Media",
query: `
query Media($xid: String!, $password: String) {
media(xid: $xid, password: $password) {
__typename
... on Video {
xid
hlsURL
duration
title
channel {
displayName
}
}
}
}
`,
variables: { xid: id }
})
}
).then(r => r.status === 200 && r.json()).catch(() => {});
const media = req?.data?.media;
if (media?.__typename !== 'Video' || !media.hlsURL) {
return { error: 'ErrorEmptyDownload' }
}
if (media.duration * 1000 > maxVideoDuration) {
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
}
const manifest = await fetch(media.hlsURL).then(r => r.text()).catch(() => {});
if (!manifest) return { error: 'ErrorSomethingWentWrong' };
const bestQuality = HLSParser.parse(manifest).variants
.filter(v => v.codecs.includes('avc1'))
.reduce((a, b) => a.bandwidth > b.bandwidth ? a : b);
if (!bestQuality) return { error: 'ErrorEmptyDownload' }
const fileMetadata = {
title: media.title,
artist: media.channel.displayName
}
return {
urls: bestQuality.uri,
isM3U8: true,
filenameAttributes: {
service: 'dailymotion',
id: media.xid,
title: fileMetadata.title,
author: fileMetadata.artist,
resolution: `${bestQuality.resolution.width}x${bestQuality.resolution.height}`,
qualityLabel: `${bestQuality.resolution.height}p`,
extension: 'mp4'
},
fileMetadata
}
}

View file

@ -1,8 +1,26 @@
import psl from "psl";
import { genericUserAgent } from "../../config.js";
export default async function(obj) {
let { subdomain } = psl.parse(obj.url.hostname);
const API_KEY = 'jrsCWX1XDuVxAFO4GkK147syAoN8BJZ5voz8tS80bPcj26Vc5Z';
const API_BASE = 'https://api-http2.tumblr.com';
function request(domain, id) {
const url = new URL(`/v2/blog/${domain}/posts/${id}/permalink`, API_BASE);
url.searchParams.set('api_key', API_KEY);
url.searchParams.set('fields[blogs]', 'uuid,name,avatar,?description,?can_message,?can_be_followed,?is_adult,?reply_conditions,'
+ '?theme,?title,?url,?is_blocked_from_primary,?placement_id,?primary,?updated,?followed,'
+ '?ask,?can_subscribe,?paywall_access,?subscription_plan,?is_blogless_advertiser,?tumblrmart_accessories');
return fetch(url, {
headers: {
'User-Agent': 'Tumblr/iPhone/33.3/333010/17.3.1/tumblr',
'X-Version': 'iPhone/33.3/333010/17.3.1/tumblr'
}
}).then(a => a.json()).catch(() => {});
}
export default async function(input) {
let { subdomain } = psl.parse(input.url.hostname);
if (subdomain?.includes('.')) {
return { error: ['ErrorBrokenLink', 'tumblr'] }
@ -10,26 +28,44 @@ export default async function(obj) {
subdomain = undefined
}
let html = await fetch(`https://${subdomain ?? obj.user}.tumblr.com/post/${obj.id}`, {
headers: { "user-agent": genericUserAgent }
}).then((r) => { return r.text() }).catch(() => { return false });
const domain = `${subdomain ?? input.user}.tumblr.com`;
const data = await request(domain, input.id);
if (!html) return { error: 'ErrorCouldntFetch' };
const element = data?.response?.timeline?.elements?.[0];
if (!element) return { error: 'ErrorEmptyDownload' };
let r;
if (html.includes('property="og:video" content="https://va.media.tumblr.com/')) {
r = {
urls: `https://va.media.tumblr.com/${html.split('property="og:video" content="https://va.media.tumblr.com/')[1].split('"')[0]}`,
filename: `tumblr_${obj.id}.mp4`,
audioFilename: `tumblr_${obj.id}_audio`
}
} else if (html.includes('property="og:audio" content="https://a.tumblr.com/')) {
r = {
urls: `https://a.tumblr.com/${html.split('property="og:audio" content="https://a.tumblr.com/')[1].split('"')[0]}`,
audioFilename: `tumblr_${obj.id}`,
const contents = [
...element.content,
...element?.trail?.map(t => t.content).flat()
]
const audio = contents.find(c => c.type === 'audio');
if (audio && audio.provider === 'tumblr') {
const fileMetadata = {
title: audio?.title,
artist: audio?.artist
};
return {
urls: audio.media.url,
filenameAttributes: {
service: 'tumblr',
id: input.id,
title: fileMetadata.title,
author: fileMetadata.artist
},
isAudioOnly: true
}
} else r = { error: 'ErrorEmptyDownload' };
}
return r
const video = contents.find(c => c.type === 'video');
if (video && video.provider === 'tumblr') {
return {
urls: video.media.url,
filename: `tumblr_${input.id}.mp4`,
audioFilename: `tumblr_${input.id}_audio`
}
}
return { error: 'ErrorEmptyDownload' }
}

View file

@ -28,7 +28,14 @@ export default async function(obj) {
let quality = obj.quality === "max" ? "9000" : obj.quality;
if (!quality || obj.isAudioOnly) quality = "9000";
let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then((r) => { return r.json() }).catch(() => { return false });
const url = new URL(`https://player.vimeo.com/video/${obj.id}/config`);
if (obj.password) {
url.searchParams.set('h', obj.password);
}
let api = await fetch(url)
.then(r => r.json())
.catch(() => {});
if (!api) return { error: 'ErrorCouldntFetch' };
let downloadType = "dash";
@ -71,6 +78,7 @@ export default async function(obj) {
}
let masterM3U8 = `${masterJSONURL.split("/sep/")[0]}/sep/video/${bestVideo.id}/master.m3u8`;
const fallbackResolution = bestVideo.height > bestVideo.width ? bestVideo.width : bestVideo.height;
return {
urls: masterM3U8,
@ -81,8 +89,8 @@ export default async function(obj) {
id: obj.id,
title: fileMetadata.title,
author: fileMetadata.artist,
resolution: `${bestVideo["width"]}x${bestVideo["height"]}`,
qualityLabel: `${resolutionMatch[bestVideo["width"]]}p`,
resolution: `${bestVideo.width}x${bestVideo.height}`,
qualityLabel: `${resolutionMatch[bestVideo.width] || fallbackResolution}p`,
extension: "mp4"
}
}

View file

@ -2,8 +2,11 @@
"audioIgnore": ["vk", "ok"],
"config": {
"bilibili": {
"alias": "bilibili.com videos",
"patterns": ["video/:id"],
"alias": "bilibili.com & bilibili.tv",
"patterns": [
"video/:comId", "_shortLink/:comShortLink",
"_tv/:lang/video/:tvId", "_tv/video/:tvId"
],
"enabled": true
},
"reddit": {
@ -32,7 +35,7 @@
"ok": {
"alias": "ok video",
"tld": "ru",
"patterns": ["video/:id"],
"patterns": ["video/:id", "videoembed/:id"],
"enabled": true
},
"youtube": {
@ -61,7 +64,7 @@
"enabled": false
},
"vimeo": {
"patterns": [":id", "video/:id"],
"patterns": [":id", "video/:id", ":id/:password"],
"enabled": true,
"bestAudio": "mp3"
},
@ -106,6 +109,11 @@
"tld": "ru",
"patterns": ["video/:id", "play/embed/:id"],
"enabled": true
},
"dailymotion": {
"alias": "dailymotion videos",
"patterns": ["video/:id"],
"enabled": true
}
}
}

View file

@ -1,6 +1,9 @@
export const testers = {
"bilibili": (patternMatch) =>
patternMatch.id?.length <= 12,
"bilibili": (patternMatch) =>
patternMatch.comId?.length <= 12 || patternMatch.comShortLink?.length <= 16
|| patternMatch.tvId?.length <= 24,
"dailymotion": (patternMatch) => patternMatch.id?.length <= 32,
"instagram": (patternMatch) =>
patternMatch.postId?.length <= 12
@ -39,7 +42,8 @@ export const testers = {
patternMatch.id?.length < 20,
"vimeo": (patternMatch) =>
patternMatch.id?.length <= 11,
patternMatch.id?.length <= 11
&& (!patternMatch.password || patternMatch.password.length < 16),
"vine": (patternMatch) =>
patternMatch.id?.length <= 12,

View file

@ -16,6 +16,7 @@ export function aliasURL(url) {
url.search = `?v=${encodeURIComponent(parts[2])}`
}
break;
case "youtu":
if (url.hostname === 'youtu.be' && parts.length >= 2) {
/* youtu.be urls can be weird, e.g. https://youtu.be/<id>//asdasd// still works
@ -25,6 +26,7 @@ export function aliasURL(url) {
}`)
}
break;
case "pin":
if (url.hostname === 'pin.it' && parts.length === 2) {
url = new URL(`https://pinterest.com/url_shortener/${
@ -46,6 +48,22 @@ export function aliasURL(url) {
url = new URL(`https://twitch.tv/_/clip/${parts[1]}`);
}
break;
case "bilibili":
if (host.tld === 'tv') {
url = new URL(`https://bilibili.com/_tv${url.pathname}`);
}
break;
case "b23":
if (url.hostname === 'b23.tv' && parts.length === 2) {
url = new URL(`https://bilibili.com/_shortLink/${parts[1]}`)
}
break;
case "dai":
if (url.hostname === 'dai.ly' && parts.length === 2) {
url = new URL(`https://dailymotion.com/video/${parts[1]}`)
}
}
return url

View file

@ -39,27 +39,27 @@ function setup() {
console.log(Bright("\nCool! What's the domain this API instance will be running on? (localhost)\nExample: co.wuk.sh"));
rl.question(q, apiURL => {
ob['apiURL'] = `http://localhost:9000/`;
ob['apiPort'] = 9000;
if (apiURL && apiURL !== "localhost") ob['apiURL'] = `https://${apiURL.toLowerCase()}/`;
ob['API_URL'] = `http://localhost:9000/`;
ob['API_PORT'] = 9000;
if (apiURL && apiURL !== "localhost") ob['API_URL'] = `https://${apiURL.toLowerCase()}/`;
console.log(Bright("\nGreat! Now, what port will it be running on? (9000)"));
rl.question(q, apiPort => {
if (apiPort) ob['apiPort'] = apiPort;
if (apiPort && (apiURL === "localhost" || !apiURL)) ob['apiURL'] = `http://localhost:${apiPort}/`;
if (apiPort) ob['API_PORT'] = apiPort;
if (apiPort && (apiURL === "localhost" || !apiURL)) ob['API_URL'] = `http://localhost:${apiPort}/`;
console.log(Bright("\nWhat will your instance's name be? Usually it's something like eu-nl aka region-country. (local)"));
rl.question(q, apiName => {
ob['apiName'] = apiName.toLowerCase();
if (!apiName || apiName === "local") ob['apiName'] = "local";
ob['API_URL'] = apiName.toLowerCase();
if (!apiName || apiName === "local") ob['API_URL'] = "local";
console.log(Bright("\nOne last thing: would you like to enable CORS? It allows other websites and extensions to use your instance's API.\ny/n (n)"));
rl.question(q, apiCors => {
let answCors = apiCors.toLowerCase().trim();
if (answCors !== "y" && answCors !== "yes") ob['cors'] = '0'
if (answCors !== "y" && answCors !== "yes") ob['CORS_WILDCARD'] = '0'
final()
})
})
@ -71,25 +71,25 @@ function setup() {
console.log(Bright("\nAwesome! What's the domain this web app instance will be running on? (localhost)\nExample: cobalt.tools"));
rl.question(q, webURL => {
ob['webURL'] = `http://localhost:9001/`;
ob['webPort'] = 9001;
if (webURL && webURL !== "localhost") ob['webURL'] = `https://${webURL.toLowerCase()}/`;
ob['WEB_URL'] = `http://localhost:9001/`;
ob['WEB_PORT'] = 9001;
if (webURL && webURL !== "localhost") ob['WEB_URL'] = `https://${webURL.toLowerCase()}/`;
console.log(
Bright("\nGreat! Now, what port will it be running on? (9001)")
)
rl.question(q, webPort => {
if (webPort) ob['webPort'] = webPort;
if (webPort && (webURL === "localhost" || !webURL)) ob['webURL'] = `http://localhost:${webPort}/`;
if (webPort) ob['WEB_PORT'] = webPort;
if (webPort && (webURL === "localhost" || !webURL)) ob['WEB_URL'] = `http://localhost:${webPort}/`;
console.log(
Bright("\nOne last thing: what default API domain should be used? (co.wuk.sh)\nIf it's hosted locally, make sure to include the port:") + Cyan(" localhost:9000")
);
rl.question(q, apiURL => {
ob['apiURL'] = `https://${apiURL.toLowerCase()}/`;
if (apiURL.includes(':')) ob['apiURL'] = `http://${apiURL.toLowerCase()}/`;
if (!apiURL) ob['apiURL'] = "https://co.wuk.sh/";
ob['API_URL'] = `https://${apiURL.toLowerCase()}/`;
if (apiURL.includes(':')) ob['API_URL'] = `http://${apiURL.toLowerCase()}/`;
if (!apiURL) ob['API_URL'] = "https://co.wuk.sh/";
final()
})
});

View file

@ -41,7 +41,7 @@ export function createStream(obj) {
encryptStream(streamData, iv, secret)
)
let streamLink = new URL('/api/stream', process.env.apiURL);
let streamLink = new URL('/api/stream', process.env.API_URL);
const params = {
't': streamID,

View file

@ -80,17 +80,33 @@ export async function streamLiveRender(streamInfo, res) {
if (streamInfo.urls.length !== 2) return shutdown();
const { body: audio } = await request(streamInfo.urls[1], {
maxRedirections: 16, signal: abortController.signal
maxRedirections: 16, signal: abortController.signal,
headers: {
'user-agent': genericUserAgent,
referer: streamInfo.service === 'bilibili'
? 'https://www.bilibili.com/'
: undefined,
}
});
let format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1],
args = [
const format = streamInfo.filename.split('.')[streamInfo.filename.split('.').length - 1];
let args = [
'-loglevel', '-8',
'-user_agent', genericUserAgent
];
if (streamInfo.service === 'bilibili') {
args.push(
'-headers', 'Referer: https://www.bilibili.com/\r\n',
)
}
args.push(
'-i', streamInfo.urls[0],
'-i', 'pipe:3',
'-map', '0:v',
'-map', '1:a',
];
);
args = args.concat(ffmpegArgs[format]);
if (streamInfo.metadata) {
@ -129,11 +145,16 @@ export function streamAudioOnly(streamInfo, res) {
try {
let args = [
'-loglevel', '-8'
]
'-loglevel', '-8',
'-user_agent', genericUserAgent
];
if (streamInfo.service === "twitter") {
args.push('-seekable', '0')
args.push('-seekable', '0');
} else if (streamInfo.service === 'bilibili') {
args.push('-headers', 'Referer: https://www.bilibili.com/\r\n');
}
args.push(
'-i', streamInfo.urls,
'-vn'
@ -178,17 +199,23 @@ export function streamVideoOnly(streamInfo, res) {
let args = [
'-loglevel', '-8'
]
if (streamInfo.service === "twitter") {
args.push('-seekable', '0')
} else if (streamInfo.service === 'bilibili') {
args.push('-headers', 'Referer: https://www.bilibili.com/\r\n')
}
args.push(
'-i', streamInfo.urls,
'-c', 'copy'
)
if (streamInfo.mute) {
args.push('-an')
}
if (streamInfo.service === "vimeo" || streamInfo.service === "rutube") {
if (["vimeo", "rutube", "dailymotion"].includes(streamInfo.service)) {
args.push('-bsf:a', 'aac_adtstoasc')
}

View file

@ -0,0 +1,23 @@
import { Red } from "./consoleText.js";
const mapping = {
apiPort: 'API_PORT',
apiURL: 'API_URL',
apiName: 'API_NAME',
cors: 'CORS_WILDCARD',
cookiePath: 'COOKIE_PATH',
webPort: 'WEB_PORT',
webURL: 'WEB_URL',
showSponsors: 'SHOW_SPONSORS',
isBeta: 'IS_BETA'
}
for (const [ oldEnv, newEnv ] of Object.entries(mapping)) {
if (process.env[oldEnv] && !process.env[newEnv]) {
process.env[newEnv] = process.env[oldEnv];
console.error(`${Red('[!]')} ${oldEnv} is deprecated and will be removed in a future version.`);
console.error(` You should use ${newEnv} instead.`);
console.error();
delete process.env[oldEnv];
}
}

View file

@ -1,4 +1,5 @@
import "dotenv/config";
import "../modules/sub/alias-envs.js";
import { getJSON } from "../modules/api.js";
import { services } from "../modules/config.js";

View file

@ -746,6 +746,23 @@
"code": 200,
"status": "stream"
}
}, {
"name": "b23.tv shortlink",
"url": "https://b23.tv/lbMyOI9",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
},
{
"name": "bilibili.tv link",
"url": "https://www.bilibili.tv/en/video/4789599404426256",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}],
"tumblr": [{
"name": "at.tumblr link",
@ -773,7 +790,7 @@
}
}, {
"name": "tumblr audio",
"url": "https://rf9weu8hjf789234hf9.tumblr.com/post/172006661342/everyone-thats-made-a-video-out-of-this-without",
"url": "https://www.tumblr.com/zedneon/737815079301562368/zedneon-ft-mr-sauceman-tech-n9ne-speed-of?source=share",
"params": {},
"expected": {
"code": 200,
@ -830,6 +847,14 @@
"code": 200,
"status": "stream"
}
}, {
"name": "private video",
"url": "https://vimeo.com/903115595/f14d06da38",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}],
"reddit": [{
"name": "video with audio",
@ -1180,5 +1205,30 @@
"code": 200,
"status": "stream"
}
}],
"dailymotion": [{
"name": "regular video",
"url": "https://www.dailymotion.com/video/x8t1eho",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "private video",
"url": "https://www.dailymotion.com/video/k41fZWpx2TaAORA2nok",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "dai.ly shortened link",
"url": "https://dai.ly/k41fZWpx2TaAORA2nok",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}]
}