tiktok images and crowdin

This commit is contained in:
wukko 2022-09-03 21:32:39 +06:00
parent 297c1ed116
commit 9ea832caf5
24 changed files with 313 additions and 269 deletions

View file

@ -6,22 +6,23 @@ Best way to save content you love.
![cobalt logo](https://raw.githubusercontent.com/wukko/cobalt/current/src/front/icons/wide.png "cobalt logo") ![cobalt logo](https://raw.githubusercontent.com/wukko/cobalt/current/src/front/icons/wide.png "cobalt logo")
## What's cobalt? ## What's cobalt?
cobalt is social media downloader with zero bullshit. It's efficient, easy to use, and doesn't bother you with ads or privacy invasion "consent" popups. cobalt is social media downloader with zero bullshit. It's friendly, efficient, works on anything with a browser, and doesn't bother you with porn ads or privacy invasion "consent" popups.
It preserves original media quality so you get best downloads possible (unless you change that in settings). It preserves original media quality so you get best downloads possible (unless you change that in settings).
## Supported services ## Support status
- bilibili.com | Service | Video + Audio | Only audio | Additional features |
- douyin (with or without watermark, preference set by user) | ------ | :-: | :-: | :----- |
- Reddit | Twitter | ✅ | ✅ | |
- SoundCloud | YouTube | ✅ | ✅ | Supports HDR and high FPS videos |
- TikTok (with or without watermark, preference set by user) | YouTube Music | ❌ | ✅ | |
- Tumblr | Reddit | ✅ | ✅ | |
- Twitter | TikTok & douyin | ✅ | ✅ | Videos can be downloaded with or without watermark, preference is set by user. |
- Vimeo | SoundCloud | ❌ | ✅ | |
- VK | bilibili.com | ✅ | ✅ | |
- YouTube (with HDR support) | Tumblr | ✅ | ✅ | |
- YouTube Music | Vimeo | ✅ | ⚒️ | |
| VK | ✅ | ⚒️ | |
## Translations ## Translations
- Spanish: [@adrigoomy](https://github.com/adrigoomy) (translation is outdated, update needed) - Spanish: [@adrigoomy](https://github.com/adrigoomy) (translation is outdated, update needed)
@ -30,32 +31,26 @@ It preserves original media quality so you get best downloads possible (unless y
- Polish: [@hexandcube](https://github.com/hexandcube) - Polish: [@hexandcube](https://github.com/hexandcube)
- Ukrainian: Löffel - Ukrainian: Löffel
### Languages that are always up to date
- English
- Russian
- Ukrainian
Other languages may be missing some strings or changes, you can help with updating those! Other languages may be missing some strings or changes, you can help with updating those!
## How you can help cobalt speak your language ## How to contribute translations
Take English or Russian localization from [this directory](https://github.com/wukko/cobalt/tree/current/src/localization/languages) and use it as a base for your translation. Then simply make a pull request and it'll be out for everyone upon review! You can translate cobalt to any language you want on [cobalt's crowdin](https://crowdin.com/project/cobalt). Feel free to ignore QA errors if you think you know better. If you don't see a language you want to translate cobalt to, open an issue, and I'll add it to crowdin.
### What you should keep in mind: ### Translation guidelines:
- Do **NOT** use formal language, that's boring and lame. Use informal language on all occasions. - Avoid formal language. Leave it for boring big tech companies. Use informal language on all occasions.
- Strings are **ALWAYS** lowercase unless it's an internal value like {ContactLink} or STRESSED LIKE THIS. - Strings are **ALWAYS** stylized as lowercase unless it's STRESSED LIKE THIS or is an internal value like `{ContactLink}`.
- Keep translations as friendly and fun as possible. Just as if cobalt user was your buddy. - Keep translations lively, friendly, and fun. Translate strings as if cobalt user was your buddy.
- Robotic translations from original language are not valid. - Automatic translations from original language are not valid, and will be ignored.
- You can (and should) rephrase sentences as long as they keep the same sense, if you think it'd be better that way. - You can (and should) rephrase sentences as long as they keep the same point, if you think it'd be better that way.
- You can add wordplays or puns if it feels natural to do so. - You can add wordplays or puns if it feels natural to do so.
- Even though I love cursing, keep that to minimum in translations, and do **NOT** use offensive words. - Even though I love cursing, keep that to minimum in translations, and do **NOT** use any offensive words.
- Always check if there are issues in UI with your localization. - Check if there are issues in UI with your localization, and optimize it accordingly, or open an issue.
- There's no need to translate `ChangelogContentTitle` and `ChangelogContent`, because those are very often changed. You can remove both of them from your translation file. - Add "(in english)" translated to your language at the end of `ChangelogLastCommit` and `ChangelogLastMajor`. Those are always kept exclusively in English, due to how often changelog changes.
- Add "(in english)" translated to your language at the end of `ChangelogLastCommit` and `ChangelogLastMajor`. Those are almost always kept exclusively in English. Remove that phrase if you do translate major update changelog. - Example of translation to Russian: `"ChangelogLastCommit": "последний коммит (на английском)"`
- Example: `"ChangelogLastCommit": "последний коммит (на английском)"`
- Be nice. - Be nice.
## Host an instance yourself ## Host an instance yourself
Code might be a little messy, but I do my best to improve it with every commit. You might find cobalt's source code a bit messy, but I do my best to improve it with every commit.
### Requirements ### Requirements
- Node.js 14.16 or above - Node.js 14.16 or above
@ -82,7 +77,7 @@ Setup script installs all needed `npm` dependencies, but you have to install `No
4. Done. 4. Done.
## Disclaimer ## Disclaimer
This is my passion project, so update scheduele depends solely on my motivation. Don't expect any consistency in that. cobalt is my passion project, so new feature release schedule depends solely on my motivation and mood. Don't expect any consistency in that.
## License ## License
cobalt is under [AGPL-3.0](https://github.com/wukko/cobalt/blob/current/LICENSE). cobalt is under [AGPL-3.0](https://github.com/wukko/cobalt/blob/current/LICENSE).

View file

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

View file

@ -83,12 +83,12 @@ if (fs.existsSync('./.env')) {
let ip = req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip let ip = req.header('x-forwarded-for') ? req.header('x-forwarded-for') : req.ip
stream(res, ip, req.query.t, req.query.h, req.query.e); stream(res, ip, req.query.t, req.query.h, req.query.e);
} else { } else {
let j = apiJSON(0, { t: loc(languageCode(req), 'ErrorNoStreamID') }) let j = apiJSON(0, { t: "no stream id" })
res.status(j.status).json(j.body); res.status(j.status).json(j.body);
} }
break; break;
default: default:
let j = apiJSON(0, { t: loc(languageCode(req), 'ErrorNoType') }) let j = apiJSON(0, { t: "wrong response type" })
res.status(j.status).json(j.body); res.status(j.status).json(j.body);
break; break;
} }

View file

@ -127,6 +127,10 @@ button:active,
cursor: pointer; cursor: pointer;
transform: scale(0.95) transform: scale(0.95)
} }
.imagepicker-image:active {
cursor: pointer;
transform: scale(0.95)
}
input[type="checkbox"] { input[type="checkbox"] {
cursor: pointer; cursor: pointer;
} }
@ -465,6 +469,30 @@ input[type="checkbox"] {
.button:active .tooltip { .button:active .tooltip {
display: none; display: none;
} }
.imagepicker-image {
object-fit: cover;
width: inherit;
height: inherit;
}
.imagepicker-image-container {
width: 8rem;
height: 8rem;
margin-bottom: 1rem;
background-color: var(--accent-button-bg);
text-align: center;
letter-spacing: -0.2rem;
line-height: 8rem;
}
#imagepicker-holder {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
align-content: space-around;
}
#popup-imagePicker .explanation {
padding-top: 0!important;
padding-bottom: 1rem;
}
/* adapt the page according to screen size */ /* adapt the page according to screen size */
@media screen and (min-width: 2300px) { @media screen and (min-width: 2300px) {
html { html {
@ -553,6 +581,11 @@ input[type="checkbox"] {
} }
} }
@media screen and (max-width: 949px) { @media screen and (max-width: 949px) {
.imagepicker-image-container {
height: 7rem;
width: 7rem;
line-height: 7rem;
}
#close-error { #close-error {
bottom: 5%; bottom: 5%;
position: absolute; position: absolute;
@ -582,3 +615,13 @@ input[type="checkbox"] {
padding-bottom: 2rem; padding-bottom: 2rem;
} }
} }
@media screen and (max-width: 400px) {
.popup-title {
line-height: inherit;
}
.imagepicker-image-container {
line-height: 6rem;
height: 6rem;
width: 6rem;
}
}

View file

@ -1,5 +1,5 @@
let isIOS = navigator.userAgent.toLowerCase().match("iphone os"); let isIOS = navigator.userAgent.toLowerCase().match("iphone os");
let version = 5; let version = 6;
let switchers = { let switchers = {
"theme": ["auto", "light", "dark"], "theme": ["auto", "light", "dark"],
@ -101,6 +101,8 @@ function hideAllPopups() {
for (let i = 0; i < filter.length; i++) { for (let i = 0; i < filter.length; i++) {
filter[i].style.visibility = "hidden"; filter[i].style.visibility = "hidden";
} }
eid("imagepicker-holder").innerHTML = '';
eid("imagepicker-download").href = '/';
eid("popup-backdrop").style.visibility = "hidden"; eid("popup-backdrop").style.visibility = "hidden";
} }
function popup(type, action, text) { function popup(type, action, text) {
@ -130,6 +132,21 @@ function popup(type, action, text) {
} }
eid("popup-download").style.visibility = vis(action); eid("popup-download").style.visibility = vis(action);
break; break;
case "imagePicker":
switch (action) {
case 1:
eid("imagepicker-download").href = text.url;
for (let i in text.images) {
eid("imagepicker-holder").innerHTML += `<div class="imagepicker-image-container"><img class="imagepicker-image" src="${text.images[i]}" onerror="this.style.display='none';this.parentNode.innerHTML=':('"></img></div>`
}
break;
case 0:
eid("imagepicker-download").href = '/';
eid("imagepicker-holder").innerHTML = ''
break;
}
eid("popup-imagePicker").style.visibility = vis(action);
break;
default: default:
eid(`popup-${type}`).style.visibility = vis(action); eid(`popup-${type}`).style.visibility = vis(action);
break; break;
@ -238,13 +255,21 @@ async function download(url) {
window.open(j.url, '_blank'); window.open(j.url, '_blank');
} }
break; break;
case "images":
case "stream": case "stream":
changeDownloadButton(2, '?..') changeDownloadButton(2, '?..')
fetch(`${j.url}&p=1&origin=front`).then(async (res) => { fetch(`${j.url}&p=1&origin=front`).then(async (res) => {
let jp = await res.json(); let jp = await res.json();
if (jp.status == "continue") { if (jp.status == "continue") {
changeDownloadButton(2, '>>>') changeDownloadButton(2, '>>>')
if (j.status === "images") {
popup('imagePicker', 1, {
url: j.url,
images: j.images
})
} else {
window.location.href = j.url window.location.href = j.url
}
setTimeout(() => { setTimeout(() => {
changeDownloadButton(1, '>>') changeDownloadButton(1, '>>')
eid("url-input-area").disabled = false eid("url-input-area").disabled = false
@ -259,7 +284,7 @@ async function download(url) {
default: default:
eid("url-input-area").disabled = false eid("url-input-area").disabled = false
changeDownloadButton(2, '!!') changeDownloadButton(2, '!!')
popup("error", 1, loc.noURLReturned); popup("error", 1, loc.unknownStatus);
break; break;
} }
} else { } else {

View file

@ -0,0 +1,5 @@
{
"ContentTitle": "tiktok images and better localization (3.4)",
"Content": "- added ability to save images from tiktok conveniently, and without watermarks.\n- it's now way easier to contribute translations to cobalt. read more on how to do it <a class=\"text-backdrop\" href=\"https://github.com/wukko/cobalt#how-to-contribute-translations\" target=\"_blank\">on github</a>. in short, you don't need to fork the repo anymore, everything is handled through crowdin :D\n- updated readme in github repo to make it easier to read and understand.\n- began to add more descriptive errors, more to come soon.\n\ninternal stuff:\n- remade entirety of tiktok module and merged it with douyin one. now both (basically identical) platforms have perfect parity of download features.\n- cleaned up the twitter module, now it's way more compact and easy to read.\n- moved changelog out of english localization.\n- other small improvements and fixes.",
"FollowTwitter": "follow cobalt's twitter account for polls, updates, and more: <a class=\"text-backdrop\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a>"
}

View file

@ -1,19 +1,14 @@
{ {
"name": "english", "name": "english",
"code": "en",
"substrings": { "substrings": {
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">let me know</a>" "ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">contact the maintainer/a>"
}, },
"strings": { "strings": {
"ChangelogContentTitle": "soundcloud and better usability (3.3)",
"ChangelogContent": "- full support for soundcloud is here. you now can save your favorite songs from there, if you want to.\n- added ability to download full audios from tiktok/douyin, and made tiktok audio downloads better in general.\n- did you know that there's an audio download mode in cobalt? if you didn't, there's now a tooltip that shows you how to switch between modes.\n- added length limit to conversion of audios, because converting a 3 hour audio to wav will give you a 4gb file, and that's just unreasonable. you can still download audio in original (best) format without any limits.\n- if best and preferred audio format match, cobalt won't needlessly convert the audio anymore.\n- fixed format override for ios, you still might have to toggle between them once.\n- increased input area length limit on frontend because some reddit and soundcloud links wouldn't fit.\n- version in settings now opens current commit page on github, instead of general commits page. it also opens in a new tab instead of replacing the current one.\n- fixed some localization stuff in english, russian, and ukrainian. it's now easier to understand what mode is on, and general cobalt description in russian doesn't sound awkward anymore.",
"FollowTwitter": "follow cobalt's twitter account for polls, updates, and more: <a class=\"text-backdrop\" href=\"https://twitter.com/justusecobalt\" target=\"_blank\">@justusecobalt</a>",
"LinkInput": "paste the link here", "LinkInput": "paste the link here",
"AboutSummary": "{appName} is your go-to place for social media downloads. zero ads, trackers, or any other creepy bullshit attached. simply paste a share link and you're ready to rock!", "AboutSummary": "{appName} is your go-to place for social media downloads. zero ads, trackers, or any other creepy bullshit. simply paste a share link and you're ready to rock!",
"AboutSupportedServices": "currently supported services:", "AboutSupportedServices": "currently supported services:",
"EmbedBriefDescription": "save content from social media without creeps following you around", "EmbedBriefDescription": "save content from social media without annoyances",
"MadeWithLove": "made with <3 by wukko and all contributors on github", "MadeWithLove": "made with <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",
@ -31,18 +26,16 @@
"ErrorUnsupported": "it seems like this service is not supported yet or your link is invalid.", "ErrorUnsupported": "it seems like this service is not supported yet or your link is invalid.",
"ErrorBrokenLink": "{s} is supported, but something is wrong with your link. maybe you didn't copy it fully?", "ErrorBrokenLink": "{s} is supported, but something is wrong with your link. maybe you didn't copy it fully?",
"ErrorNoLink": "i can't guess what you want to download! please give me a link.", "ErrorNoLink": "i can't guess what you want to download! please give me a link.",
"ErrorPageRenderFail": "something went wrong and page couldn't render. if it's a recurring or critical issue, please {ContactLink}. it'd be useful if you provided current commit hash ({s}) and error recreation steps. thank you :D", "ErrorPageRenderFail": "something went wrong and page couldn't render. if it's a recurring or critical issue, please {ContactLink}. it'd be useful if you provided current commit hash ({s}) and error recreation steps. thank you in advance :D",
"ErrorRateLimit": "you're making way too many requests. calm down and try again in a few minutes.", "ErrorRateLimit": "you're making too many requests. calm down and try again in a bit.",
"ErrorCouldntFetch": "couldn't fetch metadata. check if your link is correct and try again.", "ErrorCouldntFetch": "couldn't get any info about your link. check if your link is correct and try again.",
"ErrorLengthLimit": "current length limit is {s} minutes. what you tried to download is longer than {s} minutes. pick something else to download!", "ErrorLengthLimit": "current length limit is {s} minutes. video that you tried to download is longer than {s} minutes. pick something else!",
"ErrorBadFetch": "something went wrong with info fetching. you can try a different format and resolution or just try again later.", "ErrorBadFetch": "an error occured when i tried to get info about your link. are you sure it works? check if it does, and try again.",
"ErrorCorruptedStream": "this download is unfortunately corrupted. try again or try a different format and resolution.", "ErrorCorruptedStream": "this download is unfortunately corrupted. try again or try a different format and resolution.",
"ErrorNoInternet": "there's no internet or {appName} api is down. check your connection and try again.", "ErrorNoInternet": "there's no internet or {appName} api is down. check your connection and try again.",
"ErrorCantConnectToServiceAPI": "i couldn't connect to {s} api. seems like either {s} is down or {appName} server ip got blocked. try again later.", "ErrorCantConnectToServiceAPI": "i couldn't connect to {s} api. seems like either {s} is down or {appName} server ip got blocked. try again later.",
"ErrorEmptyDownload": "there's nothing to download. try something else!", "ErrorEmptyDownload": "i don't see anything i could download from here. try a different link.",
"ErrorLiveVideo": "i can't download a live video. wait for stream to finish and try again.", "ErrorLiveVideo": "i can't look into future and download a video live of which is ongoing. wait for the stream to finish and try again!",
"ErrorNoStreamID": "there's no such streamId.",
"ErrorNoType": "there's no such expected response type.",
"SettingsAppearanceSubtitle": "appearance", "SettingsAppearanceSubtitle": "appearance",
"SettingsThemeSubtitle": "theme", "SettingsThemeSubtitle": "theme",
"SettingsFormatSubtitle": "download format", "SettingsFormatSubtitle": "download format",
@ -61,15 +54,15 @@
"AccessibilityKeepDownloadButton": "keep the download button always visible", "AccessibilityKeepDownloadButton": "keep the download button always visible",
"SettingsEnableDownloadPopup": "ask for a way to save", "SettingsEnableDownloadPopup": "ask for a way to save",
"AccessibilityEnableDownloadPopup": "ask what to do with downloads", "AccessibilityEnableDownloadPopup": "ask what to do with downloads",
"SettingsFormatDescription": "select webm if you need max quality available. webm videos are usually higher quality but ios devices can't play them natively.", "SettingsFormatDescription": "select webm if you want max quality available. webm videos are usually higher quality but ios devices can't play them natively.",
"SettingsQualityDescription": "if selected resolution isn't available, closest one gets picked instead. if you want to post a youtube video on twitter, then select a combination of mp4 and 720p. twitter likes videos like that way more.", "SettingsQualityDescription": "if selected resolution isn't available, closest one gets picked instead. if you want to post a youtube video on twitter, then select a combination of mp4 and 720p. twitter likes videos like that way more.",
"DonateSubtitle": "help me pay for hosting", "DonateSubtitle": "help me pay for hosting",
"DonateDescription": "i don't really like crypto in its current state, but it's the only reliable way for me to receive money and pay for anything abroad.", "DonateDescription": "i don't really like crypto in its current state, but it's the only reliable way for me to receive money and pay for anything abroad.",
"LinkGitHubIssues": "&gt;&gt; report issues and check out the source code on github", "LinkGitHubIssues": "&gt;&gt; report issues and check out the source code on github",
"LinkGitHubChanges": "&gt;&gt; see previous changes and contribute on github", "LinkGitHubChanges": "&gt;&gt; see previous changes and contribute on github",
"LinkDonateContact": "&gt;&gt; let me know if currency you want to donate isn't listed", "LinkDonateContact": "&gt;&gt; let me know if currency you want to donate isn't listed",
"NoScriptMessage": "{appName} uses javascript for api requests and interactive interface. you have to allow javascript to use this site. we don't have any ads or trackers, pinky promise.", "NoScriptMessage": "{appName} uses javascript for api requests and interactive interface. you have to allow javascript to use this site. i don't have any ads or trackers, pinky promise.",
"DownloadPopupDescriptionIOS": "since you have an ios device, you have to press and hold the download button and then select \"download video\" in appeared popup to save the video. this will be required for as long as apple forces safari webview upon all browser developers on ios.", "DownloadPopupDescriptionIOS": "because you have an ios device, you have to press and hold the download button and then select \"download video\" in appeared popup to save the video. this will be required for as long as apple forces safari webview upon all browser developers on ios.",
"DownloadPopupDescription": "download button opens a new tab with requested file. you can disable this popup in settings.", "DownloadPopupDescription": "download button opens a new tab with requested file. you can disable this popup in settings.",
"DownloadPopupWayToSave": "pick a way to save", "DownloadPopupWayToSave": "pick a way to save",
"ClickToCopy": "press to copy", "ClickToCopy": "press to copy",
@ -93,9 +86,16 @@
"ErrorPopupCloseButton": "got it", "ErrorPopupCloseButton": "got it",
"ModeToggle": "mode", "ModeToggle": "mode",
"ModeToggleSmart": "smart", "ModeToggleSmart": "smart",
"PressToChange": "press to change", "ErrorLengthAudioConvert": "current length limit for audio conversion is {s} minutes. pick \"best\" format if you want to avoid limitations.",
"ErrorLengthAudioConvert": "current length limit for audio conversion is {s} minutes. pick \"best\" format instead!",
"SettingsAudioFullTikTok": "download full audio", "SettingsAudioFullTikTok": "download full audio",
"SettingsAudioFullTikTokDescription": "this audio is most often music or original sound used in video. aka audio without voiceover, tts, or trimming will be downloaded, if it's available, of course." "SettingsAudioFullTikTokDescription": "downloads original audio or sound used in video without any additional changes by the video author.",
"ErrorCantGetID": "i couldn't get the full info from the shortened link. make sure it works or try a full one.",
"ErrorNoVideosInTweet": "this tweet doesn't have videos or gifs. try another one!",
"ImagePickerTitle": "pick images to download",
"ImagePickerDownloadAudio": "download audio",
"ImagePickerExplanationPC": "right click an image to save it.",
"ImagePickerExplanationPhone": "press and hold an image to save it.",
"ErrorNoUrlReturned": "server didn't return a download link. this should never happen. reload the page and try again, but if it doesn't help, {ContactLink}.",
"ErrorUnknownStatus": "i received a response i can't process. most likely something with status is wrong. this should never happen. reload the page and try again, but if it doesn't help, {ContactLink}."
} }
} }

View file

@ -1,6 +1,5 @@
{ {
"name": "español", "name": "español",
"code": "es",
"substrings": { "substrings": {
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">hazme saber</a>" "ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">hazme saber</a>"
}, },
@ -9,7 +8,6 @@
"AboutSummary": "{appName} es tu lugar ideal para descargas de redes sociales. sin anuncios u otras mierdas sospechosas. ¡solo necesitas pegar un enlace y listo!", "AboutSummary": "{appName} es tu lugar ideal para descargas de redes sociales. sin anuncios u otras mierdas sospechosas. ¡solo necesitas pegar un enlace y listo!",
"AboutSupportedServices": "servicios compatibles:", "AboutSupportedServices": "servicios compatibles:",
"EmbedBriefDescription": "guarda contenido de redes sociales sin preocuparte por rastreadores", "EmbedBriefDescription": "guarda contenido de redes sociales sin preocuparte por rastreadores",
"MadeWithLove": "hecho con <3 por wukko y todos los contribuyentes en github",
"AccessibilityInputArea": "cuadro de captura", "AccessibilityInputArea": "cuadro de captura",
"AccessibilityOpenAbout": "abrir ventana emergente de acerca de", "AccessibilityOpenAbout": "abrir ventana emergente de acerca de",
"AccessibilityDownloadButton": "botón de descarga", "AccessibilityDownloadButton": "botón de descarga",

View file

@ -1,6 +1,5 @@
{ {
"name": "français", "name": "français",
"code": "fr",
"substrings": { "substrings": {
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">fais-moi signe</a>" "ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">fais-moi signe</a>"
}, },
@ -9,7 +8,6 @@
"AboutSummary": "{appName} est l'endroit parfait pour télécharger des vidéos sur les médias sociaux. zéro pubs, trackers ou tout autre merde. colle simplement le lien d'un post pour commencer!", "AboutSummary": "{appName} est l'endroit parfait pour télécharger des vidéos sur les médias sociaux. zéro pubs, trackers ou tout autre merde. colle simplement le lien d'un post pour commencer!",
"AboutSupportedServices": "services supportés:", "AboutSupportedServices": "services supportés:",
"EmbedBriefDescription": "sauvegarde du contenu des médias sociaux facilement et sans tracking", "EmbedBriefDescription": "sauvegarde du contenu des médias sociaux facilement et sans tracking",
"MadeWithLove": "fait avec <3 par wukko et tous les contributeurs sur github",
"AccessibilityInputArea": "zone de saisie du lien", "AccessibilityInputArea": "zone de saisie du lien",
"AccessibilityOpenAbout": "ouvrir la fenêtre contextuelle de la section à propos", "AccessibilityOpenAbout": "ouvrir la fenêtre contextuelle de la section à propos",
"AccessibilityDownloadButton": "bouton télécharger", "AccessibilityDownloadButton": "bouton télécharger",

View file

@ -1,6 +1,5 @@
{ {
"name": "indonesia", "name": "indonesia",
"code": "id",
"substrings": { "substrings": {
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">beri tau saya</a>" "ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">beri tau saya</a>"
}, },
@ -9,7 +8,6 @@
"AboutSummary": "{appName} adalah tempat terbaik kamu untuk download video sosial media tanpa iklan, pelacak, atau omong kosong lainnya. tinggal tempel link dan udah deh", "AboutSummary": "{appName} adalah tempat terbaik kamu untuk download video sosial media tanpa iklan, pelacak, atau omong kosong lainnya. tinggal tempel link dan udah deh",
"AboutSupportedServices": "layanan yang didukung:", "AboutSupportedServices": "layanan yang didukung:",
"EmbedBriefDescription": "simpan konten dari sosial media tanpa hal aneh mengikuti kamu", "EmbedBriefDescription": "simpan konten dari sosial media tanpa hal aneh mengikuti kamu",
"MadeWithLove": "dibuat dengan <3",
"AccessibilityInputArea": "tempat tempel link", "AccessibilityInputArea": "tempat tempel link",
"AccessibilityOpenAbout": "buka bagian pengantar", "AccessibilityOpenAbout": "buka bagian pengantar",
"AccessibilityDownloadButton": "tombol download", "AccessibilityDownloadButton": "tombol download",

View file

@ -1,6 +1,5 @@
{ {
"name": "polish", "name": "polish",
"code": "pl",
"substrings": { "substrings": {
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">daj mi znać</a>" "ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">daj mi znać</a>"
}, },
@ -9,7 +8,6 @@
"AboutSummary": "{appName} to najlepsze miejsce na pobieranie z mediów społecznościowych. zero reklam, trackerów i innych podobnych głupot. po prostu wklejasz link do udostępniania i lecisz!", "AboutSummary": "{appName} to najlepsze miejsce na pobieranie z mediów społecznościowych. zero reklam, trackerów i innych podobnych głupot. po prostu wklejasz link do udostępniania i lecisz!",
"AboutSupportedServices": "aktualnie wspierane strony:", "AboutSupportedServices": "aktualnie wspierane strony:",
"EmbedBriefDescription": "pobieraj rzeczy z social mediów bez reklam i trackerów", "EmbedBriefDescription": "pobieraj rzeczy z social mediów bez reklam i trackerów",
"MadeWithLove": "wykonane z <3 przez wukko i wszystkich współtwórców na github",
"AccessibilityInputArea": "pole wklejania linku", "AccessibilityInputArea": "pole wklejania linku",
"AccessibilityOpenAbout": "otwórz okno informacji", "AccessibilityOpenAbout": "otwórz okno informacji",
"AccessibilityDownloadButton": "przycisk pobierania", "AccessibilityDownloadButton": "przycisk pobierania",

View file

@ -1,17 +1,16 @@
{ {
"name": "русский", "name": "русский",
"code": "ru",
"substrings": { "substrings": {
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">напиши об этом</a>" "ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">напиши об этом мейнтейнеру</a>"
}, },
"strings": { "strings": {
"LinkInput": "вставь ссылку сюда", "LinkInput": "вставь ссылку сюда",
"AboutSummary": "{appName} — твой друг при скачивании контента из соц. сетей. никакой рекламы или трекеров. вставляешь ссылку и получаешь файл. ничего лишнего.", "AboutSummary": "{appName} — твой друг при скачивании контента из соц. сетей. никакой рекламы или трекеров. вставляешь ссылку и получаешь файл. ничего лишнего.",
"AboutSupportedServices": "что поддерживается:", "AboutSupportedServices": "что поддерживается:",
"EmbedBriefDescription": "сохраняй что хочешь, без мороки и вторжения в личное пространство", "EmbedBriefDescription": "сохраняй что хочешь, без мороки и вторжения в личное пространство",
"MadeWithLove": "сделано с <3 контрибьюторами на гитхабе и wukko", "MadeWithLove": "сделано с <3 wukko",
"AccessibilityInputArea": "зона вставки ссылки", "AccessibilityInputArea": "зона вставки ссылки",
"AccessibilityOpenAbout": "открыть окно с информацией", "AccessibilityOpenAbout": "открыть окно с инфой",
"AccessibilityDownloadButton": "кнопка скачивания", "AccessibilityDownloadButton": "кнопка скачивания",
"AccessibilityOpenSettings": "открыть настройки", "AccessibilityOpenSettings": "открыть настройки",
"AccessibilityOpenChangelog": "просмотреть последние изменения (на английском)", "AccessibilityOpenChangelog": "просмотреть последние изменения (на английском)",
@ -27,18 +26,16 @@
"ErrorUnsupported": "с твоей ссылкой что-то не так, или же этот сервис ещё не поддерживается. может быть, ты вставил не ту ссылку?", "ErrorUnsupported": "с твоей ссылкой что-то не так, или же этот сервис ещё не поддерживается. может быть, ты вставил не ту ссылку?",
"ErrorBrokenLink": "{s} поддерживается, но с твоей ссылкой что-то не так. может быть, ты её не полностью скопировал?", "ErrorBrokenLink": "{s} поддерживается, но с твоей ссылкой что-то не так. может быть, ты её не полностью скопировал?",
"ErrorNoLink": "я не гадалка и не могу угадывать, что ты хочешь скачать. попробуй в следующий раз вставить ссылку.", "ErrorNoLink": "я не гадалка и не могу угадывать, что ты хочешь скачать. попробуй в следующий раз вставить ссылку.",
"ErrorPageRenderFail": "что-то пошло не так, поэтому у меня не получилось срендерить страницу. если это что-то критичное, пожалуйста, {ContactLink}. приложи хэш текущего коммита ({s}) с действиями для получения ошибки. можно на русском языке. спасибо :)", "ErrorPageRenderFail": "что-то пошло не так, поэтому у меня не получилось срендерить страницу. если это повторится ещё раз, пожалуйста, {ContactLink}. также приложи хэш текущего коммита ({s}) с действиями для повторения этой ошибки. можно на русском языке. спасибо :)",
"ErrorRateLimit": "ты делаешь слишком много запросов. успокойся и попробуй ещё раз через несколько минут.", "ErrorRateLimit": "ты делаешь слишком много запросов. успокойся и попробуй ещё раз через несколько минут.",
"ErrorCouldntFetch": "мне не удалось получить информацию о твоей ссылке. проверь её и попробуй ещё раз.", "ErrorCouldntFetch": "мне не удалось получить инфу о твоей ссылке. проверь её и попробуй ещё раз.",
"ErrorLengthLimit": "твоё видео длиннее чем {s} минут(ы). это превышает текущий лимит. скачай что-нибудь покороче, а не экранизацию \"войны и мира\".", "ErrorLengthLimit": "твоё видео длиннее чем {s} минут(ы). это превышает текущий лимит. скачай что-нибудь покороче, а не экранизацию \"войны и мира\".",
"ErrorBadFetch": "произошла ошибки при получении информации о твоей ссылке. попробуй другой формат и разрешение. если не получится, то попробуй ещё раз чуть позже.", "ErrorBadFetch": "произошла ошибка при получении инфы о твоей ссылке. ты уверен, что ссылка работает? проверь её, и попробуй ещё раз.",
"ErrorCorruptedStream": "этот файл сломан на стороне {s}. ты можешь попробовать ещё раз, но если не получится, то попробуй другой формат и разрешение.", "ErrorCorruptedStream": "этот файл сломан на стороне {s}. ты можешь попробовать ещё раз, но если не получится, то попробуй другой формат и разрешение.",
"ErrorNoInternet": "кажется, нет подключения к интернету. возможно лежит сервер {appName}. в любом случае, проверь подключение к интернету и попробуй ещё раз.", "ErrorNoInternet": "кажется, нет подключения к интернету. возможно лежит сервер {appName}. в любом случае, проверь подключение к интернету и попробуй ещё раз.",
"ErrorCantConnectToServiceAPI": "у меня не получилось подключится к серверу {s}. возможно {s} лежит, или же мой ip добавили в чёрный список. попробуй ещё раз чуть позже.", "ErrorCantConnectToServiceAPI": "у меня не получилось подключиться к серверу {s}. скорее всего {s} лежит, или же ip адрес {appName} добавили в чёрный список. попробуй ещё раз чуть позже.",
"ErrorEmptyDownload": "я не нашёл того, что могу скачать. попробуй другую ссылку!", "ErrorEmptyDownload": "я не нашёл того, что могу скачать. попробуй другую ссылку!",
"ErrorLiveVideo": "я не могу скачать прямой эфир. дождись окончания трансляции и попробуй ещё раз.", "ErrorLiveVideo": "я не гадалка, и не умею заглядывать в будущее. дождись окончания прямого эфира и попробуй ещё раз чуть позже.",
"ErrorNoStreamID": "нет такого streamId.",
"ErrorNoType": "нет такого типа ответа от сервера.",
"SettingsAppearanceSubtitle": "внешний вид", "SettingsAppearanceSubtitle": "внешний вид",
"SettingsThemeSubtitle": "тема", "SettingsThemeSubtitle": "тема",
"SettingsFormatSubtitle": "формат загрузок", "SettingsFormatSubtitle": "формат загрузок",
@ -55,7 +52,7 @@
"SettingsQualitySwitchLowest": "худшее", "SettingsQualitySwitchLowest": "худшее",
"SettingsKeepDownloadButton": "оставлять &gt;&gt; на экране", "SettingsKeepDownloadButton": "оставлять &gt;&gt; на экране",
"AccessibilityKeepDownloadButton": "оставлять кнопку скачивания на экране", "AccessibilityKeepDownloadButton": "оставлять кнопку скачивания на экране",
"SettingsEnableDownloadPopup": "спрашивать, как сохранять", "SettingsEnableDownloadPopup": "спрашивать, что делать при скачивании",
"AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками", "AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками",
"SettingsFormatDescription": "выбирай webm, если хочешь максимальное качество. webm обычно лучше по качеству, но устройства на ios не могут проигрывать их без сторонних приложений.", "SettingsFormatDescription": "выбирай webm, если хочешь максимальное качество. webm обычно лучше по качеству, но устройства на ios не могут проигрывать их без сторонних приложений.",
"SettingsQualityDescription": "если выбранное разрешение недоступно, то выбирается ближайшее к нему. если ты хочешь твитнуть загруженное видео, то выбирай комбинацию из mp4 и 720p. твиттер такие видео обычно воспринимает намного лучше.", "SettingsQualityDescription": "если выбранное разрешение недоступно, то выбирается ближайшее к нему. если ты хочешь твитнуть загруженное видео, то выбирай комбинацию из mp4 и 720p. твиттер такие видео обычно воспринимает намного лучше.",
@ -89,9 +86,16 @@
"ErrorPopupCloseButton": "ясно", "ErrorPopupCloseButton": "ясно",
"ModeToggle": "режим", "ModeToggle": "режим",
"ModeToggleSmart": "умный", "ModeToggleSmart": "умный",
"PressToChange": "нажми, чтобы изменить", "ErrorLengthAudioConvert": "я не могу конвертировать аудио дольше чем {s} минут(ы). выбери \"лучший\" формат аудио, чтобы избежать ограничения.",
"ErrorLengthAudioConvert": "я не могу конвертировать аудио дольше чем {s} минут(ы). выбери \"лучший\" формат аудио, чтобы скачать аудио такой продолжительности.",
"SettingsAudioFullTikTok": "скачивать полное аудио", "SettingsAudioFullTikTok": "скачивать полное аудио",
"SettingsAudioFullTikTokDescription": "обычно такое аудио - оригинальный звук или песня, которое используется в видео. то есть, это аудио без обрезаний, голоса за кадром, и чего-либо подобного." "SettingsAudioFullTikTokDescription": "скачивает оригинальный звук, который использован в видео, без каких-либо изменений от автора видео.",
"ErrorCantGetID": "у меня не получилось достать инфу по этой короткой ссылке. попробуй полную ссылку, или же попробуй позже.",
"ErrorNoVideosInTweet": "у этого твита нет ни видео, ни гифок. попробуй другой!",
"ImagePickerTitle": "выбери картинки для скачивания",
"ImagePickerDownloadAudio": "скачать аудио",
"ImagePickerExplanationPC": "нажми правой кнопкой мыши на изображение, чтобы его сохранить.",
"ImagePickerExplanationPhone": "зажми и удерживай изображение, чтобы его сохранить.",
"ErrorNoUrlReturned": "я не получил ссылку для скачивания от сервера. такого происходить не должно. перезагрузи страницу, а если не поможет, то {ContactLink}.",
"ErrorUnknownStatus": "сервер ответил мне чем-то непонятным. такого происходить не должно. перезагрузи страницу, а если не поможет, то {ContactLink}."
} }
} }

View file

@ -1,6 +1,5 @@
{ {
"name": "українська", "name": "українська",
"code": "uk",
"substrings": { "substrings": {
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">дай нам знати</a>" "ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">дай нам знати</a>"
}, },
@ -9,7 +8,6 @@
"AboutSummary": "{appName} твій помічник із завантаження контенту з соцмереж. ніякої реклами, ніяких трекерів. вставляєш лінк, отримаєш файл, і допиваєш у спокої свій смузі.", "AboutSummary": "{appName} твій помічник із завантаження контенту з соцмереж. ніякої реклами, ніяких трекерів. вставляєш лінк, отримаєш файл, і допиваєш у спокої свій смузі.",
"AboutSupportedServices": "ось що підтримується:", "AboutSupportedServices": "ось що підтримується:",
"EmbedBriefDescription": "зберігай контент із соцмереж без реклами і трекерів", "EmbedBriefDescription": "зберігай контент із соцмереж без реклами і трекерів",
"MadeWithLove": "зроблено з <3 контриб'юторами на github и wukko <3",
"AccessibilityInputArea": "строка, щоб вставити в неї лінк", "AccessibilityInputArea": "строка, щоб вставити в неї лінк",
"AccessibilityOpenAbout": "відкрити інфу про {appName}", "AccessibilityOpenAbout": "відкрити інфу про {appName}",
"AccessibilityDownloadButton": "кнопка завантаження", "AccessibilityDownloadButton": "кнопка завантаження",

View file

@ -5,6 +5,7 @@ import loadJson from "../modules/sub/loadJSON.js";
const locPath = './src/localization/languages' const locPath = './src/localization/languages'
let loc = {} let loc = {}
let changelog = loadJson('./src/localization/changelog.json')
export function loadLoc() { export function loadLoc() {
fs.readdir(locPath, (err, files) => { fs.readdir(locPath, (err, files) => {
@ -13,7 +14,7 @@ export function loadLoc() {
}); });
}) })
} }
await loadLoc(); loadLoc();
export function replaceBase(s) { export function replaceBase(s) {
return s.replace(/\n/g, '<br/>').replace(/{appName}/g, appName).replace(/{repo}/g, repo) return s.replace(/\n/g, '<br/>').replace(/{appName}/g, appName).replace(/{repo}/g, repo)
} }
@ -29,6 +30,7 @@ export function replaceAll(lang, str, string, replacement) {
} }
export default function(lang, string, replacement) { export default function(lang, string, replacement) {
try { try {
if (lang === "changelog") return replaceBase(changelog[string]);
if (!Object.keys(loc).includes(lang)) lang = 'en'; if (!Object.keys(loc).includes(lang)) lang = 'en';
let str = loc[lang]["strings"]; let str = loc[lang]["strings"];
if (str && str[string]) { if (str && str[string]) {

View file

@ -9,7 +9,6 @@ import twitter from "./services/twitter.js";
import youtube from "./services/youtube.js"; import youtube from "./services/youtube.js";
import vk from "./services/vk.js"; import vk from "./services/vk.js";
import tiktok from "./services/tiktok.js"; import tiktok from "./services/tiktok.js";
import douyin from "./services/douyin.js";
import tumblr from "./services/tumblr.js"; import tumblr from "./services/tumblr.js";
import matchActionDecider from "./sub/matchActionDecider.js"; import matchActionDecider from "./sub/matchActionDecider.js";
import vimeo from "./services/vimeo.js"; import vimeo from "./services/vimeo.js";
@ -68,16 +67,10 @@ export default async function (host, patternMatch, url, lang, obj) {
title: patternMatch["title"], lang: lang, title: patternMatch["title"], lang: lang,
}); });
break; break;
case "douyin":
case "tiktok": case "tiktok":
r = await tiktok({ r = await tiktok({
postId: patternMatch["postId"], host: host,
id: patternMatch["id"], lang: lang,
noWatermark: obj.noWatermark, fullAudio: obj.fullAudio,
isAudioOnly: obj.isAudioOnly
});
break;
case "douyin":
r = await douyin({
postId: patternMatch["postId"], postId: patternMatch["postId"],
id: patternMatch["id"], lang: lang, id: patternMatch["id"], lang: lang,
noWatermark: obj.noWatermark, fullAudio: obj.fullAudio, noWatermark: obj.noWatermark, fullAudio: obj.fullAudio,

View file

@ -67,7 +67,6 @@ export function multiPagePopup(obj) {
tabs += `<button id="close-bottom" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>x</button>` tabs += `<button id="close-bottom" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>x</button>`
return ` return `
<div id="popup-${obj.name}" class="popup center box scrollable" style="visibility: hidden;"> <div id="popup-${obj.name}" class="popup center box scrollable" style="visibility: hidden;">
<div id="popup-content">${obj.header ? `<div id="popup-header" class="popup-header"> <div id="popup-content">${obj.header ? `<div id="popup-header" class="popup-header">
${obj.header.aboveTitle ? `<a id="popup-above-title" target="_blank" href="${obj.header.aboveTitle.url}">${obj.header.aboveTitle.text}</a>` : ''} ${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.title ? `<div id="popup-title">${obj.header.title}</div>` : ''}
@ -76,6 +75,22 @@ export function multiPagePopup(obj) {
</div>` </div>`
} }
export function popupWithBottomButtons(obj) {
let tabs = ``
for (let i = 0; i < obj.buttons.length; i++) {
tabs += obj.buttons[i]
}
tabs += `<button id="close-bottom" class="switch tab-${obj.name}" onclick="popup('${obj.name}', 0)" ${obj.closeAria ? `aria-label="${obj.closeAria}"` : ''}>x</button>`
return `
<div id="popup-${obj.name}" class="popup center box scrollable" style="visibility: hidden;">
<div id="popup-content">${obj.header ? `<div id="popup-header" class="popup-header">
${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.subtitle ? `<div id="popup-subtitle">${obj.header.subtitle}</div>` : ''}
${obj.header.explanation ? `<div class="explanation">${obj.header.explanation}</div>` : ''}</div>` : ''}${obj.content}</div>
<div id="popup-buttons" class="switches popup-tabs">${tabs}</div>
</div>`
}
export function backdropLink(link, text) { export function backdropLink(link, text) {
return `<a class="text-backdrop" href="${link}" target="_blank">${text}</a>` return `<a class="text-backdrop" href="${link}" target="_blank">${text}</a>`
} }

View file

@ -1,4 +1,4 @@
import { backdropLink, checkbox, footerButtons, multiPagePopup, popup, settingsCategory, switcher } from "./elements.js"; import { backdropLink, checkbox, footerButtons, multiPagePopup, popup, popupWithBottomButtons, settingsCategory, switcher } from "./elements.js";
import { services, appName, authorInfo, version, quality, repo, donations, supportedAudio } from "../config.js"; import { services, appName, authorInfo, version, quality, repo, donations, supportedAudio } from "../config.js";
import { getCommitInfo } from "../sub/currentCommit.js"; import { getCommitInfo } from "../sub/currentCommit.js";
import loc from "../../localization/manager.js"; import loc from "../../localization/manager.js";
@ -29,7 +29,9 @@ for (let i in donations["crypto"]) {
} }
export default function(obj) { export default function(obj) {
audioFormats[0]["text"] = loc(obj.lang, 'SettingsAudioFormatBest') audioFormats[0]["text"] = loc(obj.lang, 'SettingsAudioFormatBest')
let isIOS = obj.useragent.toLowerCase().match("iphone os") let ua = obj.useragent.toLowerCase()
let isIOS = ua.match("iphone os")
let isMobile = ua.match("android") || ua.match("iphone os")
try { try {
return `<!DOCTYPE html> return `<!DOCTYPE html>
<html lang="en"> <html lang="en">
@ -98,13 +100,13 @@ export default function(obj) {
text: `<div class="category-title">${loc(obj.lang, 'ChangelogLastMajor')}</div>`, text: `<div class="category-title">${loc(obj.lang, 'ChangelogLastMajor')}</div>`,
raw: true raw: true
}, { }, {
text: loc('en', 'ChangelogContentTitle'), text: loc('changelog', 'ContentTitle'),
classes: ["changelog-subtitle"], classes: ["changelog-subtitle"],
nopadding: true nopadding: true
}, { }, {
text: loc('en', 'FollowTwitter') text: loc('changelog', 'FollowTwitter')
}, { }, {
text: loc('en', 'ChangelogContent') text: loc('changelog', 'Content')
}, { }, {
text: `<div class="category-title">${loc(obj.lang, 'ChangelogLastCommit')}</div>`, text: `<div class="category-title">${loc(obj.lang, 'ChangelogLastCommit')}</div>`,
raw: true raw: true
@ -255,6 +257,17 @@ export default function(obj) {
<div id="pd-copy" class="switch full">${loc(obj.lang, 'CopyURL')}</div>` <div id="pd-copy" class="switch full">${loc(obj.lang, 'CopyURL')}</div>`
}) })
})} })}
${popupWithBottomButtons({
name: "imagePicker",
closeAria: loc(obj.lang, 'AccessibilityClosePopup'),
header: {
title: loc(obj.lang, 'ImagePickerTitle'),
explanation: isMobile ? loc(obj.lang, 'ImagePickerExplanationPhone') : loc(obj.lang, 'ImagePickerExplanationPC')
},
buttons: [`<a id="imagepicker-download" class="switch" target="_blank" href="/">${loc(obj.lang, 'ImagePickerDownloadAudio')}</a>`],
content: '<div id="imagepicker-holder"></div>'
})}
${popup({ ${popup({
name: "error", name: "error",
standalone: true, standalone: true,
@ -297,11 +310,12 @@ export default function(obj) {
</footer> </footer>
</body> </body>
<script type="text/javascript">const loc = { <script type="text/javascript">const loc = {
noInternet: "${loc(obj.lang, 'ErrorNoInternet')}", noInternet: ` + "`" + loc(obj.lang, 'ErrorNoInternet') + "`" + `,
noURLReturned: "${loc(obj.lang, 'ErrorBadFetch')}", noURLReturned: ` + "`" + loc(obj.lang, 'ErrorNoUrlReturned') + "`" + `,
unknownStatus: ` + "`" + loc(obj.lang, 'ErrorUnknownStatus') + "`" + `,
toggleDefault: '${emoji("✨")} ${loc(obj.lang, "ModeToggleSmart")} ${loc(obj.lang, "ModeToggle")}', toggleDefault: '${emoji("✨")} ${loc(obj.lang, "ModeToggleSmart")} ${loc(obj.lang, "ModeToggle")}',
toggleAudio: '${emoji("🎶")} ${loc(obj.lang, "SettingsAudioTab")} ${loc(obj.lang, "ModeToggle")}', toggleAudio: '${emoji("🎶")} ${loc(obj.lang, "SettingsAudioTab")} ${loc(obj.lang, "ModeToggle")}',
pressToChange: '<div class="tooltip">▼ ${loc(obj.lang, 'PressToChange')}</div>' pressToChange: '<div class="tooltip">▼ ${loc(obj.lang, 'AccessibilityModeToggle')}</div>'
};</script> };</script>
<script type="text/javascript" src="cobalt.js"></script> <script type="text/javascript" src="cobalt.js"></script>
</html>`; </html>`;

View file

@ -1,67 +0,0 @@
import got from "got";
import loc from "../../localization/manager.js";
import { genericUserAgent } from "../config.js";
export default async function(obj) {
try {
if (!obj.postId) {
let html = await got.get(`https://v.douyin.com/${obj.id}/`, { headers: { "user-agent": genericUserAgent } });
html.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'tiktok') };
});
if (html.body.includes('<html><head><meta charset="UTF-8" />')) {
obj.postId = html.url.split('video/')[1].split('?')[0]
} else {
obj.postId = html.body.split('video/')[1].split('/?')[0]
}
}
let iteminfo = await got.get(`https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=${obj.postId}`, {
headers: {
'authority': 'www.iesdouyin.com',
'user-agent': genericUserAgent,
'content-type': 'application/x-www-form-urlencoded',
'accept': '*/*',
'referer': `https://www.iesdouyin.com/share/video/${obj.postId}/?region=CN&u_code=15b9142gf&titleType=title&utm_source=copy_link&utm_campaign=client_share&utm_medium=android&app=aweme`,
'accept-language': 'zh-CN,zh;q=0.9,en-GB;q=0.8,en;q=0.7'
}
});
iteminfo.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'douyin') };
});
iteminfo = JSON.parse(iteminfo.body);
let video = iteminfo['item_list'][0]['video']['play_addr']['url_list'][0];
let audio = obj.isAudioOnly ? iteminfo['item_list'][0]["music"]["play_url"]["url_list"][0] : false;
if (audio && obj.fullAudio) {
return {
urls: audio,
audioFilename: `douyin_${obj.postId}_audio_full`,
isAudio: true
}
} else if (audio && audio.slice(-4) == ".mp3") {
return {
urls: audio,
audioFilename: `douyin_${obj.postId}_audio`,
isAudio: true,
isMp3: true,
};
} else if (video) {
if (!obj.noWatermark) {
return {
urls: video,
audioFilename: `douyin_${obj.postId}_audio`,
filename: `douyin_${obj.postId}.mp4`
};
} else {
return {
urls: video.replace("playwm", "play"),
audioFilename: `douyin_${obj.postId}_audio`,
filename: `douyin_${obj.postId}_nw.mp4`
};
}
} else {
return { error: loc(obj.lang, 'ErrorEmptyDownload') };
}
} catch (e) {
return { error: loc(obj.lang, 'ErrorBadFetch') };
}
}

View file

@ -36,7 +36,6 @@ export default async function(obj) {
} else return { error: loc(obj.lang, 'ErrorEmptyDownload') } } else return { error: loc(obj.lang, 'ErrorEmptyDownload') }
} else return { error: loc(obj.lang, 'ErrorBrokenLink', 'soundcloud') } } else return { error: loc(obj.lang, 'ErrorBrokenLink', 'soundcloud') }
} catch (e) { } catch (e) {
console.log(e)
return { error: loc(obj.lang, 'ErrorBadFetch') }; return { error: loc(obj.lang, 'ErrorBadFetch') };
} }
} }

View file

@ -4,66 +4,102 @@ import { genericUserAgent } from "../config.js";
import { unicodeDecode } from "../sub/utils.js"; import { unicodeDecode } from "../sub/utils.js";
let userAgent = genericUserAgent.split(' Chrome/1')[0] let userAgent = genericUserAgent.split(' Chrome/1')[0]
let config = {
tiktok: {
short: "https://vt.tiktok.com/",
api: "https://api.tiktokv.com/aweme/v1/aweme/detail/?aweme_id=",
},
douyin: {
short: "https://v.douyin.com/",
api: "https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=",
}
}
function selector(j, h) {
switch (h) {
case "tiktok":
return j["aweme_detail"]
case "douyin":
return j['item_list'][0]
}
}
export default async function(obj) { export default async function(obj) {
try { try {
if (!obj.postId) { if (!obj.postId) {
let html = await got.get(`https://vt.tiktok.com/${obj.id}`, { headers: { "user-agent": userAgent } }); let html = await got.get(`${config[obj.host]["short"]}${obj.id}`, { followRedirect: false, headers: { "user-agent": userAgent } });
html.on('error', (err) => { html.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'tiktok') }; return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', obj.host) };
}); });
html = html.body html = html.body;
if (!html.includes('<!DOCTYPE html>')) { if (html.slice(0, 17) === '<a href="https://' && html.includes('/video/')) obj.postId = html.split('video/')[1].split('?')[0].replace("/", '')
obj.postId = html.split('video/')[1].split('?')[0] }
if (!obj.postId) return { error: loc(obj.lang, 'ErrorCantGetID') };
let detail = await got.get(`${config[obj.host]["api"]}${obj.postId}`);
detail.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', obj.host) };
});
detail = selector(JSON.parse(detail.body), obj.host);
let video, videoFilename, audioFilename, isMp3, audio,
images = detail["image_post_info"] ? detail["image_post_info"]["images"] : false,
filenameBase = `${obj.host}_${obj.postId}`;
if (!obj.isAudioOnly && !images) {
if (obj.host == "tiktok") {
video = detail["video"]["play_addr"]["url_list"][0]
} else { } else {
obj.postId = html.split('aweme/detail/')[1].split('?')[0] video = detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play")
} }
} videoFilename = `${filenameBase}_video_nw.mp4` // nw - no watermark
if (!obj.noWatermark && !obj.isAudioOnly) { if (!obj.noWatermark) {
if (obj.host == "tiktok") {
let html = await got.get(`https://tiktok.com/@video/video/${obj.postId}`, { headers: { "user-agent": userAgent } }); let html = await got.get(`https://tiktok.com/@video/video/${obj.postId}`, { headers: { "user-agent": userAgent } });
html.on('error', (err) => { html.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'tiktok') }; return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', obj.host) };
}); });
html = html.body; html = html.body;
if (html.includes(',"preloadList":[{"url":"')) { if (html.includes(',"preloadList":[{"url":"')) {
return { video = unicodeDecode(html.split(',"preloadList":[{"url":"')[1].split('","id":"')[0].trim())
urls: unicodeDecode(html.split(',"preloadList":[{"url":"')[1].split('","id":"')[0].trim()),
audioFilename: `tiktok_${obj.postId}_audio`,
filename: `tiktok_${obj.postId}.mp4`
};
} else {
return { error: loc(obj.lang, 'ErrorEmptyDownload') };
} }
} else { } else {
let detail = await got.get(`https://api.tiktokv.com/aweme/v1/aweme/detail/?aweme_id=${obj.postId}`); video = detail['video']['play_addr']['url_list'][0]
detail.on('error', (err) => {
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'tiktok') };
});
detail = JSON.parse(detail.body);
let video = detail["aweme_detail"]["video"]["play_addr"]["url_list"][0];
let audio = obj.isAudioOnly ? detail["aweme_detail"]["music"]["play_url"]["url_list"][0] : false;
if (audio && obj.fullAudio) {
return {
urls: audio,
audioFilename: `tiktok_${obj.postId}_audio_full`,
isAudio: true
} }
} else if (audio && audio.slice(-4) == ".mp3") { videoFilename = `${filenameBase}_video.mp4`
return { }
urls: audio, } else {
audioFilename: `tiktok_${obj.postId}_audio`, let fallback = obj.host == "douyin" ? detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play") : detail["video"]["play_addr"]["url_list"][0];
isAudio: true, if (obj.fullAudio || fallback.includes("music")) {
isMp3: true, audio = detail["music"]["play_url"]["url_list"][0]
}; audioFilename = `${filenameBase}_audio`
} else if (video) { } else {
return { audio = fallback
audioFilename = `${filenameBase}_audio_fv` // fv - from video
}
if (audio.slice(-4) == ".mp3") isMp3 = true;
}
if (video) return {
urls: video, urls: video,
audioFilename: `tiktok_${obj.postId}_audio`, filename: videoFilename
filename: `tiktok_${obj.postId}_nw.mp4`
};
} else {
return { error: loc(obj.lang, 'ErrorEmptyDownload') };
} }
if (images) {
let imageLinks = [];
for (let i in images) {
imageLinks.push(images[i]["display_image"]["url_list"][0])
}
return {
images: imageLinks,
urls: audio,
audioFilename: audioFilename,
isAudioOnly: true,
isMp3: isMp3,
}
}
if (audio) return {
urls: audio,
audioFilename: audioFilename,
isAudioOnly: true,
isMp3: isMp3,
} }
} catch (e) { } catch (e) {
return { error: loc(obj.lang, 'ErrorBadFetch') }; return { error: loc(obj.lang, 'ErrorBadFetch') };

View file

@ -1,52 +1,38 @@
import got from "got"; import got from "got";
import loc from "../../localization/manager.js"; import loc from "../../localization/manager.js";
import { services } from "../config.js";
const configSt = services.twitter; export default async function(obj) {
async function fetchTweetInfo(obj) {
let cantConnect = { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'twitter') }
try { try {
let _headers = { let _headers = {
"Authorization": `Bearer ${configSt.token}`, "Authorization": "Bearer AAAAAAAAAAAAAAAAAAAAAIK1zgAAAAAA2tUWuhGZ2JceoId5GwYWU5GspY4%3DUq7gzFoCZs1QfwGoVdvSac3IniczZEYXIcDyumCauIXpcAPorE",
"Host": configSt.api, "Host": "api.twitter.com",
"Content-Type": "application/json", "Content-Type": "application/json",
"Content-Length": 0 "Content-Length": 0
}; };
let req_act = await got.post(`https://${configSt.api}/${configSt.apiURLs.activate}`, { let req_act = await got.post("https://api.twitter.com/1.1/guest/activate.json", {
headers: _headers headers: _headers
}); });
req_act.on('error', (err) => { req_act.on('error', (err) => {
return cantConnect return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'twitter') }
}) })
_headers["x-guest-token"] = req_act.body["guest_token"]; _headers["x-guest-token"] = req_act.body["guest_token"];
let req_status = await got.get(`https://${configSt.api}/${configSt.apiURLs.status_show}?id=${obj.id}&tweet_mode=extended`, { let req_status = await got.get(`https://api.twitter.com/1.1/statuses/show.json?id=${obj.id}&tweet_mode=extended`, {
headers: _headers headers: _headers
}); });
req_status.on('error', (err) => { req_status.on('error', (err) => {
return cantConnect return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'twitter') }
}) })
return JSON.parse(req_status.body); let parsbod = JSON.parse(req_status.body);
} catch (err) {
return cantConnect;
}
}
export default async function(obj) {
let nothing = { error: loc(obj.lang, 'ErrorEmptyDownload') }
try {
let parsbod = await fetchTweetInfo(obj);
if (!parsbod.error) {
if (parsbod.hasOwnProperty("extended_entities") && parsbod["extended_entities"].hasOwnProperty("media")) { if (parsbod.hasOwnProperty("extended_entities") && parsbod["extended_entities"].hasOwnProperty("media")) {
if (parsbod["extended_entities"]["media"][0]["type"] === "video" || parsbod["extended_entities"]["media"][0]["type"] === "animated_gif") { let media = parsbod["extended_entities"]["media"][0]
let variants = parsbod["extended_entities"]["media"][0]["video_info"]["variants"] if (media["type"] === "video" || media["type"] === "animated_gif") {
return { urls: variants.filter((v) => { if (v["content_type"] == "video/mp4") return true; }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"].split('?')[0], audioFilename: `twitter_${obj.id}_audio` } return { urls: media["video_info"]["variants"].filter((v) => { if (v["content_type"] == "video/mp4") return true; }).sort((a, b) => Number(b.bitrate) - Number(a.bitrate))[0]["url"].split('?')[0], audioFilename: `twitter_${obj.id}_audio` }
} else { } else {
return nothing return { error: loc(obj.lang, 'ErrorNoVideosInTweet') }
} }
} else { } else {
return nothing return { error: loc(obj.lang, 'ErrorNoVideosInTweet') }
} }
} else return parsbod;
} catch (err) { } catch (err) {
return { error: loc(obj.lang, 'ErrorBadFetch') }; return { error: loc(obj.lang, 'ErrorBadFetch') };
} }

View file

@ -13,14 +13,7 @@
}, },
"twitter": { "twitter": {
"patterns": [":user/status/:id"], "patterns": [":user/status/:id"],
"quality_match": ["1080", "720", "480", "360", "240", "144"], "enabled": true
"enabled": true,
"api": "api.twitter.com",
"token": "AAAAAAAAAAAAAAAAAAAAAIK1zgAAAAAA2tUWuhGZ2JceoId5GwYWU5GspY4%3DUq7gzFoCZs1QfwGoVdvSac3IniczZEYXIcDyumCauIXpcAPorE",
"apiURLs": {
"activate": "1.1/guest/activate.json",
"status_show": "1.1/statuses/show.json"
}
}, },
"vk": { "vk": {
"patterns": ["video-:userId_:videoId"], "patterns": ["video-:userId_:videoId"],

View file

@ -1,9 +1,9 @@
import { audioIgnore, services, supportedAudio } from "../config.js" import { audioIgnore, services, supportedAudio } from "../config.js"
import { apiJSON } from "./utils.js" import { apiJSON } from "./utils.js"
export default function(r, host, ip, audioFormat, isAudioOnly) { export default function(r, host, ip, audioFormat) {
if (!r.error) { if (!r.error) {
if (!isAudioOnly) { if (!r.isAudioOnly) {
switch (host) { switch (host) {
case "twitter": case "twitter":
return apiJSON(1, { u: r.urls }) return apiJSON(1, { u: r.urls })
@ -51,7 +51,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly) {
let copy = false; let copy = false;
if (!supportedAudio.includes(audioFormat)) audioFormat = "best"; if (!supportedAudio.includes(audioFormat)) audioFormat = "best";
if ((host == "tiktok" || host == "douyin") && r.isAudio && services.tiktok.audioFormats.includes(audioFormat)) { if ((host == "tiktok" || host == "douyin") && r.isAudioOnly && services.tiktok.audioFormats.includes(audioFormat)) {
if (r.isMp3) { if (r.isMp3) {
if (audioFormat == "mp3" || audioFormat == "best") { if (audioFormat == "mp3" || audioFormat == "best") {
audioFormat = "mp3" audioFormat = "mp3"
@ -70,12 +70,21 @@ export default function(r, host, ip, audioFormat, isAudioOnly) {
audioFormat = "m4a" audioFormat = "m4a"
copy = true copy = true
} }
if ((host == "tiktok" || host == "douyin") && r.images) {
return apiJSON(5, {
type: type,
images: r.images,
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
filename: r.audioFilename, salt: process.env.streamSalt, isAudioOnly: true, audioFormat: audioFormat, copy: copy
})
} else {
return apiJSON(2, { return apiJSON(2, {
type: type, type: type,
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip, u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
filename: r.audioFilename, salt: process.env.streamSalt, isAudioOnly: true, audioFormat: audioFormat, copy: copy filename: r.audioFilename, salt: process.env.streamSalt, isAudioOnly: true, audioFormat: audioFormat, copy: copy
}) })
} }
}
} else { } else {
return apiJSON(0, { t: r.error }); return apiJSON(0, { t: r.error });
} }

View file

@ -13,6 +13,8 @@ export function apiJSON(type, obj) {
return { status: 200, body: { status: "success", text: obj.t } }; return { status: 200, body: { status: "success", text: obj.t } };
case 4: case 4:
return { status: 429, body: { status: "rate-limit", text: obj.t } }; return { status: 429, body: { status: "rate-limit", text: obj.t } };
case 5:
return { status: 200, body: { status: "images", images: obj.images, url: createStream(obj) } };
default: default:
return { status: 400, body: { status: "error", text: "Bad Request" } }; return { status: 400, body: { status: "error", text: "Bad Request" } };
} }