diff --git a/scanner/scanner/fsscan.py b/scanner/scanner/fsscan.py index 8e87f4c2..5b05aa4c 100644 --- a/scanner/scanner/fsscan.py +++ b/scanner/scanner/fsscan.py @@ -3,7 +3,6 @@ import re from logging import getLogger from mimetypes import guess_file_type from os.path import dirname, exists, isdir, join -from typing import Optional from watchfiles import Change, awatch @@ -11,7 +10,7 @@ from .client import KyooClient from .identify import identify from .models.metadataid import EpisodeId, MetadataId from .models.videos import For, Video, VideoInfo -from .queue import Request, enqueue +from .requests import Request, enqueue logger = getLogger(__name__) @@ -26,7 +25,7 @@ class Scanner: except re.error as e: logger.error(f"Invalid ignore pattern. Ignoring. Error: {e}") - async def scan(self, path: Optional[str] = None, remove_deleted=False): + async def scan(self, path: str | None = None, remove_deleted=False): if path is None: logger.info("Starting scan at %s. This may take some time...", path) if self._ignore_pattern: @@ -101,6 +100,7 @@ class Scanner: kind=x.guess.kind, title=x.guess.title, year=next(iter(x.guess.years), None), + external_id=x.guess.external_id, videos=[Request.Video(id=x.id, episodes=x.guess.episodes)], ) for x in created diff --git a/scanner/scanner/guess/guess.py b/scanner/scanner/guess/guess.py index da8fe339..c1a2b32f 100644 --- a/scanner/scanner/guess/guess.py +++ b/scanner/scanner/guess/guess.py @@ -1,23 +1,19 @@ -from typing import Any, List, cast +from typing import Any, cast from guessit.api import default_api from rebulk import Rebulk from rebulk.match import Match -try: - from . import rules -except: - import rules +from . import rules default_api.configure({}) -rblk = cast(Rebulk, default_api.rebulk) -rblk.rules(rules) +rblk = cast(Rebulk, default_api.rebulk).rules(rules) def guessit( name: str, *, - expected_titles: List[str] = [], + expected_titles: list[str] = [], extra_flags: dict[str, Any] = {}, ) -> dict[str, list[Match]]: return default_api.guessit( diff --git a/scanner/scanner/guess/rules.py b/scanner/scanner/guess/rules.py index 2a0d0739..2baaa588 100644 --- a/scanner/scanner/guess/rules.py +++ b/scanner/scanner/guess/rules.py @@ -2,7 +2,7 @@ from copy import copy from logging import getLogger -from typing import Any, List, Optional, cast +from typing import Any, cast, override from rebulk import POST_PROCESS, AppendMatch, RemoveMatch, Rule from rebulk.match import Match, Matches @@ -52,11 +52,12 @@ class UnlistTitles(Rule): priority = POST_PROCESS consequence = [RemoveMatch, AppendMatch] + @override def when(self, matches: Matches, context) -> Any: - fileparts: List[Match] = matches.markers.named("path") # type: ignore + fileparts: list[Match] = matches.markers.named("path") # type: ignore for part in fileparts: - titles: List[Match] = matches.range( + titles: list[Match] = matches.range( part.start, part.end, lambda x: x.name == "title" ) # type: ignore @@ -66,7 +67,7 @@ class UnlistTitles(Rule): title = copy(titles[0]) for nmatch in titles[1:]: # Check if titles are next to each other, if they are not ignore it. - next: List[Match] = matches.next(title) # type: ignore + next: list[Match] = matches.next(title) # type: ignore if not next or next[0] != nmatch: logger.warning(f"Ignoring potential part of title: {nmatch.value}") continue @@ -107,14 +108,15 @@ class MultipleSeasonRule(Rule): priority = POST_PROCESS consequence = [RemoveMatch, AppendMatch] + @override def when(self, matches: Matches, context) -> Any: - seasons: List[Match] = matches.named("season") # type: ignore + seasons: list[Match] = matches.named("season") # type: ignore if not seasons: return # Only apply this rule if all seasons are due to the same match - initiator: Optional[Match] = seasons[0].initiator + initiator: Match | None = seasons[0].initiator if not initiator or any( True for match in seasons if match.initiator != initiator ): @@ -130,7 +132,7 @@ class MultipleSeasonRule(Rule): try: episodes = [int(x) for x in new_episodes] - parents: List[Match] = [match.parent for match in to_remove] # type: ignore + parents: list[Match] = [match.parent for match in to_remove] # type: ignore for episode in episodes: smatch = next( x @@ -181,8 +183,9 @@ class SeasonYearDedup(Rule): priority = POST_PROCESS - 1 consequence = RemoveMatch + @override def when(self, matches: Matches, context) -> Any: - season: List[Match] = matches.named("season") # type: ignore - year: List[Match] = matches.named("year") # type: ignore + season: list[Match] = matches.named("season") # type: ignore + year: list[Match] = matches.named("year") # type: ignore if len(season) == 1 and len(year) == 1 and season[0].value == year[0].value: return season diff --git a/scanner/scanner/identify.py b/scanner/scanner/identify.py index e8cbadfd..661da612 100644 --- a/scanner/scanner/identify.py +++ b/scanner/scanner/identify.py @@ -1,7 +1,8 @@ +from collections.abc import Awaitable from hashlib import sha256 from itertools import zip_longest from logging import getLogger -from typing import Awaitable, Callable, Literal, cast +from typing import Callable, Literal, cast from .guess.guess import guessit from .models.videos import Guess, Video diff --git a/scanner/scanner/queue.py b/scanner/scanner/requests.py similarity index 76% rename from scanner/scanner/queue.py rename to scanner/scanner/requests.py index ea7ea9c2..756a4fdb 100644 --- a/scanner/scanner/queue.py +++ b/scanner/scanner/requests.py @@ -1,16 +1,18 @@ from __future__ import annotations -from typing import Literal, Optional +from typing import Literal from .client import KyooClient from .models.videos import Guess from .utils import Model +from .providers.composite import CompositeProvider class Request(Model): kind: Literal["episode"] | Literal["movie"] title: str - year: Optional[int] + year: int | None + external_id: dict[str, str] videos: list[Video] class Video(Model): @@ -27,8 +29,9 @@ async def enqueue(requests: list[Request]): # TODO: how will this conflict be handled if the request is already locked for update (being processed) pass + class RequestProcessor: - def __init__(self, client: KyooClient): + def __init__(self, client: KyooClient, providers: CompositeProvider): self._client = client async def process_scan_requests(self): @@ -36,13 +39,13 @@ class RequestProcessor: request: Request = ... if request.kind == "movie": - movie = await providers.get_movie(request.title, request.year) + movie = await providers.get_movie(request.title, request.year, request.external_id) movie.videos = request.videos await self._client.create_movie(movie) else: serie = await providers.get_serie(request.title, request.year) # for vid in request.videos: # for ep in vid.episodes: - # entry = next(x for x in series.entries if (ep.season is None or x.season == ep.season), None) + # entry = next(x for x in series.entries if (ep.season is None or x.season == ep.season), None) await self._client.create_serie(serie) # delete request diff --git a/scanner/scanner/utils.py b/scanner/scanner/utils.py index 1dee812c..19ad42ca 100644 --- a/scanner/scanner/utils.py +++ b/scanner/scanner/utils.py @@ -17,11 +17,6 @@ def normalize_lang(lang: str) -> str: return str(Language.get(lang)) -class ProviderError(RuntimeError): - def __init__(self, *args: object) -> None: - super().__init__(*args) - - class Model(BaseModel): model_config = ConfigDict( use_enum_values=True,