add usenet support to indexer module, add make jackett and prowlarr search for either tv or movies

This commit is contained in:
maxDorninger
2025-07-09 20:52:01 +02:00
parent e17e6d8271
commit f2edfb076c
8 changed files with 48 additions and 16 deletions

View File

@@ -10,10 +10,11 @@ class GenericIndexer(object):
else:
raise ValueError("indexer name must not be None")
def search(self, query: str) -> list[IndexerQueryResult]:
def search(self, query: str, is_tv: bool) -> list[IndexerQueryResult]:
"""
Sends a search request to the Indexer and returns the results.
:param is_tv: Whether to search for TV shows or movies.
:param query: The search query to send to the Indexer.
:return: A list of IndexerQueryResult objects representing the search results.
"""

View File

@@ -3,6 +3,7 @@ import xml.etree.ElementTree as ET
from xml.etree.ElementTree import Element
import requests
from pydantic import HttpUrl
from media_manager.indexer.indexers.generic import GenericIndexer
from media_manager.indexer.config import JackettConfig
@@ -25,7 +26,7 @@ class Jackett(GenericIndexer):
log.debug("Registering Jacket as Indexer")
# NOTE: this could be done in parallel, but if there aren't more than a dozen indexers, it shouldn't matter
def search(self, query: str) -> list[IndexerQueryResult]:
def search(self, query: str, is_tv: bool) -> list[IndexerQueryResult]:
log.debug("Searching for " + query)
responses = []
@@ -33,7 +34,7 @@ class Jackett(GenericIndexer):
log.debug(f"Searching in indexer: {indexer}")
url = (
self.url
+ f"/api/v2.0/indexers/{indexer}/results/torznab/api?apikey={self.api_key}&t=search&q={query}"
+ f"/api/v2.0/indexers/{indexer}/results/torznab/api?apikey={self.api_key}&t={'tvsearch' if is_tv else 'movie'}&q={query}"
)
response = requests.get(url)
responses.append(response)
@@ -62,10 +63,12 @@ class Jackett(GenericIndexer):
result = IndexerQueryResult(
title=item.find("title").text,
download_url=item.find("link").text,
download_url=HttpUrl(item.find("enclosure").attrib["url"]),
seeders=seeders,
flags=[],
size=int(item.find("size").text),
usenet=False, # always False, because Jackett doesn't support usenet
age=0 # always 0 for torrents, as Jackett does not provide age information in a convenient format
)
result_list.append(result)
log.debug(f"Raw result: {result.model_dump()}")

View File

@@ -23,21 +23,22 @@ class Prowlarr(GenericIndexer):
self.url = config.url
log.debug("Registering Prowlarr as Indexer")
def search(self, query: str) -> list[IndexerQueryResult]:
def search(self, query: str, is_tv: bool) -> list[IndexerQueryResult]:
log.debug("Searching for " + query)
url = self.url + "/api/v1/search"
headers = {"accept": "application/json", "X-Api-Key": self.api_key}
params = {
"query": query,
"apikey": self.api_key,
"categories": "5000" if is_tv else "2000" # TV: 5000, Movies: 2000
}
response = requests.get(url, headers=headers, params=params)
response = requests.get(url, params=params)
if response.status_code == 200:
result_list: list[IndexerQueryResult] = []
for result in response.json():
if result["protocol"] == "torrent":
log.debug("torrent result: " + result.__str__())
is_torrent = result["protocol"] == "torrent"
if is_torrent:
result_list.append(
IndexerQueryResult(
download_url=result["downloadUrl"],
@@ -45,8 +46,24 @@ class Prowlarr(GenericIndexer):
seeders=result["seeders"],
flags=result["indexerFlags"],
size=result["size"],
usenet=True,
age=0, # Torrent results do not need age information
)
)
else:
result_list.append(
IndexerQueryResult(
download_url=result["downloadUrl"],
title=result["sortTitle"],
seeders=0, # Usenet results do not have seeders
flags=result["indexerFlags"],
size=result["size"],
usenet=False,
age=int(result["ageMinutes"])*60,
)
)
log.debug("torrent result: " + result.__str__())
return result_list
else:
log.error(f"Prowlarr Error: {response.status_code}")

View File

@@ -1,6 +1,6 @@
from uuid import UUID
from sqlalchemy import String, Integer
from sqlalchemy import String, Integer, Boolean
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql.sqltypes import BigInteger
@@ -19,3 +19,5 @@ class IndexerQueryResult(Base):
quality: Mapped[Quality]
season = mapped_column(ARRAY(Integer))
size = mapped_column(BigInteger)
usenet: Mapped[bool]
age: Mapped[int]

View File

@@ -3,7 +3,7 @@ import typing
from uuid import UUID, uuid4
import pydantic
from pydantic import BaseModel, computed_field, ConfigDict
from pydantic import BaseModel, computed_field, ConfigDict, HttpUrl
from media_manager.torrent.models import Quality
@@ -15,11 +15,14 @@ class IndexerQueryResult(BaseModel):
id: IndexerQueryResultId = pydantic.Field(default_factory=uuid4)
title: str
download_url: str
download_url: HttpUrl
seeders: int
flags: list[str]
size: int
usenet: bool
age: int
@computed_field(return_type=Quality)
@property
def quality(self) -> Quality:
@@ -73,3 +76,6 @@ class PublicIndexerQueryResult(BaseModel):
flags: list[str]
season: list[int]
size: int
usenet: bool
age: int

View File

@@ -11,10 +11,11 @@ class IndexerService:
def get_result(self, result_id: IndexerQueryResultId) -> IndexerQueryResult:
return self.repository.get_result(result_id=result_id)
def search(self, query: str) -> list[IndexerQueryResult]:
def search(self, query: str, is_tv: bool) -> list[IndexerQueryResult]:
"""
Search for results using the indexers based on a query.
:param is_tv: Whether the search is for TV shows or movies.
:param query: The search query.
:param db: The database session.
:return: A list of search results.
@@ -25,7 +26,7 @@ class IndexerService:
for indexer in indexers:
try:
indexer_results = indexer.search(query)
indexer_results = indexer.search(query, is_tv=is_tv)
results.extend(indexer_results)
log.debug(
f"Indexer {indexer.__class__.__name__} returned {len(indexer_results)} results for query: {query}"

View File

@@ -178,7 +178,8 @@ class MovieService:
search_query = f"{movie.name}"
torrents: list[IndexerQueryResult] = self.indexer_service.search(
query=search_query
query=search_query,
is_tv=False
)
if search_query_override:

View File

@@ -189,7 +189,8 @@ class TvService:
search_query = show.name + " s" + str(season_number).zfill(2)
torrents: list[IndexerQueryResult] = self.indexer_service.search(
query=search_query
query=search_query,
is_tv=True
)
if search_query_override: