mirror of
https://github.com/maxdorninger/MediaManager.git
synced 2026-06-12 09:36:21 -04:00
add usenet support to indexer module, add make jackett and prowlarr search for either tv or movies
This commit is contained in:
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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()}")
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user