Compare commits

4 Commits

22 changed files with 143 additions and 98 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/test.xml
/uv.lock
/main.py

View File

@@ -1 +1 @@
__version__ = "1.0.0"
__version__ = "1.0.3"

View File

@@ -2,27 +2,62 @@ import importlib
from pathlib import Path
from pkgutil import iter_modules
from typing import Optional, Type
from xml.etree.ElementTree import Element
from parsers.generic.parsers import DataParser
from parsers.generic.watfag import WATFAG, SeedStatus
from watfag.parsers.generic.parsers import DataParser
from watfag.parsers.generic.watfag import *
from watfag.parsers.generic.watfag import WATFAG
from watfag.trackers import TrackerInfo
class Release:
def __init__(
self,
unparsed_text,
dl_link,
**kwargs
xml_result: Element,
tracker_info: Optional[TrackerInfo] = None
):
self.original_text: str = unparsed_text
self.xml: Element = xml_result
self.tracker_info: Optional[TrackerInfo] = tracker_info
self.original_text: str = ''
self.metadata_text: Optional[str] = ''
self.dl_link: str = dl_link
self.view_link: str = kwargs.get('view_link', dl_link)
self.size: int = kwargs.get('size', 0)
self.seeders: int = kwargs.get('seeders', 0)
self.dl_link: str = ''
self.view_link: str = ''
self.size: int = 0
self.seeders: int = 0
self.tracker: str = ''
self.tracker_abbr: str = ''
self.seed_status: Optional[SeedStatus] = None
self.parser_results: dict[str, bool] = {} # Stores which parsers have been run and their results.
self.title: Optional[str] = None
self.group: Optional[Group] = None
self.group_name: Optional[str] = None
self.quality: Optional[Resolution] = None
self.source: Optional[Source] = None
self.streaming: Optional[StreamingService] = None
self.video_codec: Optional[VideoCodec] = None
self.audio_codec: Optional[AudioCodec] = None
self.audio_layout: Optional[AudioLayout] = None
self.dynamic_range: Optional[DynamicRange] = None
self.repack: Optional[Repack] = None
self.multi: Optional[Multi] = None
self._parse_xml()
def _parse_xml(self):
# Get the torznab attributes
attrs: dict[str, list[str]] = {}
for attr in self.xml.findall('torznab:attr', namespaces={'torznab': 'http://torznab.com/schemas/2015/feed'}):
attrs[attr.get('name')] = attrs.get(attr.get('name'), []) + [attr.get('value')]
self.original_text = self.xml.find('title').text
self.dl_link = self.xml.find('link').text
self.size = int(self.xml.find('size').text)
self.seeders = int(attrs.get('seeders')[0])
self.view_link = self.xml.find('comments').text
self.tracker = self.xml.find('jackettindexer').text
self.tracker_abbr = self.tracker_info.get_tracker_info(self.tracker).get('Abbreviation') if self.tracker_info else self.tracker
def __lt__(self, other):
return self.watfag < other.watfag
@@ -63,6 +98,7 @@ class GenericParser:
class ParserManager:
"""Manages and runs parsers on releases."""
def __init__(self):
self.parsers: list[Type[DataParser]] = []
self.collect_parsers()

View File

@@ -1,8 +1,8 @@
import regex as re
from parsers.generic import GenericParser
from parsers.generic.parsers import DataParser
from parsers.generic.watfag import AudioCodec, AudioLayout
from watfag.parsers.generic import GenericParser
from watfag.parsers.generic.parsers import DataParser
from watfag.parsers.generic.watfag import AudioCodec, AudioLayout
patterns = [
re.compile(

View File

@@ -1,11 +1,12 @@
import regex as re
from parsers.generic import GenericParser
from parsers.generic.watfag import Group
from parsers.generic.parsers import DataParser
from watfag.parsers.generic import GenericParser
from watfag.parsers.generic.watfag import Group
from watfag.parsers.generic.parsers import DataParser
patterns = [
re.compile(r"(?:-| - )(?P<group>[a-zA-Z0-9 &]*)\)?$", re.UNICODE),
re.compile(r"\[(?P<group>[a-zA-Z0-9 &]*)\]?$", re.UNICODE),
re.compile(r"(?: )\[?(?P<group>[a-zA-Z0-9]*?)]?\)?$", re.UNICODE)
]

View File

@@ -1,8 +1,8 @@
import regex as re
from parsers.generic import GenericParser
from parsers.generic.parsers import CheckParser
from parsers.generic.watfag import DynamicRange
from watfag.parsers.generic import GenericParser
from watfag.parsers.generic.parsers import CheckParser
from watfag.parsers.generic.watfag import DynamicRange
checks = {
re.compile(r"hybrid|do?vi? ?hdr(?:10)?[\+p]?", re.IGNORECASE): DynamicRange.HYBRID,

View File

@@ -1,8 +1,8 @@
import regex as re
from parsers.generic import GenericParser
from parsers.generic.parsers import CheckParser
from parsers.generic.watfag import Multi
from watfag.parsers.generic import GenericParser
from watfag.parsers.generic.parsers import CheckParser
from watfag.parsers.generic.watfag import Multi
checks = {
re.compile(r"multi", re.IGNORECASE): Multi.MULTI

View File

@@ -3,7 +3,7 @@ from typing import Optional, TYPE_CHECKING
from regex import Pattern
if TYPE_CHECKING:
from parsers.generic import WATFAG, Release
from watfag.parsers.generic import WATFAG, Release
class DataParser:

View File

@@ -1,8 +1,8 @@
import regex as re
from parsers.generic import GenericParser
from parsers.generic.parsers import CheckParser
from parsers.generic.watfag import Repack
from watfag.parsers.generic import GenericParser
from watfag.parsers.generic.parsers import CheckParser
from watfag.parsers.generic.watfag import Repack
checks = {
re.compile(r"repack", re.IGNORECASE): Repack.REPACK,

View File

@@ -1,8 +1,8 @@
import regex as re
from parsers.generic import GenericParser
from parsers.generic.parsers import CheckParser
from parsers.generic.watfag import Resolution
from watfag.parsers.generic import GenericParser
from watfag.parsers.generic.parsers import CheckParser
from watfag.parsers.generic.watfag import Resolution
checks = {
re.compile(r"2160p", re.IGNORECASE): Resolution.UHD,

View File

@@ -1,8 +1,8 @@
import regex as re
from parsers.generic import GenericParser
from parsers.generic.parsers import CheckParser
from parsers.generic.watfag import SeedStatus
from watfag.parsers.generic import GenericParser
from watfag.parsers.generic.parsers import CheckParser
from watfag.parsers.generic.watfag import SeedStatus
class SeederParser(CheckParser, GenericParser):
def parse(self) -> bool:

View File

@@ -1,8 +1,8 @@
import regex as re
from parsers.generic import GenericParser
from parsers.generic.parsers import CheckParser
from parsers.generic.watfag import Source
from watfag.parsers.generic import GenericParser
from watfag.parsers.generic.parsers import CheckParser
from watfag.parsers.generic.watfag import Source
checks = {
re.compile(r"remux", re.IGNORECASE): Source.REMUX,

View File

@@ -1,8 +1,8 @@
import regex as re
from parsers.generic import GenericParser
from parsers.generic.parsers import CheckParser
from parsers.generic.watfag import StreamingService
from watfag.parsers.generic import GenericParser
from watfag.parsers.generic.parsers import CheckParser
from watfag.parsers.generic.watfag import StreamingService
checks = {
re.compile(r"ATVP"): StreamingService.ATVP,

View File

@@ -1,8 +1,8 @@
import regex as re
from parsers.generic import GenericParser
from parsers.generic.parsers import CheckParser
from parsers.generic.watfag import VideoCodec
from watfag.parsers.generic import GenericParser
from watfag.parsers.generic.parsers import CheckParser
from watfag.parsers.generic.watfag import VideoCodec
checks = {
re.compile(r"avc|[hx][\. -]?264", re.IGNORECASE): VideoCodec.AVC,

View File

@@ -189,6 +189,7 @@ class Group(WATFAG):
"""
FLUX = "FLUX", 10 # Very good WEB-DL releases and fast
HONE = "HONE", 10 # High quality re-encodes
TAOE = "TAoE", 10 # High quality re-encodes
PHOCIS = "PHOCiS", 8 # Same as FLUX
LEGION = "LEGi0N", 8 # Same as FLUX
AOC = "AOC", 1 # Often low quality CAM releases. While fast, not worth it for most movies.

View File

@@ -2,33 +2,28 @@ import importlib
from pathlib import Path
from pkgutil import iter_modules
from typing import Optional
from xml.etree.ElementTree import Element
from parsers.generic import Release, ParserManager
from parsers.generic.watfag import *
from watfag.parsers.generic import Release, ParserManager
from watfag.trackers import TrackerInfo
class MovieRelease(Release):
"""Holds info representing a release of a movie."""
def __init__(self, unparsed_text, dl_link, **kwargs):
super().__init__(unparsed_text, dl_link, **kwargs)
self.title: str = ""
def __init__(
self,
xml_result: Element,
tracker_info: Optional[TrackerInfo] = None
):
super().__init__(xml_result, tracker_info)
self.year: int = 0
self.edition: Optional[str] = None
self.group: Optional[Group] = None
self.group_name: Optional[str] = None
self.quality: Optional[Resolution] = None
self.source: Optional[Source] = None
self.streaming: Optional[StreamingService] = None
self.video_codec: Optional[VideoCodec] = None
self.audio_codec: Optional[AudioCodec] = None
self.audio_layout: Optional[AudioLayout] = None
self.dynamic_range: Optional[DynamicRange] = None
self.repack: Optional[Repack] = None
self.multi: Optional[Multi] = None
def __str__(self):
parts = [f"{self.title} ({self.year})" + (f" [{self.edition}]" if self.edition else "")]
for attr in ['quality', 'video_codec', 'audio_codec', 'audio_layout', 'dynamic_range', 'repack', 'multi', 'source']:
for attr in ['quality', 'video_codec', 'audio_codec', 'audio_layout', 'dynamic_range', 'repack', 'multi',
'source']:
value = getattr(self, attr)
parts.append(f"{attr.capitalize()}: {value if value else 'Unknown'}")
if self.streaming:
@@ -39,12 +34,14 @@ class MovieRelease(Release):
parts.append(f"WATFAG: {self.watfag:.2f}")
return " | ".join(parts)
class MovieParser:
"""
This class can be inherited by any parser that is specific to movies.
It allows dynamic importing of parser classes and provides a method to run all parsers on a given movie release.
"""
class MovieParserManager(ParserManager):
"""Parses movie releases."""

View File

@@ -1,8 +1,8 @@
import regex as re
from parsers.generic.parsers import DataParser
from parsers.movie import MovieParser, MovieRelease
from parsers.movie.title_year import edition_regex
from watfag.parsers.generic.parsers import DataParser
from watfag.parsers.movie import MovieParser, MovieRelease
from watfag.parsers.movie.title_year import edition_regex
class EditionParser(DataParser, MovieParser):

View File

@@ -1,7 +1,7 @@
import regex as re
from parsers.generic.parsers import DataParser
from parsers.movie import MovieParser, MovieRelease
from watfag.parsers.generic.parsers import DataParser
from watfag.parsers.movie import MovieParser, MovieRelease
# Shamelessly stolen from Radarr: https://github.com/Radarr/Radarr/blob/develop/src/NzbDrone.Core/Parser/Parser.cs
edition_regex = r"\(?\b(?P<edition>(((Recut.|Extended.|Ultimate.)?(Director.?s|Collector.?s|Theatrical|Ultimate|Extended|Despecialized|(Special|Rouge|Final|Assembly|Imperial|Diamond|Signature|Hunter|Rekall)(?=(.(Cut|Edition|Version)))|\d{2,3}(th)?.Anniversary)(?:.(Cut|Edition|Version))?(.(Extended|Uncensored|Remastered|Unrated|Uncut|Open.?Matte|IMAX|Fan.?Edit))?|((Uncensored|Remastered|Unrated|Uncut|Open?.Matte|IMAX|Fan.?Edit|Restored|((2|3|4)in1))))))\b\)?"

View File

@@ -2,31 +2,24 @@ import importlib
from pathlib import Path
from pkgutil import iter_modules
from typing import Optional
from xml.etree.ElementTree import Element
from parsers.generic import Release, ParserManager
from parsers.generic.watfag import *
from watfag.parsers.generic import Release, ParserManager
from watfag.trackers import TrackerInfo
class TVBoxSetRelease(Release):
"""Holds info representing a release of a TV box set."""
def __init__(self, unparsed_text, dl_link, **kwargs):
super().__init__(unparsed_text, dl_link, **kwargs)
self.show_title: str = ""
def __init__(
self,
xml_result: Element,
tracker_info: Optional[TrackerInfo] = None
):
super().__init__(xml_result, tracker_info)
self.seasons: Optional[str] = None
self.group: Optional[Group] = None
self.group_name: Optional[str] = None
self.quality: Optional[Resolution] = None
self.source: Optional[Source] = None
self.streaming: Optional[StreamingService] = None
self.video_codec: Optional[VideoCodec] = None
self.audio_codec: Optional[AudioCodec] = None
self.audio_layout: Optional[AudioLayout] = None
self.dynamic_range: Optional[DynamicRange] = None
self.repack: Optional[Repack] = None
self.multi: Optional[Multi] = None
def __str__(self):
parts = [f"{self.show_title} (Seasons: {self.seasons})"]
parts = [f"{self.title} (Seasons: {self.seasons})"]
for attr in ['quality', 'video_codec', 'audio_codec', 'audio_layout', 'dynamic_range', 'repack', 'multi', 'source']:
value = getattr(self, attr)
parts.append(f"{attr.capitalize()}: {value if value else 'Unknown'}")
@@ -46,7 +39,6 @@ class TVBoxSetParser:
class TVBoxSetParserManager(ParserManager):
"""Parses TV box set releases."""
def collect_parsers(self):
"""Dynamically imports all TV box set parsers."""
super().collect_parsers()

View File

@@ -1,7 +1,7 @@
import regex as re
from parsers.generic.parsers import DataParser
from parsers.tvboxset import TVBoxSetParser, TVBoxSetRelease
from watfag.parsers.generic.parsers import DataParser
from watfag.parsers.tvboxset import TVBoxSetParser, TVBoxSetRelease
patterns = [
re.compile( # Show Name S01-S02 (year)
@@ -45,7 +45,7 @@ class TitleSeasonsParser(DataParser, TVBoxSetParser):
for pattern in patterns:
match = pattern.match(self.release.original_text)
if match:
self.release.show_title = match.group("title").replace(".", " ").replace("_", " ").strip() if match.group("title") else ""
self.release.title = match.group("title").replace(".", " ").replace("_", " ").strip() if match.group("title") else ""
season_start = int(match.group("season_start")) if match.group("season_start") else 0
season_end = int(match.group("season_end")) if "season_end" in match.groupdict() and match.group("season_end") else season_start
self.release.seasons = f"{season_start}" if season_start == season_end else f"{season_start}-{season_end}"

View File

@@ -2,9 +2,10 @@ from xml.etree import ElementTree
from httpx import AsyncClient
from parsers.generic import Release
from parsers.movie import MovieRelease, MovieParserManager
from parsers.tvboxset import TVBoxSetRelease, TVBoxSetParserManager
from watfag.parsers.generic import Release
from watfag.parsers.movie import MovieRelease, MovieParserManager
from watfag.parsers.tvboxset import TVBoxSetRelease, TVBoxSetParserManager
from watfag.trackers import TrackerInfo
class Jackett:
@@ -13,6 +14,9 @@ class Jackett:
self.base_url = base_url
self.movie_parser = MovieParserManager()
self.tvboxset_parser = TVBoxSetParserManager()
self.tracker_info = TrackerInfo(
"https://raw.githubusercontent.com/HDVinnie/Private-Trackers-Spreadsheet/refs/heads/master/trackers.json"
)
async def get_capabilities(self):
params = {
@@ -45,21 +49,15 @@ class Jackett:
# Find out from categories what kind of result this is
if any(cat.startswith('2') for cat in attrs.get('category')): # This is a movie
release = MovieRelease(
item.find('title').text,
item.find('link').text,
size=int(item.find('size').text),
seeders=int(attrs.get('seeders')[0]),
view_link=item.find('comments').text
item,
self.tracker_info
)
self.movie_parser.run_parsers(release)
releases.append(release)
elif any(cat == '100027' for cat in attrs.get('category')): # This is a TV boxset
release = TVBoxSetRelease(
item.find('title').text,
item.find('link').text,
size=int(item.find('size').text),
seeders=int(attrs.get('seeders')[0]),
view_link=item.find('comments').text
item,
self.tracker_info
)
self.tvboxset_parser.run_parsers(release)
releases.append(release)

19
src/watfag/trackers.py Normal file
View File

@@ -0,0 +1,19 @@
from typing import Optional
import httpx
class TrackerInfo:
def __init__(self, url):
self.url = url
self.json: Optional[dict] = None
def _refresh(self):
r = httpx.get(self.url)
r.raise_for_status()
self.json = r.json().get('trackers', [])
def get_tracker_info(self, tracker_name):
if self.json is None:
self._refresh()
return next((tracker for tracker in self.json if tracker['Name'] == tracker_name), None)