Skip to content

Commit f5c9ecb

Browse files
authored
Added Bayflix and Vladoon Bulgarian subtitles providers
1 parent 3c3d410 commit f5c9ecb

5 files changed

Lines changed: 816 additions & 0 deletions

File tree

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import absolute_import
3+
4+
import codecs
5+
import logging
6+
import re
7+
from hashlib import sha1
8+
from random import randint
9+
10+
from dogpile.cache.api import NO_VALUE
11+
from dogpile.cache.exception import RegionNotConfigured
12+
from requests import Session
13+
from subliminal.cache import region
14+
from subliminal.video import Episode
15+
from subliminal.video import Movie
16+
from subliminal_patch.providers import Provider
17+
from subliminal_patch.providers.utils import get_archive_from_bytes
18+
from subliminal_patch.providers.utils import get_subtitle_from_archive
19+
from subliminal_patch.providers.utils import update_matches
20+
from subliminal_patch.subtitle import Subtitle
21+
from subliminal_patch.subtitle import guess_matches
22+
from guessit import guessit
23+
from subzero.language import Language
24+
25+
from .utils import FIRST_THOUSAND_OR_SO_USER_AGENTS as AGENT_LIST
26+
27+
28+
logger = logging.getLogger(__name__)
29+
30+
_BASE_URL = "https://bayflix.sb"
31+
_SEARCH_URL = _BASE_URL + "/api/subtitles/search"
32+
_DOWNLOAD_URL = _BASE_URL + "/api/subtitles/download/{id}"
33+
34+
35+
def _extract_fps(description):
36+
match = re.search(r"FPS:\s*([\d.]+)", description or "", re.I)
37+
if not match:
38+
return None
39+
40+
try:
41+
return float(match.group(1))
42+
except ValueError:
43+
return None
44+
45+
46+
def _extract_cds(description):
47+
match = re.search(r"CDs?:\s*(\d+)", description or "", re.I)
48+
if not match:
49+
return None
50+
51+
try:
52+
return int(match.group(1))
53+
except ValueError:
54+
return None
55+
56+
57+
def _episode_tuple(text):
58+
if not text:
59+
return None
60+
61+
match = re.search(r"\b[Ss](\d{1,2})[Ee](\d{1,2})\b", text)
62+
if match:
63+
return int(match.group(1)), int(match.group(2))
64+
65+
match = re.search(r"\b(\d{1,2})x(\d{1,2})\b", text)
66+
if match:
67+
return int(match.group(1)), int(match.group(2))
68+
69+
return None
70+
71+
72+
def _wanted_episode(video):
73+
if not isinstance(video, Episode):
74+
return None
75+
76+
return video.season, video.episode
77+
78+
79+
def _matches_episode(item, video):
80+
wanted = _wanted_episode(video)
81+
if wanted is None:
82+
return True
83+
84+
for release in item.get("release_name") or []:
85+
if _episode_tuple(release) == wanted:
86+
return True
87+
88+
for line in (item.get("description") or "").splitlines():
89+
if _episode_tuple(line) == wanted:
90+
return True
91+
92+
return False
93+
94+
95+
def _matches_movie_year(item, video):
96+
if not isinstance(video, Movie) or not video.year:
97+
return True
98+
99+
release_year = (item.get("release_date") or "")[:4]
100+
if not release_year:
101+
return True
102+
103+
try:
104+
return abs(int(release_year) - int(video.year)) <= 1
105+
except ValueError:
106+
return True
107+
108+
109+
def _search_title(video):
110+
if isinstance(video, Episode):
111+
return video.series.strip()
112+
113+
return video.title.strip()
114+
115+
116+
def _cache_get(cache_key):
117+
try:
118+
return region.get(cache_key)
119+
except RegionNotConfigured:
120+
return NO_VALUE
121+
122+
123+
def _cache_set(cache_key, response):
124+
try:
125+
region.set(cache_key, response)
126+
except RegionNotConfigured:
127+
pass
128+
129+
130+
def _cache_delete(cache_key):
131+
try:
132+
region.delete(cache_key)
133+
except RegionNotConfigured:
134+
pass
135+
136+
137+
class BayflixSubtitle(Subtitle):
138+
provider_name = "bayflix"
139+
140+
def __init__(
141+
self,
142+
language,
143+
page_link,
144+
file_id,
145+
title,
146+
release_names,
147+
year=None,
148+
media_type=None,
149+
video=None,
150+
fps=None,
151+
num_cds=None,
152+
):
153+
super(BayflixSubtitle, self).__init__(language)
154+
self.page_link = page_link
155+
self.file_id = str(file_id)
156+
self.title = title or ""
157+
self.release_names = release_names or []
158+
self.release_info = "\n".join(self.release_names) or self.title
159+
self.year = year
160+
self.media_type = media_type
161+
self.video = video
162+
self.fps = fps
163+
self.num_cds = num_cds
164+
self.matches = set()
165+
166+
@property
167+
def id(self):
168+
return self.file_id
169+
170+
def get_fps(self):
171+
return self.fps
172+
173+
def make_picklable(self):
174+
self.content = None
175+
self._is_valid = False
176+
return self
177+
178+
def get_matches(self, video):
179+
self.matches = set()
180+
guess_type = "episode" if isinstance(video, Episode) else "movie"
181+
182+
self.matches |= guess_matches(video, guessit(self.title, {"type": guess_type}))
183+
update_matches(self.matches, video, self.release_info, split="\n")
184+
185+
if isinstance(video, Movie) and video.year and self.year == video.year:
186+
self.matches.add("year")
187+
188+
return self.matches
189+
190+
191+
class BayflixProvider(Provider):
192+
languages = {Language("bul")}
193+
video_types = (Episode, Movie)
194+
195+
def initialize(self):
196+
self.session = Session()
197+
self.session.headers["User-Agent"] = AGENT_LIST[randint(0, len(AGENT_LIST) - 1)]
198+
self.session.headers["Accept"] = "application/json, text/plain, */*"
199+
self.session.headers["Accept-Language"] = "en-US,en;q=0.9"
200+
self.session.headers["Referer"] = _BASE_URL + "/"
201+
202+
def terminate(self):
203+
self.session.close()
204+
205+
def query(self, language, video):
206+
subtitles = []
207+
params = {"title": _search_title(video)}
208+
209+
logger.debug("Searching Bayflix subtitles: %r", params)
210+
response = self.session.get(_SEARCH_URL, params=params, timeout=20)
211+
response.raise_for_status()
212+
213+
for item in response.json() or []:
214+
if not _matches_movie_year(item, video):
215+
continue
216+
if not _matches_episode(item, video):
217+
continue
218+
219+
file_id = item.get("_id")
220+
if not file_id:
221+
continue
222+
223+
page_link = item.get("subtitle_link") or _DOWNLOAD_URL.format(id=file_id)
224+
release_names = item.get("release_name") or []
225+
if isinstance(release_names, str):
226+
release_names = [release_names]
227+
228+
release_year = (item.get("release_date") or "")[:4]
229+
try:
230+
release_year = int(release_year) if release_year else None
231+
except ValueError:
232+
release_year = None
233+
234+
subtitle = BayflixSubtitle(
235+
language=language,
236+
page_link=page_link,
237+
file_id=file_id,
238+
title=item.get("title"),
239+
release_names=release_names,
240+
year=release_year,
241+
media_type=item.get("media_type"),
242+
video=video,
243+
fps=_extract_fps(item.get("description")),
244+
num_cds=_extract_cds(item.get("description")),
245+
)
246+
logger.debug("Found Bayflix subtitle: %s", subtitle)
247+
subtitles.append(subtitle)
248+
249+
return subtitles
250+
251+
def list_subtitles(self, video, languages):
252+
return [subtitle for language in languages for subtitle in self.query(language, video)]
253+
254+
def download_subtitle(self, subtitle):
255+
logger.debug("Downloading Bayflix subtitle %r", subtitle.page_link)
256+
cache_key = sha1(subtitle.page_link.encode("utf-8")).digest()
257+
response = _cache_get(cache_key)
258+
259+
if response is NO_VALUE:
260+
response = self.session.get(subtitle.page_link, timeout=30)
261+
response.raise_for_status()
262+
_cache_set(cache_key, response)
263+
else:
264+
logger.debug(
265+
"Using cache file %s",
266+
codecs.encode(cache_key, "hex_codec").decode("utf-8"),
267+
)
268+
269+
archive = get_archive_from_bytes(response.content)
270+
if archive is None:
271+
logger.error("Ignore unsupported Bayflix archive %r", response.headers)
272+
_cache_delete(cache_key)
273+
return
274+
275+
subtitle.content = get_subtitle_from_archive(
276+
archive,
277+
episode=subtitle.video.episode if isinstance(subtitle.video, Episode) else None,
278+
get_first_subtitle=not isinstance(subtitle.video, Episode),
279+
)
280+
if not subtitle.content:
281+
logger.error("No subtitle found in Bayflix archive %r", response.headers)
282+
_cache_delete(cache_key)

0 commit comments

Comments
 (0)