Browse Source

Really ugly progress commit

Timothy J. Warren 1 year ago
parent
commit
270f9ab167

+ 0
- 1
app/bootstrap.php View File

@@ -14,7 +14,6 @@
14 14
  * @link        https://github.com/timw4mail/HummingBirdAnimeClient
15 15
  */
16 16
 
17
-
18 17
 namespace Aviat\AnimeClient;
19 18
 
20 19
 use Aura\Html\HelperLocatorFactory;

+ 6
- 0
app/config/config.toml.example View File

@@ -14,5 +14,11 @@ show_anime_collection = true
14 14
 # do you wish to show the manga collection?
15 15
 show_manga_collection = false
16 16
 
17
+# do you have a My Anime List account set up in mal.toml?
18
+use_mal_api = false
19
+
20
+# cache driver for api calls (NullDriver, SQLDriver, RedisDriver)
21
+cache_driver = "NullDriver"
22
+
17 23
 # path to public directory on the server
18 24
 asset_dir = "/../../public"

+ 2
- 2
app/views/anime/cover.php View File

@@ -11,11 +11,11 @@
11 11
 			<section class="media-wrap">
12 12
 				<?php foreach($items as $item): ?>
13 13
 				<?php if ($item['private'] && ! $auth->is_authenticated()) continue; ?>
14
-				<article class="media" id="<?= $item['id'] ?>">
14
+				<article class="media" id="<?= $item['id'] ?>" data-kitsu-id="<?= $item['id'] ?>" data-mal-id="<?= $item['mal_id'] ?>">
15 15
 					<?php if ($auth->is_authenticated()): ?>
16 16
 					<button title="Increment episode count" class="plus_one" hidden>+1 Episode</button>
17 17
 					<?php endif ?>
18
-					<?= $helper->img($item['anime']['image']); ?>
18
+					<img src="<?= $item['anime']['image'] ?>" alt="" />
19 19
 					<div class="name">
20 20
 						<a href="<?= $url->generate('anime.details', ['id' => $item['anime']['slug']]); ?>">
21 21
 							<?= array_shift($item['anime']['titles']) ?>

+ 4
- 1
app/views/anime/details.php View File

@@ -15,16 +15,18 @@
15 15
 				</tr>
16 16
 				<tr>
17 17
 					<td>Episode Count</td>
18
-					<td><?= $data['episode_count'] ?></td>
18
+					<td><?= $data['episode_count'] ?? '-' ?></td>
19 19
 				</tr>
20 20
 				<tr>
21 21
 					<td>Episode Length</td>
22 22
 					<td><?= $data['episode_length'] ?> minutes</td>
23 23
 				</tr>
24
+				<?php if ( ! empty($data['age_rating'])): ?>
24 25
 				<tr>
25 26
 					<td>Age Rating</td>
26 27
                     <td><abbr title="<?= $data['age_rating_guide'] ?>"><?= $data['age_rating'] ?></abbr></td>
27 28
 				</tr>
29
+				<?php endif ?>
28 30
 				<tr>
29 31
 					<td>Genres</td>
30 32
 					<td>
@@ -40,6 +42,7 @@
40 42
             <?php endforeach ?>
41 43
 			<br />
42 44
 			<p><?= nl2br($data['synopsis']) ?></p>
45
+			<?php /*<pre><?= json_encode($data['included'], \JSON_PRETTY_PRINT) ?></pre> */ ?>
43 46
 		</div>
44 47
 	</section>
45 48
 </main>

+ 1
- 0
app/views/anime/edit.php View File

@@ -77,6 +77,7 @@
77 77
 						<td>&nbsp;</td>
78 78
 						<td>
79 79
 							<input type="hidden" value="<?= $item['id'] ?>" name="id" />
80
+							<input type="hidden" value="<?= $item['mal_id'] ?>" name="mal_id" />
80 81
 							<input type="hidden" value="true" name="edit" />
81 82
 							<button type="submit">Submit</button>
82 83
 						</td>

+ 0
- 1
build/phpunit.xml View File

@@ -4,7 +4,6 @@
4 4
 	stopOnFailure="false"
5 5
 	bootstrap="../tests/bootstrap.php"
6 6
 	beStrictAboutTestsThatDoNotTestAnything="true"
7
-	checkForUnintentionallyCoveredCode="true"
8 7
 	>
9 8
 	<filter>
10 9
 		<whitelist>

+ 4
- 4
composer.json View File

@@ -14,7 +14,6 @@
14 14
 		}
15 15
 	},
16 16
 	"require": {
17
-		"abeautifulsite/simpleimage": "2.5.*",
18 17
 		"aura/html": "2.*",
19 18
 		"aura/router": "3.*",
20 19
 		"aura/session": "2.*",
@@ -35,15 +34,16 @@
35 34
 		"theseer/phpdox": "0.8.1.1",
36 35
 		"phploc/phploc": "^3.0",
37 36
 		"phpmd/phpmd": "^2.4",
38
-		"phpunit/phpunit": "^5.4",
37
+		"phpunit/phpunit": "^5.7",
39 38
 		"robmorgan/phinx": "^0.6.4",
40 39
 		"humbug/humbug": "~1.0@dev",
41
-		"consolidation/robo": "~1.0@RC",
40
+		"consolidation/robo": "~1.0",
42 41
 		"henrikbjorn/lurker": "^1.1.0",
43 42
 		"symfony/var-dumper": "^3.1",
44 43
 		"squizlabs/php_codesniffer": "^3.0.0@beta"
45 44
 	},
46 45
 	"scripts": {
47
-		"build:css": "cd public && npm run build && cd .."
46
+		"build:css": "cd public && npm run build && cd ..",
47
+		"watch:css": "cd public && npm run watch"
48 48
 	}
49 49
 }

+ 20
- 21
phpunit.xml View File

@@ -1,25 +1,24 @@
1 1
 <?xml version="1.0" encoding="UTF-8"?>
2 2
 <phpunit
3
-        colors="true"
4
-        stopOnFailure="false"
5
-        bootstrap="tests/bootstrap.php"
6
-        beStrictAboutTestsThatDoNotTestAnything="true"
3
+		colors="true"
4
+		stopOnFailure="false"
5
+		bootstrap="tests/bootstrap.php"
7 6
 >
8
-    <filter>
9
-        <whitelist>
10
-            <directory suffix=".php">src</directory>
11
-        </whitelist>
12
-    </filter>
13
-    <testsuites>
14
-        <testsuite name="AnimeClient">
15
-            <directory>tests</directory>
16
-        </testsuite>
17
-    </testsuites>
18
-    <php>
19
-        <server name="HTTP_USER_AGENT" value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0" />
20
-        <server name="HTTP_HOST" value="localhost" />
21
-        <server name="SERVER_NAME" value="localhost" />
22
-        <server name="REQUEST_URI" value="/" />
23
-        <server name="REQUEST_METHOD" value="GET" />
24
-    </php>
7
+	<filter>
8
+		<whitelist>
9
+			<directory suffix=".php">src</directory>
10
+		</whitelist>
11
+	</filter>
12
+	<testsuites>
13
+		<testsuite name="AnimeClient">
14
+			<directory>tests</directory>
15
+		</testsuite>
16
+	</testsuites>
17
+	<php>
18
+		<server name="HTTP_USER_AGENT" value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0" />
19
+		<server name="HTTP_HOST" value="localhost" />
20
+		<server name="SERVER_NAME" value="localhost" />
21
+		<server name="REQUEST_URI" value="/" />
22
+		<server name="REQUEST_METHOD" value="GET" />
23
+	</php>
25 24
 </phpunit>

+ 112
- 2
src/API/JsonAPI.php View File

@@ -16,6 +16,8 @@
16 16
 
17 17
 namespace Aviat\AnimeClient\API;
18 18
 
19
+use Aviat\Ion\Json;
20
+
19 21
 /**
20 22
  * Class encapsulating Json API data structure for a request or response
21 23
  */
@@ -52,7 +54,7 @@ class JsonAPI {
52 54
 	 *
53 55
 	 * @var array
54 56
 	 */
55
-	protected $included = [];
57
+	public $included = [];
56 58
 
57 59
 	/**
58 60
 	 * Pagination links
@@ -70,6 +72,11 @@ class JsonAPI {
70 72
 	{
71 73
 		$this->data = $initial;
72 74
 	}
75
+	
76
+	public function parseFromString(string $json)
77
+	{
78
+		$this->parse(Json::decode($json));
79
+	}
73 80
 
74 81
 	/**
75 82
 	 * Parse a JsonAPI response into its components
@@ -78,7 +85,7 @@ class JsonAPI {
78 85
 	 */
79 86
 	public function parse(array $data)
80 87
 	{
81
-
88
+		$this->included = static::organizeIncludes($data['included']);
82 89
 	}
83 90
 
84 91
 	/**
@@ -91,4 +98,107 @@ class JsonAPI {
91 98
 	{
92 99
 
93 100
 	}
101
+	
102
+	/**
103
+	 * Take inlined included data and inline it into the main object's relationships
104
+	 *
105
+	 * @param array $mainObject
106
+	 * @param array $included
107
+	 * @return array
108
+	 */
109
+	public static function inlineIncludedIntoMainObject(array $mainObject, array $included): array
110
+	{
111
+		$output = clone $mainObject;
112
+	}
113
+	
114
+	/**
115
+	 * Take organized includes and inline them, where applicable
116
+	 *
117
+	 * @param array $included
118
+	 * @param string $key The key of the include to inline the other included values into
119
+	 * @return array
120
+	 */
121
+	public static function inlineIncludedRelationships(array $included, string $key): array
122
+	{
123
+		$inlined = [
124
+			$key => []
125
+		];
126
+		
127
+		foreach ($included[$key] as $itemId => $item)
128
+		{
129
+			// Duplicate the item for the output
130
+			$inlined[$key][$itemId] = $item;
131
+			
132
+			foreach($item['relationships'] as $type => $ids)
133
+			{
134
+				$inlined[$key][$itemId]['relationships'][$type] = [];
135
+				foreach($ids as $id)
136
+				{
137
+					$inlined[$key][$itemId]['relationships'][$type][$id] = $included[$type][$id];
138
+				}
139
+			}
140
+		}
141
+		
142
+		return $inlined;
143
+	}
144
+
145
+	/**
146
+	 * Reorganizes 'included' data to be keyed by
147
+	 * 	type => [
148
+	 * 		id => data/attributes,
149
+	 * 	]
150
+	 *
151
+	 * @param array $includes
152
+	 * @return array
153
+	 */
154
+	public static function organizeIncludes(array $includes): array
155
+	{
156
+		$organized = [];
157
+
158
+		foreach ($includes as $item)
159
+		{
160
+			$type = $item['type'];
161
+			$id = $item['id'];
162
+			$organized[$type] = $organized[$type] ?? [];
163
+			$organized[$type][$id] = $item['attributes'];
164
+
165
+			if (array_key_exists('relationships', $item))
166
+			{
167
+				$organized[$type][$id]['relationships'] = static::organizeRelationships($item['relationships']);
168
+			}
169
+		}
170
+
171
+		return $organized;
172
+	}
173
+
174
+	/**
175
+	 * Reorganize relationship mappings to make them simpler to use
176
+	 *
177
+	 * Remove verbose structure, and just map:
178
+	 * 	type => [ idArray ]
179
+	 *
180
+	 * @param array $relationships
181
+	 * @return array
182
+	 */
183
+	public static function organizeRelationships(array $relationships): array
184
+	{
185
+		$organized = [];
186
+
187
+		foreach($relationships as $key => $data)
188
+		{
189
+			if ( ! array_key_exists('data', $data))
190
+			{
191
+				continue;
192
+			}
193
+
194
+			$organized[$key] = $organized[$key] ?? [];
195
+
196
+			foreach ($data['data'] as $item)
197
+			{
198
+				$organized[$key][] = $item['id'];
199
+			}
200
+		}
201
+
202
+		return $organized;
203
+	}
94 204
 }

+ 11
- 1
src/API/Kitsu/Auth.php View File

@@ -18,6 +18,7 @@ namespace Aviat\AnimeClient\API\Kitsu;
18 18
 
19 19
 use Aviat\AnimeClient\AnimeClient;
20 20
 use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
21
+use Exception;
21 22
 
22 23
 /**
23 24
  * Kitsu API Authentication
@@ -64,7 +65,16 @@ class Auth {
64 65
 	{
65 66
 		$config = $this->container->get('config');
66 67
 		$username = $config->get(['kitsu_username']);
67
-		$auth_token = $this->model->authenticate($username, $password);
68
+		
69
+		try
70
+		{
71
+			$auth_token = $this->model->authenticate($username, $password);
72
+		}
73
+		catch (Exception $e)
74
+		{
75
+			return FALSE;
76
+		}
77
+		
68 78
 
69 79
 		if (FALSE !== $auth_token)
70 80
 		{

+ 6
- 16
src/API/Kitsu/KitsuModel.php View File

@@ -16,6 +16,7 @@
16 16
 
17 17
 namespace Aviat\AnimeClient\API\Kitsu;
18 18
 
19
+use Aviat\AnimeClient\API\JsonAPI;
19 20
 use Aviat\AnimeClient\API\Kitsu as K;
20 21
 use Aviat\AnimeClient\API\Kitsu\Transformer\{
21 22
 	AnimeTransformer, AnimeListTransformer, MangaTransformer, MangaListTransformer
@@ -155,30 +156,23 @@ class KitsuModel {
155 156
 					'media_type' => 'Anime',
156 157
 					'status' => $status,
157 158
 				],
158
-				'include' => 'media,media.genres',
159
+				'include' => 'media,media.genres,media.mappings',
159 160
 				'page' => [
160 161
 					'offset' => 0,
161
-					'limit' => 1000
162
+					'limit' => 500
162 163
 				],
163 164
 				'sort' => '-updated_at'
164 165
 			]
165 166
 		];
166 167
 
167 168
 		$data = $this->getRequest('library-entries', $options);
168
-		$included = K::organizeIncludes($data['included']);
169
+		$included = JsonAPI::organizeIncludes($data['included']);
170
+		$included = JsonAPI::inlineIncludedRelationships($included, 'anime');
169 171
 
170 172
 		foreach($data['data'] as $i => &$item)
171 173
 		{
172
-			$item['anime'] = $included['anime'][$item['relationships']['media']['data']['id']];
173
-
174
-			$animeGenres = $item['anime']['relationships']['genres'];
175
-
176
-			foreach($animeGenres as $id)
177
-			{
178
-				$item['genres'][] = $included['genres'][$id]['name'];
179
-			}
174
+			$item['included'] =& $included;
180 175
 		}
181
-
182 176
 		$transformed = $this->animeListTransformer->transformCollection($data['data']);
183 177
 
184 178
 		return $transformed;
@@ -309,11 +303,7 @@ class KitsuModel {
309 303
 		];
310 304
 
311 305
 		$data = $this->getRequest($type, $options);
312
-
313 306
 		$baseData = $data['data'][0]['attributes'];
314
-		$rawGenres = array_pluck($data['included'], 'attributes');
315
-		$genres = array_pluck($rawGenres, 'name');
316
-		$baseData['genres'] = $genres;
317 307
 		$baseData['included'] = $data['included'];
318 308
 		return $baseData;
319 309
 	}

+ 23
- 18
src/API/Kitsu/Transformer/AnimeListTransformer.php View File

@@ -33,19 +33,35 @@ class AnimeListTransformer extends AbstractTransformer {
33 33
 	 */
34 34
 	public function transform($item)
35 35
 	{
36
-/* ?><pre><?= print_r($item, TRUE) ?></pre><?php
37
-// die(); */
38
-		$anime = $item['anime']['attributes'] ?? $item['anime'];
39
-		$genres = $item['genres'] ?? [];
36
+		$included = $item['included'] ?? [];
37
+		$animeId = $item['relationships']['media']['data']['id'];
38
+		$anime = $included['anime'][$animeId] ?? $item['anime'];
39
+		$genres = array_column($anime['relationships']['genres'], 'name') ?? [];
40 40
 
41 41
 		$rating = (int) 2 * $item['attributes']['rating'];
42 42
 
43 43
 		$total_episodes = array_key_exists('episodeCount', $anime) && (int) $anime['episodeCount'] !== 0
44 44
 			? (int) $anime['episodeCount']
45 45
 			: '-';
46
+		
47
+		$progress = (int) $item['attributes']['progress'] ?? '-';
48
+		
49
+		$MALid = NULL;
50
+		
51
+		if (array_key_exists('mappings', $included))
52
+		{
53
+			foreach ($included['mappings'] as $mapping)
54
+			{
55
+				if ($mapping['externalSite'] === 'myanimelist/anime')
56
+				{
57
+					$MALid = $mapping['externalId'];
58
+				}
59
+			}
60
+		}
46 61
 
47 62
 		return [
48 63
 			'id' => $item['id'],
64
+			'mal_id' => $MALid,
49 65
 			'episodes' => [
50 66
 				'watched' => $item['attributes']['progress'],
51 67
 				'total' => $total_episodes,
@@ -60,7 +76,6 @@ class AnimeListTransformer extends AbstractTransformer {
60 76
 				'age_rating' => $anime['ageRating'],
61 77
 				'titles' => Kitsu::filterTitles($anime),
62 78
 				'slug' => $anime['slug'],
63
-				'url' => $anime['url'] ?? '',
64 79
 				'type' => $this->string($anime['showType'])->upperCaseFirst()->__toString(),
65 80
 				'image' => $anime['posterImage']['small'],
66 81
 				'genres' => $genres,
@@ -69,7 +84,7 @@ class AnimeListTransformer extends AbstractTransformer {
69 84
 			'notes' => $item['attributes']['notes'],
70 85
 			'rewatching' => (bool) $item['attributes']['reconsuming'],
71 86
 			'rewatched' => (int) $item['attributes']['reconsumeCount'],
72
-			'user_rating' => ($rating === 0) ? '-' : $rating,
87
+			'user_rating' => ($rating === 0) ? '-' : (int) $rating,
73 88
 			'private' => (bool) $item['attributes']['private'] ?? false,
74 89
 		];
75 90
 	}
@@ -83,18 +98,8 @@ class AnimeListTransformer extends AbstractTransformer {
83 98
 	 */
84 99
 	public function untransform($item)
85 100
 	{
86
-		// Messy mapping of boolean values to their API string equivalents
87
-		$privacy = 'false';
88
-		if (array_key_exists('private', $item) && $item['private'])
89
-		{
90
-			$privacy = 'true';
91
-		}
92
-
93
-		$rewatching = 'false';
94
-		if (array_key_exists('rewatching', $item) && $item['rewatching'])
95
-		{
96
-			$rewatching = 'true';
97
-		}
101
+		$privacy = (array_key_exists('private', $item) && $item['private']);
102
+		$rewatching = (array_key_exists('rewatching', $item) && $item['rewatching']);
98 103
 
99 104
 		$untransformed = [
100 105
 			'id' => $item['id'],

+ 4
- 2
src/API/Kitsu/Transformer/AnimeTransformer.php View File

@@ -16,7 +16,7 @@
16 16
 
17 17
 namespace Aviat\AnimeClient\API\Kitsu\Transformer;
18 18
 
19
-use Aviat\AnimeClient\API\Kitsu;
19
+use Aviat\AnimeClient\API\{JsonAPI, Kitsu};
20 20
 use Aviat\Ion\Transformer\AbstractTransformer;
21 21
 
22 22
 /**
@@ -33,7 +33,8 @@ class AnimeTransformer extends AbstractTransformer {
33 33
 	 */
34 34
 	public function transform($item)
35 35
 	{
36
-		$item['genres'] = $item['genres'] ?? [];
36
+		$item['included'] = JsonAPI::organizeIncludes($item['included']);
37
+		$item['genres'] = array_column($item['included']['genres'], 'name') ?? [];
37 38
 		sort($item['genres']);
38 39
 
39 40
 		return [
@@ -48,6 +49,7 @@ class AnimeTransformer extends AbstractTransformer {
48 49
 			'age_rating_guide' => $item['ageRatingGuide'],
49 50
 			'url' => "https://kitsu.io/anime/{$item['slug']}",
50 51
 			'genres' => $item['genres'],
52
+			'included' => $item['included']
51 53
 		];
52 54
 	}
53 55
 }

+ 1
- 1
src/API/MAL.php View File

@@ -16,7 +16,7 @@
16 16
 
17 17
 namespace Aviat\AnimeClient\API;
18 18
 
19
-use Aviat\AnimeClient\Enum\{AnimeWatchingStatus, MangaReadingStatus};
19
+use Aviat\AnimeClient\API\MAL\Enum\{AnimeWatchingStatus, MangaReadingStatus};
20 20
 
21 21
 /**
22 22
  * Constants and mappings for the My Anime List API

+ 52
- 0
src/API/MAL/ListItem.php View File

@@ -0,0 +1,52 @@
1
+<?php declare(strict_types=1);
2
+/**
3
+ * Anime List Client
4
+ *
5
+ * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
6
+ *
7
+ * PHP version 7
8
+ *
9
+ * @package     AnimeListClient
10
+ * @author      Timothy J. Warren <tim@timshomepage.net>
11
+ * @copyright   2015 - 2017  Timothy J. Warren
12
+ * @license     http://www.opensource.org/licenses/mit-license.html  MIT License
13
+ * @version     4.0
14
+ * @link        https://github.com/timw4mail/HummingBirdAnimeClient
15
+ */
16
+
17
+namespace Aviat\AnimeClient\API\MAL;
18
+
19
+use Aviat\AnimeClient\API\AbstractListItem;
20
+use Aviat\Ion\Di\ContainerAware;
21
+
22
+/**
23
+ * CRUD operations for MAL list items
24
+ */
25
+class ListItem extends AbstractListItem {
26
+	use ContainerAware;
27
+
28
+	public function __construct()
29
+	{
30
+		$this->init();
31
+	}
32
+
33
+	public function create(array $data): bool
34
+	{
35
+		return FALSE;
36
+	}
37
+
38
+	public function delete(string $id): bool
39
+	{
40
+		return FALSE;
41
+	}
42
+
43
+	public function get(string $id): array
44
+	{
45
+		return [];
46
+	}
47
+
48
+	public function update(string $id, array $data): Response
49
+	{
50
+		// @TODO implement
51
+	}
52
+}

+ 190
- 0
src/API/MAL/MALTrait.php View File

@@ -0,0 +1,190 @@
1
+<?php declare(strict_types=1);
2
+/**
3
+ * Anime List Client
4
+ *
5
+ * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
6
+ *
7
+ * PHP version 7
8
+ *
9
+ * @package     AnimeListClient
10
+ * @author      Timothy J. Warren <tim@timshomepage.net>
11
+ * @copyright   2015 - 2017  Timothy J. Warren
12
+ * @license     http://www.opensource.org/licenses/mit-license.html  MIT License
13
+ * @version     4.0
14
+ * @link        https://github.com/timw4mail/HummingBirdAnimeClient
15
+ */
16
+
17
+namespace Aviat\AnimeClient\API\MAL;
18
+
19
+use Aviat\AnimeClient\API\{
20
+	GuzzleTrait,
21
+	MAL as M,
22
+	XML
23
+};
24
+use GuzzleHttp\Client;
25
+use GuzzleHttp\Cookie\CookieJar;
26
+use GuzzleHttp\Psr7\Response;
27
+use InvalidArgumentException;
28
+
29
+trait MALTrait {
30
+	use GuzzleTrait;
31
+
32
+	/**
33
+	 * The base url for api requests
34
+	 * @var string $base_url
35
+	 */
36
+	protected $baseUrl = M::BASE_URL;
37
+
38
+	/**
39
+	 * HTTP headers to send with every request
40
+	 *
41
+	 * @var array
42
+	 */
43
+	protected $defaultHeaders = [
44
+		'User-Agent' => "Tim's Anime Client/4.0"
45
+	];
46
+
47
+	/**
48
+	 * Set up the class properties
49
+	 *
50
+	 * @return void
51
+	 */
52
+	protected function init()
53
+	{
54
+		$defaults = [
55
+			'cookies' => $this->cookieJar,
56
+			'headers' => $this->defaultHeaders,
57
+			'timeout' => 25,
58
+			'connect_timeout' => 25
59
+		];
60
+
61
+		$this->cookieJar = new CookieJar();
62
+		$this->client = new Client([
63
+			'base_uri' => $this->baseUrl,
64
+			'cookies' => TRUE,
65
+			'http_errors' => TRUE,
66
+			'defaults' => $defaults
67
+		]);
68
+	}
69
+
70
+	/**
71
+	 * Make a request via Guzzle
72
+	 *
73
+	 * @param string $type
74
+	 * @param string $url
75
+	 * @param array $options
76
+	 * @return Response
77
+	 */
78
+	private function getResponse(string $type, string $url, array $options = [])
79
+	{
80
+		$type = strtoupper($type);
81
+		$validTypes = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
82
+
83
+		if ( ! in_array($type, $validTypes))
84
+		{
85
+			throw new InvalidArgumentException('Invalid http request type');
86
+		}
87
+		
88
+		$config = $this->container->get('config');
89
+		$logger = $this->container->getLogger('request');
90
+
91
+		$defaultOptions = [
92
+			'auth' => [
93
+				$config->get(['mal','username']), 
94
+				$config->get(['mal','password'])
95
+			],
96
+			'headers' => $this->defaultHeaders
97
+		];
98
+
99
+		$options = array_merge($defaultOptions, $options);
100
+
101
+		$logger->debug(Json::encode([$type, $url]));
102
+		$logger->debug(Json::encode($options));
103
+
104
+		return $this->client->request($type, $url, $options);
105
+	}
106
+
107
+	/**
108
+	 * Make a request via Guzzle
109
+	 *
110
+	 * @param string $type
111
+	 * @param string $url
112
+	 * @param array $options
113
+	 * @return array
114
+	 */
115
+	private function request(string $type, string $url, array $options = []): array
116
+	{
117
+		$logger = null;
118
+		if ($this->getContainer())
119
+		{
120
+			$logger = $this->container->getLogger('request');
121
+		}
122
+
123
+		$response = $this->getResponse($type, $url, $options);
124
+
125
+		if ((int) $response->getStatusCode() > 299 || (int) $response->getStatusCode() < 200)
126
+		{
127
+			if ($logger)
128
+			{
129
+				$logger->warning('Non 200 response for api call');
130
+				$logger->warning($response->getBody());
131
+			}
132
+
133
+			// throw new RuntimeException($response->getBody());
134
+		}
135
+
136
+		return XML::toArray((string) $response->getBody());
137
+	}
138
+
139
+	/**
140
+	 * Remove some boilerplate for get requests
141
+	 *
142
+	 * @param array $args
143
+	 * @return array
144
+	 */
145
+	protected function getRequest(...$args): array
146
+	{
147
+		return $this->request('GET', ...$args);
148
+	}
149
+
150
+	/**
151
+	 * Remove some boilerplate for post requests
152
+	 *
153
+	 * @param array $args
154
+	 * @return array
155
+	 */
156
+	protected function postRequest(...$args): array
157
+	{
158
+		$logger = null;
159
+		if ($this->getContainer())
160
+		{
161
+			$logger = $this->container->getLogger('request');
162
+		}
163
+
164
+		$response = $this->getResponse('POST', ...$args);
165
+		$validResponseCodes = [200, 201];
166
+
167
+		if ( ! in_array((int) $response->getStatusCode(), $validResponseCodes))
168
+		{
169
+			if ($logger)
170
+			{
171
+				$logger->warning('Non 201 response for POST api call');
172
+				$logger->warning($response->getBody());
173
+			}
174
+		}
175
+
176
+		return XML::toArray((string) $response->getBody());
177
+	}
178
+
179
+	/**
180
+	 * Remove some boilerplate for delete requests
181
+	 *
182
+	 * @param array $args
183
+	 * @return bool
184
+	 */
185
+	protected function deleteRequest(...$args): bool
186
+	{
187
+		$response = $this->getResponse('DELETE', ...$args);
188
+		return ((int) $response->getStatusCode() === 204);
189
+	}
190
+}

+ 44
- 58
src/API/MAL/Model.php View File

@@ -1,63 +1,49 @@
1
-<?php declare(strict_types=1);
2
-/**
3
- * Anime List Client
4
- *
5
- * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
6
- *
7
- * PHP version 7
8
- *
9
- * @package     AnimeListClient
10
- * @author      Timothy J. Warren <tim@timshomepage.net>
11
- * @copyright   2015 - 2017  Timothy J. Warren
12
- * @license     http://www.opensource.org/licenses/mit-license.html  MIT License
13
- * @version     4.0
14
- * @link        https://github.com/timw4mail/HummingBirdAnimeClient
15
- */
16
-
17
-namespace Aviat\AnimeClient\API\Kitsu;
18
-
19
-use Aviat\AnimeClient\Model\API;
1
+<?php declare(strict_types=1);
2
+/**
3
+ * Anime List Client
4
+ *
5
+ * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
6
+ *
7
+ * PHP version 7
8
+ *
9
+ * @package     AnimeListClient
10
+ * @author      Timothy J. Warren <tim@timshomepage.net>
11
+ * @copyright   2015 - 2017  Timothy J. Warren
12
+ * @license     http://www.opensource.org/licenses/mit-license.html  MIT License
13
+ * @version     4.0
14
+ * @link        https://github.com/timw4mail/HummingBirdAnimeClient
15
+ */
16
+
17
+namespace Aviat\AnimeClient\API\MAL;
18
+
19
+use Aviat\AnimeClient\API\MAL as M;
20
+use Aviat\Ion\Di\ContainerAware;
20 21
 
21 22
 /**
22 23
  * MyAnimeList API Model
23 24
  */
24
-class Model extends API {
25
-
26
-    /**
27
-     * Base url for Kitsu API
28
-     */
29
-    protected $baseUrl = 'https://myanimelist.net/api/';
30
-
31
-    /**
32
-     * Default settings for Guzzle
33
-     * @var array
34
-     */
35
-    protected $connectionDefaults = [];
36
-
37
-    /**
38
-     * Get the access token from the Kitsu API
39
-     *
40
-     * @param string $username
41
-     * @param string $password
42
-     * @return bool|string
43
-     */
44
-    public function authenticate(string $username, string $password)
45
-    {
46
-        $response = $this->post('account/', [
47
-            'body' => http_build_query([
48
-                'grant_type' => 'password',
49
-                'username' => $username,
50
-                'password' => $password
51
-            ])
52
-        ]);
53
-
54
-        $info = $response->getBody();
55
-
56
-        if (array_key_exists('access_token', $info)) {
57
-            // @TODO save token
58
-            return true;
59
-        }
60
-
61
-        return false;
62
-    }
25
+class Model {
26
+	
27
+	use ContainerAware;
28
+	use MALTrait;
29
+
30
+	public function createListItem(array $data): bool
31
+	{
32
+
33
+	}
34
+
35
+	public function getListItem(string $listId): array
36
+	{
37
+
38
+	}
39
+
40
+	public function updateListItem(array $data)
41
+	{
42
+
43
+	}
44
+
45
+	public function deleteListItem(string $id): bool
46
+	{
47
+
48
+	}
63 49
 }

+ 46
- 0
src/API/MAL/Transformer/AnimeListTransformer.php View File

@@ -0,0 +1,46 @@
1
+<?php declare(strict_types=1);
2
+/**
3
+ * Anime List Client
4
+ *
5
+ * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
6
+ *
7
+ * PHP version 7
8
+ *
9
+ * @package     AnimeListClient
10
+ * @author      Timothy J. Warren <tim@timshomepage.net>
11
+ * @copyright   2015 - 2017  Timothy J. Warren
12
+ * @license     http://www.opensource.org/licenses/mit-license.html  MIT License
13
+ * @version     4.0
14
+ * @link        https://github.com/timw4mail/HummingBirdAnimeClient
15
+ */
16
+
17
+namespace Aviat\AnimeClient\API\MAL;
18
+
19
+use Aviat\Ion\Transformer\AbstractTransformer;
20
+
21
+/**
22
+ * Transformer for updating MAL List
23
+ */
24
+class AnimeListTransformer extends AbstractTransformer {
25
+
26
+	public function transform($item)
27
+	{
28
+		$rewatching = 'false';
29
+		if (array_key_exists('rewatching', $item) && $item['rewatching'])
30
+		{
31
+			$rewatching = 'true';
32
+		}
33
+
34
+		return [
35
+			'id' => $item['id'],
36
+			'data' => [
37
+				'status' => $item['watching_status'],
38
+				'rating' => $item['user_rating'],
39
+				'rewatch_value' => (int) $rewatching,
40
+				'times_rewatched' => $item['rewatched'],
41
+				'comments' => $item['notes'],
42
+				'episode' => $item['episodes_watched']
43
+			]
44
+		];
45
+	}
46
+}

+ 33
- 0
src/API/MAL/Transformer/MALToKitsuTransformer.php View File

@@ -0,0 +1,33 @@
1
+<?php declare(strict_types=1);
2
+/**
3
+ * Anime List Client
4
+ *
5
+ * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
6
+ *
7
+ * PHP version 7
8
+ *
9
+ * @package     AnimeListClient
10
+ * @author      Timothy J. Warren <tim@timshomepage.net>
11
+ * @copyright   2015 - 2017  Timothy J. Warren
12
+ * @license     http://www.opensource.org/licenses/mit-license.html  MIT License
13
+ * @version     4.0
14
+ * @link        https://github.com/timw4mail/HummingBirdAnimeClient
15
+ */
16
+
17
+namespace Aviat\AnimeClient\API\MAL;
18
+
19
+use Aviat\Ion\Transformer\AbstractTransformer;
20
+
21
+class MALToKitsuTransformer extends AbstractTransformer {
22
+	
23
+	
24
+	public function transform($item)
25
+	{
26
+		
27
+	}
28
+	
29
+	public function untransform($item)
30
+	{
31
+		
32
+	}
33
+}

+ 2
- 1
src/Controller.php View File

@@ -304,7 +304,8 @@ class Controller {
304 304
 			return $this->session_redirect();
305 305
 		}
306 306
 
307
-		$this->login("Invalid username or password.");
307
+		$this->set_flash_message('Invalid username or password.');
308
+		$this->redirect($this->urlGenerator->url('login'), 303);
308 309
 	}
309 310
 
310 311
 	/**

+ 0
- 1
src/Util.php View File

@@ -16,7 +16,6 @@
16 16
 
17 17
 namespace Aviat\AnimeClient;
18 18
 
19
-use abeautifulsite\SimpleImage;
20 19
 use Aviat\Ion\ConfigInterface;
21 20
 use Aviat\Ion\Di\{ContainerAware, ContainerInterface};
22 21
 use DomainException;

+ 48
- 0
tests/API/JsonAPITest.php View File

@@ -0,0 +1,48 @@
1
+<?php declare(strict_types=1);
2
+/**
3
+ * Anime List Client
4
+ *
5
+ * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
6
+ *
7
+ * PHP version 7
8
+ *
9
+ * @package     AnimeListClient
10
+ * @author      Timothy J. Warren <tim@timshomepage.net>
11
+ * @copyright   2015 - 2017  Timothy J. Warren
12
+ * @license     http://www.opensource.org/licenses/mit-license.html  MIT License
13
+ * @version     4.0
14
+ * @link        https://github.com/timw4mail/HummingBirdAnimeClient
15
+ */
16
+
17
+namespace Aviat\AnimeClient\Tests\API;
18
+
19
+use Aviat\AnimeClient\API\JsonAPI;
20
+use Aviat\Ion\Json;
21
+use PHPUnit\Framework\TestCase;
22
+
23
+class JsonAPITest extends TestCase {
24
+	
25
+	public function setUp()
26
+	{
27
+		$dir = __DIR__ . '/../test_data/JsonAPI';
28
+		$this->startData = Json::decodeFile("{$dir}/jsonApiExample.json");
29
+		$this->organizedIncludes = Json::decodeFile("{$dir}/organizedIncludes.json");
30
+		$this->inlineIncluded = Json::decodeFile("{$dir}/inlineIncluded.json");
31
+	}
32
+	
33
+	public function testOrganizeIncludes()
34
+	{
35
+		$expected = $this->organizedIncludes;
36
+		$actual = JsonAPI::organizeIncludes($this->startData['included']);
37
+
38
+		$this->assertEquals($expected, $actual);
39
+	}
40
+	
41
+	public function testInlineIncludedRelationships()
42
+	{
43
+		$expected = $this->inlineIncluded;
44
+		$actual = JsonAPI::inlineIncludedRelationships($this->organizedIncludes, 'anime');
45
+		
46
+		$this->assertEquals($expected, $actual);
47
+	}
48
+}

+ 90
- 0
tests/API/Kitsu/Transformer/AnimeListTransformerTest.php View File

@@ -0,0 +1,90 @@
1
+<?php declare(strict_types=1);
2
+
3
+namespace Aviat\AnimeClient\Tests\API\Kitsu\Transformer;
4
+
5
+use AnimeClient_TestCase;
6
+use Aviat\AnimeClient\API\Kitsu\Transformer\AnimeListTransformer;
7
+use Aviat\Ion\Friend;
8
+use Aviat\Ion\Json;
9
+
10
+class AnimeListTransformerTest extends AnimeClient_TestCase {
11
+	
12
+	public function setUp()
13
+	{
14
+		parent::setUp();
15
+		$dir = AnimeClient_TestCase::TEST_DATA_DIR . '/Kitsu';
16
+		
17
+		$this->beforeTransform = Json::decodeFile("{$dir}/animeListItemBeforeTransform.json");
18
+		$this->afterTransform = Json::decodeFile("{$dir}/animeListItemAfterTransform.json");
19
+		
20
+		$this->transformer = new AnimeListTransformer();
21
+	}
22
+	
23
+	public function testTransform()
24
+	{
25
+		$expected = $this->afterTransform;
26
+		$actual = $this->transformer->transform($this->beforeTransform);
27
+		
28
+		$this->assertEquals($expected, $actual);
29
+	}
30
+	
31
+	public function dataUntransform()
32
+	{
33
+		return [[
34
+			'input' => [
35
+				'id' => 14047981,
36
+				'watching_status' => 'current',
37
+				'user_rating' => 8,
38
+				'episodes_watched' => 38,
39
+				'rewatched' => 0,
40
+				'notes' => 'Very formulaic.',
41
+				'edit' => true
42
+			],
43
+			'expected' => [
44
+				'id' => 14047981,
45
+				'data' => [
46
+					'status' => 'current',
47
+					'rating' => 4,
48
+					'reconsuming' => false,
49
+					'reconsumeCount' => 0,
50
+					'notes' => 'Very formulaic.',
51
+					'progress' => 38,
52
+					'private' => false
53
+				]
54
+			]
55
+		], [
56
+			'input' => [
57
+				'id' => 14047981,
58
+				'watching_status' => 'current',
59
+				'user_rating' => 8,
60
+				'episodes_watched' => 38,
61
+				'rewatched' => 0,
62
+				'notes' => 'Very formulaic.',
63
+				'edit' => 'true',
64
+				'private' => 'On',
65
+				'rewatching' => 'On'
66
+			],
67
+			'expected' => [
68
+				'id' => 14047981,
69
+				'data' => [
70
+					'status' => 'current',
71
+					'rating' => 4,
72
+					'reconsuming' => true,
73
+					'reconsumeCount' => 0,
74
+					'notes' => 'Very formulaic.',
75
+					'progress' => 38,
76
+					'private' => true,
77
+				]
78
+			]
79
+		]];
80
+	}
81
+	
82
+	/**
83
+	 * @dataProvider dataUntransform
84
+	 */
85
+	public function testUntransform($input, $expected)
86
+	{
87
+		$actual = $this->transformer->untransform($input);
88
+		$this->assertEquals($expected, $actual);
89
+	}
90
+}

+ 30
- 0
tests/API/Kitsu/Transformer/AnimeTransformerTest.php View File

@@ -0,0 +1,30 @@
1
+<?php declare(strict_types=1);
2
+
3
+namespace Aviat\AnimeClient\Tests\API\Kitsu\Transformer;
4
+
5
+use AnimeClient_TestCase;
6
+use Aviat\AnimeClient\API\Kitsu\Transformer\AnimeTransformer;
7
+use Aviat\Ion\Friend;
8
+use Aviat\Ion\Json;
9
+
10
+class AnimeTransformerTest extends AnimeClient_TestCase {
11
+	
12
+	public function setUp()
13
+	{
14
+		parent::setUp();
15
+		$dir = AnimeClient_TestCase::TEST_DATA_DIR . '/Kitsu';
16
+		
17
+		//$this->beforeTransform = Json::decodeFile("{$dir}/animeBeforeTransform.json");
18
+		//$this->afterTransform = Json::decodeFile("{$dir}/animeAfterTransform.json");
19
+		
20
+		$this->transformer = new AnimeTransformer();
21
+	}
22
+	
23
+	public function testTransform()
24
+	{
25
+		/*$expected = $this->afterTransform;
26
+		$actual = $this->transformer->transform($this->beforeTransform);
27
+		
28
+		$this->assertEquals($expected, $actual);*/
29
+	}
30
+}

+ 2
- 16
tests/API/XMLTest.php View File

@@ -1,18 +1,4 @@
1 1
 <?php declare(strict_types=1);
2
-/**
3
- * Anime List Client
4
- *
5
- * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
6
- *
7
- * PHP version 7
8
- *
9
- * @package     AnimeListClient
10
- * @author      Timothy J. Warren <tim@timshomepage.net>
11
- * @copyright   2015 - 2017  Timothy J. Warren
12
- * @license     http://www.opensource.org/licenses/mit-license.html  MIT License
13
- * @version     4.0
14
- * @link        https://github.com/timw4mail/HummingBirdAnimeClient
15
- */
16 2
 
17 3
 namespace Aviat\AnimeClient\Tests\API;
18 4
 
@@ -23,8 +9,8 @@ class XMLTest extends TestCase {
23 9
 
24 10
 	public function setUp()
25 11
 	{
26
-		$this->xml = file_get_contents(__DIR__ . '/../test_data/xmlTestFile.xml');
27
-		$this->expectedXml = file_get_contents(__DIR__ . '/../test_data/minifiedXmlTestFile.xml');
12
+		$this->xml = file_get_contents(__DIR__ . '/../test_data/XML/xmlTestFile.xml');
13
+		$this->expectedXml = file_get_contents(__DIR__ . '/../test_data/XML/minifiedXmlTestFile.xml');
28 14
 
29 15
 		$this->array = [
30 16
 			'entry' => [

+ 6
- 3
tests/AnimeClient_TestCase.php View File

@@ -7,8 +7,11 @@ use GuzzleHttp\Client;
7 7
 use GuzzleHttp\Handler\MockHandler;
8 8
 use GuzzleHttp\HandlerStack;
9 9
 use GuzzleHttp\Psr7\Response;
10
-use Zend\Diactoros\Response as HttpResponse;
11
-use Zend\Diactoros\ServerRequestFactory;
10
+use PHPUnit\Framework\TestCase;
11
+use Zend\Diactoros\{
12
+	Response as HttpResponse,
13
+	ServerRequestFactory
14
+};
12 15
 
13 16
 define('ROOT_DIR', __DIR__ . '/../');
14 17
 define('TEST_DATA_DIR', __DIR__ . '/test_data');
@@ -17,7 +20,7 @@ define('TEST_VIEW_DIR', __DIR__ . '/test_views');
17 20
 /**
18 21
  * Base class for TestCases
19 22
  */
20
-class AnimeClient_TestCase extends PHPUnit_Framework_TestCase {
23
+class AnimeClient_TestCase extends TestCase {
21 24
 	// Test directory constants
22 25
 	const ROOT_DIR = ROOT_DIR;
23 26
 	const SRC_DIR = AnimeClient::SRC_DIR;

+ 6
- 4
tests/ControllerTest.php View File

@@ -1,10 +1,12 @@
1
-<?php
1
+<?php declare(strict_types=1);
2 2
 use Aura\Router\RouterFactory;
3 3
 use Aura\Web\WebFactory;
4 4
 use Aviat\AnimeClient\Controller;
5
-use Aviat\AnimeClient\Controller\Anime as AnimeController;
6
-use Aviat\AnimeClient\Controller\Collection as CollectionController;
7
-use Aviat\AnimeClient\Controller\Manga as MangaController;
5
+use Aviat\AnimeClient\Controller\{
6
+	Anime as AnimeController,
7
+	Collection as CollectionController,
8
+	Manga as MangaController
9
+};
8 10
 
9 11
 class ControllerTest extends AnimeClient_TestCase {
10 12
 

+ 1
- 0
tests/test_data/JsonAPI/inlineIncluded.json
File diff suppressed because it is too large
View File


tests/test_data/jsonApiExample.json → tests/test_data/JsonAPI/jsonApiExample.json View File


+ 385
- 0
tests/test_data/JsonAPI/organizedIncludes.json View File

@@ -0,0 +1,385 @@
1
+{
2
+    "anime": {
3
+        "11474": {
4
+            "slug": "hibike-euphonium-2",
5
+            "synopsis": "Second season of Hibike! Euphonium.",
6
+            "coverImageTopOffset": 120,
7
+            "titles": {
8
+                "en": "Sound! Euphonium 2",
9
+                "en_jp": "Hibike! Euphonium 2",
10
+                "ja_jp": "\u97ff\u3051\uff01\u30e6\u30fc\u30d5\u30a9\u30cb\u30a2\u30e0 \uff12"
11
+            },
12
+            "canonicalTitle": "Hibike! Euphonium 2",
13
+            "abbreviatedTitles": null,
14
+            "averageRating": 4.1684326428476,
15
+            "ratingFrequencies": {
16
+                "0.5": "1",
17
+                "1.0": "1",
18
+                "1.5": "2",
19
+                "2.0": "8",
20
+                "2.5": "13",
21
+                "3.0": "42",
22
+                "3.5": "90",
23
+                "4.0": "193",
24
+                "4.5": "180",
25
+                "5.0": "193",
26
+                "nil": "1972"
27
+            },
28
+            "startDate": "2016-10-06",
29
+            "endDate": null,
30
+            "posterImage": {
31
+                "tiny": "https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/tiny.jpg?1470781430",
32
+                "small": "https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/small.jpg?1470781430",
33
+                "medium": "https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/medium.jpg?1470781430",
34
+                "large": "https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/large.jpg?1470781430",
35
+                "original": "https:\/\/media.kitsu.io\/anime\/poster_images\/11474\/original.jpg?1470781430"
36
+            },
37
+            "coverImage": {
38
+                "small": "https:\/\/media.kitsu.io\/anime\/cover_images\/11474\/small.jpg?1476203965",
39
+                "large": "https:\/\/media.kitsu.io\/anime\/cover_images\/11474\/large.jpg?1476203965",
40
+                "original": "https:\/\/media.kitsu.io\/anime\/cover_images\/11474\/original.jpg?1476203965"
41
+            },
42
+            "episodeCount": 13,
43
+            "episodeLength": 25,
44
+            "subtype": "TV",
45
+            "youtubeVideoId": "d2Di5swwzxg",
46
+            "ageRating": "PG",
47
+            "ageRatingGuide": "",
48
+            "showType": "TV",
49
+            "nsfw": false,
50
+            "relationships": {
51
+                "genres": ["24", "35", "4"],
52
+                "mappings": ["3155"]
53
+            }
54
+        },
55
+        "10802": {
56
+            "slug": "nisekoimonogatari",
57
+            "synopsis": "Trailer for a fake anime created by Shaft as an April Fool's Day joke.",
58
+            "coverImageTopOffset": 80,
59
+            "titles": {
60
+                "en": "",
61
+                "en_jp": "Nisekoimonogatari",
62
+                "ja_jp": ""
63
+            },
64
+            "canonicalTitle": "Nisekoimonogatari",
65
+            "abbreviatedTitles": null,
66
+            "averageRating": 3.4857993435287,
67
+            "ratingFrequencies": {
68
+                "0.5": "22",
69
+                "1.0": "10",
70
+                "1.5": "16",
71
+                "2.0": "32",
72
+                "2.5": "74",
73
+                "3.0": "97",
74
+                "3.5": "118",
75
+                "4.0": "72",
76
+                "4.5": "34",
77
+                "5.0": "136",
78
+                "nil": "597",
79
+                "0.89": "-1",
80
+                "3.63": "-1",
81
+                "4.11": "-1",
82
+                "0.068": "-1",
83
+                "0.205": "-1",
84
+                "0.274": "-2",
85
+                "0.479": "-1",
86
+                "0.548": "-1",
87
+                "1.096": "-2",
88
+                "1.164": "-1",
89
+                "1.438": "-1",
90
+                "1.918": "-1",
91
+                "2.055": "-1",
92
+                "3.973": "-1",
93
+                "4.178": "-3",
94
+                "4.247": "-1",
95
+                "4.726": "-1",
96
+                "4.932": "-3",
97
+                "1.0958904109589": "3",
98
+                "0.89041095890411": "2",
99
+                "1.02739726027397": "1",
100
+                "1.16438356164384": "2",
101
+                "1.43835616438356": "2",
102
+                "1.57534246575342": "1",
103
+                "1.91780821917808": "1",
104
+                "2.05479452054794": "2",
105
+                "2.12328767123288": "1",
106
+                "2.73972602739726": "1",
107
+                "2.80821917808219": "2",
108
+                "2.94520547945205": "1",
109
+                "3.15068493150685": "1",
110
+                "3.35616438356164": "2",
111
+                "3.63013698630137": "2",
112
+                "3.97260273972603": "1",
113
+                "4.10958904109589": "2",
114
+                "4.17808219178082": "3",
115
+                "4.24657534246575": "1",
116
+                "4.38356164383562": "2",
117
+                "4.65753424657534": "1",
118
+                "4.72602739726027": "2",
119
+                "4.86301369863014": "1",
120
+                "4.93150684931507": "10",
121
+                "0.205479452054795": "1",
122
+                "0.273972602739726": "2",
123
+                "0.479452054794521": "2",
124
+                "0.547945205479452": "2",
125
+                "0.753424657534246": "1",
126
+                "0.0684931506849315": "1"
127
+            },
128
+            "startDate": "2015-04-01",
129
+            "endDate": null,
130
+            "posterImage": {
131
+                "tiny": "https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/tiny.jpg?1427974534",
132
+                "small": "https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/small.jpg?1427974534",
133
+                "medium": "https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/medium.jpg?1427974534",
134
+                "large": "https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/large.jpg?1427974534",
135
+                "original": "https:\/\/media.kitsu.io\/anime\/poster_images\/10802\/original.jpg?1427974534"
136
+            },
137
+            "coverImage": {
138
+                "small": "https:\/\/media.kitsu.io\/anime\/cover_images\/10802\/small.jpg?1427928458",
139
+                "large": "https:\/\/media.kitsu.io\/anime\/cover_images\/10802\/large.jpg?1427928458",
140
+                "original": "https:\/\/media.kitsu.io\/anime\/cover_images\/10802\/original.jpg?1427928458"
141
+            },
142
+            "episodeCount": 1,
143
+            "episodeLength": 1,
144
+            "subtype": "ONA",
145
+            "youtubeVideoId": "",
146
+            "ageRating": "PG",
147
+            "ageRatingGuide": "Teens 13 or older",
148
+            "showType": "ONA",
149
+            "nsfw": false,
150
+            "relationships": {
151
+                "genres": ["3"],
152
+                "mappings": ["1755"]
153
+            }
154
+        },
155
+        "11887": {
156
+            "slug": "brave-witches",
157
+            "synopsis": "In September 1944, allied forces led by the 501st Joint Fighter Wing \"Strike Witches\" successfully eliminate the Neuroi threat from the skies of the Republic of Gallia, thus ensuring the security of western Europe. Taking advantage of this victory, allied forces begin a full-fledged push toward central and eastern Europe. From a base in Petersburg in the Empire of Orussia, the 502nd Joint Fighter Wing \"Brave Witches,\" upon whom mankind has placed its hopes, flies with courage in the cold skies of eastern Europe.\n\n(Source: MAL News)",
158
+            "coverImageTopOffset": 380,
159
+            "titles": {
160
+                "en": "",
161
+                "en_jp": "Brave Witches",
162
+                "ja_jp": "\u30d6\u30ec\u30a4\u30d6\u30a6\u30a3\u30c3\u30c1\u30fc\u30ba"
163
+            },
164
+            "canonicalTitle": "Brave Witches",
165
+            "abbreviatedTitles": null,
166
+            "averageRating": 3.5846888163849,
167
+            "ratingFrequencies": {
168
+                "0.5": "1",
169
+                "1.0": "4",
170
+                "1.5": "8",
171
+                "2.0": "12",
172
+                "2.5": "17",
173
+                "3.0": "33",
174
+                "3.5": "41",
175
+                "4.0": "32",
176
+                "4.5": "9",
177
+                "5.0": "19",
178
+                "nil": "620"
179
+            },
180
+            "startDate": "2016-10-06",
181
+            "endDate": null,
182
+            "posterImage": {
183
+                "tiny": "https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/tiny.jpg?1476481854",
184
+                "small": "https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/small.jpg?1476481854",
185
+                "medium": "https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/medium.jpg?1476481854",
186
+                "large": "https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/large.jpg?1476481854",
187
+                "original": "https:\/\/media.kitsu.io\/anime\/poster_images\/11887\/original.png?1476481854"
188
+            },
189
+            "coverImage": {
190
+                "small": "https:\/\/media.kitsu.io\/anime\/cover_images\/11887\/small.jpg?1479834725",
191
+                "large": "https:\/\/media.kitsu.io\/anime\/cover_images\/11887\/large.jpg?1479834725",
192
+                "original": "https:\/\/media.kitsu.io\/anime\/cover_images\/11887\/original.jpg?1479834725"
193
+            },
194
+            "episodeCount": 12,
195
+            "episodeLength": 24,
196
+            "subtype": "TV",
197
+            "youtubeVideoId": "VLUqd-jEBuE",
198
+            "ageRating": "R",
199
+            "ageRatingGuide": "Mild Nudity",
200
+            "showType": "TV",
201
+            "nsfw": false,
202
+            "relationships": {
203
+                "genres": ["5", "8", "28", "1", "25"],
204
+                "mappings": ["2593"]
205
+            }
206
+        },
207
+        "12024": {
208
+            "slug": "www-working",
209
+            "synopsis": "Daisuke Higashida is a serious first-year student at Higashizaka High School. He lives a peaceful everyday life even though he is not satisfied with the family who doesn't laugh at all and makes him tired. However, his father's company goes bankrupt one day, and he can no longer afford allowances, cellphone bills, and commuter tickets. When his father orders him to take up a part-time job, Daisuke decides to work at a nearby family restaurant in order to avoid traveling 15 kilometers to school by bicycle.",
210
+            "coverImageTopOffset": 165,
211
+            "titles": {
212
+                "en": "WWW.WAGNARIA!!",
213
+                "en_jp": "WWW.Working!!",
214
+                "ja_jp": ""
215
+            },
216
+            "canonicalTitle": "WWW.Working!!",
217
+            "abbreviatedTitles": null,
218
+            "averageRating": 3.8238374224378,
219
+            "ratingFrequencies": {
220
+                "1.0": "2",
221
+                "1.5": "7",
222
+                "2.0": "19",
223
+                "2.5": "28",
224
+                "3.0": "68",
225
+                "3.5": "114",
226
+                "4.0": "144",
227
+                "4.5": "78",
228
+                "5.0": "74",
229
+                "nil": "1182"
230
+            },
231
+            "startDate": "2016-10-01",
232
+            "endDate": "2016-12-24",
233
+            "posterImage": {
234
+                "tiny": "https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/tiny.jpg?1473990267",
235
+                "small": "https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/small.jpg?1473990267",
236
+                "medium": "https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/medium.jpg?1473990267",
237
+                "large": "https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/large.jpg?1473990267",
238
+                "original": "https:\/\/media.kitsu.io\/anime\/poster_images\/12024\/original.jpg?1473990267"
239
+            },
240
+            "coverImage": {
241
+                "small": "https:\/\/media.kitsu.io\/anime\/cover_images\/12024\/small.jpg?1479834612",
242
+                "large": "https:\/\/media.kitsu.io\/anime\/cover_images\/12024\/large.jpg?1479834612",
243
+                "original": "https:\/\/media.kitsu.io\/anime\/cover_images\/12024\/original.png?1479834612"
244
+            },
245
+            "episodeCount": 13,
246
+            "episodeLength": 23,
247
+            "subtype": "TV",
248
+            "youtubeVideoId": "",
249
+            "ageRating": "PG",
250
+            "ageRatingGuide": "Teens 13 or older",
251
+            "showType": "TV",
252
+            "nsfw": false,
253
+            "relationships": {
254
+                "genres": ["3", "16"],
255
+                "mappings": ["2538"]
256
+            }
257
+        },
258
+        "12465": {
259
+            "slug": "bishoujo-yuugi-unit-crane-game-girls-galaxy",
260
+            "synopsis": "Second season of Bishoujo Yuugi Unit Crane Game Girls.",
261
+            "coverImageTopOffset": 0,
262
+            "titles": {
263
+                "en": "Crane Game Girls Galaxy",
264
+                "en_jp": "Bishoujo Yuugi Unit Crane Game Girls Galaxy",
265
+                "ja_jp": "\u7f8e\u5c11\u5973\u904a\u622f\u30e6\u30cb\u30c3\u30c8 \u30af\u30ec\u30fc\u30f3\u30b2\u30fc\u30eb\u30ae\u30e3\u30e9\u30af\u30b7\u30fc"
266
+            },
267
+            "canonicalTitle": "Bishoujo Yuugi Unit Crane Game Girls Galaxy",
268
+            "abbreviatedTitles": null,
269
+            "averageRating": null,
270
+            "ratingFrequencies": {
271
+                "0.5": "2",
272
+                "1.0": "2",
273
+                "1.5": "0",
274
+                "2.0": "4",
275
+                "2.5": "6",
276
+                "3.0": "2",
277
+                "3.5": "4",
278
+                "4.0": "1",
279
+                "4.5": "2",
280
+                "nil": "66"
281
+            },
282
+            "startDate": "2016-10-05",
283
+            "endDate": null,
284
+            "posterImage": {
285
+                "tiny": "https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/tiny.jpg?1473601756",
286
+                "small": "https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/small.jpg?1473601756",
287
+                "medium": "https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/medium.jpg?1473601756",
288
+                "large": "https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/large.jpg?1473601756",
289
+                "original": "https:\/\/media.kitsu.io\/anime\/poster_images\/12465\/original.png?1473601756"
290
+            },
291
+            "coverImage": null,
292
+            "episodeCount": null,
293
+            "episodeLength": 13,
294
+            "subtype": "TV",
295
+            "youtubeVideoId": "",
296
+            "ageRating": "PG",
297
+            "ageRatingGuide": "Children",
298
+            "showType": "TV",
299
+            "nsfw": false,
300
+            "relationships": {
301
+                "genres": ["3"],
302
+                "mappings": ["9871"]
303
+            }
304
+        }
305
+    },
306
+    "genres": {
307
+        "24": {
308
+            "name": "School",
309
+            "slug": "school",
310
+            "description": null
311
+        },
312
+        "35": {
313
+            "name": "Music",
314
+            "slug": "music",
315
+            "description": null
316
+        },
317
+        "4": {
318
+            "name": "Drama",
319
+            "slug": "drama",
320
+            "description": ""
321
+        },
322
+        "3": {
323
+            "name": "Comedy",
324
+            "slug": "comedy",
325
+            "description": null
326
+        },
327
+        "5": {
328
+            "name": "Sci-Fi",
329
+            "slug": "sci-fi",
330
+            "description": null
331
+        },
332
+        "8": {
333
+            "name": "Magic",
334
+            "slug": "magic",
335
+            "description": null
336
+        },
337
+        "28": {
338
+            "name": "Military",
339
+            "slug": "military",
340
+            "description": null
341
+        },
342
+        "1": {
343
+            "name": "Action",
344
+            "slug": "action",
345
+            "description": ""
346
+        },
347
+        "25": {
348
+            "name": "Ecchi",
349
+            "slug": "ecchi",
350
+            "description": ""
351
+        },
352
+        "16": {
353
+            "name": "Slice of Life",
354
+            "slug": "slice-of-life",
355
+            "description": ""
356
+        }
357
+    },
358
+    "mappings": {
359
+        "3155": {
360
+            "externalSite": "myanimelist\/anime",
361
+            "externalId": "31988",
362
+            "relationships": []
363
+        },
364
+        "1755": {
365
+            "externalSite": "myanimelist\/anime",
366
+            "externalId": "30514",
367
+            "relationships": []
368
+        },
369
+        "2593": {
370
+            "externalSite": "myanimelist\/anime",
371
+            "externalId": "32866",
372
+            "relationships": []
373
+        },
374
+        "2538": {
375
+            "externalSite": "myanimelist\/anime",
376
+            "externalId": "33094",
377
+            "relationships": []
378
+        },
379
+        "9871": {
380
+            "externalSite": "myanimelist\/anime",
381
+            "externalId": "33541",
382
+            "relationships": []
383
+        }
384
+    }
385
+}

+ 28
- 0
tests/test_data/Kitsu/animeListItemAfterTransform.json View File

@@ -0,0 +1,28 @@
1
+{
2
+    "id": "14047981",
3
+    "mal_id": null,
4
+    "episodes": {
5
+        "watched": 38,
6
+        "total": 48,
7
+        "length": 24
8
+    },
9
+    "airing": {
10
+        "status": "Finished Airing",
11
+        "started": "2012-02-05",
12
+        "ended": "2013-01-27"
13
+    },
14
+    "anime": {
15
+        "age_rating": "PG",
16
+        "titles": ["Smile Precure!", "Glitter Force", "\u30b9\u30de\u30a4\u30eb\u30d7\u30ea\u30ad\u30e5\u30a2\uff01"],
17
+        "slug": "smile-precure",
18
+        "type": "TV",
19
+        "image": "https:\/\/media.kitsu.io\/anime\/poster_images\/6687\/small.jpg?1408459122",
20
+        "genres": ["Magic", "Kids", "Mahou Shoujo", "Fantasy"]
21
+    },
22
+    "watching_status": "current",
23
+    "notes": "Very formulaic.",
24
+    "rewatching": false,
25
+    "rewatched": 0,
26
+    "user_rating": 8,
27
+    "private": false
28
+}

+ 155
- 0
tests/test_data/Kitsu/animeListItemBeforeTransform.json View File

@@ -0,0 +1,155 @@
1
+{
2
+    "id": "14047981",
3
+    "type": "libraryEntries",
4
+    "links": {
5
+        "self": "https:\/\/kitsu.io\/api\/edge\/library-entries\/14047981"
6
+    },
7
+    "attributes": {
8
+        "status": "current",
9
+        "progress": 38,
10
+        "reconsuming": false,
11
+        "reconsumeCount": 0,
12
+        "notes": "Very formulaic.",
13
+        "private": false,
14
+        "rating": "4.0",
15
+        "updatedAt": "2017-01-12T18:24:24.867Z"
16
+    },
17
+    "relationships": {
18
+        "user": {
19
+            "links": {
20
+                "self": "https:\/\/kitsu.io\/api\/edge\/library-entries\/14047981\/relationships\/user",
21
+                "related": "https:\/\/kitsu.io\/api\/edge\/library-entries\/14047981\/user"
22
+            }
23
+        },
24
+        "anime": {
25
+            "links": {
26
+                "self": "https:\/\/kitsu.io\/api\/edge\/library-entries\/14047981\/relationships\/anime",
27
+                "related": "https:\/\/kitsu.io\/api\/edge\/library-entries\/14047981\/anime"
28
+            }
29
+        },
30
+        "manga": {
31
+            "links": {
32
+                "self": "https:\/\/kitsu.io\/api\/edge\/library-entries\/14047981\/relationships\/manga",
33
+                "related": "https:\/\/kitsu.io\/api\/edge\/library-entries\/14047981\/manga"
34
+            }
35
+        },
36
+        "drama": {
37
+            "links": {
38
+                "self": "https:\/\/kitsu.io\/api\/edge\/library-entries\/14047981\/relationships\/drama",
39
+                "related": "https:\/\/kitsu.io\/api\/edge\/library-entries\/14047981\/drama"
40
+            }
41
+        },
42
+        "review": {
43
+            "links": {
44
+                "self": "https:\/\/kitsu.io\/api\/edge\/library-entries\/14047981\/relationships\/review",
45
+                "related": "https:\/\/kitsu.io\/api\/edge\/library-entries\/14047981\/review"
46
+            }
47
+        },
48
+        "media": {
49
+            "links": {
50
+                "self": "https:\/\/kitsu.io\/api\/edge\/library-entries\/14047981\/relationships\/media",
51
+                "related": "https:\/\/kitsu.io\/api\/edge\/library-entries\/14047981\/media"
52
+            },
53
+            "data": {
54
+                "type": "anime",
55
+                "id": "6687"
56
+            }
57
+        },
58
+        "unit": {
59
+            "links": {
60
+                "self": "https:\/\/kitsu.io\/api\/edge\/library-entries\/14047981\/relationships\/unit",
61
+                "related": "https:\/\/kitsu.io\/api\/edge\/library-entries\/14047981\/unit"
62
+            }
63
+        },
64
+        "nextUnit": {
65
+            "links": {
66
+                "self": "https:\/\/kitsu.io\/api\/edge\/library-entries\/14047981\/relationships\/next-unit",
67
+                "related": "https:\/\/kitsu.io\/api\/edge\/library-entries\/14047981\/next-unit"
68
+            }
69
+        }
70
+    },
71
+    "anime": {
72
+        "slug": "smile-precure",
73
+        "synopsis": "Once upon a time, there was a kingdom of fairy tales called \"M\u00e4rchenland\", where many fairy tale characters live together in joy. Suddenly, the evil emperor Pierrot made an invasion on M\u00e4rchenland, sealing its Queen in the process. To revive the Queen, the symbol of happiness called Cure Decor, \"the Queen's scattered power of light of happiness\", is required. To collect the Cure Decor, a fairy named Candy searches for the Pretty Cures on Earth. There, Candy meets a girl, who decides to collect the Cure Decor. Now, will the world earn a \"happy ending\"?",
74
+        "coverImageTopOffset": 100,
75
+        "titles": {
76
+            "en": "Glitter Force",
77
+            "en_jp": "Smile Precure!",
78
+            "ja_jp": "\u30b9\u30de\u30a4\u30eb\u30d7\u30ea\u30ad\u30e5\u30a2\uff01"
79
+        },
80
+        "canonicalTitle": "Smile Precure!",
81
+        "abbreviatedTitles": null,
82
+        "averageRating": 3.6674651842659,
83
+        "ratingFrequencies": {
84
+            "0.5": "4",
85
+            "1.0": "8",
86
+            "1.5": "3",
87
+            "2.0": "17",
88
+            "2.5": "30",
89
+            "3.0": "54",
90
+            "3.5": "69",
91
+            "4.0": "96",
92
+            "4.5": "42",
93
+            "5.0": "57",
94
+            "nil": "594"
95
+        },
96
+        "startDate": "2012-02-05",
97
+        "endDate": "2013-01-27",
98
+        "posterImage": {
99
+            "tiny": "https:\/\/media.kitsu.io\/anime\/poster_images\/6687\/tiny.jpg?1408459122",
100
+            "small": "https:\/\/media.kitsu.io\/anime\/poster_images\/6687\/small.jpg?1408459122",
101
+            "medium": "https:\/\/media.kitsu.io\/anime\/poster_images\/6687\/medium.jpg?1408459122",
102
+            "large": "https:\/\/media.kitsu.io\/anime\/poster_images\/6687\/large.jpg?1408459122",
103
+            "original": "https:\/\/media.kitsu.io\/anime\/poster_images\/6687\/original.jpg?1408459122"
104
+        },
105
+        "coverImage": {
106
+            "small": "https:\/\/media.kitsu.io\/anime\/cover_images\/6687\/small.jpg?1452609041",
107
+            "large": "https:\/\/media.kitsu.io\/anime\/cover_images\/6687\/large.jpg?1452609041",
108
+            "original": "https:\/\/media.kitsu.io\/anime\/cover_images\/6687\/original.png?1452609041"
109
+        },
110
+        "episodeCount": 48,
111
+        "episodeLength": 24,
112
+        "subtype": "TV",
113
+        "youtubeVideoId": "",
114
+        "ageRating": "PG",
115
+        "ageRatingGuide": "Children",
116
+        "showType": "TV",
117
+        "nsfw": false,
118
+        "relationships": {
119
+            "genres": {
120
+                "8": {
121
+                    "name": "Magic",
122
+                    "slug": "magic",
123
+                    "description": null
124
+                },
125
+                "40": {
126
+                    "name": "Kids",
127
+                    "slug": "kids",
128
+                    "description": null
129
+                },
130
+                "47": {
131
+                    "name": "Mahou Shoujo",
132
+                    "slug": "mahou-shoujo",
133
+                    "description": "Magical Girls"
134
+                },
135
+                "11": {
136
+                    "name": "Fantasy",
137
+                    "slug": "fantasy",
138
+                    "description": ""
139
+                }
140
+            },
141
+            "mappings": {
142
+                "778": {
143
+                    "externalSite": "myanimelist\/anime",
144
+                    "externalId": "12191",
145
+                    "relationships": []
146
+                },
147
+                "12547": {
148
+                    "externalSite": "thetvdb\/series",
149
+                    "externalId": "255904",
150
+                    "relationships": []
151
+                }
152
+            }
153
+        }
154
+    }
155
+}

tests/test_data/MALExport.xml → tests/test_data/XML/MALExport.xml View File


tests/test_data/minifiedXmlTestFile.xml → tests/test_data/XML/minifiedXmlTestFile.xml View File


tests/test_data/xmlTestFile.xml → tests/test_data/XML/xmlTestFile.xml View File