multi media tweets support

This commit is contained in:
wukko
2022-10-09 23:44:00 +06:00
parent 97cd8c6a11
commit 2c79ae3807
17 changed files with 280 additions and 145 deletions

View File

@@ -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 {

View File

@@ -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."
}, {

View File

@@ -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,

View File

@@ -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 `

View File

@@ -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>`;

View File

@@ -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 {

View File

@@ -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 });

View File

@@ -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,

View File

@@ -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,

View File

@@ -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') }
}

View File

@@ -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" } };
}