feat(new tool) multi-link-downloader

This commit is contained in:
chadmin 2024-10-17 23:52:08 -07:00
parent 0b1b98f93e
commit 7035eba246
8 changed files with 238 additions and 1 deletions

1
components.d.ts vendored
View file

@ -129,6 +129,7 @@ declare module '@vue/runtime-core' {
MenuLayout: typeof import('./src/components/MenuLayout.vue')['default']
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
MultiLinkDownloader: typeof import('./src/tools/multi-link-downloader/multi-link-downloader.vue')['default']
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']

View file

@ -392,3 +392,7 @@ tools:
text-to-binary:
title: Text to ASCII binary
description: Convert text to its ASCII binary representation and vice-versa.
multi-link-downloader:
title: Multi link downloader
description: Asynchronously downloads from multiple links into a zip file while a single link downloads directly. (Requires an internet connection)

View file

@ -71,6 +71,7 @@
"ibantools": "^4.3.3",
"js-base64": "^3.7.6",
"json5": "^2.2.3",
"jszip": "^3.10.1",
"jwt-decode": "^3.1.2",
"libphonenumber-js": "^1.10.28",
"lodash": "^4.17.21",

54
pnpm-lock.yaml generated
View file

@ -110,6 +110,9 @@ dependencies:
json5:
specifier: ^2.2.3
version: 2.2.3
jszip:
specifier: ^3.10.1
version: 3.10.1
jwt-decode:
specifier: ^3.1.2
version: 3.1.2
@ -4674,6 +4677,9 @@ packages:
browserslist: 4.22.1
dev: true
/core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
/country-code-lookup@0.1.0:
resolution: {integrity: sha512-IOI66HEG+8bXfWPy+sTzuN7161vmDZOHg1wgIPFf3WfD73FeLajnn6C+fnxOIa9RL1WRBDMXQQWW/FOaOYaQ3w==}
dev: false
@ -6195,6 +6201,9 @@ packages:
dev: true
optional: true
/immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
/import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
@ -6501,6 +6510,9 @@ packages:
is-docker: 2.2.1
dev: true
/isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
/isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
dev: true
@ -6695,6 +6707,14 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
dependencies:
lie: 3.3.0
pako: 1.0.11
readable-stream: 2.3.8
setimmediate: 1.0.5
/kolorist@1.8.0:
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
dev: true
@ -6735,6 +6755,11 @@ packages:
resolution: {integrity: sha512-1eAgjLrZA0+2Wgw4hs+4Q/kEBycxQo8ZLYnmOvZ3AlM8ImAVAJgDPlZtISLEzD1vunc2q8s2Pn7XwB7I8U3Kzw==}
dev: false
/lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
dependencies:
immediate: 3.0.6
/lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
dev: true
@ -7341,6 +7366,9 @@ packages:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
/pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
/param-case@2.1.1:
resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==}
dependencies:
@ -7580,6 +7608,7 @@ packages:
engines: {node: ^14.13.1 || >=16.0.0}
dev: true
/pretty-format@29.6.2:
resolution: {integrity: sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -7589,6 +7618,9 @@ packages:
react-is: 18.2.0
dev: true
/process-nextick-args@2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
/prosemirror-changeset@2.2.1:
resolution: {integrity: sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==}
dependencies:
@ -7841,6 +7873,17 @@ packages:
type-fest: 0.6.0
dev: true
/readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 1.0.0
process-nextick-args: 2.0.1
safe-buffer: 5.1.2
string_decoder: 1.1.1
util-deprecate: 1.0.2
/readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
@ -8047,6 +8090,9 @@ packages:
isarray: 2.0.5
dev: true
/safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
/safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
dev: true
@ -8170,6 +8216,9 @@ packages:
is-primitive: 3.0.1
dev: false
/setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
/shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@ -8376,6 +8425,11 @@ packages:
es-abstract: 1.22.3
dev: true
/string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
dependencies:
safe-buffer: 5.1.2
/string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
dependencies:

View file

@ -1,6 +1,7 @@
import { tool as base64FileConverter } from './base64-file-converter';
import { tool as base64StringConverter } from './base64-string-converter';
import { tool as basicAuthGenerator } from './basic-auth-generator';
import { tool as multiLinkDownloader } from './multi-link-downloader';
import { tool as emailNormalizer } from './email-normalizer';
import { tool as asciiTextDrawer } from './ascii-text-drawer';
@ -188,7 +189,11 @@ export const toolsByCategory: ToolCategory[] = [
},
{
name: 'Data',
components: [phoneParserAndFormatter, ibanValidatorAndParser],
components: [
phoneParserAndFormatter,
ibanValidatorAndParser,
multiLinkDownloader,
],
},
];

View file

@ -0,0 +1,12 @@
import { IconFileDownload } from '@tabler/icons-vue';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'Multi link downloader',
path: '/multi-link-downloader',
description: '',
keywords: ['multi', 'link', 'downloader'],
component: () => import('./multi-link-downloader.vue'),
icon: IconFileDownload,
createdAt: new Date('2024-10-18'),
});

View file

@ -0,0 +1,108 @@
import JSZip from 'jszip';
export async function downloadLinks(links: string): Promise<void> {
// Split links by newline and filter out empty ones
const linksArray: string[] = links.split('\n').filter(link => link.trim() !== '');
// Helper function to handle duplicate filenames
function getUniqueFileName(existingNames: Set<string>, originalName: string): string {
let fileName = originalName;
let fileExtension = '';
// Split filename and extension (if any)
const lastDotIndex = originalName.lastIndexOf('.');
if (lastDotIndex !== -1) {
fileName = originalName.substring(0, lastDotIndex);
fileExtension = originalName.substring(lastDotIndex);
}
let counter = 1;
let uniqueName = originalName;
// Append a counter to the filename if it already exists in the map
while (existingNames.has(uniqueName)) {
uniqueName = `${fileName} (${counter})${fileExtension}`;
counter++;
}
existingNames.add(uniqueName);
return uniqueName;
}
if (linksArray.length === 1) {
// Single link: download directly
const linkUrl: string = linksArray[0];
try {
const response: Response = await fetch(linkUrl);
if (!response.ok) {
throw new Error(`Failed to fetch ${linkUrl}`);
}
// Get file as blob
const blob: Blob = await response.blob();
// Extract filename from URL
const fileName: string = linkUrl.split('/').pop() || 'downloaded_file';
// Trigger download
const a: HTMLAnchorElement = document.createElement('a');
const downloadUrl: string = window.URL.createObjectURL(blob);
a.href = downloadUrl;
a.download = fileName;
document.body.appendChild(a);
a.click();
// Clean up
document.body.removeChild(a);
window.URL.revokeObjectURL(downloadUrl);
}
catch (error) {
console.error('Error downloading the file:', error);
}
}
else if (linksArray.length > 1) {
// Multiple links: create a zip file
const zip = new JSZip();
const fileNamesSet = new Set<string>(); // To track file names for duplicates
await Promise.all(
linksArray.map(async (linkUrl: string) => {
try {
const response: Response = await fetch(linkUrl);
if (!response.ok) {
throw new Error(`Failed to fetch ${linkUrl}`);
}
const blob: Blob = await response.blob();
// Extract filename from URL
let fileName: string = linkUrl.split('/').pop() || 'file';
// Get unique filename if duplicate exists
fileName = getUniqueFileName(fileNamesSet, fileName);
// Add file to the zip
zip.file(fileName, blob);
}
catch (error) {
console.error(`Error downloading file from ${linkUrl}:`, error);
}
}),
);
// Generate the zip file and trigger download
zip.generateAsync({ type: 'blob' }).then((zipBlob: Blob) => {
const downloadUrl: string = window.URL.createObjectURL(zipBlob);
// Trigger download of the zip file
const a: HTMLAnchorElement = document.createElement('a');
a.href = downloadUrl;
a.download = 'downloaded_files.zip';
document.body.appendChild(a);
a.click();
// Clean up
document.body.removeChild(a);
window.URL.revokeObjectURL(downloadUrl);
});
}
}

View file

@ -0,0 +1,52 @@
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { downloadLinks } from './multi-link-downloader.service';
export default defineComponent({
setup() {
const links = ref<string>('');
const downloadMultiLinks = () => {
if (links.value) {
downloadLinks(links.value);
}
};
const clearInput = () => {
links.value = '';
};
return {
links,
downloadMultiLinks,
clearInput,
};
},
});
</script>
<template>
<c-card>
<div class="mb-4 flex justify-between">
<c-button
class="mr-2"
:disabled="!links"
@click="downloadMultiLinks"
>
Start Download
</c-button>
<c-button
class="ml-2"
@click="clearInput"
>
Clear
</c-button>
</div>
<c-input-text
v-model:value="links"
placeholder="Add links separated by new lines..."
multiline
:rows="20"
/>
</c-card>
</template>