mirror of
https://github.com/Viren070/MediaFusion.git
synced 2025-12-01 23:21:11 +01:00
ec20feca52
Add support for telegram notification when unable to detect episode files, refactor file selection and update db function
173 lines
6.0 KiB
Python
173 lines
6.0 KiB
Python
import asyncio
|
|
from os import path
|
|
from typing import Optional, List
|
|
|
|
import aiohttp
|
|
|
|
from db.models import TorrentStreams
|
|
from streaming_providers.debrid_client import DebridClient
|
|
from streaming_providers.exceptions import ProviderException
|
|
from streaming_providers.parser import (
|
|
select_file_index_from_torrent,
|
|
)
|
|
|
|
|
|
class OffCloud(DebridClient):
|
|
BASE_URL = "https://offcloud.com"
|
|
|
|
async def initialize_headers(self):
|
|
pass
|
|
|
|
async def disable_access_token(self):
|
|
pass
|
|
|
|
async def _handle_service_specific_errors(self, error_data: dict, status_code: int):
|
|
if status_code == 403:
|
|
raise ProviderException("Invalid OffCloud API key", "invalid_token.mp4")
|
|
if status_code == 429:
|
|
raise ProviderException(
|
|
"OffCloud rate limit exceeded", "too_many_requests.mp4"
|
|
)
|
|
|
|
async def _make_request(
|
|
self,
|
|
method: str,
|
|
url: str,
|
|
params: Optional[dict] = None,
|
|
**kwargs,
|
|
) -> dict | list:
|
|
params = params or {}
|
|
params["key"] = self.token
|
|
full_url = self.BASE_URL + url
|
|
return await super()._make_request(
|
|
method=method, url=full_url, params=params, **kwargs
|
|
)
|
|
|
|
async def add_magnet_link(self, magnet_link: str) -> dict:
|
|
response_data = await self._make_request(
|
|
"POST", "/api/cloud", data={"url": magnet_link}
|
|
)
|
|
|
|
if "requestId" not in response_data:
|
|
if "not_available" in response_data:
|
|
raise ProviderException(
|
|
"Need premium OffCloud account to add this torrent",
|
|
"need_premium.mp4",
|
|
)
|
|
raise ProviderException(
|
|
f"Failed to add magnet link to OffCloud {response_data}",
|
|
"transfer_error.mp4",
|
|
)
|
|
return response_data
|
|
|
|
async def add_torrent_file(
|
|
self, torrent_file: bytes, torrent_name: Optional[str]
|
|
) -> dict:
|
|
data = aiohttp.FormData()
|
|
data.add_field(
|
|
"file",
|
|
torrent_file,
|
|
filename=torrent_name,
|
|
content_type="application/x-bittorrent",
|
|
)
|
|
response_data = await self._make_request("POST", "/torrent/upload", data=data)
|
|
if response_data.get("success") is False:
|
|
raise ProviderException(
|
|
f"Failed to add torrent file to OffCloud {response_data}",
|
|
"transfer_error.mp4",
|
|
)
|
|
return await self.add_magnet_link(response_data["url"])
|
|
|
|
async def get_user_torrent_list(self) -> List[dict]:
|
|
return await self._make_request("GET", "/api/cloud/history")
|
|
|
|
async def get_torrent_info(self, request_id: str) -> dict:
|
|
response = await self._make_request(
|
|
"POST", "/api/cloud/status", data={"requestIds": [request_id]}
|
|
)
|
|
return response.get("requests", [{}])[0]
|
|
|
|
async def get_torrent_instant_availability(self, magnet_links: List[str]) -> dict:
|
|
response = await self._make_request(
|
|
"POST", "/api/cache", data={"hashes": magnet_links}
|
|
)
|
|
return response.get("cachedItems", {})
|
|
|
|
async def get_available_torrent(self, info_hash: str) -> Optional[dict]:
|
|
available_torrents = await self.get_user_torrent_list()
|
|
return next(
|
|
(
|
|
torrent
|
|
for torrent in available_torrents
|
|
if info_hash.casefold() in torrent.get("originalLink", "").casefold()
|
|
),
|
|
None,
|
|
)
|
|
|
|
async def explore_folder_links(self, request_id: str) -> List[str]:
|
|
return await self._make_request("GET", f"/api/cloud/explore/{request_id}")
|
|
|
|
async def update_file_sizes(self, files_data: list[dict]):
|
|
"""
|
|
Update file sizes for a list of files by making HEAD requests.
|
|
|
|
Args:
|
|
files_data (list[dict]): List of file data dictionaries containing 'link' keys
|
|
|
|
Note:
|
|
This method modifies the input files_data list in-place, adding 'size' keys
|
|
where the HEAD request was successful.
|
|
"""
|
|
|
|
async def get_file_size(file_data: dict) -> tuple[dict, Optional[int]]:
|
|
"""Helper function to get file size for a single file."""
|
|
try:
|
|
async with self.session.head(
|
|
file_data["link"],
|
|
timeout=aiohttp.ClientTimeout(total=5),
|
|
allow_redirects=True,
|
|
) as response:
|
|
if response.status == 200:
|
|
return file_data, int(response.headers.get("Content-Length", 0))
|
|
except (aiohttp.ClientError, asyncio.TimeoutError):
|
|
pass
|
|
return file_data, 0
|
|
|
|
# Gather all HEAD requests with proper concurrency
|
|
tasks = [get_file_size(file_data) for file_data in files_data]
|
|
results = await asyncio.gather(*tasks, return_exceptions=False)
|
|
|
|
# Update file sizes in the original data
|
|
for file_data, size in results:
|
|
file_data["size"] = size
|
|
|
|
async def create_download_link(
|
|
self,
|
|
request_id: str,
|
|
torrent_info: dict,
|
|
stream: TorrentStreams,
|
|
filename: Optional[str],
|
|
season: Optional[int],
|
|
episode: Optional[int],
|
|
) -> str:
|
|
if not torrent_info["isDirectory"]:
|
|
return f"https://{torrent_info['server']}.offcloud.com/cloud/download/{request_id}/{torrent_info['fileName']}"
|
|
|
|
links = await self.explore_folder_links(request_id)
|
|
files_data = [{"name": path.basename(link), "link": link} for link in links]
|
|
|
|
file_index = await select_file_index_from_torrent(
|
|
torrent_info={"files": files_data},
|
|
torrent_stream=stream,
|
|
filename=filename,
|
|
season=season,
|
|
episode=episode,
|
|
file_size_callback=self.update_file_sizes,
|
|
)
|
|
|
|
selected_file_url = links[file_index]
|
|
return selected_file_url
|
|
|
|
async def delete_torrent(self, request_id: str) -> dict:
|
|
return await self._make_request("GET", f"/cloud/remove/{request_id}")
|