7.9: twitter gifs, ok.ru support, pinterest improvements, and more

merge pull request #321 from wukko/twitter-gif
This commit is contained in:
wukko 2024-01-17 17:32:12 +06:00 committed by GitHub
commit f10f9b7ce8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 396 additions and 198 deletions

View file

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

View file

@ -90,7 +90,8 @@
"mp4": ["-c:v", "copy", "-c:a", "copy", "-movflags", "faststart+frag_keyframe+empty_moov"],
"copy": ["-c:a", "copy"],
"audio": ["-ar", "48000", "-ac", "2", "-b:a", "320k"],
"m4a": ["-movflags", "frag_keyframe+empty_moov"]
"m4a": ["-movflags", "frag_keyframe+empty_moov"],
"gif": ["-vf", "scale=-1:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse", "-loop", "0"]
},
"sponsors": [{
"name": "royale",

View file

@ -107,7 +107,7 @@ a {
color: var(--accent-subtext);
}
.switches::-webkit-scrollbar,
#popup-content::-webkit-scrollbar {
.popup-content::-webkit-scrollbar {
display: none;
}
:focus-visible {
@ -450,23 +450,23 @@ button:active,
.popup.small.visible {
transform: translate(-50%, -50%);
}
.popup.small #popup-header-contents,
.popup.small .popup-header-contents,
.popup.small .popup-content-inner,
.popup.small #popup-header {
.popup.small .popup-header {
padding: 0;
}
.popup.small #popup-header {
.popup.small .popup-header {
position: relative;
border: none;
}
.popup.small #popup-title {
.popup.small .popup-title {
margin-bottom: 0.6rem;
}
.popup.small .explanation {
margin-bottom: 0.9rem;
}
#close-error {
background: var(--accent);
.popup.small .close-error.switch {
background: var(--accent)!important;
color: var(--background);
}
.popup.scrollable {
@ -520,7 +520,7 @@ button:active,
font-size: 1.1rem;
padding-bottom: var(--padding-1);
}
#popup-desc,
.popup-desc,
.desc-error,
#popup-info-desc {
width: 100%;
@ -533,7 +533,7 @@ button:active,
.desc-error {
padding-bottom: 1.5rem;
}
#popup-title {
.popup-title {
font-size: 1.5rem;
line-height: 1.2em;
display: flex;
@ -541,11 +541,11 @@ button:active,
margin-bottom: 0.4rem;
margin-top: 0.4rem;
}
#popup-above-title {
.popup-above-title {
color: var(--accent-subtext);
font-size: 0.8rem;
}
#popup-content {
.popup-content {
overflow-x: scroll;
overflow-y: auto;
height: 100%;
@ -564,7 +564,7 @@ button:active,
.bullpadding {
padding-left: 0.58rem;
}
#popup-header {
.popup-header {
position: absolute;
z-index: 999;
padding-top: calc(env(safe-area-inset-top)/2 + 1.7rem);
@ -646,16 +646,16 @@ button:active,
.switch:focus {
box-shadow: var(--inset-focus) inset;
}
#popup-tabs .switch {
.popup-tabs .switch {
background: none;
}
.desktop #popup-tabs .switch:hover,
#popup-tabs .switch:active {
.desktop .popup-tabs .switch:hover,
.popup-tabs .switch:active {
background: var(--accent-hover-transparent);
box-shadow: 0 0 0 0.1rem var(--accent-highlight) inset;
}
.switch[data-enabled="true"],
#popup-tabs .switch[data-enabled="true"] {
.popup-tabs .switch[data-enabled="true"] {
color: var(--background);
background: var(--accent)!important;
cursor: default;
@ -693,20 +693,20 @@ button:active,
padding: var(--gap-no-icon);
overflow: clip;
}
#back-button {
.back-button {
padding: 0;
background: none;
max-width: 4rem;
font-size: 1rem;
}
#back-button svg path,
.back-button svg path,
.collapse-indicator svg path {
fill: var(--accent);
}
.popup-tab-content[data-enabled="false"] {
display: none;
}
#popup-tabs {
.popup-tabs {
z-index: 999;
bottom: 0;
position: absolute;
@ -823,7 +823,7 @@ button:active,
}
.popup-content-inner,
.tab-content-settings,
#popup-header-contents {
.popup-header-contents {
padding-left: 1rem;
padding-right: 1rem;
}
@ -947,15 +947,15 @@ button:active,
#bottom #paste,
#footer .switch,
#audioMode,
#popup-content .switches,
.popup-content .switches,
.checkbox,
.changelog-img,
.changelog-banner,
#close-error,
.close-error,
.changelog-tag-version,
#download-switcher .switch,
#popup-about .switch,
#popup-tabs .switch,
.popup-tabs .switch,
.text-to-copy,
.text-to-copy.text-backdrop,
#filename-preview {
@ -965,16 +965,16 @@ button:active,
border-radius: 3px / 4px;
}
.popup,
.scrollable #popup-content {
.scrollable .popup-content {
border-radius: 8px;
}
#popup-header .glass-bkg {
.popup-header .glass-bkg {
border-top-left-radius: 8px 9px;
border-top-right-radius: 8px 9px;
border-bottom: var(--accent-highlight) solid 0.1rem;
top: -1px;
}
#popup-tabs .glass-bkg {
.popup-tabs .glass-bkg {
border-bottom-left-radius: 8px 9px;
border-bottom-right-radius: 8px 9px;
border-top: var(--accent-highlight) solid 0.1rem;
@ -1103,12 +1103,12 @@ button:active,
padding-top: calc(env(safe-area-inset-bottom)/2 + 1rem);
}
.popup,
#popup-header .glass-bkg,
#popup-tabs .glass-bkg,
.popup-header .glass-bkg,
.popup-tabs .glass-bkg,
.glass-bkg.small {
border-radius: 0;
}
#popup-tabs .glass-bkg {
.popup-tabs .glass-bkg {
bottom: 0;
}
.switches {
@ -1141,13 +1141,13 @@ button:active,
transform: none;
transition: transform 210ms cubic-bezier(0.062, 0.82, 0.165, 1), opacity 130ms ease-in-out;
}
.popup.small #popup-header {
.popup.small .popup-header {
background: none;
}
.no-animation .popup.small {
transition: none;
}
#close-error {
.close-error {
bottom: 3rem;
}
#picker-holder::-webkit-scrollbar {
@ -1166,13 +1166,13 @@ button:active,
max-height: 100%;
box-shadow: none;
}
#popup-tabs {
.popup-tabs {
padding-bottom: calc(env(safe-area-inset-bottom)/2 + 1.5rem);
}
.popup-content-inner,
.tab-content-settings,
.popup-tabs-child,
#popup-header-contents {
.popup-header-contents {
padding-left: 0.7rem;
padding-right: 0.7rem;
}

View file

@ -30,6 +30,7 @@ const checkboxes = [
"reduceTransparency",
"disableAnimations",
"disableMetadata",
"twitterGif",
];
const exceptions = { // used for mobile devices
"vQuality": "720"
@ -235,7 +236,7 @@ function popup(type, action, text) {
`<a class="picker-image-container" ${
isIOS ? `onClick="share('${text.arr[i]["url"]}')"` : `href="${text.arr[i]["url"]}" target="_blank"`
}>` +
`<img class="picker-image" src="${text.arr[i]["url"]}" onerror="this.parentNode.style.display='none'"></img>` +
`<img class="picker-image" src="${text.arr[i]["url"]}" onerror="this.parentNode.style.display='none'">` +
`</a>`
}
break;
@ -252,7 +253,7 @@ function popup(type, action, text) {
}>` +
`<div class="picker-element-name">${text.arr[i].type}</div>` +
`<div class="imageBlock"></div>` +
`<img class="picker-image" src="${text.arr[i]["thumb"]}" onerror="this.style.display='none'"></img>` +
`<img class="picker-image" src="${text.arr[i]["thumb"]}" onerror="this.style.display='none'">` +
`</a>`
}
eid("picker-download").classList.remove("visible");
@ -381,6 +382,7 @@ async function download(url) {
}
if (sGet("disableMetadata") === "true") req.disableMetadata = true;
if (sGet("twitterGif") === "true") req.twitterGif = true;
let j = await fetch(`${apiURL}/api/json`, {
method: "POST",
@ -601,9 +603,9 @@ window.onload = () => {
if (setUn !== null) {
if (setUn) {
sSet("migrated", "true")
eid("desc-migration").innerHTML += `<br/><br/>${loc.DataTransferSuccess}`
eid("desc-migration").innerHTML += `<br><br>${loc.DataTransferSuccess}`
} else {
eid("desc-migration").innerHTML += `<br/><br/>${loc.DataTransferError}`
eid("desc-migration").innerHTML += `<br><br>${loc.DataTransferError}`
}
}
}
@ -614,6 +616,11 @@ window.onload = () => {
window.history.replaceState(null, '', window.location.pathname);
notificationCheck();
// fix for animations not working in Safari
if (isIOS) {
document.addEventListener('touchstart', () => {}, true);
}
}
eid("url-input-area").addEventListener("keydown", (e) => {
button();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -8,7 +8,7 @@
"LinkInput": "paste the link here",
"AboutSummary": "cobalt is your go-to place for downloads from social and media platforms. zero ads, trackers, or other creepy bullshit. simply paste a share link and you're ready to rock!",
"EmbedBriefDescription": "save what you love. no ads, trackers, or other creepy bullshit.",
"MadeWithLove": "made with <3 by wukko",
"MadeWithLove": "made with &lt;3 by wukko",
"AccessibilityInputArea": "link input area",
"AccessibilityOpenAbout": "open about popup",
"AccessibilityDownloadButton": "download button",
@ -117,7 +117,6 @@
"ShareURL": "share",
"ErrorTweetUnavailable": "couldn't find anything about this tweet. this could be because its visibility is limited. try another one!",
"ErrorTwitterRIP": "twitter has restricted access to any content to unauthenticated users. while there's a way to get regular tweets, spaces are, unfortunately, impossible to get at this time. i am looking into possible solutions.",
"UrgentDonate": "cobalt needs your help!",
"PopupCloseDone": "done",
"Accessibility": "accessibility",
"SettingsReduceTransparency": "reduce transparency",
@ -134,10 +133,7 @@
"KeyboardShortcutClosePopup": "close all popups",
"CollapseLegal": "terms and ethics",
"FairUse": "cobalt is a web tool that makes it easier to download content from the internet and takes <span class=\"text-backdrop\">zero liability</span>. processing servers work like <span class=\"text-backdrop\">limited proxies</span>, so no media content is ever cached or stored.\n\nyou (end user) are responsible for what you download, how you use and distribute that content. please be mindful when using content of others and always credit original creators.\n\nwhen used in education purposes (lecture, homework, etc) please attach the source link.\n\nfair use and credits benefit everyone.",
"UrgentFeatureUpdate71": "more supported services!",
"UrgentThanks": "thank you for support!",
"SettingsDisableMetadata": "don't add metadata",
"UrgentNewDomain": "new domain, same cobalt",
"NewDomainWelcomeTitle": "hey there!",
"NewDomainWelcome": "cobalt is moving! same features, same owner, simply a more rememberable domain. and still no ads.\n\n<span class=\"text-backdrop\">cobalt.tools</span> is the new main domain, aka where you are now. make sure to update your bookmarks and reinstall the web app!",
"DataTransferSuccess": "btw, your settings have been transferred automatically :)",
@ -154,10 +150,12 @@
"FilenamePreviewVideoTitle": "Video Title",
"FilenamePreviewAudioTitle": "Audio Title",
"FilenamePreviewAudioAuthor": "Audio Author",
"UrgentFilenameUpdate": "customizable file names!",
"UrgentTwitterPatch": "fixes and easier downloads",
"StatusPage": "service status page",
"TroubleshootingGuide": "self-troubleshooting guide",
"UpdateNewYears": "new years clean up"
"DonateImageDescription": "cat sleeping on a laptop keyboard and typing letters repeatedly",
"UpdateNewYears": "new years clean up",
"SettingsTwitterGif": "convert gifs to .gif",
"SettingsTwitterGifDescription": "converting looping videos to .gif reduces quality and majorly increases file size. if you want best efficiency, keep this setting off.",
"UpdateTwitterGif": "twitter gifs and pinterest"
}
}

View file

@ -8,7 +8,7 @@
"LinkInput": "вставь ссылку сюда",
"AboutSummary": "кобальт - твой друг при скачивании контента из соцсетей и других сервисов. никакой рекламы, трекеров и прочего мусора. вставляешь ссылку и получаешь файл. всё. ничего лишнего.",
"EmbedBriefDescription": "сохраняй то, что любишь. без рекламы, трекеров и лишней мороки.",
"MadeWithLove": "сделано с любовью <3",
"MadeWithLove": "сделано с любовью &lt;3",
"AccessibilityInputArea": "зона вставки ссылки",
"AccessibilityOpenAbout": "открыть окно с инфой",
"AccessibilityDownloadButton": "кнопка скачивания",
@ -118,7 +118,6 @@
"ShareURL": "поделиться",
"ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость была ограничена. попробуй другой!",
"ErrorTwitterRIP": "твиттер ограничил доступ к любому контенту на сайте для пользователей без аккаунтов. я нашёл лазейку, чтобы доставать обычные твиты, а для spaces, к сожалению, нет. я ищу возможные варианты выхода из ситуации.",
"UrgentDonate": "нужна твоя помощь!",
"PopupCloseDone": "готово",
"Accessibility": "общедоступность",
"SettingsReduceTransparency": "уменьшить прозрачность",
@ -135,10 +134,7 @@
"KeyboardShortcutClosePopup": "закрыть все окна",
"CollapseLegal": "принципы и этика",
"FairUse": "кобальт - это веб инструмент для облегчения скачивания контента из интернета. сервера обработки работают как <span class=\"text-backdrop\">ограниченные прокси</span>, так что ничего никогда не сохраняется или кэшируется.\n\nкобальт <span class=\"text-backdrop\">не несёт никакой ответственности</span>, только ты (конечный пользователь) несёшь ответственность за то, что скачиваешь, как используешь и распространяешь скачанный контент. будь сознателен при использовании чужого контента и всегда указывай авторов!\n\nприкладывай ссылку на источник при использовании в образовательных целях (лекции, домашние задания и т.п.)\n\nчестное использование и указание авторства выгодно всем.",
"UrgentFeatureUpdate71": "расширение поддержки сервисов!",
"UrgentThanks": "спасибо за поддержку!",
"SettingsDisableMetadata": "не добавлять метаданные",
"UrgentNewDomain": "новый домен, тот же кобальт",
"NewDomainWelcomeTitle": "привет!",
"NewDomainWelcome": "кобальт переезжает! те же функции, тот же владелец, просто более запоминающийся домен. по-прежнему без рекламы.\n\n<span class=\"text-backdrop\">cobalt.tools</span> - новый основной домен, т.е. где ты сейчас находишься. не забудь обновить закладки и переустановить веб-приложение!",
"DataTransferSuccess": "кстати, твои настройки были перенесены автоматически :)",
@ -156,10 +152,12 @@
"FilenamePreviewVideoTitle": "Название Видео",
"FilenamePreviewAudioTitle": "Название Аудио",
"FilenamePreviewAudioAuthor": "Автор Аудио",
"UrgentFilenameUpdate": "изменяемые названия файлов!",
"UrgentTwitterPatch": "фиксы и удобное скачивание",
"StatusPage": "статус серверов",
"TroubleshootingGuide": "гайд по устранению проблем",
"UpdateNewYears": "новогодняя уборка"
"DonateImageDescription": "кошка спит на клавиатуре ноутбука и многократно печатает буквы",
"UpdateNewYears": "новогодняя уборка",
"SettingsTwitterGif": "конвертировать гифки в .gif",
"SettingsTwitterGifDescription": "конвертирование зацикленного видео в .gif снижает качество и значительно увеличивает размер файла. если важна максимальная эффективность, то не используй эту функцию.",
"UpdateTwitterGif": "гифки с твиттера и одноклассники"
}
}

View file

@ -17,7 +17,7 @@ export async function loadLoc() {
export function replaceBase(s) {
return s
.replace(/\n/g, '<br/>')
.replace(/\n/g, '<br>')
.replace(/{saveToGalleryShortcut}/g, links.saveToGalleryShortcut)
.replace(/{repo}/g, repo)
.replace(/{statusPage}/g, links.statusPage)

View file

@ -5,6 +5,7 @@
"title": "new years clean up! bug fixes and fresh look for the home page",
"banner": {
"file": "catroomba.webp",
"alt": "a cat riding a roomba vacuum",
"width": 300,
"height": 168
},
@ -16,6 +17,7 @@
"title": "bugfixes and better downloads!",
"banner": {
"file": "meowthpolishegg.webp",
"alt": "meowth polishing a togepi egg",
"width": 640,
"height": 480
},
@ -26,6 +28,7 @@
"title": "customizable file names, instagram stories, and first cobalt sponsor!",
"banner": {
"file": "meowthcenter.webp",
"alt": "meowth plush in a datacenter wearing a hardhat, wielding a hammer",
"width": 851,
"height": 640
},
@ -36,6 +39,7 @@
"title": "support for twitch clips and rutube!",
"banner": {
"file": "twitchupdate.webp",
"alt": "meowth plush staring into the camera, laptop with generic purple service in the background",
"width": 851,
"height": 640
},
@ -46,6 +50,7 @@
"title": "new domain, what's coming in future, bug fixes, and more!",
"banner": {
"file": "newdomain.webp",
"alt": "text: new domain, same cobalt",
"width": 960,
"height": 540
},
@ -56,6 +61,7 @@
"title": "extended video length limit, metadata toggle, ui improvements, and more!",
"banner": {
"file": "meowthsnap.webp",
"alt": "cartoon meowth pointing paw dramatically and saying something",
"width": 500,
"height": 280
},
@ -66,6 +72,7 @@
"title": "instagram, streamable, video metadata, and more!",
"banner": {
"file": "meowthproductions.webp",
"alt": "meowth roaring in a fancy circle, à la MGM studios intro",
"width": 640,
"height": 358
},
@ -76,6 +83,7 @@
"title": "biggest ui refresh yet!",
"banner": {
"file": "meowthcooking.webp",
"alt": "meowth handling orders in a restaurant",
"width": 640,
"height": 360
},
@ -86,6 +94,7 @@
"title": "all network issues have been fixed!",
"banner": {
"file": "meowthhammer.webp",
"alt": "meowth plush holding a hammer in real life",
"width": 1280,
"height": 827
},
@ -96,6 +105,7 @@
"title": "better reliability, new infrastructure, pinterest support, and way more!",
"banner": {
"file": "catswitchboxes.webp",
"alt": "a cat climbing into two empty boxes of asahi beer",
"width": 600,
"height": 314
},
@ -105,6 +115,7 @@
"title": "instagram support, docker, and more!",
"banner": {
"file": "catphonestand.webp",
"alt": "a cat holding a phone under its chin while a person plays clash of clans on it",
"width": 451,
"height": 272
},
@ -114,6 +125,7 @@
"title": "better looks, better feel",
"banner": {
"file": "cattired.webp",
"alt": "a cat laying on a sofa face down, wiggling its tail",
"width": 640,
"height": 286
},
@ -123,6 +135,7 @@
"title": "fastest one in the game",
"banner": {
"file": "catspeed.webp",
"alt": "a cat running very fast in an exercise wheel",
"width": 640,
"height": 356
},
@ -132,6 +145,7 @@
"title": "the evil has been defeated",
"banner": {
"file": "happymeowth.webp",
"alt": "meowth jumping up into the sky very excitedly",
"width": 500,
"height": 330
},
@ -141,6 +155,7 @@
"title": "it's all about attention to detail!",
"banner": {
"file": "valentines.webp",
"alt": "relaxed meowth with sakura petals falling in front of them",
"width": 489,
"height": 374
},
@ -150,6 +165,7 @@
"title": "prettier than ever",
"banner": {
"file": "catmakeup.webp",
"alt": "a cat being brushed with a powder makeup brush",
"width": 394,
"height": 266
},
@ -159,6 +175,7 @@
"title": "we're better together! thank you for bug reports.",
"banner": {
"file": "bettertogether.webp",
"alt": "various different pokémon jumping in happiness",
"width": 640,
"height": 358
},
@ -168,6 +185,7 @@
"title": "mute videos and proper soundcloud support",
"banner": {
"file": "shutup.webp",
"alt": "a cat yawning, with a crossed out loudspeaker icon next to it",
"width": 1024,
"height": 665
},
@ -177,6 +195,7 @@
"title": "better, faster, stronger, stable",
"banner": {
"file": "meowthstrong.webp",
"alt": "meowth stretching",
"width": 500,
"height": 280
},
@ -186,6 +205,7 @@
"title": "over 1 million monthly requests. thank you.",
"banner": {
"file": "onemillionr.webp",
"alt": "cobalt logo and a confetti emoji",
"width": 1441,
"height": 1441
},
@ -199,6 +219,7 @@
"title": "developers, developers, developers, developers",
"banner": {
"file": "developers.webp",
"alt": "steve ballmer going \"developers, developers, developers\"",
"width": 640,
"height": 360
},

View file

@ -5,33 +5,35 @@ let changelog = loadJSON('./src/modules/changelog/changelog.json')
export default function(string) {
try {
const currentChangelog = changelog.current;
switch (string) {
case "version":
return `<span class="text-backdrop changelog-tag-version">v.${changelog["current"]["version"]}</span>${
changelog["current"]["date"] ? `<span class="changelog-tag-date">· ${changelog["current"]["date"]}</span>` : ''
return `<span class="text-backdrop changelog-tag-version">v.${currentChangelog.version}</span>${
currentChangelog.date ? `<span class="changelog-tag-date">· ${currentChangelog.date}</span>` : ''
}`
case "title":
return replaceBase(changelog["current"]["title"]);
return replaceBase(currentChangelog.title);
case "banner":
return changelog["current"]["banner"] ? {
url: `updateBanners/${changelog["current"]["banner"]["file"]}`,
width: changelog["current"]["banner"]["width"],
height: changelog["current"]["banner"]["height"]
const currentBanner = changelog.current.banner;
return currentBanner ? {
...currentBanner,
url: `updateBanners/${currentBanner.file}`
} : false;
case "content":
return replaceBase(changelog["current"]["content"]);
return replaceBase(currentChangelog.content);
case "history":
return changelog["history"].map((i) => {
return changelog.history.map((log) => {
const banner = log.banner;
return {
title: replaceBase(i["title"]),
version: `<span class="text-backdrop changelog-tag-version">v.${i["version"]}</span>${
i["date"] ? `<span class="changelog-tag-date">· ${i["date"]}</span>` : ''
title: replaceBase(log.title),
version: `<span class="text-backdrop changelog-tag-version">v.${log.version}</span>${
log.date ? `<span class="changelog-tag-date">· ${log.date}</span>` : ''
}`,
content: replaceBase(i["content"]),
banner: i["banner"] ? {
url: `updateBanners/${i["banner"]["file"]}`,
width: i["banner"]["width"],
height: i["banner"]["height"]
content: replaceBase(log.content),
banner: banner ? {
...banner,
url: `updateBanners/${banner.file}`
} : false,
}
});

View file

@ -62,5 +62,5 @@ export default function(emoji, size, disablePadding, fluent) {
let filePath = `emoji/${names[emoji]}.svg`;
if (fluent) filePath = `emoji/3d/${names[emoji]}.svg`;
return `<img class="emoji" draggable=false height="${size}" width="${size}" ${padding ? `style="${padding}"` : ''}alt="${emoji}" src="${filePath}" loading="lazy">`
return `<img class="emoji" draggable=false height="${size}" width="${size}" ${padding ? `style="${padding}" ` : ''}alt="${emoji}" src="${filePath}" loading="lazy">`
}

View file

@ -59,27 +59,27 @@ export function popup(obj) {
body = ``
for (let i = 0; i < obj.body.length; i++) {
if (obj.body[i]["text"].length > 0) {
classes = obj.body[i]["classes"] ? obj.body[i]["classes"] : []
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 class="${['popup-desc', ...classes].join(' ')}">${obj.body[i]["text"]}</div>`
}
}
}
return `
${obj.standalone ? `<div id="popup-${obj.name}" class="popup center${!obj.buttonOnly ? " box" : ''}${classes.length > 0 ? ' ' + classes.join(' ') : ''}">` : ''}
<div id="popup-header" class="popup-header">
<div id="popup-header-contents">
<div class="popup-header">
<div class="popup-header-contents">
${obj.buttonOnly ? obj.header.emoji : ``}
${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.title ? `<div id="popup-title">${obj.header.title}</div>` : ''}
${obj.header.aboveTitle ? `<a class="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.title ? `<div class="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
</div>
${!obj.buttonOnly ? `<div class="glass-bkg alone"></div>` : ''}
</div>
<div id="popup-content" class="popup-content-inner">
${body}${obj.buttonOnly ? `<button id="close-error" class="switch" onclick="popup('${obj.name}', 0)">${obj.buttonText}</button>` : ''}
<div class="popup-content popup-content-inner">
${body}${obj.buttonOnly ? `<button class="close-error switch" onclick="popup('${obj.name}', 0)">${obj.buttonText}</button>` : ''}
</div>
${classes.includes("small") ? `<div class="glass-bkg small"></div>` : ''}
${obj.standalone ? `</div>` : ''}`
@ -87,7 +87,7 @@ export function popup(obj) {
export function multiPagePopup(obj) {
let tabs = `
<button id="back-button" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>
<button class="back-button switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>
${backButtonSVG}
</button>`;
@ -99,16 +99,16 @@ export function multiPagePopup(obj) {
return `
<div id="popup-${obj.name}" class="popup center box scrollable">
<div id="popup-content">
${obj.header ? `<div id="popup-header" class="popup-header">
<div id="popup-header-contents">
${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.title ? `<div id="popup-title">${obj.header.title}</div>` : ''}
<div class="popup-content">
${obj.header ? `<div class="popup-header">
<div class="popup-header-contents">
${obj.header.aboveTitle ? `<a class="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.title ? `<div class="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
</div>
<div class="glass-bkg alone"></div>
</div>` : ''}${tabContent}</div>
<div id="popup-tabs" class="switches popup-tabs">
<div class="switches popup-tabs">
<div class="switches popup-tabs-child">${tabs}</div>
<div class="glass-bkg alone"></div>
</div>
@ -131,7 +131,7 @@ export function collapsibleList(arr) {
}
export function popupWithBottomButtons(obj) {
let tabs = `
<button id="back-button" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>
<button class="back-button switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>
${backButtonSVG}
</button>`
@ -140,17 +140,17 @@ export function popupWithBottomButtons(obj) {
}
return `
<div id="popup-${obj.name}" class="popup center box scrollable">
<div id="popup-content">
${obj.header ? `<div id="popup-header" class="popup-header">
<div id="popup-header-contents">
${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.title ? `<div id="popup-title">${obj.header.title}</div>` : ''}
<div class="popup-content">
${obj.header ? `<div class="popup-header">
<div class="popup-header-contents">
${obj.header.aboveTitle ? `<a class="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''}
${obj.header.title ? `<div class="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
${obj.header.explanation ? `<div class="explanation">${obj.header.explanation}</div>` : ''}
</div>
<div class="glass-bkg alone"></div>
</div>` : ''}${obj.content}</div>
<div id="popup-tabs" class="switches popup-tabs">
<div class="switches popup-tabs">
<div id="picker-buttons" class="switches popup-tabs-child">${tabs}</div>
<div class="glass-bkg alone"></div>
</div>
@ -171,7 +171,7 @@ export function socialLinks(lang) {
}
export function settingsCategory(obj) {
return `<div id="settings-${obj.name}" class="settings-category">
<div class="category-title">${obj.title ? obj.title : obj.name}</div>
<div class="category-title">${obj.title ?? obj.name}</div>
<div class="category-content">${obj.body}</div>
</div>`
}

View file

@ -17,14 +17,15 @@ export function changelogHistory() { // blockId 0
`<div class="changelog-banner">
<img class="changelog-img" ` +
`src="${history[i]["banner"]["url"]}" ` +
`alt="${history[i]["banner"]["alt"].replaceAll('"', '&quot;')}" ` +
`width="${history[i]["banner"]["width"]}" ` +
`height="${history[i]["banner"]["height"]}" ` +
`onerror="this.style.opacity=0" loading="lazy">`+
`</img>
`
</div>` : ''}
<div id="popup-desc" class="changelog-tags">${history[i]["version"]}</div>
<div id="popup-desc" class="changelog-subtitle">${history[i]["title"]}</div>
<div id="popup-desc" class="desc-padding">${history[i]["content"]}</div>`
<div class="popup-desc changelog-tags">${history[i]["version"]}</div>
<div class="popup-desc changelog-subtitle">${history[i]["title"]}</div>
<div class="popup-desc desc-padding">${history[i]["content"]}</div>`
}
render = cleanHTML(render);
cache['0'] = render;

View file

@ -43,36 +43,40 @@ export default function(obj) {
<!DOCTYPE html>
<html lang="${obj.lang}">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="viewport-fit=cover, width=device-width, height=device-height, initial-scale=1, maximum-scale=${isIOS ? `1` : `5`}" />
<meta charset="utf-8">
<meta name="viewport" content="viewport-fit=cover, width=device-width, height=device-height, initial-scale=1, maximum-scale=${isIOS ? `1` : `5`}">
<title>${t("AppTitleCobalt")}</title>
<meta property="og:url" content="${process.env.webURL || process.env.selfURL}" />
<meta property="og:title" content="${t("AppTitleCobalt")}" />
<meta property="og:description" content="${t('EmbedBriefDescription')}" />
<meta property="og:image" content="${process.env.webURL || process.env.selfURL}icons/generic.png" />
<meta name="title" content="${t("AppTitleCobalt")}" />
<meta name="description" content="${t('AboutSummary')}" />
<meta name="theme-color" content="#000000" />
<meta name="twitter:card" content="summary" />
<meta property="og:url" content="${process.env.webURL}">
<meta property="og:title" content="${t("AppTitleCobalt")}">
<meta property="og:description" content="${t('EmbedBriefDescription')}">
<meta property="og:image" content="${process.env.webURL}icons/generic.png">
<meta name="title" content="${t("AppTitleCobalt")}">
<meta name="description" content="${t('AboutSummary')}">
<meta name="theme-color" content="#000000">
<meta name="twitter:card" content="summary">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="${t("AppTitleCobalt")}">
<link rel="icon" type="image/x-icon" href="icons/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png" />
<link rel="icon" type="image/x-icon" href="icons/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="icons/apple-touch-icon.png" />
<link rel="apple-touch-icon" sizes="180x180" href="icons/apple-touch-icon.png">
<link rel="manifest" href="manifest.webmanifest">
<link rel="preload" href="fonts/notosansmono.css" as="style">
<link rel="stylesheet" href="fonts/notosansmono.css">
<link rel="stylesheet" href="cobalt.css">
<link rel="manifest" href="manifest.webmanifest" />
<link rel="stylesheet" href="fonts/notosansmono.css" rel="preload" />
<link rel="stylesheet" href="cobalt.css" />
</head>
<body id="cobalt-body" ${platform === "d" ? 'class="desktop"' : ''} data-nosnippet ontouchstart>
<noscript>${t('NoScriptMessage')}</noscript>
<body id="cobalt-body" ${platform === "d" ? 'class="desktop"' : ''} data-nosnippet>
<noscript>
<div style="margin: 2rem;">${t('NoScriptMessage')}</div>
</noscript>
${multiPagePopup({
name: "about",
closeAria: t('AccessibilityGoBack'),
@ -145,10 +149,10 @@ export default function(obj) {
body: `${t("SupportSelfTroubleshooting")}`
+ `${socialLink(emoji("📢"), t("StatusPage"), links.statusPage)}`
+ `${socialLink(emoji("🔧"), t("TroubleshootingGuide"), links.troubleshootingGuide)}`
+ `<br/>`
+ `<br>`
+ `${t("FollowSupport")}`
+ `${socialLinks(obj.lang)}`
+ `<br/>`
+ `<br>`
+ `${t("SourceCode")}`
+ `${socialLink(emoji("🐙"), repo.replace("https://github.com/", ''), repo)}`
}, {
@ -185,15 +189,18 @@ export default function(obj) {
text: `<div class="category-title">${t('ChangelogLastMajor')}</div>`,
raw: true
}, {
text: changelogManager("banner") ?
`<div class="changelog-banner">
<img class="changelog-img" ` +
`src="${changelogManager("banner")["url"]}" ` +
`width="${changelogManager("banner")["width"]}" ` +
`height="${changelogManager("banner")["height"]}" ` +
`onerror="this.style.opacity=0" loading="lazy">`+
`</img>
</div>`: '',
text: (() => {
const banner = changelogManager('banner');
if (!banner) return '';
return `<div class="changelog-banner">
<img class="changelog-img" ` +
`src="${banner.url}" ` +
`alt="${banner.alt.replaceAll('"', '&quot;')}" ` +
`width="${banner.width}" ` +
`height="${banner.height}" ` +
`onerror="this.style.opacity=0" loading="lazy">
</div>`;
})(),
raw: true
}, {
text: changelogManager("version"),
@ -242,13 +249,14 @@ export default function(obj) {
text: `<div class="category-title">${t('DonateSub')}</div>`,
raw: true
}, {
text: `<div class="changelog-banner">
text: `
<div class="changelog-banner">
<img class="changelog-img" ` +
`src="updateBanners/catsleep.webp"` +
`src="updateBanners/catsleep.webp" ` +
`alt="${t("DonateImageDescription")}" ` +
`width="480" ` +
`height="270" ` +
`onerror="this.style.opacity=0" loading="lazy">`+
`</img>
`onerror="this.style.opacity=0" loading="lazy">
</div>`,
raw: true
}, {
@ -319,7 +327,7 @@ export default function(obj) {
})
})
+ settingsCategory({
name: "tiktok",
name: "tiktok-watermark",
title: "tiktok",
body: checkbox([{
action: "disableTikTokWatermark",
@ -327,6 +335,16 @@ export default function(obj) {
padding: "no-margin"
}])
})
+ settingsCategory({
name: "twitter",
title: "twitter",
body: checkbox([{
action: "twitterGif",
name: t("SettingsTwitterGif"),
padding: "no-margin"
}])
+ explanation(t('SettingsTwitterGifDescription'))
})
+ settingsCategory({
name: "codec",
title: t('SettingsCodecSubtitle'),
@ -395,7 +413,7 @@ export default function(obj) {
})
})
+ settingsCategory({
name: "tiktok",
name: "tiktok-audio",
title: "tiktok",
body: checkbox([{
action: "fullTikTokAudio",
@ -568,9 +586,9 @@ export default function(obj) {
<div id="download-area">
<div id="top">
<div id="link-icon">${linkSVG}</div>
<input id="url-input-area" class="mono" type="text" autocorrect="off" maxlength="128" autocapitalize="off" placeholder="${t('LinkInput')}" aria-label="${t('AccessibilityInputArea')}" oninput="button()"></input>
<input id="url-input-area" class="mono" type="text" autocomplete="off" spellcheck="false" maxlength="256" autocapitalize="off" placeholder="${t('LinkInput')}" aria-label="${t('AccessibilityInputArea')}" oninput="button()">
<button id="url-clear" onclick="clearInput()" style="display:none;">x</button>
<input id="download-button" class="mono dontRead" onclick="download(document.getElementById('url-input-area').value)" type="submit" value="" disabled=true aria-label="${t('AccessibilityDownloadButton')}">
<input id="download-button" class="mono dontRead" onclick="download(document.getElementById('url-input-area').value)" type="submit" value="" disabled aria-label="${t('AccessibilityDownloadButton')}">
</div>
<div id="bottom">
<button id="paste" class="switch" onclick="pasteClipboard()" aria-label="${t('PasteFromClipboard')}">${emoji("📋", 22)} ${t('PasteFromClipboard')}</button>
@ -608,7 +626,7 @@ export default function(obj) {
}])}
</footer>
</div>
<script type="text/javascript">
<script>
let defaultApiUrl = '${process.env.apiURL ? process.env.apiURL : ''}';
const loc = ${webLoc(t,
[
@ -632,7 +650,7 @@ export default function(obj) {
'FilenamePreviewAudioAuthor'
])}
</script>
<script type="text/javascript" src="cobalt.js"></script>
<script src="cobalt.js"></script>
</body>
</html>
`

View file

@ -13,6 +13,7 @@ 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 ok from "./services/ok.js";
import tiktok from "./services/tiktok.js";
import tumblr from "./services/tumblr.js";
import vimeo from "./services/vimeo.js";
@ -37,24 +38,31 @@ export default async function(host, patternMatch, url, lang, obj) {
case "twitter":
r = await twitter({
id: patternMatch.id,
index: patternMatch.index - 1
index: patternMatch.index - 1,
toGif: obj.twitterGif
});
break;
case "vk":
r = await vk({
userId: patternMatch["userId"],
videoId: patternMatch["videoId"],
userId: patternMatch.userId,
videoId: patternMatch.videoId,
quality: obj.vQuality
});
break;
case "ok":
r = await ok({
id: patternMatch.id,
quality: obj.vQuality
});
break;
case "bilibili":
r = await bilibili({
id: patternMatch["id"].slice(0, 12)
id: patternMatch.id.slice(0, 12)
});
break;
case "youtube":
let fetchInfo = {
id: patternMatch["id"].slice(0, 11),
id: patternMatch.id.slice(0, 11),
quality: obj.vQuality,
format: obj.vCodec,
isAudioOnly: isAudioOnly,
@ -72,16 +80,16 @@ export default async function(host, patternMatch, url, lang, obj) {
break;
case "reddit":
r = await reddit({
sub: patternMatch["sub"],
id: patternMatch["id"]
sub: patternMatch.sub,
id: patternMatch.id
});
break;
case "douyin":
case "tiktok":
r = await tiktok({
host: host,
postId: patternMatch["postId"],
id: patternMatch["id"],
postId: patternMatch.postId,
id: patternMatch.id,
noWatermark: obj.isNoTTWatermark,
fullAudio: obj.isTTFullAudio,
isAudioOnly: isAudioOnly
@ -96,7 +104,7 @@ export default async function(host, patternMatch, url, lang, obj) {
break;
case "vimeo":
r = await vimeo({
id: patternMatch["id"].slice(0, 11),
id: patternMatch.id.slice(0, 11),
quality: obj.vQuality,
isAudioOnly: isAudioOnly,
forceDash: isAudioOnly ? true : obj.vimeoDash
@ -106,10 +114,10 @@ export default async function(host, patternMatch, url, lang, obj) {
isAudioOnly = true;
r = await soundcloud({
url,
author: patternMatch["author"],
song: patternMatch["song"],
shortLink: patternMatch["shortLink"] || false,
accessKey: patternMatch["accessKey"] || false
author: patternMatch.author,
song: patternMatch.song,
shortLink: patternMatch.shortLink || false,
accessKey: patternMatch.accessKey || false
});
break;
case "instagram":
@ -120,31 +128,32 @@ export default async function(host, patternMatch, url, lang, obj) {
break;
case "vine":
r = await vine({
id: patternMatch["id"]
id: patternMatch.id
});
break;
case "pinterest":
r = await pinterest({
id: patternMatch["id"]
id: patternMatch.id,
shortLink: patternMatch.shortLink || false
});
break;
case "streamable":
r = await streamable({
id: patternMatch["id"],
id: patternMatch.id,
quality: obj.vQuality,
isAudioOnly: isAudioOnly,
});
break;
case "twitch":
r = await twitch({
clipId: patternMatch["clip"] || false,
clipId: patternMatch.clip || false,
quality: obj.vQuality,
isAudioOnly: obj.isAudioOnly
});
break;
case "rutube":
r = await rutube({
id: patternMatch["id"],
id: patternMatch.id,
quality: obj.vQuality,
isAudioOnly: isAudioOnly
});
@ -166,7 +175,11 @@ export default async function(host, patternMatch, url, lang, obj) {
: loc(lang, r.error)
})
return matchActionDecider(r, host, obj.aFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, obj.filenamePattern)
return matchActionDecider(
r, host, obj.aFormat, isAudioOnly,
lang, isAudioMuted, disableMetadata,
obj.filenamePattern, obj.twitterGif
)
} catch (e) {
return apiJSON(0, { t: genericError(lang, host) })
}

View file

@ -3,7 +3,7 @@ import { apiJSON } from "../sub/utils.js";
import loc from "../../localization/manager.js";
import createFilename from "./createFilename.js";
export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, filenamePattern) {
export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, filenamePattern, toGif) {
let action,
responseType = 2,
defaultParams = {
@ -14,13 +14,14 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
fileMetadata: !disableMetadata ? r.fileMetadata : false
},
params = {},
audioFormat = String(userFormat)
audioFormat = String(userFormat);
if (r.isPhoto) action = "photo";
else if (r.picker) action = "picker"
else if (isAudioMuted) action = "muteVideo";
else if (isAudioOnly) action = "audio";
else if (r.isM3U8) action = "singleM3U8";
else if (r.isGif && toGif) action = "gif";
else action = "video";
if (action === "picker" || action === "audio") {
@ -39,6 +40,10 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
case "photo":
responseType = 1;
break;
case "gif":
params = { type: "gif" }
break;
case "singleM3U8":
params = { type: "remux" }

View file

@ -0,0 +1,56 @@
import { genericUserAgent, maxVideoDuration } from "../../config.js";
import { cleanString } from "../../sub/utils.js";
const resolutions = {
"ultra": "2160",
"quad": "1440",
"full": "1080",
"hd": "720",
"sd": "480",
"low": "360",
"lowest": "240",
"mobile": "144"
}
export default async function(o) {
let quality = o.quality === "max" ? "2160" : o.quality;
let html = await fetch(`https://ok.ru/video/${o.id}`, {
headers: { "user-agent": genericUserAgent }
}).then((r) => { return r.text() }).catch(() => { return false });
if (!html) return { error: 'ErrorCouldntFetch' };
if (!html.includes(`<div data-module="OKVideo" data-options="{`)) {
return { error: 'ErrorEmptyDownload' };
}
let videoData = html.split(`<div data-module="OKVideo" data-options="`)[1].split('" data-')[0].replaceAll("&quot;", '"');
videoData = JSON.parse(JSON.parse(videoData).flashvars.metadata);
if (videoData.provider !== "UPLOADED_ODKL") return { error: 'ErrorUnsupported' };
if (videoData.movie.is_live) return { error: 'ErrorLiveVideo' };
if (videoData.movie.duration > maxVideoDuration / 1000) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
let videos = videoData.videos.filter(v => !v.disallowed);
let bestVideo = videos.find(v => resolutions[v.name] === quality) || videos[videos.length - 1];
let fileMetadata = {
title: cleanString(videoData.movie.title.trim()),
author: cleanString(videoData.author.name.trim()),
}
if (bestVideo) return {
urls: bestVideo.url,
filenameAttributes: {
service: "ok",
id: o.id,
title: fileMetadata.title,
author: fileMetadata.author,
resolution: `${resolutions[bestVideo.name]}p`,
qualityLabel: `${resolutions[bestVideo.name]}p`,
extension: "mp4"
}
}
return { error: 'ErrorEmptyDownload' }
}

View file

@ -1,29 +1,36 @@
import { maxVideoDuration } from "../../config.js";
import { genericUserAgent } from "../../config.js";
export default async function(obj) {
const pinId = obj.id.split('--').reverse()[0];
if (!(/^\d+$/.test(pinId))) return { error: 'ErrorCantGetID' };
let data = await fetch(`https://www.pinterest.com/resource/PinResource/get?data=${encodeURIComponent(JSON.stringify({
options: {
field_set_key: "unauth_react_main_pin",
id: pinId
}
}))}`).then((r) => { return r.json() }).catch(() => { return false });
if (!data) return { error: 'ErrorCouldntFetch' };
const videoLinkBase = {
"regular": "https://v1.pinimg.com/videos/mc/720p/",
"story": "https://v1.pinimg.com/videos/mc/720p/"
}
data = data["resource_response"]["data"];
export default async function(o) {
let id = o.id, type = "regular";
let video = null;
if (id.includes("--")) {
id = id.split("--")[1];
type = "story";
}
if (!o.id && o.shortLink) {
id = await fetch(`https://api.pinterest.com/url_shortener/${o.shortLink}/redirect/`, { redirect: "manual" }).then((r) => {
return r.headers.get("location").split('pin/')[1].split('/')[0]
}).catch(() => {});
}
if (!id) return { error: 'ErrorCouldntFetch' };
if (data.videos !== null) video = data.videos.video_list.V_720P;
else if (data.story_pin_data !== null) video = data.story_pin_data.pages[0].blocks[0].video.video_list.V_EXP7;
let html = await fetch(`https://www.pinterest.com/pin/${id}/`, {
headers: { "user-agent": genericUserAgent }
}).then((r) => { return r.text() }).catch(() => { return false });
if (!video) return { error: 'ErrorEmptyDownload' };
if (video.duration > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
if (!html) return { error: 'ErrorCouldntFetch' };
let videoLink = html.split(`"url":"${videoLinkBase[type]}`)[1]?.split('"')[0];
if (!html.includes(videoLink)) return { error: 'ErrorEmptyDownload' };
return {
urls: video.url,
filename: `pinterest_${pinId}.mp4`,
audioFilename: `pinterest_${pinId}_audio`
urls: `${videoLinkBase[type]}${videoLink}`,
filename: `pinterest_${o.id}.mp4`,
audioFilename: `pinterest_${o.id}_audio`
}
}

View file

@ -72,7 +72,7 @@ const requestTweet = (tweetId, token) => {
})
}
export default async function({ id, index }) {
export default async function({ id, index, toGif }) {
let guestToken = await getGuestToken();
if (!guestToken) return { error: 'ErrorCouldntFetch' };
@ -110,7 +110,8 @@ export default async function({ id, index }) {
type: needsFixing(media[0]) ? "remux" : "normal",
urls: bestQuality(media[0].video_info.variants),
filename: `twitter_${id}.mp4`,
audioFilename: `twitter_${id}_audio`
audioFilename: `twitter_${id}_audio`,
isGif: media[0].type === "animated_gif"
};
default:
const picker = media.map((video, i) => {
@ -120,7 +121,9 @@ export default async function({ id, index }) {
service: 'twitter',
type: 'remux',
u: url,
filename: `twitter_${id}_${i + 1}.mp4`
filename: `twitter_${id}_${i + 1}.mp4`,
isGif: media[0].type === "animated_gif",
toGif: toGif ?? false
})
}
return {

View file

@ -4,6 +4,7 @@ import { cleanString } from '../../sub/utils.js';
const resolutionMatch = {
"3840": "2160",
"2732": "1440",
"2560": "1440",
"2048": "1080",
"1920": "1080",
"1366": "720",
@ -63,7 +64,7 @@ export default async function(obj) {
if (!masterJSON) return { error: 'ErrorCouldntFetch' };
if (!masterJSON.video) return { error: 'ErrorEmptyDownload' };
let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width)).filter(a => a['format'] === "mp42"),
let masterJSON_Video = masterJSON.video.sort((a, b) => Number(b.width) - Number(a.width)).filter(a => ["dash", "mp42"].includes(a['format'])),
bestVideo = masterJSON_Video[0];
if (Number(quality) < Number(resolutionMatch[bestVideo["width"]])) {
bestVideo = masterJSON_Video.find(i => resolutionMatch[i["width"]] === quality)

View file

@ -12,7 +12,7 @@ export default async function(o) {
if (!html) return { error: 'ErrorCouldntFetch' };
// decode cyrillic from windows-1251 because vk still uses apis from prehistoring times
// decode cyrillic from windows-1251 because vk still uses apis from prehistoric times
let decoder = new TextDecoder('windows-1251');
html = decoder.decode(html);
@ -35,7 +35,7 @@ export default async function(o) {
let fileMetadata = {
title: cleanString(js.player.params[0].md_title.trim()),
artist: cleanString(js.player.params[0].md_author.trim()),
author: cleanString(js.player.params[0].md_author.trim()),
}
if (url) return {
@ -44,7 +44,7 @@ export default async function(o) {
service: "vk",
id: `${o.userId}_${o.videoId}`,
title: fileMetadata.title,
author: fileMetadata.artist,
author: fileMetadata.author,
resolution: `${quality}p`,
qualityLabel: `${quality}p`,
extension: "mp4"

View file

@ -1,5 +1,5 @@
{
"audioIgnore": ["vk"],
"audioIgnore": ["vk", "ok"],
"config": {
"bilibili": {
"alias": "bilibili.com videos",
@ -9,6 +9,7 @@
"reddit": {
"alias": "reddit videos & gifs",
"patterns": ["r/:sub/comments/:id/:title"],
"subdomains": "*",
"enabled": true
},
"twitter": {
@ -28,6 +29,12 @@
"patterns": ["video:userId_:videoId", "clip:userId_:videoId", "clips:duplicate?z=clip:userId_:videoId"],
"enabled": true
},
"ok": {
"alias": "ok video",
"tld": "ru",
"patterns": ["video/:id"],
"enabled": true
},
"youtube": {
"alias": "youtube videos, shorts & music",
"patterns": ["watch?v=:id", "embed/:id", "watch/:id"],
@ -68,7 +75,7 @@
"alias": "instagram reels, posts & stories",
"patterns": [
"reels/:postId", "reel/:postId", "p/:postId",
"stories/:username/:storyId"
"tv/:postId", "stories/:username/:storyId"
],
"enabled": true
},
@ -80,7 +87,7 @@
},
"pinterest": {
"alias": "pinterest videos & stories",
"patterns": ["pin/:id"],
"patterns": ["pin/:id", "url_shortener/:shortLink"],
"enabled": true
},
"streamable": {

View file

@ -5,9 +5,12 @@ export const testers = {
"instagram": (patternMatch) =>
patternMatch.postId?.length <= 12
|| (patternMatch.username?.length <= 30 && patternMatch.storyId?.length <= 24),
"ok": (patternMatch) =>
patternMatch.id?.length <= 16,
"pinterest": (patternMatch) =>
patternMatch.id?.length <= 128,
patternMatch.id?.length <= 128 || patternMatch.shortLink?.length <= 32,
"reddit": (patternMatch) =>
patternMatch.sub?.length <= 22 && patternMatch.id?.length <= 10,

View file

@ -25,6 +25,13 @@ export function aliasURL(url) {
}`)
}
break;
case "pin":
if (url.hostname === 'pin.it' && parts.length === 2) {
url = new URL(`https://pinterest.com/url_shortener/${
encodeURIComponent(parts[1])
}`)
}
break;
case "vxtwitter":
case "fixvx":

View file

@ -43,7 +43,7 @@ export function createStream(obj) {
exp = streamInfo.exp;
ghmac = streamInfo.hmac;
}
return `${process.env.apiURL || process.env.selfURL}api/stream?t=${streamID}&e=${exp}&h=${ghmac}`;
return `${process.env.apiURL}api/stream?t=${streamID}&e=${exp}&h=${ghmac}`;
}
export function verifyStream(id, hmac, exp) {

View file

@ -1,4 +1,4 @@
import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly } from "./types.js";
import { streamAudioOnly, streamDefault, streamLiveRender, streamVideoOnly, convertToGif } from "./types.js";
export default async function(res, streamInfo) {
try {
@ -10,6 +10,9 @@ export default async function(res, streamInfo) {
case "render":
await streamLiveRender(streamInfo, res);
break;
case "gif":
convertToGif(streamInfo, res);
break;
case "remux":
case "mute":
streamVideoOnly(streamInfo, res);

View file

@ -212,3 +212,40 @@ export function streamVideoOnly(streamInfo, res) {
shutdown();
}
}
export function convertToGif(streamInfo, res) {
let process;
const shutdown = () => (killProcess(process), closeResponse(res));
try {
let args = [
'-loglevel', '-8'
]
if (streamInfo.service === "twitter") {
args.push('-seekable', '0')
}
args.push('-i', streamInfo.urls)
args = args.concat(ffmpegArgs["gif"]);
args.push('-f', "gif", 'pipe:3');
process = spawn(ffmpeg, args, {
windowsHide: true,
stdio: [
'inherit', 'inherit', 'inherit',
'pipe'
],
});
const [,,, muxOutput] = process.stdio;
res.setHeader('Connection', 'keep-alive');
res.setHeader('Content-Disposition', contentDisposition(streamInfo.filename.split('.')[0] + ".gif"));
pipe(muxOutput, res, shutdown);
process.on('close', shutdown);
res.on('finish', shutdown);
} catch {
shutdown();
}
}

View file

@ -8,7 +8,7 @@ const apiVar = {
aFormat: ["best", "mp3", "ogg", "wav", "opus"],
filenamePattern: ["classic", "pretty", "basic", "nerdy"]
},
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash", "disableMetadata"]
booleanOnly: ["isAudioOnly", "isNoTTWatermark", "isTTFullAudio", "isAudioMuted", "dubLang", "vimeoDash", "disableMetadata", "twitterGif"]
}
const forbiddenChars = ['}', '{', '(', ')', '\\', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'", "@", '=='];
const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '='];
@ -84,7 +84,8 @@ export function checkJSONPost(obj) {
isAudioMuted: false,
disableMetadata: false,
dubLang: false,
vimeoDash: false
vimeoDash: false,
twitterGif: false
}
try {
let objKeys = Object.keys(obj);

View file

@ -1162,5 +1162,14 @@
"code": 200,
"status": "stream"
}
}],
"ok": [{
"name": "regular video",
"url": "https://ok.ru/video/7204071410346",
"params": {},
"expected": {
"code": 200,
"status": "stream"
}
}]
}