updated readme and fixed some things

This commit is contained in:
wukko 2022-12-17 17:22:39 +06:00
parent 7f1ba6b36b
commit 13c2884a41
9 changed files with 34 additions and 31 deletions

View file

@ -13,19 +13,22 @@ cobalt is social media downloader with zero bullshit. It's friendly, accessible,
It preserves original media quality so you get best downloads possible (unless you change that in settings). It preserves original media quality so you get best downloads possible (unless you change that in settings).
## Supported services ## Supported services
| Service | Video + Audio | Only audio | Additional features | | Service | Video + Audio | Only audio | Additional features |
| -------- | :---: | :---: | :----- | | -------- | :---: | :---: | :----- |
| Twitter | ✅ | ✅ | Ability to save multiple videos/GIFs from a single tweet. | | Twitter | ✅ | ✅ | Ability to save multiple videos/GIFs from a single tweet. |
| Twitter Spaces | ❌️ | ✅ | Audio metadata. | | Twitter Spaces | ❌️ | ✅ | Audio metadata. |
| YouTube & Shorts | ✅ | ✅ | Support for 8K, 4K, HDR, and high FPS videos. | | YouTube & Shorts | ✅ | ✅ | Support for 8K, 4K, HDR, and high FPS videos. |
| YouTube Music | ❌ | ✅ | Audio metadata. | | YouTube Music | ❌ | ✅ | Audio metadata. |
| Reddit | ✅ | ✅ | | | Reddit | ✅ | ✅ | |
| TikTok & douyin | ✅ | ✅ | Video downloads with or without watermark; image slideshow downloads without watermarks. | | TikTok & douyin | ✅ | ✅ | Video downloads with or without watermark; image slideshow downloads without watermarks. |
| SoundCloud | ❌ | ✅ | Audio metadata. | | SoundCloud | ❌ | ✅ | Audio metadata. |
| bilibili.com | ✅ | ✅ | | | bilibili.com | ✅ | ✅ | |
| Tumblr | ✅ | ✅ | | | Tumblr | ✅ | ✅ | |
| Vimeo | ✅ | ❌️ | | | Vimeo | ✅ | ❌️ | |
| VK Videos & Clips | ✅ | ❌️ | | | VK Videos & Clips | ✅ | ❌️ | |
## cobalt API
cobalt has an open API that you can use for free. It's pretty straightforward in use, [check out the docs](https://github.com/wukko/cobalt/blob/current/docs/API.md) and see for yourself.
## How to contribute translations ## How to contribute translations
You can translate cobalt to any language you want on [cobalt's crowdin](https://crowdin-co.wukko.me/). Feel free to ignore QA errors if you think you know better. If you don't see a language you want to translate cobalt to, open an issue, and I'll add it to crowdin. You can translate cobalt to any language you want on [cobalt's crowdin](https://crowdin-co.wukko.me/). Feel free to ignore QA errors if you think you know better. If you don't see a language you want to translate cobalt to, open an issue, and I'll add it to crowdin.

View file

@ -4,7 +4,7 @@ export default async function(obj) {
try { try {
let html = await fetch(`https://bilibili.com/video/${obj.id}`, { let html = await fetch(`https://bilibili.com/video/${obj.id}`, {
headers: {"user-agent": genericUserAgent} headers: {"user-agent": genericUserAgent}
}).then(async (r) => {return r.text()}).catch(() => {return false}); }).then((r) => {return r.text()}).catch(() => {return false});
if (!html) return { error: 'ErrorCouldntFetch' }; if (!html) return { error: 'ErrorCouldntFetch' };
if (html.includes('<script>window.__playinfo__=') && html.includes('"video_codecid"')) { if (html.includes('<script>window.__playinfo__=') && html.includes('"video_codecid"')) {

View file

@ -2,7 +2,7 @@ import { maxVideoDuration } from "../config.js";
export default async function(obj) { export default async function(obj) {
try { try {
let data = await fetch(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}/${obj.name}.json`).then(async (r) => {return r.json()}).catch(() => {return false}); let data = await fetch(`https://www.reddit.com/r/${obj.sub}/comments/${obj.id}/${obj.name}.json`).then((r) => {return r.json()}).catch(() => {return false});
if (!data) return { error: 'ErrorCouldntFetch' }; if (!data) return { error: 'ErrorCouldntFetch' };
data = data[0]["data"]["children"][0]["data"]; data = data[0]["data"]["children"][0]["data"];

View file

@ -4,7 +4,7 @@ let cachedID = {}
async function findClientID() { async function findClientID() {
try { try {
let sc = await fetch('https://soundcloud.com/').then(async (r) => {return r.text()}).catch(() => {return false}); let sc = await fetch('https://soundcloud.com/').then((r) => {return r.text()}).catch(() => {return false});
let sc_version = String(sc.match(/<script>window\.__sc_version="[0-9]{10}"<\/script>/)[0].match(/[0-9]{10}/)); let sc_version = String(sc.match(/<script>window\.__sc_version="[0-9]{10}"<\/script>/)[0].match(/[0-9]{10}/));
if (cachedID.version == sc_version) { if (cachedID.version == sc_version) {
@ -17,7 +17,7 @@ async function findClientID() {
if (url && !url.startsWith('https://a-v2.sndcdn.com')) return; if (url && !url.startsWith('https://a-v2.sndcdn.com')) return;
let scrf = await fetch(url).then(async (r) => {return r.text()}).catch(() => {return false}); let scrf = await fetch(url).then((r) => {return r.text()}).catch(() => {return false});
let id = scrf.match(/\("client_id=[A-Za-z0-9]{32}"\)/); let id = scrf.match(/\("client_id=[A-Za-z0-9]{32}"\)/);
if (id && typeof id[0] === 'string') { if (id && typeof id[0] === 'string') {
@ -41,12 +41,12 @@ export default async function(obj) {
if (!obj.author && !obj.song && obj.shortLink) { if (!obj.author && !obj.song && obj.shortLink) {
html = await fetch(`https://soundcloud.app.goo.gl/${obj.shortLink}/`, { html = await fetch(`https://soundcloud.app.goo.gl/${obj.shortLink}/`, {
headers: {"user-agent": genericUserAgent} headers: {"user-agent": genericUserAgent}
}).then(async (r) => {return r.text()}).catch(() => {return false}); }).then((r) => {return r.text()}).catch(() => {return false});
} }
if (obj.author && obj.song) { if (obj.author && obj.song) {
html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}`, { html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}`, {
headers: {"user-agent": genericUserAgent} headers: {"user-agent": genericUserAgent}
}).then(async (r) => {return r.text()}).catch(() => {return false}); }).then((r) => {return r.text()}).catch(() => {return false});
} }
if (!html) return { error: 'ErrorCouldntFetch'}; if (!html) return { error: 'ErrorCouldntFetch'};
if (html.includes('<script>window.__sc_hydration = ') && html.includes('"format":{"protocol":"progressive","mime_type":"audio/mpeg"},') && html.includes('{"hydratable":"sound","data":')) { if (html.includes('<script>window.__sc_hydration = ') && html.includes('"format":{"protocol":"progressive","mime_type":"audio/mpeg"},') && html.includes('{"hydratable":"sound","data":')) {

View file

@ -32,7 +32,7 @@ export default async function(obj) {
let html = await fetch(`${config[obj.host]["short"]}${obj.id}`, { let html = await fetch(`${config[obj.host]["short"]}${obj.id}`, {
redirect: "manual", redirect: "manual",
headers: { "user-agent": userAgent } headers: { "user-agent": userAgent }
}).then(async (r) => {return r.text()}).catch(() => {return false}); }).then((r) => {return r.text()}).catch(() => {return false});
if (!html) return { error: 'ErrorCouldntFetch' }; if (!html) return { error: 'ErrorCouldntFetch' };
if (html.slice(0, 17) === '<a href="https://' && html.includes('/video/')) { if (html.slice(0, 17) === '<a href="https://' && html.includes('/video/')) {
@ -46,7 +46,7 @@ export default async function(obj) {
let detail; let detail;
detail = await fetch(config[obj.host]["api"].replace("{postId}", obj.postId), { detail = await fetch(config[obj.host]["api"].replace("{postId}", obj.postId), {
headers: {"user-agent": "TikTok 26.2.0 rv:262018 (iPhone; iOS 14.4.2; en_US) Cronet"} headers: {"user-agent": "TikTok 26.2.0 rv:262018 (iPhone; iOS 14.4.2; en_US) Cronet"}
}).then(async (r) => {return r.json()}).catch(() => {return false}); }).then((r) => {return r.json()}).catch(() => {return false});
detail = selector(detail, obj.host, obj.postId); detail = selector(detail, obj.host, obj.postId);

View file

@ -5,7 +5,7 @@ export default async function(obj) {
let user = obj.user ? obj.user : obj.url.split('.')[0].replace('https://', ''); let user = obj.user ? obj.user : obj.url.split('.')[0].replace('https://', '');
let html = await fetch(`https://${user}.tumblr.com/post/${obj.id}`, { let html = await fetch(`https://${user}.tumblr.com/post/${obj.id}`, {
headers: {"user-agent": genericUserAgent} headers: {"user-agent": genericUserAgent}
}).then(async (r) => {return r.text()}).catch(() => {return false}); }).then((r) => {return r.text()}).catch(() => {return false});
if (!html) return { error: 'ErrorCouldntFetch' }; if (!html) return { error: 'ErrorCouldntFetch' };
if (html.includes('property="og:video" content="https://va.media.tumblr.com/')) { if (html.includes('property="og:video" content="https://va.media.tumblr.com/')) {
return { urls: `https://va.media.tumblr.com/${html.split('property="og:video" content="https://va.media.tumblr.com/')[1].split('"/>')[0]}`, audioFilename: `tumblr_${obj.id}_audio` } return { urls: `https://va.media.tumblr.com/${html.split('property="og:video" content="https://va.media.tumblr.com/')[1].split('"/>')[0]}`, audioFilename: `tumblr_${obj.id}_audio` }

View file

@ -15,13 +15,13 @@ export default async function(obj) {
let req_act = await fetch(`${apiURL}/guest/activate.json`, { let req_act = await fetch(`${apiURL}/guest/activate.json`, {
method: "POST", method: "POST",
headers: _headers headers: _headers
}).then(async (r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false}); }).then((r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false});
if (!req_act) return { error: 'ErrorCouldntFetch' }; if (!req_act) return { error: 'ErrorCouldntFetch' };
_headers["x-guest-token"] = req_act["guest_token"]; _headers["x-guest-token"] = req_act["guest_token"];
let showURL = `${apiURL}/statuses/show/${obj.id}.json?tweet_mode=extended&include_user_entities=0&trim_user=1&include_entities=0&cards_platform=Web-12&include_cards=1` let showURL = `${apiURL}/statuses/show/${obj.id}.json?tweet_mode=extended&include_user_entities=0&trim_user=1&include_entities=0&cards_platform=Web-12&include_cards=1`
if (!obj.spaceId) { if (!obj.spaceId) {
let req_status = await fetch(showURL, { headers: _headers }).then(async (r) => { return r.status == 200 ? r.json() : false;}).catch((e) => { return false}); let req_status = await fetch(showURL, { headers: _headers }).then((r) => { return r.status == 200 ? r.json() : false;}).catch((e) => { return false});
if (!req_status) { if (!req_status) {
_headers.authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw"; _headers.authorization = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw";
delete _headers["x-guest-token"] delete _headers["x-guest-token"]
@ -29,11 +29,11 @@ export default async function(obj) {
req_act = await fetch(`${apiURL}/guest/activate.json`, { req_act = await fetch(`${apiURL}/guest/activate.json`, {
method: "POST", method: "POST",
headers: _headers headers: _headers
}).then(async (r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false}); }).then((r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false});
if (!req_act) return { error: 'ErrorCouldntFetch' }; if (!req_act) return { error: 'ErrorCouldntFetch' };
_headers["x-guest-token"] = req_act["guest_token"]; _headers["x-guest-token"] = req_act["guest_token"];
req_status = await fetch(showURL, { headers: _headers }).then(async (r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false}); req_status = await fetch(showURL, { headers: _headers }).then((r) => { return r.status == 200 ? r.json() : false;}).catch(() => {return false});
} }
if (!req_status) return { error: 'ErrorCouldntFetch' } if (!req_status) return { error: 'ErrorCouldntFetch' }
if (req_status["extended_entities"] && req_status["extended_entities"]["media"]) { if (req_status["extended_entities"] && req_status["extended_entities"]["media"]) {
@ -63,13 +63,13 @@ export default async function(obj) {
variables: {"id": obj.spaceId,"isMetatagsQuery":true,"withSuperFollowsUserFields":true,"withDownvotePerspective":false,"withReactionsMetadata":false,"withReactionsPerspective":false,"withSuperFollowsTweetFields":true,"withReplays":true}, features: {"spaces_2022_h2_clipping":true,"spaces_2022_h2_spaces_communities":true,"verified_phone_label_enabled":false,"tweetypie_unmention_optimization_enabled":true,"responsive_web_uc_gql_enabled":true,"vibe_api_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":false,"interactive_text_enabled":true,"responsive_web_text_conversations_enabled":false,"responsive_web_enhance_cards_enabled":true} variables: {"id": obj.spaceId,"isMetatagsQuery":true,"withSuperFollowsUserFields":true,"withDownvotePerspective":false,"withReactionsMetadata":false,"withReactionsPerspective":false,"withSuperFollowsTweetFields":true,"withReplays":true}, features: {"spaces_2022_h2_clipping":true,"spaces_2022_h2_spaces_communities":true,"verified_phone_label_enabled":false,"tweetypie_unmention_optimization_enabled":true,"responsive_web_uc_gql_enabled":true,"vibe_api_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":false,"interactive_text_enabled":true,"responsive_web_text_conversations_enabled":false,"responsive_web_enhance_cards_enabled":true}
} }
let AudioSpaceById = await fetch(`https://twitter.com/i/api/graphql/wJ5g4zf7v8qPHSQbaozYuw/AudioSpaceById?variables=${new URLSearchParams(JSON.stringify(query.variables)).toString().slice(0, -1)}&features=${new URLSearchParams(JSON.stringify(query.features)).toString().slice(0, -1)}`, { headers: _headers }).then(async (r) => { let AudioSpaceById = await fetch(`https://twitter.com/i/api/graphql/wJ5g4zf7v8qPHSQbaozYuw/AudioSpaceById?variables=${new URLSearchParams(JSON.stringify(query.variables)).toString().slice(0, -1)}&features=${new URLSearchParams(JSON.stringify(query.features)).toString().slice(0, -1)}`, { headers: _headers }).then((r) => {
return r.status == 200 ? r.json() : false; return r.status == 200 ? r.json() : false;
}).catch((e) => {return false}); }).catch((e) => {return false});
if (AudioSpaceById) { if (AudioSpaceById) {
if (AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) { if (AudioSpaceById.data.audioSpace.metadata.is_space_available_for_replay === true) {
let streamStatus = await fetch(`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers }).then(async (r) => {return r.status == 200 ? r.json() : false;}).catch(() => {return false;}); let streamStatus = await fetch(`https://twitter.com/i/api/1.1/live_video_stream/status/${AudioSpaceById.data.audioSpace.metadata.media_key}`, { headers: _headers }).then((r) => {return r.status == 200 ? r.json() : false;}).catch(() => {return false;});
if (!streamStatus) return { error: 'ErrorCouldntFetch' }; if (!streamStatus) return { error: 'ErrorCouldntFetch' };
let participants = AudioSpaceById.data.audioSpace.participants.speakers let participants = AudioSpaceById.data.audioSpace.participants.speakers

View file

@ -2,7 +2,7 @@ import { quality, services } from "../config.js";
export default async function(obj) { export default async function(obj) {
try { try {
let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then(async (r) => {return r.json()}).catch(() => {return false}); let api = await fetch(`https://player.vimeo.com/video/${obj.id}/config`).then((r) => {return r.json()}).catch(() => {return false});
if (!api) return { error: 'ErrorCouldntFetch' }; if (!api) return { error: 'ErrorCouldntFetch' };
let downloadType = ""; let downloadType = "";
@ -32,7 +32,7 @@ export default async function(obj) {
return { urls: best["url"], filename: `tumblr_${obj.id}.mp4` }; return { urls: best["url"], filename: `tumblr_${obj.id}.mp4` };
case "dash": case "dash":
let masterJSONURL = api["request"]["files"]["dash"]["cdns"]["akfire_interconnect_quic"]["url"]; let masterJSONURL = api["request"]["files"]["dash"]["cdns"]["akfire_interconnect_quic"]["url"];
let masterJSON = await fetch(masterJSONURL).then(async (r) => {return r.json()}).catch(() => {return false}); let masterJSON = await fetch(masterJSONURL).then((r) => {return r.json()}).catch(() => {return false});
if (!masterJSON) return { error: 'ErrorCouldntFetch' }; if (!masterJSON) return { error: 'ErrorCouldntFetch' };
if (masterJSON.video) { if (masterJSON.video) {
let type = ""; let type = "";

View file

@ -7,7 +7,7 @@ export default async function(obj) {
let html; let html;
html = await fetch(`https://vk.com/video-${obj.userId}_${obj.videoId}`, { html = await fetch(`https://vk.com/video-${obj.userId}_${obj.videoId}`, {
headers: {"user-agent": genericUserAgent} headers: {"user-agent": genericUserAgent}
}).then(async (r) => {return r.text()}).catch(() => {return false}); }).then((r) => {return r.text()}).catch(() => {return false});
if (!html) return { error: 'ErrorCouldntFetch' }; if (!html) return { error: 'ErrorCouldntFetch' };
if (html.includes(`{"lang":`)) { if (html.includes(`{"lang":`)) {
let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]); let js = JSON.parse('{"lang":' + html.split(`{"lang":`)[1].split(']);')[0]);