diff --git a/README.md b/README.md index 328651f..f542a69 100644 --- a/README.md +++ b/README.md @@ -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 | ✅ | ❌ | ❌ | ✅ | ✅ | diff --git a/docs/examples/cookies.example.json b/docs/examples/cookies.example.json index faaeb56..5ebdb63 100644 --- a/docs/examples/cookies.example.json +++ b/docs/examples/cookies.example.json @@ -1,5 +1,8 @@ { "instagram": [ - "mid=replace; ig_did=this; csrftoken=cookie" + "mid=; ig_did=; csrftoken=; ds_user_id=; sessionid=" + ], + "reddit": [ + "client_id=; client_secret=; refresh_token=" ] } diff --git a/docs/examples/docker-compose.example.yml b/docs/examples/docker-compose.example.yml index b5ce8a3..b6f4a90 100644 --- a/docs/examples/docker-compose.example.yml +++ b/docs/examples/docker-compose.example.yml @@ -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 diff --git a/docs/run-an-instance.md b/docs/run-an-instance.md index 801895d..5a181cc 100644 --- a/docs/run-an-instance.md +++ b/docs/run-an-instance.md @@ -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.
***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.
`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.
***REQUIRED TO RUN WEB***. | +| `SHOW_SPONSORS` | `0` | `1` | toggles sponsor list in about popup.
`0`: disabled. `1`: enabled. | +| `IS_BETA` | `0` | `1` | toggles beta tag next to cobalt logo.
`0`: disabled. `1`: enabled. | diff --git a/package.json b/package.json index 84a250e..4378bcf 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/src/cobalt.js b/src/cobalt.js index 2d90e07..050aec4 100644 --- a/src/cobalt.js +++ b/src/cobalt.js @@ -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'); diff --git a/src/core/api.js b/src/core/api.js index a439cdd..eda3c01 100644 --- a/src/core/api.js +++ b/src/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` ) }); } diff --git a/src/core/web.js b/src/core/web.js index 08a6ffe..7c0cbf3 100644 --- a/src/core/web.js +++ b/src/core/web.js @@ -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` ) }) } diff --git a/src/modules/pageRender/elements.js b/src/modules/pageRender/elements.js index a677d2b..53ad3c4 100644 --- a/src/modules/pageRender/elements.js +++ b/src/modules/pageRender/elements.js @@ -264,5 +264,5 @@ export function sponsoredList() { } export function betaTag() { - return process.env.isBeta ? 'β' : '' + return process.env.IS_BETA ? 'β' : '' } diff --git a/src/modules/pageRender/page.js b/src/modules/pageRender/page.js index 81e6d51..77b87f2 100644 --- a/src/modules/pageRender/page.js +++ b/src/modules/pageRender/page.js @@ -48,10 +48,10 @@ export default function(obj) { ${t("AppTitleCobalt")} - + - + @@ -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) { ')[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' }; +} diff --git a/src/modules/processing/services/dailymotion.js b/src/modules/processing/services/dailymotion.js new file mode 100644 index 0000000..1993eca --- /dev/null +++ b/src/modules/processing/services/dailymotion.js @@ -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 + } +} \ No newline at end of file diff --git a/src/modules/processing/services/tumblr.js b/src/modules/processing/services/tumblr.js index 75d354e..05c7fd8 100644 --- a/src/modules/processing/services/tumblr.js +++ b/src/modules/processing/services/tumblr.js @@ -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' } } diff --git a/src/modules/processing/services/vimeo.js b/src/modules/processing/services/vimeo.js index db623bd..f51f722 100644 --- a/src/modules/processing/services/vimeo.js +++ b/src/modules/processing/services/vimeo.js @@ -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" } } diff --git a/src/modules/processing/servicesConfig.json b/src/modules/processing/servicesConfig.json index 804f597..b92f575 100644 --- a/src/modules/processing/servicesConfig.json +++ b/src/modules/processing/servicesConfig.json @@ -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 } } } diff --git a/src/modules/processing/servicesPatternTesters.js b/src/modules/processing/servicesPatternTesters.js index 970e8f4..f4dee15 100644 --- a/src/modules/processing/servicesPatternTesters.js +++ b/src/modules/processing/servicesPatternTesters.js @@ -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, diff --git a/src/modules/processing/url.js b/src/modules/processing/url.js index 9c87889..b272ff8 100644 --- a/src/modules/processing/url.js +++ b/src/modules/processing/url.js @@ -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///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 diff --git a/src/modules/setup.js b/src/modules/setup.js index f81a005..c9eafc4 100644 --- a/src/modules/setup.js +++ b/src/modules/setup.js @@ -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() }) }); diff --git a/src/modules/stream/manage.js b/src/modules/stream/manage.js index 120b6f0..2e4cb0f 100644 --- a/src/modules/stream/manage.js +++ b/src/modules/stream/manage.js @@ -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, diff --git a/src/modules/stream/types.js b/src/modules/stream/types.js index cdfb4a0..6a58058 100644 --- a/src/modules/stream/types.js +++ b/src/modules/stream/types.js @@ -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') } diff --git a/src/modules/sub/alias-envs.js b/src/modules/sub/alias-envs.js new file mode 100644 index 0000000..24f6a85 --- /dev/null +++ b/src/modules/sub/alias-envs.js @@ -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]; + } +} diff --git a/src/test/test.js b/src/test/test.js index 15b28cc..d8373ff 100644 --- a/src/test/test.js +++ b/src/test/test.js @@ -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"; diff --git a/src/test/tests.json b/src/test/tests.json index a298d15..e404859 100644 --- a/src/test/tests.json +++ b/src/test/tests.json @@ -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" + } }] }