|
| 1 | +# coding=utf-8 |
| 2 | + |
| 3 | +import io |
| 4 | +import re |
| 5 | +from zipfile import ZipFile, is_zipfile |
| 6 | +from rarfile import RarFile, is_rarfile |
| 7 | +from requests import Session |
| 8 | +from bs4 import BeautifulSoup |
| 9 | +import logging |
| 10 | +from guessit import guessit |
| 11 | +from subliminal_patch.providers import Provider |
| 12 | +from subliminal_patch.providers.mixins import ProviderSubtitleArchiveMixin |
| 13 | +from subliminal_patch.subtitle import Subtitle, guess_matches |
| 14 | +from subliminal.video import Episode, Movie |
| 15 | +from subzero.language import Language |
| 16 | +from subliminal_patch.exceptions import APIThrottled, TooManyRequests |
| 17 | + |
| 18 | +logger = logging.getLogger(__name__) |
| 19 | + |
| 20 | + |
| 21 | +class SubsRoSubtitle(Subtitle): |
| 22 | + """SubsRo Subtitle.""" |
| 23 | + |
| 24 | + provider_name = "subsro" |
| 25 | + hash_verifiable = False |
| 26 | + |
| 27 | + def __init__( |
| 28 | + self, |
| 29 | + language, |
| 30 | + title, |
| 31 | + download_link, |
| 32 | + imdb_id, |
| 33 | + is_episode=False, |
| 34 | + episode_number=None, |
| 35 | + year=None, |
| 36 | + release_info=None, |
| 37 | + season=None, |
| 38 | + ): |
| 39 | + super().__init__(language) |
| 40 | + self.title = title |
| 41 | + self.page_link = download_link |
| 42 | + self.imdb_id = imdb_id |
| 43 | + self.matches = None |
| 44 | + self.is_episode = is_episode |
| 45 | + self.episode_number = episode_number |
| 46 | + self.year = year |
| 47 | + self.release_info = self.releases = release_info |
| 48 | + self.season = season |
| 49 | + |
| 50 | + @property |
| 51 | + def id(self): |
| 52 | + logger.info("Getting ID for SubsRo subtitle: %s. ID: %s", self, self.page_link) |
| 53 | + return self.page_link |
| 54 | + |
| 55 | + def get_matches(self, video): |
| 56 | + matches = set() |
| 57 | + |
| 58 | + if video.year and self.year == video.year: |
| 59 | + matches.add("year") |
| 60 | + |
| 61 | + if isinstance(video, Movie): |
| 62 | + # title |
| 63 | + if video.title: |
| 64 | + matches.add("title") |
| 65 | + |
| 66 | + # imdb |
| 67 | + if video.imdb_id and self.imdb_id == video.imdb_id: |
| 68 | + matches.add("imdb_id") |
| 69 | + |
| 70 | + # guess match others |
| 71 | + matches |= guess_matches( |
| 72 | + video, |
| 73 | + guessit( |
| 74 | + f"{self.title} {self.season} {self.year} {self.release_info}", |
| 75 | + {"type": "movie"}, |
| 76 | + ), |
| 77 | + ) |
| 78 | + |
| 79 | + else: |
| 80 | + # title |
| 81 | + if video.series: |
| 82 | + matches.add("series") |
| 83 | + |
| 84 | + # imdb |
| 85 | + if video.series_imdb_id and self.imdb_id == video.series_imdb_id: |
| 86 | + matches.add("imdb_id") |
| 87 | + |
| 88 | + # season |
| 89 | + if video.season == self.season: |
| 90 | + matches.add("season") |
| 91 | + |
| 92 | + # episode |
| 93 | + if {"imdb_id", "season"}.issubset(matches): |
| 94 | + matches.add("episode") |
| 95 | + |
| 96 | + # guess match others |
| 97 | + matches |= guess_matches( |
| 98 | + video, |
| 99 | + guessit( |
| 100 | + f"{self.title} {self.year} {self.release_info}", {"type": "episode"} |
| 101 | + ), |
| 102 | + ) |
| 103 | + |
| 104 | + self.matches = matches |
| 105 | + return matches |
| 106 | + |
| 107 | + |
| 108 | +class SubsRoProvider(Provider, ProviderSubtitleArchiveMixin): |
| 109 | + """SubsRo Provider.""" |
| 110 | + |
| 111 | + languages = {Language(lang) for lang in ["ron", "eng"]} |
| 112 | + video_types = (Episode, Movie) |
| 113 | + hash_verifiable = False |
| 114 | + |
| 115 | + def __init__(self): |
| 116 | + self.session = None |
| 117 | + |
| 118 | + def initialize(self): |
| 119 | + self.session = Session() |
| 120 | + # Placeholder, update with real API if available |
| 121 | + self.url = "https://subs.ro/api/search" |
| 122 | + |
| 123 | + def terminate(self): |
| 124 | + self.session.close() |
| 125 | + |
| 126 | + @classmethod |
| 127 | + def check(cls, video): |
| 128 | + return isinstance(video, (Episode, Movie)) |
| 129 | + |
| 130 | + def query(self, language, imdb_id, video): |
| 131 | + logger.info("Querying SubsRo for %s subtitles of %s", language, imdb_id) |
| 132 | + if not imdb_id: |
| 133 | + return [] |
| 134 | + |
| 135 | + url = f"https://subs.ro/subtitrari/imdbid/{imdb_id}" |
| 136 | + response = self._request("get", url) |
| 137 | + |
| 138 | + results = [] |
| 139 | + soup = BeautifulSoup(response.text, "html.parser") |
| 140 | + for item in soup.find_all("div", class_="md:col-span-6"): |
| 141 | + if ( |
| 142 | + "flag-rom" in item.find("img")["src"] and language != Language("ron") |
| 143 | + ) or ( |
| 144 | + "flag-eng" in item.find("img")["src"] and language != Language("eng") |
| 145 | + ): |
| 146 | + continue # Skip if English flag and language is not English or Romanian flag and language is not Romanian |
| 147 | + |
| 148 | + episode_number = video.episode if isinstance(video, Episode) else None |
| 149 | + |
| 150 | + div_tag = item.find("div", class_="col-span-2 lg:col-span-1") |
| 151 | + download_link = None |
| 152 | + if div_tag: |
| 153 | + a_tag = div_tag.find("a") |
| 154 | + if a_tag and a_tag.has_attr("href"): |
| 155 | + download_link = a_tag["href"] |
| 156 | + |
| 157 | + h1_tag = item.find( |
| 158 | + "h1", |
| 159 | + class_="leading-tight text-base font-semibold mb-1 border-b border-dashed border-gray-300 text-[#7f431e] hover:text-red-800", |
| 160 | + ) |
| 161 | + title = None |
| 162 | + year = None |
| 163 | + if h1_tag: |
| 164 | + a_tag = h1_tag.find("a") |
| 165 | + if a_tag and a_tag.text: |
| 166 | + title_raw = a_tag.text.strip() |
| 167 | + title = re.sub( |
| 168 | + r"\s*(-\s*Sezonul\s*\d+)?\s*\(\d{4}\).*$", "", title_raw |
| 169 | + ).strip() |
| 170 | + year = re.search(r"\((\d{4})\)", title_raw).group(1) |
| 171 | + season = re.search(r"\s*Sezonul\s *\d?", title_raw) |
| 172 | + if season: |
| 173 | + season = int(season.group(0).replace("Sezonul", "").strip()) |
| 174 | + |
| 175 | + release_info = None |
| 176 | + p_tag = item.find( |
| 177 | + "p", class_="text-sm font-base overflow-auto h-auto lg:h-16" |
| 178 | + ) |
| 179 | + if p_tag: |
| 180 | + span_blue = p_tag.find("span", style=lambda s: s and "color: blue" in s) |
| 181 | + if span_blue: |
| 182 | + release_info = span_blue.get_text(strip=True) |
| 183 | + else: |
| 184 | + release_info = p_tag.get_text(separator="\n", strip=True) |
| 185 | + |
| 186 | + if download_link and title and year: |
| 187 | + results.append( |
| 188 | + SubsRoSubtitle( |
| 189 | + language, |
| 190 | + title, |
| 191 | + download_link, |
| 192 | + f"tt{imdb_id}", |
| 193 | + isinstance(video, Episode), |
| 194 | + episode_number, |
| 195 | + year, |
| 196 | + release_info, |
| 197 | + season, |
| 198 | + ) |
| 199 | + ) |
| 200 | + return results |
| 201 | + |
| 202 | + def list_subtitles(self, video, languages): |
| 203 | + imdb_id = None |
| 204 | + try: |
| 205 | + if isinstance(video, Episode): |
| 206 | + imdb_id = video.series_imdb_id[2:] |
| 207 | + else: |
| 208 | + imdb_id = video.imdb_id[2:] |
| 209 | + except: |
| 210 | + logger.error( |
| 211 | + "Error parsing imdb_id from video object {}".format(str(video)) |
| 212 | + ) |
| 213 | + |
| 214 | + subtitles = [s for lang in languages for s in self.query(lang, imdb_id, video)] |
| 215 | + return subtitles |
| 216 | + |
| 217 | + def download_subtitle(self, subtitle): |
| 218 | + logger.info("Downloading subtitle from SubsRo: %s", subtitle.page_link) |
| 219 | + response = self._request("get", subtitle.page_link) |
| 220 | + |
| 221 | + archive_stream = io.BytesIO(response.content) |
| 222 | + if is_rarfile(archive_stream): |
| 223 | + logger.debug("Archive identified as RAR") |
| 224 | + archive = RarFile(archive_stream) |
| 225 | + elif is_zipfile(archive_stream): |
| 226 | + logger.debug("Archive identified as ZIP") |
| 227 | + archive = ZipFile(archive_stream) |
| 228 | + else: |
| 229 | + if subtitle.is_valid(): |
| 230 | + subtitle.content = response.content |
| 231 | + return True |
| 232 | + else: |
| 233 | + subtitle.content = None |
| 234 | + return False |
| 235 | + |
| 236 | + subtitle.content = self.get_subtitle_from_archive(subtitle, archive) |
| 237 | + return True |
| 238 | + |
| 239 | + def _request(self, method, url, **kwargs): |
| 240 | + try: |
| 241 | + response = self.session.request(method, url, **kwargs) |
| 242 | + except Exception as e: |
| 243 | + logger.error("SubsRo request error: %s", e) |
| 244 | + raise APIThrottled(f"SubsRo request failed: {e}") |
| 245 | + |
| 246 | + if response.status_code == 429: |
| 247 | + logger.warning("SubsRo: Too many requests (HTTP 429) for %s", url) |
| 248 | + raise TooManyRequests("SubsRo: Too many requests (HTTP 429)") |
| 249 | + if response.status_code >= 500: |
| 250 | + logger.warning("SubsRo: Server error %s for %s", response.status_code, url) |
| 251 | + raise APIThrottled(f"SubsRo: Server error {response.status_code}") |
| 252 | + if response.status_code != 200: |
| 253 | + logger.warning( |
| 254 | + "SubsRo: Unexpected status %s for %s", response.status_code, url |
| 255 | + ) |
| 256 | + raise APIThrottled(f"SubsRo: Unexpected status {response.status_code}") |
| 257 | + |
| 258 | + return response |
0 commit comments