@semantest/chrome-extension
Version:
Browser extension for ChatGPT-buddy - AI automation extension built on Web-Buddy framework
399 lines (394 loc) • 19.6 kB
JavaScript
/*
Web-Buddy Framework
Google Images Downloader Entity
Copyright (C) 2025-today rydnr@acm-sl.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
var useValue = arguments.length > 2;
for (var i = 0; i < initializers.length; i++) {
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
}
return useValue ? value : void 0;
};
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
var _, done = false;
for (var i = decorators.length - 1; i >= 0; i--) {
var context = {};
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
if (kind === "accessor") {
if (result === void 0) continue;
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
if (_ = accept(result.get)) descriptor.get = _;
if (_ = accept(result.set)) descriptor.set = _;
if (_ = accept(result.init)) initializers.unshift(_);
}
else if (_ = accept(result)) {
if (kind === "field") initializers.unshift(_);
else descriptor[key] = _;
}
}
if (target) Object.defineProperty(target, contextIn.name, descriptor);
done = true;
};
import { Entity, listen } from '@typescript-eda/core';
import { GoogleImageDownloadRequested, GoogleImageDownloadCompleted, FileDownloadRequested, FileDownloadFailed, DownloadPatternDetected } from '../events/download-events';
/**
* Google Images Downloader Entity - Specialized downloader for Google Images
*
* Responsibilities:
* - Handle Google Images-specific download patterns
* - Extract high-resolution image URLs from Google Images interface
* - Learn and apply download patterns for different image types
* - Coordinate with training system for UI automation
*/
let GoogleImagesDownloader = (() => {
var _a;
let _classSuper = Entity;
let _instanceExtraInitializers = [];
let _downloadGoogleImage_decorators;
let _recordDownloadCompletion_decorators;
return _a = class GoogleImagesDownloader extends _classSuper {
constructor() {
super(...arguments);
this.searchQuery = (__runInitializers(this, _instanceExtraInitializers), '');
this.downloadedImages = new Map();
this.learnedPatterns = new Map();
}
/**
* Handles Google Images download requests
*/
async downloadGoogleImage(event) {
try {
this.searchQuery = event.searchQuery || '';
// Extract the highest resolution image URL
const imageUrl = await this.extractHighResImageUrl(event.imageElement);
if (!imageUrl) {
return new FileDownloadFailed(event.correlationId, event.imageElement.src, 'Could not extract high-resolution image URL');
}
// Generate a meaningful filename
const filename = this.generateImageFilename(event, imageUrl);
// Detect download pattern for learning
await this.detectDownloadPattern(event.imageElement, imageUrl);
// Create file download request
return new FileDownloadRequested(event.correlationId, imageUrl, filename, 'uniquify', false);
}
catch (error) {
return new FileDownloadFailed(event.correlationId, event.imageElement.src, error instanceof Error ? error.message : 'Google Images download failed');
}
}
/**
* Extracts high-resolution image URL from Google Images element
*/
async extractHighResImageUrl(imageElement) {
const originalSrc = imageElement.src;
// Check if this is a Google Images thumbnail that needs resolution
if (this.isGoogleImagesThumbnail(originalSrc)) {
return await this.resolveHighResolutionUrl(imageElement);
}
// If it's already a direct URL, use it
if (this.isDirectImageUrl(originalSrc)) {
return originalSrc;
}
// Try to find the original URL in data attributes or click behavior
return await this.findOriginalImageUrl(imageElement);
}
/**
* Checks if URL is a Google Images thumbnail
*/
isGoogleImagesThumbnail(url) {
return url.includes('encrypted-tbn0.gstatic.com') ||
url.includes('googleusercontent.com') ||
url.includes('google.com/images') ||
url.includes('gstatic.com');
}
/**
* Checks if URL is a direct image URL
*/
isDirectImageUrl(url) {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg'];
const lowerUrl = url.toLowerCase();
return imageExtensions.some(ext => lowerUrl.includes(ext)) &&
!this.isGoogleImagesThumbnail(url);
}
/**
* Resolves high-resolution URL from Google Images thumbnail
*/
async resolveHighResolutionUrl(imageElement) {
// Method 1: Check data attributes for original URL
const dataOriginal = imageElement.getAttribute?.('data-src') ||
imageElement.getAttribute?.('data-original') ||
imageElement.getAttribute?.('data-url');
if (dataOriginal && this.isDirectImageUrl(dataOriginal)) {
return dataOriginal;
}
// Method 2: Simulate click to trigger Google Images to load full res
const fullResUrl = await this.simulateClickForFullRes(imageElement);
if (fullResUrl) {
return fullResUrl;
}
// Method 3: Parse from Google Images URL structure
return this.parseOriginalFromGoogleUrl(imageElement.src);
}
/**
* Finds original image URL through DOM analysis
*/
async findOriginalImageUrl(imageElement) {
// Look for parent links that might contain the original URL
const parentLink = imageElement.closest?.('a[href]');
if (parentLink) {
const href = parentLink.href;
if (this.isDirectImageUrl(href)) {
return href;
}
}
// Check for nearby elements with image URLs
const container = imageElement.closest?.('[data-imgurl], [data-src], [data-original]');
if (container) {
const imgUrl = container.getAttribute('data-imgurl') ||
container.getAttribute('data-src') ||
container.getAttribute('data-original');
if (imgUrl && this.isDirectImageUrl(imgUrl)) {
return imgUrl;
}
}
return null;
}
/**
* Simulates click to get full resolution image from Google Images
*/
async simulateClickForFullRes(imageElement) {
return new Promise((resolve) => {
// Set up mutation observer to detect when full-res image loads
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
const addedNodes = Array.from(mutation.addedNodes);
for (const node of addedNodes) {
if (node instanceof HTMLImageElement) {
const src = node.src;
if (src && this.isDirectImageUrl(src) && !this.isGoogleImagesThumbnail(src)) {
observer.disconnect();
resolve(src);
return;
}
}
}
}
}
});
// Start observing
observer.observe(document.body, {
childList: true,
subtree: true
});
// Simulate click
try {
imageElement.click?.();
}
catch (error) {
console.warn('Could not click image element:', error);
}
// Timeout after 3 seconds
setTimeout(() => {
observer.disconnect();
resolve(null);
}, 3000);
});
}
/**
* Attempts to parse original URL from Google Images URL structure
*/
parseOriginalFromGoogleUrl(googleUrl) {
try {
// Google Images often encodes the original URL in the thumbnail URL
const url = new URL(googleUrl);
// Check for 'imgurl' parameter in Google search URLs
const imgUrl = url.searchParams.get('imgurl');
if (imgUrl) {
return decodeURIComponent(imgUrl);
}
// Check for other common Google Images URL patterns
const pathSegments = url.pathname.split('/');
for (const segment of pathSegments) {
if (segment.startsWith('http') || segment.includes('.jpg') || segment.includes('.png')) {
try {
return decodeURIComponent(segment);
}
catch {
continue;
}
}
}
return null;
}
catch {
return null;
}
}
/**
* Generates appropriate filename for downloaded image
*/
generateImageFilename(event, imageUrl) {
// Use provided filename if available
if (event.filename) {
return this.ensureImageExtension(event.filename, imageUrl);
}
// Generate filename from search query and image metadata
let baseName = '';
if (this.searchQuery) {
baseName = this.searchQuery.replace(/[^a-z0-9]/gi, '_').substring(0, 50);
}
else if (event.imageElement.alt) {
baseName = event.imageElement.alt.replace(/[^a-z0-9]/gi, '_').substring(0, 50);
}
else if (event.imageElement.title) {
baseName = event.imageElement.title.replace(/[^a-z0-9]/gi, '_').substring(0, 50);
}
else {
baseName = 'google_image';
}
// Add timestamp for uniqueness
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
const extension = this.extractImageExtension(imageUrl) || 'jpg';
return `${baseName}_${timestamp}.${extension}`;
}
/**
* Ensures filename has appropriate image extension
*/
ensureImageExtension(filename, imageUrl) {
const hasExtension = /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(filename);
if (hasExtension) {
return filename;
}
const extension = this.extractImageExtension(imageUrl) || 'jpg';
return `${filename}.${extension}`;
}
/**
* Extracts file extension from image URL
*/
extractImageExtension(url) {
const match = url.match(/\.([a-z0-9]+)(?:\?|$)/i);
return match ? match[1] : null;
}
/**
* Detects and records download pattern for learning
*/
async detectDownloadPattern(imageElement, resolvedUrl) {
const selector = this.generateSelector(imageElement);
const elementContext = this.extractElementContext(imageElement);
const pageContext = this.extractPageContext();
const patternEvent = new DownloadPatternDetected(resolvedUrl, selector, elementContext, pageContext);
// Emit pattern detection event for learning system
this.emit(patternEvent);
}
/**
* Generates CSS selector for image element
*/
generateSelector(element) {
if (!element)
return '';
const selectors = [];
// ID selector (highest priority)
if (element.id) {
selectors.push(`#${element.id}`);
}
// Class selector
if (element.className) {
const classes = element.className.split(' ').filter(Boolean);
if (classes.length > 0) {
selectors.push(`.${classes.join('.')}`);
}
}
// Attribute selectors for Google Images
if (element.src) {
selectors.push(`img[src*="${element.src.substring(0, 50)}"]`);
}
if (element.alt) {
selectors.push(`img[alt="${element.alt}"]`);
}
// Tag with position fallback
const tagName = element.tagName?.toLowerCase() || 'img';
selectors.push(tagName);
return selectors[0] || 'img';
}
/**
* Extracts element context for pattern learning
*/
extractElementContext(element) {
return {
tagName: element.tagName?.toLowerCase() || 'img',
className: element.className || '',
id: element.id || '',
textContent: element.alt || element.title || '',
href: element.src || '',
dimensions: {
width: element.naturalWidth || element.width || 0,
height: element.naturalHeight || element.height || 0
}
};
}
/**
* Extracts page context for pattern learning
*/
extractPageContext() {
return {
hostname: window.location.hostname,
pathname: window.location.pathname,
title: document.title,
isGoogleImages: window.location.hostname.includes('google.') &&
window.location.pathname.includes('/imghp')
};
}
/**
* Records successful download completion
*/
recordDownloadCompletion(event) {
this.downloadedImages.set(event.imageUrl, {
downloadId: event.downloadId,
filename: event.filename,
filepath: event.filepath,
metadata: event.metadata,
downloadedAt: new Date()
});
return Promise.resolve();
}
/**
* Gets download statistics
*/
getDownloadStats() {
return {
totalDownloaded: this.downloadedImages.size,
currentSearchQuery: this.searchQuery,
learnedPatternsCount: this.learnedPatterns.size,
recentDownloads: Array.from(this.downloadedImages.values()).slice(-10)
};
}
},
(() => {
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
_downloadGoogleImage_decorators = [listen(GoogleImageDownloadRequested)];
_recordDownloadCompletion_decorators = [listen(GoogleImageDownloadCompleted)];
__esDecorate(_a, null, _downloadGoogleImage_decorators, { kind: "method", name: "downloadGoogleImage", static: false, private: false, access: { has: obj => "downloadGoogleImage" in obj, get: obj => obj.downloadGoogleImage }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(_a, null, _recordDownloadCompletion_decorators, { kind: "method", name: "recordDownloadCompletion", static: false, private: false, access: { has: obj => "recordDownloadCompletion" in obj, get: obj => obj.recordDownloadCompletion }, metadata: _metadata }, null, _instanceExtraInitializers);
if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
})(),
_a;
})();
export { GoogleImagesDownloader };