Browse Source

Some progress toward better structure through refactoring

Timothy J. Warren 3 years ago
parent
commit
cee211621c
65 changed files with 3114 additions and 471 deletions
  1. 20
    0
      .editorconfig
  2. 2
    2
      app/Base/ApiModel.php
  3. 7
    4
      app/Base/Config.php
  4. 2
    2
      app/Base/Controller.php
  5. 2
    2
      app/Base/DBModel.php
  6. 2
    2
      app/Base/Model.php
  7. 5
    14
      app/Base/Router.php
  8. 0
    0
      app/Base/functions.php
  9. 5
    10
      app/Base/pre_conf_functions.php
  10. 7
    2
      app/Controller/Anime.php
  11. 6
    2
      app/Controller/Manga.php
  12. 5
    2
      app/Model/Anime.php
  13. 6
    2
      app/Model/AnimeCollection.php
  14. 5
    2
      app/Model/Manga.php
  15. 20
    5
      app/bootstrap.php
  16. 31
    13
      app/config/config.php
  17. 50
    43
      app/config/routes.php
  18. 10
    4
      composer.json
  19. 35
    4
      index.php
  20. 2
    8
      phpdoc.dist.xml
  21. 7
    4
      phpunit.xml
  22. 164
    0
      src/Base/Config.php
  23. 50
    0
      src/Base/Container.php
  24. 283
    0
      src/Base/Controller.php
  25. 112
    0
      src/Base/Model.php
  26. 81
    0
      src/Base/Model/API.php
  27. 34
    0
      src/Base/Model/DB.php
  28. 230
    0
      src/Base/Router.php
  29. 149
    0
      src/Base/UrlGenerator.php
  30. 197
    0
      src/Controller/Anime.php
  31. 154
    0
      src/Controller/Collection.php
  32. 94
    0
      src/Controller/Manga.php
  33. 11
    0
      src/Controller/Stats.php
  34. 239
    0
      src/Model/Anime.php
  35. 379
    0
      src/Model/AnimeCollection.php
  36. 192
    0
      src/Model/Manga.php
  37. 27
    0
      src/Model/Stats.php
  38. 29
    0
      src/Model/StatsChartsTrait.php
  39. 68
    0
      src/functions.php
  40. 0
    0
      src/views/404.php
  41. 0
    0
      src/views/anime/cover.php
  42. 0
    0
      src/views/anime/edit.php
  43. 0
    0
      src/views/anime/list.php
  44. 0
    0
      src/views/collection/add.php
  45. 0
    0
      src/views/collection/cover.php
  46. 0
    0
      src/views/collection/edit.php
  47. 0
    0
      src/views/collection/list.php
  48. 0
    0
      src/views/footer.php
  49. 5
    5
      src/views/header.php
  50. 0
    0
      src/views/login.php
  51. 0
    0
      src/views/manga/cover.php
  52. 0
    0
      src/views/manga/list.php
  53. 0
    0
      src/views/message.php
  54. 61
    0
      tests/Base/BaseControllerTest.php
  55. 3
    2
      tests/Base/BaseModelTest.php
  56. 61
    29
      tests/Base/ConfigTest.php
  57. 0
    0
      tests/Base/CoreTest.php
  58. 7
    6
      tests/Base/Model/BaseApiModelTest.php
  59. 2
    2
      tests/Base/Model/BaseDBModelTest.php
  60. 193
    0
      tests/Base/RouterTest.php
  61. 0
    0
      tests/FunctionsTest.php
  62. 7
    0
      tests/Model/AnimeCollectionModelest.php
  63. 0
    31
      tests/base/BaseControllerTest.php
  64. 0
    250
      tests/base/RouterTest.php
  65. 53
    19
      tests/bootstrap.php

+ 20
- 0
.editorconfig View File

@@ -0,0 +1,20 @@
1
+# EditorConfig is awesome: http://EditorConfig.org
2
+
3
+# top-most EditorConfig file
4
+root = true
5
+
6
+# Unix-style newlines with a newline ending every file
7
+[*]
8
+end_of_line = lf
9
+insert_final_newline = false
10
+charset = utf-8
11
+indent_style = tab
12
+trim_trailing_whitespace = true
13
+
14
+[*.{cpp,c,h,hpp,cxx}]
15
+insert_final_newline = true
16
+
17
+# Yaml files
18
+[*.{yml,yaml}]
19
+indent_style = space
20
+indent_size = 4

app/base/BaseApiModel.php → app/Base/ApiModel.php View File

@@ -2,7 +2,7 @@
2 2
 /**
3 3
  * Base API Model
4 4
  */
5
-namespace AnimeClient;
5
+namespace AnimeClient\Base;
6 6
 
7 7
 use \GuzzleHttp\Client;
8 8
 use \GuzzleHttp\Cookie\CookieJar;
@@ -10,7 +10,7 @@ use \GuzzleHttp\Cookie\CookieJar;
10 10
 /**
11 11
  * Base model for api interaction
12 12
  */
13
-class BaseApiModel extends BaseModel {
13
+class ApiModel extends Model {
14 14
 
15 15
 	/**
16 16
 	 * Base url for making api requests

app/base/Config.php → app/Base/Config.php View File

@@ -1,6 +1,9 @@
1 1
 <?php
2
+/**
3
+ * Base Configuration class
4
+ */
2 5
 
3
-namespace AnimeClient;
6
+namespace AnimeClient\Base;
4 7
 
5 8
 /**
6 9
  * Wrapper for configuration values
@@ -58,7 +61,7 @@ class Config {
58 61
 	 *
59 62
 	 * @return string
60 63
 	 */
61
-	function asset_url(/*...*/)
64
+	public function asset_url(/*...*/)
62 65
 	{
63 66
 		$args = func_get_args();
64 67
 		$base_url = rtrim($this->__get('asset_path'), '/');
@@ -74,7 +77,7 @@ class Config {
74 77
 	 * @param string $type - (optional) The controller
75 78
 	 * @return string
76 79
 	 */
77
-	function base_url($type="anime")
80
+	public function base_url($type="anime")
78 81
 	{
79 82
 		$config_path = trim($this->__get("{$type}_path"), "/");
80 83
 		$config_host = $this->__get("{$type}_host");
@@ -93,7 +96,7 @@ class Config {
93 96
 	 * @param string $type - (optional) The controller (anime or manga), defaults to anime
94 97
 	 * @return string
95 98
 	 */
96
-	function full_url($path="", $type="anime")
99
+	public function full_url($path="", $type="anime")
97 100
 	{
98 101
 		$config_path = trim($this->__get("{$type}_path"), "/");
99 102
 		$config_host = $this->__get("{$type}_host");

app/base/BaseController.php → app/Base/Controller.php View File

@@ -2,14 +2,14 @@
2 2
 /**
3 3
  * Base Controller
4 4
  */
5
-namespace AnimeClient;
5
+namespace AnimeClient\Base;
6 6
 
7 7
 use Aura\Web\WebFactory;
8 8
 
9 9
 /**
10 10
  * Base class for controllers, defines output methods
11 11
  */
12
-class BaseController {
12
+class Controller {
13 13
 
14 14
 	/**
15 15
 	 * The global configuration object

app/base/BaseDBModel.php → app/Base/DBModel.php View File

@@ -2,12 +2,12 @@
2 2
 /**
3 3
  * Base DB model
4 4
  */
5
-namespace AnimeClient;
5
+namespace AnimeClient\Base;
6 6
 
7 7
 /**
8 8
  * Base model for database interaction
9 9
  */
10
-class BaseDBModel extends BaseModel {
10
+class DBModel extends Model {
11 11
 	/**
12 12
 	 * The query builder object
13 13
 	 * @var object $db

app/base/BaseModel.php → app/Base/Model.php View File

@@ -2,14 +2,14 @@
2 2
 /**
3 3
  * Base for base models
4 4
  */
5
-namespace AnimeClient;
5
+namespace AnimeClient\Base;
6 6
 
7 7
 use abeautifulsite\SimpleImage;
8 8
 
9 9
 /**
10 10
  * Common base for all Models
11 11
  */
12
-class BaseModel {
12
+class Model {
13 13
 
14 14
 	/**
15 15
 	 * The global configuration object

app/base/Router.php → app/Base/Router.php View File

@@ -3,7 +3,7 @@
3 3
  * Routing logic
4 4
  */
5 5
 
6
-namespace AnimeClient;
6
+namespace AnimeClient\Base;
7 7
 
8 8
 /**
9 9
  * Basic routing/ dispatch
@@ -68,9 +68,9 @@ class Router {
68 68
 		$route_path = str_replace([$this->config->anime_path, $this->config->manga_path], '', $raw_route);
69 69
 		$route_path = "/" . trim($route_path, '/');
70 70
 
71
-		$defaultHandler->addDataTable('Route Info', [
71
+		/*$defaultHandler->addDataTable('Route Info', [
72 72
 			'route_path' => $route_path
73
-		]);
73
+		]);*/
74 74
 
75 75
 		$route = $this->router->match($route_path, $_SERVER);
76 76
 
@@ -107,15 +107,6 @@ class Router {
107 107
 		{
108 108
 			$failure = $this->router->getFailedRoute();
109 109
 			$defaultHandler->addDataTable('failed_route', (array)$failure);
110
-
111
-			/*$controller_name = '\\AnimeClient\\BaseController';
112
-			$action_method = 'outputHTML';
113
-			$params = [
114
-				'template' => '404',
115
-				'data' => [
116
-					'title' => 'Page Not Found'
117
-				]
118
-			];*/
119 110
 		}
120 111
 		else
121 112
 		{
@@ -194,8 +185,8 @@ class Router {
194 185
 	public function _setup_routes()
195 186
 	{
196 187
 		$route_map = [
197
-			'anime' => '\\AnimeClient\\AnimeController',
198
-			'manga' => '\\AnimeClient\\MangaController',
188
+			'anime' => '\\AnimeClient\\Controller\\Anime',
189
+			'manga' => '\\AnimeClient\\Controller\\Manga',
199 190
 		];
200 191
 
201 192
 		$output_routes = [];

app/base/functions.php → app/Base/functions.php View File


app/base/pre_conf_functions.php → app/Base/pre_conf_functions.php View File

@@ -26,18 +26,13 @@ function _setup_autoloaders()
26 26
 	require _dir(ROOT_DIR, '/vendor/autoload.php');
27 27
 	spl_autoload_register(function ($class) {
28 28
 		$class_parts = explode('\\', $class);
29
-		$class = end($class_parts);
29
+		array_shift($class_parts);
30
+		$ns_path = APP_DIR . '/' . implode('/', $class_parts) . ".php";
30 31
 
31
-		$dirs = ["base", "controllers", "models"];
32
-
33
-		foreach($dirs as $dir)
32
+		if (file_exists($ns_path))
34 33
 		{
35
-			$file = _dir(APP_DIR, $dir, "{$class}.php");
36
-			if (file_exists($file))
37
-			{
38
-				require_once $file;
39
-				return;
40
-			}
34
+			require_once($ns_path);
35
+			return;
41 36
 		}
42 37
 	});
43 38
 }

app/controllers/AnimeController.php → app/Controller/Anime.php View File

@@ -3,12 +3,17 @@
3 3
  * Anime Controller
4 4
  */
5 5
 
6
-namespace AnimeClient;
6
+namespace AnimeClient\Controller;
7
+
8
+use AnimeClient\Base\Controller as BaseController;
9
+use AnimeClient\Base\Config;
10
+use AnimeClient\Model\Anime as AnimeModel;
11
+use AnimeClient\Model\AnimeCollection as AnimeCollectionModel;
7 12
 
8 13
 /**
9 14
  * Controller for Anime-related pages
10 15
  */
11
-class AnimeController extends BaseController {
16
+class Anime extends BaseController {
12 17
 
13 18
 	/**
14 19
 	 * The anime list model

app/controllers/MangaController.php → app/Controller/Manga.php View File

@@ -2,12 +2,16 @@
2 2
 /**
3 3
  * Manga Controller
4 4
  */
5
-namespace AnimeClient;
5
+namespace AnimeClient\Controller;
6
+
7
+use AnimeClient\Base\Controller;
8
+use AnimeClient\Base\Config;
9
+use AnimeClient\Model\Manga as MangaModel;
6 10
 
7 11
 /**
8 12
  * Controller for manga list
9 13
  */
10
-class MangaController extends BaseController {
14
+class Manga extends Controller {
11 15
 
12 16
 	/**
13 17
 	 * The manga model

app/models/AnimeModel.php → app/Model/Anime.php View File

@@ -3,12 +3,15 @@
3 3
  * Anime API Model
4 4
  */
5 5
 
6
-namespace AnimeClient;
6
+namespace AnimeClient\Model;
7
+
8
+use AnimeClient\Base\ApiModel;
9
+use AnimeClient\Base\Config;
7 10
 
8 11
 /**
9 12
  * Model for handling requests dealing with the anime list
10 13
  */
11
-class AnimeModel extends BaseApiModel {
14
+class Anime extends ApiModel {
12 15
 	/**
13 16
 	 * The base url for api requests
14 17
 	 * @var string $base_url

app/models/AnimeCollectionModel.php → app/Model/AnimeCollection.php View File

@@ -3,12 +3,16 @@
3 3
  * Anime Collection DB Model
4 4
  */
5 5
 
6
-namespace AnimeClient;
6
+namespace AnimeClient\Model;
7
+
8
+use AnimeClient\Base\DBModel;
9
+use AnimeClient\Base\Config;
10
+use AnimeClient\Model\Anime as AnimeModel;
7 11
 
8 12
 /**
9 13
  * Model for getting anime collection data
10 14
  */
11
-class AnimeCollectionModel extends BaseDBModel {
15
+class AnimeCollection extends DBModel {
12 16
 
13 17
 	/**
14 18
 	 * Anime API Model

app/models/MangaModel.php → app/Model/Manga.php View File

@@ -2,12 +2,15 @@
2 2
 /**
3 3
  * Manga API Model
4 4
  */
5
-namespace AnimeClient;
5
+namespace AnimeClient\Model;
6
+
7
+use AnimeClient\Base\ApiModel;
8
+use AnimeClient\Base\Config;
6 9
 
7 10
 /**
8 11
  * Model for handling requests dealing with the manga list
9 12
  */
10
-class MangaModel extends BaseApiModel {
13
+class Manga extends ApiModel {
11 14
 
12 15
 	/**
13 16
 	 * The base url for api requests

+ 20
- 5
app/bootstrap.php View File

@@ -1,4 +1,7 @@
1 1
 <?php
2
+/**
3
+ * Bootstrap / Dependency Injection
4
+ */
2 5
 
3 6
 namespace AnimeClient;
4 7
 
@@ -6,6 +9,15 @@ use \Whoops\Handler\PrettyPageHandler;
6 9
 use \Whoops\Handler\JsonResponseHandler;
7 10
 use \Aura\Web\WebFactory;
8 11
 use \Aura\Router\RouterFactory;
12
+use \Aura\Di\Container as DiContainer;
13
+use \Aura\Di\Factory as DiFactory;
14
+
15
+require _dir(SRC_DIR, '/functions.php');
16
+
17
+// -----------------------------------------------------------------------------
18
+// Setup DI container
19
+// -----------------------------------------------------------------------------
20
+$container = new Base\Container();
9 21
 
10 22
 // -----------------------------------------------------------------------------
11 23
 // Setup error handling
@@ -23,17 +35,20 @@ $whoops->pushHandler($jsonHandler);
23 35
 
24 36
 $whoops->register();
25 37
 
38
+$container->set('error-handler', $defaultHandler);
39
+
26 40
 // -----------------------------------------------------------------------------
27 41
 // Injected Objects
28 42
 // -----------------------------------------------------------------------------
29 43
 
30 44
 // Create Config Object
31
-$config = new Config();
32
-require _dir(BASE_DIR, '/functions.php');
45
+$config = new Base\Config();
46
+$container->set('config', $config);
33 47
 
34 48
 // Create Aura Router Object
35 49
 $router_factory = new RouterFactory();
36 50
 $aura_router = $router_factory->newInstance();
51
+$container->set('aura-router', $aura_router);
37 52
 
38 53
 // Create Request/Response Objects
39 54
 $web_factory = new WebFactory([
@@ -43,13 +58,13 @@ $web_factory = new WebFactory([
43 58
 	'_SERVER' => $_SERVER,
44 59
 	'_FILES' => $_FILES
45 60
 ]);
46
-$request = $web_factory->newRequest();
47
-$response = $web_factory->newResponse();
61
+$container->set('request', $web_factory->newRequest());
62
+$container->set('response', $web_factory->newResponse());
48 63
 
49 64
 // -----------------------------------------------------------------------------
50 65
 // Router
51 66
 // -----------------------------------------------------------------------------
52
-$router = new Router($config, $aura_router, $request, $response);
67
+$router = new Base\Router($container);
53 68
 $router->dispatch();
54 69
 
55 70
 // End of bootstrap.php

+ 31
- 13
app/config/config.php View File

@@ -1,4 +1,4 @@
1
-<?php
1
+ <?php
2 2
 $config = [
3 3
 	// ----------------------------------------------------------------------------
4 4
 	// Username for anime and manga lists
@@ -12,32 +12,50 @@ $config = [
12 12
 	// do you wish to show the anime collection tab?
13 13
 	'show_anime_collection' => TRUE,
14 14
 
15
-	// path to public directory
16
-	'asset_path' => '//' . $_SERVER['HTTP_HOST'] . '/public',
17
-
18 15
 	// path to public directory on the server
19 16
 	'asset_dir' => __DIR__ . '/../../public',
20 17
 
21 18
 	// ----------------------------------------------------------------------------
22 19
 	// Routing
23
-	//
24
-	// Route by path, or route by domain. To route by path, set the _host suffixed
25
-	// options to an empty string, and set 'route_by' to 'path'. To route by host, set
26
-	// the _path suffixed options to an empty string, and set 'route_by' to 'host'.
27 20
 	// ----------------------------------------------------------------------------
28 21
 
29
-	'route_by' => 'path', // host or path
30
-	'anime_host' => '',
31
-	'manga_host' => '',
22
+	'routing' => [
23
+		// Subfolder prefix for url
24
+		'subfolder_prefix' => '',
25
+
26
+		// Path to public directory, where images/css/javascript are located,
27
+		// appended to the url
28
+		'asset_path' => '/public',
29
+
30
+		// Url paths to each content type
31
+		'anime_path' => 'anime',
32
+		'manga_path' => 'manga',
33
+		'collection_path' => 'collection',
34
+		'stats_path' => 'stats',
35
+
36
+		// Which list should be the default?
37
+		'default_list' => 'anime', // anime or manga
38
+
39
+		// Default pages for anime/manga
40
+		'default_anime_path' => "/anime/watching",
41
+		'default_manga_path' => '/manga/all',
42
+
43
+		// Default to list view?
44
+		'default_to_list_view' => FALSE,
45
+	],
46
+
47
+	// Url paths to each
32 48
 	'anime_path' => 'anime',
33 49
 	'manga_path' => 'manga',
50
+	'collection_path' => 'collection',
51
+	'stats_path' => 'stats',
34 52
 
35 53
 	// Which list should be the default?
36 54
 	'default_list' => 'anime', // anime or manga
37 55
 
38 56
 	// Default pages for anime/manga
39
-	'default_anime_path' => '/watching',
40
-	'default_manga_path' => '/all',
57
+	'default_anime_path' => "/anime/watching",
58
+	'default_manga_path' => '/manga/all',
41 59
 
42 60
 	// Default to list view?
43 61
 	'default_to_list_view' => FALSE,

+ 50
- 43
app/config/routes.php View File

@@ -22,6 +22,43 @@ return [
22 22
 			'path' => '/logout',
23 23
 			'action' => ['logout']
24 24
 		],
25
+	],
26
+	// Routes on collection controller
27
+	'collection' => [
28
+		'collection_add_form' => [
29
+			'path' => '/collection/add',
30
+			'action' => ['form'],
31
+			'params' => [],
32
+		],
33
+		'collection_edit_form' => [
34
+			'path' => '/collection/edit/{id}',
35
+			'action' => ['form'],
36
+			'tokens' => [
37
+				'id' => '[0-9]+'
38
+			]
39
+		],
40
+		'collection_add' => [
41
+			'path' => '/collection/add',
42
+			'action' => ['add'],
43
+			'verb' => 'post'
44
+		],
45
+		'collection_edit' => [
46
+			'path' => '/collection/edit',
47
+			'action' => ['edit'],
48
+			'verb' => 'post'
49
+		],
50
+		'collection' => [
51
+			'path' => '/collection/view{/view}',
52
+			'action' => ['index'],
53
+			'params' => [],
54
+			'tokens' => [
55
+				'view' => '[a-z_]+'
56
+			]
57
+		],
58
+	],
59
+	// Routes on stats controller
60
+	'stats' => [
61
+
25 62
 	],
26 63
 	// Routes on anime controller
27 64
 	'anime' => [
@@ -34,11 +71,11 @@ return [
34 71
 			]
35 72
 		],
36 73
 		'search' => [
37
-			'path' => '/search',
74
+			'path' => '/anime/search',
38 75
 			'action' => ['search'],
39 76
 		],
40 77
 		'all' => [
41
-			'path' => '/all{/view}',
78
+			'path' => '/anime/all{/view}',
42 79
 			'action' => ['anime_list'],
43 80
 			'params' => [
44 81
 				'type' => 'all',
@@ -49,7 +86,7 @@ return [
49 86
 			]
50 87
 		],
51 88
 		'watching' => [
52
-			'path' => '/watching{/view}',
89
+			'path' => '/anime/watching{/view}',
53 90
 			'action' => ['anime_list'],
54 91
 			'params' => [
55 92
 				'type' => 'currently-watching',
@@ -60,7 +97,7 @@ return [
60 97
 			]
61 98
 		],
62 99
 		'plan_to_watch' => [
63
-			'path' => '/plan_to_watch{/view}',
100
+			'path' => '/anime/plan_to_watch{/view}',
64 101
 			'action' => ['anime_list'],
65 102
 			'params' => [
66 103
 				'type' => 'plan-to-watch',
@@ -71,7 +108,7 @@ return [
71 108
 			]
72 109
 		],
73 110
 		'on_hold' => [
74
-			'path' => '/on_hold{/view}',
111
+			'path' => '/anime/on_hold{/view}',
75 112
 			'action' => ['anime_list'],
76 113
 			'params' => [
77 114
 				'type' => 'on-hold',
@@ -82,7 +119,7 @@ return [
82 119
 			]
83 120
 		],
84 121
 		'dropped' => [
85
-			'path' => '/dropped{/view}',
122
+			'path' => '/anime/dropped{/view}',
86 123
 			'action' => ['anime_list'],
87 124
 			'params' => [
88 125
 				'type' => 'dropped',
@@ -93,7 +130,7 @@ return [
93 130
 			]
94 131
 		],
95 132
 		'completed' => [
96
-			'path' => '/completed{/view}',
133
+			'path' => '/anime/completed{/view}',
97 134
 			'action' => ['anime_list'],
98 135
 			'params' => [
99 136
 				'type' => 'completed',
@@ -103,36 +140,6 @@ return [
103 140
 				'view' => '[a-z_]+'
104 141
 			]
105 142
 		],
106
-		'collection_add_form' => [
107
-			'path' => '/collection/add',
108
-			'action' => ['collection_form'],
109
-			'params' => [],
110
-		],
111
-		'collection_edit_form' => [
112
-			'path' => '/collection/edit/{id}',
113
-			'action' => ['collection_form'],
114
-			'tokens' => [
115
-				'id' => '[0-9]+'
116
-			]
117
-		],
118
-		'collection_add' => [
119
-			'path' => '/collection/add',
120
-			'action' => ['collection_add'],
121
-			'verb' => 'post'
122
-		],
123
-		'collection_edit' => [
124
-			'path' => '/collection/edit',
125
-			'action' => ['collection_edit'],
126
-			'verb' => 'post'
127
-		],
128
-		'collection' => [
129
-			'path' => '/collection/view{/view}',
130
-			'action' => ['collection'],
131
-			'params' => [],
132
-			'tokens' => [
133
-				'view' => '[a-z_]+'
134
-			]
135
-		],
136 143
 	],
137 144
 	'manga' => [
138 145
 		'index' => [
@@ -145,7 +152,7 @@ return [
145 152
 			]
146 153
 		],
147 154
 		'all' => [
148
-			'path' => '/all{/view}',
155
+			'path' => '/manga/all{/view}',
149 156
 			'action' => ['manga_list'],
150 157
 			'params' => [
151 158
 				'type' => 'all',
@@ -156,7 +163,7 @@ return [
156 163
 			]
157 164
 		],
158 165
 		'reading' => [
159
-			'path' => '/reading{/view}',
166
+			'path' => '/manga/reading{/view}',
160 167
 			'action' => ['manga_list'],
161 168
 			'params' => [
162 169
 				'type' => 'Reading',
@@ -167,7 +174,7 @@ return [
167 174
 			]
168 175
 		],
169 176
 		'plan_to_read' => [
170
-			'path' => '/plan_to_read{/view}',
177
+			'path' => '/manga/plan_to_read{/view}',
171 178
 			'action' => ['manga_list'],
172 179
 			'params' => [
173 180
 				'type' => 'Plan to Read',
@@ -178,7 +185,7 @@ return [
178 185
 			]
179 186
 		],
180 187
 		'on_hold' => [
181
-			'path' => '/on_hold{/view}',
188
+			'path' => '/manga/on_hold{/view}',
182 189
 			'action' => ['manga_list'],
183 190
 			'params' => [
184 191
 				'type' => 'On Hold',
@@ -189,7 +196,7 @@ return [
189 196
 			]
190 197
 		],
191 198
 		'dropped' => [
192
-			'path' => '/dropped{/view}',
199
+			'path' => '/manga/dropped{/view}',
193 200
 			'action' => ['manga_list'],
194 201
 			'params' => [
195 202
 				'type' => 'Dropped',
@@ -200,7 +207,7 @@ return [
200 207
 			]
201 208
 		],
202 209
 		'completed' => [
203
-			'path' => '/completed{/view}',
210
+			'path' => '/manga/completed{/view}',
204 211
 			'action' => ['manga_list'],
205 212
 			'params' => [
206 213
 				'type' => 'Completed',

+ 10
- 4
composer.json View File

@@ -1,11 +1,17 @@
1 1
 {
2
+	"name": "timw4mail/hummingbird-anime-client",
3
+	"description": "A self-hosted anime/manga client for hummingbird.",
4
+	"license":"MIT",
2 5
 	"require": {
3 6
 		"guzzlehttp/guzzle": "5.3.*",
4
-		"filp/whoops": "1.1.*",
7
+		"filp/whoops": "dev-php7#fe32a402b086b21360e82013e8a0355575c7c6f4",
5 8
 		"aura/router": "2.2.*",
6 9
 		"aura/web": "2.0.*",
7
-		"aviat4ion/query": "2.0.*",
8
-		"robmorgan/phinx": "*",
9
-		"abeautifulsite/simpleimage": "*"
10
+		"aura/html": "2.*",
11
+		"aura/session": "2.*",
12
+		"aviat4ion/query": "2.5.*",
13
+		"robmorgan/phinx": "0.4.*",
14
+		"abeautifulsite/simpleimage": "2.5.*",
15
+		"szymach/c-pchart": "1.*"
10 16
 	}
11 17
 }

+ 35
- 4
index.php View File

@@ -3,8 +3,6 @@
3 3
  * Here begins everything!
4 4
  */
5 5
 
6
-namespace AnimeClient;
7
-
8 6
 // -----------------------------------------------------------------------------
9 7
 // ! Start config
10 8
 // -----------------------------------------------------------------------------
@@ -30,9 +28,42 @@ if ($timezone === '' || $timezone === FALSE)
30 28
 // Define base directories
31 29
 define('ROOT_DIR', __DIR__);
32 30
 define('APP_DIR', ROOT_DIR . DIRECTORY_SEPARATOR . 'app');
31
+define('SRC_DIR', ROOT_DIR . DIRECTORY_SEPARATOR . 'src');
33 32
 define('CONF_DIR', APP_DIR . DIRECTORY_SEPARATOR . 'config');
34
-define('BASE_DIR', APP_DIR . DIRECTORY_SEPARATOR . 'base');
35
-require BASE_DIR . DIRECTORY_SEPARATOR . 'pre_conf_functions.php';
33
+define('BASE_DIR', SRC_DIR . DIRECTORY_SEPARATOR . 'Base');
34
+
35
+/**
36
+ * Joins paths together. Variadic to take an
37
+ * arbitrary number of arguments
38
+ *
39
+ * @return string
40
+ */
41
+function _dir()
42
+{
43
+	return implode(DIRECTORY_SEPARATOR, func_get_args());
44
+}
45
+
46
+/**
47
+ * Set up autoloaders
48
+ *
49
+ * @codeCoverageIgnore
50
+ * @return void
51
+ */
52
+function _setup_autoloaders()
53
+{
54
+	require _dir(ROOT_DIR, '/vendor/autoload.php');
55
+	spl_autoload_register(function ($class) {
56
+		$class_parts = explode('\\', $class);
57
+		array_shift($class_parts);
58
+		$ns_path = SRC_DIR . '/' . implode('/', $class_parts) . ".php";
59
+
60
+		if (file_exists($ns_path))
61
+		{
62
+			require_once($ns_path);
63
+			return;
64
+		}
65
+	});
66
+}
36 67
 
37 68
 // Setup autoloaders
38 69
 _setup_autoloaders();

+ 2
- 8
phpdoc.dist.xml View File

@@ -11,13 +11,7 @@
11 11
         <template name="clean" />
12 12
     </transformations>
13 13
 	<files>
14
-		<directory>.</directory>
15
-		<directory>app</directory>
16
-		<ignore>public/*</ignore>
17
-		<ignore>app/views/*</ignore>
18
-		<ignore>app/config/*</ignore>
19
-		<ignore>migrations/*</ignore>
20
-		<ignore>tests/*</ignore>
21
-		<ignore>vendor/*</ignore>
14
+		<directory>src</directory>
15
+		<ignore>src/views/*</ignore>
22 16
 	</files>
23 17
 </phpdoc>

+ 7
- 4
phpunit.xml View File

@@ -5,15 +5,18 @@
5 5
 	bootstrap="tests/bootstrap.php">
6 6
 	<filter>
7 7
 		<whitelist>
8
-			<directory suffix=".php">app/base</directory>
9
-			<directory suffix=".php">app/controllers</directory>
10
-			<directory suffix=".php">app/models</directory>
8
+			<directory suffix=".php">src/Base</directory>
9
+			<directory suffix=".php">src/Controller</directory>
10
+			<directory suffix=".php">src/Model</directory>
11 11
 		</whitelist>
12 12
 	</filter>
13 13
 	<testsuites>
14 14
 		<testsuite name="BaseTests">
15
-			<directory>tests/base</directory>
15
+			<directory>tests</directory>
16
+			<directory>tests/Base</directory>
16 17
 		</testsuite>
18
+		<testsuite name="ModelTests"><directory>tests/Model</directory></testsuite>
19
+		<testsuite name="ControllerTests"><directory>tests/Controller</directory></testsuite>
17 20
 	</testsuites>
18 21
 	<php>
19 22
 		<server name="HTTP_USER_AGENT" value="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:38.0) Gecko/20100101 Firefox/38.0" />

+ 164
- 0
src/Base/Config.php View File

@@ -0,0 +1,164 @@
1
+<?php
2
+/**
3
+ * Base Configuration class
4
+ */
5
+
6
+namespace AnimeClient\Base;
7
+
8
+/**
9
+ * Wrapper for configuration values
10
+ */
11
+class Config {
12
+
13
+	/**
14
+	 * Config object
15
+	 *
16
+	 * @var array
17
+	 */
18
+	protected $config = [];
19
+
20
+	/**
21
+	 * Constructor
22
+	 *
23
+	 * @param array $config_files
24
+	 */
25
+	public function __construct(Array $config_files=[])
26
+	{
27
+		// @codeCoverageIgnoreStart
28
+		if (empty($config_files))
29
+		{
30
+			require_once \_dir(CONF_DIR, 'config.php'); // $config
31
+			require_once \_dir(CONF_DIR, 'base_config.php'); // $base_config
32
+
33
+			$this->config = array_merge($config, $base_config);
34
+		}
35
+		else // @codeCoverageIgnoreEnd
36
+		{
37
+			$this->config = $config_files;
38
+		}
39
+	}
40
+
41
+	/**
42
+	 * Getter for config values
43
+	 *
44
+	 * @param string $key
45
+	 * @return mixed
46
+	 */
47
+	public function __get($key)
48
+	{
49
+		if (isset($this->config[$key]))
50
+		{
51
+			return $this->config[$key];
52
+		}
53
+
54
+		return NULL;
55
+	}
56
+
57
+	/**
58
+	 * Get the base url for css/js/images
59
+	 *
60
+	 * @return string
61
+	 */
62
+	public function asset_url(/*...*/)
63
+	{
64
+		$args = func_get_args();
65
+		$base_url = rtrim($this->url(""), '/');
66
+
67
+		$routing_config = $this->__get("routing");
68
+
69
+
70
+		$base_url = "{$base_url}" . $routing_config['asset_path'];
71
+
72
+		array_unshift($args, $base_url);
73
+
74
+		return implode("/", $args);
75
+	}
76
+
77
+	/**
78
+	 * Get the base url from the config
79
+	 *
80
+	 * @param string $type - (optional) The controller
81
+	 * @return string
82
+	 */
83
+	public function base_url($type="anime")
84
+	{
85
+		$config_path = trim($this->__get("{$type}_path"), "/");
86
+
87
+		// Set the appropriate HTTP host
88
+		$host = $_SERVER['HTTP_HOST'];
89
+		$path = ($config_path !== '') ? $config_path : "";
90
+
91
+		return implode("/", ['/', $host, $path]);
92
+	}
93
+
94
+	/**
95
+	 * Generate a proper url from the path
96
+	 *
97
+	 * @param string $path
98
+	 * @return string
99
+	 */
100
+	public function url($path)
101
+	{
102
+		$path = trim($path, '/');
103
+
104
+		// Remove any optional parameters from the route
105
+		$path = preg_replace('`{/.*?}`i', '', $path);
106
+
107
+		// Set the appropriate HTTP host
108
+		$host = $_SERVER['HTTP_HOST'];
109
+
110
+		return "//{$host}/{$path}";
111
+	}
112
+
113
+	public function default_url($type)
114
+	{
115
+		$type = trim($type);
116
+		$default_path = $this->__get("default_{$type}_path");
117
+
118
+		if ( ! is_null($default_path))
119
+		{
120
+			return $this->url($default_path);
121
+		}
122
+
123
+		return "";
124
+	}
125
+
126
+	/**
127
+	 * Generate full url path from the route path based on config
128
+	 *
129
+	 * @param string $path - (optional) The route path
130
+	 * @param string $type - (optional) The controller (anime or manga), defaults to anime
131
+	 * @return string
132
+	 */
133
+	public function full_url($path="", $type="anime")
134
+	{
135
+		$config_path = trim($this->__get("{$type}_path"), "/");
136
+		$config_default_route = $this->__get("default_{$type}_path");
137
+
138
+		// Remove beginning/trailing slashes
139
+		$config_path = trim($config_path, '/');
140
+		$path = trim($path, '/');
141
+
142
+		// Remove any optional parameters from the route
143
+		$path = preg_replace('`{/.*?}`i', '', $path);
144
+
145
+		// Set the appropriate HTTP host
146
+		$host = $_SERVER['HTTP_HOST'];
147
+
148
+		// Set the default view
149
+		if ($path === '')
150
+		{
151
+			$path .= trim($config_default_route, '/');
152
+			if ($this->__get('default_to_list_view')) $path .= '/list';
153
+		}
154
+
155
+		// Set an leading folder
156
+		/*if ($config_path !== '')
157
+		{
158
+			$path = "{$config_path}/{$path}";
159
+		}*/
160
+
161
+		return "//{$host}/{$path}";
162
+	}
163
+}
164
+// End of config.php

+ 50
- 0
src/Base/Container.php View File

@@ -0,0 +1,50 @@
1
+<?php
2
+
3
+namespace Animeclient\Base;
4
+
5
+/**
6
+ * Wrapper of Aura container to be in the anime client namespace
7
+ */
8
+class Container {
9
+
10
+	/**
11
+	 * @var array
12
+	 */
13
+	protected $container = [];
14
+
15
+	/**
16
+	 * Constructor
17
+	 */
18
+	public function __construct(array $values = [])
19
+	{
20
+		$this->container = $values;
21
+	}
22
+
23
+	/**
24
+	 * Get a value
25
+	 *
26
+	 * @param string $key
27
+	 * @retun mixed
28
+	 */
29
+	public function get($key)
30
+	{
31
+		if (array_key_exists($key, $this->container))
32
+		{
33
+			return $this->container[$key];
34
+		}
35
+	}
36
+
37
+	/**
38
+	 * Add a value to the container
39
+	 *
40
+	 * @param string $key
41
+	 * @param mixed $value
42
+	 * @return Container
43
+	 */
44
+	public function set($key, $value)
45
+	{
46
+		$this->container[$key] = $value;
47
+		return $this;
48
+	}
49
+}
50
+// End of Container.php

+ 283
- 0
src/Base/Controller.php View File

@@ -0,0 +1,283 @@
1
+<?php
2
+/**
3
+ * Base Controller
4
+ */
5
+namespace AnimeClient\Base;
6
+
7
+/**
8
+ * Base class for controllers, defines output methods
9
+ */
10
+class Controller {
11
+
12
+	/**
13
+	 * The global configuration object
14
+	 * @var object $config
15
+	 */
16
+	protected $config;
17
+
18
+	/**
19
+	 * Request object
20
+	 * @var object $request
21
+	 */
22
+	protected $request;
23
+
24
+	/**
25
+	 * Response object
26
+	 * @var object $response
27
+	 */
28
+	protected $response;
29
+
30
+	/**
31
+	 * The api model for the current controller
32
+	 * @var object
33
+	 */
34
+	protected $model;
35
+
36
+	/**
37
+	 * Common data to be sent to views
38
+	 * @var array
39
+	 */
40
+	protected $base_data = [
41
+		'url_type' => 'anime',
42
+		'other_type' => 'manga',
43
+		'nav_routes' => []
44
+	];
45
+
46
+	/**
47
+	 * Constructor
48
+	 *
49
+	 * @param Config $config
50
+	 * @param array $web
51
+	 */
52
+	public function __construct(Container $container)
53
+	{
54
+		$this->config = $container->get('config');
55
+		$this->base_data['config'] = $this->config;
56
+
57
+		$this->request = $container->get('request');
58
+		$this->response = $container->get('response');
59
+	}
60
+
61
+	/**
62
+	 * Destructor
63
+	 *
64
+	 * @codeCoverageIgnore
65
+	 */
66
+	public function __destruct()
67
+	{
68
+		$this->output();
69
+	}
70
+
71
+	/**
72
+	 * Get a class member
73
+	 *
74
+	 * @param string $key
75
+	 * @return object
76
+	 */
77
+	public function __get($key)
78
+	{
79
+		$allowed = ['request', 'response', 'config'];
80
+
81
+		if (in_array($key, $allowed))
82
+		{
83
+			return $this->$key;
84
+		}
85
+
86
+		return NULL;
87
+	}
88
+
89
+	/**
90
+	 * Get the string output of a partial template
91
+	 *
92
+	 * @codeCoverageIgnore
93
+	 * @param string $template
94
+	 * @param array|object $data
95
+	 * @return string
96
+	 */
97
+	public function load_partial($template, $data=[])
98
+	{
99
+		if (isset($this->base_data))
100
+		{
101
+			$data = array_merge($this->base_data, $data);
102
+		}
103
+
104
+		global $router, $defaultHandler;
105
+		$route = $router->get_route();
106
+		$data['route_path'] = ($route) ? $router->get_route()->path : "";
107
+
108
+		$defaultHandler->addDataTable('Template Data', $data);
109
+
110
+		$template_path = _dir(SRC_DIR, 'views', "{$template}.php");
111
+
112
+		if ( ! is_file($template_path))
113
+		{
114
+			throw new \InvalidArgumentException("Invalid template : {$path}");
115
+		}
116
+
117
+		ob_start();
118
+		extract($data);
119
+		include _dir(SRC_DIR, 'views', 'header.php');
120
+		include $template_path;
121
+		include _dir(SRC_DIR, 'views', 'footer.php');
122
+		$buffer = ob_get_contents();
123
+		ob_end_clean();
124
+
125
+		return $buffer;
126
+	}
127
+
128
+	/**
129
+	 * Output a template to HTML, using the provided data
130
+	 *
131
+	 * @codeCoverageIgnore
132
+	 * @param string $template
133
+	 * @param array|object $data
134
+	 * @return void
135
+	 */
136
+	public function outputHTML($template, $data=[])
137
+	{
138
+		$buffer = $this->load_partial($template, $data);
139
+
140
+		$this->response->content->setType('text/html');
141
+		$this->response->content->set($buffer);
142
+	}
143
+
144
+	/**
145
+	 * Output json with the proper content type
146
+	 *
147
+	 * @param mixed $data
148
+	 * @return void
149
+	 */
150
+	public function outputJSON($data)
151
+	{
152
+		if ( ! is_string($data))
153
+		{
154
+			$data = json_encode($data);
155
+		}
156
+
157
+		$this->response->content->setType('application/json');
158
+		$this->response->content->set($data);
159
+	}
160
+
161
+	/**
162
+	 * Redirect to the selected page
163
+	 *
164
+	 * @codeCoverageIgnore
165
+	 * @param string $url
166
+	 * @param int $code
167
+	 * @param string $type
168
+	 * @return void
169
+	 */
170
+	public function redirect($url, $code, $type="anime")
171
+	{
172
+		$url = $this->config->full_url($url, $type);
173
+
174
+		$this->response->redirect->to($url, $code);
175
+	}
176
+
177
+	/**
178
+	 * Add a message box to the page
179
+	 *
180
+	 * @codeCoverageIgnore
181
+	 * @param string $type
182
+	 * @param string $message
183
+	 * @return string
184
+	 */
185
+	public function show_message($type, $message)
186
+	{
187
+		return $this->load_partial('message', [
188
+			'stat_class' => $type,
189
+			'message'  => $message
190
+		]);
191
+	}
192
+
193
+	/**
194
+	 * Clear the api session
195
+	 *
196
+	 * @codeCoverageIgnore
197
+	 * @return void
198
+	 */
199
+	public function logout()
200
+	{
201
+		session_destroy();
202
+		$this->response->redirect->seeOther($this->config->full_url(''));
203
+	}
204
+
205
+	/**
206
+	 * Show the login form
207
+	 *
208
+	 * @codeCoverageIgnore
209
+	 * @param string $status
210
+	 * @return void
211
+	 */
212
+	public function login($status="")
213
+	{
214
+		$message = "";
215
+
216
+		if ($status != "")
217
+		{
218
+			$message = $this->show_message('error', $status);
219
+		}
220
+
221
+		$this->outputHTML('login', [
222
+			'title' => 'Api login',
223
+			'message' => $message
224
+		]);
225
+	}
226
+
227
+	/**
228
+	 * Attempt to log in with the api
229
+	 *
230
+	 * @return void
231
+	 */
232
+	public function login_action()
233
+	{
234
+		if (
235
+			$this->model->authenticate(
236
+				$this->config->hummingbird_username,
237
+				$this->request->post->get('password')
238
+			)
239
+		)
240
+		{
241
+			$this->response->redirect->afterPost($this->config->full_url('', $this->base_data['url_type']));
242
+			return;
243
+		}
244
+
245
+		$this->login("Invalid username or password.");
246
+	}
247
+
248
+	/**
249
+	 * Send the appropriate response
250
+	 *
251
+	 * @codeCoverageIgnore
252
+	 * @return void
253
+	 */
254
+	private function output()
255
+	{
256
+		// send status
257
+		@header($this->response->status->get(), true, $this->response->status->getCode());
258
+
259
+		// headers
260
+		foreach($this->response->headers->get() as $label => $value)
261
+		{
262
+			@header("{$label}: {$value}");
263
+		}
264
+
265
+		// cookies
266
+		foreach($this->response->cookies->get() as $name => $cookie)
267
+		{
268
+			@setcookie(
269
+				$name,
270
+				$cookie['value'],
271
+				$cookie['expire'],
272
+				$cookie['path'],
273
+				$cookie['domain'],
274
+				$cookie['secure'],
275
+				$cookie['httponly']
276
+			);
277
+		}
278
+
279
+		// send the actual response
280
+		echo $this->response->content->get();
281
+	}
282
+}
283
+// End of BaseController.php

+ 112
- 0
src/Base/Model.php View File

@@ -0,0 +1,112 @@
1
+<?php
2
+/**
3
+ * Base for base models
4
+ */
5
+namespace AnimeClient\Base;
6
+
7
+use abeautifulsite\SimpleImage;
8
+
9
+/**
10
+ * Common base for all Models
11
+ */
12
+class Model {
13
+
14
+	/**
15
+	 * The global configuration object
16
+	 * @var Config
17
+	 */
18
+	protected $config;
19
+
20
+	/**
21
+	 * The container object
22
+	 * @var Container
23
+	 */
24
+	protected $container;
25
+
26
+	/**
27
+	 * Constructor
28
+	 */
29
+	public function __construct(Container $container)
30
+	{
31
+		$this->container = $container;
32
+		$this->config = $container->get('config');
33
+	}
34
+
35
+	/**
36
+	 * Get the path of the cached version of the image. Create the cached image
37
+	 * if the file does not already exist
38
+	 *
39
+	 * @codeCoverageIgnore
40
+	 * @param string $api_path - The original image url
41
+	 * @param string $series_slug - The part of the url with the series name, becomes the image name
42
+	 * @param string $type - Anime or Manga, controls cache path
43
+	 * @return string - the frontend path for the cached image
44
+	 */
45
+	public function get_cached_image($api_path, $series_slug, $type="anime")
46
+	{
47
+		$api_path = str_replace("jjpg", "jpg", $api_path);
48
+		$path_parts = explode('?', basename($api_path));
49
+		$path = current($path_parts);
50
+		$ext_parts = explode('.', $path);
51
+		$ext = end($ext_parts);
52
+
53
+		// Workaround for some broken extensions
54
+		if ($ext == "jjpg") $ext = "jpg";
55
+
56
+		// Failsafe for weird urls
57
+		if (strlen($ext) > 3) return $api_path;
58
+
59
+		$cached_image = "{$series_slug}.{$ext}";
60
+		$cached_path = "{$this->config->img_cache_path}/{$type}/{$cached_image}";
61
+
62
+		// Cache the file if it doesn't already exist
63
+		if ( ! file_exists($cached_path))
64
+		{
65
+			if (ini_get('allow_url_fopen'))
66
+			{
67
+				copy($api_path, $cached_path);
68
+			}
69
+			elseif (function_exists('curl_init'))
70
+			{
71
+				$ch = curl_init($api_path);
72
+				$fp = fopen($cached_path, 'wb');
73
+				curl_setopt_array($ch, [
74
+					CURLOPT_FILE => $fp,
75
+					CURLOPT_HEADER => 0
76
+				]);
77
+				curl_exec($ch);
78
+				curl_close($ch);
79
+				fclose($ch);
80
+			}
81
+			else
82
+			{
83
+				throw new DomainException("Couldn't cache images because they couldn't be downloaded.");
84
+			}
85
+
86
+			// Resize the image
87
+			if ($type == 'anime')
88
+			{
89
+				$resize_width = 220;
90
+				$resize_height = 319;
91
+				$this->_resize($cached_path, $resize_width, $resize_height);
92
+			}
93
+		}
94
+
95
+		return "/public/images/{$type}/{$cached_image}";
96
+	}
97
+
98
+	/**
99
+	 * Resize an image
100
+	 *
101
+	 * @codeCoverageIgnore
102
+	 * @param string $path
103
+	 * @param string $width
104
+	 * @param string $height
105
+	 */
106
+	private function _resize($path, $width, $height)
107
+	{
108
+		$img = new SimpleImage($path);
109
+		$img->resize($width,$height)->save();
110
+	}
111
+}
112
+// End of BaseModel.php

+ 81
- 0
src/Base/Model/API.php View File

@@ -0,0 +1,81 @@
1
+<?php
2
+/**
3
+ * Base API Model
4
+ */
5
+namespace AnimeClient\Base\Model;
6
+
7
+use \GuzzleHttp\Client;
8
+use \GuzzleHttp\Cookie\CookieJar;
9
+use \AnimeClient\Base\Container;
10
+
11
+/**
12
+ * Base model for api interaction
13
+ */
14
+class API extends \AnimeClient\Base\Model {
15
+
16
+	/**
17
+	 * Base url for making api requests
18
+	 * @var string
19
+	 */
20
+	protected $base_url = '';
21
+
22
+	/**
23
+	 * The Guzzle http client object
24
+	 * @var object
25
+	 */
26
+	protected $client;
27
+
28
+	/**
29
+	 * Cookie jar object for api requests
30
+	 * @var object
31
+	 */
32
+	protected $cookieJar;
33
+
34
+	/**
35
+	 * Constructor
36
+	 */
37
+	public function __construct(Container $container)
38
+	{
39
+		parent::__construct($container);
40
+		$this->cookieJar = new CookieJar();
41
+		$this->client = new Client([
42
+			'base_url' => $this->base_url,
43
+			'defaults' => [
44
+				'cookies' => $this->cookieJar,
45
+				'headers' => [
46
+					'User-Agent' => $_SERVER['HTTP_USER_AGENT'],
47
+					'Accept-Encoding' => 'application/json'
48
+				],
49
+				'timeout' => 5,
50
+				'connect_timeout' => 5
51
+			]
52
+		]);
53
+	}
54
+
55
+	/**
56
+	 * Attempt login via the api
57
+	 *
58
+	 * @codeCoverageIgnore
59
+	 * @param string $username
60
+	 * @param string $password
61
+	 * @return bool
62
+	 */
63
+	public function authenticate($username, $password)
64
+	{
65
+		$result = $this->client->post('https://hummingbird.me/api/v1/users/authenticate', [
66
+			'body' => [
67
+				'username' => $username,
68
+				'password' => $password
69
+			]
70
+		]);
71
+
72
+		if ($result->getStatusCode() === 201)
73
+		{
74
+			$_SESSION['hummingbird_anime_token'] = $result->json();
75
+			return TRUE;
76
+		}
77
+
78
+		return FALSE;
79
+	}
80
+}
81
+// End of BaseApiModel.php

+ 34
- 0
src/Base/Model/DB.php View File

@@ -0,0 +1,34 @@
1
+<?php
2
+/**
3
+ * Base DB model
4
+ */
5
+namespace AnimeClient\Base\Model;
6
+
7
+use AnimeClient\Base\Container;
8
+
9
+/**
10
+ * Base model for database interaction
11
+ */
12
+class DB extends \AnimeClient\Base\Model {
13
+	/**
14
+	 * The query builder object
15
+	 * @var object $db
16
+	 */
17
+	protected $db;
18
+
19
+	/**
20
+	 * The database connection information array
21
+	 * @var array $db_config
22
+	 */
23
+	protected $db_config;
24
+
25
+	/**
26
+	 * Constructor
27
+	 */
28
+	public function __construct(Container $container)
29
+	{
30
+		parent::__construct($container);
31
+		$this->db_config = $this->config->database;
32
+	}
33
+}
34
+// End of BaseDBModel.php

+ 230
- 0
src/Base/Router.php View File

@@ -0,0 +1,230 @@
1
+<?php
2
+/**
3
+ * Routing logic
4
+ */
5
+namespace AnimeClient\Base;
6
+
7
+use \Aura\Web\Request;
8
+use \Aura\Web\Response;
9
+
10
+/**
11
+ * Basic routing/ dispatch
12
+ */
13
+class Router {
14
+
15
+	/**
16
+	 * The route-matching object
17
+	 * @var object $router
18
+	 */
19
+	protected $router;
20
+
21
+	/**
22
+	 * The global configuration object
23
+	 * @var object $config
24
+	 */
25
+	protected $config;
26
+
27
+	/**
28
+	 * Class wrapper for input superglobals
29
+	 * @var object
30
+	 */
31
+	protected $request;
32
+
33
+	/**
34
+	 * Array containing request and response objects
35
+	 * @var array $web
36
+	 */
37
+	protected $web;
38
+
39
+	/**
40
+	 * Routes added to router
41
+	 * @var array $output_routes
42
+	 */
43
+	protected $output_routes;
44
+
45
+	/**
46
+	 * Injection Container
47
+	 * @var Container $container
48
+	 */
49
+	protected $container;
50
+
51
+	/**
52
+	 * Constructor
53
+	 *
54
+	 * @param Config $config
55
+	 * @param Router $router
56
+	 * @param Request $request
57
+	 * @param Response $response
58
+	 */
59
+	public function __construct(Container $container)
60
+	{
61
+		$this->config = $container->get('config');
62
+		$this->router = $container->get('aura-router');
63
+		$this->request = $container->get('request');
64
+		$this->web = [$this->request, $container->get('response')];
65
+
66
+		$this->output_routes = $this->_setup_routes();
67
+
68
+		$this->container = $container;
69
+	}
70
+
71
+	/**
72
+	 * Get the current route object, if one matches
73
+	 *
74
+	 * @return object
75
+	 */
76
+	public function get_route()
77
+	{
78
+		$error_handler = $this->container->get('error-handler');
79
+
80
+		$raw_route = $this->request->server->get('PATH_INFO');
81
+		$route_path = "/" . trim($raw_route, '/');
82
+
83
+		$error_handler->addDataTable('Route Info', [
84
+			'route_path' => $route_path
85
+		]);
86
+
87
+		$route = $this->router->match($route_path, $_SERVER);
88
+
89
+		return $route;
90
+	}
91
+
92
+	/**
93
+	 * Get list of routes applied
94
+	 *
95
+	 * @return array
96
+	 */
97
+	public function get_output_routes()
98
+	{
99
+		return $this->output_routes;
100
+	}
101
+
102
+	/**
103
+	 * Handle the current route
104
+	 *
105
+	 * @codeCoverageIgnore
106
+	 * @param [object] $route
107
+	 * @return void
108
+	 */
109
+	public function dispatch($route = NULL)
110
+	{
111
+		$error_handler = $this->container->get('error-handler');
112
+
113
+		if (is_null($route))
114
+		{
115
+			$route = $this->get_route();
116
+			$error_handler->addDataTable('route_args', (array)$route);
117
+		}
118
+
119
+		if ( ! $route)
120
+		{
121
+			$failure = $this->router->getFailedRoute();
122
+			$error_handler->addDataTable('failed_route', (array)$failure);
123
+		}
124
+		else
125
+		{
126
+			list($controller_name, $action_method) = $route->params['action'];
127
+			$params = (isset($route->params['params'])) ? $route->params['params'] : [];
128
+
129
+			if ( ! empty($route->tokens))
130
+			{
131
+				foreach($route->tokens as $key => $v)
132
+				{
133
+					if (array_key_exists($key, $route->params))
134
+					{
135
+						$params[$key] = $route->params[$key];
136
+					}
137
+				}
138
+			}
139
+		}
140
+
141
+		$controller = new $controller_name($this->container);
142
+
143
+		// Run the appropriate controller method
144
+
145
+		$error_handler->addDataTable('controller_args', $params);
146
+		call_user_func_array([$controller, $action_method], $params);
147
+	}
148
+
149
+	/**
150
+	 * Get the type of route, to select the current controller
151
+	 *
152
+	 * @return string
153
+	 */
154
+	public function get_controller()
155
+	{
156
+		$route_type = $this->config->default_list;
157
+
158
+		$host = $this->request->server->get("HTTP_HOST");
159
+		$request_uri = $this->request->server->get('PATH_INFO');
160
+
161
+		$path = trim($request_uri, '/');
162
+
163
+		$route_type_map = [
164
+			$this->config->anime_path => 'anime',
165
+			$this->config->manga_path => 'manga',
166
+			$this->config->collection_path => 'collection',
167
+			$this->config->stats_path => 'stats'
168
+		];
169
+
170
+		$segments = explode('/', $path);
171
+		$controller = array_shift($segments);
172
+
173
+		if (array_key_exists($controller, array_keys($route_type_map)))
174
+		{
175
+			return $route_type_map[$controller];
176
+		}
177
+
178
+		return $route_type;
179
+	}
180
+
181
+	/**
182
+	 * Select controller based on the current url, and apply its relevent routes
183
+	 *
184
+	 * @return array
185
+	 */
186
+	public function _setup_routes()
187
+	{
188
+		$output_routes = [];
189
+
190
+		$route_type = $this->get_controller();
191
+
192
+		// Return early if invalid route array
193
+		if ( ! array_key_exists($route_type, $this->config->routes)) return [];
194
+
195
+		$applied_routes = array_merge($this->config->routes[$route_type], $this->config->routes['common']);
196
+
197
+		// Add routes
198
+		foreach($applied_routes as $name => &$route)
199
+		{
200
+			$path = $route['path'];
201
+			unset($route['path']);
202
+
203
+			$controller_class = '\\AnimeClient\\Controller\\' . ucfirst($route_type);
204
+
205
+			// Prepend the controller to the route parameters
206
+			array_unshift($route['action'], $controller_class);
207
+
208
+			// Select the appropriate router method based on the http verb
209
+			$add = (array_key_exists('verb', $route)) ? "add" . ucfirst(strtolower($route['verb'])) : "addGet";
210
+
211
+			// Add the route to the router object
212
+			if ( ! array_key_exists('tokens', $route))
213
+			{
214
+				$output_routes[] = $this->router->$add($name, $path)->addValues($route);
215
+			}
216
+			else
217
+			{
218
+				$tokens = $route['tokens'];
219
+				unset($route['tokens']);
220
+
221
+				$output_routes[] = $this->router->$add($name, $path)
222
+					->addValues($route)
223
+					->addTokens($tokens);
224
+			}
225
+		}
226
+
227
+		return $output_routes;
228
+	}
229
+}
230
+// End of Router.php

+ 149
- 0
src/Base/UrlGenerator.php View File

@@ -0,0 +1,149 @@
1
+<?php
2
+
3
+namespace AnimeClient\Base;
4
+
5
+/**
6
+ * UrlGenerator class.
7
+ */
8
+class UrlGenerator {
9
+
10
+	/**
11
+	 * Config Object
12
+	 * @var Config
13
+	 */
14
+	protected $config;
15
+
16
+	/**
17
+	 * Constructor
18
+	 *
19
+	 * @param Container $container
20
+	 */
21
+	public function __construct(Container $container)
22
+	{
23
+		$this->config = $container->get('config');
24
+	}
25
+
26
+	/**
27
+	 * Retreive the appropriate value for the routing key
28
+	 *
29
+	 * @param string $key
30
+	 * @return mixed
31
+	 */
32
+	protected function __get($key)
33
+	{
34
+		$routing_config = $this->config->__get('routing');
35
+
36
+		if (array_key_exists($key, $routing_config))
37
+		{
38
+			return $routing_config[$key];
39
+		}
40
+	}
41
+
42
+	public function __invoke()
43
+	{
44
+		$args = func_get_args();
45
+	}
46
+
47
+	/**
48
+	 * Get the base url for css/js/images
49
+	 *
50
+	 * @return string
51
+	 */
52
+	public function asset_url(/*...*/)
53
+	{
54
+		$args = func_get_args();
55
+		$base_url = rtrim($this->__get('asset_path'), '/');
56
+
57
+		array_unshift($args, $base_url);
58
+
59
+		return implode("/", $args);
60
+	}
61
+
62
+	/**
63
+	 * Get the base url from the config
64
+	 *
65
+	 * @param string $type - (optional) The controller
66
+	 * @return string
67
+	 */
68
+	public function base_url($type="anime")
69
+	{
70
+		$config_path = trim($this->__get("{$type}_path"), "/");
71
+
72
+		// Set the appropriate HTTP host
73
+		$host = $_SERVER['HTTP_HOST'];
74
+		$path = ($config_path !== '') ? $config_path : "";
75
+
76
+		return implode("/", ['/', $host, $path]);
77
+	}
78
+
79
+	/**
80
+	 * Generate a proper url from the path
81
+	 *
82
+	 * @param string $path
83
+	 * @return string
84
+	 */
85
+	public function url($path)
86
+	{
87
+		$path = trim($path, '/');
88
+
89
+		// Remove any optional parameters from the route
90
+		$path = preg_replace('`{/.*?}`i', '', $path);
91
+
92
+		// Set the appropriate HTTP host
93
+		$host = $_SERVER['HTTP_HOST'];
94
+
95
+		return "//{$host}/{$path}";
96
+	}
97
+
98
+	public function default_url($type)
99
+	{
100
+		$type = trim($type);
101
+		$default_path = $this->__get("default_{$type}_path");
102
+
103
+		if ( ! is_null($default_path))
104
+		{
105
+			return $this->url($default_path);
106
+		}
107
+
108
+		return "";
109
+	}
110
+
111
+	/**
112
+	 * Generate full url path from the route path based on config
113
+	 *
114
+	 * @param string $path - (optional) The route path
115
+	 * @param string $type - (optional) The controller (anime or manga), defaults to anime
116
+	 * @return string
117
+	 */
118
+	public function full_url($path="", $type="anime")
119
+	{
120
+		$config_path = trim($this->__get("{$type}_path"), "/");
121
+		$config_default_route = $this->__get("default_{$type}_path");
122
+
123
+		// Remove beginning/trailing slashes
124
+		$config_path = trim($config_path, '/');
125
+		$path = trim($path, '/');
126
+
127
+		// Remove any optional parameters from the route
128
+		$path = preg_replace('`{/.*?}`i', '', $path);
129
+
130
+		// Set the appropriate HTTP host
131
+		$host = $_SERVER['HTTP_HOST'];
132
+
133
+		// Set the default view
134
+		if ($path === '')
135
+		{
136
+			$path .= trim($config_default_route, '/');
137
+			if ($this->__get('default_to_list_view')) $path .= '/list';
138
+		}
139
+
140
+		// Set an leading folder
141
+		/*if ($config_path !== '')
142
+		{
143
+			$path = "{$config_path}/{$path}";
144
+		}*/
145
+
146
+		return "//{$host}/{$path}";
147
+	}
148
+}
149
+// End of UrlGenerator.php

+ 197
- 0
src/Controller/Anime.php View File

@@ -0,0 +1,197 @@
1
+<?php
2
+/**
3
+ * Anime Controller
4
+ */
5
+
6
+namespace AnimeClient\Controller;
7
+
8
+use AnimeClient\Base\Container;
9
+use AnimeClient\Base\Controller as BaseController;
10
+use AnimeClient\Base\Config;
11
+use AnimeClient\Model\Anime as AnimeModel;
12
+use AnimeClient\Model\AnimeCollection as AnimeCollectionModel;
13
+
14
+/**
15
+ * Controller for Anime-related pages
16
+ */
17
+class Anime extends BaseController {
18
+
19
+	/**
20
+	 * The anime list model
21
+	 * @var object $model
22
+	 */
23
+	protected $model;
24
+
25
+	/**
26
+	 * The anime collection model
27
+	 * @var object $collection_model
28
+	 */
29
+	private $collection_model;
30
+
31
+	/**
32
+	 * Data to ve sent to all routes in this controller
33
+	 * @var array $base_data
34
+	 */
35
+	protected $base_data;
36
+
37
+	/**
38
+	 * Route mapping for main navigation
39
+	 * @var array $nav_routes
40
+	 */
41
+	private $nav_routes = [
42
+		'Watching' => '/anime/watching{/view}',
43
+		'Plan to Watch' => '/anime/plan_to_watch{/view}',
44
+		'On Hold' => '/anime/on_hold{/view}',
45
+		'Dropped' => '/anime/dropped{/view}',
46
+		'Completed' => '/anime/completed{/view}',
47
+		'Collection' => '/collection/view{/view}',
48
+		'All' => '/anime/all{/view}'
49
+	];
50
+
51
+	/**
52
+	 * Constructor
53
+	 */
54
+	public function __construct(Container $container)
55
+	{
56
+		parent::__construct($container);
57
+
58
+		$config = $container->get('config');
59
+
60
+		if ($this->config->show_anime_collection === FALSE)
61
+		{
62
+			unset($this->nav_routes['Collection']);
63
+		}
64
+
65
+		$this->model = new AnimeModel($container);
66
+		$this->collection_model = new AnimeCollectionModel($container);
67
+		$this->base_data = [
68
+			'message' => '',
69
+			'url_type' => 'anime',
70
+			'other_type' => 'manga',
71
+			'nav_routes' => $this->nav_routes,
72
+			'config' => $this->config,
73
+		];
74
+	}
75
+
76
+	/**
77
+	 * Search for anime
78
+	 *
79
+	 * @return void
80
+	 */
81
+	public function search()
82
+	{
83
+		$query = $this->request->query->get('query');
84
+		$this->outputJSON($this->model->search($query));
85
+	}
86
+
87
+	/**
88
+	 * Show a portion, or all of the anime list
89
+	 *
90
+	 * @param string $type - The section of the list
91
+	 * @param string $title - The title of the page
92
+	 * @return void
93
+	 */
94
+	public function anime_list($type, $title, $view)
95
+	{
96
+		$view_map = [
97
+			'' => 'cover',
98
+			'list' => 'list'
99
+		];
100
+
101
+		$data = ($type != 'all')
102
+			? $this->model->get_list($type)
103
+			: $this->model->get_all_lists();
104
+
105
+		$this->outputHTML('anime/' . $view_map[$view], [
106
+			'title' => $title,
107
+			'sections' => $data
108
+		]);
109
+	}
110
+
111
+	/**
112
+	 * Show the anime collection page
113
+	 *
114
+	 * @return void
115
+	 */
116
+	public function collection($view)
117
+	{
118
+		$view_map = [
119
+			'' => 'collection',
120
+			'list' => 'collection_list'
121
+		];
122
+
123
+		$data = $this->collection_model->get_collection();
124
+
125
+		$this->outputHTML('anime/' . $view_map[$view], [
126
+			'title' => WHOSE . " Anime Collection",
127
+			'sections' => $data,
128
+			'genres' => $this->collection_model->get_genre_list()
129
+		]);
130
+	}
131
+
132
+	/**
133
+	 * Show the anime collection add/edit form
134
+	 *
135
+	 * @param int $id
136
+	 * @return void
137
+	 */
138
+	public function collection_form($id=NULL)
139
+	{
140
+		$action = (is_null($id)) ? "Add" : "Edit";
141
+
142
+		$this->outputHTML('anime/collection_' . strtolower($action), [
143
+			'action' => $action,
144
+			'action_url' => $this->config->full_url("collection/" . strtolower($action)),
145
+			'title' => WHOSE . " Anime Collection &middot; {$action}",
146
+			'media_items' => $this->collection_model->get_media_type_list(),
147
+			'item' => ($action === "Edit") ? $this->collection_model->get($id) : []
148
+		]);
149
+	}
150
+
151
+	/**
152
+	 * Update a collection item
153
+	 *
154
+	 * @return void
155
+	 */
156
+	public function collection_edit()
157
+	{
158
+		$data = $this->request->post->get();
159
+		if ( ! array_key_exists('hummingbird_id', $data))
160
+		{
161
+			$this->redirect("collection/view", 303, "anime");
162
+		}
163
+
164
+		$this->collection_model->update($data);
165
+
166
+		$this->redirect("collection/view", 303, "anime");
167
+	}
168
+
169
+	/**
170
+	 * Add a collection item
171
+	 *
172
+	 * @return void
173
+	 */
174
+	public function collection_add()
175
+	{
176
+		$data = $this->request->post->get();
177
+		if ( ! array_key_exists('id', $data))
178
+		{
179
+			$this->redirect("collection/view", 303, "anime");
180
+		}
181
+
182
+		$this->collection_model->add($data);
183
+
184
+		$this->redirect("collection/view", 303, "anime");
185
+	}
186
+
187
+	/**
188
+	 * Update an anime item
189
+	 *
190
+	 * @return bool
191
+	 */
192
+	public function update()
193
+	{
194
+		$this->outputJSON($this->model->update($this->request->post->get()));
195
+	}
196
+}
197
+// End of AnimeController.php

+ 154
- 0
src/Controller/Collection.php View File

@@ -0,0 +1,154 @@
1
+<?php
2
+/**
3
+ * Anime Collection Controller
4
+ */
5
+
6
+namespace AnimeClient\Controller;
7
+
8
+use AnimeClient\Base\Container;
9
+use AnimeClient\Base\Controller as BaseController;
10
+use AnimeClient\Base\Config;
11
+use AnimeClient\Model\Anime as AnimeModel;
12
+use AnimeClient\Model\AnimeCollection as AnimeCollectionModel;
13
+
14
+/**
15
+ * Controller for Anime collection pages
16
+ */
17
+class Collection extends BaseController {
18
+
19
+	/**
20
+	 * The anime collection model
21
+	 * @var object $collection_model
22
+	 */
23
+	private $collection_model;
24
+
25
+	/**
26
+	 * Data to ve sent to all routes in this controller
27
+	 * @var array $base_data
28
+	 */
29
+	protected $base_data;
30
+
31
+	/**
32
+	 * Route mapping for main navigation
33
+	 * @var array $nav_routes
34
+	 */
35
+	private $nav_routes = [
36
+		'Watching' => '/anime/watching{/view}',
37
+		'Plan to Watch' => '/anime/plan_to_watch{/view}',
38
+		'On Hold' => '/anime/on_hold{/view}',
39
+		'Dropped' => '/anime/dropped{/view}',
40
+		'Completed' => '/anime/completed{/view}',
41
+		'Collection' => '/collection/view{/view}',
42
+		'All' => '/anime/all{/view}'
43
+	];
44
+
45
+	/**
46
+	 * Constructor
47
+	 */
48
+	public function __construct(Container $container)
49
+	{
50
+		parent::__construct($container);
51
+
52
+		if ($this->config->show_anime_collection === FALSE)
53
+		{
54
+			unset($this->nav_routes['Collection']);
55
+		}
56
+
57
+		$this->collection_model = new AnimeCollectionModel($this->config);
58
+		$this->base_data = [
59
+			'message' => '',
60
+			'url_type' => 'anime',
61
+			'other_type' => 'manga',
62
+			'nav_routes' => $this->nav_routes,
63
+			'config' => $this->config,
64
+		];
65
+	}
66
+
67
+	/**
68
+	 * Search for anime
69
+	 *
70
+	 * @return void
71
+	 */
72
+	public function search()
73
+	{
74
+		$query = $this->request->query->get('query');
75
+		$this->outputJSON($this->model->search($query));
76
+	}
77
+
78
+	/**
79
+	 * Show the anime collection page
80
+	 *
81
+	 * @return void
82
+	 */
83
+	public function index($view)
84
+	{
85
+		$view_map = [
86
+			'' => 'cover',
87
+			'list' => 'list'
88
+		];
89
+
90
+		$data = $this->collection_model->get_collection();
91
+
92
+		$this->outputHTML('collection/' . $view_map[$view], [
93
+			'title' => WHOSE . " Anime Collection",
94
+			'sections' => $data,
95
+			'genres' => $this->collection_model->get_genre_list()
96
+		]);
97
+	}
98
+
99
+	/**
100
+	 * Show the anime collection add/edit form
101
+	 *
102
+	 * @param int $id
103
+	 * @return void
104
+	 */
105
+	public function form($id=NULL)
106
+	{
107
+		$action = (is_null($id)) ? "Add" : "Edit";
108
+
109
+		$this->outputHTML('collection/'. strtolower($action), [
110
+			'action' => $action,
111
+			'action_url' => $this->config->full_url("collection/" . strtolower($action)),
112
+			'title' => WHOSE . " Anime Collection &middot; {$action}",
113
+			'media_items' => $this->collection_model->get_media_type_list(),
114
+			'item' => ($action === "Edit") ? $this->collection_model->get($id) : []
115
+		]);
116
+	}
117
+
118
+	/**
119
+	 * Update a collection item
120
+	 *
121
+	 * @return void
122
+	 */
123
+	public function edit()
124
+	{
125
+		$data = $this->request->post->get();
126
+		if ( ! array_key_exists('hummingbird_id', $data))
127
+		{
128
+			$this->redirect("collection/view", 303, "anime");
129
+		}
130
+
131
+		$this->collection_model->update($data);
132
+
133
+		$this->redirect("collection/view", 303, "anime");
134
+	}
135
+
136
+	/**
137
+	 * Add a collection item
138
+	 *
139
+	 * @return void
140
+	 */
141
+	public function add()
142
+	{
143
+		$data = $this->request->post->get();
144
+		if ( ! array_key_exists('id', $data))
145
+		{
146
+			$this->redirect("collection/view", 303, "anime");
147
+		}
148
+
149
+		$this->collection_model->add($data);
150
+
151
+		$this->redirect("collection/view", 303, "anime");
152
+	}
153
+}
154
+// End of CollectionController.php

+ 94
- 0
src/Controller/Manga.php View File

@@ -0,0 +1,94 @@
1
+<?php
2
+/**
3
+ * Manga Controller
4
+ */
5
+namespace AnimeClient\Controller;
6
+
7
+use AnimeClient\Base\Container;
8
+use AnimeClient\Base\Controller;
9
+use AnimeClient\Base\Config;
10
+use AnimeClient\Model\Manga as MangaModel;
11
+
12
+/**
13
+ * Controller for manga list
14
+ */
15
+class Manga extends Controller {
16
+
17
+	/**
18
+	 * The manga model
19
+	 * @var object $model
20
+	 */
21
+	protected $model;
22
+
23
+	/**
24
+	 * Data to ve sent to all routes in this controller
25
+	 * @var array $base_data
26
+	 */
27
+	protected $base_data;
28
+
29
+
30
+	/**
31
+	 * Route mapping for main navigation
32
+	 * @var array $nav_routes
33
+	 */
34
+	private $nav_routes = [
35
+		'Reading' => '/manga/reading{/view}',
36
+		'Plan to Read' => '/manga/plan_to_read{/view}',
37
+		'On Hold' => '/manga/on_hold{/view}',
38
+		'Dropped' => '/manga/dropped{/view}',
39
+		'Completed' => '/manga/completed{/view}',
40
+		'All' => '/manga/all{/view}'
41
+	];
42
+
43
+	/**
44
+	 * Constructor
45
+	 */
46
+	public function __construct(Container $container)
47
+	{
48
+		parent::__construct($container);
49
+		$config = $container->get('config');
50
+		$this->model = new MangaModel($container);
51
+		$this->base_data = [
52
+			'config' => $this->config,
53
+			'url_type' => 'manga',
54
+			'other_type' => 'anime',
55
+			'nav_routes' => $this->nav_routes
56
+		];
57
+	}
58
+
59
+	/**
60
+	 * Update an anime item
61
+	 *
62
+	 * @return bool
63
+	 */
64
+	public function update()
65
+	{
66
+		$this->outputJSON($this->model->update($this->request->post->get()));
67
+	}
68
+
69
+	/**
70
+	 * Get a section of the manga list
71
+	 *
72
+	 * @param string $status
73
+	 * @param string $title
74
+	 * @param string $view
75
+	 * @return void
76
+	 */
77
+	public function manga_list($status, $title, $view)
78
+	{
79
+		$view_map = [
80
+			'' => 'cover',
81
+			'list' => 'list'
82
+		];
83
+
84
+		$data = ($status !== 'all')
85
+			? [$status => $this->model->get_list($status)]
86
+			: $this->model->get_all_lists();
87
+
88
+		$this->outputHTML('manga/' . $view_map[$view], [
89
+			'title' => $title,
90
+			'sections' => $data
91
+		]);
92
+	}
93
+}
94
+// End of MangaController.php

+ 11
- 0
src/Controller/Stats.php View File

@@ -0,0 +1,11 @@
1
+<?php
2
+
3
+namespace AnimeClient\Controller;
4
+
5
+use AnimeClient\Base\Container;
6
+use AnimeClient\Base\Controller;
7
+
8
+class Stats extends Controller {
9
+
10
+}
11
+// End of Stats.php

+ 239
- 0
src/Model/Anime.php View File

@@ -0,0 +1,239 @@
1
+<?php
2
+/**
3
+ * Anime API Model
4
+ */
5
+
6
+namespace AnimeClient\Model;
7
+
8
+use AnimeClient\Base\Model\API;
9
+
10
+/**
11
+ * Model for handling requests dealing with the anime list
12
+ */
13
+class Anime extends API {
14
+	/**
15
+	 * The base url for api requests
16
+	 * @var string $base_url
17
+	 */
18
+	protected $base_url = "https://hummingbird.me/api/v1/";
19
+
20
+	/**
21
+	 * Update the selected anime
22
+	 *
23
+	 * @param array $data
24
+	 * @return array
25
+	 */
26
+	public function update($data)
27
+	{
28
+		$data['auth_token'] = $_SESSION['hummingbird_anime_token'];
29
+
30
+		$result = $this->client->post("libraries/{$data['id']}", [
31
+			'body' => $data
32
+		]);
33
+
34
+		return $result->json();
35
+	}
36
+
37
+	/**
38
+	 * Get the full set of anime lists
39
+	 *
40
+	 * @return array
41
+	 */
42
+	public function get_all_lists()
43
+	{
44
+		$output = [
45
+			'Watching' => [],
46
+			'Plan to Watch' => [],
47
+			'On Hold' => [],
48
+			'Dropped' => [],
49
+			'Completed' => [],
50
+		];
51
+
52
+		$data = $this->_get_list();
53
+
54
+		foreach($data as $datum)
55
+		{
56
+			switch($datum['status'])
57
+			{
58
+				case "completed":
59
+					$output['Completed'][] = $datum;
60
+				break;
61
+
62
+				case "plan-to-watch":
63
+					$output['Plan to Watch'][] = $datum;
64
+				break;
65
+
66
+				case "dropped":
67
+					$output['Dropped'][] = $datum;
68
+				break;
69
+
70
+				case "on-hold":
71
+					$output['On Hold'][] = $datum;
72
+				break;
73
+
74
+				case "currently-watching":
75
+					$output['Watching'][] = $datum;
76
+				break;
77
+			}
78
+		}
79
+
80
+		// Sort anime by name
81
+		foreach($output as &$status_list)
82
+		{
83
+			$this->sort_by_name($status_list);
84
+		}
85
+
86
+		return $output;
87
+	}
88
+
89
+	/**
90
+	 * Get a category out of the full list
91
+	 *
92
+	 * @param string $status
93
+	 * @return array
94
+	 */
95
+	public function get_list($status)
96
+	{
97
+		$map = [
98
+			'currently-watching' => 'Watching',
99
+			'plan-to-watch' => 'Plan to Watch',
100
+			'on-hold' => 'On Hold',
101
+			'dropped' => 'Dropped',
102
+			'completed' => 'Completed',
103
+		];
104
+
105
+		$data = $this->_get_list($status);
106
+		$this->sort_by_name($data);
107
+
108
+		$output = [];
109
+		$output[$map[$status]] = $data;
110
+
111
+		return $output;
112
+	}
113
+
114
+	/**
115
+	 * Get information about an anime from its id
116
+	 *
117
+	 * @param string $anime_id
118
+	 * @return array
119
+	 */
120
+	public function get_anime($anime_id)
121
+	{
122
+		$config = [
123
+			'query' => [
124
+				'id' => $anime_id
125
+			]
126
+		];
127
+
128
+		$response = $this->client->get("anime/{$anime_id}", $config);
129
+
130
+		return $response->json();
131
+	}
132
+
133
+	/**
134
+	 * Search for anime by name
135
+	 *
136
+	 * @param string $name
137
+	 * @return array
138
+	 */
139
+	public function search($name)
140
+	{
141
+		global $defaultHandler;
142
+
143
+		$config = [
144
+			'query' => [
145
+				'query' => $name
146
+			]
147
+		];
148
+
149
+		$response = $this->client->get('search/anime', $config);
150
+		$defaultHandler->addDataTable('anime_search_response', (array)$response);
151
+
152
+		if ($response->getStatusCode() != 200)
153
+		{
154
+			throw new RuntimeException($response->getEffectiveUrl());
155
+		}
156
+
157
+		return $response->json();
158
+	}
159
+
160
+	/**
161
+	 * Actually retreive the data from the api
162
+	 *
163
+	 * @param string $status - Status to filter by
164
+	 * @return array
165
+	 */
166
+	private function _get_list($status="all")
167
+	{
168
+		global $defaultHandler;
169
+
170
+		$cache_file = "{$this->config->data_cache_path}/anime-{$status}.json";
171
+
172
+		$config = [
173
+			'allow_redirects' => FALSE
174
+		];
175
+
176
+		if ($status != "all")
177
+		{
178
+			$config['query']['status'] = $status;
179
+		}
180
+
181
+		$response = $this->client->get("users/{$this->config->hummingbird_username}/library", $config);
182
+
183
+		$defaultHandler->addDataTable('anime_list_response', (array)$response);
184
+
185
+		if ($response->getStatusCode() != 200)
186
+		{
187
+			if ( ! file_exists($cache_file))
188
+			{
189
+				throw new DomainException($response->getEffectiveUrl());
190
+			}
191
+			else
192
+			{
193
+				$output = json_decode(file_get_contents($cache_file), TRUE);
194
+			}
195
+		}
196
+		else
197
+		{
198
+			$output = $response->json();
199
+			$output_json = json_encode($output);
200
+
201
+			if (( ! file_exists($cache_file)) || file_get_contents($cache_file) !== $output_json)
202
+			{
203
+				// Attempt to create the cache folder if it doesn't exist
204
+				if ( ! is_dir($this->config->data_cache_path))
205
+				{
206
+					mkdir($this->config->data_cache_path);
207
+				}
208
+				// Cache the call in case of downtime
209
+				file_put_contents($cache_file, json_encode($output));
210
+			}
211
+		}
212
+
213
+		foreach($output as &$row)
214
+		{
215
+			$row['anime']['cover_image'] = $this->get_cached_image($row['anime']['cover_image'], $row['anime']['slug'], 'anime');
216
+		}
217
+
218
+		return $output;
219
+	}
220
+
221
+	/**
222
+	 * Sort the list by title
223
+	 *
224
+	 * @param array $array
225
+	 * @return void
226
+	 */
227
+	private function sort_by_name(&$array)
228
+	{
229
+		$sort = array();
230
+
231
+		foreach($array as $key => $item)
232
+		{
233
+			$sort[$key] = $item['anime']['title'];
234
+		}
235
+
236
+		array_multisort($sort, SORT_ASC, $array);
237
+	}
238
+}
239
+// End of AnimeModel.php

+ 379
- 0
src/Model/AnimeCollection.php View File

@@ -0,0 +1,379 @@
1
+<?php
2
+/**
3
+ * Anime Collection DB Model
4
+ */
5
+
6
+namespace AnimeClient\Model;
7
+
8
+use AnimeClient\Base\Model\DB;
9
+use \AnimeClient\Base\Container;
10
+use AnimeClient\Model\Anime as AnimeModel;
11
+
12
+/**
13
+ * Model for getting anime collection data
14
+ */
15
+class AnimeCollection extends DB {
16
+
17
+	/**
18
+	 * Anime API Model
19
+	 * @var object $anime_model
20
+	 */
21
+	private $anime_model;
22
+
23
+	/**
24
+	 * Whether the database is valid for querying
25
+	 * @var bool
26
+	 */
27
+	private $valid_database = FALSE;
28
+
29
+	/**
30
+	 * Constructor
31
+	 */
32
+	public function __construct(Container $container)
33
+	{
34
+		parent::__construct($container);
35
+
36
+		$this->db = \Query($this->db_config['collection']);
37
+		$this->anime_model = new AnimeModel($container);
38
+
39
+		// Is database valid? If not, set a flag so the
40
+		// app can be run without a valid database
41
+		$db_file_name = $this->db_config['collection']['file'];
42
+		if ($db_file_name !== ':memory:')
43
+		{
44
+			$db_file = file_get_contents($db_file_name);
45
+			$this->valid_database = (strpos($db_file, 'SQLite format 3') === 0);
46
+		}
47
+		else
48
+		{
49
+			$this->valid_database = TRUE;
50
+		}
51
+
52
+		// Do an import if an import file exists
53
+		$this->json_import();
54
+	}
55
+
56
+	/**
57
+	 * Get genres for anime collection items
58
+	 *
59
+	 * @param array $filter
60
+	 * @return array
61
+	 */
62
+	public function get_genre_list($filter=[])
63
+	{
64
+		$this->db->select('hummingbird_id, genre')
65
+			->from('genre_anime_set_link gl')
66
+			->join('genres g', 'g.id=gl.genre_id', 'left');
67
+