Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,19 +1,21 @@
|
|
| 1 |
from fastapi import FastAPI, Request, Query
|
| 2 |
-
from fastapi.responses import JSONResponse, RedirectResponse
|
| 3 |
-
from fastapi.staticfiles import StaticFiles
|
| 4 |
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
-
from datetime import datetime, timedelta
|
| 6 |
import requests
|
| 7 |
from bs4 import BeautifulSoup
|
| 8 |
import os
|
| 9 |
import logging
|
| 10 |
import time
|
| 11 |
-
from typing import
|
| 12 |
|
| 13 |
-
#
|
| 14 |
-
app = FastAPI(title="StreamFlix Addon", version="1.
|
| 15 |
|
| 16 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
app.add_middleware(
|
| 18 |
CORSMiddleware,
|
| 19 |
allow_origins=["*"],
|
|
@@ -22,70 +24,38 @@ app.add_middleware(
|
|
| 22 |
allow_headers=["*"],
|
| 23 |
)
|
| 24 |
|
| 25 |
-
#
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
# Constants
|
| 29 |
-
BASE_URL = os.getenv("BASE_URL", "")
|
| 30 |
-
TMDB_API_KEY = os.getenv("TMDB_API_KEY", "")
|
| 31 |
TMDB_API_URL = "https://api.themoviedb.org/3"
|
| 32 |
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"
|
| 33 |
REQUEST_TIMEOUT = 15
|
| 34 |
MAX_RETRIES = 2
|
| 35 |
|
| 36 |
-
#
|
| 37 |
-
CATALOG_PAGE_SIZE = 20
|
| 38 |
-
RECENT_RELEASES_LIMIT = 20
|
| 39 |
-
RECENT_MOVIES_DAYS = 30
|
| 40 |
-
RECENT_TV_DAYS = 14
|
| 41 |
-
TMDB_DISCOVER_CACHE_TIME = 3600
|
| 42 |
-
|
| 43 |
-
# Configure logging
|
| 44 |
-
logging.basicConfig(level=logging.INFO)
|
| 45 |
-
logger = logging.getLogger(__name__)
|
| 46 |
-
|
| 47 |
-
# Manifest with all features
|
| 48 |
MANIFEST = {
|
| 49 |
"id": "community.streamflix.hf",
|
| 50 |
-
"version": "1.
|
| 51 |
"catalogs": [
|
| 52 |
{
|
| 53 |
"type": "movie",
|
| 54 |
"id": "streamflix_movies",
|
| 55 |
"name": "StreamFlix - Filmes",
|
| 56 |
-
"extra": [
|
| 57 |
-
{"name": "search", "isRequired": False},
|
| 58 |
-
{"name": "genre", "isRequired": False}
|
| 59 |
-
]
|
| 60 |
},
|
| 61 |
{
|
| 62 |
"type": "series",
|
| 63 |
"id": "streamflix_series",
|
| 64 |
"name": "StreamFlix - Séries",
|
| 65 |
-
"extra": [
|
| 66 |
-
{"name": "search", "isRequired": False},
|
| 67 |
-
{"name": "genre", "isRequired": False}
|
| 68 |
-
]
|
| 69 |
-
},
|
| 70 |
-
{
|
| 71 |
-
"type": "movie",
|
| 72 |
-
"id": "streamflix_recent_movies",
|
| 73 |
-
"name": "Lançamentos Recentes - Filmes",
|
| 74 |
-
"extra": []
|
| 75 |
-
},
|
| 76 |
-
{
|
| 77 |
-
"type": "series",
|
| 78 |
-
"id": "streamflix_recent_series",
|
| 79 |
-
"name": "Lançamentos Recentes - Séries",
|
| 80 |
-
"extra": []
|
| 81 |
}
|
| 82 |
],
|
| 83 |
"resources": ["catalog", "stream", "meta"],
|
| 84 |
"types": ["movie", "series"],
|
| 85 |
-
"name": "StreamFlix
|
| 86 |
-
"description": "
|
| 87 |
-
"logo": "https://
|
| 88 |
-
"background": "https://
|
| 89 |
"idPrefixes": ["tt", "tmdb"],
|
| 90 |
"behaviorHints": {"adult": False}
|
| 91 |
}
|
|
@@ -96,6 +66,7 @@ class VOD:
|
|
| 96 |
self.session = requests.Session()
|
| 97 |
self.session.headers.update({"User-Agent": USER_AGENT})
|
| 98 |
self.cache = {}
|
|
|
|
| 99 |
|
| 100 |
def _request(self, url, method="get", data=None, referer=None, retry=0):
|
| 101 |
headers = {"User-Agent": USER_AGENT}
|
|
@@ -206,6 +177,7 @@ class VOD:
|
|
| 206 |
if not options_data or not options_data.get('data') or not options_data['data'].get('options'):
|
| 207 |
return None
|
| 208 |
|
|
|
|
| 209 |
selected_option = None
|
| 210 |
for option in options_data['data']['options']:
|
| 211 |
title = option.get('title', '').lower()
|
|
@@ -251,6 +223,7 @@ class VOD:
|
|
| 251 |
if not player_items:
|
| 252 |
return None
|
| 253 |
|
|
|
|
| 254 |
selected_player = None
|
| 255 |
for player in player_items:
|
| 256 |
title = player.get_text(strip=True).lower()
|
|
@@ -281,167 +254,70 @@ class VOD:
|
|
| 281 |
logger.error(f"Error in movie: {e}", exc_info=True)
|
| 282 |
return None
|
| 283 |
|
| 284 |
-
|
| 285 |
-
def __init__(self):
|
| 286 |
-
self.cache = {}
|
| 287 |
-
self.genre_map = {}
|
| 288 |
-
self.last_genre_update = 0
|
| 289 |
-
|
| 290 |
-
async def initialize_genres(self):
|
| 291 |
-
if time.time() - self.last_genre_update < 86400:
|
| 292 |
-
return
|
| 293 |
-
|
| 294 |
-
try:
|
| 295 |
-
movie_url = f"{TMDB_API_URL}/genre/movie/list"
|
| 296 |
-
movie_params = {"api_key": TMDB_API_KEY, "language": "pt-BR"}
|
| 297 |
-
movie_resp = requests.get(movie_url, params=movie_params)
|
| 298 |
-
movie_genres = {g['id']: g['name'] for g in movie_resp.json().get('genres', [])}
|
| 299 |
-
|
| 300 |
-
tv_url = f"{TMDB_API_URL}/genre/tv/list"
|
| 301 |
-
tv_params = {"api_key": TMDB_API_KEY, "language": "pt-BR"}
|
| 302 |
-
tv_resp = requests.get(tv_url, params=tv_params)
|
| 303 |
-
tv_genres = {g['id']: g['name'] for g in tv_resp.json().get('genres', [])}
|
| 304 |
-
|
| 305 |
-
self.genre_map = {
|
| 306 |
-
'movie': movie_genres,
|
| 307 |
-
'tv': tv_genres
|
| 308 |
-
}
|
| 309 |
-
self.last_genre_update = time.time()
|
| 310 |
-
except Exception as e:
|
| 311 |
-
logger.error(f"Erro ao carregar gêneros: {e}")
|
| 312 |
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
return self.cache[cache_key]
|
| 318 |
-
|
| 319 |
-
await self.initialize_genres()
|
| 320 |
|
| 321 |
-
|
|
|
|
|
|
|
| 322 |
params = {
|
| 323 |
"api_key": TMDB_API_KEY,
|
| 324 |
-
"
|
| 325 |
"page": page,
|
| 326 |
-
"
|
| 327 |
-
"include_adult": False,
|
| 328 |
-
"region": "BR"
|
| 329 |
}
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
url = f"{TMDB_API_URL}/search/{media_type}"
|
| 333 |
-
params["query"] = search
|
| 334 |
-
else:
|
| 335 |
-
url = f"{TMDB_API_URL}/discover/{media_type}"
|
| 336 |
-
if genre and genre.isdigit():
|
| 337 |
-
params["with_genres"] = genre
|
| 338 |
-
|
| 339 |
-
if media_type == "movie":
|
| 340 |
-
params["primary_release_date.lte"] = datetime.now().strftime("%Y-%m-%d")
|
| 341 |
-
params["vote_count.gte"] = 100
|
| 342 |
-
else:
|
| 343 |
-
params["first_air_date.lte"] = datetime.now().strftime("%Y-%m-%d")
|
| 344 |
-
params["vote_count.gte"] = 50
|
| 345 |
-
|
| 346 |
-
response = requests.get(url, params=params, timeout=10)
|
| 347 |
-
response.raise_for_status()
|
| 348 |
-
data = response.json()
|
| 349 |
-
|
| 350 |
-
metas = []
|
| 351 |
-
for item in data.get("results", []):
|
| 352 |
-
if not item.get("poster_path"):
|
| 353 |
-
continue
|
| 354 |
-
|
| 355 |
-
meta = self._build_meta(media_type, item)
|
| 356 |
-
metas.append(meta)
|
| 357 |
-
|
| 358 |
-
result = {"metas": metas, "hasMore": page < data.get("total_pages", 1)}
|
| 359 |
-
self.cache[cache_key] = result
|
| 360 |
-
return result
|
| 361 |
-
|
| 362 |
-
except Exception as e:
|
| 363 |
-
logger.error(f"Erro ao buscar catálogo: {e}")
|
| 364 |
-
return {"metas": [], "hasMore": False}
|
| 365 |
-
|
| 366 |
-
async def get_recent_releases(self, media_type: str) -> Dict:
|
| 367 |
-
cache_key = f"recent_{media_type}"
|
| 368 |
-
|
| 369 |
-
if cache_key in self.cache:
|
| 370 |
-
return self.cache[cache_key]
|
| 371 |
-
|
| 372 |
-
try:
|
| 373 |
-
await self.initialize_genres()
|
| 374 |
-
|
| 375 |
params = {
|
| 376 |
"api_key": TMDB_API_KEY,
|
| 377 |
-
"language": "pt-BR",
|
| 378 |
"sort_by": "popularity.desc",
|
| 379 |
-
"
|
| 380 |
-
"
|
| 381 |
-
"vote_count.gte": 50 if media_type == "tv" else 100
|
| 382 |
}
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
metas = []
|
| 400 |
-
for item in data.get("results", [])[:RECENT_RELEASES_LIMIT]:
|
| 401 |
-
if not item.get("poster_path"):
|
| 402 |
-
continue
|
| 403 |
-
|
| 404 |
-
meta = self._build_meta(media_type, item)
|
| 405 |
-
metas.append(meta)
|
| 406 |
-
|
| 407 |
-
result = {"metas": metas}
|
| 408 |
-
self.cache[cache_key] = result
|
| 409 |
-
return result
|
| 410 |
-
|
| 411 |
-
except Exception as e:
|
| 412 |
-
logger.error(f"Erro ao buscar lançamentos recentes: {e}")
|
| 413 |
-
return {"metas": []}
|
| 414 |
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
|
| 420 |
-
|
| 421 |
-
|
|
|
|
| 422 |
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
"imdbRating": round(item.get("vote_average", 0), 1),
|
| 433 |
-
"popularity": round(item.get("popularity", 0), 1)
|
| 434 |
-
}
|
| 435 |
-
|
| 436 |
-
# Initialize services
|
| 437 |
-
vod_api = VOD()
|
| 438 |
-
catalog_manager = CatalogManager()
|
| 439 |
|
| 440 |
-
#
|
| 441 |
@app.get("/")
|
| 442 |
-
async def
|
| 443 |
-
|
| 444 |
-
return HTMLResponse(content=f.read(), status_code=200)
|
| 445 |
|
| 446 |
@app.get("/manifest.json")
|
| 447 |
async def get_manifest():
|
|
@@ -452,46 +328,55 @@ async def get_catalog(
|
|
| 452 |
type: str,
|
| 453 |
id: str,
|
| 454 |
request: Request,
|
| 455 |
-
skip: int = Query(0
|
| 456 |
-
search: str = Query(None
|
| 457 |
-
genre: str = Query(None, alias="genre")
|
| 458 |
):
|
| 459 |
-
logger.info(f"Catalog request
|
| 460 |
-
|
| 461 |
-
if id in ["streamflix_recent_movies", "streamflix_recent_series"]:
|
| 462 |
-
media_type = "movie" if id == "streamflix_recent_movies" else "tv"
|
| 463 |
-
recent_releases = await catalog_manager.get_recent_releases(media_type)
|
| 464 |
-
return JSONResponse(recent_releases)
|
| 465 |
|
| 466 |
if id not in ["streamflix_movies", "streamflix_series"]:
|
| 467 |
return JSONResponse({"metas": []})
|
| 468 |
|
|
|
|
| 469 |
media_type = "movie" if id == "streamflix_movies" else "tv"
|
| 470 |
-
|
|
|
|
|
|
|
|
|
|
| 471 |
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
|
| 479 |
-
return JSONResponse(
|
| 480 |
|
| 481 |
@app.get("/meta/{type}/{id}.json")
|
| 482 |
async def get_meta(type: str, id: str):
|
| 483 |
try:
|
| 484 |
-
tmdb_id = id
|
| 485 |
media_type = "movie" if type == "movie" else "tv"
|
| 486 |
|
| 487 |
-
if not tmdb_id
|
| 488 |
imdb_id = id
|
| 489 |
-
elif tmdb_id
|
| 490 |
imdb_id = convert_tmdb_to_imdb(tmdb_id, media_type) if type == "movie" else ""
|
| 491 |
else:
|
| 492 |
raise ValueError("Invalid ID format")
|
| 493 |
|
| 494 |
-
if not tmdb_id
|
| 495 |
return JSONResponse({
|
| 496 |
"meta": {
|
| 497 |
"id": id,
|
|
@@ -506,28 +391,25 @@ async def get_meta(type: str, id: str):
|
|
| 506 |
params = {
|
| 507 |
"api_key": TMDB_API_KEY,
|
| 508 |
"language": "pt-BR",
|
| 509 |
-
"append_to_response": "external_ids
|
| 510 |
}
|
| 511 |
|
| 512 |
response = requests.get(url, params=params, timeout=REQUEST_TIMEOUT)
|
| 513 |
response.raise_for_status()
|
| 514 |
details = response.json()
|
| 515 |
|
| 516 |
-
genre_ids = [g["id"] for g in details.get("genres", [])]
|
| 517 |
-
genres = [catalog_manager.genre_map.get(media_type, {}).get(gid, "") for gid in genre_ids[:3]]
|
| 518 |
-
|
| 519 |
meta = {
|
| 520 |
"id": id,
|
| 521 |
"type": type,
|
| 522 |
"name": details.get("title") if media_type == "movie" else details.get("name"),
|
| 523 |
-
"genres": [
|
| 524 |
"description": details.get("overview", "Sem descrição disponível"),
|
| 525 |
"poster": f"https://image.tmdb.org/t/p/w500{details['poster_path']}" if details.get("poster_path") else MANIFEST["logo"],
|
| 526 |
"background": f"https://image.tmdb.org/t/p/original{details['backdrop_path']}" if details.get("backdrop_path") else MANIFEST["background"],
|
| 527 |
"releaseInfo": details.get("release_date", "")[:4] if media_type == "movie" else details.get("first_air_date", "")[:4],
|
| 528 |
"imdbRating": round(details.get("vote_average", 0), 1),
|
| 529 |
"director": ", ".join(
|
| 530 |
-
[crew["name"] for crew in details.get("
|
| 531 |
if crew.get("job") == "Director"
|
| 532 |
][:2]) if media_type == "movie" else None
|
| 533 |
}
|
|
@@ -565,7 +447,7 @@ async def get_stream(type: str, id: str):
|
|
| 565 |
try:
|
| 566 |
if type == "movie":
|
| 567 |
if id.startswith("tmdb:"):
|
| 568 |
-
tmdb_id = id
|
| 569 |
imdb_id = convert_tmdb_to_imdb(tmdb_id, "movie")
|
| 570 |
if not imdb_id:
|
| 571 |
logger.warning(f"Failed to convert TMDB to IMDB: {id}")
|
|
@@ -612,29 +494,4 @@ async def get_stream(type: str, id: str):
|
|
| 612 |
except Exception as e:
|
| 613 |
logger.error(f"Stream request failed: {e}")
|
| 614 |
|
| 615 |
-
return JSONResponse({"streams": []})
|
| 616 |
-
|
| 617 |
-
@app.get("/genres/{media_type}.json")
|
| 618 |
-
async def get_genres(media_type: str):
|
| 619 |
-
await catalog_manager.initialize_genres()
|
| 620 |
-
genres = catalog_manager.genre_map.get(media_type, {})
|
| 621 |
-
return JSONResponse({"genres": genres})
|
| 622 |
-
|
| 623 |
-
def convert_tmdb_to_imdb(tmdb_id, media_type="movie"):
|
| 624 |
-
cache_key = f"tmdb_to_imdb_{media_type}_{tmdb_id}"
|
| 625 |
-
if cache_key in vod_api.cache:
|
| 626 |
-
return vod_api.cache[cache_key]
|
| 627 |
-
|
| 628 |
-
try:
|
| 629 |
-
url = f"{TMDB_API_URL}/{media_type}/{tmdb_id}/external_ids"
|
| 630 |
-
params = {"api_key": TMDB_API_KEY}
|
| 631 |
-
|
| 632 |
-
response = requests.get(url, params=params, timeout=REQUEST_TIMEOUT)
|
| 633 |
-
response.raise_for_status()
|
| 634 |
-
data = response.json()
|
| 635 |
-
imdb_id = data.get("imdb_id", "")
|
| 636 |
-
vod_api.cache[cache_key] = imdb_id
|
| 637 |
-
return imdb_id
|
| 638 |
-
except Exception as e:
|
| 639 |
-
logger.error(f"TMDB conversion failed: {e}")
|
| 640 |
-
return ""
|
|
|
|
| 1 |
from fastapi import FastAPI, Request, Query
|
| 2 |
+
from fastapi.responses import JSONResponse, RedirectResponse
|
|
|
|
| 3 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
| 4 |
import requests
|
| 5 |
from bs4 import BeautifulSoup
|
| 6 |
import os
|
| 7 |
import logging
|
| 8 |
import time
|
| 9 |
+
from typing import Optional
|
| 10 |
|
| 11 |
+
# Configuração do app FastAPI
|
| 12 |
+
app = FastAPI(title="StreamFlix Addon", version="1.0.0")
|
| 13 |
|
| 14 |
+
# Configuração do logging
|
| 15 |
+
logging.basicConfig(level=logging.INFO)
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
# Middleware CORS
|
| 19 |
app.add_middleware(
|
| 20 |
CORSMiddleware,
|
| 21 |
allow_origins=["*"],
|
|
|
|
| 24 |
allow_headers=["*"],
|
| 25 |
)
|
| 26 |
|
| 27 |
+
# Configurações (usar variáveis de ambiente no Hugging Face)
|
| 28 |
+
BASE_URL = os.getenv("BASE_URL", "https://superflixapi.pw")
|
| 29 |
+
TMDB_API_KEY = os.getenv("TMDB_API_KEY", "84ec49f90ebcffe1e7c61efca1c0a606")
|
|
|
|
|
|
|
|
|
|
| 30 |
TMDB_API_URL = "https://api.themoviedb.org/3"
|
| 31 |
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"
|
| 32 |
REQUEST_TIMEOUT = 15
|
| 33 |
MAX_RETRIES = 2
|
| 34 |
|
| 35 |
+
# Manifesto do Addon (atualizado para Hugging Face)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
MANIFEST = {
|
| 37 |
"id": "community.streamflix.hf",
|
| 38 |
+
"version": "1.0.0",
|
| 39 |
"catalogs": [
|
| 40 |
{
|
| 41 |
"type": "movie",
|
| 42 |
"id": "streamflix_movies",
|
| 43 |
"name": "StreamFlix - Filmes",
|
| 44 |
+
"extra": [{"name": "search", "isRequired": False}]
|
|
|
|
|
|
|
|
|
|
| 45 |
},
|
| 46 |
{
|
| 47 |
"type": "series",
|
| 48 |
"id": "streamflix_series",
|
| 49 |
"name": "StreamFlix - Séries",
|
| 50 |
+
"extra": [{"name": "search", "isRequired": False}]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
],
|
| 53 |
"resources": ["catalog", "stream", "meta"],
|
| 54 |
"types": ["movie", "series"],
|
| 55 |
+
"name": "StreamFlix (HF)",
|
| 56 |
+
"description": "Addon StreamFlix hospedado no Hugging Face",
|
| 57 |
+
"logo": "https://huggingface.co/spaces/username/streamflix-addon/raw/main/logo.png",
|
| 58 |
+
"background": "https://huggingface.co/spaces/username/streamflix-addon/raw/main/background.jpg",
|
| 59 |
"idPrefixes": ["tt", "tmdb"],
|
| 60 |
"behaviorHints": {"adult": False}
|
| 61 |
}
|
|
|
|
| 66 |
self.session = requests.Session()
|
| 67 |
self.session.headers.update({"User-Agent": USER_AGENT})
|
| 68 |
self.cache = {}
|
| 69 |
+
self.cache_timeout = 3600 # 1 hora
|
| 70 |
|
| 71 |
def _request(self, url, method="get", data=None, referer=None, retry=0):
|
| 72 |
headers = {"User-Agent": USER_AGENT}
|
|
|
|
| 177 |
if not options_data or not options_data.get('data') or not options_data['data'].get('options'):
|
| 178 |
return None
|
| 179 |
|
| 180 |
+
# Find Portuguese option
|
| 181 |
selected_option = None
|
| 182 |
for option in options_data['data']['options']:
|
| 183 |
title = option.get('title', '').lower()
|
|
|
|
| 223 |
if not player_items:
|
| 224 |
return None
|
| 225 |
|
| 226 |
+
# Find Portuguese player
|
| 227 |
selected_player = None
|
| 228 |
for player in player_items:
|
| 229 |
title = player.get_text(strip=True).lower()
|
|
|
|
| 254 |
logger.error(f"Error in movie: {e}", exc_info=True)
|
| 255 |
return None
|
| 256 |
|
| 257 |
+
vod_api = VOD()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
|
| 259 |
+
def get_tmdb_media(media_type, page=1, search_query=None):
|
| 260 |
+
cache_key = f"tmdb_{media_type}_{page}_{search_query}"
|
| 261 |
+
if cache_key in vod_api.cache:
|
| 262 |
+
return vod_api.cache[cache_key]
|
|
|
|
|
|
|
|
|
|
| 263 |
|
| 264 |
+
try:
|
| 265 |
+
if search_query:
|
| 266 |
+
url = f"{TMDB_API_URL}/search/{media_type}"
|
| 267 |
params = {
|
| 268 |
"api_key": TMDB_API_KEY,
|
| 269 |
+
"query": search_query,
|
| 270 |
"page": page,
|
| 271 |
+
"language": "pt-BR"
|
|
|
|
|
|
|
| 272 |
}
|
| 273 |
+
else:
|
| 274 |
+
url = f"{TMDB_API_URL}/discover/{media_type}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
params = {
|
| 276 |
"api_key": TMDB_API_KEY,
|
|
|
|
| 277 |
"sort_by": "popularity.desc",
|
| 278 |
+
"page": page,
|
| 279 |
+
"language": "pt-BR"
|
|
|
|
| 280 |
}
|
| 281 |
+
|
| 282 |
+
response = requests.get(url, params=params, timeout=REQUEST_TIMEOUT)
|
| 283 |
+
response.raise_for_status()
|
| 284 |
+
data = response.json()
|
| 285 |
+
vod_api.cache[cache_key] = data
|
| 286 |
+
return data
|
| 287 |
+
except Exception as e:
|
| 288 |
+
logger.error(f"TMDB request failed: {e}")
|
| 289 |
+
return None
|
| 290 |
+
|
| 291 |
+
def extract_tmdb_id(id_str):
|
| 292 |
+
if id_str.startswith("tmdb:"):
|
| 293 |
+
return id_str.split(":")[1]
|
| 294 |
+
if id_str.isdigit():
|
| 295 |
+
return id_str
|
| 296 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
|
| 298 |
+
def convert_tmdb_to_imdb(tmdb_id, media_type="movie"):
|
| 299 |
+
cache_key = f"tmdb_to_imdb_{media_type}_{tmdb_id}"
|
| 300 |
+
if cache_key in vod_api.cache:
|
| 301 |
+
return vod_api.cache[cache_key]
|
| 302 |
|
| 303 |
+
try:
|
| 304 |
+
url = f"{TMDB_API_URL}/{media_type}/{tmdb_id}/external_ids"
|
| 305 |
+
params = {"api_key": TMDB_API_KEY}
|
| 306 |
|
| 307 |
+
response = requests.get(url, params=params, timeout=REQUEST_TIMEOUT)
|
| 308 |
+
response.raise_for_status()
|
| 309 |
+
data = response.json()
|
| 310 |
+
imdb_id = data.get("imdb_id", "")
|
| 311 |
+
vod_api.cache[cache_key] = imdb_id
|
| 312 |
+
return imdb_id
|
| 313 |
+
except Exception as e:
|
| 314 |
+
logger.error(f"TMDB conversion failed: {e}")
|
| 315 |
+
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
|
| 317 |
+
# Rotas do FastAPI
|
| 318 |
@app.get("/")
|
| 319 |
+
async def root():
|
| 320 |
+
return RedirectResponse(url="/manifest.json")
|
|
|
|
| 321 |
|
| 322 |
@app.get("/manifest.json")
|
| 323 |
async def get_manifest():
|
|
|
|
| 328 |
type: str,
|
| 329 |
id: str,
|
| 330 |
request: Request,
|
| 331 |
+
skip: int = Query(0),
|
| 332 |
+
search: str = Query(None)
|
|
|
|
| 333 |
):
|
| 334 |
+
logger.info(f"Catalog request: {type}/{id}?skip={skip}&search={search}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
|
| 336 |
if id not in ["streamflix_movies", "streamflix_series"]:
|
| 337 |
return JSONResponse({"metas": []})
|
| 338 |
|
| 339 |
+
page = (skip // 20) + 1
|
| 340 |
media_type = "movie" if id == "streamflix_movies" else "tv"
|
| 341 |
+
tmdb_data = get_tmdb_media(media_type, page, search)
|
| 342 |
+
|
| 343 |
+
if not tmdb_data or "results" not in tmdb_data:
|
| 344 |
+
return JSONResponse({"metas": []})
|
| 345 |
|
| 346 |
+
metas = []
|
| 347 |
+
for item in tmdb_data["results"]:
|
| 348 |
+
if not item.get("poster_path"):
|
| 349 |
+
continue
|
| 350 |
+
|
| 351 |
+
meta = {
|
| 352 |
+
"id": f"tmdb:{item['id']}",
|
| 353 |
+
"type": media_type,
|
| 354 |
+
"name": item.get("title") if media_type == "movie" else item.get("name"),
|
| 355 |
+
"genres": [genre["name"] for genre in item.get("genres", [])][:3],
|
| 356 |
+
"description": item.get("overview", "Sem descrição disponível")[:300],
|
| 357 |
+
"poster": f"https://image.tmdb.org/t/p/w500{item['poster_path']}",
|
| 358 |
+
"background": f"https://image.tmdb.org/t/p/original{item['backdrop_path']}" if item.get("backdrop_path") else MANIFEST["background"],
|
| 359 |
+
"releaseInfo": item.get("release_date", "")[:4] if media_type == "movie" else item.get("first_air_date", "")[:4],
|
| 360 |
+
"imdbRating": round(item.get("vote_average", 0), 1)
|
| 361 |
+
}
|
| 362 |
+
metas.append(meta)
|
| 363 |
|
| 364 |
+
return JSONResponse({"metas": metas})
|
| 365 |
|
| 366 |
@app.get("/meta/{type}/{id}.json")
|
| 367 |
async def get_meta(type: str, id: str):
|
| 368 |
try:
|
| 369 |
+
tmdb_id = extract_tmdb_id(id)
|
| 370 |
media_type = "movie" if type == "movie" else "tv"
|
| 371 |
|
| 372 |
+
if not tmdb_id and type == "movie" and id.startswith("tt"):
|
| 373 |
imdb_id = id
|
| 374 |
+
elif tmdb_id:
|
| 375 |
imdb_id = convert_tmdb_to_imdb(tmdb_id, media_type) if type == "movie" else ""
|
| 376 |
else:
|
| 377 |
raise ValueError("Invalid ID format")
|
| 378 |
|
| 379 |
+
if not tmdb_id and type != "movie":
|
| 380 |
return JSONResponse({
|
| 381 |
"meta": {
|
| 382 |
"id": id,
|
|
|
|
| 391 |
params = {
|
| 392 |
"api_key": TMDB_API_KEY,
|
| 393 |
"language": "pt-BR",
|
| 394 |
+
"append_to_response": "external_ids"
|
| 395 |
}
|
| 396 |
|
| 397 |
response = requests.get(url, params=params, timeout=REQUEST_TIMEOUT)
|
| 398 |
response.raise_for_status()
|
| 399 |
details = response.json()
|
| 400 |
|
|
|
|
|
|
|
|
|
|
| 401 |
meta = {
|
| 402 |
"id": id,
|
| 403 |
"type": type,
|
| 404 |
"name": details.get("title") if media_type == "movie" else details.get("name"),
|
| 405 |
+
"genres": [genre["name"] for genre in details.get("genres", [])],
|
| 406 |
"description": details.get("overview", "Sem descrição disponível"),
|
| 407 |
"poster": f"https://image.tmdb.org/t/p/w500{details['poster_path']}" if details.get("poster_path") else MANIFEST["logo"],
|
| 408 |
"background": f"https://image.tmdb.org/t/p/original{details['backdrop_path']}" if details.get("backdrop_path") else MANIFEST["background"],
|
| 409 |
"releaseInfo": details.get("release_date", "")[:4] if media_type == "movie" else details.get("first_air_date", "")[:4],
|
| 410 |
"imdbRating": round(details.get("vote_average", 0), 1),
|
| 411 |
"director": ", ".join(
|
| 412 |
+
[crew["name"] for crew in details.get("crew", [])
|
| 413 |
if crew.get("job") == "Director"
|
| 414 |
][:2]) if media_type == "movie" else None
|
| 415 |
}
|
|
|
|
| 447 |
try:
|
| 448 |
if type == "movie":
|
| 449 |
if id.startswith("tmdb:"):
|
| 450 |
+
tmdb_id = extract_tmdb_id(id)
|
| 451 |
imdb_id = convert_tmdb_to_imdb(tmdb_id, "movie")
|
| 452 |
if not imdb_id:
|
| 453 |
logger.warning(f"Failed to convert TMDB to IMDB: {id}")
|
|
|
|
| 494 |
except Exception as e:
|
| 495 |
logger.error(f"Stream request failed: {e}")
|
| 496 |
|
| 497 |
+
return JSONResponse({"streams": []})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|