tiktok is back!

- added support for tiktok (images won't work, they're only accessible through the app)
- hopefully main input bar is now not rounded on ios, i fucking hate apple
- if service is not supported, a correlating error will appear, not generic one
- removed duplicates from config that are present in package json already
- tiny bit of clean up
This commit is contained in:
wukko 2022-07-28 22:03:17 +06:00
parent a4a9af6120
commit 1b4872c1de
12 changed files with 86 additions and 41 deletions

View file

@ -4,53 +4,47 @@ Sleek and easy to use social media downloader built on JavaScript. Try it out li
![cobalt logo](https://raw.githubusercontent.com/wukko/cobalt/current/src/static/icons/wide.png "cobalt logo") ![cobalt logo](https://raw.githubusercontent.com/wukko/cobalt/current/src/static/icons/wide.png "cobalt logo")
## What is cobalt? ## What is cobalt?
Everyone is annoyed by the mess video downloaders are on the web, and cobalt aims to be the ultimate social media downloader, that is efficient, pretty, and doesn't bother you with ads or privacy invasion agreement popups. cobalt aims to be the ultimate social media downloader, that is efficient, pretty, and doesn't bother you with ads or privacy invasion agreement popups.
cobalt doesn't remux any videos, so you get videos of max quality available (unless you change that in settings). cobalt doesn't remux any videos, so you get videos of max quality available (unless you change that in settings).
## What's supported? ## What's supported?
- Twitter - Twitter
- TikTok
- YouTube and YouTube Music - YouTube and YouTube Music
- bilibili.com - bilibili.com
- Reddit - Reddit
- VK - VK
## What still has to be done ## TO-DO
- [ ] Instagram support
- [ ] Quality switching for bilibili and Twitter - [ ] Quality switching for bilibili and Twitter
- [ ] Language picker in settings - [ ] Language picker in settings
- [x] Clean up the mess that localisation is right now
- [x] Sort contents of .json files
- [x] Rename each entry key to be less linked to specific service (entries like youtubeBroke are awful, I'm sorry)
- [x] Add support for more languages when localisation clean up is done
- [ ] Use esmbuild to minify frontend css and js - [ ] Use esmbuild to minify frontend css and js
- [ ] Make switch buttons in settings selectable with keyboard - [ ] Make switch buttons in settings selectable with keyboard
- [ ] Do something about changelog because the way it is right now is not really great
- [ ] Remake page rendering module to be more versatile - [ ] Remake page rendering module to be more versatile
- [ ] Matching could be redone, I'll see what I can do - [ ] Matching could be redone, I'll see what I can do
- [ ] Facebook and Instagram support
- [ ] TikTok support (?)
- [ ] Support for bilibili.tv (?)
## Disclaimer ## Disclaimer
This is my passion project, so update scheduele depends on my motivation. Don't expect any consistency in that. This is my passion project, so update scheduele depends on my motivation. Don't expect any consistency in that.
## Host an instance yourself ## Host an instance yourself
Code might be a little messy, but I promise to improve it over time. Code might be a little messy, but I do my best to improve it with every commit.
### Requirements ### Requirements
- Node.js 14.16 or above - Node.js 14.16 or above
- git - git
### npm modules ### npm modules
- express
- cors - cors
- got
- url-pattern
- xml-js
- dotenv - dotenv
- express
- express-rate-limit - express-rate-limit
- ffmpeg-static - ffmpeg-static
- got
- node-cache - node-cache
- url-pattern
- xml-js
- ytdl-core - ytdl-core
Setup script installs all needed **npm** dependencies, but you have to install Node.js and git yourself, if you don't have those already. Setup script installs all needed **npm** dependencies, but you have to install Node.js and git yourself, if you don't have those already.
@ -61,4 +55,4 @@ Setup script installs all needed **npm** dependencies, but you have to install N
4. Done. 4. Done.
## License ## License
cobalt is under [GPL-3.0 license](https://github.com/wukko/cobalt/blob/current/LICENSE), please keep that in mind. cobalt is under [AGPL-3.0 license](https://github.com/wukko/cobalt/blob/current/LICENSE), please keep that in mind.

View file

@ -1,9 +1,9 @@
{ {
"name": "cobalt", "name": "cobalt",
"description": "probably the friendliest social media downloader yet", "description": "probably the friendliest social media downloader yet",
"version": "2.2.5", "version": "2.2.6",
"author": "wukko", "author": "wukko",
"exports": "./cobalt.js", "exports": "./src/cobalt.js",
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">=14.16" "node": ">=14.16"
@ -29,7 +29,7 @@
"ffmpeg-static": "^5.0.0", "ffmpeg-static": "^5.0.0",
"got": "^12.1.0", "got": "^12.1.0",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"url-pattern": "^1.0.3", "url-pattern": "1.0.3",
"xml-js": "^1.6.11", "xml-js": "^1.6.11",
"ytdl-core": "4.11.0" "ytdl-core": "4.11.0"
} }

View file

@ -1,10 +1,7 @@
{ {
"appName": "cobalt",
"version": "2.2.5",
"streamLifespan": 1800000, "streamLifespan": 1800000,
"maxVideoDuration": 1920000, "maxVideoDuration": 1920000,
"genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36", "genericUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
"repo": "https://github.com/wukko/cobalt",
"authorInfo": { "authorInfo": {
"name": "wukko", "name": "wukko",
"link": "https://wukko.me/", "link": "https://wukko.me/",

View file

@ -21,11 +21,12 @@ export async function getJSON(originalURL, ip, lang, format, quality) {
if (host && host.length < 20 && host in patterns && patterns[host]["enabled"]) { if (host && host.length < 20 && host in patterns && patterns[host]["enabled"]) {
for (let i in patterns[host]["patterns"]) { for (let i in patterns[host]["patterns"]) {
patternMatch = new UrlPattern(patterns[host]["patterns"][i]).match(cleanURL(url, host).split(".com/")[1]); patternMatch = new UrlPattern(patterns[host]["patterns"][i]).match(cleanURL(url, host).split(".com/")[1]);
if (patternMatch) break;
} }
if (patternMatch) { if (patternMatch) {
return await match(host, patternMatch, url, ip, lang, format, quality); return await match(host, patternMatch, url, ip, lang, format, quality);
} else throw Error() } return apiJSON(0, { t: errorUnsupported(lang) } )
} else throw Error() } return apiJSON(0, { t: errorUnsupported(lang) } )
} else { } else {
return apiJSON(0, { t: errorUnsupported(lang) } ) return apiJSON(0, { t: errorUnsupported(lang) } )
} }

View file

@ -1,14 +1,15 @@
import loadJson from "./sub/loadJSON.js"; import loadJson from "./sub/loadJSON.js";
const config = loadJson("./src/config.json"); const config = loadJson("./src/config.json");
const packageJson = loadJson("./package.json");
export const export const
services = loadJson("./src/modules/services/_config.json"), services = loadJson("./src/modules/services/_config.json"),
appName = config.appName, appName = packageJson.name,
version = config.version, version = packageJson.version,
streamLifespan = config.streamLifespan, streamLifespan = config.streamLifespan,
maxVideoDuration = config.maxVideoDuration, maxVideoDuration = config.maxVideoDuration,
genericUserAgent = config.genericUserAgent, genericUserAgent = config.genericUserAgent,
repo = config.repo, repo = packageJson["bugs"]["url"].replace('/issues', ''),
authorInfo = config.authorInfo, authorInfo = config.authorInfo,
supportedLanguages = config.supportedLanguages, supportedLanguages = config.supportedLanguages,
quality = config.quality, quality = config.quality,

View file

@ -6,6 +6,7 @@ import reddit from "./services/reddit.js";
import twitter from "./services/twitter.js"; import twitter from "./services/twitter.js";
import youtube from "./services/youtube.js"; import youtube from "./services/youtube.js";
import vk from "./services/vk.js"; import vk from "./services/vk.js";
import tiktok from "./services/tiktok.js";
export default async function (host, patternMatch, url, ip, lang, format, quality) { export default async function (host, patternMatch, url, ip, lang, format, quality) {
try { try {
@ -19,13 +20,16 @@ export default async function (host, patternMatch, url, ip, lang, format, qualit
return (!r.error) ? apiJSON(1, { u: r.split('?')[0] }) : apiJSON(0, { t: r.error }) return (!r.error) ? apiJSON(1, { u: r.split('?')[0] }) : apiJSON(0, { t: r.error })
} else throw Error() } else throw Error()
case "vk": case "vk":
if (patternMatch["userId"] && patternMatch["videoId"] && patternMatch["userId"].length <= 10 && patternMatch["videoId"].length == 9) { if (patternMatch["userId"] && patternMatch["videoId"] &&
patternMatch["userId"].length <= 10 && patternMatch["videoId"].length == 9) {
let r = await vk({ let r = await vk({
userId: patternMatch["userId"], userId: patternMatch["userId"],
videoId: patternMatch["videoId"], videoId: patternMatch["videoId"],
lang: lang, quality: quality lang: lang, quality: quality
}); });
return (!r.error) ? apiJSON(2, { type: "bridge", lang: lang, u: r.url, filename: r.filename, service: host, ip: ip, salt: process.env.streamSalt }) : apiJSON(0, { t: r.error }); return (!r.error) ? apiJSON(2,
{ type: "bridge", lang: lang, u: r.url, filename:
r.filename, service: host, ip: ip, salt: process.env.streamSalt }) : apiJSON(0, { t: r.error });
} else throw Error() } else throw Error()
case "bilibili": case "bilibili":
if (patternMatch["id"] && patternMatch["id"].length >= 12) { if (patternMatch["id"] && patternMatch["id"].length >= 12) {
@ -69,7 +73,8 @@ export default async function (host, patternMatch, url, ip, lang, format, qualit
}) : apiJSON(0, { t: r.error }); }) : apiJSON(0, { t: r.error });
} else throw Error() } else throw Error()
case "reddit": case "reddit":
if (patternMatch["sub"] && patternMatch["id"] && patternMatch["title"] && patternMatch["sub"].length <= 22 && patternMatch["id"].length <= 10 && patternMatch["title"].length <= 96) { if (patternMatch["sub"] && patternMatch["id"] && patternMatch["title"] &&
patternMatch["sub"].length <= 22 && patternMatch["id"].length <= 10 && patternMatch["title"].length <= 96) {
let r = await reddit({ let r = await reddit({
sub: patternMatch["sub"], sub: patternMatch["sub"],
id: patternMatch["id"], id: patternMatch["id"],
@ -81,6 +86,19 @@ export default async function (host, patternMatch, url, ip, lang, format, qualit
filename: r.filename, salt: process.env.streamSalt filename: r.filename, salt: process.env.streamSalt
}) : apiJSON(0, { t: r.error }); }) : apiJSON(0, { t: r.error });
} else throw Error() } else throw Error()
case "tiktok":
if ((patternMatch["user"] && patternMatch["type"] == "video" && patternMatch["postId"] && patternMatch["postId"].length <= 21) ||
(patternMatch["id"] && patternMatch["id"].length <= 13)) {
let r = await tiktok({
postId: patternMatch["postId"],
id: patternMatch["id"], lang: lang,
});
return (!r.error) ? apiJSON(2, {
type: "bridge", u: r.urls, lang: lang,
service: host, ip: ip,
filename: r.filename, salt: process.env.streamSalt
}) : apiJSON(0, { t: r.error });
} else throw Error()
default: default:
return apiJSON(0, { t: errorUnsupported(lang) }) return apiJSON(0, { t: errorUnsupported(lang) })
} }

View file

@ -163,7 +163,7 @@ export default function(obj) {
<div id="theme-switcher" class="switch-container small-padding"> <div id="theme-switcher" class="switch-container small-padding">
<div class="subtitle">${loc(obj.lang, 'DownloadPopupDescription')}</div> <div class="subtitle">${loc(obj.lang, 'DownloadPopupDescription')}</div>
<div class="switches"> <div class="switches">
<a id="pd-download" class="switch full space-right" target="_blank"">${loc(obj.lang, 'Download')}</a> <a id="pd-download" class="switch full space-right" target="_blank" href="/">${loc(obj.lang, 'Download')}</a>
<div id="pd-copy" class="switch full">${loc(obj.lang, 'CopyURL')}</div> <div id="pd-copy" class="switch full">${loc(obj.lang, 'CopyURL')}</div>
</div> </div>
</div> </div>

View file

@ -66,7 +66,7 @@
"enabled": false "enabled": false
}, },
"tiktok": { "tiktok": {
"patterns": [":pageid/:type/:postid", ":id"], "patterns": [":user/:type/:postId", ":id"],
"enabled": false "enabled": true
} }
} }

View file

@ -0,0 +1,30 @@
import got from "got";
import loc from "../../localization/manager.js";
import { genericUserAgent} from "../config.js";
import { unicodeDecode } from "../sub/utils.js";
export default async function(obj) {
try {
if (!obj.postId) {
let html = await got.get(`https://vt.tiktok.com/${obj.id}`, { headers: { "user-agent": genericUserAgent } });
html.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'tiktok') };
});
html = html.body
obj.postId = html.split('video/')[1].split('?')[0]
}
let url = `https://tiktok.com/@video/video/${obj.postId}`
let html = await got.get(url, { headers: { "user-agent": genericUserAgent } });
html.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'tiktok') };
});
html = html.body;
if (html.includes(',"preloadList":[{"url":"')) {
return { urls: unicodeDecode(html.split(',"preloadList":[{"url":"')[1].split('","id":"')[0].trim()), filename: `tiktok_${obj.postId}.mp4` };
} else {
return { error: loc(obj.lang, 'ErrorEmptyDownload') };
}
} catch (e) {
return { error: loc(obj.lang, 'ErrorBadFetch') };
}
}

View file

@ -16,10 +16,10 @@ export async function streamDefault(streamInfo, res) {
isStream: true isStream: true
}); });
stream.pipe(res).on('error', (err) => { stream.pipe(res).on('error', (err) => {
throw Error("File stream pipe error."); internalError(res);
}); });
stream.on('error', (err) => { stream.on('error', (err) => {
throw Error("File stream error.") internalError(res);
}); });
} catch (e) { } catch (e) {
internalError(res); internalError(res);

View file

@ -35,7 +35,7 @@ export function msToTime(d) {
return r; return r;
} }
export function cleanURL(url, host) { export function cleanURL(url, host) {
url = url.replace('}', '').replace('{', '').replace(')', '').replace('(', '').replace(' ', ''); url = url.replace('}', '').replace('{', '').replace(')', '').replace('(', '').replace(' ', '').replace('@', '');
if (url.includes('youtube.com/shorts/')) { if (url.includes('youtube.com/shorts/')) {
url = url.split('?')[0].replace('shorts/', 'watch?v='); url = url.split('?')[0].replace('shorts/', 'watch?v=');
} }
@ -52,3 +52,8 @@ export function cleanURL(url, host) {
export function languageCode(req) { export function languageCode(req) {
return req.header('Accept-Language') ? req.header('Accept-Language').slice(0, 2) : "en" return req.header('Accept-Language') ? req.header('Accept-Language').slice(0, 2) : "en"
} }
export function unicodeDecode(str) {
return str.replace(/\\u[\dA-F]{4}/gi, (unicode) => {
return String.fromCharCode(parseInt(unicode.replace(/\\u/g, ""), 16));
});
}

View file

@ -93,8 +93,10 @@ button {
color: var(--accent); color: var(--accent);
font-size: 0.9rem; font-size: 0.9rem;
} }
input { input,
border-radius: none; input[type="text"],
[type="text"] {
border-radius: 0;
} }
button:hover, button:hover,
.switch:hover, .switch:hover,
@ -232,9 +234,6 @@ input[type="checkbox"] {
font-size: 0.9rem; font-size: 0.9rem;
max-height: 80%; max-height: 80%;
} }
.popup-big {
width: 55%;
}
#popup-backdrop { #popup-backdrop {
opacity: 0.5; opacity: 0.5;
background-color: var(--background); background-color: var(--background);