├── phpunit.php ├── src └── Wallhaven │ ├── Exceptions │ ├── LoginException.php │ ├── ParseException.php │ ├── WallhavenException.php │ ├── DownloadException.php │ └── NotFoundException.php │ ├── Category.php │ ├── Order.php │ ├── Purity.php │ ├── Sorting.php │ ├── User.php │ ├── WallpaperList.php │ ├── Filter.php │ ├── Wallhaven.php │ └── Wallpaper.php ├── .travis.yml ├── phpunit.xml ├── composer.json ├── LICENSE ├── .gitignore ├── README.md └── tests └── WallhavenTest.php /phpunit.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./tests/ 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Wallhaven/Category.php: -------------------------------------------------------------------------------- 1 | username = $username; 23 | } 24 | 25 | /** 26 | * @return string Username. 27 | */ 28 | public function getUsername() 29 | { 30 | return $this->username; 31 | } 32 | 33 | /** 34 | * @return string Username. 35 | */ 36 | public function __toString() 37 | { 38 | return $this->username; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ivkos/wallhaven", 3 | "description": "Wallhaven API - Search, filter and download wallpapers", 4 | "keywords": [ 5 | "wallhaven", 6 | "wallpapers", 7 | "download", 8 | "downloader", 9 | "batch", 10 | "wallbase", 11 | "api", 12 | "library" 13 | ], 14 | "homepage": "https://github.com/ivkos/Wallhaven", 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Ivaylo Stoyanov", 19 | "email": "me@ivkos.com", 20 | "homepage": "https://github.com/ivkos" 21 | } 22 | ], 23 | "require": { 24 | "php": ">= 5.4.0", 25 | "paquettg/php-html-parser": "1.7.0", 26 | "guzzlehttp/guzzle": "6.*" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "Wallhaven\\": "src/Wallhaven" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 Ivaylo Stoyanov - https://github.com/ivkos 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 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Composer template 3 | composer.phar 4 | /vendor/ 5 | 6 | # Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file 7 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file 8 | # composer.lock 9 | ### JetBrains template 10 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 11 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 12 | 13 | # User-specific stuff: 14 | .idea/workspace.xml 15 | .idea/tasks.xml 16 | .idea/dictionaries 17 | .idea/vcs.xml 18 | .idea/jsLibraryMappings.xml 19 | 20 | # Sensitive or high-churn files: 21 | .idea/dataSources.ids 22 | .idea/dataSources.xml 23 | .idea/dataSources.local.xml 24 | .idea/sqlDataSources.xml 25 | .idea/dynamic.xml 26 | .idea/uiDesigner.xml 27 | 28 | # Gradle: 29 | .idea/gradle.xml 30 | .idea/libraries 31 | 32 | # Mongo Explorer plugin: 33 | .idea/mongoSettings.xml 34 | 35 | ## File-based project format: 36 | *.iws 37 | 38 | ## Plugin-specific files: 39 | 40 | # IntelliJ 41 | /out/ 42 | .idea/ 43 | 44 | # mpeltonen/sbt-idea plugin 45 | .idea_modules/ 46 | 47 | # JIRA plugin 48 | atlassian-ide-plugin.xml 49 | 50 | # Crashlytics plugin (for Android Studio and IntelliJ) 51 | com_crashlytics_export_strings.xml 52 | crashlytics.properties 53 | crashlytics-build.properties 54 | fabric.properties 55 | -------------------------------------------------------------------------------- /src/Wallhaven/WallpaperList.php: -------------------------------------------------------------------------------- 1 | wallpapers as $w) { 42 | $url = $w->getImageUrl(true); 43 | 44 | $requests[] = $client->createRequest('GET', $url, [ 45 | 'save_to' => $directory . '/' . basename($url) 46 | ]); 47 | } 48 | 49 | $results = Pool::batch($client, $requests); 50 | 51 | // Retry with PNG 52 | $retryRequests = []; 53 | foreach ($results->getFailures() as $e) { 54 | // Delete failed files 55 | unlink($directory . '/' . basename($e->getRequest()->getUrl())); 56 | 57 | $urlPng = str_replace('.jpg', '.png', $e->getRequest()->getUrl()); 58 | $statusCode = $e->getResponse()->getStatusCode(); 59 | 60 | if ($statusCode == 404) { 61 | $retryRequests[] = $client->createRequest('GET', $urlPng, [ 62 | 'save_to' => $directory . '/' . basename($urlPng) 63 | ]); 64 | } 65 | } 66 | 67 | Pool::batch($client, $retryRequests); 68 | } 69 | 70 | 71 | /** 72 | * 73 | * @param $offset 74 | * 75 | * @return bool 76 | */ 77 | public function offsetExists($offset) 78 | { 79 | return isset($this->wallpapers[$offset]); 80 | } 81 | 82 | /** 83 | * 84 | * @param $offset 85 | * 86 | * @return null|Wallpaper 87 | */ 88 | public function offsetGet($offset) 89 | { 90 | return isset($this->wallpapers[$offset]) ? $this->wallpapers[$offset] : null; 91 | } 92 | 93 | /** 94 | * 95 | * @param $offset 96 | * @param Wallpaper $value 97 | * 98 | * @throws WallhavenException 99 | */ 100 | public function offsetSet($offset, $value) 101 | { 102 | if (!$value instanceof Wallpaper) { 103 | throw new WallhavenException("Not a Wallpaper object."); 104 | } 105 | 106 | if (is_null($offset)) { 107 | $this->wallpapers[] = $value; 108 | } else { 109 | $this->wallpapers[$offset] = $value; 110 | } 111 | } 112 | 113 | /** 114 | * @param $offset 115 | */ 116 | public function offsetUnset($offset) 117 | { 118 | unset($this->wallpapers[$offset]); 119 | } 120 | 121 | /** 122 | * @return \ArrayIterator 123 | */ 124 | public function getIterator() 125 | { 126 | return new \ArrayIterator($this->wallpapers); 127 | } 128 | 129 | /** 130 | * @return int Wallpaper count. 131 | */ 132 | public function count() 133 | { 134 | return count($this->wallpapers); 135 | } 136 | 137 | /** 138 | * All all items from the given list to the current list. 139 | * 140 | * @param WallpaperList $wallpaperList 141 | */ 142 | public function addAll(WallpaperList $wallpaperList) 143 | { 144 | foreach ($wallpaperList as $wallpaper) { 145 | $this->wallpapers[] = $wallpaper; 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Wallhaven/Filter.php: -------------------------------------------------------------------------------- 1 | wallhaven = $wallhaven; 26 | } 27 | 28 | /** 29 | * @param string $keywords What to search for. Can be keywords or #tagnames, e.g. #cars 30 | * 31 | * @return self 32 | */ 33 | public function keywords($keywords) 34 | { 35 | $this->keywords = $keywords; 36 | 37 | return $this; 38 | } 39 | 40 | /** 41 | * @param int $categories Categories to include. This is a bit field, for example: 42 | * Category::GENERAL | Category::PEOPLE 43 | * 44 | * @return self 45 | */ 46 | public function categories($categories) 47 | { 48 | $this->categories = $categories; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * @param int $purity Purity of wallpapers. This is a bit field, for example: 55 | * Purity::SFW | Purity::NSFW 56 | * 57 | * @return self 58 | */ 59 | public function purity($purity) 60 | { 61 | $this->purity = $purity; 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * @param string $sorting Sorting, e.g. Sorting::RELEVANCE 68 | * 69 | * @return self 70 | */ 71 | public function sorting($sorting) 72 | { 73 | $this->sorting = $sorting; 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * @param string $order Order of results. Can be Order::ASC or Order::DESC 80 | * 81 | * @return self 82 | */ 83 | public function order($order) 84 | { 85 | $this->order = $order; 86 | 87 | return $this; 88 | } 89 | 90 | /** 91 | * @param string[] $resolutions Array of resolutions in the format of WxH, for example: 92 | * ['1920x1080', '1280x720'] 93 | * 94 | * @return self 95 | */ 96 | public function resolutions(array $resolutions) 97 | { 98 | $this->resolutions = $resolutions; 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * @param string[] $ratios Array of ratios in the format of WxH, for example: 105 | * ['16x9', '4x3'] 106 | * 107 | * @return self 108 | */ 109 | public function ratios(array $ratios) 110 | { 111 | $this->ratios = $ratios; 112 | 113 | return $this; 114 | } 115 | 116 | /** 117 | * Set number of pages of wallpapers to fetch. A single page typically consists of 24, 32 or 64 wallpapers. 118 | * 119 | * @param int $pages Number of pages. 120 | * 121 | * @throws \InvalidArgumentException Thrown if the number of pages is negative or zero. 122 | * @return self 123 | */ 124 | public function pages($pages) 125 | { 126 | if ($pages <= 0) { 127 | throw new \InvalidArgumentException("Number of pages must be positive."); 128 | } 129 | 130 | $this->pages = $pages; 131 | 132 | return $this; 133 | } 134 | 135 | /** 136 | * Execute the search with the specified filters. 137 | * 138 | * @return WallpaperList Wallpapers matching the specified filters. 139 | */ 140 | public function getWallpapers() 141 | { 142 | $wallpapers = new WallpaperList(); 143 | 144 | for ($i = 1; $i <= $this->pages; ++$i) { 145 | $wallpapers->addAll($this->wallhaven->search( 146 | $this->keywords, 147 | $this->categories, 148 | $this->purity, 149 | $this->sorting, 150 | $this->order, 151 | $this->resolutions, 152 | $this->ratios, 153 | $i 154 | )); 155 | } 156 | 157 | return $wallpapers; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Wallhaven API for PHP 2 | =================== 3 | [![](https://img.shields.io/packagist/v/ivkos/wallhaven.svg?style=flat-square)](https://packagist.org/packages/ivkos/wallhaven) 4 | [![](https://img.shields.io/packagist/dt/ivkos/wallhaven.svg?style=flat-square)](https://packagist.org/packages/ivkos/wallhaven) 5 | [![](https://img.shields.io/github/license/ivkos/Wallhaven.svg?style=flat-square)](LICENSE) 6 | 7 | ## Description 8 | A PHP library for **[Wallhaven](https://wallhaven.cc)** that allows you to search for wallpapers and get information 9 | about them in convenient OOP syntax. Additionally, this library provides the ability to download individual 10 | wallpapers, or batch download many wallpapers asynchronously which considerably reduces download times. 11 | 12 | ## Requirements 13 | * PHP 5.4 or newer 14 | * Composer 15 | 16 | ## Install 17 | Create a `composer.json` file in your project root: 18 | ```json 19 | { 20 | "require": { 21 | "ivkos/wallhaven": "2.*" 22 | } 23 | } 24 | ``` 25 | 26 | Run `php composer.phar install` to download the library and its dependencies. 27 | 28 | ## Quick Documentation 29 | Add this line to include Composer packages: 30 | ```php 31 | filter() 86 | ->keywords("#cars") 87 | ->categories(Category::GENERAL) 88 | ->purity(Purity::SFW) 89 | ->sorting(Sorting::FAVORITES) 90 | ->order(Order::DESC) 91 | ->resolutions(["1920x1080", "2560x1440"]) 92 | ->ratios(["16x9"]) 93 | ->pages(3) 94 | ->getWallpapers(); 95 | ``` 96 | 97 | ```php 98 | $wallpapers = $wh->filter() 99 | ->keywords("landscape") 100 | ->ratios(["16x9"]) 101 | ->pages(2) 102 | ->getWallpapers(); 103 | ``` 104 | Returns a `WallpaperList` object containing `Wallpaper` objects that match the criteria above. 105 | 106 | The `WallpaperList` object can be accessed like an array, iterated over using `foreach`, and has a `WallpaperList::count()` method: 107 | ```php 108 | // Get favorites count for the first wallpaper in the list 109 | $wallpapers[0]->getFavorites(); 110 | 111 | // Print resolutions of all wallpapers in the list 112 | foreach ($wallpapers as $w) { 113 | echo $w->getResolution() . PHP_EOL; 114 | } 115 | 116 | // Get the number of wallpapers in the list 117 | echo "There are " . $wallpapers->count() . " wallpapers!" . PHP_EOL; 118 | ``` 119 | 120 | ### Wallpaper Information 121 | The `Wallpaper` object has a number of methods that provide information about the wallpaper: 122 | 123 | - `getId()` 124 | - `getTags()` 125 | - `getPurity()` 126 | - `getResolution()` 127 | - `getSize()` 128 | - `getCategory()` 129 | - `getViews()` 130 | - `getFavorites()` 131 | - `getFeaturedBy()` - not accessible if not logged in 132 | - `getFeaturedDate()` - not accessible if not logged in 133 | - `getUploadedBy()` 134 | - `getUploadedDate()` 135 | - `getImageUrl()` 136 | - `getThumbnailUrl()` 137 | 138 | You can get information about a single wallpaper if you know its ID: 139 | ```php 140 | $w = $wh->wallpaper(198320); 141 | 142 | $w->getTags(); // ["cats", "closeups"] 143 | $w->getViews(); // int(3500) 144 | ``` 145 | 146 | You can also get information about wallpapers from a search result: 147 | ```php 148 | $wallpapers = $wh->filter()->keywords(...)->getWallpapers(); 149 | 150 | $wallpapers[0]->getId(); // int(103929) 151 | $wallpapers[0]->getFavorites(); // int(367) 152 | ``` 153 | 154 | ### Downloading 155 | To download a single wallpaper to a specific directory: 156 | ```php 157 | $wh->wallpaper(198320)->download("/home/user/wallpapers"); 158 | ``` 159 | 160 | To batch download wallpapers from a search result: 161 | ```php 162 | $wallpapers = $wh->filter()->keywords(...)->getWallpapers(); 163 | $wallpapers->downloadAll("/home/user/wallpapers"); 164 | ``` 165 | 166 | You can also create a `WallpaperList`, add specific wallpapers to it, and then batch download them, like so: 167 | ```php 168 | use Wallhaven\Wallhaven; 169 | use Wallhaven\WallpaperList; 170 | 171 | $wh = new Wallhaven(); 172 | $batch = new WallpaperList(); 173 | $batch[] = $wh->wallpaper(198320); 174 | $batch[] = $wh->wallpaper(103929); 175 | 176 | $batch->downloadAll("/home/user/wallpapers"); 177 | ``` 178 | 179 | 180 | #### For more information, please refer to the source code and the PHPDoc blocks. 181 | -------------------------------------------------------------------------------- /tests/WallhavenTest.php: -------------------------------------------------------------------------------- 1 | invoke($wh); 18 | $token = $getToken->invoke($wh); 19 | 20 | $this->assertNotEmpty($token); 21 | } 22 | 23 | private static function getProtectedMethod($class, $method) 24 | { 25 | $m = new ReflectionMethod($class, $method); 26 | $m->setAccessible(true); 27 | 28 | return $m; 29 | } 30 | 31 | public function testNoLogin() 32 | { 33 | new Wallhaven(); 34 | } 35 | 36 | public function testLogin() 37 | { 38 | new Wallhaven(self::getEnvUsername(), self::getEnvPassword()); 39 | } 40 | 41 | private static function getEnvUsername() 42 | { 43 | $username = getenv('WALLHAVEN_USERNAME'); 44 | 45 | if (empty($username)) { 46 | self::fail("Cannot get username from environment variable."); 47 | } 48 | 49 | return $username; 50 | } 51 | 52 | private static function getEnvPassword() 53 | { 54 | $password = getenv('WALLHAVEN_PASSWORD'); 55 | 56 | if (empty($password)) { 57 | self::fail("Cannot get password from environment variable."); 58 | } 59 | 60 | return $password; 61 | } 62 | 63 | /** 64 | * @expectedException \Wallhaven\Exceptions\LoginException 65 | * @expectedExceptionMessage Incorrect username or password. 66 | */ 67 | public function testLoginWithEmptyPassword() 68 | { 69 | new Wallhaven(self::getEnvUsername()); 70 | } 71 | 72 | /** 73 | * @expectedException \Wallhaven\Exceptions\LoginException 74 | * @expectedExceptionMessage Incorrect username or password. 75 | */ 76 | public function testLoginWithIncorrectCredentials() 77 | { 78 | new Wallhaven("this user should not exist", "wrong password"); 79 | } 80 | 81 | public function testSearch() 82 | { 83 | $wh = new Wallhaven(); 84 | 85 | $wallpapers = $wh->search("macro", 86 | Category::PEOPLE, 87 | Purity::SFW, 88 | Sorting::RELEVANCE, 89 | Order::DESC 90 | ); 91 | 92 | $this->assertNotEmpty($wallpapers); 93 | } 94 | 95 | public function testSearchLoggedIn() 96 | { 97 | $wh = new Wallhaven(self::getEnvUsername(), self::getEnvPassword()); 98 | 99 | $wallpapers = $wh->search("macro", 100 | Category::PEOPLE, 101 | Purity::SFW, 102 | Sorting::RELEVANCE, 103 | Order::DESC 104 | ); 105 | 106 | $this->assertNotEmpty($wallpapers); 107 | } 108 | 109 | public function testSearchNsfwLoggedIn() 110 | { 111 | $wh = new Wallhaven(self::getEnvUsername(), self::getEnvPassword()); 112 | 113 | $wallpapers = $wh->search("", 114 | Category::ALL, 115 | Purity::NSFW, 116 | Sorting::RANDOM 117 | ); 118 | 119 | $this->assertNotEmpty($wallpapers); 120 | } 121 | 122 | public function testSearchNsfwIsEmptyWhenNotLoggedIn() 123 | { 124 | $wh = new Wallhaven(); 125 | 126 | $wallpapers = $wh->search("", 127 | Category::ALL, 128 | Purity::NSFW, 129 | Sorting::RANDOM 130 | ); 131 | 132 | $this->assertEmpty($wallpapers); 133 | } 134 | 135 | public function testWallpaperInformationLoggedIn() 136 | { 137 | $wh = new Wallhaven(self::getEnvUsername(), self::getEnvPassword()); 138 | $w = $wh->wallpaper(198320); 139 | 140 | $this->assertNotEmpty($w->getTags()); 141 | $this->assertEquals(Purity::SFW, $w->getPurity()); 142 | $this->assertEquals("1920x1080", $w->getResolution()); 143 | $this->assertEquals("374.1 KiB", $w->getSize()); 144 | $this->assertEquals(Category::GENERAL, $w->getCategory()); 145 | $this->assertNotEmpty($w->getViews()); 146 | $this->assertNotEmpty($w->getFavorites()); 147 | 148 | $this->assertNotEmpty($w->getFeaturedBy()); 149 | $this->assertInstanceOf("DateTime", $w->getFeaturedDate()); 150 | 151 | $this->assertNotEmpty($w->getUploadedBy()); 152 | $this->assertInstanceOf("DateTime", $w->getUploadedDate()); 153 | } 154 | 155 | /** 156 | * @expectedException \Wallhaven\Exceptions\LoginException 157 | * @expectedExceptionMessage Access to wallpaper is forbidden. 158 | */ 159 | public function testWallpaperInformationNsfwLoggedOut() 160 | { 161 | (new Wallhaven())->wallpaper(8273)->getUploadedDate(); 162 | } 163 | 164 | public function testWallpaperInformationNsfwLoggedIn() 165 | { 166 | $wh = new Wallhaven(self::getEnvUsername(), self::getEnvPassword()); 167 | $wh->wallpaper(8273)->getUploadedDate(); 168 | } 169 | 170 | /** 171 | * @expectedException \Wallhaven\Exceptions\NotFoundException 172 | * @expectedExceptionMessage Wallpaper not found. 173 | */ 174 | public function testNonExistentWallpaperInformation() 175 | { 176 | (new Wallhaven())->wallpaper(300000000)->getUploadedDate(); 177 | } 178 | 179 | public function testCurrentUser() 180 | { 181 | $wh = new Wallhaven(self::getEnvUsername(), self::getEnvPassword()); 182 | 183 | $this->assertEquals(self::getEnvUsername(), $wh->user()->getUsername()); 184 | } 185 | 186 | public function testAnotherUser() 187 | { 188 | $wh = new Wallhaven(); 189 | 190 | $this->assertEquals('Gandalf', $wh->user('Gandalf')->getUsername()); 191 | } 192 | 193 | public function testImageUrlPng() 194 | { 195 | $wh = new Wallhaven(); 196 | 197 | $url = $wh->wallpaper(43118)->getImageUrl(); 198 | 199 | $this->assertEquals('https://wallpapers.wallhaven.cc/wallpapers/full/wallhaven-43118.png', $url); 200 | } 201 | 202 | public function testThumbnailUrl() 203 | { 204 | $wh = new Wallhaven(); 205 | 206 | $thumbUrl = $wh->wallpaper(198320)->getThumbnailUrl(); 207 | 208 | $this->assertEquals('https://alpha.wallhaven.cc/wallpapers/thumb/small/th-198320.jpg', $thumbUrl); 209 | } 210 | 211 | public function testFluentInterface() 212 | { 213 | $wh = new Wallhaven(); 214 | $result = $wh->filter() 215 | ->purity(Purity::SFW) 216 | ->sorting(Sorting::RANDOM) 217 | ->pages(2) 218 | ->getWallpapers(); 219 | 220 | $this->assertInstanceOf("Wallhaven\\WallpaperList", $result); 221 | $this->assertNotEmpty($result); 222 | $this->assertEquals(48, $result->count()); 223 | } 224 | 225 | public function testCachedIsFaster() 226 | { 227 | $wh = new Wallhaven(); 228 | 229 | $start1 = microtime(true); 230 | $w1 = $wh->search("cars")[0]; 231 | $w1->setCacheEnabled(false); 232 | $w1->getFavorites(); 233 | $time1 = microtime(true) - $start1; 234 | echo "Cache disabled: " . round($time1 * 1000) . " ms" . PHP_EOL; 235 | 236 | $start2 = microtime(true); 237 | $w1 = $wh->search("cars")[0]; 238 | // Cache is implicitly enabled 239 | $w1->getFavorites(); 240 | $time2 = microtime(true) - $start2; 241 | echo "Cache enabled: " . round($time2 * 1000) . " ms" . PHP_EOL; 242 | 243 | $this->assertTrue($time2 < $time1); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/Wallhaven/Wallhaven.php: -------------------------------------------------------------------------------- 1 | login($username, $password); 48 | } else { 49 | $this->initClient(); 50 | } 51 | } 52 | 53 | /** 54 | * Login to Wallhaven. 55 | * 56 | * @param string $username Username. 57 | * @param string $password Password. 58 | * 59 | * @throws LoginException 60 | */ 61 | public function login($username, $password) 62 | { 63 | if (empty($username) || empty($password)) { 64 | throw new LoginException("Incorrect username or password."); 65 | } 66 | 67 | $this->initClient(true); 68 | 69 | $login = $this->client->post(self::URL_LOGIN, [ 70 | 'form_params' => [ 71 | '_token' => $this->getToken(), 72 | 'username' => $username, 73 | 'password' => $password 74 | ], 75 | 'on_stats' => function (TransferStats $stats) use (&$url) { 76 | $url = $stats->getEffectiveUri(); 77 | } 78 | ]); 79 | 80 | if ($url == self::URL_HOME . self::URL_LOGIN) { 81 | throw new LoginException("Incorrect username or password."); 82 | } 83 | 84 | $this->username = $username; 85 | } 86 | 87 | /** 88 | * Initialize HTTP client. 89 | * 90 | * @param bool $withCookies Whether cookies should be enabled. 91 | */ 92 | private function initClient($withCookies = false) 93 | { 94 | 95 | if ($withCookies) { 96 | $jar = new CookieJar(); 97 | $this->client = new Client( 98 | [ 99 | 'base_uri' => self::URL_HOME, 100 | 'cookies' => $jar 101 | ]); 102 | } else { 103 | $this->client = new Client(['base_uri' => self::URL_HOME]); 104 | } 105 | } 106 | 107 | /** 108 | * Get token for login. 109 | * 110 | * @return string Token. 111 | * @throws WallhavenException Thrown if no token is found. 112 | */ 113 | private function getToken() 114 | { 115 | $body = $this->client->get('/')->getBody()->getContents(); 116 | 117 | $dom = new Dom(); 118 | $dom->load($body); 119 | 120 | $token = $dom->find('input[name="_token"]')[0]->value; 121 | 122 | if (empty($token)) { 123 | throw new LoginException("Cannot find login token on Wallhaven's homepage."); 124 | } 125 | 126 | return $token; 127 | } 128 | 129 | /** 130 | * User. 131 | * 132 | * @param string $username Username. If empty, returns the current user. 133 | * 134 | * @return User User. 135 | */ 136 | public function user($username = null) 137 | { 138 | return new User($username ?: $this->username); 139 | } 140 | 141 | /** 142 | * Search for wallpapers. 143 | * 144 | * @param string $query What to search for. Searching for specific tags can be done with #tagname, e.g. 145 | * #cars 146 | * @param int $categories Categories to include. This is a bit field, e.g.: Category::GENERAL | 147 | * Category::PEOPLE 148 | * @param int $purity Purity of wallpapers. This is a bit field, e.g.: Purity::SFW | 149 | * Purity::NSFW 150 | * @param string $sorting Sorting, e.g. Sorting::RELEVANCE 151 | * @param string $order Order of results. Can be Order::ASC or Order::DESC 152 | * @param string[] $resolutions Array of resolutions in the format of WxH, e.g.: ['1920x1080', 153 | * '1280x720'] 154 | * @param string[] $ratios Array of ratios in the format of WxH, e.g.: ['16x9', '4x3'] 155 | * @param int $page The id of the page to fetch. This is not a total number of pages to 156 | * fetch. 157 | * 158 | * @return WallpaperList Wallpapers. 159 | */ 160 | public function search( 161 | $query, 162 | $categories = Category::ALL, 163 | $purity = Purity::SFW, 164 | $sorting = Sorting::RELEVANCE, 165 | $order = Order::DESC, 166 | $resolutions = [], 167 | $ratios = [], 168 | $page = 1 169 | ) { 170 | $result = $this->client->get(self::URL_SEARCH, [ 171 | 'query' => [ 172 | 'q' => $query, 173 | 'categories' => self::getBinary($categories), 174 | 'purity' => self::getBinary($purity), 175 | 'sorting' => $sorting, 176 | 'order' => $order, 177 | 'resolutions' => implode(',', $resolutions), 178 | 'ratios' => implode(',', $ratios), 179 | 'page' => $page 180 | ], 181 | 'headers' => [ 182 | 'X-Requested-With' => 'XMLHttpRequest' 183 | ] 184 | ]); 185 | 186 | $body = $result->getBody()->getContents(); 187 | $dom = new Dom(); 188 | $dom->load($body); 189 | 190 | $figures = $dom->find('figure.thumb'); 191 | 192 | $wallpapers = new WallpaperList(); 193 | 194 | foreach ($figures as $figure) { 195 | $id = preg_split('#' . self::URL_HOME . self::URL_WALLPAPER . '/#', 196 | $figure->find('a.preview')->getAttribute('href'))[1]; 197 | 198 | $classText = $figure->getAttribute('class'); 199 | preg_match("/thumb thumb-(sfw|sketchy|nsfw) thumb-(general|anime|people)/", $classText, $classMatches); 200 | 201 | $purity = constant('Wallhaven\Purity::' . strtoupper($classMatches[1])); 202 | $category = constant('Wallhaven\Category::' . strtoupper($classMatches[2])); 203 | $resolution = str_replace(' ', '', trim($figure->find('span.wall-res')->text)); 204 | $favorites = (int)$figure->find('.wall-favs')->text; 205 | 206 | $w = new Wallpaper($id, $this->client); 207 | 208 | $w->setProperties([ 209 | 'purity' => $purity, 210 | 'category' => $category, 211 | 'resolution' => $resolution, 212 | 'favorites' => $favorites 213 | ]); 214 | 215 | $wallpapers[] = $w; 216 | } 217 | 218 | return $wallpapers; 219 | } 220 | 221 | /** 222 | * Convert a bit field into Wallhaven's format. 223 | * 224 | * @param int $bitField Bit field. 225 | * 226 | * @return string Converted to binary. 227 | */ 228 | private static function getBinary($bitField) 229 | { 230 | return str_pad(decbin($bitField), 3, '0', STR_PAD_LEFT); 231 | } 232 | 233 | /** 234 | * Wallpaper. 235 | * 236 | * @param int $id Wallpaper's ID. 237 | * 238 | * @return Wallpaper Wallpaper. 239 | */ 240 | public function wallpaper($id) 241 | { 242 | return new Wallpaper($id, $this->client); 243 | } 244 | 245 | /** 246 | * Returns a new Filter object to use as a fluent interface. 247 | * 248 | * @return Filter 249 | */ 250 | public function filter() 251 | { 252 | return new Filter($this); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/Wallhaven/Wallpaper.php: -------------------------------------------------------------------------------- 1 | id = $id; 63 | $this->client = $client; 64 | $this->cacheEnabled = true; 65 | } 66 | 67 | /** 68 | * @return int Wallpaper ID. 69 | */ 70 | public function getId() 71 | { 72 | return $this->id; 73 | } 74 | 75 | /** 76 | * @param array $properties Properties. 77 | */ 78 | public function setProperties(array $properties) 79 | { 80 | foreach ($properties as $key => $value) { 81 | $this->$key = $value; 82 | } 83 | } 84 | 85 | /** 86 | * Enable or disable caching of wallpaper information. It's recommended to leave this enabled (default) unless 87 | * you really need real-time information. If you disable caching, performance will be severely degraded. 88 | * 89 | * @param bool $enabled Whether caching should be enabled. 90 | */ 91 | public function setCacheEnabled($enabled) 92 | { 93 | $this->cacheEnabled = $enabled; 94 | } 95 | 96 | /** 97 | * Get wallpaper tags. 98 | * 99 | * @return string[] Tags. 100 | */ 101 | public function getTags() 102 | { 103 | if ($this->cacheEnabled && $this->tags !== null) { 104 | return $this->tags; 105 | } 106 | 107 | $dom = $this->getDom(); 108 | 109 | $this->tags = []; 110 | 111 | foreach ($dom->find('a.tagname') as $e) { 112 | $this->tags[] = $e->text; 113 | } 114 | 115 | return $this->tags; 116 | } 117 | 118 | /** 119 | * @return Dom 120 | * @throws LoginException Thrown if access to the wallpaper was denied. 121 | * @throws NotFoundException Thrown if the wallpaper was not found. 122 | */ 123 | private function getDom() 124 | { 125 | if ($this->cacheEnabled && $this->dom !== null) { 126 | return $this->dom; 127 | } 128 | 129 | try { 130 | $response = $this->client->get(Wallhaven::URL_WALLPAPER . '/' . $this->id)->getBody()->getContents(); 131 | } catch (RequestException $e) { 132 | $code = $e->getCode(); 133 | if ($code == 403) { 134 | throw new LoginException("Access to wallpaper is forbidden."); 135 | } else if ($code == 404) { 136 | throw new NotFoundException("Wallpaper not found."); 137 | } else { 138 | throw $e; 139 | } 140 | } 141 | 142 | $dom = new Dom(); 143 | $dom->load($response); 144 | 145 | $this->dom = $this->cacheEnabled ? $dom : null; 146 | 147 | return $dom; 148 | } 149 | 150 | /** 151 | * @return int Purity. 152 | */ 153 | public function getPurity() 154 | { 155 | if ($this->cacheEnabled && $this->purity !== null) { 156 | return $this->purity; 157 | } 158 | 159 | $dom = $this->getDom(); 160 | 161 | $purityClass 162 | = $dom->find('#wallpaper-purity-form')[0]->find('fieldset.framed')[0]->find('input[checked="checked"]')[0] 163 | ->nextSibling()->getAttribute('class'); 164 | 165 | $purityText = preg_split("/purity /", $purityClass)[1]; 166 | 167 | $this->purity = constant('Wallhaven\Purity::' . strtoupper($purityText)); 168 | 169 | return $this->purity; 170 | } 171 | 172 | /** 173 | * @return string Resolution. 174 | */ 175 | public function getResolution() 176 | { 177 | if (!$this->cacheEnabled || $this->resolution === null) { 178 | $resolutionElement = $this->getDom()->find('h3.showcase-resolution'); 179 | $this->resolution = str_replace(' ', '', $resolutionElement->text); 180 | } 181 | 182 | return $this->resolution; 183 | } 184 | 185 | /** 186 | * @param string $contents 187 | * 188 | * @return \PHPHtmlParser\Dom\AbstractNode 189 | * @throws ParseException 190 | */ 191 | private function getSibling($contents) 192 | { 193 | $dom = $this->getDom(); 194 | 195 | $result = $dom->find('div[data-storage-id="showcase-info"]')[0]->find('dl')[0]->find('dt'); 196 | 197 | foreach ($result as $e) { 198 | if ($e->text == $contents) { 199 | return $e->nextSibling(); 200 | } 201 | } 202 | 203 | throw new ParseException("Sibling of element with content \"" . $contents . "\" not found."); 204 | } 205 | 206 | /** 207 | * @return string Size of the image. 208 | */ 209 | public function getSize() 210 | { 211 | if (!$this->cacheEnabled || $this->size === null) { 212 | $this->size = $this->getSibling("Size")->text; 213 | } 214 | 215 | return $this->size; 216 | } 217 | 218 | /** 219 | * @return int Category. 220 | */ 221 | public function getCategory() 222 | { 223 | if (!$this->cacheEnabled || $this->category === null) { 224 | $this->category = constant('Wallhaven\Category::' . strtoupper($this->getSibling("Category")->text)); 225 | } 226 | 227 | return $this->category; 228 | } 229 | 230 | /** 231 | * @return int Number of views. 232 | */ 233 | public function getViews() 234 | { 235 | if (!$this->cacheEnabled || $this->views === null) { 236 | $this->views = (int)str_replace(',', '', $this->getSibling("Views")->text); 237 | } 238 | 239 | return $this->views; 240 | } 241 | 242 | /** 243 | * @return int Number of favorites. 244 | */ 245 | public function getFavorites() 246 | { 247 | if (!$this->cacheEnabled || $this->favorites === null) { 248 | $favsLink = $this->getSibling("Favorites")->find('a'); 249 | 250 | if (!$favsLink[0]) { 251 | $this->favorites = 0; 252 | } else { 253 | $this->favorites = (int)$favsLink[0]->text; 254 | } 255 | } 256 | 257 | return $this->favorites; 258 | } 259 | 260 | /** 261 | * @return User User that featured the wallpaper. 262 | * @throws ParseException 263 | */ 264 | public function getFeaturedBy() 265 | { 266 | if (!$this->cacheEnabled || $this->featuredBy === null) { 267 | $usernameElement = $this->getDom() 268 | ->find("footer.sidebar-section") 269 | ->find(".username"); 270 | 271 | if ($usernameElement != null) { 272 | $this->featuredBy = new User($usernameElement->text); 273 | } 274 | } 275 | 276 | return $this->featuredBy; 277 | } 278 | 279 | /** 280 | * @return DateTime Date and time when the wallpaper was featured. 281 | * @throws ParseException 282 | */ 283 | public function getFeaturedDate() 284 | { 285 | if (!$this->cacheEnabled || $this->featuredDate === null) { 286 | $featuredDateElement = $this->getDom() 287 | ->find("footer.sidebar-section") 288 | ->find("time"); 289 | 290 | if ($featuredDateElement != null) { 291 | $this->featuredDate = new DateTime($featuredDateElement->getAttribute('datetime')); 292 | } 293 | } 294 | 295 | return $this->featuredDate; 296 | } 297 | 298 | /** 299 | * @return User User that uploaded the wallpaper. 300 | */ 301 | public function getUploadedBy() 302 | { 303 | if (!$this->cacheEnabled || $this->uploadedBy === null) { 304 | $username = $this->getDom() 305 | ->find(".showcase-uploader") 306 | ->find("a.username") 307 | ->text; 308 | 309 | $this->uploadedBy = new User($username); 310 | } 311 | 312 | return $this->uploadedBy; 313 | } 314 | 315 | /** 316 | * @return DateTime Date and time when the wallpaper was uploaded. 317 | */ 318 | public function getUploadedDate() 319 | { 320 | if (!$this->cacheEnabled || $this->uploadedDate === null) { 321 | $timeElement = $this->getDom()->find(".showcase-uploader > time:nth-child(4)")[0]; 322 | 323 | $this->uploadedDate = new DateTime($timeElement->getAttribute('datetime')); 324 | } 325 | 326 | return $this->uploadedDate; 327 | } 328 | 329 | public function __toString() 330 | { 331 | return "Wallpaper " . $this->id; 332 | } 333 | 334 | /** 335 | * @return string Thumbnail URL. 336 | */ 337 | public function getThumbnailUrl() 338 | { 339 | return Wallhaven::URL_HOME . Wallhaven::URL_THUMB_PREFIX . $this->id . '.jpg'; 340 | } 341 | 342 | /** 343 | * Download the wallpaper. 344 | * 345 | * @param string $directory Where to download the wallpaper. 346 | * 347 | * @throws DownloadException Thrown if the download directory cannot be created. 348 | */ 349 | public function download($directory) 350 | { 351 | if (!file_exists($directory)) { 352 | if (!@mkdir($directory, null, true)) { 353 | throw new DownloadException("The download directory cannot be created."); 354 | } 355 | } 356 | 357 | $url = $this->getImageUrl(); 358 | 359 | $this->client->get($url, [ 360 | 'save_to' => $directory . '/' . basename($url), 361 | ]); 362 | } 363 | 364 | /** 365 | * @param bool $assumeJpg Assume the wallpaper is JPG. May speed up the method at the cost of potentially wrong URL. 366 | * 367 | * @return string URL. 368 | * @throws LoginException 369 | * @throws NotFoundException 370 | */ 371 | public function getImageUrl($assumeJpg = false) 372 | { 373 | if ($assumeJpg) { 374 | return Wallhaven::URL_IMG_PREFIX . $this->id . '.jpg'; 375 | } 376 | 377 | if (!$this->cacheEnabled || $this->imageUrl === null) { 378 | $dom = $this->getDom(); 379 | $url = $dom->find('img#wallpaper')[0]->getAttribute('src'); 380 | $this->imageUrl = (parse_url($url, PHP_URL_SCHEME) ?: "https:") . $url; 381 | } 382 | 383 | return $this->imageUrl; 384 | } 385 | } 386 | --------------------------------------------------------------------------------