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", "name": "cobalt",
"description": "save what you love", "description": "save what you love",
"version": "7.8.6", "version": "7.9",
"author": "wukko", "author": "wukko",
"exports": "./src/cobalt.js", "exports": "./src/cobalt.js",
"type": "module", "type": "module",

View file

@ -90,7 +90,8 @@
"mp4": ["-c:v", "copy", "-c:a", "copy", "-movflags", "faststart+frag_keyframe+empty_moov"], "mp4": ["-c:v", "copy", "-c:a", "copy", "-movflags", "faststart+frag_keyframe+empty_moov"],
"copy": ["-c:a", "copy"], "copy": ["-c:a", "copy"],
"audio": ["-ar", "48000", "-ac", "2", "-b:a", "320k"], "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": [{ "sponsors": [{
"name": "royale", "name": "royale",

View file

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

View file

@ -30,6 +30,7 @@ const checkboxes = [
"reduceTransparency", "reduceTransparency",
"disableAnimations", "disableAnimations",
"disableMetadata", "disableMetadata",
"twitterGif",
]; ];
const exceptions = { // used for mobile devices const exceptions = { // used for mobile devices
"vQuality": "720" "vQuality": "720"
@ -235,7 +236,7 @@ function popup(type, action, text) {
`<a class="picker-image-container" ${ `<a class="picker-image-container" ${
isIOS ? `onClick="share('${text.arr[i]["url"]}')"` : `href="${text.arr[i]["url"]}" target="_blank"` 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>` `</a>`
} }
break; break;
@ -252,7 +253,7 @@ function popup(type, action, text) {
}>` + }>` +
`<div class="picker-element-name">${text.arr[i].type}</div>` + `<div class="picker-element-name">${text.arr[i].type}</div>` +
`<div class="imageBlock"></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>` `</a>`
} }
eid("picker-download").classList.remove("visible"); eid("picker-download").classList.remove("visible");
@ -381,6 +382,7 @@ async function download(url) {
} }
if (sGet("disableMetadata") === "true") req.disableMetadata = true; if (sGet("disableMetadata") === "true") req.disableMetadata = true;
if (sGet("twitterGif") === "true") req.twitterGif = true;
let j = await fetch(`${apiURL}/api/json`, { let j = await fetch(`${apiURL}/api/json`, {
method: "POST", method: "POST",
@ -601,9 +603,9 @@ window.onload = () => {
if (setUn !== null) { if (setUn !== null) {
if (setUn) { if (setUn) {
sSet("migrated", "true") sSet("migrated", "true")
eid("desc-migration").innerHTML += `<br/><br/>${loc.DataTransferSuccess}` eid("desc-migration").innerHTML += `<br><br>${loc.DataTransferSuccess}`
} else { } 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); window.history.replaceState(null, '', window.location.pathname);
notificationCheck(); notificationCheck();
// fix for animations not working in Safari
if (isIOS) {
document.addEventListener('touchstart', () => {}, true);
}
} }
eid("url-input-area").addEventListener("keydown", (e) => { eid("url-input-area").addEventListener("keydown", (e) => {
button(); 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", "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!", "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.", "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", "AccessibilityInputArea": "link input area",
"AccessibilityOpenAbout": "open about popup", "AccessibilityOpenAbout": "open about popup",
"AccessibilityDownloadButton": "download button", "AccessibilityDownloadButton": "download button",
@ -117,7 +117,6 @@
"ShareURL": "share", "ShareURL": "share",
"ErrorTweetUnavailable": "couldn't find anything about this tweet. this could be because its visibility is limited. try another one!", "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.", "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", "PopupCloseDone": "done",
"Accessibility": "accessibility", "Accessibility": "accessibility",
"SettingsReduceTransparency": "reduce transparency", "SettingsReduceTransparency": "reduce transparency",
@ -134,10 +133,7 @@
"KeyboardShortcutClosePopup": "close all popups", "KeyboardShortcutClosePopup": "close all popups",
"CollapseLegal": "terms and ethics", "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.", "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", "SettingsDisableMetadata": "don't add metadata",
"UrgentNewDomain": "new domain, same cobalt",
"NewDomainWelcomeTitle": "hey there!", "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!", "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 :)", "DataTransferSuccess": "btw, your settings have been transferred automatically :)",
@ -154,10 +150,12 @@
"FilenamePreviewVideoTitle": "Video Title", "FilenamePreviewVideoTitle": "Video Title",
"FilenamePreviewAudioTitle": "Audio Title", "FilenamePreviewAudioTitle": "Audio Title",
"FilenamePreviewAudioAuthor": "Audio Author", "FilenamePreviewAudioAuthor": "Audio Author",
"UrgentFilenameUpdate": "customizable file names!",
"UrgentTwitterPatch": "fixes and easier downloads",
"StatusPage": "service status page", "StatusPage": "service status page",
"TroubleshootingGuide": "self-troubleshooting guide", "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": "вставь ссылку сюда", "LinkInput": "вставь ссылку сюда",
"AboutSummary": "кобальт - твой друг при скачивании контента из соцсетей и других сервисов. никакой рекламы, трекеров и прочего мусора. вставляешь ссылку и получаешь файл. всё. ничего лишнего.", "AboutSummary": "кобальт - твой друг при скачивании контента из соцсетей и других сервисов. никакой рекламы, трекеров и прочего мусора. вставляешь ссылку и получаешь файл. всё. ничего лишнего.",
"EmbedBriefDescription": "сохраняй то, что любишь. без рекламы, трекеров и лишней мороки.", "EmbedBriefDescription": "сохраняй то, что любишь. без рекламы, трекеров и лишней мороки.",
"MadeWithLove": "сделано с любовью <3", "MadeWithLove": "сделано с любовью &lt;3",
"AccessibilityInputArea": "зона вставки ссылки", "AccessibilityInputArea": "зона вставки ссылки",
"AccessibilityOpenAbout": "открыть окно с инфой", "AccessibilityOpenAbout": "открыть окно с инфой",
"AccessibilityDownloadButton": "кнопка скачивания", "AccessibilityDownloadButton": "кнопка скачивания",
@ -118,7 +118,6 @@
"ShareURL": "поделиться", "ShareURL": "поделиться",
"ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость была ограничена. попробуй другой!", "ErrorTweetUnavailable": "не смог найти что-либо об этом твите. возможно его видимость была ограничена. попробуй другой!",
"ErrorTwitterRIP": "твиттер ограничил доступ к любому контенту на сайте для пользователей без аккаунтов. я нашёл лазейку, чтобы доставать обычные твиты, а для spaces, к сожалению, нет. я ищу возможные варианты выхода из ситуации.", "ErrorTwitterRIP": "твиттер ограничил доступ к любому контенту на сайте для пользователей без аккаунтов. я нашёл лазейку, чтобы доставать обычные твиты, а для spaces, к сожалению, нет. я ищу возможные варианты выхода из ситуации.",
"UrgentDonate": "нужна твоя помощь!",
"PopupCloseDone": "готово", "PopupCloseDone": "готово",
"Accessibility": "общедоступность", "Accessibility": "общедоступность",
"SettingsReduceTransparency": "уменьшить прозрачность", "SettingsReduceTransparency": "уменьшить прозрачность",
@ -135,10 +134,7 @@
"KeyboardShortcutClosePopup": "закрыть все окна", "KeyboardShortcutClosePopup": "закрыть все окна",
"CollapseLegal": "принципы и этика", "CollapseLegal": "принципы и этика",
"FairUse": "кобальт - это веб инструмент для облегчения скачивания контента из интернета. сервера обработки работают как <span class=\"text-backdrop\">ограниченные прокси</span>, так что ничего никогда не сохраняется или кэшируется.\n\nкобальт <span class=\"text-backdrop\">не несёт никакой ответственности</span>, только ты (конечный пользователь) несёшь ответственность за то, что скачиваешь, как используешь и распространяешь скачанный контент. будь сознателен при использовании чужого контента и всегда указывай авторов!\n\nприкладывай ссылку на источник при использовании в образовательных целях (лекции, домашние задания и т.п.)\n\nчестное использование и указание авторства выгодно всем.", "FairUse": "кобальт - это веб инструмент для облегчения скачивания контента из интернета. сервера обработки работают как <span class=\"text-backdrop\">ограниченные прокси</span>, так что ничего никогда не сохраняется или кэшируется.\n\nкобальт <span class=\"text-backdrop\">не несёт никакой ответственности</span>, только ты (конечный пользователь) несёшь ответственность за то, что скачиваешь, как используешь и распространяешь скачанный контент. будь сознателен при использовании чужого контента и всегда указывай авторов!\n\nприкладывай ссылку на источник при использовании в образовательных целях (лекции, домашние задания и т.п.)\n\nчестное использование и указание авторства выгодно всем.",
"UrgentFeatureUpdate71": "расширение поддержки сервисов!",
"UrgentThanks": "спасибо за поддержку!",
"SettingsDisableMetadata": "не добавлять метаданные", "SettingsDisableMetadata": "не добавлять метаданные",
"UrgentNewDomain": "новый домен, тот же кобальт",
"NewDomainWelcomeTitle": "привет!", "NewDomainWelcomeTitle": "привет!",
"NewDomainWelcome": "кобальт переезжает! те же функции, тот же владелец, просто более запоминающийся домен. по-прежнему без рекламы.\n\n<span class=\"text-backdrop\">cobalt.tools</span> - новый основной домен, т.е. где ты сейчас находишься. не забудь обновить закладки и переустановить веб-приложение!", "NewDomainWelcome": "кобальт переезжает! те же функции, тот же владелец, просто более запоминающийся домен. по-прежнему без рекламы.\n\n<span class=\"text-backdrop\">cobalt.tools</span> - новый основной домен, т.е. где ты сейчас находишься. не забудь обновить закладки и переустановить веб-приложение!",
"DataTransferSuccess": "кстати, твои настройки были перенесены автоматически :)", "DataTransferSuccess": "кстати, твои настройки были перенесены автоматически :)",
@ -156,10 +152,12 @@
"FilenamePreviewVideoTitle": "Название Видео", "FilenamePreviewVideoTitle": "Название Видео",
"FilenamePreviewAudioTitle": "Название Аудио", "FilenamePreviewAudioTitle": "Название Аудио",
"FilenamePreviewAudioAuthor": "Автор Аудио", "FilenamePreviewAudioAuthor": "Автор Аудио",
"UrgentFilenameUpdate": "изменяемые названия файлов!",
"UrgentTwitterPatch": "фиксы и удобное скачивание",
"StatusPage": "статус серверов", "StatusPage": "статус серверов",
"TroubleshootingGuide": "гайд по устранению проблем", "TroubleshootingGuide": "гайд по устранению проблем",
"UpdateNewYears": "новогодняя уборка" "DonateImageDescription": "кошка спит на клавиатуре ноутбука и многократно печатает буквы",
"UpdateNewYears": "новогодняя уборка",
"SettingsTwitterGif": "конвертировать гифки в .gif",
"SettingsTwitterGifDescription": "конвертирование зацикленного видео в .gif снижает качество и значительно увеличивает размер файла. если важна максимальная эффективность, то не используй эту функцию.",
"UpdateTwitterGif": "гифки с твиттера и одноклассники"
} }
} }

View file

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

View file

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

View file

@ -62,5 +62,5 @@ export default function(emoji, size, disablePadding, fluent) {
let filePath = `emoji/${names[emoji]}.svg`; let filePath = `emoji/${names[emoji]}.svg`;
if (fluent) filePath = `emoji/3d/${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 = `` body = ``
for (let i = 0; i < obj.body.length; i++) { for (let i = 0; i < obj.body.length; i++) {
if (obj.body[i]["text"].length > 0) { 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"]) { if (i !== obj.body.length - 1 && !obj.body[i]["nopadding"]) {
classes.push("desc-padding") 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 ` return `
${obj.standalone ? `<div id="popup-${obj.name}" class="popup center${!obj.buttonOnly ? " box" : ''}${classes.length > 0 ? ' ' + classes.join(' ') : ''}">` : ''} ${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 class="popup-header">
<div id="popup-header-contents"> <div class="popup-header-contents">
${obj.buttonOnly ? obj.header.emoji : ``} ${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.aboveTitle ? `<a class="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.title ? `<div class="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''} ${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
</div> </div>
${!obj.buttonOnly ? `<div class="glass-bkg alone"></div>` : ''} ${!obj.buttonOnly ? `<div class="glass-bkg alone"></div>` : ''}
</div> </div>
<div id="popup-content" class="popup-content-inner"> <div class="popup-content popup-content-inner">
${body}${obj.buttonOnly ? `<button id="close-error" class="switch" onclick="popup('${obj.name}', 0)">${obj.buttonText}</button>` : ''} ${body}${obj.buttonOnly ? `<button class="close-error switch" onclick="popup('${obj.name}', 0)">${obj.buttonText}</button>` : ''}
</div> </div>
${classes.includes("small") ? `<div class="glass-bkg small"></div>` : ''} ${classes.includes("small") ? `<div class="glass-bkg small"></div>` : ''}
${obj.standalone ? `</div>` : ''}` ${obj.standalone ? `</div>` : ''}`
@ -87,7 +87,7 @@ export function popup(obj) {
export function multiPagePopup(obj) { export function multiPagePopup(obj) {
let tabs = ` 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} ${backButtonSVG}
</button>`; </button>`;
@ -99,16 +99,16 @@ export function multiPagePopup(obj) {
return ` return `
<div id="popup-${obj.name}" class="popup center box scrollable"> <div id="popup-${obj.name}" class="popup center box scrollable">
<div id="popup-content"> <div class="popup-content">
${obj.header ? `<div id="popup-header" class="popup-header"> ${obj.header ? `<div class="popup-header">
<div id="popup-header-contents"> <div class="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.aboveTitle ? `<a class="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.title ? `<div class="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''} ${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
</div> </div>
<div class="glass-bkg alone"></div> <div class="glass-bkg alone"></div>
</div>` : ''}${tabContent}</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="switches popup-tabs-child">${tabs}</div>
<div class="glass-bkg alone"></div> <div class="glass-bkg alone"></div>
</div> </div>
@ -131,7 +131,7 @@ export function collapsibleList(arr) {
} }
export function popupWithBottomButtons(obj) { export function popupWithBottomButtons(obj) {
let tabs = ` 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} ${backButtonSVG}
</button>` </button>`
@ -140,17 +140,17 @@ export function popupWithBottomButtons(obj) {
} }
return ` return `
<div id="popup-${obj.name}" class="popup center box scrollable"> <div id="popup-${obj.name}" class="popup center box scrollable">
<div id="popup-content"> <div class="popup-content">
${obj.header ? `<div id="popup-header" class="popup-header"> ${obj.header ? `<div class="popup-header">
<div id="popup-header-contents"> <div class="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.aboveTitle ? `<a class="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.title ? `<div class="popup-title">${obj.header.title}</div>` : ''}
${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''} ${obj.header.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
${obj.header.explanation ? `<div class="explanation">${obj.header.explanation}</div>` : ''} ${obj.header.explanation ? `<div class="explanation">${obj.header.explanation}</div>` : ''}
</div> </div>
<div class="glass-bkg alone"></div> <div class="glass-bkg alone"></div>
</div>` : ''}${obj.content}</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 id="picker-buttons" class="switches popup-tabs-child">${tabs}</div>
<div class="glass-bkg alone"></div> <div class="glass-bkg alone"></div>
</div> </div>
@ -171,7 +171,7 @@ export function socialLinks(lang) {
} }
export function settingsCategory(obj) { export function settingsCategory(obj) {
return `<div id="settings-${obj.name}" class="settings-category"> 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 class="category-content">${obj.body}</div>
</div>` </div>`
} }

View file

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

View file

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

View file

@ -13,6 +13,7 @@ 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 ok from "./services/ok.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 vimeo from "./services/vimeo.js"; import vimeo from "./services/vimeo.js";
@ -37,24 +38,31 @@ export default async function(host, patternMatch, url, lang, obj) {
case "twitter": case "twitter":
r = await twitter({ r = await twitter({
id: patternMatch.id, id: patternMatch.id,
index: patternMatch.index - 1 index: patternMatch.index - 1,
toGif: obj.twitterGif
}); });
break; break;
case "vk": case "vk":
r = await vk({ r = await vk({
userId: patternMatch["userId"], userId: patternMatch.userId,
videoId: patternMatch["videoId"], videoId: patternMatch.videoId,
quality: obj.vQuality
});
break;
case "ok":
r = await ok({
id: patternMatch.id,
quality: obj.vQuality quality: obj.vQuality
}); });
break; break;
case "bilibili": case "bilibili":
r = await bilibili({ r = await bilibili({
id: patternMatch["id"].slice(0, 12) id: patternMatch.id.slice(0, 12)
}); });
break; break;
case "youtube": case "youtube":
let fetchInfo = { let fetchInfo = {
id: patternMatch["id"].slice(0, 11), id: patternMatch.id.slice(0, 11),
quality: obj.vQuality, quality: obj.vQuality,
format: obj.vCodec, format: obj.vCodec,
isAudioOnly: isAudioOnly, isAudioOnly: isAudioOnly,
@ -72,16 +80,16 @@ export default async function(host, patternMatch, url, lang, obj) {
break; break;
case "reddit": case "reddit":
r = await reddit({ r = await reddit({
sub: patternMatch["sub"], sub: patternMatch.sub,
id: patternMatch["id"] id: patternMatch.id
}); });
break; break;
case "douyin": case "douyin":
case "tiktok": case "tiktok":
r = await tiktok({ r = await tiktok({
host: host, host: host,
postId: patternMatch["postId"], postId: patternMatch.postId,
id: patternMatch["id"], id: patternMatch.id,
noWatermark: obj.isNoTTWatermark, noWatermark: obj.isNoTTWatermark,
fullAudio: obj.isTTFullAudio, fullAudio: obj.isTTFullAudio,
isAudioOnly: isAudioOnly isAudioOnly: isAudioOnly
@ -96,7 +104,7 @@ export default async function(host, patternMatch, url, lang, obj) {
break; break;
case "vimeo": case "vimeo":
r = await vimeo({ r = await vimeo({
id: patternMatch["id"].slice(0, 11), id: patternMatch.id.slice(0, 11),
quality: obj.vQuality, quality: obj.vQuality,
isAudioOnly: isAudioOnly, isAudioOnly: isAudioOnly,
forceDash: isAudioOnly ? true : obj.vimeoDash forceDash: isAudioOnly ? true : obj.vimeoDash
@ -106,10 +114,10 @@ export default async function(host, patternMatch, url, lang, obj) {
isAudioOnly = true; isAudioOnly = true;
r = await soundcloud({ r = await soundcloud({
url, url,
author: patternMatch["author"], author: patternMatch.author,
song: patternMatch["song"], song: patternMatch.song,
shortLink: patternMatch["shortLink"] || false, shortLink: patternMatch.shortLink || false,
accessKey: patternMatch["accessKey"] || false accessKey: patternMatch.accessKey || false
}); });
break; break;
case "instagram": case "instagram":
@ -120,31 +128,32 @@ export default async function(host, patternMatch, url, lang, obj) {
break; break;
case "vine": case "vine":
r = await vine({ r = await vine({
id: patternMatch["id"] id: patternMatch.id
}); });
break; break;
case "pinterest": case "pinterest":
r = await pinterest({ r = await pinterest({
id: patternMatch["id"] id: patternMatch.id,
shortLink: patternMatch.shortLink || false
}); });
break; break;
case "streamable": case "streamable":
r = await streamable({ r = await streamable({
id: patternMatch["id"], id: patternMatch.id,
quality: obj.vQuality, quality: obj.vQuality,
isAudioOnly: isAudioOnly, isAudioOnly: isAudioOnly,
}); });
break; break;
case "twitch": case "twitch":
r = await twitch({ r = await twitch({
clipId: patternMatch["clip"] || false, clipId: patternMatch.clip || false,
quality: obj.vQuality, quality: obj.vQuality,
isAudioOnly: obj.isAudioOnly isAudioOnly: obj.isAudioOnly
}); });
break; break;
case "rutube": case "rutube":
r = await rutube({ r = await rutube({
id: patternMatch["id"], id: patternMatch.id,
quality: obj.vQuality, quality: obj.vQuality,
isAudioOnly: isAudioOnly isAudioOnly: isAudioOnly
}); });
@ -166,7 +175,11 @@ export default async function(host, patternMatch, url, lang, obj) {
: loc(lang, r.error) : 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) { } catch (e) {
return apiJSON(0, { t: genericError(lang, host) }) 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 loc from "../../localization/manager.js";
import createFilename from "./createFilename.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, let action,
responseType = 2, responseType = 2,
defaultParams = { defaultParams = {
@ -14,13 +14,14 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
fileMetadata: !disableMetadata ? r.fileMetadata : false fileMetadata: !disableMetadata ? r.fileMetadata : false
}, },
params = {}, params = {},
audioFormat = String(userFormat) audioFormat = String(userFormat);
if (r.isPhoto) action = "photo"; if (r.isPhoto) action = "photo";
else if (r.picker) action = "picker" else if (r.picker) action = "picker"
else if (isAudioMuted) action = "muteVideo"; else if (isAudioMuted) action = "muteVideo";
else if (isAudioOnly) action = "audio"; else if (isAudioOnly) action = "audio";
else if (r.isM3U8) action = "singleM3U8"; else if (r.isM3U8) action = "singleM3U8";
else if (r.isGif && toGif) action = "gif";
else action = "video"; else action = "video";
if (action === "picker" || action === "audio") { if (action === "picker" || action === "audio") {
@ -40,6 +41,10 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
responseType = 1; responseType = 1;
break; break;
case "gif":
params = { type: "gif" }
break;
case "singleM3U8": case "singleM3U8":
params = { type: "remux" } params = { type: "remux" }
break; break;

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 videoLinkBase = {
const pinId = obj.id.split('--').reverse()[0]; "regular": "https://v1.pinimg.com/videos/mc/720p/",
if (!(/^\d+$/.test(pinId))) return { error: 'ErrorCantGetID' }; "story": "https://v1.pinimg.com/videos/mc/720p/"
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' };
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; let html = await fetch(`https://www.pinterest.com/pin/${id}/`, {
else if (data.story_pin_data !== null) video = data.story_pin_data.pages[0].blocks[0].video.video_list.V_EXP7; headers: { "user-agent": genericUserAgent }
}).then((r) => { return r.text() }).catch(() => { return false });
if (!video) return { error: 'ErrorEmptyDownload' }; if (!html) return { error: 'ErrorCouldntFetch' };
if (video.duration > maxVideoDuration) return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
let videoLink = html.split(`"url":"${videoLinkBase[type]}`)[1]?.split('"')[0];
if (!html.includes(videoLink)) return { error: 'ErrorEmptyDownload' };
return { return {
urls: video.url, urls: `${videoLinkBase[type]}${videoLink}`,
filename: `pinterest_${pinId}.mp4`, filename: `pinterest_${o.id}.mp4`,
audioFilename: `pinterest_${pinId}_audio` 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(); let guestToken = await getGuestToken();
if (!guestToken) return { error: 'ErrorCouldntFetch' }; if (!guestToken) return { error: 'ErrorCouldntFetch' };
@ -110,7 +110,8 @@ export default async function({ id, index }) {
type: needsFixing(media[0]) ? "remux" : "normal", type: needsFixing(media[0]) ? "remux" : "normal",
urls: bestQuality(media[0].video_info.variants), urls: bestQuality(media[0].video_info.variants),
filename: `twitter_${id}.mp4`, filename: `twitter_${id}.mp4`,
audioFilename: `twitter_${id}_audio` audioFilename: `twitter_${id}_audio`,
isGif: media[0].type === "animated_gif"
}; };
default: default:
const picker = media.map((video, i) => { const picker = media.map((video, i) => {
@ -120,7 +121,9 @@ export default async function({ id, index }) {
service: 'twitter', service: 'twitter',
type: 'remux', type: 'remux',
u: url, u: url,
filename: `twitter_${id}_${i + 1}.mp4` filename: `twitter_${id}_${i + 1}.mp4`,
isGif: media[0].type === "animated_gif",
toGif: toGif ?? false
}) })
} }
return { return {

View file

@ -4,6 +4,7 @@ import { cleanString } from '../../sub/utils.js';
const resolutionMatch = { const resolutionMatch = {
"3840": "2160", "3840": "2160",
"2732": "1440", "2732": "1440",
"2560": "1440",
"2048": "1080", "2048": "1080",
"1920": "1080", "1920": "1080",
"1366": "720", "1366": "720",
@ -63,7 +64,7 @@ export default async function(obj) {
if (!masterJSON) return { error: 'ErrorCouldntFetch' }; if (!masterJSON) return { error: 'ErrorCouldntFetch' };
if (!masterJSON.video) return { error: 'ErrorEmptyDownload' }; 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]; bestVideo = masterJSON_Video[0];
if (Number(quality) < Number(resolutionMatch[bestVideo["width"]])) { if (Number(quality) < Number(resolutionMatch[bestVideo["width"]])) {
bestVideo = masterJSON_Video.find(i => resolutionMatch[i["width"]] === quality) 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' }; 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'); let decoder = new TextDecoder('windows-1251');
html = decoder.decode(html); html = decoder.decode(html);
@ -35,7 +35,7 @@ export default async function(o) {
let fileMetadata = { let fileMetadata = {
title: cleanString(js.player.params[0].md_title.trim()), 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 { if (url) return {
@ -44,7 +44,7 @@ export default async function(o) {
service: "vk", service: "vk",
id: `${o.userId}_${o.videoId}`, id: `${o.userId}_${o.videoId}`,
title: fileMetadata.title, title: fileMetadata.title,
author: fileMetadata.artist, author: fileMetadata.author,
resolution: `${quality}p`, resolution: `${quality}p`,
qualityLabel: `${quality}p`, qualityLabel: `${quality}p`,
extension: "mp4" extension: "mp4"

View file

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

View file

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

View file

@ -25,6 +25,13 @@ export function aliasURL(url) {
}`) }`)
} }
break; 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 "vxtwitter":
case "fixvx": case "fixvx":

View file

@ -43,7 +43,7 @@ export function createStream(obj) {
exp = streamInfo.exp; exp = streamInfo.exp;
ghmac = streamInfo.hmac; 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) { 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) { export default async function(res, streamInfo) {
try { try {
@ -10,6 +10,9 @@ export default async function(res, streamInfo) {
case "render": case "render":
await streamLiveRender(streamInfo, res); await streamLiveRender(streamInfo, res);
break; break;
case "gif":
convertToGif(streamInfo, res);
break;
case "remux": case "remux":
case "mute": case "mute":
streamVideoOnly(streamInfo, res); streamVideoOnly(streamInfo, res);

View file

@ -212,3 +212,40 @@ export function streamVideoOnly(streamInfo, res) {
shutdown(); 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"], aFormat: ["best", "mp3", "ogg", "wav", "opus"],
filenamePattern: ["classic", "pretty", "basic", "nerdy"] 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 forbiddenChars = ['}', '{', '(', ')', '\\', '>', '<', '^', '*', '!', '~', ';', ':', ',', '`', '[', ']', '#', '$', '"', "'", "@", '=='];
const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '=']; const forbiddenCharsString = ['}', '{', '%', '>', '<', '^', ';', '`', '$', '"', "@", '='];
@ -84,7 +84,8 @@ export function checkJSONPost(obj) {
isAudioMuted: false, isAudioMuted: false,
disableMetadata: false, disableMetadata: false,
dubLang: false, dubLang: false,
vimeoDash: false vimeoDash: false,
twitterGif: false
} }
try { try {
let objKeys = Object.keys(obj); let objKeys = Object.keys(obj);

View file

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