Skip to content

Commit e7b49d5

Browse files
committed
bulk download video/audio tiktok
1 parent d3e6837 commit e7b49d5

File tree

3 files changed

+221
-45
lines changed

3 files changed

+221
-45
lines changed

scripts/auto_redirectLargestImageSrc.js

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export async function getLargestImageSrc(imgSrc, webUrl) {
7171

7272
// bypass redirect
7373
imgSrc = UfsGlobal.Utils.makeUrlValid(imgSrc);
74-
let redirectedUrl = await getRedirectedUrl(imgSrc);
74+
let redirectedUrl = await UfsGlobal.Utils.getRedirectedUrl(imgSrc);
7575
if (redirectedUrl) {
7676
imgSrc = redirectedUrl;
7777
}
@@ -450,21 +450,3 @@ function testRegex(str, regexs) {
450450
}
451451
return false;
452452
}
453-
async function getRedirectedUrl(url) {
454-
try {
455-
while (true) {
456-
let res = await UfsGlobal.Extension.fetchByPassOrigin(url, {
457-
method: "HEAD",
458-
});
459-
if (res?.redirected) {
460-
console.log("redirected:", url, "->", res.url);
461-
url = res.url;
462-
} else {
463-
return url;
464-
}
465-
}
466-
} catch (e) {
467-
console.log("ERROR:", e);
468-
return url;
469-
}
470-
}

scripts/content-scripts/ufs_global.js

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const UfsGlobal = {
3232
getWatchingVideoSrc,
3333
},
3434
Utils: {
35+
getRedirectedUrl,
3536
sanitizeName,
3637
debounce,
3738
makeUrlValid,
@@ -522,6 +523,25 @@ function getWatchingVideoSrc() {
522523

523524
// #region Utils
524525

526+
async function getRedirectedUrl(url) {
527+
try {
528+
while (true) {
529+
let res = await UfsGlobal.Extension.fetchByPassOrigin(url, {
530+
method: "HEAD",
531+
});
532+
if (res?.redirected) {
533+
console.log("redirected:", url, "->", res.url);
534+
url = res.url;
535+
} else {
536+
return url;
537+
}
538+
}
539+
} catch (e) {
540+
console.log("ERROR:", e);
541+
return url;
542+
}
543+
}
544+
525545
// https://github.com/parshap/node-sanitize-filename/blob/master/index.js
526546
// https://github.com/Dinoosauro/tiktok-to-ytdlp/blob/main/script.js
527547
function sanitizeName(name, modifyIfPosible = true) {
@@ -745,11 +765,12 @@ async function chooseFolderToDownload(subDirName = "") {
745765
return subDir;
746766
}
747767
async function downloadToFolder(url, fileName, dirHandler, subFolderName = "") {
768+
if (!url) return false;
748769
try {
749-
const f = getOriginalWindowFunction("fetch");
750-
770+
// const f = getOriginalWindowFunction("fetch");
751771
// try download directly, using fetch blob
752-
const res = await f(url);
772+
console.log(url);
773+
const res = await fetch(url);
753774
const blob = await res.blob();
754775
const fileHandler = await dirHandler.getFileHandle(fileName, {
755776
create: true,
@@ -760,7 +781,6 @@ async function downloadToFolder(url, fileName, dirHandler, subFolderName = "") {
760781
return true;
761782
} catch (e) {
762783
console.error(e);
763-
debugger;
764784

765785
// backup download: using extension api
766786
await download({

scripts/tiktok_batchDownload.js

Lines changed: 196 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ export default {
107107
max-height: 90vh;
108108
overflow-y: auto;
109109
overflow-x: hidden;
110+
display: flex;
111+
flex-direction: column;
110112
}
111113
.ufs_popup table, .ufs_popup th, .ufs_popup td {
112114
border: 1px solid #aaa;
@@ -120,6 +122,19 @@ export default {
120122
top: -22px;
121123
background: #333;
122124
}
125+
.ufs_popup input {
126+
padding: 5px;
127+
}
128+
.ufs_popup button {
129+
padding: 5px;
130+
background: #333;
131+
color: #eee;
132+
border: 1px solid #777;
133+
cursor: pointer;
134+
}
135+
.ufs_popup button:hover {
136+
background: #666;
137+
}
123138
</style>
124139
125140
<div id="${floatingBtnId}">📥</div>
@@ -131,6 +146,17 @@ export default {
131146
const floatingBtn = ui.querySelector("#" + floatingBtnId);
132147
const container = ui.querySelector("#" + containerId);
133148

149+
container.onclick = (e) => {
150+
if (e.target === container) {
151+
toggle(false);
152+
}
153+
};
154+
155+
floatingBtn.onclick = () => {
156+
let isShow = toggle();
157+
if (isShow) renderData();
158+
};
159+
134160
function toggle(willShow) {
135161
if (!(typeof willShow === "boolean")) {
136162
let isShowing = container.style.display == "flex";
@@ -146,15 +172,23 @@ export default {
146172
<h1 style="text-align:center">Tiktok - Useful Scripts</h1>
147173
<h2 style="text-align:center">Found ${CACHED.videoById.size} videos</h2>
148174
175+
<div style="align-self: flex-end;">
176+
<button id="video">🎬 Download video</button>
177+
<button id="audio">🎧 Download audio</button>
178+
<button id="json">📄 Download json</button>
179+
<button id="clear">🗑️ Clear all</button>
180+
<input type="text" id="search" placeholder="🔎 Search..." >
181+
</div>
182+
149183
<table>
150184
<thead>
151185
<tr>
152186
<th data-sort="index">#</th>
153-
<th>Video</th>
187+
<th>🎬 Video</th>
154188
<th data-sort="title">Title</th>
155-
<th data-sort="user">User</th>
156-
<th data-sort="view">View</th>
157-
<th data-sort="length">Length</th>
189+
<th data-sort="user">👤 User</th>
190+
<th data-sort="view">👀 View</th>
191+
<th data-sort="length">🕒 Length</th>
158192
<th>Download</th>
159193
</tr>
160194
</thead>
@@ -164,18 +198,153 @@ export default {
164198
</div>`;
165199

166200
const tbody = container.querySelector("tbody");
167-
renderTable(tbody);
201+
202+
// render initial data
203+
let data = Array.from(CACHED.videoById.values()).map((_, index) => ({
204+
..._,
205+
index,
206+
}));
207+
renderTable(tbody, data);
208+
209+
// btn
210+
const clearBtn = container.querySelector("button#clear");
211+
clearBtn.addEventListener("click", () => {
212+
if (confirm("Are you sure want to clear all data?")) {
213+
CACHED.videoById.clear();
214+
renderData();
215+
floatingBtn.innerText = "📥";
216+
}
217+
});
218+
const downVideoBtn = container.querySelector("button#video");
219+
downVideoBtn.addEventListener("click", () => {
220+
download(
221+
data.map((_, i) => {
222+
const urlList =
223+
_.video.bitrateInfo.find((b) => b.Bitrate === _.video.bitrate)
224+
?.PlayAddr?.UrlList || [];
225+
226+
const bestUrl = urlList[urlList.length - 1];
227+
228+
return {
229+
url: bestUrl || _.video.playAddr,
230+
filename:
231+
i +
232+
"_" +
233+
UfsGlobal.Utils.sanitizeName(_.desc.substr(0, 50)) +
234+
".mp4",
235+
};
236+
}),
237+
(i, total) => {
238+
downVideoBtn.textContent = `🎬 Download video (${i}/${total})`;
239+
}
240+
);
241+
});
242+
const downAudioBtn = container.querySelector("button#audio");
243+
downAudioBtn.addEventListener("click", () => {
244+
const uniqueMusic = new Map();
245+
for (const item of data) {
246+
if (!uniqueMusic.has(item.music.id))
247+
uniqueMusic.set(item.music.id, item);
248+
}
249+
download(
250+
Array.from(uniqueMusic.values()).map((_, i) => ({
251+
url: _.music.playUrl,
252+
filename:
253+
i +
254+
"_" +
255+
UfsGlobal.Utils.sanitizeName(
256+
_.music.title.substr(0, 50) || "audio"
257+
) +
258+
".mp3",
259+
})),
260+
(i, total) => {
261+
downAudioBtn.textContent = `🎧 Download audio (${i}/${total})`;
262+
}
263+
);
264+
});
265+
const downJsonBtn = container.querySelector("button#json");
266+
downJsonBtn.addEventListener("click", () => {
267+
UfsGlobal.Utils.downloadData(
268+
JSON.stringify(data, null, 4),
269+
"tiktok.json"
270+
);
271+
});
272+
273+
// search
274+
const searchInp = container.querySelector("#search");
275+
searchInp.addEventListener("input", (event) => {
276+
const value = event.target.value;
277+
let trs = tbody.querySelectorAll("tr");
278+
for (const tr of trs) {
279+
const tds = tr.querySelectorAll("td");
280+
let found = false;
281+
for (const td of tds) {
282+
if (td.textContent.toLowerCase().includes(value)) {
283+
found = true;
284+
break;
285+
}
286+
}
287+
tr.style.display = found ? "" : "none";
288+
}
289+
});
290+
291+
// sorting
292+
const sorting = {};
293+
const ths = container.querySelectorAll("th");
294+
for (const th of ths) {
295+
const sort = th.getAttribute("data-sort");
296+
// if (!sort) return;
297+
th.style.cursor = "pointer";
298+
th.title = "Sort";
299+
300+
th.addEventListener("click", () => {
301+
sorting[sort] = sorting[sort] == 1 ? -1 : 1;
302+
switch (sort) {
303+
case "index":
304+
data = data.sort((a, b) =>
305+
sorting[sort] == -1 ? a.index - b.index : b.index - a.index
306+
);
307+
break;
308+
case "title":
309+
data = data.sort((a, b) =>
310+
sorting[sort] == -1
311+
? a.desc.localeCompare(b.desc)
312+
: b.desc.localeCompare(a.desc)
313+
);
314+
break;
315+
case "user":
316+
data = data.sort((a, b) =>
317+
sorting[sort] == -1
318+
? a.author.uniqueId.localeCompare(b.author.uniqueId)
319+
: b.author.uniqueId.localeCompare(a.author.uniqueId)
320+
);
321+
break;
322+
case "view":
323+
data = data.sort((a, b) =>
324+
sorting[sort] == -1
325+
? a.stats.playCount - b.stats.playCount
326+
: b.stats.playCount - a.stats.playCount
327+
);
328+
break;
329+
case "length":
330+
data = data.sort((a, b) =>
331+
sorting[sort] == -1
332+
? a.video.duration - b.video.duration
333+
: b.video.duration - a.video.duration
334+
);
335+
break;
336+
}
337+
renderTable(tbody, data);
338+
});
339+
}
168340
}
169341

170-
function renderTable(
171-
tbody,
172-
data = Array.from(CACHED.videoById.values())
173-
) {
342+
function renderTable(tbody, data) {
174343
tbody.innerHTML = data
175344
.map(
176345
(v, i) => /*html*/ `
177346
<tr>
178-
<td>${i + 1}</td>
347+
<td>${v.index}</td>
179348
<td>
180349
<a target="_blank" href="${v.video.playAddr}">
181350
<img src="${v.video.originCover}" style="width:150px" />
@@ -194,12 +363,13 @@ export default {
194363
<td>${v.video.duration}s</td>
195364
<td>
196365
<p style="max-width:200px">
197-
<a href="${v.video.playAddr}" download target="_blank">Video</a><br/>
366+
<a href="${v.video.playAddr}" download target="_blank">🎬 Video</a><br/>
367+
<a href="${v.video.cover}" download target="_blank">🖼️ Cover</a><br/>
198368
<a href="${v.author.avatarLarger}" download target="_blank">
199-
Avatar
369+
👤 Avatar
200370
</a><br/>
201371
<a href="${v.music.playUrl}" download target="_blank">
202-
Music: ${v.music.title}
372+
🎧 Music: ${v.music.title}
203373
</a>
204374
</p>
205375
</td>
@@ -208,16 +378,20 @@ export default {
208378
.join("");
209379
}
210380

211-
container.onclick = (e) => {
212-
if (e.target === container) {
213-
toggle(false);
214-
}
215-
};
381+
async function download(data, onProgress) {
382+
const dir = await UfsGlobal.Utils.chooseFolderToDownload("tiktok");
216383

217-
floatingBtn.onclick = () => {
218-
let isShow = toggle();
219-
if (isShow) renderData();
220-
};
384+
for (let i = 0; i < data.length; ++i) {
385+
try {
386+
const { url, filename } = data[i];
387+
const realUrl = await UfsGlobal.Utils.getRedirectedUrl(url);
388+
await UfsGlobal.Utils.downloadToFolder(realUrl, filename, dir);
389+
onProgress?.(i + 1, data.length);
390+
} catch (e) {
391+
console.error(e);
392+
}
393+
}
394+
}
221395

222396
hookFetch({
223397
onAfter: async (url, options, response) => {

0 commit comments

Comments
 (0)