feat(new tool) multi-link-downloader
This commit is contained in:
		
							parent
							
								
									0b1b98f93e
								
							
						
					
					
						commit
						7035eba246
					
				
							
								
								
									
										1
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -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'] | ||||
|  | ||||
| @ -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) | ||||
| @ -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
									
									
									
								
							
							
						
						
									
										54
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @ -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: | ||||
|  | ||||
| @ -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, | ||||
|     ], | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										12
									
								
								src/tools/multi-link-downloader/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/multi-link-downloader/index.ts
									
									
									
									
									
										Normal 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'), | ||||
| }); | ||||
							
								
								
									
										108
									
								
								src/tools/multi-link-downloader/multi-link-downloader.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/tools/multi-link-downloader/multi-link-downloader.service.ts
									
									
									
									
									
										Normal 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); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										52
									
								
								src/tools/multi-link-downloader/multi-link-downloader.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/tools/multi-link-downloader/multi-link-downloader.vue
									
									
									
									
									
										Normal 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> | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user