Bsweb1 commited on
Commit
5480f29
·
verified ·
1 Parent(s): 8dade3e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +115 -113
app.py CHANGED
@@ -4,52 +4,71 @@ from fastapi import FastAPI, HTTPException, Request
4
  from fastapi.responses import StreamingResponse, RedirectResponse
5
  from fastapi.middleware.cors import CORSMiddleware
6
  import httpx
7
- from urllib.parse import urlparse, quote
8
- from datetime import datetime, timedelta
9
  from PIL import Image
10
  import io
11
- import hashlib
12
 
13
- # Configuração básica
14
  app = FastAPI(
15
- title="Stremio Image Proxy",
16
- description="Proxy de imagens para projetos Stremio hospedado no Hugging Face",
17
- version="1.0.0"
18
  )
19
 
20
- # Configurar logging
21
- logging.basicConfig(level=logging.INFO)
22
- logger = logging.getLogger("stremio-proxy")
 
 
 
23
 
24
- # Configuração CORS para permitir acesso de qualquer origem
25
  app.add_middleware(
26
  CORSMiddleware,
27
  allow_origins=["*"],
28
  allow_methods=["GET"],
29
  allow_headers=["*"],
 
30
  )
31
 
32
  # Configurações otimizadas para Hugging Face Spaces
33
- CACHE_TIME = 86400 # 1 dia em segundos
34
- TIMEOUT = 8.0 # Timeout menor para o ambiente do Spaces
35
- MAX_SIZE = 3 * 1024 * 1024 # 3MB (limite recomendado para Spaces)
36
- MAX_DIMENSION = 2000 # Dimensão máxima para redimensionamento
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  @app.get("/")
39
  async def home():
40
- """Página inicial com instruções"""
41
  return {
42
- "message": "Bem-vindo ao Proxy de Imagens para Stremio!",
43
- "usage": "GET /proxy-image/?url=URL_DA_IMAGEM",
 
44
  "parameters": {
45
- "url": "URL da imagem original (obrigatório)",
46
  "w": "Largura desejada (opcional)",
47
  "h": "Altura desejada (opcional)",
48
- "q": "Qualidade da imagem (1-100, opcional)"
49
  },
50
- "example": "/proxy-image/?url=https://example.com/poster.jpg&w=300&q=85",
51
- "huggingface_space": "https://huggingface.co/spaces",
52
- "stremio": "https://www.stremio.com/"
53
  }
54
 
55
  @app.get("/proxy-image/")
@@ -60,78 +79,63 @@ async def proxy_image(
60
  h: int = None,
61
  q: int = 85
62
  ):
63
- """Endpoint principal do proxy de imagens"""
64
- logger.info(f"Requisição para: {url}")
65
-
66
- # Validar parâmetros
67
- if not url:
68
- raise HTTPException(status_code=400, detail="Parâmetro 'url' é obrigatório")
69
-
70
- if q and (q < 1 or q > 100):
71
- raise HTTPException(status_code=400, detail="Qualidade deve estar entre 1 e 100")
72
-
73
- # Validar e normalizar URL
74
- parsed_url = urlparse(url)
75
- if not parsed_url.scheme or not parsed_url.netloc:
76
- raise HTTPException(status_code=400, detail="URL inválida")
77
-
78
- if parsed_url.scheme not in ("http", "https"):
79
- raise HTTPException(status_code=400, detail="Protocolo não suportado")
80
-
81
- # Headers para evitar bloqueio
82
- headers = {
83
- "User-Agent": "Stremio-Image-Proxy/1.0 (compatible; Stremio/4.0; +https://stremio.com)",
84
- "Accept": "image/webp,image/apng,image/*,*/*;q=0.8",
85
- "Referer": f"{parsed_url.scheme}://{parsed_url.netloc}/"
86
- }
87
-
88
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  # Buscar a imagem
90
- async with httpx.AsyncClient() as client:
91
  response = await client.get(
92
- url,
93
  headers=headers,
94
- timeout=TIMEOUT,
95
- follow_redirects=True
96
  )
97
 
 
98
  if response.status_code != 200:
99
- logger.error(f"Erro ao buscar imagem: {url} - Status: {response.status_code}")
100
- raise HTTPException(
101
- status_code=response.status_code,
102
- detail=f"Erro ao buscar imagem: {response.status_code}"
103
- )
104
 
105
  content_type = response.headers.get("Content-Type", "")
106
  if not content_type.startswith("image/"):
107
- logger.error(f"Conteúdo não é imagem: {content_type}")
108
- raise HTTPException(
109
- status_code=400,
110
- detail=f"Tipo de conteúdo não suportado: {content_type}"
111
- )
112
-
113
- # Processar imagem se necessário
114
- image_data = response.content
115
-
116
- # Verificar tamanho máximo
117
- if len(image_data) > MAX_SIZE:
118
- logger.warning(f"Imagem muito grande: {len(image_data)} bytes")
119
- raise HTTPException(
120
- status_code=400,
121
- detail=f"Imagem excede o tamanho máximo de {MAX_SIZE} bytes"
122
- )
123
 
124
- # Se foram solicitadas dimensões ou qualidade
125
  if w or h or q != 85:
126
  try:
127
- with Image.open(io.BytesIO(image_data)) as img:
128
  # Limitar dimensões máximas
129
  if w and w > MAX_DIMENSION:
130
  w = MAX_DIMENSION
131
  if h and h > MAX_DIMENSION:
132
  h = MAX_DIMENSION
133
 
134
- # Preservar proporções se apenas uma dimensão for fornecida
135
  if w and not h:
136
  h = int(w * img.height / img.width)
137
  elif h and not w:
@@ -141,62 +145,60 @@ async def proxy_image(
141
  if w or h:
142
  img = img.resize((w or img.width, h or img.height))
143
 
144
- # Converter para JPEG para compactação (a menos que seja PNG transparente)
145
  output_format = "JPEG"
146
  if img.format == "PNG" and img.mode == "RGBA":
147
  output_format = "PNG"
148
 
149
  output = io.BytesIO()
150
- img.save(
151
- output,
152
- format=output_format,
153
- quality=q,
154
- optimize=True
155
- )
156
- image_data = output.getvalue()
157
  content_type = f"image/{output_format.lower()}"
158
  except Exception as img_error:
159
- logger.error(f"Erro ao processar imagem: {str(img_error)}")
160
- # Se falhar, retorna a imagem original
161
- pass
162
 
163
- logger.info(f"Imagem processada: {len(image_data)} bytes, tipo: {content_type}")
 
 
 
 
 
 
164
 
165
- # Retornar resposta
166
  return StreamingResponse(
167
- io.BytesIO(image_data),
168
  media_type=content_type,
169
- headers={
170
- "Cache-Control": f"public, max-age={CACHE_TIME}",
171
- "Content-Length": str(len(image_data)),
172
- "Access-Control-Allow-Origin": "*",
173
- "X-Image-Original-Size": str(len(response.content)),
174
- "X-Image-Processed-Size": str(len(image_data))
175
- }
176
  )
177
 
 
 
 
 
178
  except httpx.TimeoutException:
179
- logger.error(f"Timeout ao buscar imagem: {url}")
180
- raise HTTPException(
181
- status_code=504,
182
- detail="Tempo de requisição excedido"
183
- )
184
  except httpx.RequestError as e:
185
- logger.error(f"Erro de conexão: {str(e)}")
186
- raise HTTPException(
187
- status_code=502,
188
- detail=f"Erro de conexão: {str(e)}"
189
- )
 
 
190
  except Exception as e:
191
- logger.error(f"Erro interno: {str(e)}")
192
- raise HTTPException(
193
- status_code=500,
194
- detail=f"Erro interno: {str(e)}"
195
- )
196
 
197
  @app.get("/favicon.ico")
198
  async def favicon():
199
- return RedirectResponse(url="https://www.stremio.com/favicon.ico")
 
 
 
 
200
 
201
  # Função para compatibilidade com Hugging Face Spaces
202
  def get_app():
 
4
  from fastapi.responses import StreamingResponse, RedirectResponse
5
  from fastapi.middleware.cors import CORSMiddleware
6
  import httpx
7
+ from urllib.parse import urlparse, unquote
 
8
  from PIL import Image
9
  import io
10
+ import re
11
 
 
12
  app = FastAPI(
13
+ title="Universal Image Proxy",
14
+ description="Proxy de imagens para qualquer aplicação com tratamento robusto de erros",
15
+ version="2.1.0"
16
  )
17
 
18
+ # Configuração de logs
19
+ logging.basicConfig(
20
+ level=logging.INFO,
21
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
22
+ )
23
+ logger = logging.getLogger("image-proxy")
24
 
25
+ # CORS completo para acesso universal
26
  app.add_middleware(
27
  CORSMiddleware,
28
  allow_origins=["*"],
29
  allow_methods=["GET"],
30
  allow_headers=["*"],
31
+ expose_headers=["*"]
32
  )
33
 
34
  # Configurações otimizadas para Hugging Face Spaces
35
+ TIMEOUT = 10.0
36
+ MAX_SIZE = 3 * 1024 * 1024 # 3MB
37
+ MAX_DIMENSION = 2000
38
+ MAX_REDIRECTS = 5
39
+
40
+ # Lista de domínios problemáticos que precisam de tratamento especial
41
+ PROBLEMATIC_DOMAINS = [
42
+ "static.wikia.nocookie.net",
43
+ "tmsimg.com",
44
+ "espncdn.com"
45
+ ]
46
+
47
+ def clean_url(url: str) -> str:
48
+ """Remove caracteres problemáticos e decodifica URL"""
49
+ # Decodificar URL codificada
50
+ cleaned = unquote(url)
51
+
52
+ # Remover parâmetros de cache para alguns domínios
53
+ if any(domain in cleaned for domain in PROBLEMATIC_DOMAINS):
54
+ cleaned = re.sub(r'(\?|&)(cb|revision|__cf_chl_[^=]+)=[^&]+', '', cleaned)
55
+ cleaned = re.sub(r'\?.*', '', cleaned)
56
+
57
+ return cleaned
58
 
59
  @app.get("/")
60
  async def home():
61
+ """Página inicial com instruções simplificadas"""
62
  return {
63
+ "service": "Universal Image Proxy",
64
+ "status": "active",
65
+ "endpoint": "/proxy-image/?url=URL_DA_IMAGEM",
66
  "parameters": {
 
67
  "w": "Largura desejada (opcional)",
68
  "h": "Altura desejada (opcional)",
69
+ "q": "Qualidade (1-100, padrão 85)"
70
  },
71
+ "example": "/proxy-image/?url=https://example.com/photo.jpg&w=300&q=85"
 
 
72
  }
73
 
74
  @app.get("/proxy-image/")
 
79
  h: int = None,
80
  q: int = 85
81
  ):
82
+ """Endpoint principal do proxy de imagens com tratamento robusto de erros"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  try:
84
+ # Limpar e validar URL
85
+ cleaned_url = clean_url(url)
86
+ parsed = urlparse(cleaned_url)
87
+
88
+ if not parsed.scheme or not parsed.netloc or parsed.scheme not in ("http", "https"):
89
+ raise HTTPException(400, "URL inválida")
90
+
91
+ # Headers para evitar bloqueio
92
+ headers = {
93
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
94
+ "Accept": "image/*,*/*;q=0.8",
95
+ "Referer": f"{parsed.scheme}://{parsed.netloc}/"
96
+ }
97
+
98
  # Buscar a imagem
99
+ async with httpx.AsyncClient(follow_redirects=True, max_redirects=MAX_REDIRECTS) as client:
100
  response = await client.get(
101
+ cleaned_url,
102
  headers=headers,
103
+ timeout=TIMEOUT
 
104
  )
105
 
106
+ # Tratamento de erros HTTP
107
  if response.status_code != 200:
108
+ error_msg = f"Origem retornou erro {response.status_code}"
109
+ logger.warning(f"{error_msg} para: {cleaned_url}")
110
+ raise HTTPException(response.status_code, error_msg)
 
 
111
 
112
  content_type = response.headers.get("Content-Type", "")
113
  if not content_type.startswith("image/"):
114
+ error_msg = f"Tipo de conteúdo inválido: {content_type}"
115
+ logger.warning(f"{error_msg} - URL: {cleaned_url}")
116
+ raise HTTPException(400, error_msg)
117
+
118
+ # Verificar tamanho
119
+ if len(response.content) > MAX_SIZE:
120
+ error_msg = f"Imagem muito grande ({len(response.content)} bytes)"
121
+ logger.warning(f"{error_msg} - URL: {cleaned_url}")
122
+ raise HTTPException(400, error_msg)
123
+
124
+ # Processar imagem
125
+ img_bytes = response.content
126
+ content_type = response.headers.get("Content-Type", "image/jpeg")
 
 
 
127
 
128
+ # Aplicar transformações se solicitado
129
  if w or h or q != 85:
130
  try:
131
+ with Image.open(io.BytesIO(img_bytes)) as img:
132
  # Limitar dimensões máximas
133
  if w and w > MAX_DIMENSION:
134
  w = MAX_DIMENSION
135
  if h and h > MAX_DIMENSION:
136
  h = MAX_DIMENSION
137
 
138
+ # Preservar proporções
139
  if w and not h:
140
  h = int(w * img.height / img.width)
141
  elif h and not w:
 
145
  if w or h:
146
  img = img.resize((w or img.width, h or img.height))
147
 
148
+ # Converter para JPEG (exceto PNGs com transparência)
149
  output_format = "JPEG"
150
  if img.format == "PNG" and img.mode == "RGBA":
151
  output_format = "PNG"
152
 
153
  output = io.BytesIO()
154
+ img.save(output, format=output_format, quality=q, optimize=True)
155
+ img_bytes = output.getvalue()
 
 
 
 
 
156
  content_type = f"image/{output_format.lower()}"
157
  except Exception as img_error:
158
+ logger.error(f"Erro no processamento: {str(img_error)} - URL: {cleaned_url}")
159
+ # Manter a imagem original se falhar no processamento
 
160
 
161
+ # Headers de resposta
162
+ headers = {
163
+ "Cache-Control": "public, max-age=86400",
164
+ "Access-Control-Allow-Origin": "*",
165
+ "X-Image-Proxy": "processed" if (w or h or q != 85) else "original",
166
+ "X-Original-URL": cleaned_url
167
+ }
168
 
 
169
  return StreamingResponse(
170
+ io.BytesIO(img_bytes),
171
  media_type=content_type,
172
+ headers=headers
 
 
 
 
 
 
173
  )
174
 
175
+ except httpx.HTTPStatusError as e:
176
+ logger.error(f"Erro HTTP {e.response.status_code} para: {cleaned_url}")
177
+ raise HTTPException(e.response.status_code, f"Erro na origem: {e.response.status_code}")
178
+
179
  except httpx.TimeoutException:
180
+ logger.error(f"Timeout para: {cleaned_url}")
181
+ raise HTTPException(504, "Tempo de requisição excedido")
182
+
 
 
183
  except httpx.RequestError as e:
184
+ logger.error(f"Erro de conexão: {str(e)} - URL: {cleaned_url}")
185
+ raise HTTPException(502, f"Erro de conexão: {str(e)}")
186
+
187
+ except HTTPException:
188
+ # Re-lançar exceções HTTP já tratadas
189
+ raise
190
+
191
  except Exception as e:
192
+ logger.exception(f"Erro inesperado: {str(e)} - URL: {cleaned_url}")
193
+ raise HTTPException(500, f"Erro interno: {str(e)}")
 
 
 
194
 
195
  @app.get("/favicon.ico")
196
  async def favicon():
197
+ return RedirectResponse(
198
+ "https://www.stremio.com/favicon.ico",
199
+ status_code=301,
200
+ headers={"Cache-Control": "public, max-age=31536000"}
201
+ )
202
 
203
  # Função para compatibilidade com Hugging Face Spaces
204
  def get_app():