From 64773baecb7bb454f10b9e449e68fb4c0ba4ea97 Mon Sep 17 00:00:00 2001 From: Mohamed Zumair Date: Sat, 28 Sep 2024 09:44:53 +0530 Subject: [PATCH] Add torrent blocking feature for DMCA (#302) --- api/middleware.py | 2 +- db/crud.py | 20 +++++++++++++---- db/models.py | 1 + db/schemas.py | 5 +++++ resources/html/scraper.html | 13 +++++++++-- resources/js/scraperControl.js | 41 ++++++++++++++++++++++++++++++++++ scrapers/routes.py | 22 ++++++++++++++++-- 7 files changed, 95 insertions(+), 9 deletions(-) diff --git a/api/middleware.py b/api/middleware.py index 4de2323..f5d5da8 100644 --- a/api/middleware.py +++ b/api/middleware.py @@ -59,7 +59,7 @@ class UserDataMiddleware(BaseHTTPMiddleware): user_data = crypto.decrypt_user_data(secret_str) # validate api password if set - if settings.api_password and settings.is_public_instance is False: + if settings.is_public_instance is False: is_auth_required = getattr(endpoint, "auth_required", False) if is_auth_required and user_data.api_password != settings.api_password: return Response( diff --git a/db/crud.py b/db/crud.py index 32d0d40..2408e31 100644 --- a/db/crud.py +++ b/db/crud.py @@ -64,6 +64,7 @@ async def get_meta_list( else: query_filters = {"catalog": {"$in": [catalog]}} + query_filters["is_blocked"] = {"$ne": True} match_filter = { "type": catalog_type, } @@ -265,7 +266,9 @@ async def get_cached_torrent_streams( # If the data is not in the cache, query it from the database if season is not None and episode is not None: streams = ( - await TorrentStreams.find({"meta_id": video_id}) + await TorrentStreams.find( + {"meta_id": video_id, "is_blocked": {"$ne": True}} + ) .find( { "season.season_number": season, @@ -277,7 +280,9 @@ async def get_cached_torrent_streams( ) else: streams = ( - await TorrentStreams.find({"meta_id": video_id}) + await TorrentStreams.find( + {"meta_id": video_id, "is_blocked": {"$ne": True}} + ) .sort("-updated_at") .to_list() ) @@ -802,7 +807,12 @@ async def process_search_query( "localField": "_id", "foreignField": "meta_id", "pipeline": [ - {"$match": {"catalog": {"$in": user_data.selected_catalogs}}}, + { + "$match": { + "catalog": {"$in": user_data.selected_catalogs}, + "is_blocked": {"$ne": True}, + } + }, {"$count": "num_torrents"}, ], "as": "torrent_count", @@ -885,7 +895,9 @@ async def process_tv_search_query(search_query: str, namespace: str) -> dict: async def get_stream_by_info_hash(info_hash: str) -> TorrentStreams | None: - stream = await TorrentStreams.get(info_hash) + stream = await TorrentStreams.find_one( + {"_id": info_hash, "is_blocked": {"$ne": True}} + ) return stream diff --git a/db/models.py b/db/models.py index 06c2e8b..41bb9fb 100644 --- a/db/models.py +++ b/db/models.py @@ -45,6 +45,7 @@ class TorrentStreams(Document): seeders: Optional[int] = None cached: Optional[bool] = Field(default=False, exclude=True) indexer_flags: Optional[list[str]] = Field(default_factory=list) + is_blocked: Optional[bool] = False def __eq__(self, other): if not isinstance(other, TorrentStreams): diff --git a/db/schemas.py b/db/schemas.py index ecbd3a2..8eb4740 100644 --- a/db/schemas.py +++ b/db/schemas.py @@ -350,3 +350,8 @@ class TVMetaDataUpload(BaseModel): class KodiConfig(BaseModel): code: str = Field(max_length=6) manifest_url: HttpUrl + + +class BlockTorrent(BaseModel): + info_hash: str + api_password: str diff --git a/resources/html/scraper.html b/resources/html/scraper.html index 42b772c..ecaffd4 100644 --- a/resources/html/scraper.html +++ b/resources/html/scraper.html @@ -32,6 +32,7 @@ + @@ -41,7 +42,8 @@ + + @@ -255,7 +264,7 @@
- + diff --git a/resources/js/scraperControl.js b/resources/js/scraperControl.js index 0994221..a54852a 100644 --- a/resources/js/scraperControl.js +++ b/resources/js/scraperControl.js @@ -201,6 +201,7 @@ function updateFormFields() { setElementDisplay("imdbDataParameters", "none"); setElementDisplay("torrentUploadParameters", "none"); setElementDisplay("apiPasswordContainer", "none"); + setElementDisplay('blockTorrentParameters', 'none'); // Get the selected scraper type const scraperType = document.getElementById('scraperSelect').value; @@ -233,6 +234,9 @@ function updateFormFields() { case 'update_imdb_data': setElementDisplay("imdbDataParameters", "block"); break; + case 'block_torrent': + setElementDisplay("blockTorrentParameters", "block"); + break; default: // Optionally handle any default cases if needed break; @@ -524,6 +528,40 @@ async function handleScrapyParameters(payload, submitBtn, loadingSpinner) { } } +async function handleBlockTorrent(apiPassword, submitBtn, loadingSpinner) { + const infoHash = document.getElementById('blockTorrentInfoHash').value.trim(); + if (!infoHash) { + showNotification('Torrent Info Hash is required.', 'error'); + resetButton(submitBtn, loadingSpinner); + return; + } + + try { + const response = await fetch('/scraper/block_torrent', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + info_hash: infoHash, + api_password: apiPassword + }) + }); + + const data = await response.json(); + if (data.detail) { + showNotification(data.detail, 'error'); + } else { + showNotification(data.status, 'success'); + } + } catch (error) { + console.error('Error blocking torrent:', error); + showNotification(`Error blocking torrent. Error: ${error.toString()}`, 'error'); + } finally { + resetButton(submitBtn, loadingSpinner); + } +} + // Main function async function submitScraperForm() { const apiPassword = document.getElementById('api_password').value; @@ -550,6 +588,9 @@ async function submitScraperForm() { case 'update_imdb_data': await handleUpdateImdbData(submitBtn, loadingSpinner); break; + case 'block_torrent': + await handleBlockTorrent(apiPassword, submitBtn, loadingSpinner); + break; default: await handleScrapyParameters(payload, submitBtn, loadingSpinner); break; diff --git a/scrapers/routes.py b/scrapers/routes.py index f922e6f..11b341f 100644 --- a/scrapers/routes.py +++ b/scrapers/routes.py @@ -30,8 +30,8 @@ router = APIRouter() def validate_api_password(api_password: str): - if settings.api_password and api_password != settings.api_password: - raise HTTPException(status_code=200, detail="Invalid API password.") + if api_password != settings.api_password: + raise HTTPException(status_code=401, detail="Invalid API password.") return True @@ -377,3 +377,21 @@ async def handle_series_stream_store(info_hash, parsed_data, video_id, season): await store_new_torrent_streams([torrent_stream]) return torrent_stream + + +@router.post("/block_torrent", tags=["scraper"]) +async def block_torrent(block_data: schemas.BlockTorrent): + validate_api_password(block_data.api_password) + torrent_stream = await TorrentStreams.get(block_data.info_hash) + if not torrent_stream: + raise HTTPException(status_code=404, detail="Torrent not found.") + if torrent_stream.is_blocked: + return {"status": f"Torrent {block_data.info_hash} is already blocked."} + torrent_stream.is_blocked = True + try: + await torrent_stream.save() + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Failed to block torrent: {str(e)}" + ) + return {"status": f"Torrent {block_data.info_hash} has been successfully blocked."}