Bsweb1 commited on
Commit
e06ee4f
·
verified ·
1 Parent(s): 5cdb673

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +203 -0
app.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ 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/")
56
+ async def proxy_image(
57
+ request: Request,
58
+ url: str,
59
+ w: int = None,
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:
138
+ w = int(h * img.width / img.height)
139
+
140
+ # Redimensionar se necessário
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():
203
+ return app