From ecfecd93d6687c49b293540ac3e987c78181890f Mon Sep 17 00:00:00 2001 From: Bala Vijaya Krishna Saladi Date: Fri, 13 Oct 2023 23:37:14 +0530 Subject: [PATCH 1/9] DebridLink support initial commit --- api/main.py | 9 +- db/schemas.py | 2 +- resources/html/configure.html | 1 + resources/js/config_script.js | 4 +- streaming_providers/debridlink/__init__.py | 0 streaming_providers/debridlink/api.py | 21 +++ streaming_providers/debridlink/client.py | 156 +++++++++++++++++++++ streaming_providers/debridlink/utils.py | 103 ++++++++++++++ 8 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 streaming_providers/debridlink/__init__.py create mode 100644 streaming_providers/debridlink/api.py create mode 100644 streaming_providers/debridlink/client.py create mode 100644 streaming_providers/debridlink/utils.py diff --git a/api/main.py b/api/main.py index 330f3b4..fc5e01f 100644 --- a/api/main.py +++ b/api/main.py @@ -16,6 +16,8 @@ from streaming_providers.realdebrid.api import router as realdebrid_router from streaming_providers.realdebrid.utils import get_direct_link_from_realdebrid from streaming_providers.seedr.api import router as seedr_router 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.utils import get_direct_link_from_debridlink from utils import crypto, torrent from utils.const import CATALOG_ID_DATA, CATALOG_NAME_DATA @@ -284,10 +286,14 @@ async def streaming_provider_endpoint( video_url = await get_direct_link_from_seedr( info_hash, magnet_link, user_data, stream, episode_data, 3, 1 ) - else: + elif user_data.streaming_provider.service == "realdebrid": video_url = get_direct_link_from_realdebrid( info_hash, magnet_link, user_data, stream, episode_data, 3, 1 ) + else: + video_url = get_direct_link_from_debridlink( + info_hash, magnet_link, user_data, stream, episode_data, 3, 1 + ) except ProviderException as error: logging.info("Exception occurred: %s", error.message) video_url = f"{settings.host_url}/static/exceptions/{error.video_file_name}" @@ -297,3 +303,4 @@ async def streaming_provider_endpoint( app.include_router(seedr_router, prefix="/seedr", tags=["seedr"]) app.include_router(realdebrid_router, prefix="/realdebrid", tags=["realdebrid"]) +app.include_router(debridlink_router, prefix="/debridlink", tags=["debridlink"]) \ No newline at end of file diff --git a/db/schemas.py b/db/schemas.py index 5fdbe54..84493ab 100644 --- a/db/schemas.py +++ b/db/schemas.py @@ -37,7 +37,7 @@ class Streams(BaseModel): class StreamingProvider(BaseModel): - service: Literal["realdebrid", "seedr"] + service: Literal["realdebrid", "seedr", "debridlink"] token: str class Config: diff --git a/resources/html/configure.html b/resources/html/configure.html index 41e4872..d16238b 100644 --- a/resources/html/configure.html +++ b/resources/html/configure.html @@ -28,6 +28,7 @@ + diff --git a/resources/js/config_script.js b/resources/js/config_script.js index cc62155..35e3e75 100644 --- a/resources/js/config_script.js +++ b/resources/js/config_script.js @@ -93,7 +93,7 @@ function adjustOAuthSectionDisplay() { const providerToken = document.getElementById('provider_token').value; setOAuthBtnTextContent(provider) - if ((provider === 'seedr' || provider === 'realdebrid') && !providerToken) { + if ((provider === 'seedr' || provider === 'realdebrid' || provider === 'debridlink') && !providerToken) { setElementDisplay('oauth_section', 'block'); } else { setElementDisplay('oauth_section', 'none'); @@ -136,6 +136,8 @@ oAuthBtn.addEventListener('click', async function () { await initiateOAuthFlow('/seedr/get-device-code', '/seedr/authorize'); } else if (provider === 'realdebrid') { await initiateOAuthFlow('/realdebrid/get-device-code', '/realdebrid/authorize'); + } else if (provider === 'debridlink') { + await initiateOAuthFlow('/debridlink/get-device-code', '/debridlink/authorize') } }); diff --git a/streaming_providers/debridlink/__init__.py b/streaming_providers/debridlink/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streaming_providers/debridlink/api.py b/streaming_providers/debridlink/api.py new file mode 100644 index 0000000..2b877f3 --- /dev/null +++ b/streaming_providers/debridlink/api.py @@ -0,0 +1,21 @@ +from fastapi import APIRouter +from fastapi.responses import JSONResponse + +from db.schemas import AuthorizeData +from streaming_providers.debridlink.client import DebridLink + +router = APIRouter() +headers = {"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", "Pragma": "no-cache", "Expires": "0"} + + +@router.get("/get-device-code") +async def get_device_code(): + dl_client = DebridLink() + return JSONResponse(content=dl_client.get_device_code(), headers=headers) + + +@router.post("/authorize") +async def authorize(data: AuthorizeData): + dl_client = DebridLink() + response = dl_client.authorize(data.device_code) + return JSONResponse(content=response, headers=headers) diff --git a/streaming_providers/debridlink/client.py b/streaming_providers/debridlink/client.py new file mode 100644 index 0000000..29825f4 --- /dev/null +++ b/streaming_providers/debridlink/client.py @@ -0,0 +1,156 @@ +import PTN +import traceback + +from requests import RequestException, JSONDecodeError +from typing import Any + +import requests +from base64 import b64encode, b64decode + +from streaming_providers.exceptions import ProviderException + + +class DebridLink: + BASE_URL = "https://debrid-link.com/api/v2" + OAUTH_URL = "https://debrid-link.com/api/oauth" + OPENSOURCE_CLIENT_ID = "RyrV22FOg30DsxjYPziRKA" + + def __init__(self, encoded_token=None): + self.encoded_token = encoded_token + self.headers = {} + self.initialize_headers() + + def __del__(self): + if self.encoded_token: + self.disable_access_token() + + def _make_request( + self, method: str, url: str, data=None, params=None, is_return_none=False, is_expected_to_fail=False + ) -> dict: + if method == "GET": + response = requests.get(url, params=params, headers=self.headers) + elif method == "POST": + response = requests.post(url, data=data, headers=self.headers) + elif method == "DELETE": + response = requests.delete(url, headers=self.headers) + else: + raise ValueError(f"Unsupported method: {method}") + + try: + response.raise_for_status() + except RequestException as error: + if is_expected_to_fail: + pass + elif error.response.status_code == 401: + raise ProviderException("Invalid token", "invalid_token.mp4") + else: + formatted_traceback = "".join(traceback.format_exception(error)) + raise ProviderException( + f"status code: {error.response.status_code}, data: {error.response.content}, trace log:\n {formatted_traceback}", + "api_error.mp4", + ) + + if is_return_none: + return {} + try: + return response.json() + except JSONDecodeError as error: + formatted_traceback = "".join(traceback.format_exception(error)) + raise ProviderException( + f"Failed to parse response. content: {response.text}, trace log:\n {formatted_traceback}", + "api_error.mp4", + ) + + def initialize_headers(self): + if self.encoded_token: + token_data = self.decode_token_str(self.encoded_token) + access_token_data = self.refresh_token(token_data["client_id"], token_data["code"]) + self.headers = {"Authorization": f"Bearer {access_token_data['access_token']}"} + + @staticmethod + def encode_token_data(client_id: str, code: str): + token = f"{client_id}:{code}" + return b64encode(str(token).encode()).decode() + + @staticmethod + def decode_token_str(token: str) -> dict[str, str]: + try: + client_id, code = b64decode(token).decode().split(":") + except ValueError: + raise ProviderException("Invalid token", "invalid_token.mp4") + return {"client_id": client_id, "code": code} + + def get_device_code(self): + return self._make_request( + "POST", + f"{self.OAUTH_URL}/device/code", + data={ + "client_id": self.OPENSOURCE_CLIENT_ID, + "scope": "get.post.downloader get.post.seedbox get.account get.files get.post.stream" + }, + ) + + def get_token(self, client_id, device_code): + return self._make_request( + "POST", + f"{self.OAUTH_URL}/token", + data={ + "client_id": client_id, + "code": device_code, + "grant_type": "http://oauth.net/grant_type/device/1.0", + }, + is_expected_to_fail=True, + ) + + def refresh_token(self, client_id, refresh_token): + return self._make_request( + "POST", + f"{self.OAUTH_URL}/token", + data={ + "client_id": client_id, + "refresh_token": refresh_token, + "grant_type": "refresh_token", + }, + ) + + def authorize(self, device_code): + token_data = self.get_token(self.OPENSOURCE_CLIENT_ID, device_code) + + if "error" in token_data: + return token_data + + if "access_token" in token_data: + token = self.encode_token_data( + self.OPENSOURCE_CLIENT_ID, token_data["refresh_token"] + ) + return {"token": token} + else: + return token_data + + def add_magent_link(self, magnet_link): + return self._make_request("POST", f"{self.BASE_URL}/seedbox/add", data={"url": magnet_link}) + + def get_user_torrent_list(self): + return self._make_request("GET", f"{self.BASE_URL}/seedbox/list") + + def get_torrent_info(self, torrent_id): + return self._make_request("GET", f"{self.BASE_URL}/seedbox/list", data={"url": torrent_id}) + + def get_torrent_files_list(self, torrent_id): + return self._make_request("GET", f"{self.BASE_URL}/files/{torrent_id}/list") + + def get_torrent_instant_availability(self, torrent_hash): + return self._make_request("GET", f"{self.BASE_URL}/seedbox/cached/", data={"url": torrent_hash}) + + def disable_access_token(self): + return self._make_request("GET", f"{self.OAUTH_URL}/revoke", is_return_none=True) + + def get_available_torrent(self, info_hash: str) -> dict[str, Any] | None: + torrent_list_response = self.get_user_torrent_list() + if "error" in torrent_list_response: + raise ProviderException("Failed to get torrent info from Debrid-Link", "transfer_error.mp4") + + available_torrents = torrent_list_response["value"] + for torrent in available_torrents: + if torrent["hashString"] == info_hash: + return torrent diff --git a/streaming_providers/debridlink/utils.py b/streaming_providers/debridlink/utils.py new file mode 100644 index 0000000..a881074 --- /dev/null +++ b/streaming_providers/debridlink/utils.py @@ -0,0 +1,103 @@ +import time +from typing import Any + +from db.models import Streams, Episode +from db.schemas import UserData +from streaming_providers.exceptions import ProviderException +from streaming_providers.debridlink.client import DebridLink + +import PTN + + +def get_direct_link_from_debridlink( + info_hash: str, + magnet_link: str, + user_data: UserData, + stream: Streams, + episode_data: Episode = None, + max_retries=5, + retry_interval=5, +) -> str: + dl_client = DebridLink(encoded_token=user_data.streaming_provider.token) + filename = episode_data.filename if episode_data else stream.filename + + # Check if the torrent already exists + downloadUrl = check_existing_torrent(dl_client, info_hash, episode_data, max_retries, retry_interval) + if downloadUrl: + return downloadUrl + + # If torrent doesn't exist, add it + response_data = dl_client.add_magent_link(magnet_link) + if "error" in response_data: + raise ProviderException("Failed to add magnet link to Debrid-Link", "transfer_error.mp4") + + torrent_id = response_data["value"]["id"] + + return wait_for_torrent_download(dl_client, torrent_id, filename, max_retries, retry_interval) + + +def check_existing_torrent( + dl_client: DebridLink, info_hash: str, episode_data: Episode | None, max_retries: int, retry_interval: int +) -> str: + """Check if the torrent is already in torrent list and return the direct link if available.""" + retries = 0 + + torrent_info = dl_client.get_available_torrent(info_hash) + if not torrent_info: + return None + + torrent_id = torrent_info.get("id") + while retries < max_retries: + torrent_info_response = dl_client.get_torrent_info(torrent_id) + if not torrent_info_response["success"] and not torrent_info_response["value"]: + raise ProviderException("Failed to get torrent info from Debrid-Link", "transfer_error.mp4") + + torrent_info = torrent_info_response["value"][0] + if torrent_info["downloadPercent"] == 100: + return get_direct_link(torrent_info, episode_data) + + time.sleep(retry_interval) + retries += 1 + raise ProviderException("Torrent not downloaded yet.", "torrent_not_downloaded.mp4") + + +def wait_for_torrent_download(dl_client, torrent_id: str, episode_data: Episode | None, max_retries: int, retry_interval: int) -> str: + """Wait for the torrent to be downloaded and return the direct link.""" + retries = 0 + while retries < max_retries: + torrent_info_response = dl_client.get_torrent_info(torrent_id) + + if not torrent_info_response["success"] and not torrent_info_response["value"]: + raise ProviderException("Failed to get torrent info from Debrid-Link", "transfer_error.mp4") + + torrent_info = torrent_info_response["value"][0] + if not torrent_info["files"]: + raise ProviderException("No files available for this torrent", "transfer_error.mp4") + + if torrent_info["downloadPercent"] == 100: + return get_direct_link(torrent_info, episode_data) + + time.sleep(retry_interval) + retries += 1 + raise ProviderException("Torrent not downloaded yet.", "torrent_not_downloaded.mp4") + + +def get_direct_link(torrent_info, episode_data: Episode | None) -> str: + if episode_data: + selected_file = select_episode_file(torrent_info["files"], episode_data.episode_number, "name") + else: + # Otherwise, select the largest file for download + selected_file = max(torrent_info["files"], key=lambda x: x["size"]) + return selected_file["downloadUrl"] + + +def select_episode_file(torrent_files: list, episode: int, file_name_key: str) -> dict: + """Select the file with the specified episode number.""" + + for file in torrent_files: + torrent_data = PTN.parse(file[file_name_key]) + file_episode = torrent_data.get("episode") + if file_episode and int(file_episode) == episode: + return file + else: + raise ProviderException(f"Episode {episode} not found in this torrent", "episode_not_found.mp4") \ No newline at end of file From 8c1748315231bf71f0a834a90b51b1dd24768763 Mon Sep 17 00:00:00 2001 From: mhdzumair Date: Mon, 16 Oct 2023 19:49:03 +0530 Subject: [PATCH 2/9] tamil_blasters: bugfixes on scraping script --- scrappers/helpers.py | 8 ++++- ...blasters_scrapper.py => tamil_blasters.py} | 32 ++++++++----------- 2 files changed, 21 insertions(+), 19 deletions(-) rename scrappers/{tamil_blasters_scrapper.py => tamil_blasters.py} (93%) mode change 100755 => 100644 diff --git a/scrappers/helpers.py b/scrappers/helpers.py index 10987f5..5458028 100644 --- a/scrappers/helpers.py +++ b/scrappers/helpers.py @@ -1,5 +1,6 @@ import logging +import PTN import cloudscraper import requests from requests.adapters import HTTPAdapter @@ -73,7 +74,12 @@ async def download_and_save_torrent( logging.error(f"Info hash not found for {torrent_link}") return False - metadata.update({"torrent_metadata": torrent_metadata}) + parsed_data = PTN.parse(torrent_metadata["torrent_name"]) + metadata.update({"torrent_metadata": torrent_metadata, **parsed_data}) + + if not metadata.get("year"): + logging.error(f"Year not found for {page_link}") + return False # Saving the metadata if media_type == "series": diff --git a/scrappers/tamil_blasters_scrapper.py b/scrappers/tamil_blasters.py old mode 100755 new mode 100644 similarity index 93% rename from scrappers/tamil_blasters_scrapper.py rename to scrappers/tamil_blasters.py index a942377..7fd825f --- a/scrappers/tamil_blasters_scrapper.py +++ b/scrappers/tamil_blasters.py @@ -7,7 +7,6 @@ import math import random import re -import PTN from bs4 import BeautifulSoup from dateutil.parser import parse as dateparser from playwright.async_api import async_playwright @@ -20,7 +19,7 @@ from scrappers.helpers import ( download_and_save_torrent, ) -HOMEPAGE = "https://www.tamilblasters.life" +HOMEPAGE = "https://www.1tamilblasters.art" TAMIL_BLASTER_LINKS = { "tamil": { "hdrip": "7-tamil-new-movies-hdrips-bdrips-dvdrips-hdtv", @@ -44,7 +43,7 @@ TAMIL_BLASTER_LINKS = { "tcrip": "87-hindi-new-movies-tcrip-dvdscr-hdcam-predvd", "hdrip": "86-hindi-new-movies-hdrips-bdrips-dvdrips-hdtv", "dubbed": "88-hindi-dubbed-movies-bdrips-hdrips-dvdscr-hdcam", - "series": "forums/forum/89-hindi-new-web-series-tv-shows", + "series": "89-hindi-new-web-series-tv-shows", }, "kannada": { "tcrip": "83-kannada-new-movies-tcrip-dvdscr-hdcam-predvd", @@ -95,7 +94,6 @@ async def process_movie( logging.error(f"Movie link not found") return - metadata = PTN.parse(re.sub(r"\s+", " ", movie_link.text)) page_link = movie_link.get("href") try: @@ -118,16 +116,14 @@ async def process_movie( dateparser(datetime_element.get("datetime")) if datetime_element else None ) - # Updating metadata - metadata.update( - { - "catalog": f"{language}_{media_type}", - "poster": poster, - "created_at": created_at, - "scrap_language": language.title(), - "source": "TamilBlasters", - } - ) + # Define metadata + metadata = { + "catalog": f"{language}_{media_type}", + "poster": poster, + "created_at": created_at, + "scrap_language": language.title(), + "source": "TamilBlasters", + } # Extracting torrent details torrent_elements = movie_page.select("a[data-fileext='torrent']") @@ -141,7 +137,7 @@ async def process_movie( torrent_element, scraper=scraper, page=page, - metadata=metadata, + metadata=metadata.copy(), media_type=media_type, page_link=page_link, ) @@ -254,12 +250,12 @@ async def run_scraper( await scrap_search_keyword(search_keyword, proxy_url) return try: - scrap_link = f"{HOMEPAGE}/index.php?/forums/forum/{TAMIL_BLASTER_LINKS[language][video_type]}" + scrap_link_prefix = f"{HOMEPAGE}/index.php?/forums/forum/{TAMIL_BLASTER_LINKS[language][video_type]}" except KeyError: logging.error(f"Unsupported language or video type: {language}_{video_type}") return for page in range(start_page, pages + start_page): - scrap_link = f"{scrap_link}/page/{page}/" + scrap_link = f"{scrap_link_prefix}/page/{page}/" logging.info(f"Scrap page: {page}") if scrap_with_playwright is True: await scrap_page_with_playwright( @@ -308,7 +304,7 @@ if __name__ == "__main__": "--video-type", help="scrap movie video type", default="hdrip", - choices=["hdrip", "tcrip", "dubbed", "series"], + choices=["hdrip", "tcrip", "dubbed", "series", "old"], ) parser.add_argument( "-p", "--pages", type=int, default=1, help="number of scrap pages" From d8ef162fa997b14d2fbbfda9844bae39117300e5 Mon Sep 17 00:00:00 2001 From: mhdzumair Date: Mon, 16 Oct 2023 19:51:45 +0530 Subject: [PATCH 3/9] tamilblasters: adding old movies catalog --- resources/manifest.json | 55 +++++++++++++++++++++++++++++++++++++ scrappers/tamil_blasters.py | 5 ++++ utils/const.py | 10 +++++++ 3 files changed, 70 insertions(+) mode change 100644 => 100755 scrappers/tamil_blasters.py diff --git a/resources/manifest.json b/resources/manifest.json index d25941f..26bc8d0 100644 --- a/resources/manifest.json +++ b/resources/manifest.json @@ -83,6 +83,17 @@ } ] }, + { + "id": "tamil_old", + "type": "movie", + "name": "Tamil Old Movies", + "extra": [ + { + "name": "skip", + "isRequired": false + } + ] + }, { "id": "tamil_dubbed", "type": "movie", @@ -127,6 +138,17 @@ } ] }, + { + "id": "malayalam_old", + "type": "movie", + "name": "Malayalam Old Movies", + "extra": [ + { + "name": "skip", + "isRequired": false + } + ] + }, { "id": "malayalam_dubbed", "type": "movie", @@ -171,6 +193,17 @@ } ] }, + { + "id": "telugu_old", + "type": "movie", + "name": "Telugu Old Movies", + "extra": [ + { + "name": "skip", + "isRequired": false + } + ] + }, { "id": "telugu_dubbed", "type": "movie", @@ -215,6 +248,17 @@ } ] }, + { + "id": "hindi_old", + "type": "movie", + "name": "Hindi Old Movies", + "extra": [ + { + "name": "skip", + "isRequired": false + } + ] + }, { "id": "hindi_dubbed", "type": "movie", @@ -259,6 +303,17 @@ } ] }, + { + "id": "kannada_old", + "type": "movie", + "name": "Kannada Old Movies", + "extra": [ + { + "name": "skip", + "isRequired": false + } + ] + }, { "id": "kannada_dubbed", "type": "movie", diff --git a/scrappers/tamil_blasters.py b/scrappers/tamil_blasters.py old mode 100644 new mode 100755 index 7fd825f..7601077 --- a/scrappers/tamil_blasters.py +++ b/scrappers/tamil_blasters.py @@ -26,30 +26,35 @@ TAMIL_BLASTER_LINKS = { "tcrip": "8-tamil-new-movies-tcrip-dvdscr-hdcam-predvd", "dubbed": "9-tamil-dubbed-movies-bdrips-hdrips-dvdscr-hdcam-in-multi-audios", "series": "63-tamil-new-web-series-tv-shows", + "old": "56-tamil-old-mid-movies-bdrips-hdrips-dvdrips-hdtv", }, "malayalam": { "tcrip": "75-malayalam-new-movies-tcrip-dvdscr-hdcam-predvd", "hdrip": "74-malayalam-new-movies-hdrips-bdrips-dvdrips-hdtv", "dubbed": "76-malayalam-dubbed-movies-bdrips-hdrips-dvdscr-hdcam", "series": "98-malayalam-new-web-series-tv-shows", + "old": "77-malayalam-old-mid-movies-bdrips-hdrips-dvdrips", }, "telugu": { "tcrip": "79-telugu-new-movies-tcrip-dvdscr-hdcam-predvd", "hdrip": "78-telugu-new-movies-hdrips-bdrips-dvdrips-hdtv", "dubbed": "80-telugu-dubbed-movies-bdrips-hdrips-dvdscr-hdcam", "series": "96-telugu-new-web-series-tv-shows", + "old": "81-telugu-old-mid-movies-bdrips-hdrips-dvdrips", }, "hindi": { "tcrip": "87-hindi-new-movies-tcrip-dvdscr-hdcam-predvd", "hdrip": "86-hindi-new-movies-hdrips-bdrips-dvdrips-hdtv", "dubbed": "88-hindi-dubbed-movies-bdrips-hdrips-dvdscr-hdcam", "series": "89-hindi-new-web-series-tv-shows", + "old": "102-hindi-old-mid-movies-bdrips-hdrips-dvdrips", }, "kannada": { "tcrip": "83-kannada-new-movies-tcrip-dvdscr-hdcam-predvd", "hdrip": "82-kannada-new-movies-hdrips-bdrips-dvdrips-hdtv", "dubbed": "84-kannada-dubbed-movies-bdrips-hdrips-dvdscr-hdcam", "series": "103-kannada-new-web-series-tv-shows", + "old": "85-kannada-old-mid-movies-bdrips-hdrips-dvdrips", }, "english": { "tcrip": "52-english-movies-hdcam-dvdscr-predvd", diff --git a/utils/const.py b/utils/const.py index f3ff203..4f7e3a5 100644 --- a/utils/const.py +++ b/utils/const.py @@ -3,22 +3,27 @@ CATALOG_ID_DATA = [ "mediafusion_search_series", "tamil_hdrip", "tamil_tcrip", + "tamil_old", "tamil_dubbed", "tamil_series", "malayalam_tcrip", "malayalam_hdrip", + "malayalam_old", "malayalam_dubbed", "malayalam_series", "telugu_tcrip", "telugu_hdrip", + "telugu_old", "telugu_dubbed", "telugu_series", "hindi_tcrip", "hindi_hdrip", + "hindi_old", "hindi_dubbed", "hindi_series", "kannada_tcrip", "kannada_hdrip", + "kannada_old", "kannada_dubbed", "kannada_series", "english_hdrip", @@ -31,22 +36,27 @@ CATALOG_NAME_DATA = [ "MediaFusion Search Series", "Tamil HD Movies", "Tamil TCRip/DVDScr/HDCAM Movies", + "Tamil Old Movies", "Tamil Dubbed Movies", "Tamil Series", "Malayalam TCRip/DVDScr/HDCAM Movies", "Malayalam HD Movies", + "Malayalam Old Movies", "Malayalam Dubbed Movies", "Malayalam Series", "Telugu TCRip/DVDScr/HDCAM Movies", "Telugu HD Movies", + "Telugu Old Movies", "Telugu Dubbed Movies", "Telugu Series", "Hindi TCRip/DVDScr/HDCAM Movies", "Hindi HD Movies", + "Hindi Old Movies", "Hindi Dubbed Movies", "Hindi Series", "Kannada TCRip/DVDScr/HDCAM Movies", "Kannada HD Movies", + "Kannada Old Movies", "Kannada Dubbed Movies", "Kannada Series", "English HD Movies", From 1652f03af48d93832f728c46a47d1d79afe3fae7 Mon Sep 17 00:00:00 2001 From: mhdzumair Date: Mon, 16 Oct 2023 19:53:16 +0530 Subject: [PATCH 4/9] TamilMV: Create scrapper script --- scrappers/tamilmv.py | 374 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 scrappers/tamilmv.py diff --git a/scrappers/tamilmv.py b/scrappers/tamilmv.py new file mode 100644 index 0000000..19090cf --- /dev/null +++ b/scrappers/tamilmv.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python3 + +import argparse +import asyncio +import logging +import math +import random +import re + +from bs4 import BeautifulSoup +from dateutil.parser import parse as dateparser +from playwright.async_api import async_playwright +from playwright_stealth import stealth_async + +from db import database +from scrappers.helpers import ( + get_page_content, + get_scrapper_session, + download_and_save_torrent, +) + +HOMEPAGE = "https://www.1tamilmv.prof" +TAMIL_MV_LINKS = { + "tamil": { + "hdrip": [ + "11-web-hd-itunes-hd-bluray", + "12-hd-rips-dvd-rips-br-rips", + "14-hdtv-sdtv-hdtv-rips", + ], + "tcrip": "10-predvd-dvdscr-cam-tc", + "dubbed": "17-hollywood-movies-in-multi-audios", + "series": "19-web-series-tv-shows", + }, + "malayalam": { + "hdrip": [ + "36-web-hd-itunes-hd-bluray", + "37-hd-rips-dvd-rips-br-rips", + "39-hdtv-sdtv-hdtv-rips", + ], + "tcrip": "35-predvd-dvdscr-cam-tc", + "dubbed": "42-malayalam-dubbed-subtitled-movies", + "series": "44-web-series-tv-shows", + }, + "telugu": { + "tcrip": "23-predvd-dvdscr-cam-tc", + "hdrip": [ + "24-web-hd-itunes-hd-bluray", + "25-hd-rips-dvd-rips-br-rips", + "27-hdtv-sdtv-hdtv-rips", + ], + "dubbed": "31-telugu-dubbed-movies", + "series": "33-web-series-tv-shows", + }, + "hindi": { + "tcrip": "57-predvd-dvdscr-cam-tc", + "hdrip": [ + "58-web-hd-itunes-hd-bluray", + "59-hd-rips-dvd-rips-br-rips", + "61-hdtv-sdtv-hdtv-rips", + ], + "dubbed": "64-hindi-dubbed-movies", + "series": "66-web-series-tv-shows", + }, + "kannada": { + "tcrip": "68-predvd-dvdscr-cam-tc", + "hdrip": [ + "69-web-hd-itunes-hd-bluray", + "70-hd-rips-dvd-rips-br-rips", + "72-hdtv-sdtv-hdtv-rips", + ], + "dubbed": "75-watch-kannada-movies-online", + "series": "77-web-series-tv-shows", + }, + "english": { + "tcrip": "46-predvd-dvdscr-cam-tc", + "hdrip": ["49-web-hd-itunes-hd-bluray", "50-hd-rips-dvd-rips-br-rips"], + "series": "55-web-series-tv-shows", + }, +} + + +async def process_movie( + movie, + scraper=None, + page=None, + keyword=None, + language=None, + media_type=None, + supported_forums=None, +): + if keyword: + movie_link = movie.find("a", {"data-linktype": "link"}) + forum_link = movie.find("a", href=re.compile(r"forums/forum/")).get("href") + forum_id = re.search(r"forums/forum/([^/]+)/", forum_link)[1] + if forum_id not in supported_forums: + logging.error(f"Unsupported forum {forum_id}") + return + # Extracting language and media_type from supported_forums + language = supported_forums[forum_id]["language"] + media_type = supported_forums[forum_id]["media_type"] + else: + movie_link = movie.find("a") + + if not movie_link: + logging.error(f"Movie link not found") + return + + page_link = movie_link.get("href") + + try: + if scraper: # If using the scraper + response = scraper.get(page_link) + movie_page_content = response.content + else: # If using playwright + movie_page_content = await get_page_content(page, page_link) + + movie_page = BeautifulSoup(movie_page_content, "html.parser") + + # Extracting other details + poster_element = movie_page.select_one("div[data-commenttype='forums'] img") + poster = poster_element.get("src") if poster_element else None + + datetime_element = movie_page.select_one("time") + created_at = ( + dateparser(datetime_element.get("datetime")) if datetime_element else None + ) + + # Define metadata + metadata = { + "catalog": f"{language}_{media_type}", + "poster": poster, + "created_at": created_at, + "scrap_language": language.title(), + "source": "TamilMV", + } + + # Extracting torrent details + torrent_elements = movie_page.select("a[data-fileext='torrent']") + + if not torrent_elements: + logging.error(f"No torrents found for {page_link}") + return + + for torrent_element in torrent_elements: + await download_and_save_torrent( + torrent_element, + scraper=scraper, + page=page, + metadata=metadata.copy(), + media_type=media_type, + page_link=page_link, + ) + + return True + except Exception as e: + logging.error( + f"Error processing movie {page_link}: {e}", exc_info=True, stack_info=True + ) + return False + + +async def scrap_page(url, language, media_type, proxy_url=None): + scraper = get_scrapper_session(proxy_url) + response = scraper.get(url) + if response.status_code == 403: + logging.error( + "Cloudflare validation required. Run with --scrap-with-playwright" + ) + return + + response.raise_for_status() + tamil_blasters = BeautifulSoup(response.content, "html.parser") + movies = tamil_blasters.select("li[data-rowid]") + + for movie in movies: + await process_movie( + movie, scraper=scraper, language=language, media_type=media_type + ) + + +async def scrap_page_with_playwright(url, language, media_type, proxy_url=None): + async with async_playwright() as p: + # Launch a new browser session + browser = await p.firefox.launch( + headless=False, + proxy={"server": proxy_url} if proxy_url else None, + ) + page = await browser.new_page() + await stealth_async(page) + await asyncio.sleep(2) + + page_content = await get_page_content(page, url) + tamil_blasters = BeautifulSoup(page_content, "html.parser") + + movies = tamil_blasters.select("li[data-rowid]") + + for movie in movies: + await process_movie( + movie, page=page, language=language, media_type=media_type + ) + + await browser.close() + + +async def get_search_results(scraper, keyword, page_number=1): + search_link = f"{HOMEPAGE}/index.php?/search/&q={keyword}&type=forums_topic&page={page_number}&search_and_or=or&search_in=titles&sortby=relevancy" + # Get page content and initialize BeautifulSoup + response = scraper.get(search_link) + response.raise_for_status() + page_content = response.content + soup = BeautifulSoup(page_content, "html.parser") + + return soup + + +async def scrap_search_keyword(keyword, proxy_url=None): + supported_forums = {} + for language in TAMIL_MV_LINKS: + for video_type in TAMIL_MV_LINKS[language]: + forum_ids = TAMIL_MV_LINKS[language][video_type] + if isinstance(forum_ids, list): + for forum_id in forum_ids: + supported_forums[forum_id] = { + "language": language, + "media_type": video_type, + } + else: + supported_forums[forum_ids] = { + "language": language, + "media_type": video_type, + } + + scraper = get_scrapper_session(proxy_url) + soup = await get_search_results(scraper, keyword) + results_element = soup.find("div", {"data-role": "resultsArea"}) + + results_count = int(re.search(r"\d+", results_element.find("p").text).group()) + logging.info(f"Found {results_count} results for {keyword}") + + movies = results_element.select("li[data-role='activityItem']") + if results_count > 25: + number_of_pages = math.ceil(results_count / 25) + logging.info(f"Found {number_of_pages} pages for {keyword}") + for page_number in range(2, number_of_pages + 1): + soup = await get_search_results(scraper, keyword, page_number) + movies.extend(soup.select("li[data-role='activityItem']")) + await asyncio.sleep(random.randint(2, 5)) + + for movie in movies: + await process_movie( + movie, scraper=scraper, keyword=keyword, supported_forums=supported_forums + ) + + +async def run_scraper( + language: str = None, + video_type: str = None, + pages: int = None, + start_page: int = None, + search_keyword: str = None, + scrap_with_playwright: bool = None, + proxy_url: str = None, +): + await database.init() + if search_keyword: + await scrap_search_keyword(search_keyword, proxy_url) + return + link_prefix = f"{HOMEPAGE}/index.php?/forums/forum/" + try: + forum_ids = TAMIL_MV_LINKS[language][video_type] + scrap_links = ( + [link_prefix + link for link in forum_ids] + if isinstance(forum_ids, list) + else [link_prefix + forum_ids] + ) + except KeyError: + logging.error(f"Unsupported language or video type: {language}_{video_type}") + return + for scrap_link_prefix in scrap_links: + for page in range(start_page, pages + start_page): + scrap_link = f"{scrap_link_prefix}/page/{page}/" + logging.info(f"Scrap page: {scrap_link}") + if scrap_with_playwright is True: + await scrap_page_with_playwright( + scrap_link, language, video_type, proxy_url + ) + else: + await scrap_page(scrap_link, language, video_type, proxy_url) + + logging.info(f"Scrap completed for : {language}_{video_type}") + + +async def run_schedule_scrape( + pages: int = 1, + start_page: int = 1, + scrap_with_playwright: bool = None, + proxy_url: str = None, +): + for language in TAMIL_MV_LINKS: + for video_type in TAMIL_MV_LINKS[language]: + await run_scraper( + language, + video_type, + pages=pages, + start_page=start_page, + scrap_with_playwright=scrap_with_playwright, + proxy_url=proxy_url, + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Scrap Movie metadata from TamilMV") + parser.add_argument( + "--all", action="store_true", help="scrap all type of movies & series" + ) + parser.add_argument( + "-l", + "--language", + help="scrap movie language", + default="tamil", + choices=["tamil", "malayalam", "telugu", "hindi", "kannada", "english"], + ) + parser.add_argument( + "-t", + "--video-type", + help="scrap movie video type", + default="hdrip", + choices=["hdrip", "tcrip", "dubbed", "series"], + ) + parser.add_argument( + "-p", "--pages", type=int, default=1, help="number of scrap pages" + ) + parser.add_argument( + "-s", "--start-pages", type=int, default=1, help="page number to start scrap." + ) + parser.add_argument( + "-k", + "--search-keyword", + help="search keyword to scrap movies & series. ex: 'bigg boss'", + default=None, + ) + parser.add_argument( + "--scrap-with-playwright", action="store_true", help="scrap with playwright" + ) + parser.add_argument( + "--proxy-url", + help="proxy url to scrap. ex: socks5://127.0.0.1:1080", + default=None, + ) + args = parser.parse_args() + + logging.basicConfig( + format="%(levelname)s::%(asctime)s - %(message)s", + datefmt="%d-%b-%y %H:%M:%S", + level=logging.INFO, + ) + if args.all: + asyncio.run( + run_schedule_scrape( + args.pages, args.start_pages, args.scrap_with_playwright, args.proxy_url + ) + ) + else: + asyncio.run( + run_scraper( + args.language, + args.video_type, + args.pages, + args.start_pages, + args.search_keyword, + args.scrap_with_playwright, + args.proxy_url, + ) + ) From 02939e7a3a203de079e07bd55c033e550f1ca3e5 Mon Sep 17 00:00:00 2001 From: mhdzumair Date: Mon, 16 Oct 2023 20:07:04 +0530 Subject: [PATCH 5/9] README: Update the readme content for tamilmv scrapper --- scrappers/README.md | 59 ++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/scrappers/README.md b/scrappers/README.md index aefed1e..d1b4565 100644 --- a/scrappers/README.md +++ b/scrappers/README.md @@ -1,6 +1,8 @@ # Scrapping with Media Fusion -Currently, the project supports scraping from TamilBlasters. Here's how to use the scraper: +The project offers scraping capabilities for TamilBlasters and TamilMV. Here are the guidelines on how to use these scrapers: + +## TamilBlasters 1. **Setup Playwright**: @@ -10,38 +12,12 @@ Currently, the project supports scraping from TamilBlasters. Here's how to use t pipenv run playwright install firefox ``` -2. **Get Help on Available Options **: +2. **Get Help on Available Options**: To understand the available options for the scraper, run: ```bash - pipenv run python3 -m scrappers.tamil_blasters_scrapper --help - ``` - - This will display the available options: - - ```bash - usage: tamil_blasters_scrapper.py [-h] [--all] [-l {tamil,malayalam,telugu,hindi,kannada,english}] [-t {hdrip,tcrip,dubbed,series}] [-p PAGES] [-s START_PAGES] [-k SEARCH_KEYWORD] [--scrap-with-playwright] [--proxy-url PROXY_URL] - - Scrap Movie metadata from TamilBlasters - - options: - -h, --help show this help message and exit - --all scrap all type of movies & series - -l {tamil,malayalam,telugu,hindi,kannada,english}, --language {tamil,malayalam,telugu,hindi,kannada,english} - scrap movie language - -t {hdrip,tcrip,dubbed,series}, --video-type {hdrip,tcrip,dubbed,series} - scrap movie video type - -p PAGES, --pages PAGES - number of scrap pages - -s START_PAGES, --start-pages START_PAGES - page number to start scrap. - -k SEARCH_KEYWORD, --search-keyword SEARCH_KEYWORD - search keyword to scrap movies & series. ex: 'bigg boss' - --scrap-with-playwright - scrap with playwright - --proxy-url PROXY_URL - proxy url to scrap. ex: socks5://127.0.0.1:1080 + pipenv run python3 -m scrappers.tamil_blasters --help ``` 3. **Run the Scraper**: @@ -51,7 +27,30 @@ Currently, the project supports scraping from TamilBlasters. Here's how to use t e.g. To scrap all movies & series, run: ```bash - pipenv run python3 -m scrappers.tamil_blasters_scrapper --all --scrap-with-playwright + pipenv run python3 -m scrappers.tamil_blasters --all --scrap-with-playwright ``` Note: You may have to solve the cloudflare validation challenge manually when its required. + + +## TamilMV + +1. **Get Help on Available Options**: + + To understand the available options for the TamilMV scraper, run: + + ```bash + pipenv run python3 -m scrappers.tamilmv --help + ``` + +2. **Run the TamilMV Scraper**: + + Execute the scraper with the desired options based on your requirements. + + e.g. To scrap all movies & series from TamilMV for 5 pages, run: + + ```bash + pipenv run python3 -m scrappers.tamilmv --all -p 5 + ``` + + Note: Ensure you have Playwright set up as mentioned in the TamilBlasters section if you intend to use it with the TamilMV scraper. \ No newline at end of file From a249d922b010e364caf4f66b0cae477819fd9c99 Mon Sep 17 00:00:00 2001 From: mhdzumair Date: Mon, 16 Oct 2023 20:15:30 +0530 Subject: [PATCH 6/9] Setting up rating poster & reduce image size by using proxy poster endpoint --- .gitignore | 3 + Pipfile | 3 + Pipfile.lock | 397 +++++++++++++++++++++++---------- api/main.py | 57 +++-- db/crud.py | 121 ++++------ db/models.py | 2 + db/schemas.py | 5 +- resources/images/logo_text.png | Bin 0 -> 13044 bytes utils/parser.py | 29 +++ utils/poster.py | 76 +++++++ 10 files changed, 483 insertions(+), 210 deletions(-) create mode 100644 resources/images/logo_text.png create mode 100644 utils/poster.py diff --git a/.gitignore b/.gitignore index be4ef3c..0f52d05 100644 --- a/.gitignore +++ b/.gitignore @@ -132,3 +132,6 @@ dmypy.json *.service http-client.private.env.json + +localhost.pem +localhost-key.pem \ No newline at end of file diff --git a/Pipfile b/Pipfile index 3e9c166..cc59731 100644 --- a/Pipfile +++ b/Pipfile @@ -25,8 +25,11 @@ seedrcc = "*" parse-torrent-title = {editable = true, git = "https://github.com/mhdzumair/parse-torrent-title"} playwright = "==1.38.0" playwright-stealth = "*" +pillow = "*" +httpx = "*" [dev-packages] +pysocks = "*" [requires] python_version = "3.11" diff --git a/Pipfile.lock b/Pipfile.lock index 2db19ec..8c12d3a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "183ccb6cf1162b614d2096a00892cfebbeaacf4ebf80abf708242a8e938a31a3" + "sha256": "78844b6c17f6cdde79cec519db027b7ed84cdf96d0cf4998084c359b91385fe3" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,11 @@ "default": { "annotated-types": { "hashes": [ - "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802", - "sha256:58da39888f92c276ad970249761ebea80ba544b77acddaa1a4d6cf78287d45fd" + "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", + "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d" ], - "markers": "python_version >= '3.7'", - "version": "==0.5.0" + "markers": "python_version >= '3.8'", + "version": "==0.6.0" }, "anyio": { "hashes": [ @@ -38,15 +38,17 @@ "sha256:fb91e8a768632a4756a585f79ec834e0e27aad5860bac7eaa523d9ccefd87661" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==3.10.4" }, "beanie": { "hashes": [ - "sha256:2b03cfe43ff383526d8ec67a88d8f0e60e251f2631af180c30fb4ac1fb9d900b", - "sha256:9721e3b0dc435e5a42b64e58d4f88bb9a447eb94c489de7ceb75854f45ec2a19" + "sha256:537e0c59c84644d78afaf7cf83821093c2a37f7a9c7f51e05191caf8b8142b92", + "sha256:6670849bffc1902a33e62ad5b5dc536a68ead6bafa2823e99a803d12a600b379" ], "index": "pypi", - "version": "==1.22.6" + "markers": "python_version >= '3.7' and python_version < '4.0'", + "version": "==1.23.0" }, "beautifulsoup4": { "hashes": [ @@ -54,6 +56,7 @@ "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a" ], "index": "pypi", + "markers": "python_full_version >= '3.6.0'", "version": "==4.12.2" }, "bencodepy": { @@ -68,7 +71,7 @@ "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" ], - "markers": "python_full_version >= '3.6.0'", + "markers": "python_version >= '3.6'", "version": "==2023.7.22" }, "charset-normalizer": { @@ -164,7 +167,7 @@ "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e", "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==3.3.0" }, "cinemagoer": { @@ -194,6 +197,7 @@ "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984" ], "index": "pypi", + "markers": "python_version >= '3.8' and python_version < '4.0'", "version": "==2.4.2" }, "fastapi": { @@ -202,6 +206,7 @@ "sha256:75a11f6bfb8fc4d2bec0bd710c2d5f2829659c0e8c0afd5560fdda6ce25ec653" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==0.103.2" }, "greenlet": { @@ -282,6 +287,14 @@ "markers": "python_version >= '3.7'", "version": "==0.14.0" }, + "httpcore": { + "hashes": [ + "sha256:13b5e5cd1dca1a6636a6aaea212b19f4f85cd88c366a2b82304181b769aab3c9", + "sha256:adc5398ee0a476567bf87467063ee63584a8bce86078bf748e48754f60202ced" + ], + "markers": "python_version >= '3.8'", + "version": "==0.18.0" + }, "httptools": { "hashes": [ "sha256:03bfd2ae8a2d532952ac54445a2fb2504c804135ed28b53fefaf03d3a93eb1fd", @@ -322,6 +335,15 @@ ], "version": "==0.6.0" }, + "httpx": { + "hashes": [ + "sha256:181ea7f8ba3a82578be86ef4171554dd45fec26a02556a744db029a0a27b7100", + "sha256:47ecda285389cb32bb2691cc6e069e3ab0205956f681c5b2ad2325719751d875" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.25.0" + }, "idna": { "hashes": [ "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", @@ -336,6 +358,7 @@ "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==3.1.2" }, "lazy-model": { @@ -516,16 +539,74 @@ "sha256:c5eb400e27d722a3db03a9826656b6d13acf9b6c70c2fb4604f474eac9da5be4" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==3.3.1" }, "parse-torrent-title": { "editable": true, "git": "https://github.com/mhdzumair/parse-torrent-title", + "ref": "a1bb2b927669d5105622f381b856a24e822307d7" + }, + "pillow": { "hashes": [ - "sha256:556830ea268f808a3cee3fc6e9380ab3293b13a8172567a5a39b5d858a5387af", - "sha256:9852cfba2b746683733d77d2019a24f57d5e8c2c4db395f6ff03035ea628a4bb" + "sha256:00f438bb841382b15d7deb9a05cc946ee0f2c352653c7aa659e75e592f6fa17d", + "sha256:0248f86b3ea061e67817c47ecbe82c23f9dd5d5226200eb9090b3873d3ca32de", + "sha256:04f6f6149f266a100374ca3cc368b67fb27c4af9f1cc8cb6306d849dcdf12616", + "sha256:062a1610e3bc258bff2328ec43f34244fcec972ee0717200cb1425214fe5b839", + "sha256:0a026c188be3b443916179f5d04548092e253beb0c3e2ee0a4e2cdad72f66099", + "sha256:0f7c276c05a9767e877a0b4c5050c8bee6a6d960d7f0c11ebda6b99746068c2a", + "sha256:1a8413794b4ad9719346cd9306118450b7b00d9a15846451549314a58ac42219", + "sha256:1ab05f3db77e98f93964697c8efc49c7954b08dd61cff526b7f2531a22410106", + "sha256:1c3ac5423c8c1da5928aa12c6e258921956757d976405e9467c5f39d1d577a4b", + "sha256:1c41d960babf951e01a49c9746f92c5a7e0d939d1652d7ba30f6b3090f27e412", + "sha256:1fafabe50a6977ac70dfe829b2d5735fd54e190ab55259ec8aea4aaea412fa0b", + "sha256:1fb29c07478e6c06a46b867e43b0bcdb241b44cc52be9bc25ce5944eed4648e7", + "sha256:24fadc71218ad2b8ffe437b54876c9382b4a29e030a05a9879f615091f42ffc2", + "sha256:2cdc65a46e74514ce742c2013cd4a2d12e8553e3a2563c64879f7c7e4d28bce7", + "sha256:2ef6721c97894a7aa77723740a09547197533146fba8355e86d6d9a4a1056b14", + "sha256:3b834f4b16173e5b92ab6566f0473bfb09f939ba14b23b8da1f54fa63e4b623f", + "sha256:3d929a19f5469b3f4df33a3df2983db070ebb2088a1e145e18facbc28cae5b27", + "sha256:41f67248d92a5e0a2076d3517d8d4b1e41a97e2df10eb8f93106c89107f38b57", + "sha256:47e5bf85b80abc03be7455c95b6d6e4896a62f6541c1f2ce77a7d2bb832af262", + "sha256:4d0152565c6aa6ebbfb1e5d8624140a440f2b99bf7afaafbdbf6430426497f28", + "sha256:50d08cd0a2ecd2a8657bd3d82c71efd5a58edb04d9308185d66c3a5a5bed9610", + "sha256:61f1a9d247317fa08a308daaa8ee7b3f760ab1809ca2da14ecc88ae4257d6172", + "sha256:6932a7652464746fcb484f7fc3618e6503d2066d853f68a4bd97193a3996e273", + "sha256:7a7e3daa202beb61821c06d2517428e8e7c1aab08943e92ec9e5755c2fc9ba5e", + "sha256:7dbaa3c7de82ef37e7708521be41db5565004258ca76945ad74a8e998c30af8d", + "sha256:7df5608bc38bd37ef585ae9c38c9cd46d7c81498f086915b0f97255ea60c2818", + "sha256:806abdd8249ba3953c33742506fe414880bad78ac25cc9a9b1c6ae97bedd573f", + "sha256:883f216eac8712b83a63f41b76ddfb7b2afab1b74abbb413c5df6680f071a6b9", + "sha256:912e3812a1dbbc834da2b32299b124b5ddcb664ed354916fd1ed6f193f0e2d01", + "sha256:937bdc5a7f5343d1c97dc98149a0be7eb9704e937fe3dc7140e229ae4fc572a7", + "sha256:9882a7451c680c12f232a422730f986a1fcd808da0fd428f08b671237237d651", + "sha256:9a92109192b360634a4489c0c756364c0c3a2992906752165ecb50544c251312", + "sha256:9d7bc666bd8c5a4225e7ac71f2f9d12466ec555e89092728ea0f5c0c2422ea80", + "sha256:a5f63b5a68daedc54c7c3464508d8c12075e56dcfbd42f8c1bf40169061ae666", + "sha256:a646e48de237d860c36e0db37ecaecaa3619e6f3e9d5319e527ccbc8151df061", + "sha256:a89b8312d51715b510a4fe9fc13686283f376cfd5abca8cd1c65e4c76e21081b", + "sha256:a92386125e9ee90381c3369f57a2a50fa9e6aa8b1cf1d9c4b200d41a7dd8e992", + "sha256:ae88931f93214777c7a3aa0a8f92a683f83ecde27f65a45f95f22d289a69e593", + "sha256:afc8eef765d948543a4775f00b7b8c079b3321d6b675dde0d02afa2ee23000b4", + "sha256:b0eb01ca85b2361b09480784a7931fc648ed8b7836f01fb9241141b968feb1db", + "sha256:b1c25762197144e211efb5f4e8ad656f36c8d214d390585d1d21281f46d556ba", + "sha256:b4005fee46ed9be0b8fb42be0c20e79411533d1fd58edabebc0dd24626882cfd", + "sha256:b920e4d028f6442bea9a75b7491c063f0b9a3972520731ed26c83e254302eb1e", + "sha256:baada14941c83079bf84c037e2d8b7506ce201e92e3d2fa0d1303507a8538212", + "sha256:bb40c011447712d2e19cc261c82655f75f32cb724788df315ed992a4d65696bb", + "sha256:c0949b55eb607898e28eaccb525ab104b2d86542a85c74baf3a6dc24002edec2", + "sha256:c9aeea7b63edb7884b031a35305629a7593272b54f429a9869a4f63a1bf04c34", + "sha256:cfe96560c6ce2f4c07d6647af2d0f3c54cc33289894ebd88cfbb3bcd5391e256", + "sha256:d27b5997bdd2eb9fb199982bb7eb6164db0426904020dc38c10203187ae2ff2f", + "sha256:d921bc90b1defa55c9917ca6b6b71430e4286fc9e44c55ead78ca1a9f9eba5f2", + "sha256:e6bf8de6c36ed96c86ea3b6e1d5273c53f46ef518a062464cd7ef5dd2cf92e38", + "sha256:eaed6977fa73408b7b8a24e8b14e59e1668cfc0f4c40193ea7ced8e210adf996", + "sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a", + "sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793" ], - "ref": "0ccb3aa7523063375ca8a6e3677a493ddba31e26" + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==10.1.0" }, "playwright": { "hashes": [ @@ -538,6 +619,7 @@ "sha256:d0288c8932d7f14bc231e4a6761ecf76fff879d1601cfa3b6f6aefd544468911" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==1.38.0" }, "playwright-stealth": { @@ -546,6 +628,7 @@ "sha256:b504d951d00fac755c7d13665a29611d415180510bd7d23f14ebc89439ba2043" ], "index": "pypi", + "markers": "python_version >= '3' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==1.0.6" }, "pycryptodome": { @@ -584,6 +667,7 @@ "sha256:fc7a79590e2b5d08530175823a242de6790abc73638cc6dc9d2684e7be2f5e49" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==3.19.0" }, "pydantic": { @@ -592,6 +676,7 @@ "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.4.2" }, "pydantic-core": { @@ -712,6 +797,7 @@ "sha256:ddd907b066622bd67603b75e2ff791875540dc485b7307c4fffc015719da8625" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.0.3" }, "pyee": { @@ -808,7 +894,7 @@ "sha256:fe48f50fb6348511a3268a893bfd4ab5f263f5ac220782449d03cd05964d1ae7", "sha256:fff7d17d30b2cd45afd654b3fc117755c5d84506ed25fda386494e4e0a3416e1" ], - "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==4.5.0" }, "pyparsing": { @@ -825,6 +911,7 @@ "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.2" }, "python-dotenv": { @@ -903,6 +990,7 @@ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.31.0" }, "requests-toolbelt": { @@ -919,6 +1007,7 @@ "sha256:e19b1086e304ce24e4fc706fc444523a9b873b9e9c7802a9c0d90bbac62ed417" ], "index": "pypi", + "markers": "python_version >= '3.0'", "version": "==1.0.1" }, "six": { @@ -947,50 +1036,58 @@ }, "sqlalchemy": { "hashes": [ - "sha256:014794b60d2021cc8ae0f91d4d0331fe92691ae5467a00841f7130fe877b678e", - "sha256:0268256a34806e5d1c8f7ee93277d7ea8cc8ae391f487213139018b6805aeaf6", - "sha256:05b971ab1ac2994a14c56b35eaaa91f86ba080e9ad481b20d99d77f381bb6258", - "sha256:141675dae56522126986fa4ca713739d00ed3a6f08f3c2eb92c39c6dfec463ce", - "sha256:1e7dc99b23e33c71d720c4ae37ebb095bebebbd31a24b7d99dfc4753d2803ede", - "sha256:2e617727fe4091cedb3e4409b39368f424934c7faa78171749f704b49b4bb4ce", - "sha256:3cf229704074bce31f7f47d12883afee3b0a02bb233a0ba45ddbfe542939cca4", - "sha256:3eb7c03fe1cd3255811cd4e74db1ab8dca22074d50cd8937edf4ef62d758cdf4", - "sha256:3f7d57a7e140efe69ce2d7b057c3f9a595f98d0bbdfc23fd055efdfbaa46e3a5", - "sha256:419b1276b55925b5ac9b4c7044e999f1787c69761a3c9756dec6e5c225ceca01", - "sha256:44ac5c89b6896f4740e7091f4a0ff2e62881da80c239dd9408f84f75a293dae9", - "sha256:4615623a490e46be85fbaa6335f35cf80e61df0783240afe7d4f544778c315a9", - "sha256:50a69067af86ec7f11a8e50ba85544657b1477aabf64fa447fd3736b5a0a4f67", - "sha256:513fd5b6513d37e985eb5b7ed89da5fd9e72354e3523980ef00d439bc549c9e9", - "sha256:6ff3dc2f60dbf82c9e599c2915db1526d65415be323464f84de8db3e361ba5b9", - "sha256:73c079e21d10ff2be54a4699f55865d4b275fd6c8bd5d90c5b1ef78ae0197301", - "sha256:7614f1eab4336df7dd6bee05bc974f2b02c38d3d0c78060c5faa4cd1ca2af3b8", - "sha256:785e2f2c1cb50d0a44e2cdeea5fd36b5bf2d79c481c10f3a88a8be4cfa2c4615", - "sha256:7ca38746eac23dd7c20bec9278d2058c7ad662b2f1576e4c3dbfcd7c00cc48fa", - "sha256:7f0c4ee579acfe6c994637527c386d1c22eb60bc1c1d36d940d8477e482095d4", - "sha256:87bf91ebf15258c4701d71dcdd9c4ba39521fb6a37379ea68088ce8cd869b446", - "sha256:89e274604abb1a7fd5c14867a412c9d49c08ccf6ce3e1e04fffc068b5b6499d4", - "sha256:8c323813963b2503e54d0944813cd479c10c636e3ee223bcbd7bd478bf53c178", - "sha256:a95aa0672e3065d43c8aa80080cdd5cc40fe92dc873749e6c1cf23914c4b83af", - "sha256:af520a730d523eab77d754f5cf44cc7dd7ad2d54907adeb3233177eeb22f271b", - "sha256:b19ae41ef26c01a987e49e37c77b9ad060c59f94d3b3efdfdbf4f3daaca7b5fe", - "sha256:b4eae01faee9f2b17f08885e3f047153ae0416648f8e8c8bd9bc677c5ce64be9", - "sha256:b69f1f754d92eb1cc6b50938359dead36b96a1dcf11a8670bff65fd9b21a4b09", - "sha256:b977bfce15afa53d9cf6a632482d7968477625f030d86a109f7bdfe8ce3c064a", - "sha256:bf8eebccc66829010f06fbd2b80095d7872991bfe8415098b9fe47deaaa58063", - "sha256:c111cd40910ffcb615b33605fc8f8e22146aeb7933d06569ac90f219818345ef", - "sha256:c2d494b6a2a2d05fb99f01b84cc9af9f5f93bf3e1e5dbdafe4bed0c2823584c1", - "sha256:c9cba4e7369de663611ce7460a34be48e999e0bbb1feb9130070f0685e9a6b66", - "sha256:cca720d05389ab1a5877ff05af96551e58ba65e8dc65582d849ac83ddde3e231", - "sha256:ccb99c3138c9bde118b51a289d90096a3791658da9aea1754667302ed6564f6e", - "sha256:d59cb9e20d79686aa473e0302e4a82882d7118744d30bb1dfb62d3c47141b3ec", - "sha256:e36339a68126ffb708dc6d1948161cea2a9e85d7d7b0c54f6999853d70d44430", - "sha256:ea7da25ee458d8f404b93eb073116156fd7d8c2a776d8311534851f28277b4ce", - "sha256:f9fefd6298433b6e9188252f3bff53b9ff0443c8fde27298b8a2b19f6617eeb9", - "sha256:fb87f763b5d04a82ae84ccff25554ffd903baafba6698e18ebaf32561f2fe4aa", - "sha256:fc6b15465fabccc94bf7e38777d665b6a4f95efd1725049d6184b3a39fd54880" + "sha256:0b0b3f2686c3f162123adba3cb8b626ed7e9b8433ab528e36ed270b4f70d1cdb", + "sha256:0c1fea8c0abcb070ffe15311853abfda4e55bf7dc1d4889497b3403629f3bf00", + "sha256:0e1ce8ebd2e040357dde01a3fb7d30d9b5736b3e54a94002641dfd0aa12ae6ce", + "sha256:129415f89744b05741c6f0b04a84525f37fbabe5dc3774f7edf100e7458c48cd", + "sha256:13790cb42f917c45c9c850b39b9941539ca8ee7917dacf099cc0b569f3d40da7", + "sha256:14cd3bcbb853379fef2cd01e7c64a5d6f1d005406d877ed9509afb7a05ff40a5", + "sha256:154a32f3c7b00de3d090bc60ec8006a78149e221f1182e3edcf0376016be9396", + "sha256:19c6986cf2fb4bc8e0e846f97f4135a8e753b57d2aaaa87c50f9acbe606bd1db", + "sha256:2096d6b018d242a2bcc9e451618166f860bb0304f590d205173d317b69986c95", + "sha256:2c9bac865ee06d27a1533471405ad240a6f5d83195eca481f9fc4a71d8b87df8", + "sha256:3076740335e4aaadd7deb3fe6dcb96b3015f1613bd190a4e1634e1b99b02ec86", + "sha256:3940677d341f2b685a999bffe7078697b5848a40b5f6952794ffcf3af150c301", + "sha256:3aa1472bf44f61dd27987cd051f1c893b7d3b17238bff8c23fceaef4f1133868", + "sha256:40b1206a0d923e73aa54f0a6bd61419a96b914f1cd19900b6c8226899d9742ad", + "sha256:4bb062784f37b2d75fd9b074c8ec360ad5df71f933f927e9e95c50eb8e05323c", + "sha256:4e869a8ff7ee7a833b74868a0887e8462445ec462432d8cbeff5e85f475186da", + "sha256:4f6ff392b27a743c1ad346d215655503cec64405d3b694228b3454878bf21590", + "sha256:505f503763a767556fa4deae5194b2be056b64ecca72ac65224381a0acab7ebe", + "sha256:53a766cb0b468223cafdf63e2d37f14a4757476157927b09300c8c5832d88560", + "sha256:5434cc601aa17570d79e5377f5fd45ff92f9379e2abed0be5e8c2fba8d353d2b", + "sha256:54bcceaf4eebef07dadfde424f5c26b491e4a64e61761dea9459103ecd6ccc95", + "sha256:55914d45a631b81a8a2cb1a54f03eea265cf1783241ac55396ec6d735be14883", + "sha256:564e9f9e4e6466273dbfab0e0a2e5fe819eec480c57b53a2cdee8e4fdae3ad5f", + "sha256:56a7e2bb639df9263bf6418231bc2a92a773f57886d371ddb7a869a24919face", + "sha256:58a3aba1bfb32ae7af68da3f277ed91d9f57620cf7ce651db96636790a78b736", + "sha256:625b72d77ac8ac23da3b1622e2da88c4aedaee14df47c8432bf8f6495e655de2", + "sha256:69fd9e41cf9368afa034e1c81f3570afb96f30fcd2eb1ef29cb4d9371c6eece2", + "sha256:6ac28bd6888fe3c81fbe97584eb0b96804bd7032d6100b9701255d9441373ec1", + "sha256:7c6c3e9350f9fb16de5b5e5fbf17b578811a52d71bb784cc5ff71acb7de2a7f9", + "sha256:7ee7ccf47aa503033b6afd57efbac6b9e05180f492aeed9fcf70752556f95624", + "sha256:875de9414393e778b655a3d97d60465eb3fae7c919e88b70cc10b40b9f56042d", + "sha256:8db5ba8b7da759b727faebc4289a9e6a51edadc7fc32207a30f7c6203a181592", + "sha256:92e512a6af769e4725fa5b25981ba790335d42c5977e94ded07db7d641490a85", + "sha256:9886a72c8e6371280cb247c5d32c9c8fa141dc560124348762db8a8b236f8692", + "sha256:9e55dff5ec115316dd7a083cdc1a52de63693695aecf72bc53a8e1468ce429e5", + "sha256:a42c9fa3abcda0dcfad053e49c4f752eef71ecd8c155221e18b99d4224621176", + "sha256:a571bc8ac092a3175a1d994794a8e7a1f2f651e7c744de24a19b4f740fe95034", + "sha256:af66001d7b76a3fab0d5e4c1ec9339ac45748bc4a399cbc2baa48c1980d3c1f4", + "sha256:b39a6e21110204a8c08d40ff56a73ba542ec60bab701c36ce721e7990df49fb9", + "sha256:b560f075c151900587ade06706b0c51d04b3277c111151997ea0813455378ae0", + "sha256:c8f1792d20d2f4e875ce7a113f43c3561ad12b34ff796b84002a256f37ce9437", + "sha256:cb9a758ad973e795267da334a92dd82bb7555cb36a0960dcabcf724d26299db8", + "sha256:ccca778c0737a773a1ad86b68bda52a71ad5950b25e120b6eb1330f0df54c3d0", + "sha256:ccd87c25e4c8559e1b918d46b4fa90b37f459c9b4566f1dfbce0eb8122571547", + "sha256:d143c5a9dada696bcfdb96ba2de4a47d5a89168e71d05a076e88a01386872f97", + "sha256:d80eeb5189d7d4b1af519fc3f148fe7521b9dfce8f4d6a0820e8f5769b005051", + "sha256:e04ab55cf49daf1aeb8c622c54d23fa4bec91cb051a43cc24351ba97e1dd09f5", + "sha256:f146c61ae128ab43ea3a0955de1af7e1633942c2b2b4985ac51cc292daf33222", + "sha256:f776c2c30f0e5f4db45c3ee11a5f2a8d9de68e81eb73ec4237de1e32e04ae81c" ], "markers": "python_version >= '3.7'", - "version": "==2.0.21" + "version": "==2.0.22" }, "starlette": { "hashes": [ @@ -1018,11 +1115,11 @@ }, "tzlocal": { "hashes": [ - "sha256:46eb99ad4bdb71f3f72b7d24f4267753e240944ecfc16f25d2719ba89827a803", - "sha256:f3596e180296aaf2dbd97d124fe76ae3a0e3d32b258447de7b939b3fd4be992f" + "sha256:2938498395d5f6a898ab8009555cb37a4d360913ad375d4747ef16826b03ef23", + "sha256:a5ccb2365b295ed964e0a98ad076fe10c495591e75505d34f154d60a7f1ed722" ], "markers": "python_version >= '3.7'", - "version": "==5.0.1" + "version": "==5.1" }, "urllib3": { "hashes": [ @@ -1040,43 +1137,49 @@ "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53", "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a" ], - "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==0.23.2" }, "uvloop": { "hashes": [ - "sha256:0949caf774b9fcefc7c5756bacbbbd3fc4c05a6b7eebc7c7ad6f825b23998d6d", - "sha256:0ddf6baf9cf11a1a22c71487f39f15b2cf78eb5bde7e5b45fbb99e8a9d91b9e1", - "sha256:1436c8673c1563422213ac6907789ecb2b070f5939b9cbff9ef7113f2b531595", - "sha256:23609ca361a7fc587031429fa25ad2ed7242941adec948f9d10c045bfecab06b", - "sha256:2a6149e1defac0faf505406259561bc14b034cdf1d4711a3ddcdfbaa8d825a05", - "sha256:2deae0b0fb00a6af41fe60a675cec079615b01d68beb4cc7b722424406b126a8", - "sha256:307958f9fc5c8bb01fad752d1345168c0abc5d62c1b72a4a8c6c06f042b45b20", - "sha256:30babd84706115626ea78ea5dbc7dd8d0d01a2e9f9b306d24ca4ed5796c66ded", - "sha256:3378eb62c63bf336ae2070599e49089005771cc651c8769aaad72d1bd9385a7c", - "sha256:3d97672dc709fa4447ab83276f344a165075fd9f366a97b712bdd3fee05efae8", - "sha256:3db8de10ed684995a7f34a001f15b374c230f7655ae840964d51496e2f8a8474", - "sha256:3ebeeec6a6641d0adb2ea71dcfb76017602ee2bfd8213e3fcc18d8f699c5104f", - "sha256:45cea33b208971e87a31c17622e4b440cac231766ec11e5d22c76fab3bf9df62", - "sha256:6708f30db9117f115eadc4f125c2a10c1a50d711461699a0cbfaa45b9a78e376", - "sha256:68532f4349fd3900b839f588972b3392ee56042e440dd5873dfbbcd2cc67617c", - "sha256:6aafa5a78b9e62493539456f8b646f85abc7093dd997f4976bb105537cf2635e", - "sha256:7d37dccc7ae63e61f7b96ee2e19c40f153ba6ce730d8ba4d3b4e9738c1dccc1b", - "sha256:864e1197139d651a76c81757db5eb199db8866e13acb0dfe96e6fc5d1cf45fc4", - "sha256:8887d675a64cfc59f4ecd34382e5b4f0ef4ae1da37ed665adba0c2badf0d6578", - "sha256:8efcadc5a0003d3a6e887ccc1fb44dec25594f117a94e3127954c05cf144d811", - "sha256:9b09e0f0ac29eee0451d71798878eae5a4e6a91aa275e114037b27f7db72702d", - "sha256:a4aee22ece20958888eedbad20e4dbb03c37533e010fb824161b4f05e641f738", - "sha256:a5abddb3558d3f0a78949c750644a67be31e47936042d4f6c888dd6f3c95f4aa", - "sha256:c092a2c1e736086d59ac8e41f9c98f26bbf9b9222a76f21af9dfe949b99b2eb9", - "sha256:c686a47d57ca910a2572fddfe9912819880b8765e2f01dc0dd12a9bf8573e539", - "sha256:cbbe908fda687e39afd6ea2a2f14c2c3e43f2ca88e3a11964b297822358d0e6c", - "sha256:ce9f61938d7155f79d3cb2ffa663147d4a76d16e08f65e2c66b77bd41b356718", - "sha256:dbbaf9da2ee98ee2531e0c780455f2841e4675ff580ecf93fe5c48fe733b5667", - "sha256:f1e507c9ee39c61bfddd79714e4f85900656db1aec4d40c6de55648e85c2799c", - "sha256:ff3d00b70ce95adce264462c930fbaecb29718ba6563db354608f37e49e09024" + "sha256:1121087dfeb46e9e65920b20d1f46322ba299b8d93f7cb61d76c94b5a1adc20c", + "sha256:12af0d2e1b16780051d27c12de7e419b9daeb3516c503ab3e98d364cc55303bb", + "sha256:1f354d669586fca96a9a688c585b6257706d216177ac457c92e15709acaece10", + "sha256:1f4a549cd747e6f4f8446f4b4c8cb79504a8372d5d3a9b4fc20e25daf8e76c05", + "sha256:211ce38d84118ae282a91408f61b85cf28e2e65a0a8966b9a97e0e9d67c48722", + "sha256:25b714f07c68dcdaad6994414f6ec0f2a3b9565524fba181dcbfd7d9598a3e73", + "sha256:280904236a5b333a273292b3bcdcbfe173690f69901365b973fa35be302d7781", + "sha256:2b8b7cf7806bdc745917f84d833f2144fabcc38e9cd854e6bc49755e3af2b53e", + "sha256:4d90858f32a852988d33987d608bcfba92a1874eb9f183995def59a34229f30d", + "sha256:53aca21735eee3859e8c11265445925911ffe410974f13304edb0447f9f58420", + "sha256:54b211c46facb466726b227f350792770fc96593c4ecdfaafe20dc00f3209aef", + "sha256:56c1026a6b0d12b378425e16250acb7d453abaefe7a2f5977143898db6cfe5bd", + "sha256:585b7281f9ea25c4a5fa993b1acca4ad3d8bc3f3fe2e393f0ef51b6c1bcd2fe6", + "sha256:58e44650cbc8607a218caeece5a689f0a2d10be084a69fc32f7db2e8f364927c", + "sha256:61151cc207cf5fc88863e50de3d04f64ee0fdbb979d0b97caf21cae29130ed78", + "sha256:6132318e1ab84a626639b252137aa8d031a6c0550250460644c32ed997604088", + "sha256:680da98f12a7587f76f6f639a8aa7708936a5d17c5e7db0bf9c9d9cbcb616593", + "sha256:6e20bb765fcac07879cd6767b6dca58127ba5a456149717e0e3b1f00d8eab51c", + "sha256:74020ef8061678e01a40c49f1716b4f4d1cc71190d40633f08a5ef8a7448a5c6", + "sha256:75baba0bfdd385c886804970ae03f0172e0d51e51ebd191e4df09b929771b71e", + "sha256:847f2ed0887047c63da9ad788d54755579fa23f0784db7e752c7cf14cf2e7506", + "sha256:8849b8ef861431543c07112ad8436903e243cdfa783290cbee3df4ce86d8dd48", + "sha256:895a1e3aca2504638a802d0bec2759acc2f43a0291a1dff886d69f8b7baff399", + "sha256:99deae0504547d04990cc5acf631d9f490108c3709479d90c1dcd14d6e7af24d", + "sha256:ad79cd30c7e7484bdf6e315f3296f564b3ee2f453134a23ffc80d00e63b3b59e", + "sha256:b028776faf9b7a6d0a325664f899e4c670b2ae430265189eb8d76bd4a57d8a6e", + "sha256:b0a8f706b943c198dcedf1f2fb84899002c195c24745e47eeb8f2fb340f7dfc3", + "sha256:c65585ae03571b73907b8089473419d8c0aff1e3826b3bce153776de56cbc687", + "sha256:c6d341bc109fb8ea69025b3ec281fcb155d6824a8ebf5486c989ff7748351a37", + "sha256:d5d1135beffe9cd95d0350f19e2716bc38be47d5df296d7cc46e3b7557c0d1ff", + "sha256:db1fcbad5deb9551e011ca589c5e7258b5afa78598174ac37a5f15ddcfb4ac7b", + "sha256:e14de8800765b9916d051707f62e18a304cde661fa2b98a58816ca38d2b94029", + "sha256:e3d301e23984dcbc92d0e42253e0e0571915f0763f1eeaf68631348745f2dccc", + "sha256:ed3c28337d2fefc0bac5705b9c66b2702dc392f2e9a69badb1d606e7e7f773bb", + "sha256:edbb4de38535f42f020da1e3ae7c60f2f65402d027a08a8c60dc8569464873a6", + "sha256:f3b18663efe0012bc4c315f1b64020e44596f5fabc281f5b0d9bc9465288559c" ], - "version": "==0.17.0" + "version": "==0.18.0" }, "validators": { "hashes": [ @@ -1088,30 +1191,83 @@ }, "watchfiles": { "hashes": [ - "sha256:007dcc4a401093010b389c044e81172c8a2520dba257c88f8828b3d460c6bb38", - "sha256:08dc702529bb06a2b23859110c214db245455532da5eaea602921687cfcd23db", - "sha256:0d82dbc1832da83e441d112069833eedd4cf583d983fb8dd666fbefbea9d99c0", - "sha256:13f995d5152a8ba4ed7c2bbbaeee4e11a5944defc7cacd0ccb4dcbdcfd78029a", - "sha256:3796312bd3587e14926013612b23066912cf45a14af71cf2b20db1c12dadf4e9", - "sha256:5392dd327a05f538c56edb1c6ebba6af91afc81b40822452342f6da54907bbdf", - "sha256:570848706440373b4cd8017f3e850ae17f76dbdf1e9045fc79023b11e1afe490", - "sha256:608cd94a8767f49521901aff9ae0c92cc8f5a24d528db7d6b0295290f9d41193", - "sha256:728575b6b94c90dd531514677201e8851708e6e4b5fe7028ac506a200b622019", - "sha256:7d4e66a857621584869cfbad87039e65dadd7119f0d9bb9dbc957e089e32c164", - "sha256:835df2da7a5df5464c4a23b2d963e1a9d35afa422c83bf4ff4380b3114603644", - "sha256:87d9e1f75c4f86c93d73b5bd1ebe667558357548f11b4f8af4e0e272f79413ce", - "sha256:89d1de8218874925bce7bb2ae9657efc504411528930d7a83f98b1749864f2ef", - "sha256:99f4c65fd2fce61a571b2a6fcf747d6868db0bef8a934e8ca235cc8533944d95", - "sha256:9a0351d20d03c6f7ad6b2e8a226a5efafb924c7755ee1e34f04c77c3682417fa", - "sha256:9b5c8d3be7b502f8c43a33c63166ada8828dbb0c6d49c8f9ce990a96de2f5a49", - "sha256:a03d1e6feb7966b417f43c3e3783188167fd69c2063e86bad31e62c4ea794cc5", - "sha256:b17d4176c49d207865630da5b59a91779468dd3e08692fe943064da260de2c7c", - "sha256:d0002d81c89a662b595645fb684a371b98ff90a9c7d8f8630c82f0fde8310458", - "sha256:d97db179f7566dcf145c5179ddb2ae2a4450e3a634eb864b09ea04e68c252e8e", - "sha256:e43af4464daa08723c04b43cf978ab86cc55c684c16172622bdac64b34e36af0", - "sha256:eccc8942bcdc7d638a01435d915b913255bbd66f018f1af051cd8afddb339ea3" + "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc", + "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365", + "sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0", + "sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e", + "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124", + "sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c", + "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317", + "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094", + "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7", + "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235", + "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c", + "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c", + "sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c", + "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235", + "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293", + "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa", + "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef", + "sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19", + "sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8", + "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d", + "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915", + "sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429", + "sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097", + "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe", + "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0", + "sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d", + "sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99", + "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1", + "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a", + "sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895", + "sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94", + "sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562", + "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab", + "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360", + "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1", + "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7", + "sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f", + "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03", + "sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01", + "sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58", + "sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052", + "sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e", + "sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765", + "sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6", + "sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137", + "sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85", + "sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca", + "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f", + "sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214", + "sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7", + "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7", + "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3", + "sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b", + "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7", + "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6", + "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994", + "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9", + "sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec", + "sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128", + "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c", + "sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2", + "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078", + "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3", + "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e", + "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a", + "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6", + "sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49", + "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b", + "sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28", + "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9", + "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586", + "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400", + "sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165", + "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303", + "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d" ], - "version": "==0.20.0" + "version": "==0.21.0" }, "websockets": { "hashes": [ @@ -1189,5 +1345,16 @@ "version": "==11.0.3" } }, - "develop": {} + "develop": { + "pysocks": { + "hashes": [ + "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299", + "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", + "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0" + ], + "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.7.1" + } + } } diff --git a/api/main.py b/api/main.py index fc5e01f..1f08478 100644 --- a/api/main.py +++ b/api/main.py @@ -3,9 +3,10 @@ import logging from typing import Literal from apscheduler.schedulers.asyncio import AsyncIOScheduler -from fastapi import FastAPI, Request, Response, BackgroundTasks, Depends, HTTPException +from apscheduler.triggers.cron import CronTrigger +from fastapi import FastAPI, Request, Response, Depends, HTTPException from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import RedirectResponse, FileResponse +from fastapi.responses import RedirectResponse, FileResponse, StreamingResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates @@ -18,8 +19,9 @@ from streaming_providers.seedr.api import router as seedr_router 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.utils import get_direct_link_from_debridlink -from utils import crypto, torrent +from utils import crypto, torrent, poster from utils.const import CATALOG_ID_DATA, CATALOG_NAME_DATA +from scrappers import tamil_blasters, tamilmv logging.basicConfig( format="%(levelname)s::%(asctime)s - %(message)s", @@ -57,7 +59,8 @@ async def init_db(): @app.on_event("startup") async def start_scheduler(): scheduler = AsyncIOScheduler() - # scheduler.add_job(tb_scrapper.run_schedule_scrape, CronTrigger(hour="*/3")) + scheduler.add_job(tamil_blasters.run_schedule_scrape, CronTrigger(hour="*/3")) + scheduler.add_job(tamilmv.run_schedule_scrape, CronTrigger(hour="*/3")) scheduler.start() app.state.scheduler = scheduler @@ -176,28 +179,35 @@ async def get_catalog( @app.get( - "/{secret_str}/catalog/{catalog_type}/tamil_blasters/search={search_query}.json", - response_model=schemas.Metas, + "/catalog/{catalog_type}/{catalog_id}/search={search_query}.json", tags=["search"], -) -@app.get( - "/catalog/{catalog_type}/tamil_blasters/search={search_query}.json", response_model=schemas.Metas, - tags=["search"], + response_model_exclude_none=True, ) async def search_movie( response: Response, catalog_type: Literal["movie", "series"], + catalog_id: Literal["mediafusion_search_movies", "mediafusion_search_series"], search_query: str, ): response.headers.update(headers) - logging.debug("Searching for %s", search_query) + logging.debug("Searching for %s : %s", catalog_id, search_query) return await crud.process_search_query(search_query, catalog_type) -@app.get("/{secret_str}/meta/{catalog_type}/{meta_id}.json", tags=["meta"]) -@app.get("/meta/{catalog_type}/{meta_id}.json", tags=["meta"]) +@app.get( + "/{secret_str}/meta/{catalog_type}/{meta_id}.json", + tags=["meta"], + response_model=schemas.Meta, + response_model_exclude_none=True, +) +@app.get( + "/meta/{catalog_type}/{meta_id}.json", + tags=["meta"], + response_model=schemas.Meta, + response_model_exclude_none=True, +) async def get_meta( catalog_type: Literal["movie", "series"], meta_id: str, response: Response ): @@ -301,6 +311,27 @@ async def streaming_provider_endpoint( return RedirectResponse(url=video_url, headers=response.headers) +@app.get("/poster/{catalog_type}/{mediafusion_id}.jpg", tags=["poster"]) +async def get_poster(catalog_type: Literal["movie", "series"], mediafusion_id: str): + # Query the MediaFusion data + if catalog_type == "movie": + mediafusion_data = await crud.get_movie_data_by_id(mediafusion_id) + else: + mediafusion_data = await crud.get_series_data_by_id(mediafusion_id) + + if not mediafusion_data: + raise HTTPException(status_code=404, detail="MediaFusion ID not found.") + + try: + image_byte_io = await poster.create_poster(mediafusion_data) + return StreamingResponse( + image_byte_io, media_type="image/jpeg", headers=headers + ) + except Exception as e: + logging.error(f"Unexpected error while creating poster: {e}", exc_info=True) + raise HTTPException(status_code=404, detail="Failed to create poster.") + + app.include_router(seedr_router, prefix="/seedr", tags=["seedr"]) app.include_router(realdebrid_router, prefix="/realdebrid", tags=["realdebrid"]) app.include_router(debridlink_router, prefix="/debridlink", tags=["debridlink"]) \ No newline at end of file diff --git a/db/crud.py b/db/crud.py index ac7223d..1790977 100644 --- a/db/crud.py +++ b/db/crud.py @@ -3,76 +3,43 @@ from typing import Optional from uuid import uuid4 from beanie import WriteRules -from imdb import Cinemagoer, IMDbDataAccessError +from beanie.operators import In from db import schemas +from db.config import settings from db.models import ( MediaFusionMovieMetaData, MediaFusionSeriesMetaData, Streams, Season, Episode, - MediaFusionMetaData, ) from db.schemas import Stream, MetaIdProjection -from utils.parser import parse_stream_data, get_catalogs - -ia = Cinemagoer() +from utils.parser import parse_stream_data, get_catalogs, search_imdb async def get_meta_list( - catalog_type: str, catalog: str, skip: int = 0, limit: int = 100 + catalog_type: str, catalog: str, skip: int = 0, limit: int = 25 ) -> list[schemas.Meta]: - - pipeline = [ - # Lookup to join with the Streams collection - { - "$lookup": { - "from": "Streams", - "localField": "streams.$id", - "foreignField": "_id", - "as": "joined_streams", - } - }, - # Unwind the joined streams to process each stream separately - {"$unwind": "$joined_streams"}, - # Match the series based on catalog - {"$match": {"joined_streams.catalog": catalog}}, - # Group by series ID and determine the latest date - { - "$group": { - "_id": "$_id", - "latest_date": {"$max": "$joined_streams.created_at"}, - "title": {"$first": "$title"}, - "poster": {"$first": "$poster"}, - "type": {"$first": "$type"}, - } - }, - # Rename _id to id - { - "$project": { - "id": "$_id", - "latest_date": 1, - "title": 1, - "poster": 1, - "type": 1, - } - }, - # Sort by the latest_date in descending order - {"$sort": {"latest_date": -1}}, - # Pagination - {"$skip": skip}, - {"$limit": limit}, - ] - if catalog_type == "movie": - meta_list = await MediaFusionMovieMetaData.aggregate( - pipeline, projection_model=schemas.Meta - ).to_list() + meta_class = MediaFusionMovieMetaData else: - meta_list = await MediaFusionSeriesMetaData.aggregate( - pipeline, projection_model=schemas.Meta - ).to_list() + meta_class = MediaFusionSeriesMetaData + + meta_list = ( + await meta_class.find( + In(meta_class.streams.catalog, [catalog]), + fetch_links=True, + ) + .sort(-meta_class.streams.created_at) + .skip(skip) + .limit(limit) + .project(schemas.Meta) + .to_list() + ) + for meta in meta_list: + meta.poster = f"{settings.host_url}/poster/{catalog_type}/{meta.id}.jpg" + return meta_list @@ -124,10 +91,10 @@ async def get_movie_meta(meta_id: str): return { "meta": { - "id": meta_id, + "_id": meta_id, "type": "movie", - "name": movie_data.title, - "poster": movie_data.poster, + "title": movie_data.title, + "poster": f"{settings.host_url}/poster/movie/{meta_id}.jpg", "background": movie_data.poster, } } @@ -141,10 +108,10 @@ async def get_series_meta(meta_id: str): metadata = { "meta": { - "id": meta_id, + "_id": meta_id, "type": "series", - "name": series_data.title, - "poster": series_data.poster, + "title": series_data.title, + "poster": f"{settings.host_url}/poster/series/{meta_id}.jpg", "background": series_data.poster, "videos": [], } @@ -168,20 +135,6 @@ async def get_series_meta(meta_id: str): return metadata -def search_imdb(title: str, year: int, retry: int = 5) -> dict: - try: - result = ia.search_movie(f"{title} {year}") - except IMDbDataAccessError: - return search_imdb(title, year, retry - 1) if retry > 0 else {} - for movie in result: - if movie.get("year") == year and movie.get("title").lower() in title.lower(): - return { - "imdb_id": f"tt{movie.movieID}", - "poster": movie.get("full-size cover url"), - } - return {} - - async def save_movie_metadata(metadata: dict): # Try to get the existing movie existing_movie = await MediaFusionMovieMetaData.find_one( @@ -200,8 +153,10 @@ async def save_movie_metadata(metadata: dict): meta_id = f"mf{uuid4().fields[-1]}" # Update the poster from IMDb if available poster = imdb_data.get("poster") or metadata["poster"] + background = imdb_data.get("background") or metadata["poster"] else: poster = existing_movie.poster + background = existing_movie.background meta_id = existing_movie.id # Determine file index for the main movie file (largest file) @@ -255,6 +210,7 @@ async def save_movie_metadata(metadata: dict): title=metadata["title"], year=metadata["year"], poster=poster, + background=background, streams=[new_stream], ) await movie_data.insert(link_rule=WriteRules.WRITE) @@ -277,6 +233,7 @@ async def save_series_metadata(metadata: dict): if not series: meta_id = meta_id or f"mf{uuid4().fields[-1]}" poster = imdb_data.get("poster") or metadata["poster"] + background = imdb_data.get("background") or metadata["poster"] # Create an initial entry for the series series = MediaFusionSeriesMetaData( @@ -284,6 +241,7 @@ async def save_series_metadata(metadata: dict): title=metadata["title"], year=metadata["year"], poster=poster, + background=background, streams=[], ) await series.insert() @@ -353,15 +311,18 @@ async def save_series_metadata(metadata: dict): async def process_search_query(search_query: str, catalog_type: str) -> dict: - # Define the query with the text search and catalog type - query = {"$text": {"$search": search_query}, "type": catalog_type} + if catalog_type == "movie": + meta_class = MediaFusionMovieMetaData + else: + meta_class = MediaFusionSeriesMetaData - # Perform the search search_results = ( - await MediaFusionMetaData.find(query).project(MetaIdProjection).to_list() + await meta_class.find({"$text": {"$search": search_query}}) + .project(MetaIdProjection) + .to_list() ) - logging.debug( + logging.info( "Found %s results for %s in %s", len(search_results), search_query, catalog_type ) @@ -377,7 +338,7 @@ async def process_search_query(search_query: str, catalog_type: str) -> dict: if not meta: continue - metas.append(meta["meta"]) # Extract the 'meta' key from the result + metas.append(meta["meta"]) return {"metas": metas} diff --git a/db/models.py b/db/models.py index 45d0f97..feade45 100644 --- a/db/models.py +++ b/db/models.py @@ -55,7 +55,9 @@ class MediaFusionMetaData(Document): title: str year: Optional[int] poster: str + background: str streams: list[Link[Streams]] + type: str class Settings: is_root = True diff --git a/db/schemas.py b/db/schemas.py index 84493ab..0ba4df5 100644 --- a/db/schemas.py +++ b/db/schemas.py @@ -12,10 +12,11 @@ class Catalog(BaseModel): class Meta(BaseModel): - id: str + id: str = Field(alias="_id") name: str = Field(alias="title") type: str = Field(default="movie") poster: str + background: str videos: list | None = None @@ -57,4 +58,4 @@ class AuthorizeData(BaseModel): class MetaIdProjection(BaseModel): - id: str + id: str = Field(alias="_id") diff --git a/resources/images/logo_text.png b/resources/images/logo_text.png new file mode 100644 index 0000000000000000000000000000000000000000..8a5b6f0d09be916757f802cd582a2e5e3935300f GIT binary patch literal 13044 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%0DL(C|p7iA$MO`=;aFJyjnMX zRw2s9uaL~YSe61M-#;~!#XNo#4o7%K&|%@A=de34Fqfe9WymV6+(l6L2rvWJHMnDH zs=t=U1n~cpmdBDfl&limy|+$}p(6w|JD)z|+Wng3U?`Wb+?`qA64?u`=oT1E{Nwe^ zZC1p@2(WLBT4Q#s>D)k}45Q;xhqRIsw1PsDa4nrvf>Q%PG>`DLZ3Q0QzL7Kxe3ytF_RYhbx&nhgo_#1c+%TK`Je@ z0ngObc!66Dpk^zz%y)C&1KWN);T`Wtj92D#FM9(<-sgYF5Gd=tdhL)vyO=PwY~SY2 zh1`X2bC{D02Bp%wlLD=SCR{K!n=O-EDvQwVwv~l3r6dWUIgDyDCF61Gq&?R#Y={#P ziyZP~@wD9zla&!v78_|!fa?JONvrkLd3-(lKGFf`0MEh4dpew=48m+BwH9Y$QTEdz z356vfK#C#(Q76n_0!uEDxhV?u@$P?T5_ z_BZzA-r}=M%S)Y))Vc8NYPf7yK}TATGIJO}t5(+j`|Y7n_&jOBKX76{WG!#~ynX!m z1ZNgG6I34k%YW>(wk=`3s!FvRkXQQa&<*2DzZ4}CkyGa5t~GTI^??D~D-RKJyLh| z_8Pito*_y~l-a4Y{7n&Uj!F7)&^1|2Hp8`~QFWj${fO@z5~gMFf0f1tcXVFIp~a G5b!@W#hqmU literal 0 HcmV?d00001 diff --git a/utils/parser.py b/utils/parser.py index 48cf425..e4573f3 100644 --- a/utils/parser.py +++ b/utils/parser.py @@ -1,6 +1,9 @@ import math import re +import requests +from imdb import Cinemagoer, IMDbDataAccessError + from db.config import settings from db.models import Streams from db.schemas import Stream, UserData @@ -8,6 +11,8 @@ from streaming_providers.realdebrid.utils import ( order_streams_by_instant_availability_and_date, ) +ia = Cinemagoer() + def parse_stream_data( streams: list[Streams], @@ -120,3 +125,27 @@ def get_catalogs(catalog: str, languages: list[str]) -> list[str]: # Generate the catalog for each supported language return [f"{lang.lower()}_{base_catalog}" for lang in languages] + + +def search_imdb(title: str, year: int, retry: int = 5) -> dict: + try: + result = ia.search_movie(f"{title} {year}") + except IMDbDataAccessError: + return search_imdb(title, year, retry - 1) if retry > 0 else {} + for movie in result: + if movie.get("year") == year and movie.get("title").lower() in title.lower(): + imdb_id = f"tt{movie.movieID}" + poster = f"https://live.metahub.space/poster/small/{imdb_id}/img" + if requests.get(poster).status_code == 200: + return { + "imdb_id": imdb_id, + "poster": poster.replace("small", "medium"), + "background": f"https://live.metahub.space/background/medium/{imdb_id}/img", + } + poster = movie.get("full-size cover url") + return { + "imdb_id": imdb_id, + "poster": poster, + "background": poster, + } + return {} diff --git a/utils/poster.py b/utils/poster.py new file mode 100644 index 0000000..ce46988 --- /dev/null +++ b/utils/poster.py @@ -0,0 +1,76 @@ +from io import BytesIO + +from PIL import Image, ImageDraw, ImageFont +from imdb import Cinemagoer +import requests + +from db.models import MediaFusionMetaData + +ia = Cinemagoer() + + +async def create_poster(mediafusion_data: MediaFusionMetaData) -> BytesIO: + response = requests.get(mediafusion_data.poster, timeout=10) + + response.raise_for_status() + imdb_rating = None + if mediafusion_data.id.startswith("tt"): + result = ia.get_movie(mediafusion_data.id[2:], info="main") + imdb_rating = result.get("rating") + + image = Image.open(BytesIO(response.content)) + image = image.resize((300, 450)) + image = add_elements_to_poster(image, imdb_rating) + image = image.convert("RGB") + + byte_io = BytesIO() + image.save(byte_io, "JPEG") + byte_io.seek(0) + + return byte_io + + +def add_elements_to_poster( + image: Image.Image, imdb_rating: float = None +) -> Image.Image: + draw = ImageDraw.Draw(image) + margin = 10 + + # Adding IMDb rating at the bottom left with a semi-transparent background + if imdb_rating: + font = ImageFont.truetype("Arial.ttf", size=24) + imdb_text = f"IMDb: {imdb_rating}/10" + + # Calculate text bounding box using the draw instance + left, top, right, bottom = draw.textbbox((0, 0), imdb_text, font=font) + text_width = right - left + text_height = bottom - top + + # Draw a semi-transparent rectangle behind the text for better visibility + rectangle_x0 = margin + rectangle_y0 = image.height - text_height - margin - 5 # 5 for a little padding + rectangle_x1 = rectangle_x0 + text_width + 10 # 10 for padding + rectangle_y1 = image.height - margin + + draw.rectangle( + (rectangle_x0, rectangle_y0, rectangle_x1, rectangle_y1), + fill=(0, 0, 0, 128), + ) + draw.text( + (rectangle_x0 + 5, rectangle_y0), imdb_text, font=font, fill="#F5C518" + ) # 5 for padding + + # Add MediaFusion watermark at the top right + watermark = Image.open("resources/images/logo_text.png") + + # Resizing the watermark to fit the new poster size + aspect_ratio = watermark.width / watermark.height + new_width = int(image.width * 0.5) # Reduced size for better aesthetics + new_height = int(new_width / aspect_ratio) + watermark = watermark.resize((new_width, new_height)) + + # Position watermark at top right + watermark_position = (image.width - watermark.width - margin, margin) + image.paste(watermark, watermark_position, watermark) + + return image From 315528f1b1c0cbc4efabafc15571480119ff385e Mon Sep 17 00:00:00 2001 From: mhdzumair Date: Mon, 16 Oct 2023 20:37:17 +0530 Subject: [PATCH 7/9] add details about running https server for serve posters --- README.md | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6c99518..5d89437 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ - **Python**: This project uses Python version 3.11. Ensure you have it installed. - **MongoDB**: Set up a MongoDB server. You can use [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) to create a free MongoDB cluster. +- **mkcert**: To set up local HTTPS, you'll need to use mkcert to generate SSL certificates. If not installed, get it from [here](https://github.com/FiloSottile/mkcert). ### Setup @@ -44,9 +45,35 @@ ```bash MONGO_URI= SECRET_KEY= - HOST_URL=http://127.0.0.1:8000 + HOST_URL=https://localhost:8443 ``` -4. **For scraping instructions**: refer to the [scrapping README](/scrappers/README.md). +4. **Local HTTPS Setup**: + + - Navigate to the MediaFusion directory. + - Generate local SSL certificates using mkcert: + + ```bash + mkcert -install + mkcert localhost + ``` + + This will generate two files: localhost.pem and localhost-key.pem. + +5. **Run Servers**: + + - To serve your application over HTTPS on port 8443: + + ```bash + pipenv run uvicorn main:app --host 0.0.0.0 --port 8443 --ssl-keyfile localhost-key.pem --ssl-certfile localhost.pem + ``` + + - Since Stremio doesn't support localhost HTTPS servers to install add-on, also run an HTTP server on port 8000: + + ```bash + pipenv run uvicorn main:app --host 0.0.0.0 --port 8000 + ``` + +6. **For scraping instructions**: refer to the [scrapping README](/scrappers/README.md). ## :books: References From f8743ed133e900789d58c04606b184015345bc8c Mon Sep 17 00:00:00 2001 From: mhdzumair Date: Mon, 16 Oct 2023 20:40:42 +0530 Subject: [PATCH 8/9] dump add-on version to 3.4.0 --- resources/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/manifest.json b/resources/manifest.json index 26bc8d0..4dc32b0 100644 --- a/resources/manifest.json +++ b/resources/manifest.json @@ -1,6 +1,6 @@ { "id": "mhdzumair.addons.mediafusion", - "version": "3.3.0", + "version": "3.4.0", "name": "Media Fusion", "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", From 1e0506ae8b78cd8b535754683eaeab42cbb2bb99 Mon Sep 17 00:00:00 2001 From: mhdzumair Date: Mon, 16 Oct 2023 21:45:55 +0530 Subject: [PATCH 9/9] capture premium account exception for RD & DL --- beamup.json | 2 +- resources/exceptions/need_premium.mp4 | Bin 0 -> 103694 bytes streaming_providers/debridlink/client.py | 49 ++++++++++++++++++----- streaming_providers/realdebrid/client.py | 7 ++++ utils/exception_video.py | 8 ++-- 5 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 resources/exceptions/need_premium.mp4 diff --git a/beamup.json b/beamup.json index affa6ac..ed06e13 100644 --- a/beamup.json +++ b/beamup.json @@ -1,4 +1,4 @@ { "projectName": "mediafusion", - "lastCommit": "b3d3866" + "lastCommit": "f8743ed" } \ No newline at end of file diff --git a/resources/exceptions/need_premium.mp4 b/resources/exceptions/need_premium.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..3e22a4bd5808cee9614f48235de2eb9bc75d8b00 GIT binary patch literal 103694 zcma&NbC@O1vNpQfwrx$@nzn75)3$BfHl{gk+nBa(+wQ*oJLm4Rzx|#2$9PXzJu{XYOcg4FCWG{yBetnG9Tv7_IGC7y$qPq^X062>{3@!P?ls z>D%THIM~-$#k$Bz=i!QYa|+Ee;R?y+l^YWq3n49`k*$LXA>+5`z|6qON@!?cV#30} z@LeHJ_iaEgBQGLG%R(riD)en>Y-03XA!KXkZe?QXM99Rzz)Z`;z{K>eG;?yYT^Zj6k~gw`fJW+rZgj?RYP5<8)tqx-k(_fgNm zn1_*$;oIr^Kxl35W@4=O402JkbNKf724W(#vbD7|F#9g){Y%M2=xAkb^c~E<6bytm4*%7Nk-4>j(?5Zj+c=px zSQ&g9ed`UaoE;3@^^9z-?F^j0%SPX0Zv4+9uGpJgfkh6YZ;zpKoqM@$gu zcD!1idj}jYl0`kj)hPCwA%&AHu5auEm4-m;?f4unRhlXv%KVc)sA0wN*L5^F~j?cz#Uk4-xXl zd~#0-sw~fH-mHU`?XDu<5I?);b)-KgK{ZsH!lYK~(k5-Vt2wQqg@k;05y?P*r{#Mtm5wfUTZ*`my4fYek~{DlHHAQrgK@=Sane zlA4vQH34PzYxw-kJko)i1M24Zz2m>I)=jd|peMd$U}l?9DZc)Zh!LCrJCdUs z?ru}ep1L)d%?1y+2?xPi1sNJu_{+feOG5K}{wcUdlATX8Lgzg)wCZ(PfTJX2YwEp( ze=glrCI)^_7%mR6c4xa7d=G?7wQ%OPD1AX>bmchT*6?mF(0?b7FLgynmV2{CtyslE ztiUa2OPjri{YyRA5RAudTQBMWMzxZ;*lRrxLRFe5Lq%E@U_<4uJ==#2t&QB*?sk`~CmndORT=r9 zaia78wa8v*7^7x@y3syN5%+e+d~6ZOffCmpY{Rd%kR(@nf;u3`_IIMTGigB1*3~3e z=ga{3`uQE<7~+){oTp8>&UWUd7;pW47EIsM#In6G20h?mXUt}>ED7$kW*l~$VZPw; z8IPA-Ic)Dwwcsiy-R!l0j%{6J3F{e9Cv6;N;lgWc&wNC1JUOt2`#h~NkSy}*3cb{Jg;WlzJHS3{7F3Oo#h@RFeqxPzjGvY=4 zQTgRx|0LpCDRCMR}Z#Zoc@eK6HzQn==&>BZex`HOTq^ z15wRxj3~FRjDjza+mkv}m_tVjXoB4*U|QYE@KlCBIql()Y83!W3XNAG1nvdu3Rjc; zUlokzv4M>&J?4`UM7??@z|m}Lu;Ae6Q`*YwN%QzJQ#e7F6F3m1^ZX?U%}!6UJ%fKM zbf3jI14^cO|G=hbK-ecNsJBcfay?K17uK_UjJ5-nWB}2qp&zL6&=9%V8 z%j|i1`ii@^rW+-N2Nlg|Eso`K^H$md4BtSDEf-|0L|19Nsxa`$D|bw8r%4mp@SpM7 z^|IwIZ*{DFEpN@?F1!PX*}pWMs!d1Kg}CHxiDwnE;r~SQ8s=K2=Okmk-mu}(>ENFB zU`Edq@SQreHpe7|HUW1X`g@Nv-)fpwsX;MHA{mea=-xDLwd2yg4S9 zpr!Qq{Na0(Z*(sh?4eh()}a`kK)U9jg8!k=KmEIq?DtR*fz9JcbS7Z7i;vySu^neE zGc@f7E;r>M_osIZkEvA>T8Ga3c}$65ch`EpJH6|Ax^M(LJh;P&3Vmzu>Mw8%|7Ff; z@WbgZ1(f{R8wyqBQWpu%KlgBel;aFcXEwy(iQ426?G2`pVb0nbipJgftaqObyMZq( z$|=hh)Q&``h3vA^(bb2@$f+ot$v}Y)K}X))_}LQY@*a_CWw%~bbG&Qy_(oMIBs#Z? z>k0MDQ#v7N1i>%JvQ7WVlmI-t-%_ad{XktDR>faLR9QN}??%d?KT>7XeifSCl4dX7 z{Mp-X&u~7VuV!2eTU{~DmL|?WE($Bw4_PbAl5KlLLit#tm^%LK zChCR@Iq6#>B5eE3mAl4*y|3g;A6Ru>e0T=1J)93u&BnHa8-}sK$UV=ptaaArj8t

Y5_Xwp#x)+34j20;`?CV&!-$!xWJ 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 dict: if method == "GET": response = requests.get(url, params=params, headers=self.headers) @@ -43,6 +49,13 @@ class DebridLink: pass elif error.response.status_code == 401: raise ProviderException("Invalid token", "invalid_token.mp4") + elif ( + error.response.status_code == 400 + and response.json().get("error") == "freeServerOverload" + ): + raise ProviderException( + "Debrid-Link free servers are overloaded", "need_premium.mp4" + ) else: formatted_traceback = "".join(traceback.format_exception(error)) raise ProviderException( @@ -64,8 +77,12 @@ class DebridLink: def initialize_headers(self): if self.encoded_token: token_data = self.decode_token_str(self.encoded_token) - access_token_data = self.refresh_token(token_data["client_id"], token_data["code"]) - self.headers = {"Authorization": f"Bearer {access_token_data['access_token']}"} + access_token_data = self.refresh_token( + token_data["client_id"], token_data["code"] + ) + self.headers = { + "Authorization": f"Bearer {access_token_data['access_token']}" + } @staticmethod def encode_token_data(client_id: str, code: str): @@ -86,7 +103,7 @@ class DebridLink: f"{self.OAUTH_URL}/device/code", data={ "client_id": self.OPENSOURCE_CLIENT_ID, - "scope": "get.post.downloader get.post.seedbox get.account get.files get.post.stream" + "scope": "get.post.downloader get.post.seedbox get.account get.files get.post.stream", }, ) @@ -101,7 +118,7 @@ class DebridLink: }, is_expected_to_fail=True, ) - + def refresh_token(self, client_id, refresh_token): return self._make_request( "POST", @@ -128,27 +145,37 @@ class DebridLink: return token_data def add_magent_link(self, magnet_link): - return self._make_request("POST", f"{self.BASE_URL}/seedbox/add", data={"url": magnet_link}) + return self._make_request( + "POST", f"{self.BASE_URL}/seedbox/add", data={"url": magnet_link} + ) def get_user_torrent_list(self): return self._make_request("GET", f"{self.BASE_URL}/seedbox/list") - + def get_torrent_info(self, torrent_id): - return self._make_request("GET", f"{self.BASE_URL}/seedbox/list", data={"url": torrent_id}) + return self._make_request( + "GET", f"{self.BASE_URL}/seedbox/list", data={"url": torrent_id} + ) def get_torrent_files_list(self, torrent_id): return self._make_request("GET", f"{self.BASE_URL}/files/{torrent_id}/list") def get_torrent_instant_availability(self, torrent_hash): - return self._make_request("GET", f"{self.BASE_URL}/seedbox/cached/", data={"url": torrent_hash}) + return self._make_request( + "GET", f"{self.BASE_URL}/seedbox/cached/", data={"url": torrent_hash} + ) def disable_access_token(self): - return self._make_request("GET", f"{self.OAUTH_URL}/revoke", is_return_none=True) + return self._make_request( + "GET", f"{self.OAUTH_URL}/revoke", is_return_none=True + ) def get_available_torrent(self, info_hash: str) -> dict[str, Any] | None: torrent_list_response = self.get_user_torrent_list() if "error" in torrent_list_response: - raise ProviderException("Failed to get torrent info from Debrid-Link", "transfer_error.mp4") + raise ProviderException( + "Failed to get torrent info from Debrid-Link", "transfer_error.mp4" + ) available_torrents = torrent_list_response["value"] for torrent in available_torrents: diff --git a/streaming_providers/realdebrid/client.py b/streaming_providers/realdebrid/client.py index fd11a0c..1d43a95 100644 --- a/streaming_providers/realdebrid/client.py +++ b/streaming_providers/realdebrid/client.py @@ -47,6 +47,13 @@ class RealDebrid: pass elif error.response.status_code == 401: raise ProviderException("Invalid token", "invalid_token.mp4") + elif ( + error.response.status_code == 403 + and response.json().get("error_code") == 9 + ): + raise ProviderException( + "Real-Debrid Permission denied for free account", "need_premium.mp4" + ) else: formatted_traceback = "".join(traceback.format_exception(error)) raise ProviderException( diff --git a/utils/exception_video.py b/utils/exception_video.py index 573fa8d..573d299 100644 --- a/utils/exception_video.py +++ b/utils/exception_video.py @@ -1,11 +1,11 @@ import os -def create_text_video(output_path, text, duration=30, resolution=(1280, 720), fontsize=45, bgcolor="black"): +def create_text_video( + output_path, text, duration=30, resolution=(1280, 720), fontsize=45, bgcolor="black" +): # Create a solid color background using FFmpeg - background_command = ( - f"ffmpeg -f lavfi -i color=c={bgcolor}:s={resolution[0]}x{resolution[1]} -t {duration} -y background.mp4" - ) + background_command = f"ffmpeg -f lavfi -i color=c={bgcolor}:s={resolution[0]}x{resolution[1]} -t {duration} -y background.mp4" os.system(background_command) # Overlay the text with shadow and fade-in and fade-out effects