├── .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 | --------------------------------------------------------------------------------