tiktok images and crowdin
This commit is contained in:
parent
297c1ed116
commit
9ea832caf5
24 changed files with 313 additions and 269 deletions
61
README.md
61
README.md
|
@ -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")
|
||||
|
||||
## 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).
|
||||
|
||||
## Supported services
|
||||
- bilibili.com
|
||||
- douyin (with or without watermark, preference set by user)
|
||||
- Reddit
|
||||
- SoundCloud
|
||||
- TikTok (with or without watermark, preference set by user)
|
||||
- Tumblr
|
||||
- Twitter
|
||||
- Vimeo
|
||||
- VK
|
||||
- YouTube (with HDR support)
|
||||
- YouTube Music
|
||||
## Support status
|
||||
| Service | Video + Audio | Only audio | Additional features |
|
||||
| ------ | :-: | :-: | :----- |
|
||||
| Twitter | ✅ | ✅ | |
|
||||
| YouTube | ✅ | ✅ | Supports HDR and high FPS videos |
|
||||
| YouTube Music | ❌ | ✅ | |
|
||||
| Reddit | ✅ | ✅ | |
|
||||
| TikTok & douyin | ✅ | ✅ | Videos can be downloaded with or without watermark, preference is set by user. |
|
||||
| SoundCloud | ❌ | ✅ | |
|
||||
| bilibili.com | ✅ | ✅ | |
|
||||
| Tumblr | ✅ | ✅ | |
|
||||
| Vimeo | ✅ | ⚒️ | |
|
||||
| VK | ✅ | ⚒️ | |
|
||||
|
||||
## Translations
|
||||
- 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)
|
||||
- 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!
|
||||
|
||||
## How you can help cobalt speak your language
|
||||
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!
|
||||
## How to contribute translations
|
||||
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:
|
||||
- Do **NOT** use formal language, that's boring and lame. Use informal language on all occasions.
|
||||
- Strings are **ALWAYS** lowercase unless it's an internal value like {ContactLink} or STRESSED LIKE THIS.
|
||||
- Keep translations as friendly and fun as possible. Just as if cobalt user was your buddy.
|
||||
- Robotic translations from original language are not valid.
|
||||
- You can (and should) rephrase sentences as long as they keep the same sense, if you think it'd be better that way.
|
||||
### Translation guidelines:
|
||||
- Avoid formal language. Leave it for boring big tech companies. Use informal language on all occasions.
|
||||
- Strings are **ALWAYS** stylized as lowercase unless it's STRESSED LIKE THIS or is an internal value like `{ContactLink}`.
|
||||
- Keep translations lively, friendly, and fun. Translate strings as if cobalt user was your buddy.
|
||||
- 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 point, if you think it'd be better that way.
|
||||
- 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.
|
||||
- Always check if there are issues in UI with your localization.
|
||||
- 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 almost always kept exclusively in English. Remove that phrase if you do translate major update changelog.
|
||||
- Example: `"ChangelogLastCommit": "последний коммит (на английском)"`
|
||||
- Even though I love cursing, keep that to minimum in translations, and do **NOT** use any offensive words.
|
||||
- Check if there are issues in UI with your localization, and optimize it accordingly, or open an issue.
|
||||
- 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.
|
||||
- Example of translation to Russian: `"ChangelogLastCommit": "последний коммит (на английском)"`
|
||||
- Be nice.
|
||||
|
||||
## 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
|
||||
- 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.
|
||||
|
||||
## 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
|
||||
cobalt is under [AGPL-3.0](https://github.com/wukko/cobalt/blob/current/LICENSE).
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "cobalt",
|
||||
"description": "save what you love",
|
||||
"version": "3.3.6",
|
||||
"version": "3.4",
|
||||
"author": "wukko",
|
||||
"exports": "./src/cobalt.js",
|
||||
"type": "module",
|
||||
|
|
|
@ -83,12 +83,12 @@ if (fs.existsSync('./.env')) {
|
|||
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);
|
||||
} 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);
|
||||
}
|
||||
break;
|
||||
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);
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -127,6 +127,10 @@ button:active,
|
|||
cursor: pointer;
|
||||
transform: scale(0.95)
|
||||
}
|
||||
.imagepicker-image:active {
|
||||
cursor: pointer;
|
||||
transform: scale(0.95)
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -465,6 +469,30 @@ input[type="checkbox"] {
|
|||
.button:active .tooltip {
|
||||
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 */
|
||||
@media screen and (min-width: 2300px) {
|
||||
html {
|
||||
|
@ -553,6 +581,11 @@ input[type="checkbox"] {
|
|||
}
|
||||
}
|
||||
@media screen and (max-width: 949px) {
|
||||
.imagepicker-image-container {
|
||||
height: 7rem;
|
||||
width: 7rem;
|
||||
line-height: 7rem;
|
||||
}
|
||||
#close-error {
|
||||
bottom: 5%;
|
||||
position: absolute;
|
||||
|
@ -582,3 +615,13 @@ input[type="checkbox"] {
|
|||
padding-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 400px) {
|
||||
.popup-title {
|
||||
line-height: inherit;
|
||||
}
|
||||
.imagepicker-image-container {
|
||||
line-height: 6rem;
|
||||
height: 6rem;
|
||||
width: 6rem;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
let isIOS = navigator.userAgent.toLowerCase().match("iphone os");
|
||||
let version = 5;
|
||||
let version = 6;
|
||||
|
||||
let switchers = {
|
||||
"theme": ["auto", "light", "dark"],
|
||||
|
@ -101,6 +101,8 @@ function hideAllPopups() {
|
|||
for (let i = 0; i < filter.length; i++) {
|
||||
filter[i].style.visibility = "hidden";
|
||||
}
|
||||
eid("imagepicker-holder").innerHTML = '';
|
||||
eid("imagepicker-download").href = '/';
|
||||
eid("popup-backdrop").style.visibility = "hidden";
|
||||
}
|
||||
function popup(type, action, text) {
|
||||
|
@ -130,6 +132,21 @@ function popup(type, action, text) {
|
|||
}
|
||||
eid("popup-download").style.visibility = vis(action);
|
||||
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:
|
||||
eid(`popup-${type}`).style.visibility = vis(action);
|
||||
break;
|
||||
|
@ -238,13 +255,21 @@ async function download(url) {
|
|||
window.open(j.url, '_blank');
|
||||
}
|
||||
break;
|
||||
case "images":
|
||||
case "stream":
|
||||
changeDownloadButton(2, '?..')
|
||||
fetch(`${j.url}&p=1&origin=front`).then(async (res) => {
|
||||
let jp = await res.json();
|
||||
if (jp.status == "continue") {
|
||||
changeDownloadButton(2, '>>>')
|
||||
window.location.href = j.url
|
||||
if (j.status === "images") {
|
||||
popup('imagePicker', 1, {
|
||||
url: j.url,
|
||||
images: j.images
|
||||
})
|
||||
} else {
|
||||
window.location.href = j.url
|
||||
}
|
||||
setTimeout(() => {
|
||||
changeDownloadButton(1, '>>')
|
||||
eid("url-input-area").disabled = false
|
||||
|
@ -259,7 +284,7 @@ async function download(url) {
|
|||
default:
|
||||
eid("url-input-area").disabled = false
|
||||
changeDownloadButton(2, '!!')
|
||||
popup("error", 1, loc.noURLReturned);
|
||||
popup("error", 1, loc.unknownStatus);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
|
|
5
src/localization/changelog.json
Normal file
5
src/localization/changelog.json
Normal 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>"
|
||||
}
|
|
@ -1,19 +1,14 @@
|
|||
{
|
||||
"name": "english",
|
||||
"code": "en",
|
||||
"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": {
|
||||
"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",
|
||||
"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:",
|
||||
"EmbedBriefDescription": "save content from social media without creeps following you around",
|
||||
"MadeWithLove": "made with <3 by wukko and all contributors on github",
|
||||
"EmbedBriefDescription": "save content from social media without annoyances",
|
||||
"MadeWithLove": "made with <3 by wukko",
|
||||
"AccessibilityInputArea": "link input area",
|
||||
"AccessibilityOpenAbout": "open about popup",
|
||||
"AccessibilityDownloadButton": "download button",
|
||||
|
@ -31,18 +26,16 @@
|
|||
"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?",
|
||||
"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",
|
||||
"ErrorRateLimit": "you're making way too many requests. calm down and try again in a few minutes.",
|
||||
"ErrorCouldntFetch": "couldn't fetch metadata. 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!",
|
||||
"ErrorBadFetch": "something went wrong with info fetching. you can try a different format and resolution or just try again later.",
|
||||
"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 too many requests. calm down and try again in a bit.",
|
||||
"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. video that you tried to download is longer than {s} minutes. pick something else!",
|
||||
"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.",
|
||||
"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.",
|
||||
"ErrorEmptyDownload": "there's nothing to download. try something else!",
|
||||
"ErrorLiveVideo": "i can't download a live video. wait for stream to finish and try again.",
|
||||
"ErrorNoStreamID": "there's no such streamId.",
|
||||
"ErrorNoType": "there's no such expected response type.",
|
||||
"ErrorEmptyDownload": "i don't see anything i could download from here. try a different link.",
|
||||
"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!",
|
||||
"SettingsAppearanceSubtitle": "appearance",
|
||||
"SettingsThemeSubtitle": "theme",
|
||||
"SettingsFormatSubtitle": "download format",
|
||||
|
@ -61,15 +54,15 @@
|
|||
"AccessibilityKeepDownloadButton": "keep the download button always visible",
|
||||
"SettingsEnableDownloadPopup": "ask for a way to save",
|
||||
"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.",
|
||||
"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.",
|
||||
"LinkGitHubIssues": ">> report issues and check out the source code on github",
|
||||
"LinkGitHubChanges": ">> see previous changes and contribute on github",
|
||||
"LinkDonateContact": ">> 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.",
|
||||
"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.",
|
||||
"NoScriptMessage": "{appName} uses javascript for api requests and interactive interface. you have to allow javascript to use this site. i don't have any ads or trackers, pinky promise.",
|
||||
"DownloadPopupDescriptionIOS": "because you have an ios device, you have to press and hold the download button and then select \"download video\" in appeared popup to save the video. this will be required for as long as apple forces safari webview upon all browser developers on ios.",
|
||||
"DownloadPopupDescription": "download button opens a new tab with requested file. you can disable this popup in settings.",
|
||||
"DownloadPopupWayToSave": "pick a way to save",
|
||||
"ClickToCopy": "press to copy",
|
||||
|
@ -93,9 +86,16 @@
|
|||
"ErrorPopupCloseButton": "got it",
|
||||
"ModeToggle": "mode",
|
||||
"ModeToggleSmart": "smart",
|
||||
"PressToChange": "press to change",
|
||||
"ErrorLengthAudioConvert": "current length limit for audio conversion is {s} minutes. pick \"best\" format instead!",
|
||||
"ErrorLengthAudioConvert": "current length limit for audio conversion is {s} minutes. pick \"best\" format if you want to avoid limitations.",
|
||||
"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}."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"name": "español",
|
||||
"code": "es",
|
||||
"substrings": {
|
||||
"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!",
|
||||
"AboutSupportedServices": "servicios compatibles:",
|
||||
"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",
|
||||
"AccessibilityOpenAbout": "abrir ventana emergente de acerca de",
|
||||
"AccessibilityDownloadButton": "botón de descarga",
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"name": "français",
|
||||
"code": "fr",
|
||||
"substrings": {
|
||||
"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!",
|
||||
"AboutSupportedServices": "services supportés:",
|
||||
"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",
|
||||
"AccessibilityOpenAbout": "ouvrir la fenêtre contextuelle de la section à propos",
|
||||
"AccessibilityDownloadButton": "bouton télécharger",
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"name": "indonesia",
|
||||
"code": "id",
|
||||
"substrings": {
|
||||
"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",
|
||||
"AboutSupportedServices": "layanan yang didukung:",
|
||||
"EmbedBriefDescription": "simpan konten dari sosial media tanpa hal aneh mengikuti kamu",
|
||||
"MadeWithLove": "dibuat dengan <3",
|
||||
"AccessibilityInputArea": "tempat tempel link",
|
||||
"AccessibilityOpenAbout": "buka bagian pengantar",
|
||||
"AccessibilityDownloadButton": "tombol download",
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"name": "polish",
|
||||
"code": "pl",
|
||||
"substrings": {
|
||||
"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!",
|
||||
"AboutSupportedServices": "aktualnie wspierane strony:",
|
||||
"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",
|
||||
"AccessibilityOpenAbout": "otwórz okno informacji",
|
||||
"AccessibilityDownloadButton": "przycisk pobierania",
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
{
|
||||
"name": "русский",
|
||||
"code": "ru",
|
||||
"substrings": {
|
||||
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">напиши об этом</a>"
|
||||
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">напиши об этом мейнтейнеру</a>"
|
||||
},
|
||||
"strings": {
|
||||
"LinkInput": "вставь ссылку сюда",
|
||||
"AboutSummary": "{appName} — твой друг при скачивании контента из соц. сетей. никакой рекламы или трекеров. вставляешь ссылку и получаешь файл. ничего лишнего.",
|
||||
"AboutSupportedServices": "что поддерживается:",
|
||||
"EmbedBriefDescription": "сохраняй что хочешь, без мороки и вторжения в личное пространство",
|
||||
"MadeWithLove": "сделано с <3 контрибьюторами на гитхабе и wukko",
|
||||
"MadeWithLove": "сделано с <3 wukko",
|
||||
"AccessibilityInputArea": "зона вставки ссылки",
|
||||
"AccessibilityOpenAbout": "открыть окно с информацией",
|
||||
"AccessibilityOpenAbout": "открыть окно с инфой",
|
||||
"AccessibilityDownloadButton": "кнопка скачивания",
|
||||
"AccessibilityOpenSettings": "открыть настройки",
|
||||
"AccessibilityOpenChangelog": "просмотреть последние изменения (на английском)",
|
||||
|
@ -27,18 +26,16 @@
|
|||
"ErrorUnsupported": "с твоей ссылкой что-то не так, или же этот сервис ещё не поддерживается. может быть, ты вставил не ту ссылку?",
|
||||
"ErrorBrokenLink": "{s} поддерживается, но с твоей ссылкой что-то не так. может быть, ты её не полностью скопировал?",
|
||||
"ErrorNoLink": "я не гадалка и не могу угадывать, что ты хочешь скачать. попробуй в следующий раз вставить ссылку.",
|
||||
"ErrorPageRenderFail": "что-то пошло не так, поэтому у меня не получилось срендерить страницу. если это что-то критичное, пожалуйста, {ContactLink}. приложи хэш текущего коммита ({s}) с действиями для получения ошибки. можно на русском языке. спасибо :)",
|
||||
"ErrorPageRenderFail": "что-то пошло не так, поэтому у меня не получилось срендерить страницу. если это повторится ещё раз, пожалуйста, {ContactLink}. также приложи хэш текущего коммита ({s}) с действиями для повторения этой ошибки. можно на русском языке. спасибо :)",
|
||||
"ErrorRateLimit": "ты делаешь слишком много запросов. успокойся и попробуй ещё раз через несколько минут.",
|
||||
"ErrorCouldntFetch": "мне не удалось получить информацию о твоей ссылке. проверь её и попробуй ещё раз.",
|
||||
"ErrorCouldntFetch": "мне не удалось получить инфу о твоей ссылке. проверь её и попробуй ещё раз.",
|
||||
"ErrorLengthLimit": "твоё видео длиннее чем {s} минут(ы). это превышает текущий лимит. скачай что-нибудь покороче, а не экранизацию \"войны и мира\".",
|
||||
"ErrorBadFetch": "произошла ошибки при получении информации о твоей ссылке. попробуй другой формат и разрешение. если не получится, то попробуй ещё раз чуть позже.",
|
||||
"ErrorBadFetch": "произошла ошибка при получении инфы о твоей ссылке. ты уверен, что ссылка работает? проверь её, и попробуй ещё раз.",
|
||||
"ErrorCorruptedStream": "этот файл сломан на стороне {s}. ты можешь попробовать ещё раз, но если не получится, то попробуй другой формат и разрешение.",
|
||||
"ErrorNoInternet": "кажется, нет подключения к интернету. возможно лежит сервер {appName}. в любом случае, проверь подключение к интернету и попробуй ещё раз.",
|
||||
"ErrorCantConnectToServiceAPI": "у меня не получилось подключится к серверу {s}. возможно {s} лежит, или же мой ip добавили в чёрный список. попробуй ещё раз чуть позже.",
|
||||
"ErrorCantConnectToServiceAPI": "у меня не получилось подключиться к серверу {s}. скорее всего {s} лежит, или же ip адрес {appName} добавили в чёрный список. попробуй ещё раз чуть позже.",
|
||||
"ErrorEmptyDownload": "я не нашёл того, что могу скачать. попробуй другую ссылку!",
|
||||
"ErrorLiveVideo": "я не могу скачать прямой эфир. дождись окончания трансляции и попробуй ещё раз.",
|
||||
"ErrorNoStreamID": "нет такого streamId.",
|
||||
"ErrorNoType": "нет такого типа ответа от сервера.",
|
||||
"ErrorLiveVideo": "я не гадалка, и не умею заглядывать в будущее. дождись окончания прямого эфира и попробуй ещё раз чуть позже.",
|
||||
"SettingsAppearanceSubtitle": "внешний вид",
|
||||
"SettingsThemeSubtitle": "тема",
|
||||
"SettingsFormatSubtitle": "формат загрузок",
|
||||
|
@ -55,7 +52,7 @@
|
|||
"SettingsQualitySwitchLowest": "худшее",
|
||||
"SettingsKeepDownloadButton": "оставлять >> на экране",
|
||||
"AccessibilityKeepDownloadButton": "оставлять кнопку скачивания на экране",
|
||||
"SettingsEnableDownloadPopup": "спрашивать, как сохранять",
|
||||
"SettingsEnableDownloadPopup": "спрашивать, что делать при скачивании",
|
||||
"AccessibilityEnableDownloadPopup": "спрашивать, что делать с загрузками",
|
||||
"SettingsFormatDescription": "выбирай webm, если хочешь максимальное качество. webm обычно лучше по качеству, но устройства на ios не могут проигрывать их без сторонних приложений.",
|
||||
"SettingsQualityDescription": "если выбранное разрешение недоступно, то выбирается ближайшее к нему. если ты хочешь твитнуть загруженное видео, то выбирай комбинацию из mp4 и 720p. твиттер такие видео обычно воспринимает намного лучше.",
|
||||
|
@ -89,9 +86,16 @@
|
|||
"ErrorPopupCloseButton": "ясно",
|
||||
"ModeToggle": "режим",
|
||||
"ModeToggleSmart": "умный",
|
||||
"PressToChange": "нажми, чтобы изменить",
|
||||
"ErrorLengthAudioConvert": "я не могу конвертировать аудио дольше чем {s} минут(ы). выбери \"лучший\" формат аудио, чтобы скачать аудио такой продолжительности.",
|
||||
"ErrorLengthAudioConvert": "я не могу конвертировать аудио дольше чем {s} минут(ы). выбери \"лучший\" формат аудио, чтобы избежать ограничения.",
|
||||
"SettingsAudioFullTikTok": "скачивать полное аудио",
|
||||
"SettingsAudioFullTikTokDescription": "обычно такое аудио - оригинальный звук или песня, которое используется в видео. то есть, это аудио без обрезаний, голоса за кадром, и чего-либо подобного."
|
||||
"SettingsAudioFullTikTokDescription": "скачивает оригинальный звук, который использован в видео, без каких-либо изменений от автора видео.",
|
||||
"ErrorCantGetID": "у меня не получилось достать инфу по этой короткой ссылке. попробуй полную ссылку, или же попробуй позже.",
|
||||
"ErrorNoVideosInTweet": "у этого твита нет ни видео, ни гифок. попробуй другой!",
|
||||
"ImagePickerTitle": "выбери картинки для скачивания",
|
||||
"ImagePickerDownloadAudio": "скачать аудио",
|
||||
"ImagePickerExplanationPC": "нажми правой кнопкой мыши на изображение, чтобы его сохранить.",
|
||||
"ImagePickerExplanationPhone": "зажми и удерживай изображение, чтобы его сохранить.",
|
||||
"ErrorNoUrlReturned": "я не получил ссылку для скачивания от сервера. такого происходить не должно. перезагрузи страницу, а если не поможет, то {ContactLink}.",
|
||||
"ErrorUnknownStatus": "сервер ответил мне чем-то непонятным. такого происходить не должно. перезагрузи страницу, а если не поможет, то {ContactLink}."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"name": "українська",
|
||||
"code": "uk",
|
||||
"substrings": {
|
||||
"ContactLink": "<a class=\"text-backdrop\" href=\"{repo}\" target=\"_blank\">дай нам знати</a>"
|
||||
},
|
||||
|
@ -9,7 +8,6 @@
|
|||
"AboutSummary": "{appName} – твій помічник із завантаження контенту з соцмереж. ніякої реклами, ніяких трекерів. вставляєш лінк, отримаєш файл, і допиваєш у спокої свій смузі.",
|
||||
"AboutSupportedServices": "ось що підтримується:",
|
||||
"EmbedBriefDescription": "зберігай контент із соцмереж без реклами і трекерів",
|
||||
"MadeWithLove": "зроблено з <3 контриб'юторами на github и wukko <3",
|
||||
"AccessibilityInputArea": "строка, щоб вставити в неї лінк",
|
||||
"AccessibilityOpenAbout": "відкрити інфу про {appName}",
|
||||
"AccessibilityDownloadButton": "кнопка завантаження",
|
||||
|
|
|
@ -5,6 +5,7 @@ import loadJson from "../modules/sub/loadJSON.js";
|
|||
const locPath = './src/localization/languages'
|
||||
|
||||
let loc = {}
|
||||
let changelog = loadJson('./src/localization/changelog.json')
|
||||
|
||||
export function loadLoc() {
|
||||
fs.readdir(locPath, (err, files) => {
|
||||
|
@ -13,7 +14,7 @@ export function loadLoc() {
|
|||
});
|
||||
})
|
||||
}
|
||||
await loadLoc();
|
||||
loadLoc();
|
||||
export function replaceBase(s) {
|
||||
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) {
|
||||
try {
|
||||
if (lang === "changelog") return replaceBase(changelog[string]);
|
||||
if (!Object.keys(loc).includes(lang)) lang = 'en';
|
||||
let str = loc[lang]["strings"];
|
||||
if (str && str[string]) {
|
||||
|
|
|
@ -9,7 +9,6 @@ import twitter from "./services/twitter.js";
|
|||
import youtube from "./services/youtube.js";
|
||||
import vk from "./services/vk.js";
|
||||
import tiktok from "./services/tiktok.js";
|
||||
import douyin from "./services/douyin.js";
|
||||
import tumblr from "./services/tumblr.js";
|
||||
import matchActionDecider from "./sub/matchActionDecider.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,
|
||||
});
|
||||
break;
|
||||
case "douyin":
|
||||
case "tiktok":
|
||||
r = await tiktok({
|
||||
postId: patternMatch["postId"],
|
||||
id: patternMatch["id"], lang: lang,
|
||||
noWatermark: obj.noWatermark, fullAudio: obj.fullAudio,
|
||||
isAudioOnly: obj.isAudioOnly
|
||||
});
|
||||
break;
|
||||
case "douyin":
|
||||
r = await douyin({
|
||||
host: host,
|
||||
postId: patternMatch["postId"],
|
||||
id: patternMatch["id"], lang: lang,
|
||||
noWatermark: obj.noWatermark, fullAudio: obj.fullAudio,
|
||||
|
|
|
@ -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>`
|
||||
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>` : ''}
|
||||
|
@ -76,6 +75,22 @@ export function multiPagePopup(obj) {
|
|||
</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) {
|
||||
return `<a class="text-backdrop" href="${link}" target="_blank">${text}</a>`
|
||||
}
|
||||
|
|
|
@ -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 { getCommitInfo } from "../sub/currentCommit.js";
|
||||
import loc from "../../localization/manager.js";
|
||||
|
@ -29,7 +29,9 @@ for (let i in donations["crypto"]) {
|
|||
}
|
||||
export default function(obj) {
|
||||
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 {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
@ -98,13 +100,13 @@ export default function(obj) {
|
|||
text: `<div class="category-title">${loc(obj.lang, 'ChangelogLastMajor')}</div>`,
|
||||
raw: true
|
||||
}, {
|
||||
text: loc('en', 'ChangelogContentTitle'),
|
||||
text: loc('changelog', 'ContentTitle'),
|
||||
classes: ["changelog-subtitle"],
|
||||
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>`,
|
||||
raw: true
|
||||
|
@ -255,6 +257,17 @@ export default function(obj) {
|
|||
<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({
|
||||
name: "error",
|
||||
standalone: true,
|
||||
|
@ -297,11 +310,12 @@ export default function(obj) {
|
|||
</footer>
|
||||
</body>
|
||||
<script type="text/javascript">const loc = {
|
||||
noInternet: "${loc(obj.lang, 'ErrorNoInternet')}",
|
||||
noURLReturned: "${loc(obj.lang, 'ErrorBadFetch')}",
|
||||
noInternet: ` + "`" + loc(obj.lang, 'ErrorNoInternet') + "`" + `,
|
||||
noURLReturned: ` + "`" + loc(obj.lang, 'ErrorNoUrlReturned') + "`" + `,
|
||||
unknownStatus: ` + "`" + loc(obj.lang, 'ErrorUnknownStatus') + "`" + `,
|
||||
toggleDefault: '${emoji("✨")} ${loc(obj.lang, "ModeToggleSmart")} ${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 type="text/javascript" src="cobalt.js"></script>
|
||||
</html>`;
|
||||
|
|
|
@ -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') };
|
||||
}
|
||||
}
|
|
@ -36,7 +36,6 @@ export default async function(obj) {
|
|||
} else return { error: loc(obj.lang, 'ErrorEmptyDownload') }
|
||||
} else return { error: loc(obj.lang, 'ErrorBrokenLink', 'soundcloud') }
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return { error: loc(obj.lang, 'ErrorBadFetch') };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,66 +4,102 @@ import { genericUserAgent } from "../config.js";
|
|||
import { unicodeDecode } from "../sub/utils.js";
|
||||
|
||||
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) {
|
||||
try {
|
||||
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) => {
|
||||
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'tiktok') };
|
||||
});
|
||||
html = html.body
|
||||
if (!html.includes('<!DOCTYPE html>')) {
|
||||
obj.postId = html.split('video/')[1].split('?')[0]
|
||||
} else {
|
||||
obj.postId = html.split('aweme/detail/')[1].split('?')[0]
|
||||
}
|
||||
}
|
||||
if (!obj.noWatermark && !obj.isAudioOnly) {
|
||||
let html = await got.get(`https://tiktok.com/@video/video/${obj.postId}`, { headers: { "user-agent": userAgent } });
|
||||
html.on('error', (err) => {
|
||||
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'tiktok') };
|
||||
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', obj.host) };
|
||||
});
|
||||
html = html.body;
|
||||
if (html.includes(',"preloadList":[{"url":"')) {
|
||||
return {
|
||||
urls: unicodeDecode(html.split(',"preloadList":[{"url":"')[1].split('","id":"')[0].trim()),
|
||||
audioFilename: `tiktok_${obj.postId}_audio`,
|
||||
filename: `tiktok_${obj.postId}.mp4`
|
||||
};
|
||||
if (html.slice(0, 17) === '<a href="https://' && html.includes('/video/')) obj.postId = html.split('video/')[1].split('?')[0].replace("/", '')
|
||||
}
|
||||
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 {
|
||||
return { error: loc(obj.lang, 'ErrorEmptyDownload') };
|
||||
video = detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play")
|
||||
}
|
||||
videoFilename = `${filenameBase}_video_nw.mp4` // nw - no watermark
|
||||
if (!obj.noWatermark) {
|
||||
if (obj.host == "tiktok") {
|
||||
let html = await got.get(`https://tiktok.com/@video/video/${obj.postId}`, { headers: { "user-agent": userAgent } });
|
||||
html.on('error', (err) => {
|
||||
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', obj.host) };
|
||||
});
|
||||
html = html.body;
|
||||
if (html.includes(',"preloadList":[{"url":"')) {
|
||||
video = unicodeDecode(html.split(',"preloadList":[{"url":"')[1].split('","id":"')[0].trim())
|
||||
}
|
||||
} else {
|
||||
video = detail['video']['play_addr']['url_list'][0]
|
||||
}
|
||||
videoFilename = `${filenameBase}_video.mp4`
|
||||
}
|
||||
} else {
|
||||
let detail = await got.get(`https://api.tiktokv.com/aweme/v1/aweme/detail/?aweme_id=${obj.postId}`);
|
||||
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") {
|
||||
return {
|
||||
urls: audio,
|
||||
audioFilename: `tiktok_${obj.postId}_audio`,
|
||||
isAudio: true,
|
||||
isMp3: true,
|
||||
};
|
||||
} else if (video) {
|
||||
return {
|
||||
urls: video,
|
||||
audioFilename: `tiktok_${obj.postId}_audio`,
|
||||
filename: `tiktok_${obj.postId}_nw.mp4`
|
||||
};
|
||||
let fallback = obj.host == "douyin" ? detail["video"]["play_addr"]["url_list"][0].replace("playwm", "play") : detail["video"]["play_addr"]["url_list"][0];
|
||||
if (obj.fullAudio || fallback.includes("music")) {
|
||||
audio = detail["music"]["play_url"]["url_list"][0]
|
||||
audioFilename = `${filenameBase}_audio`
|
||||
} else {
|
||||
return { error: loc(obj.lang, 'ErrorEmptyDownload') };
|
||||
audio = fallback
|
||||
audioFilename = `${filenameBase}_audio_fv` // fv - from video
|
||||
}
|
||||
if (audio.slice(-4) == ".mp3") isMp3 = true;
|
||||
}
|
||||
if (video) return {
|
||||
urls: video,
|
||||
filename: videoFilename
|
||||
}
|
||||
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) {
|
||||
return { error: loc(obj.lang, 'ErrorBadFetch') };
|
||||
|
|
|
@ -1,52 +1,38 @@
|
|||
import got from "got";
|
||||
import loc from "../../localization/manager.js";
|
||||
import { services } from "../config.js";
|
||||
|
||||
const configSt = services.twitter;
|
||||
|
||||
async function fetchTweetInfo(obj) {
|
||||
let cantConnect = { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'twitter') }
|
||||
export default async function(obj) {
|
||||
try {
|
||||
let _headers = {
|
||||
"Authorization": `Bearer ${configSt.token}`,
|
||||
"Host": configSt.api,
|
||||
"Authorization": "Bearer AAAAAAAAAAAAAAAAAAAAAIK1zgAAAAAA2tUWuhGZ2JceoId5GwYWU5GspY4%3DUq7gzFoCZs1QfwGoVdvSac3IniczZEYXIcDyumCauIXpcAPorE",
|
||||
"Host": "api.twitter.com",
|
||||
"Content-Type": "application/json",
|
||||
"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
|
||||
});
|
||||
req_act.on('error', (err) => {
|
||||
return cantConnect
|
||||
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'twitter') }
|
||||
})
|
||||
_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
|
||||
});
|
||||
req_status.on('error', (err) => {
|
||||
return cantConnect
|
||||
return { error: loc(obj.lang, 'ErrorCantConnectToServiceAPI', 'twitter') }
|
||||
})
|
||||
return 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["extended_entities"]["media"][0]["type"] === "video" || parsbod["extended_entities"]["media"][0]["type"] === "animated_gif") {
|
||||
let variants = parsbod["extended_entities"]["media"][0]["video_info"]["variants"]
|
||||
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` }
|
||||
} else {
|
||||
return nothing
|
||||
}
|
||||
let parsbod = JSON.parse(req_status.body);
|
||||
if (parsbod.hasOwnProperty("extended_entities") && parsbod["extended_entities"].hasOwnProperty("media")) {
|
||||
let media = parsbod["extended_entities"]["media"][0]
|
||||
if (media["type"] === "video" || media["type"] === "animated_gif") {
|
||||
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 {
|
||||
return nothing
|
||||
return { error: loc(obj.lang, 'ErrorNoVideosInTweet') }
|
||||
}
|
||||
} else return parsbod;
|
||||
} else {
|
||||
return { error: loc(obj.lang, 'ErrorNoVideosInTweet') }
|
||||
}
|
||||
} catch (err) {
|
||||
return { error: loc(obj.lang, 'ErrorBadFetch') };
|
||||
}
|
||||
|
|
|
@ -13,14 +13,7 @@
|
|||
},
|
||||
"twitter": {
|
||||
"patterns": [":user/status/:id"],
|
||||
"quality_match": ["1080", "720", "480", "360", "240", "144"],
|
||||
"enabled": true,
|
||||
"api": "api.twitter.com",
|
||||
"token": "AAAAAAAAAAAAAAAAAAAAAIK1zgAAAAAA2tUWuhGZ2JceoId5GwYWU5GspY4%3DUq7gzFoCZs1QfwGoVdvSac3IniczZEYXIcDyumCauIXpcAPorE",
|
||||
"apiURLs": {
|
||||
"activate": "1.1/guest/activate.json",
|
||||
"status_show": "1.1/statuses/show.json"
|
||||
}
|
||||
"enabled": true
|
||||
},
|
||||
"vk": {
|
||||
"patterns": ["video-:userId_:videoId"],
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { audioIgnore, services, supportedAudio } from "../config.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 (!isAudioOnly) {
|
||||
if (!r.isAudioOnly) {
|
||||
switch (host) {
|
||||
case "twitter":
|
||||
return apiJSON(1, { u: r.urls })
|
||||
|
@ -51,7 +51,7 @@ export default function(r, host, ip, audioFormat, isAudioOnly) {
|
|||
let copy = false;
|
||||
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 (audioFormat == "mp3" || audioFormat == "best") {
|
||||
audioFormat = "mp3"
|
||||
|
@ -70,11 +70,20 @@ export default function(r, host, ip, audioFormat, isAudioOnly) {
|
|||
audioFormat = "m4a"
|
||||
copy = true
|
||||
}
|
||||
return apiJSON(2, {
|
||||
type: type,
|
||||
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
|
||||
filename: r.audioFilename, salt: process.env.streamSalt, isAudioOnly: true, audioFormat: audioFormat, copy: copy
|
||||
})
|
||||
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, {
|
||||
type: type,
|
||||
u: Array.isArray(r.urls) ? r.urls[1] : r.urls, service: host, ip: ip,
|
||||
filename: r.audioFilename, salt: process.env.streamSalt, isAudioOnly: true, audioFormat: audioFormat, copy: copy
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return apiJSON(0, { t: r.error });
|
||||
|
|
|
@ -13,6 +13,8 @@ export function apiJSON(type, obj) {
|
|||
return { status: 200, body: { status: "success", text: obj.t } };
|
||||
case 4:
|
||||
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:
|
||||
return { status: 400, body: { status: "error", text: "Bad Request" } };
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue