mirror of
https://github.com/Gabi-Zar/Images-Scrapper-JS.git
synced 2026-04-17 05:36:06 +02:00
Add images download feature and minor UI improvements
This commit is contained in:
54
assets/downloadButton.svg
Normal file
54
assets/downloadButton.svg
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
viewBox="0 0 135.46667 135.46667"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
|
||||||
|
sodipodi:docname="downloadButton.svg"
|
||||||
|
inkscape:export-filename="..\public\assets\downloadButton.png"
|
||||||
|
inkscape:export-xdpi="24"
|
||||||
|
inkscape:export-ydpi="24"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#505050"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:zoom="0.65382425"
|
||||||
|
inkscape:cx="217.94848"
|
||||||
|
inkscape:cy="279.127"
|
||||||
|
inkscape:window-width="3440"
|
||||||
|
inkscape:window-height="1369"
|
||||||
|
inkscape:window-x="1072"
|
||||||
|
inkscape:window-y="201"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Calque 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-opacity:0.5;stroke:#ffffff;stroke-width:6.35;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 67.733333,4.2333333 V 123.29583 M 110.06667,80.9625 67.733333,123.29583 25.4,80.9625"
|
||||||
|
id="path1"
|
||||||
|
sodipodi:nodetypes="ccccc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;fill-opacity:0.5;stroke:#ffffff;stroke-width:7.9375;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 5.6539753,131.00141 H 129.86077"
|
||||||
|
id="path2" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
53
main.js
53
main.js
@@ -1,9 +1,15 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
import axios from "axios";
|
||||||
|
import archiver from "archiver";
|
||||||
|
import mime from "mime";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
|
|
||||||
|
let cachedImagesUrls = {};
|
||||||
|
|
||||||
app.use(express.static("public"));
|
app.use(express.static("public"));
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
@@ -56,7 +62,52 @@ app.get("/api/getImagesURL", async (req, res) => {
|
|||||||
|
|
||||||
imagesUrls = imagesUrls.slice(0, count);
|
imagesUrls = imagesUrls.slice(0, count);
|
||||||
|
|
||||||
res.send(imagesUrls);
|
const uuid = randomUUID();
|
||||||
|
cachedImagesUrls[uuid] = imagesUrls;
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
delete cachedImagesUrls[uuid];
|
||||||
|
},
|
||||||
|
10 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
res.send({ uuid: uuid, urls: imagesUrls });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/downloadImages", async (req, res) => {
|
||||||
|
try {
|
||||||
|
let { uuid } = req.query;
|
||||||
|
const imagesUrls = cachedImagesUrls[uuid];
|
||||||
|
if (!imagesUrls) {
|
||||||
|
return res.status(400).send("Invalid 'uuid'");
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader("Content-Type", "application/zip");
|
||||||
|
res.setHeader("Content-Disposition", `attachment; filename=${uuid}.zip`);
|
||||||
|
|
||||||
|
const archive = archiver("zip", { zlib: { level: 9 } });
|
||||||
|
archive.pipe(res);
|
||||||
|
|
||||||
|
let errorNumber = 0;
|
||||||
|
for (let i = 0; i < imagesUrls.length; i++) {
|
||||||
|
const url = imagesUrls[i];
|
||||||
|
try {
|
||||||
|
const response = await axios.get(url, { responseType: "arraybuffer", timeout: 5000 });
|
||||||
|
const contentType = response.headers["content-type"];
|
||||||
|
const extension = mime.getExtension(contentType) || url.split(".").pop();
|
||||||
|
|
||||||
|
archive.append(response.data, { name: `image-${i + 1 - errorNumber}.${extension}` });
|
||||||
|
//console.log(`image downloaded ${url}`);
|
||||||
|
} catch (error) {
|
||||||
|
errorNumber += 1;
|
||||||
|
console.warn(`Unable to download image ${url} : ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await archive.finalize();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|||||||
1155
package-lock.json
generated
1155
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,10 @@
|
|||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"archiver": "^7.0.1",
|
||||||
|
"axios": "^1.13.6",
|
||||||
"cheerio": "^1.2.0",
|
"cheerio": "^1.2.0",
|
||||||
"express": "^5.2.1"
|
"express": "^5.2.1",
|
||||||
|
"mime": "^4.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/assets/downloadButton.png
Normal file
BIN
public/assets/downloadButton.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -17,7 +17,7 @@
|
|||||||
<input id="search-input" type="text" placeholder="Search For Images..." />
|
<input id="search-input" type="text" placeholder="Search For Images..." />
|
||||||
<button onclick="search()">Search</button>
|
<button onclick="search()">Search</button>
|
||||||
</form>
|
</form>
|
||||||
<button onclick="download()">Download Images</button>
|
<button class="square" onclick="download()"><img src="assets/downloadButton.png" /></button>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="loader-div" id="loader-div">
|
<div class="loader-div" id="loader-div">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const imagesDiv = document.getElementById("images-div");
|
|||||||
const imageTemplate = document.getElementById("image-template");
|
const imageTemplate = document.getElementById("image-template");
|
||||||
const loaderDiv = document.getElementById("loader-div");
|
const loaderDiv = document.getElementById("loader-div");
|
||||||
let imagesUrls = [];
|
let imagesUrls = [];
|
||||||
|
let uuid;
|
||||||
|
|
||||||
starsCanvas(starsNumber);
|
starsCanvas(starsNumber);
|
||||||
searchForm.addEventListener("submit", function (event) {
|
searchForm.addEventListener("submit", function (event) {
|
||||||
@@ -86,10 +87,15 @@ function starsCanvas(number) {
|
|||||||
async function getImagesURL(query, offset, count, smart) {
|
async function getImagesURL(query, offset, count, smart) {
|
||||||
const url = `/api/getImagesURL?q=${encodeURIComponent(query)}&offset=${offset}&count=${count}&smart=${smart}`;
|
const url = `/api/getImagesURL?q=${encodeURIComponent(query)}&offset=${offset}&count=${count}&smart=${smart}`;
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(await response.text());
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
uuid = data.uuid;
|
||||||
|
|
||||||
loaderDiv.classList.toggle("show");
|
loaderDiv.classList.toggle("show");
|
||||||
for (const url of data) {
|
for (const url of data.urls) {
|
||||||
imagesUrls.push(url);
|
imagesUrls.push(url);
|
||||||
const imageTemplateCopy = imageTemplate.content.cloneNode(true);
|
const imageTemplateCopy = imageTemplate.content.cloneNode(true);
|
||||||
imageTemplateCopy.getElementById("image").src = url;
|
imageTemplateCopy.getElementById("image").src = url;
|
||||||
@@ -97,8 +103,31 @@ async function getImagesURL(query, offset, count, smart) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function downloadImages(uuid) {
|
||||||
|
const url = `/api/downloadImages?uuid=${encodeURIComponent(uuid)}`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(await response.text());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const blobUrl = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = blobUrl;
|
||||||
|
a.download = `${uuid}.zip`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
window.URL.revokeObjectURL(blobUrl);
|
||||||
|
}
|
||||||
|
|
||||||
async function search() {
|
async function search() {
|
||||||
imagesDiv.replaceChildren();
|
imagesDiv.replaceChildren();
|
||||||
loaderDiv.classList.toggle("show");
|
loaderDiv.classList.toggle("show");
|
||||||
await getImagesURL(searchInput.value, 1, 1000, false);
|
await getImagesURL(searchInput.value, 1, 1000, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function download() {
|
||||||
|
await downloadImages(uuid);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-family: "Segoe UI", sans-serif;
|
font-family: "Segoe UI", sans-serif;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: var(--border-color) transparent;
|
scrollbar-color: var(--secondary-color) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--border-color: #ffffff;
|
--primary-color: #ffffff;
|
||||||
|
--secondary-color: #888888;
|
||||||
--text-color: #eee;
|
--text-color: #eee;
|
||||||
--transparent-white: rgba(255, 255, 255, 0.1);
|
--transparent-white: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
@@ -28,7 +29,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: var(--border-color);
|
background: var(--secondary-color);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ body {
|
|||||||
width: 200px;
|
width: 200px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
background: var(--transparent-white);
|
background: var(--transparent-white);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--primary-color);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@@ -68,12 +69,27 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content button:hover {
|
||||||
|
border-color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content button.square {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content button img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 5px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
.content input {
|
.content input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
background: var(--transparent-white);
|
background: var(--transparent-white);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--primary-color);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
@@ -81,6 +97,14 @@ body {
|
|||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content input:hover {
|
||||||
|
border-color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content input:focus {
|
||||||
|
border-color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
.content form {
|
.content form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -88,7 +112,7 @@ body {
|
|||||||
|
|
||||||
.content hr {
|
.content hr {
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--secondary-color);
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +124,7 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--primary-color);
|
||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
|
|
||||||
@@ -152,7 +176,7 @@ body {
|
|||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
border: 16px solid var(--transparent-white);
|
border: 16px solid var(--transparent-white);
|
||||||
border-top: 16px solid var(--border-color);
|
border-top: 16px solid var(--primary-color);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
|
|||||||
Reference in New Issue
Block a user