multi media tweets support
This commit is contained in:
@@ -5,7 +5,7 @@ import { services as patterns } from "./config.js";
|
||||
import { cleanURL, apiJSON } from "./sub/utils.js";
|
||||
import { errorUnsupported } from "./sub/errors.js";
|
||||
import loc from "../localization/manager.js";
|
||||
import match from "./match.js";
|
||||
import match from "./processing/match.js";
|
||||
|
||||
export async function getJSON(originalURL, lang, obj) {
|
||||
try {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
{
|
||||
"current": {
|
||||
"title": "less disturbance (3.6.2 + 3.6.3)",
|
||||
"content": "changelog popup no longer annoys you after a major update! this action has been replaced with a notification dot. if you see a red dot, then there's something new.\n\nyour old setting that disabled the changelog popup now applies to notifications.\n\nnew users will see a notification dot instead of an about popup, too. this was mostly done to prevent complications if your browser is set up to clean local storage when you close it.\n\nother changes:\n- popups are now a bit wider, just so more content fits at once.\n- better interface scaling.\n- code is a bit cleaner now.\n- changed twitter api endpoint. there should no longer be any rate limits."
|
||||
"title": "support for multi media tweets is here! (3.7)",
|
||||
"content": "{appName} now lets you save any of the videos or gifs in a tweet. even if there are many of them.\n\nsimply paste a link like you'd usually do and {appName} will ask what exactly you want to save.\n\nFIREFOX USERS: if you have strict tracking protection on, you might wanna turn it off for {appName}, or else twitter video previews won't load. firefox filters out twitter image cdn as if it was a tracker, which it's not. it's a false-positive.\n\nhowever, you can leave it on if you're fine with blank squares and video numbers. i have thought of that in prior, you're welcome.\n\nother changes:\n- repurposed ex tiktok-only image picker to be dynamic and adapt depending on content to pick. that's exactly how twitter multi media downloads work.\n- cobalt is now properly viewable on phones with tiny screens, such as first gen iphone se.\n- scrollbars now should be visible only where they're needed.\n- brought back proper twitter api, because other one doesn't have multi media stuff (at least yet).\n- cleaned up some internal files, including main frontend js file.\n- reorganized some files in project directory, now you won't get lost when contributing or just looking through cobalt's code."
|
||||
},
|
||||
"history": [{
|
||||
"title": "less disturbance (3.6.2 + 3.6.3)",
|
||||
"content": "changelog popup no longer annoys you after a major update! this action has been replaced with a notification dot. if you see a red dot, then there's something new.\n\nyour old setting that disabled the changelog popup now applies to notifications.\n\nnew users will see a notification dot instead of an about popup, too. this was mostly done to prevent complications if your browser is set up to clean local storage when you close it.\n\nother changes:\n- popups are now a bit wider, just so more content fits at once.\n- better interface scaling.\n- code is a bit cleaner now.\n- changed twitter api endpoint. there should no longer be any rate limits."
|
||||
}, {
|
||||
"title": "improvements all around! (3.6)",
|
||||
"content": "- download mode switcher is moving places, it's now right next to link input area.\n- smart mode has been renamed to auto mode, because this name is easier to understand.\n- all spacings in ui have been evened out. no more eye strain.\n- added support for twitter /video/1 links\n- clipboard button exception has been redone to prepare for adoption of readtext clipboard api in firefox.\n- cobalt is now using different tiktok api endpoint, because previous one got killed, just like the one before.\n- \"other\" settings tab has been cleaned up."
|
||||
}, {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import loadJson from "./sub/loadJSON.js";
|
||||
const config = loadJson("./src/config.json");
|
||||
const packageJson = loadJson("./package.json");
|
||||
const servicesConfigJson = loadJson("./src/modules/servicesConfig.json");
|
||||
const servicesConfigJson = loadJson("./src/modules/processing/servicesConfig.json");
|
||||
|
||||
export const
|
||||
services = servicesConfigJson.config,
|
||||
|
||||
@@ -44,11 +44,13 @@ export function popup(obj) {
|
||||
if (Array.isArray(obj.body)) {
|
||||
body = ``
|
||||
for (let i = 0; i < obj.body.length; i++) {
|
||||
classes = obj.body[i]["classes"] ? obj.body[i]["classes"] : []
|
||||
if (i != obj.body.length - 1 && !obj.body[i]["nopadding"]) {
|
||||
classes.push("desc-padding")
|
||||
if (obj.body[i]["text"].length > 0) {
|
||||
classes = obj.body[i]["classes"] ? obj.body[i]["classes"] : []
|
||||
if (i != obj.body.length - 1 && !obj.body[i]["nopadding"]) {
|
||||
classes.push("desc-padding")
|
||||
}
|
||||
body += obj.body[i]["raw"] ? obj.body[i]["text"] : `<div id="popup-desc" class="${classes.length > 0 ? classes.join(' ') : ''}">${obj.body[i]["text"]}</div>`
|
||||
}
|
||||
body += obj.body[i]["raw"] ? obj.body[i]["text"] : `<div id="popup-desc" class="${classes.length > 0 ? classes.join(' ') : ''}">${obj.body[i]["text"]}</div>`
|
||||
}
|
||||
}
|
||||
return `
|
||||
|
||||
@@ -84,8 +84,7 @@ export default function(obj) {
|
||||
}, {
|
||||
text: `${loc(obj.lang, 'AboutSupportedServices')} ${enabledServices}.`
|
||||
}, {
|
||||
text: obj.lang != "ru" ? `<div id="popup-desc" class="desc-padding">${loc(obj.lang, 'FollowTwitter')}</div>` : "",
|
||||
raw: true
|
||||
text: obj.lang != "ru" ? loc(obj.lang, 'FollowTwitter') : ""
|
||||
}, {
|
||||
text: backdropLink(repo, loc(obj.lang, 'LinkGitHubIssues')),
|
||||
classes: ["bottom-link"]
|
||||
@@ -271,14 +270,14 @@ export default function(obj) {
|
||||
})
|
||||
})}
|
||||
${popupWithBottomButtons({
|
||||
name: "imagePicker",
|
||||
name: "picker",
|
||||
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
|
||||
header: {
|
||||
title: loc(obj.lang, 'ImagePickerTitle'),
|
||||
explanation: isMobile ? loc(obj.lang, 'ImagePickerExplanationPhone') : loc(obj.lang, 'ImagePickerExplanationPC')
|
||||
title: `<div id="picker-title"></div>`,
|
||||
explanation: `<div id="picker-subtitle"></div>`,
|
||||
},
|
||||
buttons: [`<a id="imagepicker-download" class="switch" target="_blank" href="/">${loc(obj.lang, 'ImagePickerDownloadAudio')}</a>`],
|
||||
content: '<div id="imagepicker-holder"></div>'
|
||||
buttons: [`<a id="picker-download" class="switch" target="_blank" href="/">${loc(obj.lang, 'ImagePickerDownloadAudio')}</a>`],
|
||||
content: '<div id="picker-holder"></div>'
|
||||
})}
|
||||
${popup({
|
||||
name: "error",
|
||||
@@ -293,19 +292,6 @@ export default function(obj) {
|
||||
},
|
||||
body: `<div id="desc-error" class="desc-padding subtext"></div>`
|
||||
})}
|
||||
${popup({
|
||||
name: "info",
|
||||
standalone: true,
|
||||
buttonOnly: true,
|
||||
emoji: emoji("✨", 48, 1),
|
||||
classes: ["small"],
|
||||
buttonText: loc(obj.lang, 'ErrorPopupCloseButton'),
|
||||
header: {
|
||||
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
|
||||
title: "popup title"
|
||||
},
|
||||
body: `<div id="popup-info-desc" class="desc-padding subtext"></div>`
|
||||
})}
|
||||
<div id="popup-backdrop" style="visibility: hidden;" onclick="hideAllPopups()"></div>
|
||||
<div id="cobalt-main-box" class="center" style="visibility: hidden;">
|
||||
<div id="logo-area">${appName}</div>
|
||||
@@ -342,7 +328,11 @@ export default function(obj) {
|
||||
noURLReturned: ` + "`" + loc(obj.lang, 'ErrorNoUrlReturned') + "`" + `,
|
||||
unknownStatus: ` + "`" + loc(obj.lang, 'ErrorUnknownStatus') + "`" + `,
|
||||
toggleDefault: '${emoji("✨")} ${loc(obj.lang, "ModeToggleAuto")}',
|
||||
toggleAudio: '${emoji("🎶")} ${loc(obj.lang, "ModeToggleAudio")}'
|
||||
toggleAudio: '${emoji("🎶")} ${loc(obj.lang, "ModeToggleAudio")}',
|
||||
pickerDefault: ` + "`" + loc(obj.lang, 'MediaPickerTitle') + "`" + `,
|
||||
pickerImages: ` + "`" + loc(obj.lang, 'ImagePickerTitle') + "`" + `,
|
||||
pickerImagesExpl: ` + "`" + loc(obj.lang, `ImagePickerExplanation${isMobile ? "Phone" : "PC"}`) + "`" + `,
|
||||
pickerDefaultExpl: ` + "`" + loc(obj.lang, `MediaPickerExplanation${isMobile ? `Phone${isIOS ? "IOS" : ""}` : "PC"}`) + "`" + `,
|
||||
};</script>
|
||||
<script type="text/javascript" src="cobalt.js"></script>
|
||||
</html>`;
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { apiJSON } from "./sub/utils.js";
|
||||
import { errorUnsupported, genericError } from "./sub/errors.js";
|
||||
import { apiJSON } from "../sub/utils.js";
|
||||
import { errorUnsupported, genericError } from "../sub/errors.js";
|
||||
|
||||
import { testers } from "./servicesPatternTesters.js";
|
||||
|
||||
import bilibili from "./services/bilibili.js";
|
||||
import reddit from "./services/reddit.js";
|
||||
import twitter from "./services/twitter.js";
|
||||
import youtube from "./services/youtube.js";
|
||||
import vk from "./services/vk.js";
|
||||
import tiktok from "./services/tiktok.js";
|
||||
import tumblr from "./services/tumblr.js";
|
||||
import matchActionDecider from "./sub/matchActionDecider.js";
|
||||
import vimeo from "./services/vimeo.js";
|
||||
import soundcloud from "./services/soundcloud.js";
|
||||
import bilibili from "../services/bilibili.js";
|
||||
import reddit from "../services/reddit.js";
|
||||
import twitter from "../services/twitter.js";
|
||||
import youtube from "../services/youtube.js";
|
||||
import vk from "../services/vk.js";
|
||||
import tiktok from "../services/tiktok.js";
|
||||
import tumblr from "../services/tumblr.js";
|
||||
import matchActionDecider from "./matchActionDecider.js";
|
||||
import vimeo from "../services/vimeo.js";
|
||||
import soundcloud from "../services/soundcloud.js";
|
||||
|
||||
export default async function (host, patternMatch, url, lang, obj) {
|
||||
try {
|
||||
@@ -1,9 +1,9 @@
|
||||
import { audioIgnore, services, supportedAudio } from "../config.js"
|
||||
import { apiJSON } from "./utils.js"
|
||||
import { apiJSON } from "../sub/utils.js"
|
||||
|
||||
export default function(r, host, ip, audioFormat, isAudioOnly) {
|
||||
if (!r.error) {
|
||||
if (!isAudioOnly) {
|
||||
if (!isAudioOnly && !r.picker) {
|
||||
switch (host) {
|
||||
case "twitter":
|
||||
return apiJSON(1, { u: r.urls });
|
||||
@@ -44,13 +44,26 @@ export default function(r, host, ip, audioFormat, isAudioOnly) {
|
||||
case "vimeo":
|
||||
return apiJSON(1, { u: r.urls });
|
||||
}
|
||||
} else if (r.picker) {
|
||||
switch (host) {
|
||||
case "douyin":
|
||||
case "tiktok":
|
||||
return apiJSON(5, {
|
||||
picker: r.picker,
|
||||
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
|
||||
filename: r.audioFilename, salt: process.env.streamSalt, isAudioOnly: true, audioFormat: audioFormat, copy: audioFormat == "best" ? true : false
|
||||
})
|
||||
case "twitter":
|
||||
return apiJSON(5, {
|
||||
picker: r.picker, service: host
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (host == "reddit" && r.typeId == 1 || audioIgnore.includes(host)) return apiJSON(0, { t: r.audioFilename });
|
||||
|
||||
let type = "render";
|
||||
let copy = false;
|
||||
if (!supportedAudio.includes(audioFormat)) audioFormat = "best";
|
||||
|
||||
if (!supportedAudio.includes(audioFormat)) audioFormat = "best";
|
||||
if ((host == "tiktok" || host == "douyin") && isAudioOnly && services.tiktok.audioFormats.includes(audioFormat)) {
|
||||
if (r.isMp3) {
|
||||
if (audioFormat == "mp3" || audioFormat == "best") {
|
||||
@@ -62,7 +75,6 @@ export default function(r, host, ip, audioFormat, isAudioOnly) {
|
||||
type = "bridge"
|
||||
}
|
||||
}
|
||||
|
||||
if ((audioFormat == "best" && services[host]["bestAudio"]) || services[host]["bestAudio"] && (audioFormat == services[host]["bestAudio"])) {
|
||||
audioFormat = services[host]["bestAudio"]
|
||||
type = "bridge"
|
||||
@@ -70,20 +82,11 @@ export default function(r, host, ip, audioFormat, isAudioOnly) {
|
||||
audioFormat = "m4a"
|
||||
copy = true
|
||||
}
|
||||
if ((host == "tiktok" || host == "douyin") && r.images) {
|
||||
return apiJSON(5, {
|
||||
type: type,
|
||||
images: r.images,
|
||||
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
|
||||
filename: r.audioFilename, salt: process.env.streamSalt, isAudioOnly: true, audioFormat: audioFormat, copy: copy
|
||||
})
|
||||
} else {
|
||||
return apiJSON(2, {
|
||||
type: type,
|
||||
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
|
||||
filename: r.audioFilename, salt: process.env.streamSalt, isAudioOnly: true, audioFormat: audioFormat, copy: copy
|
||||
})
|
||||
}
|
||||
return apiJSON(2, {
|
||||
type: type,
|
||||
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
|
||||
filename: r.audioFilename, salt: process.env.streamSalt, isAudioOnly: true, audioFormat: audioFormat, copy: copy
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return apiJSON(0, { t: r.error });
|
||||
@@ -17,6 +17,9 @@
|
||||
},
|
||||
"vk": {
|
||||
"alias": "vk clips, vk video",
|
||||
"localizedAlias": {
|
||||
"ru": "vk видео, vk клипы"
|
||||
},
|
||||
"patterns": ["video-:userId_:videoId", "clip-:userId_:videoId", "clips-:userId?z=clip-:userId_:videoId"],
|
||||
"quality_match": {
|
||||
"2160": 7,
|
||||
@@ -93,10 +93,10 @@ export default async function(obj) {
|
||||
if (images) {
|
||||
let imageLinks = [];
|
||||
for (let i in images) {
|
||||
imageLinks.push(images[i]["display_image"]["url_list"][0])
|
||||
imageLinks.push({url: images[i]["display_image"]["url_list"][0]})
|
||||
}
|
||||
return {
|
||||
images: imageLinks,
|
||||
picker: imageLinks,
|
||||
urls: audio,
|
||||
audioFilename: audioFilename,
|
||||
isAudioOnly: true,
|
||||
|
||||
@@ -2,18 +2,51 @@ import got from "got";
|
||||
import loc from "../../localization/manager.js";
|
||||
import { genericUserAgent } from "../config.js";
|
||||
|
||||
function bestQuality(arr) {
|
||||
return arr.filter((v) => { if (v["content_type"] == "video/mp4") return true; }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"].split("?")[0]
|
||||
}
|
||||
const apiURL = "https://api.twitter.com/1.1"
|
||||
|
||||
export default async function(obj) {
|
||||
try {
|
||||
let req_status = await got.get(`https://cdn.syndication.twimg.com/tweet?id=${obj.id}&tweet_mode=extended`, {
|
||||
headers: { "User-Agent": genericUserAgent }
|
||||
let _headers = {
|
||||
"User-Agent": genericUserAgent,
|
||||
"Authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA",
|
||||
"Host": "api.twitter.com",
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": 0
|
||||
};
|
||||
let req_act = await got.post(`${apiURL}/guest/activate.json`, {
|
||||
headers: _headers
|
||||
});
|
||||
req_act.on('error', (err) => {
|
||||
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'twitter') }
|
||||
})
|
||||
_headers["x-guest-token"] = req_act.body["guest_token"];
|
||||
let req_status = await got.get(`${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`, {
|
||||
headers: _headers
|
||||
});
|
||||
req_status.on('error', (err) => {
|
||||
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'twitter') }
|
||||
})
|
||||
let parsbod = JSON.parse(req_status.body);
|
||||
if (parsbod["video"] && parsbod["video"]["variants"]) {
|
||||
let media = parsbod["video"]["variants"];
|
||||
return { urls: media.filter((v) => { if (v["type"] == "video/mp4") return true; }).sort((a, b) => Number(b["src"].split("vid/")[1].split("x")[0]) - Number(a["src"].split("vid/")[1].split("x")[0]))[0]["src"].split('?')[0], audioFilename: `twitter_${obj.id}_audio` }
|
||||
if (parsbod["extended_entities"] && parsbod["extended_entities"]["media"]) {
|
||||
let single, multiple = [], media = parsbod["extended_entities"]["media"];
|
||||
media = media.filter((i) => { if (i["type"] == "video" || i["type"] == "animated_gif") return true })
|
||||
if (media.length > 1) {
|
||||
for (let i in media) {
|
||||
multiple.push({type: "video", thumb: media[i]["media_url_https"], url: bestQuality(media[i]["video_info"]["variants"])})
|
||||
}
|
||||
} else {
|
||||
single = bestQuality(media[0]["video_info"]["variants"])
|
||||
}
|
||||
if (single) {
|
||||
return { urls: single, audioFilename: `twitter_${obj.id}_audio` }
|
||||
} else if (multiple) {
|
||||
return { picker: multiple }
|
||||
} else {
|
||||
return { error: loc(obj.lang, 'ErrorNoVideosInTweet') }
|
||||
}
|
||||
} else {
|
||||
return { error: loc(obj.lang, 'ErrorNoVideosInTweet') }
|
||||
}
|
||||
|
||||
@@ -14,7 +14,15 @@ export function apiJSON(type, obj) {
|
||||
case 4:
|
||||
return { status: 429, body: { status: "rate-limit", text: obj.t } };
|
||||
case 5:
|
||||
return { status: 200, body: { status: "images", images: obj.images, url: createStream(obj) } };
|
||||
let pickerType = "various", audio = false
|
||||
switch (obj.service) {
|
||||
case "douyin":
|
||||
case "tiktok":
|
||||
audio = createStream(obj)
|
||||
pickerType = "images"
|
||||
break;
|
||||
}
|
||||
return { status: 200, body: { status: "picker", pickerType: pickerType, url: obj.picker, audio: audio } };
|
||||
default:
|
||||
return { status: 400, body: { status: "error", text: "Bad Request" } };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user