From dd270372ffbb7329cffdabddf991efd1de6f933c Mon Sep 17 00:00:00 2001 From: Denis Gheorghescu Date: Sat, 19 Jul 2025 18:23:53 +0300 Subject: [PATCH] Added SubsRo provider --- README.md | 1 + .../subliminal_patch/providers/subsro.py | 258 ++++++++++++++++++ frontend/src/pages/Settings/Providers/list.ts | 5 + 3 files changed, 264 insertions(+) create mode 100644 custom_libs/subliminal_patch/providers/subsro.py diff --git a/README.md b/README.md index 66a87b7ba..68649ee3b 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ If you need something that is not already part of Bazarr, feel free to create a - Subs4Series - Subscene - Subscenter +- SubsRo - Subsunacs.net - SubSynchro - Subtitrari-noi.ro diff --git a/custom_libs/subliminal_patch/providers/subsro.py b/custom_libs/subliminal_patch/providers/subsro.py new file mode 100644 index 000000000..4fc324cba --- /dev/null +++ b/custom_libs/subliminal_patch/providers/subsro.py @@ -0,0 +1,258 @@ +# coding=utf-8 + +import io +import re +from zipfile import ZipFile, is_zipfile +from rarfile import RarFile, is_rarfile +from requests import Session +from bs4 import BeautifulSoup +import logging +from guessit import guessit +from subliminal_patch.providers import Provider +from subliminal_patch.providers.mixins import ProviderSubtitleArchiveMixin +from subliminal_patch.subtitle import Subtitle, guess_matches +from subliminal.video import Episode, Movie +from subzero.language import Language +from subliminal_patch.exceptions import APIThrottled, TooManyRequests + +logger = logging.getLogger(__name__) + + +class SubsRoSubtitle(Subtitle): + """SubsRo Subtitle.""" + + provider_name = "subsro" + hash_verifiable = False + + def __init__( + self, + language, + title, + download_link, + imdb_id, + is_episode=False, + episode_number=None, + year=None, + release_info=None, + season=None, + ): + super().__init__(language) + self.title = title + self.page_link = download_link + self.imdb_id = imdb_id + self.matches = None + self.is_episode = is_episode + self.episode_number = episode_number + self.year = year + self.release_info = self.releases = release_info + self.season = season + + @property + def id(self): + logger.info("Getting ID for SubsRo subtitle: %s. ID: %s", self, self.page_link) + return self.page_link + + def get_matches(self, video): + matches = set() + + if video.year and self.year == video.year: + matches.add("year") + + if isinstance(video, Movie): + # title + if video.title: + matches.add("title") + + # imdb + if video.imdb_id and self.imdb_id == video.imdb_id: + matches.add("imdb_id") + + # guess match others + matches |= guess_matches( + video, + guessit( + f"{self.title} {self.season} {self.year} {self.release_info}", + {"type": "movie"}, + ), + ) + + else: + # title + if video.series: + matches.add("series") + + # imdb + if video.series_imdb_id and self.imdb_id == video.series_imdb_id: + matches.add("imdb_id") + + # season + if video.season == self.season: + matches.add("season") + + # episode + if {"imdb_id", "season"}.issubset(matches): + matches.add("episode") + + # guess match others + matches |= guess_matches( + video, + guessit( + f"{self.title} {self.year} {self.release_info}", {"type": "episode"} + ), + ) + + self.matches = matches + return matches + + +class SubsRoProvider(Provider, ProviderSubtitleArchiveMixin): + """SubsRo Provider.""" + + languages = {Language(lang) for lang in ["ron", "eng"]} + video_types = (Episode, Movie) + hash_verifiable = False + + def __init__(self): + self.session = None + + def initialize(self): + self.session = Session() + # Placeholder, update with real API if available + self.url = "https://subs.ro/api/search" + + def terminate(self): + self.session.close() + + @classmethod + def check(cls, video): + return isinstance(video, (Episode, Movie)) + + def query(self, language, imdb_id, video): + logger.info("Querying SubsRo for %s subtitles of %s", language, imdb_id) + if not imdb_id: + return [] + + url = f"https://subs.ro/subtitrari/imdbid/{imdb_id}" + response = self._request("get", url) + + results = [] + soup = BeautifulSoup(response.text, "html.parser") + for item in soup.find_all("div", class_="md:col-span-6"): + if ( + "flag-rom" in item.find("img")["src"] and language != Language("ron") + ) or ( + "flag-eng" in item.find("img")["src"] and language != Language("eng") + ): + continue # Skip if English flag and language is not English or Romanian flag and language is not Romanian + + episode_number = video.episode if isinstance(video, Episode) else None + + div_tag = item.find("div", class_="col-span-2 lg:col-span-1") + download_link = None + if div_tag: + a_tag = div_tag.find("a") + if a_tag and a_tag.has_attr("href"): + download_link = a_tag["href"] + + h1_tag = item.find( + "h1", + class_="leading-tight text-base font-semibold mb-1 border-b border-dashed border-gray-300 text-[#7f431e] hover:text-red-800", + ) + title = None + year = None + if h1_tag: + a_tag = h1_tag.find("a") + if a_tag and a_tag.text: + title_raw = a_tag.text.strip() + title = re.sub( + r"\s*(-\s*Sezonul\s*\d+)?\s*\(\d{4}\).*$", "", title_raw + ).strip() + year = re.search(r"\((\d{4})\)", title_raw).group(1) + season = re.search(r"\s*Sezonul\s *\d?", title_raw) + if season: + season = int(season.group(0).replace("Sezonul", "").strip()) + + release_info = None + p_tag = item.find( + "p", class_="text-sm font-base overflow-auto h-auto lg:h-16" + ) + if p_tag: + span_blue = p_tag.find("span", style=lambda s: s and "color: blue" in s) + if span_blue: + release_info = span_blue.get_text(strip=True) + else: + release_info = p_tag.get_text(separator="\n", strip=True) + + if download_link and title and year: + results.append( + SubsRoSubtitle( + language, + title, + download_link, + f"tt{imdb_id}", + isinstance(video, Episode), + episode_number, + year, + release_info, + season, + ) + ) + return results + + def list_subtitles(self, video, languages): + imdb_id = None + try: + if isinstance(video, Episode): + imdb_id = video.series_imdb_id[2:] + else: + imdb_id = video.imdb_id[2:] + except: + logger.error( + "Error parsing imdb_id from video object {}".format(str(video)) + ) + + subtitles = [s for lang in languages for s in self.query(lang, imdb_id, video)] + return subtitles + + def download_subtitle(self, subtitle): + logger.info("Downloading subtitle from SubsRo: %s", subtitle.page_link) + response = self._request("get", subtitle.page_link) + + archive_stream = io.BytesIO(response.content) + if is_rarfile(archive_stream): + logger.debug("Archive identified as RAR") + archive = RarFile(archive_stream) + elif is_zipfile(archive_stream): + logger.debug("Archive identified as ZIP") + archive = ZipFile(archive_stream) + else: + if subtitle.is_valid(): + subtitle.content = response.content + return True + else: + subtitle.content = None + return False + + subtitle.content = self.get_subtitle_from_archive(subtitle, archive) + return True + + def _request(self, method, url, **kwargs): + try: + response = self.session.request(method, url, **kwargs) + except Exception as e: + logger.error("SubsRo request error: %s", e) + raise APIThrottled(f"SubsRo request failed: {e}") + + if response.status_code == 429: + logger.warning("SubsRo: Too many requests (HTTP 429) for %s", url) + raise TooManyRequests("SubsRo: Too many requests (HTTP 429)") + if response.status_code >= 500: + logger.warning("SubsRo: Server error %s for %s", response.status_code, url) + raise APIThrottled(f"SubsRo: Server error {response.status_code}") + if response.status_code != 200: + logger.warning( + "SubsRo: Unexpected status %s for %s", response.status_code, url + ) + raise APIThrottled(f"SubsRo: Unexpected status {response.status_code}") + + return response diff --git a/frontend/src/pages/Settings/Providers/list.ts b/frontend/src/pages/Settings/Providers/list.ts index 622ee9d1d..117568ef2 100644 --- a/frontend/src/pages/Settings/Providers/list.ts +++ b/frontend/src/pages/Settings/Providers/list.ts @@ -490,6 +490,11 @@ export const ProviderList: Readonly = [ "Greek Subtitles Provider.\nRequires anti-captcha provider to solve captchas for each download.", }, { key: "subscenter", description: "Hebrew Subtitles Provider" }, + { + key: "subsro", + name: "subs.ro", + description: "Romanian Subtitles Provider", + }, { key: "subsunacs", name: "Subsunacs.net",