├── .gitignore ├── src ├── Facades │ └── Torrent.php ├── Categories.php ├── HttpClientTrait.php ├── TorrentAdapterInterface.php ├── TorrentScraper.php ├── TorrentServiceProvider.php ├── BuildUrlTrait.php ├── Result │ └── Torrent.php └── Adapter │ ├── PirateBayAdapter.php │ └── KickassAdapter.php ├── composer.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .idea 3 | composer.lock -------------------------------------------------------------------------------- /src/Facades/Torrent.php: -------------------------------------------------------------------------------- 1 | httpClient = $httpClient; 14 | } 15 | 16 | public function getHttpClient() 17 | { 18 | return $this->httpClient; 19 | } 20 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bstien/laravel-torrent", 3 | "description": "Search for torrents with this package", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Bastian Stien", 8 | "email": "bastian.stien@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "guzzlehttp/guzzle": "5.2.0", 13 | "symfony/dom-crawler": "~2.6", 14 | "symfony/css-selector": "~2.6" 15 | }, 16 | "require-dev": { 17 | "illuminate/support": "~5.0" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "Stien\\Torrent\\": "src" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/TorrentAdapterInterface.php: -------------------------------------------------------------------------------- 1 | adapters[] = $adapter; 11 | } 12 | 13 | public function search($query, $category = null) 14 | { 15 | $results = []; 16 | 17 | // Default to Categories::ALL if none specified. 18 | if ( ! is_int($category) || $category == null ) 19 | { 20 | $category = Categories::ALL; 21 | } 22 | 23 | foreach ($this->adapters as $adapter) 24 | { 25 | $results[] = $adapter->search($query, $category); 26 | } 27 | $results = array_flatten($results); 28 | 29 | return $this->sortResults($results); 30 | } 31 | 32 | protected function sortResults($results) 33 | { 34 | // TODO: Do actual sorting. 35 | // For now, sort it by seeders. 36 | usort($results, function ($a, $b) 37 | { 38 | return $a->getSeeders() <= $b->getSeeders(); 39 | }); 40 | 41 | return $results; 42 | } 43 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Bastian Stien 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/TorrentServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind('bstien.torrent.scraper', function ($app) 28 | { 29 | $torrentScraper = new TorrentScraper(); 30 | 31 | // Add PirateBayAdapter 32 | $pirateBayAdapter = new PirateBayAdapter(); 33 | $pirateBayAdapter->setHttpClient(new Client()); 34 | $torrentScraper->addAdapter($pirateBayAdapter); 35 | 36 | // Add KickassAdapter 37 | $kickassAdapter = new KickassAdapter(); 38 | $kickassAdapter->setHttpClient(new Client()); 39 | $torrentScraper->addAdapter($kickassAdapter); 40 | 41 | return $torrentScraper; 42 | }); 43 | } 44 | 45 | /** 46 | * Get the services provided by the provider. 47 | * 48 | * @return array 49 | */ 50 | public function provides() 51 | { 52 | return []; 53 | } 54 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Torrent 2 | 3 | A package for [Laravel 5](http://laravel.com/) to scrape for torrents. 4 | 5 | ## Installation 6 | Add this to your `composer.json`: 7 | ```json 8 | "require": { 9 | "bstien/laravel-torrent": "dev-master" 10 | } 11 | ``` 12 | 13 | Register the facade and ServiceProvider in `config/app.php`: 14 | ```php 15 | 'providers' => [ 16 | // ... 17 | 'Stien\Torrent\TorrentServiceProvider', 18 | ]; 19 | 20 | 'aliases' => [ 21 | // ... 22 | 'Torrent => 'Stien\Torrent\Facades\Torrent', 23 | ]; 24 | ``` 25 | 26 | ## Usage 27 | 28 | ### Regular search 29 | Returns an array with `Stien\Torrent\Result\Torrent`-objects if matches are found. If not, an empty array is returned. 30 | ```php 31 | use Stien\Torrent\Facades\Torrent; 32 | # You can register this to your facades-array in config/app.php if you like 33 | 34 | $torrents = Torrent::search("Modern Family"); 35 | 36 | foreach( $torrents as $torrent ) 37 | { 38 | echo $torrent->getTitle(); 39 | } 40 | 41 | 42 | # To search within a specific category, use any of the constants in 43 | # Stien\Torrent\Categories. 44 | ``` 45 | 46 | ### Search in category 47 | Include a category as the second argument to `Torrent::search()`. See constants in `Stien\Torrent\Categories` for reference. 48 | 49 | It defaults to `Categories::ALL` if none are given. 50 | ```php 51 | use Stien\Torrent\Facades\Torrent; 52 | use Stien\Torrent\Categories as CAT; 53 | 54 | $torrents = Torrent::search("Die Hard", CAT::MOVIES_HD); 55 | ``` 56 | 57 | ## Implement your own adapter 58 | To extend this package with another adapter, create a new class and have it implement `Stien\Torrent\TorrentAdapterInterface`. 59 | 60 | Register your adapter with the scraper 61 | ```php 62 | use Stien\Torrent\Facades\Torrent; 63 | 64 | $myAdapter = new MyAdapter(); 65 | $myAdapter->setHttpClient(new \GuzzleHttp\Client); 66 | 67 | Torrent::addAdapter( $myAdapter ); 68 | ``` 69 | -------------------------------------------------------------------------------- /src/BuildUrlTrait.php: -------------------------------------------------------------------------------- 1 | baseUrl = $baseUrl; 27 | } 28 | 29 | /** 30 | * Set placeholders where needed. 31 | * 32 | * %QUERY% - The query. 33 | * %CATEGORY% - The category, as set via setCategoryIdentifiers 34 | * 35 | * @param $searchUrl 36 | */ 37 | protected function setSearchUrl($searchUrl) 38 | { 39 | $this->searchUrl = $searchUrl; 40 | } 41 | 42 | /** 43 | * Set the keywords for categories, which are used to build the URL. 44 | * 45 | * @param array $identifiers 46 | */ 47 | protected function setCategoryIdentifiers(array $identifiers) 48 | { 49 | $this->categoryIdentifiers = $identifiers; 50 | } 51 | 52 | /** 53 | * Build a full URL based on category and sorting order. 54 | * 55 | * @param $query 56 | * @param int $category 57 | * @param int $sort_by 58 | * @return string 59 | */ 60 | protected function makeUrl($query, $category = TC::ALL, $sort_by = null) 61 | { 62 | // TODO: Make URL based on category and sort. 63 | // Make URL. 64 | $url = $this->baseUrl . $this->searchUrl; 65 | $url = $this->formatQuery($url, $query); 66 | $url = $this->formatCategory($url, $category); 67 | 68 | return $url; 69 | } 70 | 71 | /** 72 | * Replace placeholder for query with query 73 | * 74 | * @param string $url 75 | * @param string $query 76 | * @return string 77 | */ 78 | protected function formatQuery($url, $query) 79 | { 80 | // Replace placeholder with actual query. 81 | $query = urlencode($query); 82 | 83 | return preg_replace("/%QUERY%/", $query, $url); 84 | } 85 | 86 | /** 87 | * Replace placeholder for category with category 88 | * 89 | * @param string $url 90 | * @param string $category 91 | * @return string 92 | */ 93 | protected function formatCategory($url, $category) 94 | { 95 | if ( ! isset($this->categoryIdentifiers[$category]) ) 96 | { 97 | return $url; 98 | } 99 | 100 | $category = $this->categoryIdentifiers[$category]; 101 | 102 | return preg_replace("/%CATEGORY%/", $category, $url); 103 | } 104 | } -------------------------------------------------------------------------------- /src/Result/Torrent.php: -------------------------------------------------------------------------------- 1 | site; 53 | } 54 | 55 | /** 56 | * @param string $site 57 | */ 58 | public function setSite($site) 59 | { 60 | $this->site = $site; 61 | } 62 | 63 | /** 64 | * @return string 65 | */ 66 | public function getTitle() 67 | { 68 | return $this->title; 69 | } 70 | 71 | /** 72 | * @param string $title 73 | */ 74 | public function setTitle($title) 75 | { 76 | $this->title = $title; 77 | } 78 | 79 | /** 80 | * @return string 81 | */ 82 | public function getCategory() 83 | { 84 | return $this->category; 85 | } 86 | 87 | /** 88 | * @param string $category 89 | */ 90 | public function setCategory($category) 91 | { 92 | $this->category = $category; 93 | } 94 | 95 | /** 96 | * @return string 97 | */ 98 | public function getSize() 99 | { 100 | return $this->size; 101 | } 102 | 103 | /** 104 | * @param string $size 105 | */ 106 | public function setSize($size) 107 | { 108 | $this->size = $size; 109 | } 110 | 111 | /** 112 | * @return int 113 | */ 114 | public function getSeeders() 115 | { 116 | return $this->seeders; 117 | } 118 | 119 | /** 120 | * @param int $seeders 121 | */ 122 | public function setSeeders($seeders) 123 | { 124 | $this->seeders = $seeders; 125 | } 126 | 127 | /** 128 | * @return int 129 | */ 130 | public function getLeechers() 131 | { 132 | return $this->leechers; 133 | } 134 | 135 | /** 136 | * @param int $leechers 137 | */ 138 | public function setLeechers($leechers) 139 | { 140 | $this->leechers = $leechers; 141 | } 142 | 143 | /** 144 | * @return string 145 | */ 146 | public function getMagnet() 147 | { 148 | return $this->magnet; 149 | } 150 | 151 | /** 152 | * @param string $magnet 153 | */ 154 | public function setMagnet($magnet) 155 | { 156 | $this->magnet = $magnet; 157 | } 158 | 159 | /** 160 | * @return string 161 | */ 162 | public function getAge() 163 | { 164 | return $this->age; 165 | } 166 | 167 | /** 168 | * @param string $age 169 | */ 170 | public function setAge($age) 171 | { 172 | $this->age = $age; 173 | } 174 | } -------------------------------------------------------------------------------- /src/Adapter/PirateBayAdapter.php: -------------------------------------------------------------------------------- 1 | setBaseUrl("https://thepiratebay.se/"); 24 | $this->setSearchUrl("search/%QUERY%/0/7/%CATEGORY%"); 25 | $this->setCategoryIdentifiers([ 26 | TC::ALL => "0", 27 | TC::MOVIES => "201,207", 28 | TC::MOVIES_HD => "207", 29 | TC::TV => "205,208", 30 | TC::TV_HD => "208", 31 | TC::ANIME => "602", 32 | TC::MUSIC => "101,102,103,104", 33 | TC::BOOKS => "601", 34 | // TODO: Should be able to sort by platform. Both apps and games. 35 | TC::APPS => "301,302", 36 | TC::GAMES => "401,402", 37 | // If anyone really needs this category, they can fix the sorting of porn 38 | // and send me a pull-request :) 39 | TC::XXX => "501,502,503,504,505,506,599", 40 | ]); 41 | } 42 | 43 | /** 44 | * Search for torrents. 45 | * 46 | * @param string $query 47 | * @param int $category 48 | * @return array Array of torrents. Either empty or filled. 49 | */ 50 | public function search($query, $category) 51 | { 52 | # Set single-cell view for torrents. 53 | $requestOptions = [ 54 | 'headers' => [ 55 | 'User-Agent' => 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36', 56 | ], 57 | 'cookies' => [ 58 | 'lw' => 's' 59 | ] 60 | ]; 61 | 62 | try 63 | { 64 | $url = $this->makeUrl($query, $category); 65 | $response = $this->httpClient->get($url, $requestOptions); 66 | $crawler = new Crawler((string)$response->getBody()); 67 | } catch (\Exception $e) 68 | { 69 | // TODO: Log error. Some error has occured. 70 | return []; 71 | } 72 | 73 | $items = $crawler->filter('#searchResult tr'); 74 | 75 | $torrents = []; 76 | $firstRow = true; 77 | foreach ($items as $item) 78 | { 79 | // Ignore the first row. 80 | if ( $firstRow ) 81 | { 82 | $firstRow = false; 83 | continue; 84 | } 85 | 86 | $torrent = new Torrent(); 87 | $itemCrawler = new Crawler($item); 88 | 89 | // Set details for torrent. 90 | $torrent->setSite($this->tag); 91 | $torrent->setTitle(trim($itemCrawler->filter('td')->eq(1)->text())); 92 | $torrent->setSeeders((int)$itemCrawler->filter('td')->eq(5)->text()); 93 | $torrent->setLeechers((int)$itemCrawler->filter('td')->eq(6)->text()); 94 | $torrent->setMagnet($itemCrawler->filterXpath('/td[3]/a[0]')->attr('href')); 95 | $torrent->setSize($itemCrawler->filter('td')->eq(4)->text()); 96 | $torrent->setAge($itemCrawler->filterXPath('/td[2]')->text()); 97 | $torrent->setCategory($itemCrawler->filterXPath('/td[0]')->text()); 98 | 99 | $torrents[] = $torrent; 100 | } 101 | 102 | return $torrents; 103 | } 104 | } -------------------------------------------------------------------------------- /src/Adapter/KickassAdapter.php: -------------------------------------------------------------------------------- 1 | setBaseUrl("https://kickass.to/"); 24 | $this->setSearchUrl("usearch/%QUERY%+category:%CATEGORY%?field=seeders&sorder=desc&rss=1"); 25 | $this->setCategoryIdentifiers([ 26 | TC::ALL => "all", 27 | TC::MOVIES => "movies", 28 | TC::MOVIES_HD => "movies", 29 | TC::TV => "tv", 30 | TC::TV_HD => "tv", 31 | TC::ANIME => "anime", 32 | TC::MUSIC => "music", 33 | TC::BOOKS => "books", 34 | TC::APPS => "apps", 35 | TC::GAMES => "games", 36 | TC::XXX => "xxx", 37 | ]); 38 | } 39 | 40 | /** 41 | * Search for torrents. 42 | * 43 | * @param string $query 44 | * @param int $category 45 | * @return array Array of torrents. Either empty or filled. 46 | */ 47 | public function search($query, $category) 48 | { 49 | # Set single-cell view for torrents. 50 | $requestOptions = [ 51 | 'headers' => [ 52 | 'User-Agent' => 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36', 53 | ] 54 | ]; 55 | 56 | try 57 | { 58 | $url = $this->makeUrl($query, $category); 59 | $response = $this->httpClient->get($url, $requestOptions); 60 | $crawler = new Crawler((string)$response->getBody()); 61 | } catch (\Exception $e) 62 | { 63 | return []; 64 | } 65 | 66 | $items = $crawler->filterXpath('//channel/item'); 67 | 68 | $torrents = []; 69 | foreach ($items as $item) 70 | { 71 | $torrent = new Torrent(); 72 | $itemCrawler = new Crawler($item); 73 | 74 | // Set details for torrent. 75 | $torrent->setSite($this->tag); 76 | $torrent->setTitle($itemCrawler->filterXpath('//title')->text()); 77 | $torrent->setSeeders((int)$itemCrawler->filterXpath('//torrent:seeds')->text()); 78 | $torrent->setLeechers((int)$itemCrawler->filterXpath('//torrent:peers')->text()); 79 | $torrent->setMagnet($itemCrawler->filterXpath('//torrent:magnetURI')->text()); 80 | $torrent->setSize($this->formatBytes((int)$itemCrawler->filterXPath('//torrent:contentLength')->text())); 81 | $torrent->setAge($itemCrawler->filterXPath('//pubDate')->text()); 82 | $torrent->setCategory($itemCrawler->filterXPath('//category')->text()); 83 | 84 | $torrents[] = $torrent; 85 | } 86 | 87 | return $torrents; 88 | } 89 | 90 | protected function formatBytes($bytes, $precision = 2) 91 | { 92 | // TODO: Move this method to either a helper or a trait 93 | $units = ['B', 'KB', 'MB', 'GB', 'TB']; 94 | 95 | $bytes = max($bytes, 0); 96 | $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); 97 | $pow = min($pow, count($units) - 1); 98 | 99 | // Uncomment one of the following alternatives 100 | // $bytes /= pow(1024, $pow); 101 | $bytes /= (1 << (10 * $pow)); 102 | 103 | return round($bytes, $precision) . ' ' . $units[$pow]; 104 | } 105 | } --------------------------------------------------------------------------------