habulaj commited on
Commit
a1b34e4
·
verified ·
1 Parent(s): 209e4ad

Delete routers/search.py

Browse files
Files changed (1) hide show
  1. routers/search.py +0 -382
routers/search.py DELETED
@@ -1,382 +0,0 @@
1
- from fastapi import APIRouter, HTTPException, Query
2
- from fastapi.responses import JSONResponse
3
- import httpx
4
- import json
5
- import re
6
- from urllib.parse import unquote
7
- from PIL import Image
8
- import io
9
- import asyncio
10
- import struct
11
- from typing import Optional, Tuple
12
-
13
- router = APIRouter()
14
-
15
- @router.get("/search")
16
- async def search(
17
- q: str = Query(..., description="Termo de pesquisa para imagens"),
18
- min_width: int = Query(1200, description="Largura mínima das imagens (padrão: 1200px)")
19
- ):
20
- """
21
- Busca imagens no Google Imagens e retorna uma lista estruturada
22
- Agora com filtro de largura mínima
23
- """
24
-
25
- # URL do Google Imagens com parâmetros para imagens grandes
26
- google_images_url = "http://www.google.com/search"
27
-
28
- params = {
29
- "tbm": "isch", # Google Images
30
- "q": q,
31
- "start": 0,
32
- "sa": "N",
33
- "asearch": "arc",
34
- "cs": "1",
35
- "tbs": "isz:l", # Adiciona filtro para imagens grandes (Large)
36
- # Outras opções disponíveis:
37
- # "isz:m" = Medium
38
- # "isz:i" = Icon
39
- # "isz:lt,islt:2mp" = Larger than 2MP
40
- # "isz:ex,iszw:1920,iszh:1080" = Exact size
41
- "async": f"arc_id:srp_GgSMaOPQOtL_5OUPvbSTOQ_110,ffilt:all,ve_name:MoreResultsContainer,inf:1,_id:arc-srp_GgSMaOPQOtL_5OUPvbSTOQ_110,_pms:s,_fmt:pc"
42
- }
43
-
44
- headers = {
45
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
46
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
47
- "Accept-Language": "pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3",
48
- "Accept-Encoding": "gzip, deflate",
49
- "Connection": "keep-alive",
50
- "Referer": "https://www.google.com/"
51
- }
52
-
53
- try:
54
- async with httpx.AsyncClient(timeout=30.0) as client:
55
- response = await client.get(google_images_url, params=params, headers=headers)
56
-
57
- if response.status_code != 200:
58
- raise HTTPException(status_code=response.status_code, detail="Erro ao buscar no Google Imagens")
59
-
60
- # Extrair dados das imagens do conteúdo retornado
61
- images = extract_images_from_response(response.text)
62
-
63
- # Enriquecer com dimensões reais das imagens (otimizado)
64
- enriched_images = await enrich_images_with_dimensions_optimized(images)
65
-
66
- # Filtrar apenas imagens que têm dimensões válidas E largura >= min_width
67
- valid_images = [
68
- img for img in enriched_images
69
- if img.get('width') is not None
70
- and img.get('height') is not None
71
- and img.get('width') >= min_width
72
- ]
73
-
74
- # Se não temos resultados suficientes, tenta buscar mais com filtros mais agressivos
75
- if len(valid_images) < 20:
76
- print(f"Poucos resultados com largura >= {min_width}px, buscando mais imagens...")
77
-
78
- # Tenta uma segunda busca com filtro de imagens extra grandes
79
- params["tbs"] = "isz:lt,islt:4mp" # Larger than 4MP
80
-
81
- async with httpx.AsyncClient(timeout=30.0) as client:
82
- response2 = await client.get(google_images_url, params=params, headers=headers)
83
-
84
- if response2.status_code == 200:
85
- additional_images = extract_images_from_response(response2.text)
86
- additional_enriched = await enrich_images_with_dimensions_optimized(additional_images)
87
-
88
- # Combina os resultados e remove duplicatas
89
- all_images = enriched_images + additional_enriched
90
- seen_urls = set()
91
- unique_images = []
92
-
93
- for img in all_images:
94
- if (img.get('url') not in seen_urls
95
- and img.get('width') is not None
96
- and img.get('height') is not None
97
- and img.get('width') >= min_width):
98
- seen_urls.add(img.get('url'))
99
- unique_images.append(img)
100
-
101
- valid_images = unique_images
102
-
103
- # Ordena por largura (maiores primeiro) e limita a 50 resultados
104
- valid_images.sort(key=lambda x: x.get('width', 0), reverse=True)
105
- final_images = valid_images[:50]
106
-
107
- return JSONResponse(content={
108
- "query": q,
109
- "min_width_filter": min_width,
110
- "total_found": len(final_images),
111
- "images": final_images
112
- })
113
-
114
- except httpx.TimeoutException:
115
- raise HTTPException(status_code=408, detail="Timeout na requisição ao Google")
116
- except Exception as e:
117
- raise HTTPException(status_code=500, detail=f"Erro ao executar a busca: {str(e)}")
118
-
119
-
120
- def clean_wikimedia_url(url: str) -> str:
121
- """
122
- Remove 'thumb/' das URLs do Wikimedia para obter imagem em resolução original
123
- Funciona com URLs que terminam direto no arquivo ou com redimensionamento
124
- """
125
- if 'wikimedia.org' in url and '/thumb/' in url:
126
- try:
127
- # Casos possíveis:
128
- # 1. https://upload.wikimedia.org/wikipedia/commons/thumb/7/79/James_Gunn_%2828557194032%29_%28cropped%29.jpg
129
- # 2. https://upload.wikimedia.org/wikipedia/commons/thumb/7/79/James_Gunn_%2828557194032%29_%28cropped%29.jpg/220px-James_Gunn_%2828557194032%29_%28cropped%29.jpg
130
- # Ambos devem virar: https://upload.wikimedia.org/wikipedia/commons/7/79/James_Gunn_%2828557194032%29_%28cropped%29.jpg
131
-
132
- # Divide a URL na parte /thumb/
133
- parts = url.split('/thumb/')
134
- if len(parts) == 2:
135
- before_thumb = parts[0] # https://upload.wikimedia.org/wikipedia/commons
136
- after_thumb = parts[1] # 7/79/James_Gunn_%2828557194032%29_%28cropped%29.jpg ou 7/79/James_Gunn_%2828557194032%29_%28cropped%29.jpg/220px-...
137
-
138
- # Divide o after_thumb por barras
139
- path_parts = after_thumb.split('/')
140
-
141
- if len(path_parts) >= 3:
142
- # Estrutura: ['7', '79', 'filename.jpg'] ou ['7', '79', 'filename.jpg', 'resized-filename.jpg']
143
- # Queremos sempre pegar os 3 primeiros elementos (diretório + nome original)
144
- original_path = '/'.join(path_parts[:3]) # '7/79/James_Gunn_%2828557194032%29_%28cropped%29.jpg'
145
- cleaned_url = f"{before_thumb}/{original_path}"
146
- print(f"URL limpa: {url} -> {cleaned_url}")
147
- return cleaned_url
148
- elif len(path_parts) == 2:
149
- # Caso onde só tem diretório/arquivo: 7/79 (sem o nome do arquivo)
150
- # Neste caso, a URL original pode estar malformada, retorna como está
151
- print(f"URL do Wikimedia malformada (sem nome do arquivo): {url}")
152
-
153
- except Exception as e:
154
- print(f"Erro ao limpar URL do Wikimedia: {e}")
155
-
156
- return url
157
-
158
-
159
- def extract_images_from_response(response_text: str) -> list:
160
- """
161
- Extrai informações das imagens do HTML/JavaScript retornado pelo Google
162
- Agora busca mais URLs para garantir resultados com alta resolução
163
- """
164
- images = []
165
-
166
- try:
167
- # Usar o regex antigo que funcionava para pegar todas as URLs
168
- pattern = r'https?:\/\/[^\s"\'<>]+?\.(?:jpg|png|webp|jpeg)\b'
169
- image_urls = re.findall(pattern, response_text, re.IGNORECASE)
170
-
171
- # Remove duplicatas mantendo a ordem
172
- seen_urls = set()
173
- unique_urls = []
174
- for url in image_urls:
175
- # Limpa a URL imediatamente ao extrair
176
- cleaned_url = clean_wikimedia_url(url)
177
- if cleaned_url not in seen_urls:
178
- seen_urls.add(cleaned_url)
179
- unique_urls.append(cleaned_url)
180
-
181
- # Extrai mais URLs inicialmente (150) porque muitas serão filtradas por largura
182
- # Isso garante que tenhamos pelo menos 50 resultados válidos com largura >= 1200px
183
- for url in unique_urls[:150]:
184
- images.append({
185
- "url": url,
186
- "width": None,
187
- "height": None
188
- })
189
-
190
- except Exception as e:
191
- print(f"Erro na extração: {e}")
192
-
193
- return images
194
-
195
-
196
- def get_image_size_from_bytes(data: bytes) -> Optional[Tuple[int, int]]:
197
- """
198
- Extrai dimensões da imagem usando apenas os primeiros bytes (muito rápido)
199
- Suporta JPEG, PNG, GIF, WebP sem usar PIL - versão melhorada
200
- """
201
- if len(data) < 24:
202
- return None
203
-
204
- try:
205
- # JPEG
206
- if data[:2] == b'\xff\xd8':
207
- i = 2
208
- while i < len(data) - 8:
209
- if data[i:i+2] == b'\xff\xc0' or data[i:i+2] == b'\xff\xc2':
210
- if i + 9 <= len(data):
211
- height = struct.unpack('>H', data[i+5:i+7])[0]
212
- width = struct.unpack('>H', data[i+7:i+9])[0]
213
- if width > 0 and height > 0:
214
- return width, height
215
- i += 1
216
-
217
- # PNG
218
- elif data[:8] == b'\x89PNG\r\n\x1a\n':
219
- if len(data) >= 24:
220
- width = struct.unpack('>I', data[16:20])[0]
221
- height = struct.unpack('>I', data[20:24])[0]
222
- if width > 0 and height > 0:
223
- return width, height
224
-
225
- # GIF
226
- elif data[:6] in (b'GIF87a', b'GIF89a'):
227
- if len(data) >= 10:
228
- width = struct.unpack('<H', data[6:8])[0]
229
- height = struct.unpack('<H', data[8:10])[0]
230
- if width > 0 and height > 0:
231
- return width, height
232
-
233
- # WebP
234
- elif data[:4] == b'RIFF' and len(data) > 12 and data[8:12] == b'WEBP':
235
- if len(data) >= 30:
236
- if data[12:16] == b'VP8 ':
237
- # VP8 format
238
- if len(data) >= 30:
239
- width = struct.unpack('<H', data[26:28])[0] & 0x3fff
240
- height = struct.unpack('<H', data[28:30])[0] & 0x3fff
241
- if width > 0 and height > 0:
242
- return width, height
243
- elif data[12:16] == b'VP8L':
244
- # VP8L format
245
- if len(data) >= 25:
246
- bits = struct.unpack('<I', data[21:25])[0]
247
- width = (bits & 0x3fff) + 1
248
- height = ((bits >> 14) & 0x3fff) + 1
249
- if width > 0 and height > 0:
250
- return width, height
251
- elif data[12:16] == b'VP8X':
252
- # VP8X format (extended)
253
- if len(data) >= 30:
254
- width = struct.unpack('<I', data[24:27] + b'\x00')[0] + 1
255
- height = struct.unpack('<I', data[27:30] + b'\x00')[0] + 1
256
- if width > 0 and height > 0:
257
- return width, height
258
-
259
- except (struct.error, IndexError) as e:
260
- # Se houver erro no parsing, retorna None silenciosamente
261
- pass
262
-
263
- return None
264
-
265
-
266
- async def get_image_dimensions_fast(client: httpx.AsyncClient, url: str) -> Tuple[str, Optional[int], Optional[int]]:
267
- """
268
- Obtém dimensões da imagem de forma otimizada
269
- A URL já foi limpa na função extract_images_from_response
270
- """
271
- try:
272
- # Limpa a URL de caracteres escapados e problemáticos
273
- clean_url = url.replace('\\u003d', '=').replace('\\u0026', '&').replace('\\\\', '').replace('\\/', '/')
274
-
275
- # Headers otimizados - primeiro tenta com range pequeno
276
- headers = {
277
- 'Range': 'bytes=0-2048', # Aumentado para 2KB para ser mais confiável
278
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
279
- 'Accept': 'image/*',
280
- 'Accept-Encoding': 'gzip, deflate',
281
- 'Connection': 'close'
282
- }
283
-
284
- # Timeout bem baixo para ser rápido
285
- response = await client.get(clean_url, headers=headers, timeout=5.0)
286
-
287
- if response.status_code in [200, 206]: # 206 = Partial Content (normal com Range)
288
- # Tenta primeiro com parsing manual (mais rápido)
289
- dimensions = get_image_size_from_bytes(response.content)
290
- if dimensions:
291
- print(f"Dimensões obtidas via parsing manual para {clean_url}: {dimensions[0]}x{dimensions[1]}")
292
- return clean_url, dimensions[0], dimensions[1]
293
-
294
- # Se não conseguiu com parsing manual, tenta baixar mais dados
295
- print(f"Parsing manual falhou para {clean_url}, tentando baixar mais dados...")
296
-
297
- # Remove o Range header para baixar mais dados
298
- headers_full = {
299
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
300
- 'Accept': 'image/*',
301
- 'Accept-Encoding': 'gzip, deflate',
302
- 'Connection': 'close'
303
- }
304
-
305
- # Tenta baixar os primeiros 10KB
306
- headers_full['Range'] = 'bytes=0-10240'
307
- response_full = await client.get(clean_url, headers=headers_full, timeout=5.0)
308
-
309
- if response_full.status_code in [200, 206]:
310
- # Tenta novamente com parsing manual
311
- dimensions = get_image_size_from_bytes(response_full.content)
312
- if dimensions:
313
- print(f"Dimensões obtidas via parsing manual (10KB) para {clean_url}: {dimensions[0]}x{dimensions[1]}")
314
- return clean_url, dimensions[0], dimensions[1]
315
-
316
- # Fallback para PIL se necessário
317
- try:
318
- image = Image.open(io.BytesIO(response_full.content))
319
- width, height = image.size
320
- print(f"Dimensões obtidas via PIL para {clean_url}: {width}x{height}")
321
- return clean_url, width, height
322
- except Exception as pil_error:
323
- print(f"PIL também falhou para {clean_url}: {pil_error}")
324
-
325
- else:
326
- print(f"Erro HTTP {response.status_code} para {clean_url}")
327
-
328
- except Exception as e:
329
- print(f"Erro ao obter dimensões para {url}: {e}")
330
-
331
- print(f"Não foi possível obter dimensões para {url}")
332
- return url, None, None
333
-
334
-
335
- async def enrich_images_with_dimensions_optimized(images: list) -> list:
336
- """
337
- Versão otimizada para obter dimensões das imagens
338
- """
339
- if not images:
340
- return []
341
-
342
- # Configurações otimizadas para velocidade
343
- connector = httpx.AsyncClient(
344
- timeout=httpx.Timeout(3.0), # Timeout bem baixo
345
- limits=httpx.Limits(
346
- max_keepalive_connections=20,
347
- max_connections=30,
348
- keepalive_expiry=5.0
349
- )
350
- # http2=True removido para evitar dependência extra
351
- )
352
-
353
- # Semáforo para controlar concorrência (aumentado para 15 para processar mais rápido)
354
- semaphore = asyncio.Semaphore(15)
355
-
356
- async def process_image_with_semaphore(image_data):
357
- async with semaphore:
358
- url, width, height = await get_image_dimensions_fast(connector, image_data["url"])
359
- return {
360
- "url": url,
361
- "width": width,
362
- "height": height
363
- }
364
-
365
- try:
366
- # Processa todas as imagens em paralelo
367
- tasks = [process_image_with_semaphore(img) for img in images]
368
- results = await asyncio.gather(*tasks, return_exceptions=True)
369
-
370
- # Filtra apenas resultados válidos
371
- valid_images = []
372
- for result in results:
373
- if not isinstance(result, Exception):
374
- # Adiciona informação se a URL foi limpa (para debug)
375
- if 'wikimedia.org' in result['url'] and '/thumb/' not in result['url']:
376
- result['cleaned_wikimedia_url'] = True
377
- valid_images.append(result)
378
-
379
- return valid_images
380
-
381
- finally:
382
- await connector.aclose()