Skip to content

Commit dd27037

Browse files
Added SubsRo provider
1 parent aee7dd6 commit dd27037

File tree

3 files changed

+264
-0
lines changed

3 files changed

+264
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ If you need something that is not already part of Bazarr, feel free to create a
7979
- Subs4Series
8080
- Subscene
8181
- Subscenter
82+
- SubsRo
8283
- Subsunacs.net
8384
- SubSynchro
8485
- Subtitrari-noi.ro
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
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

frontend/src/pages/Settings/Providers/list.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,11 @@ export const ProviderList: Readonly<ProviderInfo[]> = [
490490
"Greek Subtitles Provider.\nRequires anti-captcha provider to solve captchas for each download.",
491491
},
492492
{ key: "subscenter", description: "Hebrew Subtitles Provider" },
493+
{
494+
key: "subsro",
495+
name: "subs.ro",
496+
description: "Romanian Subtitles Provider",
497+
},
493498
{
494499
key: "subsunacs",
495500
name: "Subsunacs.net",

0 commit comments

Comments
 (0)