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

@ -1,7 +1,7 @@
{ {
"name": "cobalt", "name": "cobalt",
"description": "save what you love", "description": "save what you love",
"version": "3.6.3", "version": "3.7",
"author": "wukko", "author": "wukko",
"exports": "./src/cobalt.js", "exports": "./src/cobalt.js",
"type": "module", "type": "module",

View file

@ -66,7 +66,7 @@ a {
::placeholder { ::placeholder {
color: var(--accent-unhover-2); color: var(--accent-unhover-2);
} }
::-webkit-scrollbar { .switches::-webkit-scrollbar, #popup-content::-webkit-scrollbar {
display: none; display: none;
} }
:focus-visible { :focus-visible {
@ -128,7 +128,7 @@ button:active,
cursor: pointer; cursor: pointer;
transform: scale(0.95) transform: scale(0.95)
} }
.imagepicker-image:active { .picker-image:active {
cursor: pointer; cursor: pointer;
transform: scale(0.95) transform: scale(0.95)
} }
@ -422,7 +422,7 @@ input[type="checkbox"] {
margin-top: 0.5rem; margin-top: 0.5rem;
} }
.explanation { .explanation {
padding-top: 1rem; margin-top: 1rem;
width: 100%; width: 100%;
font-size: 0.8rem; font-size: 0.8rem;
text-align: left; text-align: left;
@ -503,29 +503,54 @@ input[type="checkbox"] {
.button:active .tooltip { .button:active .tooltip {
display: none; display: none;
} }
.imagepicker-image { .picker-image {
object-fit: cover; object-fit: cover;
width: inherit; width: inherit;
height: inherit; height: inherit;
} }
.imagepicker-image-container { .picker-image-container {
width: 8rem; width: 8rem;
height: 8rem; height: 8rem;
margin-bottom: 1rem; margin-bottom: 1rem;
background-color: var(--accent-button-bg); background-color: var(--accent-button-bg);
text-align: center;
letter-spacing: -0.2rem;
line-height: 8rem;
} }
#imagepicker-holder { .picker-various-container {
height: 20rem;
width: 25rem;
margin-bottom: 1rem;
background-color: var(--accent-button-bg);
position: relative;
}
#picker-holder {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap; flex-wrap: wrap;
align-content: space-around; align-content: space-around;
} }
#popup-imagePicker .explanation { #picker-holder.various {
padding-top: 0!important; justify-content: left;
padding-bottom: 1rem; flex-wrap: unset;
overflow-x: scroll;
gap: 2rem;
}
.imageBlock {
height: 100%;
width: 100%;
position: absolute;
z-index: 9999;
}
.picker-element-name {
position: absolute;
background: var(--background);
color: var(--accent);
padding: 0.3rem 0.6rem;
font-size: 0.8rem;
opacity: 0.7;
margin: 0.4rem;
}
#popup-picker .explanation {
margin-top: 0!important;
margin-bottom: 1rem;
} }
.popup-tabs { .popup-tabs {
padding-top: 0.5rem; padding-top: 0.5rem;
@ -592,7 +617,7 @@ input[type="checkbox"] {
width: 60%; width: 60%;
} }
} }
@media screen and (max-height: 850px) { @media screen and (max-height: 605px) {
.popup { .popup {
height: 80% height: 80%
} }
@ -611,7 +636,7 @@ input[type="checkbox"] {
} }
@media screen and (max-width: 475px) { @media screen and (max-width: 475px) {
.tab { .tab {
font-size: 0; font-size: 0!important;
} }
.tab .emoji { .tab .emoji {
margin-right: 0; margin-right: 0;
@ -621,17 +646,49 @@ input[type="checkbox"] {
} }
} }
@media screen and (max-width: 320px) { @media screen and (max-width: 320px) {
#popup-title {
font-size: 1.3rem;
line-height: 2rem;
}
.footer-button { .footer-button {
font-size: 0; font-size: 0!important;
} }
#cobalt-main-box #bottom button { .switch, .checkbox, .category-title, .subtitle, #popup-desc {
font-size: 0; font-size: .75rem;
} }
.footer-button .emoji, #cobalt-main-box #bottom button .emoji { .explanation {
font-size: .77rem;
margin-top: 0.8rem;
}
#popup-desc {
line-height: 1.4rem;
}
.changelog-subtitle, #popup-subtitle {
font-size: 0.9rem!important;
}
.category-title {
margin-bottom: 0.8rem;
}
.footer-button .emoji {
margin-right: 0; margin-right: 0;
} }
} }
@media screen and (max-width: 949px) { @media screen and (max-width: 949px) {
#picker-holder::-webkit-scrollbar {
display: none;
}
#picker-holder.various {
flex-wrap: wrap;
align-content: left;
gap: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.picker-various-container {
width: 100%;
height: 20rem;
max-width: 100%;
}
#cobalt-main-box #bottom { #cobalt-main-box #bottom {
flex-direction: column; flex-direction: column;
} }
@ -649,7 +706,7 @@ input[type="checkbox"] {
.footer-pair .footer-button { .footer-pair .footer-button {
width: 100%!important; width: 100%!important;
} }
.imagepicker-image-container { .picker-image-container {
height: 7rem; height: 7rem;
width: 7rem; width: 7rem;
line-height: 7rem; line-height: 7rem;
@ -687,7 +744,7 @@ input[type="checkbox"] {
.popup-title { .popup-title {
line-height: inherit; line-height: inherit;
} }
.imagepicker-image-container { .picker-image-container {
line-height: 6rem; line-height: 6rem;
height: 6rem; height: 6rem;
width: 6rem; width: 6rem;

View file

@ -1,5 +1,5 @@
let isIOS = navigator.userAgent.toLowerCase().match("iphone os"); let isIOS = navigator.userAgent.toLowerCase().match("iphone os");
let version = 12; let version = 13;
let regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/); let regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/);
let notification = `<div class="notification-dot"></div>` let notification = `<div class="notification-dot"></div>`
@ -141,8 +141,8 @@ function hideAllPopups() {
for (let i = 0; i < filter.length; i++) { for (let i = 0; i < filter.length; i++) {
filter[i].style.visibility = "hidden"; filter[i].style.visibility = "hidden";
} }
eid("imagepicker-holder").innerHTML = ''; eid("picker-holder").innerHTML = '';
eid("imagepicker-download").href = '/'; eid("picker-download").href = '/';
eid("popup-backdrop").style.visibility = "hidden"; eid("popup-backdrop").style.visibility = "hidden";
} }
function popup(type, action, text) { function popup(type, action, text) {
@ -163,19 +163,44 @@ function popup(type, action, text) {
eid("pd-download").href = text; eid("pd-download").href = text;
eid("pd-copy").setAttribute("onClick", `copy('pd-copy', '${text}')`); eid("pd-copy").setAttribute("onClick", `copy('pd-copy', '${text}')`);
break; break;
case "imagePicker": case "picker":
eid("imagepicker-download").href = text.url; switch (text.type) {
for (let i in text.images) { case "images":
eid("imagepicker-holder").innerHTML += `<div class="imagepicker-image-container"><img class="imagepicker-image" src="${text.images[i]}" onerror="this.parentNode.style.display='none'"></img></div>` eid("picker-title").innerHTML = loc.pickerImages;
eid("picker-subtitle").innerHTML = loc.pickerImagesExpl;
if (!eid("popup-picker").classList.contains("scrollable")) eid("popup-picker").classList.add("scrollable");
if (eid("picker-holder").classList.contains("various")) eid("picker-holder").classList.remove("various");
eid("picker-download").href = text.audio;
eid("picker-download").style.visibility = "visible"
for (let i in text.arr) {
eid("picker-holder").innerHTML += `<a class="picker-image-container"><img class="picker-image" src="${text.arr[i]["url"]}" onerror="this.parentNode.style.display='none'"></img></a>`
}
break;
default:
eid("picker-title").innerHTML = loc.pickerDefault;
eid("picker-subtitle").innerHTML = loc.pickerDefaultExpl;
if (eid("popup-picker").classList.contains("scrollable")) eid("popup-picker").classList.remove("scrollable");
if (!eid("picker-holder").classList.contains("various")) eid("picker-holder").classList.add("various");
for (let i in text.arr) {
let s = text.arr[i], item;
switch (s.type) {
case "video":
item = `<a class="picker-various-container" href="${text.arr[i]["url"]}" target="_blank"><div class="picker-element-name">VIDEO ${Number(i)+1}</div><div class="imageBlock"></div><img class="picker-image" src="${text.arr[i]["thumb"]}" onerror="this.style.display='none'"></img></a>`
break;
}
eid("picker-holder").innerHTML += item
}
eid("picker-download").style.visibility = "hidden";
break;
} }
break; break;
default: default:
break; break;
} }
} else { } else {
if (type == "imagePicker") { if (type == "picker") {
eid("imagepicker-download").href = '/'; eid("picker-download").href = '/';
eid("imagepicker-holder").innerHTML = '' eid("picker-holder").innerHTML = ''
} }
} }
eid("popup-backdrop").style.visibility = vis(action); eid("popup-backdrop").style.visibility = vis(action);
@ -261,6 +286,21 @@ function loadSettings() {
changeSwitcher(i, sGet(i)) changeSwitcher(i, sGet(i))
} }
} }
function changeButton(type, text) {
switch (type) {
case 0: //error
eid("url-input-area").disabled = false
eid("url-clear").style.display = "block";
changeDownloadButton(2, '!!')
popup("error", 1, text);
break;
case 1: //enable back
changeDownloadButton(1, '>>');
eid("url-clear").style.display = "block";
eid("url-input-area").disabled = false
break;
}
}
async function pasteClipboard() { async function pasteClipboard() {
let t = await navigator.clipboard.readText(); let t = await navigator.clipboard.readText();
if (regex.test(t)) { if (regex.test(t)) {
@ -291,64 +331,52 @@ async function download(url) {
if (j.url) { if (j.url) {
switch (j.status) { switch (j.status) {
case "redirect": case "redirect":
changeDownloadButton(2, '>>>') changeDownloadButton(2, '>>>');
setTimeout(() => { setTimeout(() => { changeButton(1); }, 3000);
changeDownloadButton(1, '>>'); sGet("downloadPopup") == "true" ? popup('download', 1, j.url) : window.open(j.url, '_blank');
eid("url-clear").style.display = "block"; break;
eid("url-input-area").disabled = false case "picker":
}, 3000) if (j.audio && j.url) {
if (sGet("downloadPopup") == "true") { changeDownloadButton(2, '?..')
popup('download', 1, j.url) fetch(`${j.audio}&p=1&origin=front`).then(async (res) => {
let jp = await res.json();
if (jp.status === "continue") {
changeDownloadButton(2, '>>>');
popup('picker', 1, { audio: j.audio, arr: j.url, type: j.pickerType });
setTimeout(() => { changeButton(1) }, 5000);
} else {
changeButton(0, jp.text);
}
}).catch((error) => internetError());
} else if (j.url) {
changeDownloadButton(2, '>>>');
popup('picker', 1, { arr: j.url, type: j.pickerType });
setTimeout(() => { changeButton(1) }, 5000);
} else { } else {
window.open(j.url, '_blank'); changeButton(0, loc.noURLReturned);
} }
break; break;
case "images":
case "stream": case "stream":
changeDownloadButton(2, '?..') changeDownloadButton(2, '?..')
fetch(`${j.url}&p=1&origin=front`).then(async (res) => { fetch(`${j.url}&p=1&origin=front`).then(async (res) => {
let jp = await res.json(); let jp = await res.json();
if (jp.status == "continue") { if (jp.status === "continue") {
changeDownloadButton(2, '>>>') changeDownloadButton(2, '>>>'); window.location.href = j.url;
if (j.status === "images") { setTimeout(() => { changeButton(1) }, 5000);
popup('imagePicker', 1, {
url: j.url,
images: j.images
})
} else {
window.location.href = j.url
}
setTimeout(() => {
changeDownloadButton(1, '>>');
eid("url-clear").style.display = "block";
eid("url-input-area").disabled = false
}, 5000)
} else { } else {
eid("url-input-area").disabled = false changeButton(0, jp.text);
changeDownloadButton(2, '!!');
eid("url-clear").style.display = "block";
popup("error", 1, jp.text);
} }
}).catch((error) => internetError()); }).catch((error) => internetError());
break; break;
default: default:
eid("url-input-area").disabled = false changeButton(0, loc.unknownStatus);
changeDownloadButton(2, '!!');
eid("url-clear").style.display = "block";
popup("error", 1, loc.unknownStatus);
break; break;
} }
} else { } else {
eid("url-input-area").disabled = false changeButton(0, loc.noURLReturned);
eid("url-clear").style.display = "block";
changeDownloadButton(2, '!!')
popup("error", 1, loc.noURLReturned);
} }
} else { } else {
eid("url-input-area").disabled = false changeButton(0, j.text);
eid("url-clear").style.display = "block";
changeDownloadButton(2, '!!')
popup("error", 1, j.text);
} }
}).catch((error) => internetError()); }).catch((error) => internetError());
} }

View file

@ -1,7 +1,7 @@
{ {
"name": "english", "name": "english",
"substrings": { "substrings": {
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">contact the maintainer/a>" "ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">contact the maintainer</a>"
}, },
"strings": { "strings": {
"LinkInput": "paste the link here", "LinkInput": "paste the link here",
@ -61,7 +61,7 @@
"LinkGitHubChanges": "&gt;&gt; see previous commits and contribute on github", "LinkGitHubChanges": "&gt;&gt; see previous commits and contribute on github",
"LinkDonateContact": "&gt;&gt; let me know if currency you want to donate isn't listed", "LinkDonateContact": "&gt;&gt; let me know if currency you want to donate isn't listed",
"NoScriptMessage": "{appName} uses javascript for api requests and interactive interface. you have to allow javascript to use this site. i don't have any ads or trackers, pinky promise.", "NoScriptMessage": "{appName} uses javascript for api requests and interactive interface. you have to allow javascript to use this site. i don't have any ads or trackers, pinky promise.",
"DownloadPopupDescriptionIOS": "because you have an ios device, you have to press and hold the download button and then select \"download video\" in appeared popup to save the video. this will be required for as long as apple forces safari webview upon all browser developers on ios.", "DownloadPopupDescriptionIOS": "on ios devices, you have to press and hold the download button, hide the video preview, and then select \"download linked file\" in appeared popup to save the video. this will be required for as long as apple forces safari webview upon all browser developers on ios.",
"DownloadPopupDescription": "download button opens a new tab with requested file. you can disable this popup in settings.", "DownloadPopupDescription": "download button opens a new tab with requested file. you can disable this popup in settings.",
"DownloadPopupWayToSave": "pick a way to save", "DownloadPopupWayToSave": "pick a way to save",
"ClickToCopy": "press to copy", "ClickToCopy": "press to copy",
@ -101,6 +101,10 @@
"Miscellaneous": "miscellaneous", "Miscellaneous": "miscellaneous",
"ModeToggleAuto": "auto mode", "ModeToggleAuto": "auto mode",
"ModeToggleAudio": "audio mode", "ModeToggleAudio": "audio mode",
"SettingsDisableNotifications": "hide notification dots" "SettingsDisableNotifications": "hide notification dots",
"MediaPickerTitle": "pick what to save",
"MediaPickerExplanationPC": "click or right click to download what you want.",
"MediaPickerExplanationPhone": "press or press and hold to download what you want.",
"MediaPickerExplanationPhoneIOS": "press and hold, hide the preview, and then select \"download linked file\" to save."
} }
} }

View file

@ -62,7 +62,7 @@
"LinkGitHubChanges": "&gt;&gt; смотри предыдущие изменения на гитхабе", "LinkGitHubChanges": "&gt;&gt; смотри предыдущие изменения на гитхабе",
"LinkDonateContact": "&gt;&gt; напиши мне, если в этом списке нет подходящей валюты", "LinkDonateContact": "&gt;&gt; напиши мне, если в этом списке нет подходящей валюты",
"NoScriptMessage": "{appName} использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких трекеров или рекламы, обещаю.", "NoScriptMessage": "{appName} использует javascript для обработки ссылок и интерактивного интерфейса. ты должен разрешить использование javascript, чтобы пользоваться сайтом. тут нет никаких трекеров или рекламы, обещаю.",
"DownloadPopupDescriptionIOS": "так как у тебя устройство на ios, тебе нужно зажать кнопку \"скачать\" и выбрать что-то похожее на \"сохранить в галерею\" в появившемся окне.", "DownloadPopupDescriptionIOS": "так как у тебя устройство на ios, тебе нужно зажать кнопку \"скачать\", затем скрыть превью видео и выбрать что-то похожее на \"сохранить файл\" в появившемся окне.",
"DownloadPopupDescription": "кнопка скачивания открывает новое окно с файлом. ты можешь отключить выбор метода сохранения файла в настройках.", "DownloadPopupDescription": "кнопка скачивания открывает новое окно с файлом. ты можешь отключить выбор метода сохранения файла в настройках.",
"DownloadPopupWayToSave": "выбери, как сохранить", "DownloadPopupWayToSave": "выбери, как сохранить",
"ClickToCopy": "нажми, чтобы скопировать", "ClickToCopy": "нажми, чтобы скопировать",
@ -104,6 +104,10 @@
"Miscellaneous": "разное", "Miscellaneous": "разное",
"ModeToggleAuto": "авто режим", "ModeToggleAuto": "авто режим",
"ModeToggleAudio": "аудио режим", "ModeToggleAudio": "аудио режим",
"SettingsDisableNotifications": "скрыть значки уведомлений" "SettingsDisableNotifications": "скрыть значки уведомлений",
"MediaPickerTitle": "выбери, что сохранить",
"MediaPickerExplanationPC": "кликни, чтобы скачать. также можно скачать через контекстное меню правой кнопки мыши.",
"MediaPickerExplanationPhone": "нажми, или нажми и удерживай, чтобы скачать.",
"MediaPickerExplanationPhoneIOS": "нажми и удерживай, затем скрой превью, и наконец выбери что-то похожее на \"сохранить файл\", чтобы скачать."
} }
} }

View file

@ -5,7 +5,7 @@ import { services as patterns } from "./config.js";
import { cleanURL, apiJSON } from "./sub/utils.js"; import { cleanURL, apiJSON } from "./sub/utils.js";
import { errorUnsupported } from "./sub/errors.js"; import { errorUnsupported } from "./sub/errors.js";
import loc from "../localization/manager.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) { export async function getJSON(originalURL, lang, obj) {
try { try {

View file

@ -1,9 +1,12 @@
{ {
"current": { "current": {
"title": "less disturbance (3.6.2 + 3.6.3)", "title": "support for multi media tweets is here! (3.7)",
"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." "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": [{ "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)", "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." "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"; import loadJson from "./sub/loadJSON.js";
const config = loadJson("./src/config.json"); const config = loadJson("./src/config.json");
const packageJson = loadJson("./package.json"); const packageJson = loadJson("./package.json");
const servicesConfigJson = loadJson("./src/modules/servicesConfig.json"); const servicesConfigJson = loadJson("./src/modules/processing/servicesConfig.json");
export const export const
services = servicesConfigJson.config, services = servicesConfigJson.config,

View file

@ -44,11 +44,13 @@ export function popup(obj) {
if (Array.isArray(obj.body)) { if (Array.isArray(obj.body)) {
body = `` body = ``
for (let i = 0; i < obj.body.length; i++) { for (let i = 0; i < obj.body.length; i++) {
classes = obj.body[i]["classes"] ? obj.body[i]["classes"] : [] if (obj.body[i]["text"].length > 0) {
if (i != obj.body.length - 1 && !obj.body[i]["nopadding"]) { classes = obj.body[i]["classes"] ? obj.body[i]["classes"] : []
classes.push("desc-padding") 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 ` return `

View file

@ -84,8 +84,7 @@ export default function(obj) {
}, { }, {
text: `${loc(obj.lang, 'AboutSupportedServices')} ${enabledServices}.` text: `${loc(obj.lang, 'AboutSupportedServices')} ${enabledServices}.`
}, { }, {
text: obj.lang != "ru" ? `<div id="popup-desc" class="desc-padding">${loc(obj.lang, 'FollowTwitter')}</div>` : "", text: obj.lang != "ru" ? loc(obj.lang, 'FollowTwitter') : ""
raw: true
}, { }, {
text: backdropLink(repo, loc(obj.lang, 'LinkGitHubIssues')), text: backdropLink(repo, loc(obj.lang, 'LinkGitHubIssues')),
classes: ["bottom-link"] classes: ["bottom-link"]
@ -271,14 +270,14 @@ export default function(obj) {
}) })
})} })}
${popupWithBottomButtons({ ${popupWithBottomButtons({
name: "imagePicker", name: "picker",
closeAria: loc(obj.lang, 'AccessibilityClosePopup'), closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
header: { header: {
title: loc(obj.lang, 'ImagePickerTitle'), title: `<div id="picker-title"></div>`,
explanation: isMobile ? loc(obj.lang, 'ImagePickerExplanationPhone') : loc(obj.lang, 'ImagePickerExplanationPC') explanation: `<div id="picker-subtitle"></div>`,
}, },
buttons: [`<a id="imagepicker-download" class="switch" target="_blank" href="/">${loc(obj.lang, 'ImagePickerDownloadAudio')}</a>`], buttons: [`<a id="picker-download" class="switch" target="_blank" href="/">${loc(obj.lang, 'ImagePickerDownloadAudio')}</a>`],
content: '<div id="imagepicker-holder"></div>' content: '<div id="picker-holder"></div>'
})} })}
${popup({ ${popup({
name: "error", name: "error",
@ -293,19 +292,6 @@ export default function(obj) {
}, },
body: `<div id="desc-error" class="desc-padding subtext"></div>` 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="popup-backdrop" style="visibility: hidden;" onclick="hideAllPopups()"></div>
<div id="cobalt-main-box" class="center" style="visibility: hidden;"> <div id="cobalt-main-box" class="center" style="visibility: hidden;">
<div id="logo-area">${appName}</div> <div id="logo-area">${appName}</div>
@ -342,7 +328,11 @@ export default function(obj) {
noURLReturned: ` + "`" + loc(obj.lang, 'ErrorNoUrlReturned') + "`" + `, noURLReturned: ` + "`" + loc(obj.lang, 'ErrorNoUrlReturned') + "`" + `,
unknownStatus: ` + "`" + loc(obj.lang, 'ErrorUnknownStatus') + "`" + `, unknownStatus: ` + "`" + loc(obj.lang, 'ErrorUnknownStatus') + "`" + `,
toggleDefault: '${emoji("✨")} ${loc(obj.lang, "ModeToggleAuto")}', 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>
<script type="text/javascript" src="cobalt.js"></script> <script type="text/javascript" src="cobalt.js"></script>
</html>`; </html>`;

View file

@ -1,18 +1,18 @@
import { apiJSON } from "./sub/utils.js"; import { apiJSON } from "../sub/utils.js";
import { errorUnsupported, genericError } from "./sub/errors.js"; import { errorUnsupported, genericError } from "../sub/errors.js";
import { testers } from "./servicesPatternTesters.js"; import { testers } from "./servicesPatternTesters.js";
import bilibili from "./services/bilibili.js"; import bilibili from "../services/bilibili.js";
import reddit from "./services/reddit.js"; 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"; import tiktok from "../services/tiktok.js";
import tumblr from "./services/tumblr.js"; import tumblr from "../services/tumblr.js";
import matchActionDecider from "./sub/matchActionDecider.js"; import matchActionDecider from "./matchActionDecider.js";
import vimeo from "./services/vimeo.js"; import vimeo from "../services/vimeo.js";
import soundcloud from "./services/soundcloud.js"; import soundcloud from "../services/soundcloud.js";
export default async function (host, patternMatch, url, lang, obj) { export default async function (host, patternMatch, url, lang, obj) {
try { try {

View file

@ -1,9 +1,9 @@
import { audioIgnore, services, supportedAudio } from "../config.js" 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) { export default function(r, host, ip, audioFormat, isAudioOnly) {
if (!r.error) { if (!r.error) {
if (!isAudioOnly) { if (!isAudioOnly && !r.picker) {
switch (host) { switch (host) {
case "twitter": case "twitter":
return apiJSON(1, { u: r.urls }); return apiJSON(1, { u: r.urls });
@ -44,13 +44,26 @@ export default function(r, host, ip, audioFormat, isAudioOnly) {
case "vimeo": case "vimeo":
return apiJSON(1, { u: r.urls }); 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 { } else {
if (host == "reddit" && r.typeId == 1 || audioIgnore.includes(host)) return apiJSON(0, { t: r.audioFilename }); if (host == "reddit" && r.typeId == 1 || audioIgnore.includes(host)) return apiJSON(0, { t: r.audioFilename });
let type = "render"; let type = "render";
let copy = false; 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 ((host == "tiktok" || host == "douyin") && isAudioOnly && services.tiktok.audioFormats.includes(audioFormat)) {
if (r.isMp3) { if (r.isMp3) {
if (audioFormat == "mp3" || audioFormat == "best") { if (audioFormat == "mp3" || audioFormat == "best") {
@ -62,7 +75,6 @@ export default function(r, host, ip, audioFormat, isAudioOnly) {
type = "bridge" type = "bridge"
} }
} }
if ((audioFormat == "best" && services[host]["bestAudio"]) || services[host]["bestAudio"] && (audioFormat == services[host]["bestAudio"])) { if ((audioFormat == "best" && services[host]["bestAudio"]) || services[host]["bestAudio"] && (audioFormat == services[host]["bestAudio"])) {
audioFormat = services[host]["bestAudio"] audioFormat = services[host]["bestAudio"]
type = "bridge" type = "bridge"
@ -70,20 +82,11 @@ export default function(r, host, ip, audioFormat, isAudioOnly) {
audioFormat = "m4a" audioFormat = "m4a"
copy = true copy = true
} }
if ((host == "tiktok" || host == "douyin") && r.images) { return apiJSON(2, {
return apiJSON(5, { type: type,
type: type, u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
images: r.images, filename: r.audioFilename, salt: process.env.streamSalt, isAudioOnly: true, audioFormat: audioFormat, copy: copy
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
})
}
} }
} else { } else {
return apiJSON(0, { t: r.error }); return apiJSON(0, { t: r.error });

View file

@ -17,6 +17,9 @@
}, },
"vk": { "vk": {
"alias": "vk clips, vk video", "alias": "vk clips, vk video",
"localizedAlias": {
"ru": "vk видео, vk клипы"
},
"patterns": ["video-:userId_:videoId", "clip-:userId_:videoId", "clips-:userId?z=clip-:userId_:videoId"], "patterns": ["video-:userId_:videoId", "clip-:userId_:videoId", "clips-:userId?z=clip-:userId_:videoId"],
"quality_match": { "quality_match": {
"2160": 7, "2160": 7,

View file

@ -93,10 +93,10 @@ export default async function(obj) {
if (images) { if (images) {
let imageLinks = []; let imageLinks = [];
for (let i in images) { 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 { return {
images: imageLinks, picker: imageLinks,
urls: audio, urls: audio,
audioFilename: audioFilename, audioFilename: audioFilename,
isAudioOnly: true, isAudioOnly: true,

View file

@ -2,18 +2,51 @@ import got from "got";
import loc from "../../localization/manager.js"; import loc from "../../localization/manager.js";
import { genericUserAgent } from "../config.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) { export default async function(obj) {
try { try {
let req_status = await got.get(`https://cdn.syndication.twimg.com/tweet?id=${obj.id}&tweet_mode=extended`, { let _headers = {
headers: { "User-Agent": genericUserAgent } "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) => { req_status.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'twitter') } return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'twitter') }
}) })
let parsbod = JSON.parse(req_status.body); let parsbod = JSON.parse(req_status.body);
if (parsbod["video"] && parsbod["video"]["variants"]) { if (parsbod["extended_entities"] && parsbod["extended_entities"]["media"]) {
let media = parsbod["video"]["variants"]; let single, multiple = [], media = parsbod["extended_entities"]["media"];
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` } 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 { } else {
return { error: loc(obj.lang, 'ErrorNoVideosInTweet') } return { error: loc(obj.lang, 'ErrorNoVideosInTweet') }
} }

View file

@ -14,7 +14,15 @@ export function apiJSON(type, obj) {
case 4: case 4:
return { status: 429, body: { status: "rate-limit", text: obj.t } }; return { status: 429, body: { status: "rate-limit", text: obj.t } };
case 5: 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: default:
return { status: 400, body: { status: "error", text: "Bad Request" } }; return { status: 400, body: { status: "error", text: "Bad Request" } };
} }