mirror of
https://github.com/wukko/cobalt.git
synced 2024-11-06 00:10:02 +00:00
Merge branch '7.11' into encrypt-stream
Signed-off-by: jj <log@riseup.net>
This commit is contained in:
commit
3e36c5e2ca
26 changed files with 504 additions and 114 deletions
|
@ -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 | ✅ | ❌ | ❌ | ✅ | ✅ |
|
||||
|
|
|
@ -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>"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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`
|
||||
)
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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>' : ''
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) });
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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' };
|
||||
}
|
||||
|
|
107
src/modules/processing/services/dailymotion.js
Normal file
107
src/modules/processing/services/dailymotion.js
Normal 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
|
||||
}
|
||||
}
|
|
@ -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' }
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
||||
|
|
23
src/modules/sub/alias-envs.js
Normal file
23
src/modules/sub/alias-envs.js
Normal 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];
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue