shotminer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +72 -0
- package/build/cli/index.js +48 -0
- package/build/cli/index.js.map +1 -0
- package/build/config/index.js +15 -0
- package/build/config/index.js.map +1 -0
- package/build/index.js +11 -0
- package/build/index.js.map +1 -0
- package/build/scraper/index.js +94 -0
- package/build/scraper/index.js.map +1 -0
- package/build/utils/downloader.js +42 -0
- package/build/utils/downloader.js.map +1 -0
- package/build/utils/logger.js +13 -0
- package/build/utils/logger.js.map +1 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Naseem Ansari
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Shotminer
|
|
2
|
+
|
|
3
|
+
Shotminer is a powerful, developer-friendly **Dribbble Design Scraper CLI Tool**. It allows you to search and download high-quality UI/UX designs directly from your terminal using a highly aesthetic and modular setup built with Node.js, TypeScript, and Puppeteer.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔍 **Automated Scraping:** Retrieve designs using just a search prompt.
|
|
8
|
+
- ⏬ **Bulk Downloading:** Downloads images sequentially to a well-organized folder structure constraint to your prompt.
|
|
9
|
+
- 🎨 **Beautiful CLI UX:** Utilizes interactive prompts and status spinners using Commander, Inquirer, and Ora.
|
|
10
|
+
- ⚡ **Puppeteer Engine:** Efficiently fetches data bypassing general restrictions.
|
|
11
|
+
- 🛠️ **Developer Friendly:** Completely typed, modular, and optimized for contribution.
|
|
12
|
+
|
|
13
|
+
## Pre-requisites
|
|
14
|
+
|
|
15
|
+
- Node.js (v18.x or above)
|
|
16
|
+
- NPM or Yarn
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
1. Clone the repository:
|
|
21
|
+
```bash
|
|
22
|
+
git clone https://github.com/Gitnaseem745/shotminer.git
|
|
23
|
+
cd shotminer
|
|
24
|
+
```
|
|
25
|
+
2. Install dependencies:
|
|
26
|
+
```bash
|
|
27
|
+
npm install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Setup
|
|
31
|
+
|
|
32
|
+
Create a `.env` file in the root of the project to configure the scraper (optional):
|
|
33
|
+
```env
|
|
34
|
+
# Disable or enable headless mode (default: false for true headless execution)
|
|
35
|
+
HEADLESS=true
|
|
36
|
+
|
|
37
|
+
# Limit how many pictures you want per command execution
|
|
38
|
+
RESULTS_LIMIT=10
|
|
39
|
+
|
|
40
|
+
# Destination folder name
|
|
41
|
+
DESIGNS_SUBDIR=designs
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
You can use the CLI tool in varying setups. Once compiled, it acts as a standalone script.
|
|
47
|
+
|
|
48
|
+
### Using Development Mode
|
|
49
|
+
```bash
|
|
50
|
+
npm run dev
|
|
51
|
+
```
|
|
52
|
+
Wait for the prompt and enter your search term (e.g., `mobile app UI design`).
|
|
53
|
+
|
|
54
|
+
### Compiled Usage
|
|
55
|
+
First build the TypeScript source:
|
|
56
|
+
```bash
|
|
57
|
+
npm run build
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Then execute it via node:
|
|
61
|
+
```bash
|
|
62
|
+
npm start -- "dashboard UI" -l 5
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Options:
|
|
66
|
+
- `-l, --limit <number>` : Limit the number of designs to fetch.
|
|
67
|
+
- `-h, --help` : Show help for command.
|
|
68
|
+
|
|
69
|
+
## Folder Structure
|
|
70
|
+
|
|
71
|
+
The application will construct a folder based on your prompt query. E.g., for `dashboard UI` it will yield the path:
|
|
72
|
+
`./designs/dashboard_ui/design_1.png`
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { scrapeDesigns } from '../scraper/index.js';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
export async function runCLI() {
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name('shotminer')
|
|
10
|
+
.description('Dribbble Design Scraper CLI Tool')
|
|
11
|
+
.version('1.0.0')
|
|
12
|
+
.argument('[prompt]', 'Search prompt to scrape')
|
|
13
|
+
.option('-l, --limit <number>', 'Number of results to scrape', '10')
|
|
14
|
+
.parse(process.argv);
|
|
15
|
+
const options = program.opts();
|
|
16
|
+
let args = program.args;
|
|
17
|
+
let prompt = args[0];
|
|
18
|
+
if (!prompt) {
|
|
19
|
+
const answers = await inquirer.prompt([
|
|
20
|
+
{
|
|
21
|
+
type: 'input',
|
|
22
|
+
name: 'prompt',
|
|
23
|
+
message: 'Enter a search prompt (e.g., "mobile app UI design"):',
|
|
24
|
+
validate: (input) => input.trim() ? true : 'Prompt cannot be empty!',
|
|
25
|
+
},
|
|
26
|
+
]);
|
|
27
|
+
prompt = answers.prompt;
|
|
28
|
+
}
|
|
29
|
+
const limit = parseInt(options.limit, 10);
|
|
30
|
+
console.log(); // Add empty line
|
|
31
|
+
const spinner = ora(`Scraping designs for "${prompt}"...`).start();
|
|
32
|
+
try {
|
|
33
|
+
const results = await scrapeDesigns({ prompt, limit });
|
|
34
|
+
spinner.succeed(`Successfully scraped ${results.length} designs!`);
|
|
35
|
+
console.log('\n--- Results ---');
|
|
36
|
+
results.forEach((res, i) => {
|
|
37
|
+
console.log(`${i + 1}. [${res.designer}] ${res.title}`);
|
|
38
|
+
console.log(` URL: ${res.url}`);
|
|
39
|
+
console.log(` Saved to: ${res.localPath}\n`);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
spinner.fail(`Scraping failed: ${error.message}`);
|
|
44
|
+
logger.error(error);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,QAAQ,MAAM,UAAU,CAAC;AAChC,OAAO,GAAG,MAAM,KAAK,CAAC;AACtB,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,MAAM,CAAC,KAAK,UAAU,MAAM;IAC1B,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAE9B,OAAO;SACJ,IAAI,CAAC,WAAW,CAAC;SACjB,WAAW,CAAC,kCAAkC,CAAC;SAC/C,OAAO,CAAC,OAAO,CAAC;SAChB,QAAQ,CAAC,UAAU,EAAE,yBAAyB,CAAC;SAC/C,MAAM,CAAC,sBAAsB,EAAE,6BAA6B,EAAE,IAAI,CAAC;SACnE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvB,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAC/B,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAExB,IAAI,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IAErB,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC;YACpC;gBACE,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,uDAAuD;gBAChE,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,yBAAyB;aACrE;SACF,CAAC,CAAC;QACH,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC1B,CAAC;IAED,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAE1C,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,iBAAiB;IAChC,MAAM,OAAO,GAAG,GAAG,CAAC,yBAAyB,MAAM,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC;IAEnE,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QACvD,OAAO,CAAC,OAAO,CAAC,wBAAwB,OAAO,CAAC,MAAM,WAAW,CAAC,CAAC;QAEnE,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;QACjC,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE;YACzB,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,QAAQ,KAAK,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;YACxD,OAAO,CAAC,GAAG,CAAC,WAAW,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC;YAClC,OAAO,CAAC,GAAG,CAAC,gBAAgB,GAAG,CAAC,SAAS,IAAI,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QACpB,OAAO,CAAC,IAAI,CAAC,oBAAoB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAClD,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import dotenv from 'dotenv';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
dotenv.config();
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
export const config = {
|
|
8
|
+
// Application configs
|
|
9
|
+
headless: process.env.HEADLESS !== 'false', // default true
|
|
10
|
+
resultsLimit: parseInt(process.env.RESULTS_LIMIT || '10', 10),
|
|
11
|
+
designsSubdir: process.env.DESIGNS_SUBDIR || 'designs',
|
|
12
|
+
// Computed paths
|
|
13
|
+
outputDir: path.resolve(process.cwd(), process.env.DESIGNS_SUBDIR || 'designs'),
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,MAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAE3C,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,sBAAsB;IACtB,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,OAAO,EAAE,eAAe;IAC3D,YAAY,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,IAAI,EAAE,EAAE,CAAC;IAC7D,aAAa,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,SAAS;IAEtD,iBAAiB;IACjB,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,SAAS,CAAC;CAChF,CAAC"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runCLI } from './cli/index.js';
|
|
3
|
+
process.on('unhandledRejection', (err) => {
|
|
4
|
+
console.error('Unhandled Promise Rejection:', err);
|
|
5
|
+
process.exit(1);
|
|
6
|
+
});
|
|
7
|
+
runCLI().catch((err) => {
|
|
8
|
+
console.error('Fatal Error:', err);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAExC,OAAO,CAAC,EAAE,CAAC,oBAAoB,EAAE,CAAC,GAAG,EAAE,EAAE;IACvC,OAAO,CAAC,KAAK,CAAC,8BAA8B,EAAE,GAAG,CAAC,CAAC;IACnD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import puppeteer from 'puppeteer';
|
|
2
|
+
import { config } from '../config/index.js';
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
import { downloadImage } from '../utils/downloader.js';
|
|
5
|
+
export async function scrapeDesigns({ prompt, limit = config.resultsLimit }) {
|
|
6
|
+
logger.info(`Starting scrape for prompt: "${prompt}" (limit: ${limit})`);
|
|
7
|
+
const browser = await puppeteer.launch({
|
|
8
|
+
headless: config.headless,
|
|
9
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security']
|
|
10
|
+
});
|
|
11
|
+
try {
|
|
12
|
+
const page = await browser.newPage();
|
|
13
|
+
// Set a realistic user agent
|
|
14
|
+
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
|
|
15
|
+
await page.setViewport({ width: 1280, height: 800 });
|
|
16
|
+
const searchUrl = `https://dribbble.com/search/${encodeURIComponent(prompt)}`;
|
|
17
|
+
logger.debug(`Navigating to ${searchUrl}`);
|
|
18
|
+
await page.goto(searchUrl, { waitUntil: 'networkidle2' });
|
|
19
|
+
// Wait for the results grid to load
|
|
20
|
+
await page.waitForSelector('.shot-thumbnail', { timeout: 15000 }).catch(() => {
|
|
21
|
+
logger.warn('Could not find shots grid. Dribbble might have blocked the request or UI changed.');
|
|
22
|
+
});
|
|
23
|
+
// We will evaluate the page to extract data
|
|
24
|
+
const extractedData = await page.evaluate((maxLimit) => {
|
|
25
|
+
const shots = document.querySelectorAll('.shot-thumbnail');
|
|
26
|
+
const results = [];
|
|
27
|
+
for (let i = 0; i < shots.length; i++) {
|
|
28
|
+
if (results.length >= maxLimit)
|
|
29
|
+
break;
|
|
30
|
+
const shot = shots[i];
|
|
31
|
+
// Find the image element inside the shot
|
|
32
|
+
const imgEl = shot.querySelector('figure img');
|
|
33
|
+
let imageUrl = imgEl?.getAttribute('data-src') || imgEl?.getAttribute('src') || '';
|
|
34
|
+
// Often Dribbble serves a low-res image. The high-res or video could be in a data-attr
|
|
35
|
+
// Look for better resolutions if available
|
|
36
|
+
const srcset = imgEl?.getAttribute('srcset') || '';
|
|
37
|
+
if (srcset) {
|
|
38
|
+
const sources = srcset.split(',').map(s => s.trim().split(' '));
|
|
39
|
+
// Naive approach: take the last one which is usually highest res
|
|
40
|
+
imageUrl = sources[sources.length - 1]?.[0] || imageUrl;
|
|
41
|
+
}
|
|
42
|
+
else if (imageUrl.includes('compress')) {
|
|
43
|
+
imageUrl = imageUrl.replace(/(compress=1&|resize=\d+x\d+)/g, ''); // Try to clean up URL
|
|
44
|
+
}
|
|
45
|
+
if (imageUrl.startsWith('data:')) {
|
|
46
|
+
const videoEl = shot.querySelector('video source');
|
|
47
|
+
if (videoEl) {
|
|
48
|
+
imageUrl = videoEl.getAttribute('src') || imageUrl;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const titleEl = shot.querySelector('.shot-title');
|
|
52
|
+
const title = titleEl?.textContent?.trim() || `Design ${i + 1}`;
|
|
53
|
+
const designerEl = shot.querySelector('.display-name');
|
|
54
|
+
const designer = designerEl?.textContent?.trim() || 'Unknown';
|
|
55
|
+
const linkEl = shot.querySelector('.dribbble-link');
|
|
56
|
+
const url = linkEl ? `https://dribbble.com${linkEl.getAttribute('href')}` : '';
|
|
57
|
+
if (imageUrl && !imageUrl.startsWith('data:')) {
|
|
58
|
+
results.push({
|
|
59
|
+
title,
|
|
60
|
+
designer,
|
|
61
|
+
url,
|
|
62
|
+
imageUrl
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return results;
|
|
67
|
+
}, limit);
|
|
68
|
+
logger.info(`Extracted ${extractedData.length} designs. Starting downloads...`);
|
|
69
|
+
const downloadedResults = [];
|
|
70
|
+
// Download images locally
|
|
71
|
+
for (let i = 0; i < extractedData.length; i++) {
|
|
72
|
+
const item = extractedData[i];
|
|
73
|
+
try {
|
|
74
|
+
const extMatch = item.imageUrl.match(/\\.([a-zA-Z0-9]{2,5})(?:[\\?#]|$)/);
|
|
75
|
+
const ext = extMatch ? extMatch[1] : 'jpg';
|
|
76
|
+
const filename = `design_${i + 1}.${ext}`;
|
|
77
|
+
const localPath = await downloadImage(item.imageUrl, filename, prompt);
|
|
78
|
+
downloadedResults.push({
|
|
79
|
+
...item,
|
|
80
|
+
localPath
|
|
81
|
+
});
|
|
82
|
+
logger.debug(`Saved ${filename}`);
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
logger.error(`Failed to save ${item.imageUrl}: ${e.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return downloadedResults;
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await browser.close();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/scraper/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAevD,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,EAAE,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC,YAAY,EAAiB;IACxF,MAAM,CAAC,IAAI,CAAC,gCAAgC,MAAM,aAAa,KAAK,GAAG,CAAC,CAAC;IAEzE,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,MAAM,CAAC;QACrC,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,IAAI,EAAE,CAAC,cAAc,EAAE,0BAA0B,EAAE,wBAAwB,CAAC;KAC7E,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QAErC,6BAA6B;QAC7B,MAAM,IAAI,CAAC,YAAY,CAAC,iHAAiH,CAAC,CAAC;QAC3I,MAAM,IAAI,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QAErD,MAAM,SAAS,GAAG,+BAA+B,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9E,MAAM,CAAC,KAAK,CAAC,iBAAiB,SAAS,EAAE,CAAC,CAAC;QAE3C,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC,CAAC;QAE1D,oCAAoC;QACpC,MAAM,IAAI,CAAC,eAAe,CAAC,iBAAiB,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;YAC3E,MAAM,CAAC,IAAI,CAAC,mFAAmF,CAAC,CAAC;QACnG,CAAC,CAAC,CAAC;QAEH,4CAA4C;QAC5C,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC,QAAQ,EAAE,EAAE;YACrD,MAAM,KAAK,GAAG,QAAQ,CAAC,gBAAgB,CAAC,iBAAiB,CAAC,CAAC;YAC3D,MAAM,OAAO,GAAiB,EAAE,CAAC;YAEjC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBACtC,IAAI,OAAO,CAAC,MAAM,IAAI,QAAQ;oBAAE,MAAM;gBAEtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;gBAEtB,yCAAyC;gBACzC,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC;gBAC/C,IAAI,QAAQ,GAAG,KAAK,EAAE,YAAY,CAAC,UAAU,CAAC,IAAI,KAAK,EAAE,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;gBAEnF,uFAAuF;gBACvF,2CAA2C;gBAC3C,MAAM,MAAM,GAAG,KAAK,EAAE,YAAY,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;gBACnD,IAAI,MAAM,EAAE,CAAC;oBACR,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;oBAChE,iEAAiE;oBACjE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC;gBAC7D,CAAC;qBAAM,IAAI,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;oBACtC,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,+BAA+B,EAAE,EAAE,CAAC,CAAC,CAAC,sBAAsB;gBAC7F,CAAC;gBAED,IAAI,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;oBAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC;oBACnD,IAAI,OAAO,EAAE,CAAC;wBACV,QAAQ,GAAG,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,QAAQ,CAAC;oBACvD,CAAC;gBACN,CAAC;gBAED,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;gBAClD,MAAM,KAAK,GAAG,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,UAAU,CAAC,GAAC,CAAC,EAAE,CAAC;gBAE9D,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC;gBACvD,MAAM,QAAQ,GAAG,UAAU,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC;gBAE9D,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,gBAAgB,CAAC,CAAC;gBACpD,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,uBAAuB,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAE/E,IAAI,QAAQ,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;oBAC9C,OAAO,CAAC,IAAI,CAAC;wBACX,KAAK;wBACL,QAAQ;wBACR,GAAG;wBACH,QAAQ;qBACT,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;YAED,OAAO,OAAO,CAAC;QACjB,CAAC,EAAE,KAAK,CAAC,CAAC;QAEV,MAAM,CAAC,IAAI,CAAC,aAAa,aAAa,CAAC,MAAM,iCAAiC,CAAC,CAAC;QAEhF,MAAM,iBAAiB,GAAG,EAAE,CAAC;QAE7B,0BAA0B;QAC1B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,aAAa,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC5C,MAAM,IAAI,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;YAC9B,IAAI,CAAC;gBACF,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;gBAC1E,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;gBAC3C,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC;gBAC1C,MAAM,SAAS,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;gBACvE,iBAAiB,CAAC,IAAI,CAAC;oBACrB,GAAG,IAAI;oBACP,SAAS;iBACV,CAAC,CAAC;gBACH,MAAM,CAAC,KAAK,CAAC,SAAS,QAAQ,EAAE,CAAC,CAAC;YACrC,CAAC;YAAC,OAAO,CAAM,EAAE,CAAC;gBACf,MAAM,CAAC,KAAK,CAAC,kBAAkB,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;YACjE,CAAC;QACL,CAAC;QAED,OAAO,iBAAiB,CAAC;IAC3B,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { logger } from './logger.js';
|
|
4
|
+
import { config } from '../config/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Downloads an image from a URL and saves it to the output directory
|
|
7
|
+
* @param url The image URL
|
|
8
|
+
* @param filename The desired filename
|
|
9
|
+
* @param prompt Dir name derived from the prompt
|
|
10
|
+
*/
|
|
11
|
+
export async function downloadImage(url, filename, prompt) {
|
|
12
|
+
const dirPath = path.join(config.outputDir, prompt.replace(/[^a-z0-9]/gi, '_').toLowerCase());
|
|
13
|
+
if (!fs.existsSync(dirPath)) {
|
|
14
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
const filePath = path.join(dirPath, filename);
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(url);
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
|
21
|
+
}
|
|
22
|
+
if (!response.body) {
|
|
23
|
+
throw new Error('No body in response');
|
|
24
|
+
}
|
|
25
|
+
// Convert Web ReadableStream to Node.js Readable
|
|
26
|
+
const fileStream = fs.createWriteStream(filePath);
|
|
27
|
+
const reader = response.body.getReader();
|
|
28
|
+
while (true) {
|
|
29
|
+
const { done, value } = await reader.read();
|
|
30
|
+
if (done)
|
|
31
|
+
break;
|
|
32
|
+
fileStream.write(value);
|
|
33
|
+
}
|
|
34
|
+
fileStream.end();
|
|
35
|
+
return filePath;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
logger.error(`Error downloading image ${filename}: ${error.message}`);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=downloader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"downloader.js","sourceRoot":"","sources":["../../src/utils/downloader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAW,EAAE,QAAgB,EAAE,MAAc;IAC/E,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IAE9F,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAE9C,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,mBAAmB,GAAG,KAAK,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;QACpE,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC;QACzC,CAAC;QAED,iDAAiD;QACjD,MAAM,UAAU,GAAG,EAAE,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QAEzC,OAAO,IAAI,EAAE,CAAC;YACZ,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,IAAI;gBAAE,MAAM;YAChB,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC1B,CAAC;QACD,UAAU,CAAC,GAAG,EAAE,CAAC;QAEjB,OAAO,QAAQ,CAAC;IAClB,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QACpB,MAAM,CAAC,KAAK,CAAC,2BAA2B,QAAQ,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACtE,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
export const logger = pino({
|
|
3
|
+
transport: {
|
|
4
|
+
target: 'pino-pretty',
|
|
5
|
+
options: {
|
|
6
|
+
colorize: true,
|
|
7
|
+
ignore: 'pid,hostname',
|
|
8
|
+
translateTime: 'SYS:standard',
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
12
|
+
});
|
|
13
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE;QACT,MAAM,EAAE,aAAa;QACrB,OAAO,EAAE;YACP,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE,cAAc;YACtB,aAAa,EAAE,cAAc;SAC9B;KACF;IACD,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,MAAM;CACvC,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shotminer",
|
|
3
|
+
"author": "Naseem Ansari (GitHub: @Gitnaseem745)",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "A developer-friendly Dribbble Design Scraper CLI tool to search and download high-quality UI/UX designs via terminal.",
|
|
6
|
+
"main": "build/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"shotminer": "./build/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"build/**/*"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "node build/index.js",
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "nodemon --watch src -e ts --exec \"node --no-warnings --loader ts-node/esm\" src/index.ts",
|
|
17
|
+
"clean": "rimraf build"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/Gitnaseem745/shotminer.git"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"scraper",
|
|
25
|
+
"cli",
|
|
26
|
+
"dribbble",
|
|
27
|
+
"ui",
|
|
28
|
+
"ux",
|
|
29
|
+
"design",
|
|
30
|
+
"puppeteer",
|
|
31
|
+
"image-downloader",
|
|
32
|
+
"web-scraping",
|
|
33
|
+
"typescript",
|
|
34
|
+
"tool"
|
|
35
|
+
],
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/Gitnaseem745/shotminer/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/Gitnaseem745/shotminer#readme",
|
|
40
|
+
"license": "ISC",
|
|
41
|
+
"type": "module",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"commander": "^14.0.3",
|
|
44
|
+
"dotenv": "^17.3.1",
|
|
45
|
+
"inquirer": "^8.2.7",
|
|
46
|
+
"ora": "^5.4.1",
|
|
47
|
+
"pino": "^10.3.1",
|
|
48
|
+
"pino-pretty": "^13.1.3",
|
|
49
|
+
"puppeteer": "^24.40.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/inquirer": "^8.2.12",
|
|
53
|
+
"@types/node": "^25.5.0",
|
|
54
|
+
"nodemon": "^3.1.14",
|
|
55
|
+
"rimraf": "^6.1.3",
|
|
56
|
+
"ts-node": "^10.9.2",
|
|
57
|
+
"typescript": "^5.9.3"
|
|
58
|
+
}
|
|
59
|
+
}
|