Skip to content

Commit

Permalink
feat: add manual torrent adding (#785)
Browse files Browse the repository at this point in the history
* feat: add manual torrent adding

* feat: add cache check when manually adding torrent
chore: remove not needed state transition

* fix: wrong import

---------

Co-authored-by: Filip Trplan <[email protected]>
  • Loading branch information
davidemarcoli and Filip Trplan authored Oct 26, 2024
1 parent 45528a9 commit acb22ce
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 5 deletions.
3 changes: 3 additions & 0 deletions src/program/downloaders/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ def download(self, item, active_stream: dict) -> str:
"DEBRID",
f"Downloaded {item.log_string} from '{item.active_stream['name']}' [{item.active_stream['infohash']}]",
)

def add_torrent(self, infohash: str):
return self.service.add_torrent(infohash)

def add_torrent_magnet(self, magnet_link: str):
return self.service.add_torrent_magnet(magnet_link)
Expand Down
6 changes: 3 additions & 3 deletions src/program/downloaders/torbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ def download(self, item: MediaItem):

# If it doesnt, lets download it and refresh the torrent_list
if not exists:
id = self.create_torrent(item.active_stream["hash"])
id = self.add_torrent(item.active_stream["hash"])
torrent_list = self.get_torrent_list()

# Find the torrent, correct file and we gucci
Expand Down Expand Up @@ -316,8 +316,8 @@ def get_torrent_cached(self, hash_list):
)
return response.data["data"]

def create_torrent(self, hash) -> int:
magnet_url = f"magnet:?xt=urn:btih:{hash}&dn=&tr="
def add_torrent(self, infohash) -> int:
magnet_url = f"magnet:?xt=urn:btih:{infohash}&dn=&tr="
response = post(
f"{self.base_url}/torrents/createtorrent",
data={"magnet": magnet_url, "seed": 1, "allow_zip": False},
Expand Down
2 changes: 1 addition & 1 deletion src/program/indexers/trakt.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def __init__(self):
@staticmethod
def copy_attributes(source, target):
"""Copy attributes from source to target."""
attributes = ["file", "folder", "update_folder", "symlinked", "is_anime", "symlink_path", "subtitles", "requested_by", "requested_at", "overseerr_id", "active_stream", "requested_id"]
attributes = ["file", "folder", "update_folder", "symlinked", "is_anime", "symlink_path", "subtitles", "requested_by", "requested_at", "overseerr_id", "active_stream", "requested_id", "streams"]
for attr in attributes:
target.set(attr, getattr(source, attr, None))

Expand Down
90 changes: 89 additions & 1 deletion src/routers/secure/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

import Levenshtein
from fastapi import APIRouter, HTTPException, Request, status
from RTN import ParsedData, Torrent
from fastapi import APIRouter, HTTPException, Request
from sqlalchemy import func, select
from sqlalchemy.exc import NoResultFound

from fastapi import APIRouter, HTTPException, Request
from program.content import Overseerr
from program.db.db import db
from program.db.db_functions import (
Expand All @@ -27,6 +33,11 @@
from sqlalchemy.exc import NoResultFound
from loguru import logger

from program.indexers.trakt import TraktIndexer

from ..models.shared import MessageResponse
from utils.torrent import get_type_and_infohash

from ..models.shared import MessageResponse

router = APIRouter(
Expand Down Expand Up @@ -222,6 +233,82 @@ async def add_items(request: Request, imdb_ids: str = None) -> MessageResponse:
return {"message": f"Added {len(valid_ids)} item(s) to the queue"}


@router.post(
"/add-manually",
summary="Add Media Items Manually",
description="Add media item manually with a magnet link or infohash",
operation_id="add_item_manually",
)
async def add_item_manually(request: Request, imdb_id: str = None, input: str = None) -> MessageResponse:
if not imdb_id:
raise HTTPException(status_code=400, detail="No IMDb ID provided")

if not imdb_id.startswith("tt"):
raise HTTPException(status_code=400, detail="No valid IMDb ID provided")

type, infohash = get_type_and_infohash(input)

if not infohash:
raise HTTPException(status_code=400, detail="No valid input provided")

trakt: TraktIndexer = request.app.program.services.get(TraktIndexer)
downloader: Downloader = request.app.program.services.get(Downloader)
with db.Session() as session:
item = MediaItem(
{"imdb_id": imdb_id, "requested_by": "user", "requested_at": datetime.now()}
)
item = next(trakt.run(item), None)
if item is None:
raise HTTPException(status_code=500, detail="Failed to index item")

needed_media = get_needed_media(item)
cached_streams = downloader.get_cached_streams([infohash], needed_media)

if len(cached_streams) == 0:
session.rollback()
raise HTTPException(
status_code=400,
detail=f"The selected torrent is not cached for {item.log_string}",
)


raw_title = f"Manual Torrent for {imdb_id}"
torrent = Torrent(
raw_title=raw_title,
infohash=infohash,
data=ParsedData(raw_title=raw_title),
)
stream = Stream(torrent)
session.add(stream)
item.streams = [stream]
item.active_stream = cached_streams[0]
session.add(item)
session.commit()

try:
downloader.download(item, item.active_stream)
except Exception as e:
logger.error(f"Failed to download {item.log_string}: {e}")
if item.active_stream.get("infohash", None):
downloader._delete_and_reset_active_stream(item)
session.rollback()
raise HTTPException(
status_code=500, detail=f"Failed to download {item.log_string}: {e}"
) from e

item.last_state = States.Downloaded
session.commit()

request.app.program.em.add_event(Event("Symlinker", item._id))

return {
"success": True,
"message": f"Added {imdb_id} manually to the database (format was {type})",
"item_id": item._id,
"torrent_id": input,
}


@router.get(
"/{id}",
summary="Retrieve Media Item",
Expand Down Expand Up @@ -382,7 +469,8 @@ def add_torrent(request: Request, id: int, magnet: str) -> SetTorrentRDResponse:
torrent_id = ""
downloader: Downloader = request.app.program.services.get(Downloader).service
try:
torrent_id = downloader.add_torrent_magnet(magnet)
_, infohash = get_type_and_infohash(magnet)
torrent_id = downloader.add_torrent(infohash)
except Exception:
raise HTTPException(status_code=500, detail="Failed to add torrent.") from None

Expand Down
49 changes: 49 additions & 0 deletions src/utils/torrent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import re
from urllib.parse import urlparse, parse_qs

def extract_infohash(magnet_link):
# Check if the input is a valid magnet link
if not magnet_link.startswith('magnet:?'):
raise ValueError("Invalid magnet link format")

# Parse the magnet link
parsed = urlparse(magnet_link)
params = parse_qs(parsed.query)

# Look for the 'xt' parameter
xt_params = params.get('xt', [])
for xt in xt_params:
# Check if it's a BitTorrent info hash
if xt.startswith('urn:btih:'):
# Extract the info hash
infohash = xt.split(':')[-1].lower()

# Validate the infohash
if re.match(r'^[0-9a-f]{40}$', infohash):
return infohash
elif re.match(r'^[0-9a-z]{32}$', infohash):
# It's a base32 encoded infohash
return infohash

raise ValueError("No valid BitTorrent info hash found in the magnet link")


def get_type_and_infohash(input_string):
# Check if it's a magnet link
if input_string.startswith('magnet:?'):
try:
infohash = extract_infohash(input_string)
return "Magnet Link", infohash
except ValueError:
return "Invalid Magnet Link", None

# Check if it's a SHA-1 infohash (40 hexadecimal characters)
if re.match(r'^[0-9a-fA-F]{40}$', input_string):
return "Infohash (SHA-1)", input_string.lower()

# Check if it's a base32 encoded infohash (32 alphanumeric characters)
if re.match(r'^[0-9a-zA-Z]{32}$', input_string):
return "Infohash (Base32)", input_string.lower()

# If it's neither
return "Unknown", None

0 comments on commit acb22ce

Please sign in to comment.