sane-bt-search: refactor: split out Tracker details into own class

This commit is contained in:
2025-07-07 20:54:16 +00:00
parent cf38651e8d
commit 604599b3b6

View File

@@ -20,7 +20,7 @@ returns select results and magnet links.
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum, IntEnum
import argparse import argparse
import logging import logging
import json import json
@@ -46,6 +46,14 @@ class SortMethod(Enum):
Seeders = "seeders" Seeders = "seeders"
Tracker = "tracker" Tracker = "tracker"
class TrackerQuality(IntEnum):
Authoritative = 0
Trustworthy = 1
Good = 2
Mediocre = 3
Bad = 4
Unknown = 5
class BadCliArgs(Exception): class BadCliArgs(Exception):
def __init__(self, msg: str | None = None) -> None: def __init__(self, msg: str | None = None) -> None:
helpstr = __doc__ helpstr = __doc__
@@ -68,37 +76,59 @@ def try_parse_time(t: str) -> datetime:
def parse_time(t: str) -> datetime: def parse_time(t: str) -> datetime:
return try_parse_time(t).astimezone() or epoch return try_parse_time(t).astimezone() or epoch
@dataclass(eq=True, order=True, unsafe_hash=True)
class Tracker:
name: str
# preference: major, minor.
# lower value = more preferable.
# trackers with the same major are on a similar "tier"
quality: TrackerQuality
order: int
# preference, best to worst @staticmethod
TRACKER_RANKS = [ def by_name(name: str) -> 'Tracker':
[ 'BitMagnet (Local DHT)', ], if name in KNOWN_TRACKERS:
[ return KNOWN_TRACKERS[name]
'BakaBT', logger.warning(f"unknown tracker: {name!r}")
'SubsPlease', return Tracker(name, TrackerQuality.Unknown, len(KNOWN_TRACKERS))
'Nyaa.si',
'sukebei.nyaa.si', def __repr__(self) -> str:
], return self.name
[ 'YTS', ],
[ 'MioBT', ], def _KNOWN_TRACKERS() -> dict[str, Tracker]:
[ 'Internet Archive', ], trackers = {}
[ 'The Pirate Bay', ],
# haven't sorted these order = 0
[ def add_tracker(*args):
'1337x', nonlocal order
'Bengumi Moe', t = Tracker(*args, order)
'kickasstorrents.to', order += 1
'Tokyo Toshokan', trackers[t.name] = t
'Torlock',
] # the order of this list (even withint sections) is significant:
] # most preferred -> least preferred
# returns the tracker rank, as a tuple of (major, minor). add_tracker('BitMagnet (Local DHT)', TrackerQuality.Authoritative)
# trackers with the same major rank are _roughly_ equal.
def tracker_rank(tracker: str) -> tuple[int, int]: add_tracker('BakaBT', TrackerQuality.Trustworthy)
for major, trackers in enumerate(TRACKER_RANKS): add_tracker('SubsPlease', TrackerQuality.Trustworthy)
if tracker in trackers: add_tracker('Nyaa.si', TrackerQuality.Trustworthy)
return major, trackers.index(tracker) add_tracker('sukebei.nyaa.si', TrackerQuality.Trustworthy)
logger.warning(f"unknown tracker: {tracker!r}")
return len(TRACKER_RANKS), 0 add_tracker('YTS', TrackerQuality.Good)
add_tracker('Tokyo Toshokan', TrackerQuality.Good)
add_tracker('Internet Archive', TrackerQuality.Mediocre)
add_tracker('The Pirate Bay', TrackerQuality.Mediocre)
add_tracker('MioBT', TrackerQuality.Mediocre)
add_tracker('Bengumi Moe', TrackerQuality.Mediocre)
add_tracker('Torlock', TrackerQuality.Mediocre)
add_tracker('1337x', TrackerQuality.Bad)
add_tracker('kickasstorrents.to', TrackerQuality.Bad)
return trackers
KNOWN_TRACKERS = _KNOWN_TRACKERS()
DROP_CATS = { "dvd", "hd", "misc", "other", "sd", "uhd" } DROP_CATS = { "dvd", "hd", "misc", "other", "sd", "uhd" }
BOOK_CATS = { "audio", "books", "ebook" } BOOK_CATS = { "audio", "books", "ebook" }
@@ -167,7 +197,7 @@ class Torrent:
seeders: int seeders: int
pub_date: datetime pub_date: datetime
size: int size: int
tracker: str tracker: Tracker
title: str title: str
magnet: str | None magnet: str | None
http_dl_uri: str | None # probably a .torrent file but it COULD be a referral to a magnet:// URI http_dl_uri: str | None # probably a .torrent file but it COULD be a referral to a magnet:// URI
@@ -226,7 +256,7 @@ class Torrent:
if seeders is not None and pub_date is not None and title is not None and (magnet is not None or http_dl_uri is not None): if seeders is not None and pub_date is not None and title is not None and (magnet is not None or http_dl_uri is not None):
pub_date = parse_time(pub_date) pub_date = parse_time(pub_date)
return Torrent(seeders, pub_date, size, tracker, title, magnet, http_dl_uri, tracker_uri, categories=categories) return Torrent(seeders, pub_date, size, Tracker.by_name(tracker), title, magnet, http_dl_uri, tracker_uri, categories=categories)
def to_dict(self) -> dict: def to_dict(self) -> dict:
# N.B.: not all fields: needs to be kept in sync with consumers like mx-sanebot # N.B.: not all fields: needs to be kept in sync with consumers like mx-sanebot
@@ -269,12 +299,11 @@ class Client:
def sort_results(torrents: list[Torrent], by: SortMethod) -> list[Torrent]: def sort_results(torrents: list[Torrent], by: SortMethod) -> list[Torrent]:
def key(t: Torrent) -> tuple[int, int, Torrent]: def key(t: Torrent) -> tuple[int, int, Torrent]:
rank_seeders = -t.seeders rank_seeders = -t.seeders
rank_tracker_major, rank_tracker_minor = tracker_rank(t.tracker)
# TODO: `Balanced` should consider `size` and `pub_date` # TODO: `Balanced` should consider `size` and `pub_date`
return { return {
SortMethod.Balanced: (rank_tracker_major, rank_seeders, rank_tracker_minor, t), SortMethod.Balanced: (t.tracker.quality, rank_seeders, t.tracker.order, t),
SortMethod.Seeders: (rank_seeders, rank_tracker_major, rank_tracker_minor, t), SortMethod.Seeders: (rank_seeders, t.tracker.quality, t.tracker.order, t),
SortMethod.Tracker: (rank_tracker_major, rank_tracker_minor, rank_seeders, t), SortMethod.Tracker: (t.tracker.quality, t.tracker.order, rank_seeders, t),
}[by] }[by]
return sorted(torrents, key=key) return sorted(torrents, key=key)