5.4: instagram support

This commit is contained in:
wukko 2023-04-25 01:01:25 +06:00
parent b4eddd06fe
commit 0dca373237
17 changed files with 146 additions and 56 deletions

View file

@ -1,7 +1,7 @@
{
"name": "cobalt",
"description": "save what you love",
"version": "5.3.3",
"version": "5.4",
"author": "wukko",
"exports": "./src/cobalt.js",
"type": "module",
@ -34,6 +34,6 @@
"node-cache": "^5.1.2",
"url-pattern": "1.0.3",
"xml-js": "^1.6.11",
"youtubei.js": "4.1.1"
"youtubei.js": "4.3.0"
}
}

View file

@ -82,37 +82,51 @@ a {
:focus-visible {
outline: var(--border-15);
}
.checkbox {
display: inline-flex;
align-items: center;
flex-direction: row;
flex-wrap: nowrap;
padding: 0.55rem 1rem 0.55rem 0.7rem;
width: auto;
margin-right: var(--padding-1);
margin-bottom: var(--padding-1);
background: var(--accent-button-bg);
}
.checkbox-label {
line-height: 1.3rem;
}
[type="checkbox"] {
-webkit-appearance: none;
appearance: none;
margin-right: var(--padding-1);
width: 20px;
height: 20px;
z-index: 0;
border: 0;
height: 15px;
width: 15px;
margin-right: var(--padding-1);
border: 0.15rem solid var(--accent);
}
[type="checkbox"]::before {
content: "";
width: 15px;
height: 15px;
border: 0.15rem solid var(--accent);
display: block;
z-index: 5;
display: none;
position: relative;
width: 6px;
height: 12px;
z-index: 5;
transform: scaleX(0.9)rotate(45deg);
left: 6px;
top: 1px;
border-bottom: 0.18rem solid var(--background);
border-right: 0.18rem solid var(--background);
}
[type="checkbox"]:checked::before {
background: var(--checkmark);
background-size: 90%;
background-position: center;
background-repeat: no-repeat;
display: block;
}
[type="checkbox"]:checked::before {
[type="checkbox"]:checked {
background-color: var(--accent);
border: 0.15rem solid var(--accent);
border: 0;
}
.checkbox span {
margin-top: 0.21rem;
margin-left: 0.4rem;
input[type="checkbox"] {
cursor: pointer;
}
button {
background: none;
@ -160,9 +174,6 @@ button:active,
cursor: pointer;
transform: scale(0.95)
}
input[type="checkbox"] {
cursor: pointer;
}
.button {
background: none;
border: var(--border-15);
@ -449,20 +460,6 @@ input[type="checkbox"] {
.no-margin {
margin: 0!important;
}
.checkbox {
display: inline-flex;
align-items: center;
flex-direction: row;
flex-wrap: nowrap;
padding: 0.55rem 1rem 0.8rem 0.7rem;
width: auto;
margin-right: var(--padding-1);
margin-bottom: var(--padding-1);
background: var(--accent-button-bg);
}
.checkbox-label {
line-height: 1.3rem;
}
.switch-container {
width: 100%;
}
@ -579,6 +576,7 @@ input[type="checkbox"] {
width: 25rem;
margin-bottom: var(--padding-1);
background-color: var(--accent-button-bg);
border: var(--accent-button-bg) 0.18rem solid;
position: relative;
}
#picker-holder {

View file

@ -1,7 +1,7 @@
let ua = navigator.userAgent.toLowerCase();
let isIOS = ua.match("iphone os");
let isMobile = ua.match("android") || ua.match("iphone os");
let version = 25;
let version = 26;
let regex = new RegExp(/https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/);
let notification = `<div class="notification-dot"></div>`

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 KiB

View file

@ -1,3 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 13.3871H2.1188L2.57078 14.1436L12.1982 30.2565L12.3437 30.5H12.6274H14.9529H15.2564L15.3965 30.2308L29.4436 3.23077L29.8238 2.5H29H25.6087H25.3024L25.1633 2.77281L13.875 24.903L6.45111 13.6124L6.30297 13.3871H6.03333H3Z" fill="white" stroke="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 366 B

View file

@ -1,3 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 13.3871H2.1188L2.57078 14.1436L12.1982 30.2565L12.3437 30.5H12.6274H14.9529H15.2564L15.3965 30.2308L29.4436 3.23077L29.8238 2.5H29H25.6087H25.3024L25.1633 2.77281L13.875 24.903L6.45111 13.6124L6.30297 13.3871H6.03333H3Z" fill="black" stroke="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 366 B

View file

@ -94,9 +94,9 @@
"ChangelogPressToHide": "collapse",
"Donate": "donate",
"DonateSub": "help me keep it up",
"DonateExplanation": "{appName} does not (and will never) serve ads or sell your data, therefore it's <span class=\"text-backdrop\">completely free to use</span>. but turns out keeping up a web service used by over 40 thousand people is somewhat costly.\n\nif you ever found {appName} useful and want to keep it online, or simply want to thank the developer, consider chipping in! each and every cent helps and is VERY appreciated :D",
"DonateExplanation": "{appName} does not (and will never) serve ads or sell your data, therefore it's <span class=\"text-backdrop\">completely free to use</span>. but turns out developing and keeping up a web service used by over 80 thousand people is not that easy.\n\nif you ever found {appName} useful and want to keep it online, or simply want to thank the developer, consider chipping in! every cent helps and is VERY appreciated :D",
"DonateVia": "donate via",
"DonateHireMe": "or you can <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">hire me</a>",
"DonateHireMe": "...or you can <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">hire me</a> :)",
"SettingsVideoMute": "mute audio",
"SettingsVideoMuteExplanation": "removes audio from video downloads when possible.",
"ErrorSoundCloudNoClientId": "i couldn't get the temporary token that's required to download songs from soundcloud. try again, but if issue persists, {ContactLink}.",

View file

@ -94,9 +94,9 @@
"ChangelogPressToHide": "скрыть",
"Donate": "задонатить",
"DonateSub": "ты можешь помочь!",
"DonateExplanation": "{appName} не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает <span class=\"text-backdrop\">совершенно бесплатно</span>. но оказывается, что хостинг сервиса, которым пользуются более 40 тысяч людей, обходится довольно дорого.\n\nесли {appName} тебе помог и ты хочешь поблагодарить или помочь разработчику, то это можно сделать через донаты! каждый рубль помогает мне, моим котам, и {appName}! спасибо :)",
"DonateExplanation": "{appName} не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает <span class=\"text-backdrop\">совершенно бесплатно</span>. но оказывается, что разработка и поддержка сервиса, которым пользуются более 80 тысяч людей, обходится довольно трудно.\n\nесли {appName} тебе помог и ты хочешь поблагодарить разработчика, то это можно сделать через донаты! каждый рубль помогает мне, моим котам, и {appName}! спасибо :)",
"DonateVia": "открыть",
"DonateHireMe": "или же ты можешь <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">пригласить меня на работу</a>",
"DonateHireMe": "...или же ты можешь <a class=\"text-backdrop italic\" href=\"{s}\" target=\"_blank\">пригласить меня на работу</a> :)",
"SettingsVideoMute": "убрать аудио",
"SettingsVideoMuteExplanation": "убирает аудио при загрузке видео, но только когда это возможно.",
"ErrorSoundCloudNoClientId": "мне не удалось достать временный токен, который необходим для скачивания аудио из soundcloud. попробуй ещё раз, но если так и не получится, {ContactLink}.",

View file

@ -1,11 +1,16 @@
{
"current": {
"version": "5.4",
"title": "instagram support, hop, docker, and more!",
"banner": "catphonestand.webp",
"content": "something many of you've been waiting for is finally here! try it out and let me know what you think :)\n\n<span class='text-backdrop'>tl;dr:</span>\n*; added experimental instagram support! download any reels or videos you like, and make sure to report any issues you encounter. yes, you can convert either to audio.\n*; fixed support for on.soundcloud links.\n*; added share button to \"how to save?\" popup.\n*; added docker support.\n*; moved main instance to hop.io.\n\nservice improvements:\n*; added experimental support for videos from instagram. currently only reels and post videos are downloadable, but i'm looking into ways to save high resolution photos too. if you experience any issues, please report them on either of support platforms.\n*; fixed support for on.soundcloud share links. should work just as well as other versions!\n*; fixed an issue that made some youtube videos impossible to download.\n\ninterface improvements:\n*; new css-only checkmark! yes, i can't stop tinkering with it because slight flashing on svg load annoyed me. now it loads instantly (and also looks slightly better).\n*; fixed copy animation.\n*; minor localization improvements.\n*; fixed the embed logo that i broke somewhere in between 5.3 and 5.4.\n\ninternal improvements:\n*; now using nanoid for live render stream ids.\n*; added support for docker. it's kind of clumsy because of how i get .git folder inside the container, but if you know how to do it better, feel free to make a pr.\n*; cobalt now checks only for existence of environment variables, not exactly the .env file.\n*; changed the way user ip address is retrieved for instances using cloudflare.\n*; added ability to disable cors, both to setup script and environment variables.\n*; moved main instance to hop.io. it should no longer randomly go down. huge shout out to the hop team for being so generous and saving my ass :D\n\ni can't believe how diverse and widespread cobalt has become. it's used in all fields: music production, education, content creation, and even game development. <span class='text-backdrop'>thank you</span>. this is absolutely nuts.\nif you don't mind sharing, please tell me about your use case (on any platform). i'd really love to hear how you use cobalt and how i could make it even more useful for you."
},
"history": [{
"version": "5.3",
"title": "better looks, better feel",
"banner": "cattired.webp",
"content": "this update isn't as big as previous ones, but it still greatly enhances the cobalt experience.\n\nhere's what's up:\n*; new mode switcher! elegant and 100% clear. should no longer cause any confusion. let me know if you like it better this way :D\n*; wide paste button on mobile is back, but now it's even closer to your finger.\n*; removed the weird grey chin on changelog banners.\n*; removed left-handed layout toggle since it is no longer needed.\n*; fixed input area display in chromium 112+.\n*; centered the main action box.\n*; cleaned up css of main action box to get rid of tricks and ensure correct display on all devices.\n*; fixed a bug that'd cause notifications dots to disappear when an unrelated checkbox was checked.\n\nhopefully from now on i'll focus on adding support for more services.\nthank you for using cobalt. stay cool :)"
},
"history": [{
}, {
"version": "5.2",
"title": "fastest one in the game",
"banner": "catspeed.webp",

View file

@ -51,7 +51,7 @@ export default function(obj) {
<meta property="og:url" content="${process.env.selfURL}" />
<meta property="og:title" content="${appName}" />
<meta property="og:description" content="${t('EmbedBriefDescription')}" />
<meta property="og:image" content="icons/generic.png" />
<meta property="og:image" content="${process.env.selfURL}icons/generic.png" />
<meta name="title" content="${appName}" />
<meta name="description" content="${t('AboutSummary')}" />
<meta name="theme-color" content="#000000" />

View file

@ -15,6 +15,7 @@ import tiktok from "./services/tiktok.js";
import tumblr from "./services/tumblr.js";
import vimeo from "./services/vimeo.js";
import soundcloud from "./services/soundcloud.js";
import instagram from "./services/instagram.js";
export default async function (host, patternMatch, url, lang, obj) {
try {
@ -102,6 +103,9 @@ export default async function (host, patternMatch, url, lang, obj) {
format: obj.aFormat
});
break;
case "instagram":
r = await instagram({ id: patternMatch["id"] ? patternMatch["id"] : false });
break;
default:
return apiJSON(0, { t: errorUnsupported(lang) });
}

View file

@ -52,6 +52,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
params = { type: "bridge" };
break;
case "instagram":
case "tumblr":
case "twitter":
responseType = 1;
@ -72,6 +73,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly, lang, isAudioMute
case "picker":
responseType = 5;
switch (host) {
case "instagram":
case "twitter":
params = { picker: r.picker };
break;

View file

@ -0,0 +1,36 @@
import got from "got";
export default async function(obj) {
// i hate this implementation but fetch doesn't work here for some reason (i personally blame facebook)
let html;
try {
html = await got.get(`https://www.instagram.com/p/${obj.id}/`)
html.on('error', () => {
html = false;
});
html = html ? html.body : false;
} catch (e) {
html = false;
}
if (!html) return { error: 'ErrorCouldntFetch' };
if (!html.includes('application/ld+json')) return { error: 'ErrorEmptyDownload' };
let single, multiple = [], postInfo = JSON.parse(html.split('script type="application/ld+json"')[1].split('">')[1].split('</script>')[0]);
if (postInfo.video.length > 1) {
for (let i in postInfo.video) { multiple.push({type: "video", thumb: postInfo.video[i]["thumbnailUrl"], url: postInfo.video[i]["contentUrl"]}) }
} else if (postInfo.video.length === 1) {
single = postInfo.video[0]["contentUrl"]
} else {
return { error: 'ErrorEmptyDownload' }
}
if (single) {
return { urls: single, filename: `instagram_${obj.id}.mp4`, audioFilename: `instagram_${obj.id}_audio` }
} else if (multiple) {
return { picker: multiple }
} else {
return { error: 'ErrorEmptyDownload' }
}
}

View file

@ -36,12 +36,13 @@ async function findClientID() {
export default async function(obj) {
let html;
if (!obj.author && !obj.song && obj.shortLink) {
html = await fetch(`https://soundcloud.app.goo.gl/${obj.shortLink}/`).then((r) => { return r.text() }).catch(() => { return false });
html = await fetch(`https://soundcloud.app.goo.gl/${obj.shortLink}/`).then((r) => { return r.status === 404 ? false : r.text() }).catch(() => { return false });
if (!html) html = await fetch(`https://on.soundcloud.com/${obj.shortLink}/`).then((r) => { return r.status === 404 ? false : r.text() }).catch(() => { return false })
}
if (obj.author && obj.song) {
html = await fetch(`https://soundcloud.com/${obj.author}/${obj.song}${obj.accessKey ? `/s-${obj.accessKey}` : ''}`).then((r) => { return r.text() }).catch(() => { return false });
}
if (!html) return { error: 'ErrorCouldntFetch'};
if (!html) return { error: 'ErrorCouldntFetch' };
if (!(html.includes('<script>window.__sc_hydration = ')
&& html.includes('"format":{"protocol":"progressive","mime_type":"audio/mpeg"},')
&& html.includes('{"hydratable":"sound","data":'))) {

View file

@ -51,6 +51,11 @@
"patterns": [":author/:song/s-:accessKey", ":author/:song", ":shortLink"],
"bestAudio": "none",
"enabled": true
},
"instagram": {
"alias": "instagram reels & video posts",
"patterns": ["reels/:id", "reel/:id", "p/:id"],
"enabled": true
}
}
}

View file

@ -5,9 +5,9 @@ export const testers = {
"vk": (patternMatch) => (patternMatch["userId"] && patternMatch["videoId"]
&& patternMatch["userId"].length <= 10 && patternMatch["videoId"].length === 9),
"bilibili": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length >= 12),
"bilibili": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 12),
"youtube": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length >= 11),
"youtube": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 11),
"reddit": (patternMatch) => (patternMatch["sub"] && patternMatch["id"] && patternMatch["title"]
&& patternMatch["sub"].length <= 22 && patternMatch["id"].length <= 10 && patternMatch["title"].length <= 96),
@ -24,5 +24,7 @@ export const testers = {
"vimeo": (patternMatch) => ((patternMatch["id"] && patternMatch["id"].length <= 11)),
"soundcloud": (patternMatch) => ((patternMatch["author"] && patternMatch["song"]
&& (patternMatch["author"].length + patternMatch["song"].length) <= 96) || (patternMatch["shortLink"] && patternMatch["shortLink"].length <= 32))
&& (patternMatch["author"].length + patternMatch["song"].length) <= 96) || (patternMatch["shortLink"] && patternMatch["shortLink"].length <= 32)),
"instagram": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 12)
}

View file

@ -755,5 +755,48 @@
"code": 200,
"status": "redirect"
}
}],
"instagram": [{
"name": "several videos in a post (picker)",
"url": "https://www.instagram.com/p/CqifaD0qiDt/",
"params": {},
"expected": {
"code": 200,
"status": "picker"
}
}, {
"name": "reel",
"url": "https://www.instagram.com/reel/CoEBV3eM4QR/",
"params": {},
"expected": {
"code": 200,
"status": "redirect"
}
}, {
"name": "reel (isAudioOnly)",
"url": "https://www.instagram.com/reel/CoEBV3eM4QR/",
"params": {
"isAudioOnly": true
},
"expected": {
"code": 200,
"status": "stream"
}
}, {
"name": "inexistent reel",
"url": "https://www.instagram.com/reel/XXXXXXXXXX/",
"params": {},
"expected": {
"code": 400,
"status": "error"
}
}, {
"name": "inexistent post",
"url": "https://www.instagram.com/reel/XXXXXXXXXX/",
"params": {},
"expected": {
"code": 400,
"status": "error"
}
}]
}