mirror of
https://github.com/Viren070/MediaFusion.git
synced 2025-12-01 23:21:11 +01:00
Add support for Live TV & fix several issues
This commit is contained in:
+72
-12
@@ -4,7 +4,7 @@ from typing import Literal
|
|||||||
|
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
from apscheduler.triggers.cron import CronTrigger
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
from fastapi import FastAPI, Request, Response, Depends, HTTPException
|
from fastapi import FastAPI, Request, Response, Depends, HTTPException, status
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import RedirectResponse, FileResponse, StreamingResponse
|
from fastapi.responses import RedirectResponse, FileResponse, StreamingResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
@@ -19,7 +19,7 @@ from streaming_providers.seedr.api import router as seedr_router
|
|||||||
from streaming_providers.seedr.utils import get_direct_link_from_seedr
|
from streaming_providers.seedr.utils import get_direct_link_from_seedr
|
||||||
from streaming_providers.debridlink.api import router as debridlink_router
|
from streaming_providers.debridlink.api import router as debridlink_router
|
||||||
from streaming_providers.debridlink.utils import get_direct_link_from_debridlink
|
from streaming_providers.debridlink.utils import get_direct_link_from_debridlink
|
||||||
from utils import crypto, torrent, poster
|
from utils import crypto, torrent, poster, validation_helper
|
||||||
from utils.const import CATALOG_ID_DATA, CATALOG_NAME_DATA
|
from utils.const import CATALOG_ID_DATA, CATALOG_NAME_DATA
|
||||||
from scrappers import tamil_blasters, tamilmv
|
from scrappers import tamil_blasters, tamilmv
|
||||||
|
|
||||||
@@ -174,18 +174,47 @@ async def get_manifest(
|
|||||||
response_model_by_alias=False,
|
response_model_by_alias=False,
|
||||||
tags=["catalog"],
|
tags=["catalog"],
|
||||||
)
|
)
|
||||||
|
@app.get(
|
||||||
|
"/{secret_str}/catalog/{catalog_type}/{catalog_id}/genre={genre}.json",
|
||||||
|
response_model=schemas.Metas,
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
response_model_by_alias=False,
|
||||||
|
tags=["catalog"],
|
||||||
|
)
|
||||||
|
@app.get(
|
||||||
|
"/catalog/{catalog_type}/{catalog_id}/genre={genre}.json",
|
||||||
|
response_model=schemas.Metas,
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
response_model_by_alias=False,
|
||||||
|
tags=["catalog"],
|
||||||
|
)
|
||||||
async def get_catalog(
|
async def get_catalog(
|
||||||
response: Response,
|
response: Response,
|
||||||
catalog_type: Literal["movie", "series"],
|
catalog_type: Literal["movie", "series", "tv"],
|
||||||
catalog_id: str,
|
catalog_id: str,
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
|
genre: str = None,
|
||||||
):
|
):
|
||||||
response.headers.update(headers)
|
response.headers.update(headers)
|
||||||
|
if genre and "&" in genre:
|
||||||
|
genre, skip = genre.split("&")
|
||||||
|
skip = skip.split("=")[1] if "=" in skip else "0"
|
||||||
|
skip = int(skip) if skip and skip.isdigit() else 0
|
||||||
|
|
||||||
metas = schemas.Metas()
|
metas = schemas.Metas()
|
||||||
metas.metas.extend(await crud.get_meta_list(catalog_type, catalog_id, skip))
|
if catalog_type == "tv":
|
||||||
|
metas.metas.extend(await crud.get_tv_meta_list(genre, skip))
|
||||||
|
else:
|
||||||
|
metas.metas.extend(await crud.get_meta_list(catalog_type, catalog_id, skip))
|
||||||
return metas
|
return metas
|
||||||
|
|
||||||
|
|
||||||
|
@app.get(
|
||||||
|
"/{secret_str}/catalog/{catalog_type}/{catalog_id}/search={search_query}.json",
|
||||||
|
tags=["search"],
|
||||||
|
response_model=schemas.Metas,
|
||||||
|
response_model_exclude_none=True,
|
||||||
|
)
|
||||||
@app.get(
|
@app.get(
|
||||||
"/catalog/{catalog_type}/{catalog_id}/search={search_query}.json",
|
"/catalog/{catalog_type}/{catalog_id}/search={search_query}.json",
|
||||||
tags=["search"],
|
tags=["search"],
|
||||||
@@ -194,8 +223,12 @@ async def get_catalog(
|
|||||||
)
|
)
|
||||||
async def search_movie(
|
async def search_movie(
|
||||||
response: Response,
|
response: Response,
|
||||||
catalog_type: Literal["movie", "series"],
|
catalog_type: Literal["movie", "series", "tv"],
|
||||||
catalog_id: Literal["mediafusion_search_movies", "mediafusion_search_series"],
|
catalog_id: Literal[
|
||||||
|
"mediafusion_search_movies",
|
||||||
|
"mediafusion_search_series",
|
||||||
|
"mediafusion_search_tv",
|
||||||
|
],
|
||||||
search_query: str,
|
search_query: str,
|
||||||
):
|
):
|
||||||
response.headers.update(headers)
|
response.headers.update(headers)
|
||||||
@@ -219,13 +252,15 @@ async def search_movie(
|
|||||||
response_model_by_alias=False,
|
response_model_by_alias=False,
|
||||||
)
|
)
|
||||||
async def get_meta(
|
async def get_meta(
|
||||||
catalog_type: Literal["movie", "series"], meta_id: str, response: Response
|
catalog_type: Literal["movie", "series", "tv"], meta_id: str, response: Response
|
||||||
):
|
):
|
||||||
response.headers.update(headers)
|
response.headers.update(headers)
|
||||||
if catalog_type == "movie":
|
if catalog_type == "movie":
|
||||||
data = await crud.get_movie_meta(meta_id)
|
data = await crud.get_movie_meta(meta_id)
|
||||||
else:
|
elif catalog_type == "series":
|
||||||
data = await crud.get_series_meta(meta_id)
|
data = await crud.get_series_meta(meta_id)
|
||||||
|
else:
|
||||||
|
data = await crud.get_tv_meta(meta_id)
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
raise HTTPException(status_code=404, detail="Meta ID not found.")
|
raise HTTPException(status_code=404, detail="Meta ID not found.")
|
||||||
@@ -258,7 +293,7 @@ async def get_meta(
|
|||||||
tags=["stream"],
|
tags=["stream"],
|
||||||
)
|
)
|
||||||
async def get_streams(
|
async def get_streams(
|
||||||
catalog_type: Literal["movie", "series"],
|
catalog_type: Literal["movie", "series", "tv"],
|
||||||
video_id: str,
|
video_id: str,
|
||||||
response: Response,
|
response: Response,
|
||||||
secret_str: str = None,
|
secret_str: str = None,
|
||||||
@@ -270,10 +305,12 @@ async def get_streams(
|
|||||||
|
|
||||||
if catalog_type == "movie":
|
if catalog_type == "movie":
|
||||||
fetched_streams = await crud.get_movie_streams(user_data, secret_str, video_id)
|
fetched_streams = await crud.get_movie_streams(user_data, secret_str, video_id)
|
||||||
else:
|
elif catalog_type == "series":
|
||||||
fetched_streams = await crud.get_series_streams(
|
fetched_streams = await crud.get_series_streams(
|
||||||
user_data, secret_str, video_id, season, episode
|
user_data, secret_str, video_id, season, episode
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
fetched_streams = await crud.get_tv_streams(video_id)
|
||||||
|
|
||||||
return {"streams": fetched_streams}
|
return {"streams": fetched_streams}
|
||||||
|
|
||||||
@@ -328,12 +365,16 @@ async def streaming_provider_endpoint(
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/poster/{catalog_type}/{mediafusion_id}.jpg", tags=["poster"])
|
@app.get("/poster/{catalog_type}/{mediafusion_id}.jpg", tags=["poster"])
|
||||||
async def get_poster(catalog_type: Literal["movie", "series"], mediafusion_id: str):
|
async def get_poster(
|
||||||
|
catalog_type: Literal["movie", "series", "tv"], mediafusion_id: str
|
||||||
|
):
|
||||||
# Query the MediaFusion data
|
# Query the MediaFusion data
|
||||||
if catalog_type == "movie":
|
if catalog_type == "movie":
|
||||||
mediafusion_data = await crud.get_movie_data_by_id(mediafusion_id)
|
mediafusion_data = await crud.get_movie_data_by_id(mediafusion_id)
|
||||||
else:
|
elif catalog_type == "series":
|
||||||
mediafusion_data = await crud.get_series_data_by_id(mediafusion_id)
|
mediafusion_data = await crud.get_series_data_by_id(mediafusion_id)
|
||||||
|
else:
|
||||||
|
mediafusion_data = await crud.get_tv_data_by_id(mediafusion_id)
|
||||||
|
|
||||||
if not mediafusion_data:
|
if not mediafusion_data:
|
||||||
raise HTTPException(status_code=404, detail="MediaFusion ID not found.")
|
raise HTTPException(status_code=404, detail="MediaFusion ID not found.")
|
||||||
@@ -351,6 +392,25 @@ async def get_poster(catalog_type: Literal["movie", "series"], mediafusion_id: s
|
|||||||
raise HTTPException(status_code=404, detail="Failed to create poster.")
|
raise HTTPException(status_code=404, detail="Failed to create poster.")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/tv-metadata", status_code=status.HTTP_201_CREATED, tags=["tv"])
|
||||||
|
async def add_tv_metadata(metadata: schemas.TVMetaData):
|
||||||
|
try:
|
||||||
|
metadata.streams = validation_helper.validate_tv_metadata(metadata)
|
||||||
|
except validation_helper.ValidationError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
tv_channel_id, is_new = await crud.save_tv_channel_metadata(metadata)
|
||||||
|
|
||||||
|
if is_new:
|
||||||
|
return {
|
||||||
|
"status": f"Metadata with ID {tv_channel_id} has been created and is pending approval. Thanks for your contribution."
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": f"Tv Channel with ID {tv_channel_id} Streams has been updated. Thanks for your contribution."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
app.include_router(seedr_router, prefix="/seedr", tags=["seedr"])
|
app.include_router(seedr_router, prefix="/seedr", tags=["seedr"])
|
||||||
app.include_router(realdebrid_router, prefix="/realdebrid", tags=["realdebrid"])
|
app.include_router(realdebrid_router, prefix="/realdebrid", tags=["realdebrid"])
|
||||||
app.include_router(debridlink_router, prefix="/debridlink", tags=["debridlink"])
|
app.include_router(debridlink_router, prefix="/debridlink", tags=["debridlink"])
|
||||||
|
|||||||
+24
-2
@@ -17,6 +17,21 @@ Accept: application/json
|
|||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
|
GET http://127.0.0.1:8000/catalog/series/tamil_series.json
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
GET http://127.0.0.1:8000/catalog/tv/live_tv.json
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
GET http://127.0.0.1:8000/stream/tv/mf7c537827a3.json
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
GET http://127.0.0.1:8000/catalog/movie/tamil_hdrip/skip=10.json
|
GET http://127.0.0.1:8000/catalog/movie/tamil_hdrip/skip=10.json
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
|
|
||||||
@@ -37,7 +52,7 @@ Accept: application/json
|
|||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
GET http://127.0.0.1:8000/{{secret_str}}/meta/series/mf39644457048721.json
|
GET http://127.0.0.1:8000/{{secret_str}}/meta/series/mf279172080279251.json
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
|
|
||||||
###
|
###
|
||||||
@@ -53,4 +68,11 @@ Accept: application/json
|
|||||||
###
|
###
|
||||||
|
|
||||||
GET http://127.0.0.1:8000/meta/movie/tt11468258.json
|
GET http://127.0.0.1:8000/meta/movie/tt11468258.json
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
GET http://127.0.0.1:8000/stream/tv/mf1358a3be25.json
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"projectName": "mediafusion",
|
"projectName": "mediafusion",
|
||||||
"lastCommit": "65a6781"
|
"lastCommit": "8d1b505"
|
||||||
}
|
}
|
||||||
+139
-3
@@ -1,3 +1,4 @@
|
|||||||
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
@@ -5,7 +6,7 @@ from uuid import uuid4
|
|||||||
from beanie import WriteRules
|
from beanie import WriteRules
|
||||||
from beanie.operators import In
|
from beanie.operators import In
|
||||||
|
|
||||||
from db import schemas
|
from db import schemas, models
|
||||||
from db.config import settings
|
from db.config import settings
|
||||||
from db.models import (
|
from db.models import (
|
||||||
MediaFusionMovieMetaData,
|
MediaFusionMovieMetaData,
|
||||||
@@ -13,9 +14,15 @@ from db.models import (
|
|||||||
Streams,
|
Streams,
|
||||||
Season,
|
Season,
|
||||||
Episode,
|
Episode,
|
||||||
|
MediaFusionTVMetaData,
|
||||||
)
|
)
|
||||||
from db.schemas import Stream, MetaIdProjection
|
from db.schemas import Stream, MetaIdProjection
|
||||||
from utils.parser import parse_stream_data, get_catalogs, search_imdb
|
from utils.parser import (
|
||||||
|
parse_stream_data,
|
||||||
|
get_catalogs,
|
||||||
|
search_imdb,
|
||||||
|
parse_tv_stream_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_meta_list(
|
async def get_meta_list(
|
||||||
@@ -43,6 +50,29 @@ async def get_meta_list(
|
|||||||
return meta_list
|
return meta_list
|
||||||
|
|
||||||
|
|
||||||
|
async def get_tv_meta_list(
|
||||||
|
genre: Optional[str] = None, skip: int = 0, limit: int = 25
|
||||||
|
) -> list[schemas.Meta]:
|
||||||
|
query = MediaFusionTVMetaData.find(
|
||||||
|
MediaFusionTVMetaData.is_approved == True, fetch_links=True
|
||||||
|
)
|
||||||
|
if genre:
|
||||||
|
query = query.find(In(MediaFusionTVMetaData.genres, [genre]))
|
||||||
|
|
||||||
|
tv_meta_list = (
|
||||||
|
await query.skip(skip)
|
||||||
|
.limit(limit)
|
||||||
|
.sort(-MediaFusionTVMetaData.streams.created_at)
|
||||||
|
.project(schemas.Meta)
|
||||||
|
.to_list()
|
||||||
|
)
|
||||||
|
|
||||||
|
for meta in tv_meta_list:
|
||||||
|
meta.poster = f"{settings.host_url}/poster/tv/{meta.id}.jpg"
|
||||||
|
|
||||||
|
return tv_meta_list
|
||||||
|
|
||||||
|
|
||||||
async def get_movie_data_by_id(
|
async def get_movie_data_by_id(
|
||||||
movie_id: str, fetch_links: bool = False
|
movie_id: str, fetch_links: bool = False
|
||||||
) -> Optional[MediaFusionMovieMetaData]:
|
) -> Optional[MediaFusionMovieMetaData]:
|
||||||
@@ -59,6 +89,13 @@ async def get_series_data_by_id(
|
|||||||
return series_data
|
return series_data
|
||||||
|
|
||||||
|
|
||||||
|
async def get_tv_data_by_id(
|
||||||
|
tv_id: str, fetch_links: bool = False
|
||||||
|
) -> Optional[MediaFusionTVMetaData]:
|
||||||
|
tv_data = await MediaFusionTVMetaData.get(tv_id, fetch_links=fetch_links)
|
||||||
|
return tv_data
|
||||||
|
|
||||||
|
|
||||||
async def get_movie_streams(user_data, secret_str: str, video_id: str) -> list[Stream]:
|
async def get_movie_streams(user_data, secret_str: str, video_id: str) -> list[Stream]:
|
||||||
movie_data = await get_movie_data_by_id(video_id, True)
|
movie_data = await get_movie_data_by_id(video_id, True)
|
||||||
if not movie_data:
|
if not movie_data:
|
||||||
@@ -83,6 +120,14 @@ async def get_series_streams(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_tv_streams(video_id: str) -> list[Stream]:
|
||||||
|
tv_data = await get_tv_data_by_id(video_id, True)
|
||||||
|
if not tv_data:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return parse_tv_stream_data(tv_data.streams)
|
||||||
|
|
||||||
|
|
||||||
async def get_movie_meta(meta_id: str):
|
async def get_movie_meta(meta_id: str):
|
||||||
movie_data = await get_movie_data_by_id(meta_id)
|
movie_data = await get_movie_data_by_id(meta_id)
|
||||||
|
|
||||||
@@ -122,9 +167,23 @@ async def get_series_meta(meta_id: str):
|
|||||||
stream: Streams
|
stream: Streams
|
||||||
if stream.season: # Ensure the stream has season data
|
if stream.season: # Ensure the stream has season data
|
||||||
for episode in stream.season.episodes:
|
for episode in stream.season.episodes:
|
||||||
|
stream_id = (
|
||||||
|
f"{meta_id}:{stream.season.season_number}:{episode.episode_number}"
|
||||||
|
)
|
||||||
|
# check if the stream is already in the list
|
||||||
|
if next(
|
||||||
|
(
|
||||||
|
video
|
||||||
|
for video in metadata["meta"]["videos"]
|
||||||
|
if video["id"] == stream_id
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
metadata["meta"]["videos"].append(
|
metadata["meta"]["videos"].append(
|
||||||
{
|
{
|
||||||
"id": f"{meta_id}:{stream.season.season_number}:{episode.episode_number}",
|
"id": stream_id,
|
||||||
"name": f"S{stream.season.season_number} EP{episode.episode_number}",
|
"name": f"S{stream.season.season_number} EP{episode.episode_number}",
|
||||||
"season": stream.season.season_number,
|
"season": stream.season.season_number,
|
||||||
"episode": episode.episode_number,
|
"episode": episode.episode_number,
|
||||||
@@ -135,6 +194,17 @@ async def get_series_meta(meta_id: str):
|
|||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
|
async def get_tv_meta(meta_id: str):
|
||||||
|
tv_data = await get_tv_data_by_id(meta_id)
|
||||||
|
|
||||||
|
if not tv_data:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"meta": {"_id": meta_id, **tv_data.model_dump()},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def save_movie_metadata(metadata: dict):
|
async def save_movie_metadata(metadata: dict):
|
||||||
# Try to get the existing movie
|
# Try to get the existing movie
|
||||||
existing_movie = await MediaFusionMovieMetaData.find_one(
|
existing_movie = await MediaFusionMovieMetaData.find_one(
|
||||||
@@ -313,6 +383,8 @@ async def save_series_metadata(metadata: dict):
|
|||||||
async def process_search_query(search_query: str, catalog_type: str) -> dict:
|
async def process_search_query(search_query: str, catalog_type: str) -> dict:
|
||||||
if catalog_type == "movie":
|
if catalog_type == "movie":
|
||||||
meta_class = MediaFusionMovieMetaData
|
meta_class = MediaFusionMovieMetaData
|
||||||
|
elif catalog_type == "tv":
|
||||||
|
meta_class = MediaFusionTVMetaData
|
||||||
else:
|
else:
|
||||||
meta_class = MediaFusionSeriesMetaData
|
meta_class = MediaFusionSeriesMetaData
|
||||||
|
|
||||||
@@ -332,6 +404,8 @@ async def process_search_query(search_query: str, catalog_type: str) -> dict:
|
|||||||
# Use the appropriate function to get the meta data
|
# Use the appropriate function to get the meta data
|
||||||
if catalog_type == "movie":
|
if catalog_type == "movie":
|
||||||
meta = await get_movie_meta(item.id)
|
meta = await get_movie_meta(item.id)
|
||||||
|
elif catalog_type == "tv":
|
||||||
|
meta = await get_tv_meta(item.id)
|
||||||
else:
|
else:
|
||||||
meta = await get_series_meta(item.id)
|
meta = await get_series_meta(item.id)
|
||||||
|
|
||||||
@@ -346,3 +420,65 @@ async def process_search_query(search_query: str, catalog_type: str) -> dict:
|
|||||||
async def get_stream_by_info_hash(info_hash: str) -> Streams | None:
|
async def get_stream_by_info_hash(info_hash: str) -> Streams | None:
|
||||||
stream = await Streams.get(info_hash)
|
stream = await Streams.get(info_hash)
|
||||||
return stream
|
return stream
|
||||||
|
|
||||||
|
|
||||||
|
async def save_tv_channel_metadata(tv_metadata: schemas.TVMetaData) -> tuple[str, bool]:
|
||||||
|
# Try to get the existing TV channel
|
||||||
|
channel_id = "mf" + hashlib.sha256(tv_metadata.title.encode()).hexdigest()[:10]
|
||||||
|
logging.info(f"Processing TV channel id {channel_id}")
|
||||||
|
existing_channel = await models.MediaFusionTVMetaData.get(channel_id)
|
||||||
|
|
||||||
|
# Map the TVStreams schema to the TVStreams model
|
||||||
|
streams_models = []
|
||||||
|
for stream in tv_metadata.streams:
|
||||||
|
streams_models.append(
|
||||||
|
models.TVStreams(
|
||||||
|
url=stream.url,
|
||||||
|
name=stream.name,
|
||||||
|
behaviorHints=stream.behaviorHints.model_dump(exclude_none=True),
|
||||||
|
ytId=stream.ytId,
|
||||||
|
source=stream.source,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing_channel:
|
||||||
|
# Check for existing streams and update if necessary
|
||||||
|
await existing_channel.fetch_all_links()
|
||||||
|
for new_stream in streams_models:
|
||||||
|
matching_stream = next(
|
||||||
|
(
|
||||||
|
s
|
||||||
|
for s in existing_channel.streams
|
||||||
|
if (s.url and s.url == new_stream.url)
|
||||||
|
or (s.ytId and s.ytId == new_stream.ytId)
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if not matching_stream:
|
||||||
|
existing_channel.streams.append(new_stream)
|
||||||
|
await existing_channel.save(link_rule=WriteRules.WRITE)
|
||||||
|
logging.info(f"Updated TV channel {existing_channel.title}")
|
||||||
|
is_new = False
|
||||||
|
else:
|
||||||
|
tv_metadata.genres.extend([tv_metadata.country, tv_metadata.tv_language])
|
||||||
|
genres = list(set(tv_metadata.genres))
|
||||||
|
|
||||||
|
# If the channel doesn't exist, create a new one
|
||||||
|
tv_channel_data = models.MediaFusionTVMetaData(
|
||||||
|
id=channel_id,
|
||||||
|
title=tv_metadata.title,
|
||||||
|
poster=tv_metadata.poster,
|
||||||
|
background=tv_metadata.background,
|
||||||
|
streams=streams_models,
|
||||||
|
type="tv",
|
||||||
|
country=tv_metadata.country,
|
||||||
|
tv_language=tv_metadata.tv_language,
|
||||||
|
logo=tv_metadata.logo,
|
||||||
|
genres=genres,
|
||||||
|
is_approved=False,
|
||||||
|
)
|
||||||
|
await tv_channel_data.insert(link_rule=WriteRules.WRITE)
|
||||||
|
logging.info(f"Added TV channel {tv_channel_data.title}")
|
||||||
|
is_new = True
|
||||||
|
|
||||||
|
return channel_id, is_new
|
||||||
|
|||||||
+17
-2
@@ -2,7 +2,13 @@ import motor.motor_asyncio
|
|||||||
from beanie import init_beanie
|
from beanie import init_beanie
|
||||||
|
|
||||||
from db.config import settings
|
from db.config import settings
|
||||||
from db.models import MediaFusionSeriesMetaData, MediaFusionMovieMetaData, Streams
|
from db.models import (
|
||||||
|
MediaFusionSeriesMetaData,
|
||||||
|
MediaFusionMovieMetaData,
|
||||||
|
Streams,
|
||||||
|
TVStreams,
|
||||||
|
MediaFusionTVMetaData,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def init():
|
async def init():
|
||||||
@@ -10,4 +16,13 @@ async def init():
|
|||||||
client = motor.motor_asyncio.AsyncIOMotorClient(settings.mongo_uri)
|
client = motor.motor_asyncio.AsyncIOMotorClient(settings.mongo_uri)
|
||||||
|
|
||||||
# Init beanie with the Product document class
|
# Init beanie with the Product document class
|
||||||
await init_beanie(database=client.mediafusion, document_models=[MediaFusionMovieMetaData, MediaFusionSeriesMetaData, Streams])
|
await init_beanie(
|
||||||
|
database=client.mediafusion,
|
||||||
|
document_models=[
|
||||||
|
MediaFusionMovieMetaData,
|
||||||
|
MediaFusionSeriesMetaData,
|
||||||
|
Streams,
|
||||||
|
TVStreams,
|
||||||
|
MediaFusionTVMetaData,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|||||||
+24
-4
@@ -1,9 +1,10 @@
|
|||||||
|
import hashlib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional, Any
|
||||||
|
|
||||||
import pymongo
|
import pymongo
|
||||||
from beanie import Document, Link
|
from beanie import Document, Link
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, computed_field
|
||||||
from pymongo import IndexModel, ASCENDING
|
from pymongo import IndexModel, ASCENDING
|
||||||
|
|
||||||
|
|
||||||
@@ -50,12 +51,21 @@ class Streams(Document):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class TVStreams(Document):
|
||||||
|
name: str
|
||||||
|
url: str | None = None
|
||||||
|
ytId: str | None = None
|
||||||
|
source: str
|
||||||
|
behaviorHints: dict[str, Any] | None = None
|
||||||
|
created_at: datetime = Field(default_factory=datetime.now)
|
||||||
|
|
||||||
|
|
||||||
class MediaFusionMetaData(Document):
|
class MediaFusionMetaData(Document):
|
||||||
id: str
|
id: str
|
||||||
title: str
|
title: str
|
||||||
year: Optional[int]
|
year: Optional[int] = None
|
||||||
poster: str
|
poster: str
|
||||||
background: str
|
background: Optional[str] = None
|
||||||
streams: list[Link[Streams]]
|
streams: list[Link[Streams]]
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
@@ -73,3 +83,13 @@ class MediaFusionMovieMetaData(MediaFusionMetaData):
|
|||||||
|
|
||||||
class MediaFusionSeriesMetaData(MediaFusionMetaData):
|
class MediaFusionSeriesMetaData(MediaFusionMetaData):
|
||||||
type: str = "series"
|
type: str = "series"
|
||||||
|
|
||||||
|
|
||||||
|
class MediaFusionTVMetaData(MediaFusionMetaData):
|
||||||
|
type: str = "tv"
|
||||||
|
country: str
|
||||||
|
tv_language: str
|
||||||
|
logo: Optional[str] = None
|
||||||
|
genres: Optional[list[str]] = None
|
||||||
|
is_approved: bool = False
|
||||||
|
streams: list[Link[TVStreams]]
|
||||||
|
|||||||
+44
-4
@@ -1,6 +1,6 @@
|
|||||||
from typing import Optional, Any, Literal
|
from typing import Optional, Literal
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, model_validator
|
||||||
|
|
||||||
from utils.const import CATALOG_ID_DATA
|
from utils.const import CATALOG_ID_DATA
|
||||||
|
|
||||||
@@ -16,8 +16,12 @@ class Meta(BaseModel):
|
|||||||
name: str = Field(alias="title")
|
name: str = Field(alias="title")
|
||||||
type: str = Field(default="movie")
|
type: str = Field(default="movie")
|
||||||
poster: str
|
poster: str
|
||||||
background: str
|
background: str | None = None
|
||||||
videos: list | None = None
|
videos: list | None = None
|
||||||
|
country: str | None = None
|
||||||
|
language: str | None = Field(None, alias="tv_language")
|
||||||
|
logo: Optional[str] = None
|
||||||
|
genres: Optional[list[str]] = None
|
||||||
|
|
||||||
|
|
||||||
class MetaItem(BaseModel):
|
class MetaItem(BaseModel):
|
||||||
@@ -28,13 +32,20 @@ class Metas(BaseModel):
|
|||||||
metas: list[Meta] = []
|
metas: list[Meta] = []
|
||||||
|
|
||||||
|
|
||||||
|
class StreamBehaviorHints(BaseModel):
|
||||||
|
notWebReady: Optional[bool] = None
|
||||||
|
bingeGroup: Optional[str] = None
|
||||||
|
proxyHeaders: Optional[dict[Literal["request", "response"], dict]] = None
|
||||||
|
|
||||||
|
|
||||||
class Stream(BaseModel):
|
class Stream(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
infoHash: str | None = None
|
infoHash: str | None = None
|
||||||
fileIdx: int | None = None
|
fileIdx: int | None = None
|
||||||
url: str | None = None
|
url: str | None = None
|
||||||
behaviorHints: dict[str, Any] | None = None
|
ytId: str | None = None
|
||||||
|
behaviorHints: StreamBehaviorHints | None = None
|
||||||
|
|
||||||
|
|
||||||
class Streams(BaseModel):
|
class Streams(BaseModel):
|
||||||
@@ -63,3 +74,32 @@ class AuthorizeData(BaseModel):
|
|||||||
|
|
||||||
class MetaIdProjection(BaseModel):
|
class MetaIdProjection(BaseModel):
|
||||||
id: str = Field(alias="_id")
|
id: str = Field(alias="_id")
|
||||||
|
|
||||||
|
|
||||||
|
class TVStreamsBehaviorHints(StreamBehaviorHints):
|
||||||
|
is_redirect: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class TVStreams(BaseModel):
|
||||||
|
name: str
|
||||||
|
url: str | None = None
|
||||||
|
ytId: str | None = None
|
||||||
|
source: str
|
||||||
|
behaviorHints: TVStreamsBehaviorHints | None = None
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_url_or_yt_id(self) -> "TVStreams":
|
||||||
|
if not self.url and not self.ytId:
|
||||||
|
raise ValueError("Either url or ytId must be present")
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class TVMetaData(BaseModel):
|
||||||
|
title: str
|
||||||
|
poster: str
|
||||||
|
background: Optional[str] = None
|
||||||
|
country: str
|
||||||
|
tv_language: str
|
||||||
|
logo: Optional[str] = None
|
||||||
|
genres: list[str] = []
|
||||||
|
streams: list[TVStreams]
|
||||||
|
|||||||
+49
-5
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"id": "mhdzumair.addons.mediafusion",
|
"id": "mhdzumair.addons.mediafusion",
|
||||||
"version": "3.4.5",
|
"version": "3.5.0",
|
||||||
"name": "Media Fusion",
|
"name": "Media Fusion",
|
||||||
"contactEmail": "mhdzumair@gmail",
|
"contactEmail": "mhdzumair@gmail",
|
||||||
"description": "Watch Tamil, Hindi, Malayalam, Kannada, English, and dubbed movies & series with the Media Fusion add-on. Source: https://github.com/mhdzumair/MediaFusion",
|
"description": "Watch Tamil, Hindi, Malayalam, Kannada, English, and dubbed movies, series & live TV with the Media Fusion add-on. Source: https://github.com/mhdzumair/MediaFusion",
|
||||||
"logo": "https://882b9915d0fe-mediafusion.baby-beamup.club/static/images/mediafusion_logo.png",
|
"logo": "https://882b9915d0fe-mediafusion.baby-beamup.club/static/images/mediafusion_logo.png",
|
||||||
"behaviorHints": {
|
"behaviorHints": {
|
||||||
"configurable": true,
|
"configurable": true,
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
"name": "stream",
|
"name": "stream",
|
||||||
"types": [
|
"types": [
|
||||||
"movie",
|
"movie",
|
||||||
"series"
|
"series",
|
||||||
|
"tv"
|
||||||
],
|
],
|
||||||
"idPrefixes": [
|
"idPrefixes": [
|
||||||
"tt",
|
"tt",
|
||||||
@@ -26,7 +27,8 @@
|
|||||||
"name": "meta",
|
"name": "meta",
|
||||||
"types": [
|
"types": [
|
||||||
"movie",
|
"movie",
|
||||||
"series"
|
"series",
|
||||||
|
"tv"
|
||||||
],
|
],
|
||||||
"idPrefixes": [
|
"idPrefixes": [
|
||||||
"tt",
|
"tt",
|
||||||
@@ -36,7 +38,8 @@
|
|||||||
],
|
],
|
||||||
"types": [
|
"types": [
|
||||||
"movie",
|
"movie",
|
||||||
"series"
|
"series",
|
||||||
|
"tv"
|
||||||
],
|
],
|
||||||
"catalogs": [
|
"catalogs": [
|
||||||
{
|
{
|
||||||
@@ -368,6 +371,47 @@
|
|||||||
"isRequired": false
|
"isRequired": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "live_tv",
|
||||||
|
"type": "tv",
|
||||||
|
"name": "Live TV",
|
||||||
|
"extra": [
|
||||||
|
{
|
||||||
|
"name": "skip",
|
||||||
|
"isRequired": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "genre",
|
||||||
|
"isRequired": false,
|
||||||
|
"options": [
|
||||||
|
"Tamil",
|
||||||
|
"Malayalam",
|
||||||
|
"English",
|
||||||
|
"Sports",
|
||||||
|
"Tamil Entertainment",
|
||||||
|
"India",
|
||||||
|
"Sri Lanka",
|
||||||
|
"Tamil Infotainment",
|
||||||
|
"Tamil Movies",
|
||||||
|
"Tamil Music",
|
||||||
|
"Tamil News",
|
||||||
|
"Tamil Kids",
|
||||||
|
"Hindi"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mediafusion_search_tv",
|
||||||
|
"type": "tv",
|
||||||
|
"name": "Live TV",
|
||||||
|
"extra": [
|
||||||
|
{
|
||||||
|
"name": "search",
|
||||||
|
"isRequired": true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from urllib.parse import urlparse, urljoin
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
format="%(levelname)s::%(asctime)s - %(message)s", level=logging.INFO
|
||||||
|
)
|
||||||
|
BASE_URL = "https://tamilultra.in"
|
||||||
|
MEDIAFUSION_URL = "http://127.0.0.1:8000"
|
||||||
|
|
||||||
|
|
||||||
|
async def scrape_tv_channels(page):
|
||||||
|
# Scrape channel metadata
|
||||||
|
channels_data = []
|
||||||
|
channel_elements = await page.query_selector_all("article.item.movies")
|
||||||
|
|
||||||
|
# First, store all channel information in a list
|
||||||
|
channel_info_list = []
|
||||||
|
for channel_element in channel_elements:
|
||||||
|
title_element = await channel_element.query_selector("h3 > a")
|
||||||
|
title = (
|
||||||
|
(await title_element.text_content())
|
||||||
|
.replace("\u2013", "-")
|
||||||
|
.split("-")[0]
|
||||||
|
.strip()
|
||||||
|
.title()
|
||||||
|
if title_element
|
||||||
|
else "No Title"
|
||||||
|
)
|
||||||
|
poster_element = await channel_element.query_selector(".poster > img")
|
||||||
|
poster_url = (
|
||||||
|
await poster_element.get_attribute("src")
|
||||||
|
if poster_element
|
||||||
|
else "No Poster URL"
|
||||||
|
)
|
||||||
|
stream_page_link_element = await channel_element.query_selector(".poster > a")
|
||||||
|
stream_page_url = (
|
||||||
|
await stream_page_link_element.get_attribute("href")
|
||||||
|
if stream_page_link_element
|
||||||
|
else "No Stream Page URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
channel_info_list.append((title, poster_url, stream_page_url))
|
||||||
|
|
||||||
|
# Then, navigate to each channel's stream page and capture the M3U8 URLs
|
||||||
|
for title, poster_url, stream_page_url in channel_info_list:
|
||||||
|
# Navigate to the stream page
|
||||||
|
await page.goto(stream_page_url)
|
||||||
|
|
||||||
|
m3u8_url_data = []
|
||||||
|
|
||||||
|
# Scrape genre tags
|
||||||
|
genre_elements = await page.query_selector_all(".sgeneros a[rel='tag']")
|
||||||
|
genres = [await genre.text_content() for genre in genre_elements]
|
||||||
|
genres = [genre.title() for genre in genres]
|
||||||
|
|
||||||
|
# Query for player option elements and click to load M3U8 URL
|
||||||
|
player_option_elements = await page.query_selector_all(
|
||||||
|
"#playeroptionsul > li.dooplay_player_option"
|
||||||
|
)
|
||||||
|
for player_option_element in player_option_elements:
|
||||||
|
# Click the player option element
|
||||||
|
await player_option_element.click()
|
||||||
|
# Wait for the iframe to load and get its 'src' attribute
|
||||||
|
iframe_element = await page.wait_for_selector("iframe.metaframe.rptss")
|
||||||
|
iframe_src = await iframe_element.get_attribute("src")
|
||||||
|
m3u8_url_part = iframe_src.replace("/player.php?", "")
|
||||||
|
# Check if iframe_src is a valid URL
|
||||||
|
parsed_src = urlparse(m3u8_url_part)
|
||||||
|
behavior_hints = {}
|
||||||
|
if parsed_src.scheme and parsed_src.netloc:
|
||||||
|
# If it's a valid URL, use it directly
|
||||||
|
m3u8_url = m3u8_url_part
|
||||||
|
if "jio.tamilultra.in" in m3u8_url:
|
||||||
|
behavior_hints = {
|
||||||
|
"notWebReady": True,
|
||||||
|
"proxyHeaders": {
|
||||||
|
"request": {
|
||||||
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
|
||||||
|
"Referer": BASE_URL + "/",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Otherwise, join with the BASE_URL
|
||||||
|
m3u8_url = urljoin(BASE_URL, m3u8_url_part)
|
||||||
|
behavior_hints = {
|
||||||
|
"is_redirect": True,
|
||||||
|
"proxyHeaders": {
|
||||||
|
"request": {
|
||||||
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
|
||||||
|
"Referer": urljoin(BASE_URL, iframe_src),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
m3u8_url_data.append((m3u8_url, behavior_hints))
|
||||||
|
|
||||||
|
channels_data.append(
|
||||||
|
{
|
||||||
|
"title": title.replace("Hd", "").strip(),
|
||||||
|
"poster": poster_url,
|
||||||
|
"genres": genres,
|
||||||
|
"country": "India", # Set default country
|
||||||
|
"tv_language": genres[0],
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"name": f"{title} - {index}",
|
||||||
|
"url": m3u8_url,
|
||||||
|
"source": "TamilUltra",
|
||||||
|
"behaviorHints": behavior_hints,
|
||||||
|
}
|
||||||
|
for index, (m3u8_url, behavior_hints) in enumerate(m3u8_url_data, 1)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logging.info("Scraped %s", title)
|
||||||
|
|
||||||
|
return channels_data
|
||||||
|
|
||||||
|
|
||||||
|
async def scrape_category(category_url, page):
|
||||||
|
# Navigate to the category page
|
||||||
|
await page.goto(category_url)
|
||||||
|
|
||||||
|
# Scrape channels from the current page
|
||||||
|
channels_data = await scrape_tv_channels(
|
||||||
|
page
|
||||||
|
) # Assuming scrape_tv_channels accepts a page argument
|
||||||
|
|
||||||
|
# Try to find a pagination control and collect all page URLs
|
||||||
|
pagination_links = await page.query_selector_all("div.pagination a.inactive")
|
||||||
|
page_urls = [category_url] # Include the first page
|
||||||
|
for link in pagination_links:
|
||||||
|
page_url = await link.get_attribute("href")
|
||||||
|
if page_url:
|
||||||
|
page_urls.append(page_url)
|
||||||
|
|
||||||
|
logging.info("found %d pages", len(page_urls))
|
||||||
|
|
||||||
|
# Iterate over each page URL and scrape channels
|
||||||
|
for page_url in page_urls:
|
||||||
|
if page_url != category_url: # We already scraped the first page
|
||||||
|
await page.goto(page_url)
|
||||||
|
# Scrape channels from this page
|
||||||
|
channels_data.extend(await scrape_tv_channels(page))
|
||||||
|
|
||||||
|
return channels_data
|
||||||
|
|
||||||
|
|
||||||
|
async def scrape_all_categories():
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.firefox.launch(headless=True)
|
||||||
|
page = await browser.new_page()
|
||||||
|
|
||||||
|
# Extract category URLs
|
||||||
|
await page.goto(BASE_URL)
|
||||||
|
category_elements = await page.query_selector_all(".main-header a")
|
||||||
|
category_urls = [
|
||||||
|
urljoin(BASE_URL, await element.get_attribute("href"))
|
||||||
|
for element in category_elements
|
||||||
|
]
|
||||||
|
|
||||||
|
# Scrape channels from each category
|
||||||
|
all_channels_data = []
|
||||||
|
for category_url in category_urls:
|
||||||
|
logging.info("Scraping %s", category_url)
|
||||||
|
all_channels_data.extend(await scrape_category(category_url, page))
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
# remove duplicates
|
||||||
|
unique_channels = {channel["title"]: channel for channel in all_channels_data}
|
||||||
|
unique_channels_data = list(unique_channels.values())
|
||||||
|
|
||||||
|
logging.info("found %d channels", len(unique_channels_data))
|
||||||
|
|
||||||
|
with open("tamilultra.json", "w") as file:
|
||||||
|
json.dump({"channels": unique_channels_data}, file, indent=4)
|
||||||
|
|
||||||
|
logging.info(
|
||||||
|
"Done scraping TamilUltra. Manually verify the data & add it via /tv-metadata endpoint"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main(is_scraping: bool = True):
|
||||||
|
if is_scraping:
|
||||||
|
asyncio.run(scrape_all_categories())
|
||||||
|
return
|
||||||
|
|
||||||
|
with open("tamilultra.json") as file:
|
||||||
|
channels = json.load(file)["channels"]
|
||||||
|
|
||||||
|
for channel in channels:
|
||||||
|
logging.info("Adding %s", channel["title"])
|
||||||
|
response = requests.post(f"{MEDIAFUSION_URL}/tv-metadata", json=channel)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response.raise_for_status()
|
||||||
|
except requests.HTTPError as err:
|
||||||
|
logging.info("Response data: %s", response.text)
|
||||||
|
continue
|
||||||
|
|
||||||
|
logging.info("Response data: %s", response.json())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Scrape TamilUltra Live TV")
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-scrape",
|
||||||
|
action="store_true",
|
||||||
|
help="Don't scrape TamilUltra. Use this option to add the data to MediaFusion",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
main(not args.no_scrape)
|
||||||
@@ -29,6 +29,8 @@ CATALOG_ID_DATA = [
|
|||||||
"english_hdrip",
|
"english_hdrip",
|
||||||
"english_tcrip",
|
"english_tcrip",
|
||||||
"english_series",
|
"english_series",
|
||||||
|
"live_tv",
|
||||||
|
"mediafusion_search_tv",
|
||||||
]
|
]
|
||||||
|
|
||||||
CATALOG_NAME_DATA = [
|
CATALOG_NAME_DATA = [
|
||||||
@@ -62,4 +64,6 @@ CATALOG_NAME_DATA = [
|
|||||||
"English HD Movies",
|
"English HD Movies",
|
||||||
"English TCRip/DVDScr/HDCAM Movies",
|
"English TCRip/DVDScr/HDCAM Movies",
|
||||||
"English Series",
|
"English Series",
|
||||||
|
"Live TV",
|
||||||
|
"MediaFusion Search TV",
|
||||||
]
|
]
|
||||||
|
|||||||
+25
-1
@@ -5,7 +5,7 @@ import requests
|
|||||||
from imdb import Cinemagoer, IMDbDataAccessError
|
from imdb import Cinemagoer, IMDbDataAccessError
|
||||||
|
|
||||||
from db.config import settings
|
from db.config import settings
|
||||||
from db.models import Streams
|
from db.models import Streams, TVStreams
|
||||||
from db.schemas import Stream, UserData
|
from db.schemas import Stream, UserData
|
||||||
from streaming_providers.realdebrid.utils import (
|
from streaming_providers.realdebrid.utils import (
|
||||||
order_streams_by_instant_availability_and_date,
|
order_streams_by_instant_availability_and_date,
|
||||||
@@ -149,3 +149,27 @@ def search_imdb(title: str, year: int, retry: int = 5) -> dict:
|
|||||||
"background": poster,
|
"background": poster,
|
||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_tv_stream_data(stream: list[TVStreams]) -> list[Stream]:
|
||||||
|
stream_list = []
|
||||||
|
for stream in stream:
|
||||||
|
if stream.behaviorHints.get("is_redirect", False):
|
||||||
|
response = requests.get(
|
||||||
|
stream.url,
|
||||||
|
headers=stream.behaviorHints["proxyHeaders"]["request"],
|
||||||
|
allow_redirects=False,
|
||||||
|
)
|
||||||
|
if response.status_code == 302:
|
||||||
|
stream.url = response.headers["Location"]
|
||||||
|
stream_list.append(
|
||||||
|
Stream(
|
||||||
|
name="MediaFusion",
|
||||||
|
description=f"{stream.name}, {stream.source}",
|
||||||
|
url=stream.url,
|
||||||
|
ytId=stream.ytId,
|
||||||
|
behaviorHints=stream.behaviorHints,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return stream_list
|
||||||
|
|||||||
+2
-2
@@ -14,7 +14,7 @@ async def create_poster(mediafusion_data: MediaFusionMetaData) -> BytesIO:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
# Check if the response content type is an image
|
# Check if the response content type is an image
|
||||||
if not response.headers["Content-Type"].startswith("image/"):
|
if not response.headers["Content-Type"].lower().startswith("image/"):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Unexpected content type: {response.headers['Content-Type']} for URL: {mediafusion_data.poster}"
|
f"Unexpected content type: {response.headers['Content-Type']} for URL: {mediafusion_data.poster}"
|
||||||
)
|
)
|
||||||
@@ -24,7 +24,7 @@ async def create_poster(mediafusion_data: MediaFusionMetaData) -> BytesIO:
|
|||||||
raise ValueError(f"Empty content for URL: {mediafusion_data.poster}")
|
raise ValueError(f"Empty content for URL: {mediafusion_data.poster}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
image = Image.open(BytesIO(response.content))
|
image = Image.open(BytesIO(response.content)).convert("RGBA")
|
||||||
except UnidentifiedImageError:
|
except UnidentifiedImageError:
|
||||||
raise ValueError(f"Cannot identify image from URL: {mediafusion_data.poster}")
|
raise ValueError(f"Cannot identify image from URL: {mediafusion_data.poster}")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from urllib.parse import urlparse, quote
|
||||||
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
|
from db import schemas
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_url(url: str) -> bool:
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
return all([parsed_url.scheme, parsed_url.netloc])
|
||||||
|
|
||||||
|
|
||||||
|
def does_url_exist(url: str) -> bool:
|
||||||
|
try:
|
||||||
|
response = requests.head(url, allow_redirects=True, timeout=5)
|
||||||
|
return response.status_code == 200
|
||||||
|
except RequestException:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def validate_image_url(url: str) -> bool:
|
||||||
|
return is_valid_url(url) and does_url_exist(url)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_m3u8_url(url: str, behaviour_hint: dict) -> bool:
|
||||||
|
if not is_valid_url(url):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
headers = behaviour_hint.get("proxyHeaders", {}).get("request", {})
|
||||||
|
response = requests.get(url, allow_redirects=True, headers=headers, timeout=30)
|
||||||
|
content_type = response.headers.get("Content-Type", "").lower()
|
||||||
|
return (
|
||||||
|
"application/vnd.apple.mpegurl" in content_type
|
||||||
|
or "application/x-mpegurl" in content_type
|
||||||
|
)
|
||||||
|
except RequestException as err:
|
||||||
|
logging.error(err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def validate_yt_id(yt_id: str) -> bool:
|
||||||
|
image_url = f"https://img.youtube.com/vi/{yt_id}/mqdefault.jpg"
|
||||||
|
response = requests.head(image_url, allow_redirects=True, timeout=5)
|
||||||
|
return response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def validate_tv_metadata(metadata: schemas.TVMetaData) -> list[schemas.TVStreams]:
|
||||||
|
if (
|
||||||
|
not validate_image_url(metadata.poster)
|
||||||
|
or (metadata.logo and not validate_image_url(metadata.logo))
|
||||||
|
or (metadata.background and not validate_image_url(metadata.background))
|
||||||
|
):
|
||||||
|
raise ValidationError("Invalid image URL provided.")
|
||||||
|
|
||||||
|
# Validate stream URLs at least 1 stream should be valid
|
||||||
|
valid_streams = [
|
||||||
|
stream
|
||||||
|
for stream in metadata.streams
|
||||||
|
if (
|
||||||
|
stream.url
|
||||||
|
and validate_m3u8_url(
|
||||||
|
stream.url, stream.behaviorHints.model_dump(exclude_none=True)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
or (stream.ytId and validate_yt_id(stream.ytId))
|
||||||
|
]
|
||||||
|
if not valid_streams:
|
||||||
|
raise ValidationError("Invalid stream URLs provided.")
|
||||||
|
|
||||||
|
unique_streams = {stream.url or stream.ytId: stream for stream in valid_streams}
|
||||||
|
return list(unique_streams.values())
|
||||||
Reference in New Issue
Block a user