├── .travis.yml
├── README.md
├── Subscene.php
├── phpunit.xml
└── tests
└── SubsceneTest.php
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: focal
2 |
3 | language: php
4 |
5 | php:
6 | - 7.4
7 | - 8.0
8 |
9 | script: phpunit --configuration phpunit.xml --coverage-text
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Subscene-API-PHP
2 |
3 | Unofficial API for Subscene subtitle service, written in PHP
4 |
5 | ## Usage
6 |
7 | Just include `Subscene.php` to your project and use it:
8 |
9 | ```php
10 | Array
18 | // (
19 | // [title] => Fast Five - How We Roll (Fast Five) [Album- iDon] (2009)
20 | // [url] => https://subscene.com/subtitles/fast-five-how-we-roll-fast-five-album-idon
21 | // )
22 | //
23 | // [1] => Array
24 | // (
25 | // [title] => Fast Five (Fast & Furious 5: The Rio Heist) (2011)
26 | // [url] => https://subscene.com/subtitles/fast-five-fast-and-furious-5-the-rio-heist
27 | // )
28 | //
29 | // [2] => Array
30 | // (
31 | // [title] => How We Roll Fast Five Remix (2011)
32 | // [url] => https://subscene.com/subtitles/how-we-roll-fast-five-remix
33 | // )
34 | //
35 | // [3] => Array
36 | // (
37 | // [title] => Fast Five (Fast & Furious 5: The Rio Heist) (2011)
38 | // [url] => https://subscene.com/subtitles/fast-five-fast-and-furious-5-the-rio-heist
39 | // )
40 | // )
41 |
42 | $subtitles = Subscene::getSubtitles("https://subscene.com/subtitles/fast-five-fast-and-furious-5-the-rio-heist");
43 | // Array
44 | // (
45 | // [title] => Fast Five (Fast & Furious 5: The Rio Heist)
46 | // [year] => 2011
47 | // [poster] => https://i.jeded.com/i/fast-five-fast-and-furious-5-the-rio-heist.154-8128.jpg
48 | // [imdb] => https://www.imdb.com/title/tt1596343
49 | // [subtitles] => Array
50 | // (
51 | // [0] => Array
52 | // (
53 | // [title] => Fast.Five.2011.EXTENDED.720p.BluRay.DTS.x264-DON
54 | // [language] => Farsi/Persian
55 | // [author] => Array
56 | // (
57 | // [name] => msasanmh
58 | // [url] => https://subscene.com/u/797826
59 | // )
60 | //
61 | // [comment] => برای تمامی نسخههای بلوری و غیر اکستندد ---ترجمه جدید سال 2015
62 | // [url] => https://subscene.com/subtitles/fast-five-fast-and-furious-5-the-rio-heist/farsi_persian/1108695
63 | // )
64 | //
65 | // [1] => Array
66 | // (
67 | // [title] => Fast.Five.2011.BluRay.720p.DTS.x264-CHD
68 | // [language] => Farsi/Persian
69 | // [author] => Array
70 | // (
71 | // [name] => msasanmh
72 | // [url] => https://subscene.com/u/797826
73 | // )
74 | //
75 | // [comment] => برای تمامی نسخههای بلوری و غیر اکستندد ---ترجمه جدید سال 2015
76 | // [url] => https://subscene.com/subtitles/fast-five-fast-and-furious-5-the-rio-heist/farsi_persian/1108695
77 | // )
78 | // )
79 |
80 | $subtitle_info = Subscene::getSubtitleInfo("https://subscene.com/subtitles/fast-five-fast-and-furious-5-the-rio-heist/farsi_persian/1108695");
81 | // Array
82 | // (
83 | // [title] => Fast Five (Fast & Furious 5: The Rio Heist)
84 | // [poster] => https://i.jeded.com/i/fast-five-fast-and-furious-5-the-rio-heist.154-8128.jpg
85 | // [author] => msasanmh
86 | // [comment] => برای تمامی نسخههای بلوری و غیر اکستندد ---
87 | // ترجمه جدید سال 2015
88 | // [imdb] => https://www.imdb.com/title/tt1596343
89 | // [preview] =>
90 | // 1
00:00:35,702 --> 00:00:38,128
JUDGE: Dominic Toretto.
2
00:00:38,455 --> 00:00:42,499
You are // hereby sentenced
to serve 25 years to life
3
00:00:42,500 --> 00:00:46,295
at the Lompoc Maximum Security
Prison system
4
00:00:46,296 --> 00:00:50,098
<i>without the possibility
of early parole.</i>
5
00:01:35,845 --> 00:01:37,645
(TIRES SQUEALING)
6
91 | //
92 | // [info] => Fast.Five.2011.720p.BluRay.x264.YIFY
93 | // Fast.Five.2011.1080p.BluRay.x264.YIFY
94 | // Fast.Five.2011.720p.BluRay.x264.DTS-WiKi
95 | // Fast.Five.2011.1080p.BluRay.x264.DTS-WiKi
96 | // Fast.Five.2011.BluRay.720p.DTS.x264-CHD
97 | // Fast.Five.2011.BluRay.1080p.DTS.x264-CHD
98 | // Fast.Five.2011.EXTENDED.720p.BluRay.DTS.x264-DON
99 | // Fast.Five.2011.BluRay.1080p.DTS.DUAL.x264
100 | // Fast and Furious 5 Fast Five 2011 BDRip 500MB x264 AAC-DiDee
101 | // Fast Five 2011 720p BDRip XviD AC3-ViSiON
102 | //
103 | // [details] => Online:5/1/2015 9:34 AM
104 | // Hearing Impaired:No
105 | // Foreign parts:Yes
106 | // Framerate:23.976
107 | // Files:2 (72,727bytes)
108 | // Production type:Translated a subtitle
109 | // Release type:Blu-ray
110 | // ---------------------------------------
111 | // Rated:10/10 from15 users
112 | // Voted as Good by:15 users
113 | // Downloads:2,438
114 | //
115 | // [download_url] => https://subscene.com/subtitles/farsi_persian-text/WqoLpe1QAjx6piQHWXmhrAM3cg2siBVAHTCqvpqNOvuT4VkzRdV0daps-FEMwk__g1FFHwEbjQ9G23wsaVEeYnk2VAVS3x2ExEMA-z7JdEGvZHH-sYVYDMyfsiTKaj_50
116 | // )
117 | ```
118 |
--------------------------------------------------------------------------------
/Subscene.php:
--------------------------------------------------------------------------------
1 | $title,
19 | ]
20 | );
21 | $titles = self::xpathQuery("//ul/li/div[@class='title']/a/text()", $page);
22 | $urls = self::xpathQuery("//ul/li/div[@class='title']/a/@href", $page);
23 | $results = [];
24 | for ($i = 0; $i < count($titles); $i++) {
25 | $results[] = [
26 | 'title' => $titles[$i]->nodeValue,
27 | 'url' => self::BASE_URL.$urls[$i]->nodeValue,
28 | ];
29 | }
30 |
31 | return $results;
32 | }
33 |
34 | public static function getSubtitles(string $url, array $languages = []): array
35 | {
36 | $cookie = null;
37 | if (!empty($languages)) {
38 | $cookie = 'LanguageFilter='.implode(',', $languages);
39 | }
40 | $page = self::curl_get_contents($url, $cookie);
41 | $result = [];
42 | foreach ([
43 | 'title' => '//h2/text()',
44 | 'year' => "//li[strong[contains(text(), 'Year')]]/text()[last()]",
45 | 'poster' => "//img[@alt='Poster']/@src",
46 | 'imdb' => "//a[@target='_blank' and @class='imdb']/@href",
47 | ] as $part => $query) {
48 | ${$part} = self::xpathQuery($query, $page);
49 | if (count(${$part}) > 0) {
50 | $result[$part] = trim(${$part}[0]->nodeValue);
51 | }
52 | }
53 | $subtitle_nodes = self::xpathQuery("//tr[td[@class='a5']]", $page);
54 | $titles = self::xpathQuery('//tr/td/a/span[2]/text()', $page);
55 | $languages = self::xpathQuery('//tr/td/a/span[1]/text()', $page);
56 | $authors_names = self::xpathQuery("//td[@class='a5']/a/text()", $page);
57 | $authors_urls = self::xpathQuery("//td[@class='a5']/a/@href", $page);
58 | $comments = self::xpathQuery("//td[@class='a6']/div/text()", $page);
59 | $urls = self::xpathQuery('//tr/td[1]/a/@href', $page);
60 | $subtitles = [];
61 | foreach ($subtitle_nodes as $subtitle_node) {
62 | $subtitle_node_html = $subtitle_node->ownerDocument->saveHTML($subtitle_node);
63 | $title = trim(self::xpathQuery('//td/a/span[2]/text()', $subtitle_node_html)[0]->nodeValue);
64 | $language = trim(self::xpathQuery('//td/a/span[1]/text()', $subtitle_node_html)[0]->nodeValue);
65 | $author_name = self::xpathEvaluate("boolean(//td[@class='a5']/a/text())", $subtitle_node_html) ? trim(self::xpathQuery("//td[@class='a5']/a/text()", $subtitle_node_html)[0]->nodeValue) : trim(self::xpathQuery("//td[@class='a5']/text()", $subtitle_node_html)[0]->nodeValue);
66 | $author_url = $author_name = self::xpathEvaluate("boolean(//td[@class='a5']/a/@href)", $subtitle_node_html) ? (self::BASE_URL.trim(self::xpathQuery("//td[@class='a5']/a/@href", $subtitle_node_html)[0]->nodeValue)) : 'n/A';
67 | $author = [
68 | 'name' => $author_name,
69 | 'url' => $author_url,
70 | ];
71 | $comment = trim(self::xpathQuery("//td[@class='a6']/div/text()", $subtitle_node_html)[0]->nodeValue);
72 | $url = self::BASE_URL.trim(self::xpathQuery('//tr/td[1]/a/@href', $subtitle_node_html)[0]->nodeValue);
73 | $subtitles[] = compact('title', 'language', 'author', 'comment', 'url');
74 | }
75 | $result['subtitles'] = $subtitles;
76 |
77 | return $result;
78 | }
79 |
80 | public static function getSubtitleInfo(string $url): array
81 | {
82 | $page = self::curl_get_contents($url);
83 | $result = [];
84 | $url = self::xpathQuery("//a[@id='downloadButton']/@href", $page);
85 | if (count($url) < 1) {
86 | return false;
87 | }
88 | foreach ([
89 | 'title' => "//span[@itemprop='name']",
90 | 'poster' => "//img[@alt='Poster']/@src",
91 | 'author' => "//li[@class='author']/a/text()",
92 | 'comment' => "//div[@class='comment']",
93 | 'imdb' => "//a[@class='imdb']/@href",
94 | ] as $part => $query) {
95 | ${$part} = self::xpathQuery($query, $page);
96 | if (count(${$part}) > 0) {
97 | $result[$part] = trim(${$part}[0]->nodeValue);
98 | }
99 | }
100 | $preview = self::xpathQuery("//div[@id='preview']/p", $page);
101 | $result['preview'] = $preview[0]->ownerDocument->saveHTML($preview[0]);
102 | $info = self::xpathQuery("//li[@class='release']/div", $page);
103 | if (count($info) > 0) {
104 | $info_text = '';
105 | for ($i = 0; $i < count($info); $i++) {
106 | $info_text .= trim($info[$i]->nodeValue)."\n";
107 | }
108 | $result['info'] = $info_text;
109 | }
110 | $details = self::xpathQuery("//div[@id='details']/ul/li", $page);
111 | if (count($details) > 0) {
112 | $details_text = '';
113 | for ($i = 0; $i < count($details); $i++) {
114 | $details_text .= trim(str_replace(["\n", "\r", "\t"], '', $details[$i]->nodeValue))."\n";
115 | }
116 | $result['details'] = $details_text;
117 | }
118 | $result['download_url'] = self::BASE_URL.$url[0]->nodeValue;
119 |
120 | return $result;
121 | }
122 |
123 | public static function getHome(): array
124 | {
125 | $page = self::curl_get_contents(self::BASE_URL);
126 | $result = [
127 | 'popular' => [],
128 | 'popular_tv' => [],
129 | 'recent' => [],
130 | ];
131 |
132 | // Popular subtitles
133 | $titles = self::xpathQuery("//div[@class='popular-films']/div[@class='box'][1]//div[@class='title']/a[1]/text()", $page);
134 | $posters = self::xpathQuery("//div[@class='popular-films']/div[@class='box'][1]//div[@class='poster']/img/@src", $page);
135 | $imdbs = self::xpathQuery("//div[@class='popular-films']/div[@class='box'][1]//div[@class='title']/a[2]/@href", $page);
136 | $urls = self::xpathQuery("//div[@class='popular-films']/div[@class='box'][1]//div[@class='title']/a[1]/@href", $page);
137 | for ($i = 0; $i < count($titles); $i++) {
138 | $item = [
139 | 'title' => trim($titles[$i]->nodeValue),
140 | 'poster' => $posters[$i]->nodeValue,
141 | 'url' => self::BASE_URL.$urls[$i]->nodeValue,
142 | ];
143 | if (!empty($imdbs[$i])) {
144 | $item['poseter'] = $posters[$i]->nodeValue;
145 | }
146 | $result['popular'][] = $item;
147 | }
148 |
149 | // Popular tv subtitles
150 | $titles = self::xpathQuery("//div[@class='popular-films']/div[@class='box'][2]//div[@class='title']/a[1]/text()", $page);
151 | $posters = self::xpathQuery("//div[@class='popular-films']/div[@class='box'][2]//div[@class='poster']/img/@src", $page);
152 | $imdbs = self::xpathQuery("//div[@class='popular-films']/div[@class='box'][2]//div[@class='title']/a[2]/@href", $page);
153 | $urls = self::xpathQuery("//div[@class='popular-films']/div[@class='box'][2]//div[@class='title']/a[1]/@href", $page);
154 | for ($i = 0; $i < count($titles); $i++) {
155 | $item = [
156 | 'title' => trim($titles[$i]->nodeValue),
157 | 'poster' => $posters[$i]->nodeValue,
158 | 'url' => self::BASE_URL.$urls[$i]->nodeValue,
159 | ];
160 | if (!empty($imdbs[$i])) {
161 | $item['poseter'] = $posters[$i]->nodeValue;
162 | }
163 | $result['popular_tv'][] = $item;
164 | }
165 |
166 | // Recent subtitles
167 | $titles = self::xpathQuery("//div[@class='recent-subtitles']//li/div/a/text()[last()]", $page);
168 | $urls = self::xpathQuery("//div[@class='recent-subtitles']//li/div/a/@href", $page);
169 | $contributors_names = self::xpathQuery("//div[@class='recent-subtitles']//li/address/a/text()", $page);
170 | $contributors_urls = self::xpathQuery("//div[@class='recent-subtitles']//li/address/a/@href", $page);
171 | for ($i = 0; $i < count($titles); $i++) {
172 | $result['recent'][] = [
173 | 'title' => trim($titles[$i]->nodeValue),
174 | 'contributor' => [
175 | 'name' => trim($contributors_names[$i]->nodeValue),
176 | 'url' => self::BASE_URL.trim($contributors_urls[$i]->nodeValue),
177 | ],
178 | 'url' => self::BASE_URL.$urls[$i]->nodeValue,
179 | ];
180 | }
181 |
182 | return $result;
183 | }
184 |
185 | public static function getDownload(string $url, string $filename): void
186 | {
187 | $data = self::curl_get_contents($url);
188 | $file_name = $filename;
189 | header('Content-Type: application/zip');
190 | header("Content-Disposition: attachment; filename=$file_name");
191 | header('Content-Length: '.strlen($data));
192 | echo $data;
193 | }
194 |
195 | private static function curl_get_contents(string $url, ?string $cookie = null): string
196 | {
197 | $ch = curl_init($url);
198 | curl_setopt_array($ch, [
199 | CURLOPT_RETURNTRANSFER => true,
200 | CURLOPT_FOLLOWLOCATION => true,
201 | CURLOPT_SSL_VERIFYPEER => false,
202 | CURLOPT_SSL_VERIFYHOST => false,
203 | CURLOPT_USERAGENT => self::USERAGENT,
204 | ]);
205 | if (!is_null($cookie)) {
206 | curl_setopt($ch, CURLOPT_COOKIE, $cookie);
207 | }
208 | $response = curl_exec($ch);
209 | curl_close($ch);
210 |
211 | return $response;
212 | }
213 |
214 | private static function curl_post(string $url, ?array $parameters = null): string
215 | {
216 | $ch = curl_init($url);
217 | curl_setopt_array($ch, [
218 | CURLOPT_RETURNTRANSFER => true,
219 | CURLOPT_FOLLOWLOCATION => true,
220 | CURLOPT_SSL_VERIFYPEER => false,
221 | CURLOPT_SSL_VERIFYHOST => false,
222 | CURLOPT_USERAGENT => self::USERAGENT,
223 | CURLOPT_POST => true,
224 | CURLOPT_POSTFIELDS => $parameters,
225 | ]);
226 | $response = curl_exec($ch);
227 | curl_close($ch);
228 |
229 | return $response;
230 | }
231 |
232 | private static function xpathQuery(string $query, string $html): DOMNodeList
233 | {
234 | $libxml_use_internal_errors = libxml_use_internal_errors(true);
235 | if (empty($query) || empty($html)) {
236 | return new DOMNodeList();
237 | }
238 | $xpath = self::htmlToDomXPath($html);
239 | $results = $xpath->query($query);
240 | libxml_use_internal_errors($libxml_use_internal_errors);
241 |
242 | return $results;
243 | }
244 |
245 | private static function xpathEvaluate(string $expression, string $html)
246 | {
247 | $libxml_use_internal_errors = libxml_use_internal_errors(true);
248 | if (empty($expression) || empty($html)) {
249 | return false;
250 | }
251 | $xpath = self::htmlToDomXPath($html);
252 | $result = $xpath->evaluate($expression);
253 | libxml_use_internal_errors($libxml_use_internal_errors);
254 |
255 | return $result;
256 | }
257 |
258 | private static function htmlToDomXPath(string $html): DomXPath
259 | {
260 | $dom = new DomDocument();
261 | $dom->loadHTML("$html");
262 | $xpath = new DomXPath($dom);
263 |
264 | return $xpath;
265 | }
266 | }
267 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | ./tests
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/tests/SubsceneTest.php:
--------------------------------------------------------------------------------
1 | assertNotEmpty(Subscene::search('Fast Five'));
15 | }
16 |
17 | public function testGetSubtitles()
18 | {
19 | $this->assertNotEmpty(Subscene::getSubtitles('https://subscene.com/subtitles/fast-five-fast-and-furious-5-the-rio-heist'));
20 | }
21 |
22 | public function testGetSubtitlesCustomLanguage()
23 | {
24 | $subtitles = Subscene::getSubtitles('https://subscene.com/subtitles/fast-five-fast-and-furious-5-the-rio-heist', [46]);
25 | $invalid_language_found = false;
26 | foreach ($subtitles['subtitles'] as $subtitle) {
27 | if ($subtitle['language'] != 'Farsi/Persian') {
28 | $invalid_language_found = true;
29 | break;
30 | }
31 | }
32 | $this->assertFalse($invalid_language_found);
33 | }
34 |
35 | public function testGetSubtitleInfo()
36 | {
37 | $this->assertNotEmpty(Subscene::getSubtitleInfo('https://subscene.com/subtitles/fast-five-fast-and-furious-5-the-rio-heist/farsi_persian/1108695'));
38 | }
39 | }
40 |
--------------------------------------------------------------------------------