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:
parent
a4a9af6120
commit
1b4872c1de
12 changed files with 86 additions and 41 deletions
26
README.md
26
README.md
|
@ -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.
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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/",
|
||||||
|
|
|
@ -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) } )
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) })
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
"tiktok": {
|
"tiktok": {
|
||||||
"patterns": [":pageid/:type/:postid", ":id"],
|
"patterns": [":user/:type/:postId", ":id"],
|
||||||
"enabled": false
|
"enabled": true
|
||||||
}
|
}
|
||||||
}
|
}
|
30
src/modules/services/tiktok.js
Normal file
30
src/modules/services/tiktok.js
Normal 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') };
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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));
|
||||||
|
});
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue