Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tiktok: use webapp-based downloading method #503

Merged
merged 12 commits into from
May 21, 2024
18 changes: 12 additions & 6 deletions src/modules/processing/matchActionDecider.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { audioIgnore, services, supportedAudio } from "../config.js";
import { createResponse } from "../processing/request.js";
import loc from "../../localization/manager.js";
import createFilename from "./createFilename.js";
import { createStream } from "../stream/manage.js";

export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, disableMetadata, filenamePattern, toGif, requestIP) {
let action,
Expand Down Expand Up @@ -41,7 +42,7 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
case "photo":
responseType = "redirect";
break;

case "gif":
params = { type: "gif" }
break;
Expand Down Expand Up @@ -76,8 +77,13 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
params = {
type: pickerType,
picker: r.picker,
u: Array.isArray(r.urls) ? r.urls[1] : r.urls,
copy: audioFormat === "best" ? true : false
u: createStream({
otomir23 marked this conversation as resolved.
Show resolved Hide resolved
service: "tiktok",
type: pickerType,
u: r.urls,
filename: r.audioFilename,
}),
copy: audioFormat === "best"
}
}
break;
Expand All @@ -101,7 +107,7 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
responseType = "redirect";
}
break;

case "twitter":
if (r.type === "remux") {
params = { type: r.type };
Expand All @@ -125,15 +131,15 @@ export default function(r, host, userFormat, isAudioOnly, lang, isAudioMuted, di
}
break;

case "audio":
case "audio":
if (audioIgnore.includes(host)
|| (host === "reddit" && r.typeId === "redirect")) {
return createResponse("error", { t: loc(lang, 'ErrorEmptyDownload') })
}

let processType = "render",
copy = false;

if (!supportedAudio.includes(audioFormat)) {
audioFormat = "best"
}
Expand Down
79 changes: 38 additions & 41 deletions src/modules/processing/services/tiktok.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { genericUserAgent, env } from "../../config.js";
import { genericUserAgent } from "../../config.js";
import { updateCookie } from "../cookie/manager.js";
import { extract } from "../url.js";
import Cookie from "../cookie/cookie.js";

const shortDomain = "https://vt.tiktok.com/";
const apiPath = "https://api22-normal-c-alisg.tiktokv.com/aweme/v1/feed/?region=US&carrier_region=US";
const apiUserAgent = "TikTok/338014 CFNetwork/1410.1 Darwin/22.6.0";
export const cookie = new Cookie({})

export default async function(obj) {
let postId = obj.postId ? obj.postId : false;

if (!env.tiktokDeviceInfo) return { error: 'ErrorCouldntFetch' };
let postId = obj.postId

if (!postId) {
let html = await fetch(`${shortDomain}${obj.id}`, {
Expand All @@ -19,54 +19,54 @@ export default async function(obj) {

if (!html) return { error: 'ErrorCouldntFetch' };

if (html.slice(0, 17) === '<a href="https://') {
postId = html.split('<a href="https://')[1].split('?')[0].split('/')[3]
} else if (html.slice(0, 32) === '<a href="https://m.tiktok.com/v/' && html.includes('/v/')) {
postId = html.split('/v/')[1].split('.html')[0].replace("/", '')
if (html.startsWith('<a href="https://')) {
const { patternMatch } = extract(html.split('<a href="https://')[1].split('?')[0])
postId = patternMatch.postId
}
}
if (!postId) return { error: 'ErrorCantGetID' };

let deviceInfo = new URLSearchParams(env.tiktokDeviceInfo).toString();

let apiURL = new URL(apiPath);
apiURL.searchParams.append("aweme_id", postId);

let detail = await fetch(`${apiURL.href}&${deviceInfo}`, {
// should always be /video/, even for photos
const res = await fetch(`https://tiktok.com/@i/video/${postId}`, {
headers: {
"user-agent": apiUserAgent
"user-agent": genericUserAgent,
cookie,
}
}).then(r => r.json()).catch(() => {});
})
updateCookie(cookie, res.headers)

const html = await res.text()

detail = detail?.aweme_list?.find(v => v.aweme_id === postId);
if (!detail) return { error: 'ErrorCouldntFetch' };
let detail
try {
const json = html
.split('<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application/json">')[1]
.split('</script>')[0]
const data = JSON.parse(json)
detail = data["__DEFAULT_SCOPE__"]["webapp.video-detail"]["itemInfo"]["itemStruct"]
} catch {
return { error: 'ErrorCouldntFetch' };
}

let video, videoFilename, audioFilename, audio, images,
filenameBase = `tiktok_${detail.author.unique_id}_${postId}`,
filenameBase = `tiktok_${detail.author.uniqueId}_${postId}`,
bestAudio = 'm4a';

images = detail.image_post_info?.images;
images = detail.imagePost?.images;

let playAddr = detail.video.play_addr_h264;
let playAddr = detail.video.playAddr;
if (obj.h265) {
playAddr = detail.video.bit_rate[0].play_addr
}
if (!playAddr && detail.video.play_addr) {
playAddr = detail.video.play_addr
const h265PlayAddr = detail.video.bitrateInfo.find(b => b.CodecType.includes("h265"))?.PlayAddr.UrlList[0]
playAddr = h265PlayAddr || playAddr
}

if (!obj.isAudioOnly && !images) {
video = playAddr.url_list[0];
video = playAddr;
videoFilename = `${filenameBase}.mp4`;
} else {
let fallback = playAddr.url_list[0];
audio = fallback;
audio = detail.music.playUrl || playAddr;
audioFilename = `${filenameBase}_audio`;
if (obj.fullAudio || fallback.includes("music")) {
wukko marked this conversation as resolved.
Show resolved Hide resolved
audio = detail.music.play_url.url_list[0]
audioFilename = `${filenameBase}_audio_original`
}
if (audio.slice(-4) === ".mp3") bestAudio = 'mp3';
if (audio.endsWith(".mp3")) bestAudio = 'mp3';
wukko marked this conversation as resolved.
Show resolved Hide resolved
}

if (video) return {
Expand All @@ -80,12 +80,9 @@ export default async function(obj) {
bestAudio
}
if (images) {
let imageLinks = [];
for (let i in images) {
let sel = images[i].display_image.url_list;
sel = sel.filter(p => p.includes(".jpeg?"))
imageLinks.push({url: sel[0]})
}
let imageLinks = images
.map(i => i.imageURL.urlList.find(p => p.includes(".jpeg?")))
.map(url => ({ url }))
return {
picker: imageLinks,
urls: audio,
Expand Down
8 changes: 7 additions & 1 deletion src/modules/stream/shared.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { genericUserAgent } from "../config.js";
import { cookie as tiktokCookie } from "../processing/services/tiktok.js";

const defaultHeaders = {
'user-agent': genericUserAgent
Expand All @@ -13,9 +14,14 @@ const serviceHeaders = {
origin: 'https://www.youtube.com',
referer: 'https://www.youtube.com',
DNT: '?1'
},
tiktok: {
cookie: tiktokCookie
}
}

export function getHeaders(service) {
return { ...defaultHeaders, ...serviceHeaders[service] }
// Converting all header values to strings
return Object.entries({ ...defaultHeaders, ...serviceHeaders[service] })
.reduce((p, [key, val]) => ({ ...p, [key]: String(val) }), {})
}