├── 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://packagist.org/packages/ivkos/wallhaven)
4 | [](https://packagist.org/packages/ivkos/wallhaven)
5 | [](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 |
--------------------------------------------------------------------------------