diff --git a/app/bootstrap.php b/app/bootstrap.php index a1398ab7..507f3a3b 100644 --- a/app/bootstrap.php +++ b/app/bootstrap.php @@ -153,6 +153,9 @@ return function(array $configArray = []) { $container->set('anime-collection-model', function($container) { return new Model\AnimeCollection($container); }); + $container->set('manga-collection-model', function($container) { + return new Model\MangaCollection($container); + }); // Miscellaneous Classes $container->set('auth', function($container) { diff --git a/app/views/collection/add.php b/app/views/collection/add.php index 9970cf26..91e46d33 100644 --- a/app/views/collection/add.php +++ b/app/views/collection/add.php @@ -1,6 +1,6 @@ isAuthenticated()): ?>
-

Add Anime to your Collection

+

Add to your Collection

- +
@@ -39,5 +39,5 @@
- + \ No newline at end of file diff --git a/app/views/main-menu.php b/app/views/main-menu.php index 8bdc747a..7c424b57 100644 --- a/app/views/main-menu.php +++ b/app/views/main-menu.php @@ -32,6 +32,12 @@ $extraSegment = $lastSegment === 'list' ? '/list' : ''; ) ?>] + get("show_{$other_type}_collection")): ?> + [a( + $url->generate("{$other_type}.collection.view") . $extraSegment, + ucfirst($other_type) . ' Collection' + ) ?>] + [a($urlGenerator->defaultUrl('anime') . $extraSegment, 'Anime List') ?>] [a($urlGenerator->defaultUrl('manga') . $extraSegment, 'Manga List') ?>] diff --git a/migrations/20170914200308_add_manga_collection_tables.php b/migrations/20170914200308_add_manga_collection_tables.php new file mode 100644 index 00000000..45c3863c --- /dev/null +++ b/migrations/20170914200308_add_manga_collection_tables.php @@ -0,0 +1,54 @@ +table('manga_set', ['id' => FALSE, 'primary_key' => ['hummingbird_id']]); + $manga_set->addColumn('hummingbird_id', 'biginteger') + ->addColumn('slug', 'string', ['comment' => "URL slug used for image caching and generating links"]) + ->addColumn('title', 'string') + ->addColumn('alternate_title', 'string', ['null' => TRUE]) + ->addColumn('media_id', 'integer', ['default' => 3, 'null' => TRUE]) + ->addColumn('show_type', 'string', ['default' => 'TV', 'null' => TRUE, 'comment' => "TV Series/OVA/etc"]) + ->addColumn('age_rating', 'string', ['default' => 'PG13', 'null' => TRUE]) + ->addColumn('cover_image', 'string', ['null' => TRUE]) + ->addColumn('episode_count', 'integer', ['null' => TRUE]) + ->addColumn('episode_length', 'integer', ['null' => TRUE]) + ->addColumn('notes', 'text', ['null' => TRUE]) + ->addForeignKey('media_id', 'media', 'id') + ->create(); + + // Create genre_manga_set_link table + $genre_manga_set_link = $this->table('genre_manga_set_link', ['id' => FALSE, 'primary_key' => ['hummingbird_id', 'genre_id']]); + $genre_manga_set_link->addColumn('hummingbird_id', 'biginteger') + ->addColumn('genre_id', 'integer') + ->addForeignKey('hummingbird_id', 'manga_set', 'hummingbird_id') + ->addForeignKey('genre_id', 'genres', 'id') + ->create(); + } +} diff --git a/src/Controller/MangaCollection.php b/src/Controller/MangaCollection.php new file mode 100644 index 00000000..b5bfcc94 --- /dev/null +++ b/src/Controller/MangaCollection.php @@ -0,0 +1,184 @@ + + * @copyright 2015 - 2017 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.0 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Controller; + +use Aviat\AnimeClient\Controller as BaseController; +use Aviat\AnimeClient\Model\{ + manga as mangaModel, + mangaCollection as mangaCollectionModel +}; +use Aviat\AnimeClient\UrlGenerator; +use Aviat\Ion\Di\ContainerInterface; + +/** + * Controller for manga collection pages + */ +class MangaCollection extends BaseController { + + /** + * The manga collection model + * @var mangaCollectionModel $mangaCollectionModel + */ + private $mangaCollectionModel; + + /** + * The manga API model + * @var mangaModel $mangaModel + */ + private $mangaModel; + + /** + * Constructor + * + * @param ContainerInterface $container + */ + public function __construct(ContainerInterface $container) + { + parent::__construct($container); + + $this->mangaModel = $container->get('manga-model'); + $this->mangaCollectionModel = $container->get('manga-collection-model'); + $this->baseData = array_merge($this->baseData, [ + 'collection_type' => 'manga', + 'menu_name' => 'manga-collection', + 'url_type' => 'manga', + 'other_type' => 'anime', + 'config' => $this->config, + ]); + } + + /** + * Search for manga + * + * @return void + */ + public function search() + { + $queryParams = $this->request->getQueryParams(); + $query = $queryParams['query']; + $this->outputJSON($this->mangaModel->search($query)); + } + + /** + * Show the manga collection page + * + * @param string $view + * @return void + */ + public function index($view) + { + $viewMap = [ + '' => 'cover', + 'list' => 'list' + ]; + + $data = $this->mangaCollectionModel->getCollection(); + + $this->outputHTML('collection/' . $viewMap[$view], [ + 'title' => $this->config->get('whose_list') . "'s Manga Collection", + 'sections' => $data, + 'genres' => $this->mangaCollectionModel->getGenreList() + ]); + } + + /** + * Show the manga collection add/edit form + * + * @param integer|null $id + * @return void + */ + public function form($id = NULL) + { + $this->setSessionRedirect(); + + $action = (is_null($id)) ? "Add" : "Edit"; + $urlAction = strtolower($action); + + $this->outputHTML('collection/' . $urlAction, [ + 'action' => $action, + 'action_url' => $this->url->generate("manga.collection.{$urlAction}.post"), + 'title' => $this->formatTitle( + $this->config->get('whose_list') . "'s manga Collection", + $action + ), + 'media_items' => $this->mangaCollectionModel->getMediaTypeList(), + 'item' => ($action === "Edit") ? $this->mangaCollectionModel->get($id) : [] + ]); + } + + /** + * Update a collection item + * + * @return void + */ + public function edit() + { + $data = $this->request->getParsedBody(); + if (array_key_exists('hummingbird_id', $data)) + { + $this->mangaCollectionModel->update($data); + $this->setFlashMessage('Successfully updated collection item.', 'success'); + } + else + { + $this->setFlashMessage('Failed to update collection item', 'error'); + } + + $this->sessionRedirect(); + } + + /** + * Add a collection item + * + * @return void + */ + public function add() + { + $data = $this->request->getParsedBody(); + if (array_key_exists('id', $data)) + { + $this->mangaCollectionModel->add($data); + $this->setFlashMessage('Successfully added collection item', 'success'); + } + else + { + $this->setFlashMessage('Failed to add collection item.', 'error'); + } + + $this->sessionRedirect(); + } + + /** + * Remove a collection item + * + * @return void + */ + public function delete() + { + $data = $this->request->getParsedBody(); + if ( ! array_key_exists('hummingbird_id', $data)) + { + $this->redirect("/manga-collection/view", 303); + } + + $this->mangaCollectionModel->delete($data); + $this->setFlashMessage("Successfully removed manga from collection.", 'success'); + + $this->redirect("/manga-collection/view", 303); + } +} +// End of CollectionController.php \ No newline at end of file diff --git a/src/Model/AnimeCollection.php b/src/Model/AnimeCollection.php index 5d440f80..cf2ce5fb 100644 --- a/src/Model/AnimeCollection.php +++ b/src/Model/AnimeCollection.php @@ -26,6 +26,23 @@ use PDO; */ class AnimeCollection extends Collection { + /** + * Anime API Model + * @var object $animeModel + */ + protected $animeModel; + + /** + * Create the collection model + * + * @param ContainerInterface $container + */ + public function __construct(ContainerInterface $container) + { + parent::__construct($container); + $this->animeModel = $container->get('anime-model'); + } + /** * Get collection from the database, and organize by media type * diff --git a/src/Model/Collection.php b/src/Model/Collection.php index 1a787b4c..50003973 100644 --- a/src/Model/Collection.php +++ b/src/Model/Collection.php @@ -25,14 +25,8 @@ use PDOException; * Base model for anime and manga collections */ class Collection extends DB { - - use ContainerAware; - /** - * Anime API Model - * @var object $animeModel - */ - protected $animeModel; + use ContainerAware; /** * Whether the database is valid for querying @@ -46,7 +40,7 @@ class Collection extends DB { * @param ContainerInterface $container */ public function __construct(ContainerInterface $container) - { + { parent::__construct($container); try @@ -58,7 +52,6 @@ class Collection extends DB { //$this->validDatabase = FALSE; //return FALSE; } - $this->animeModel = $container->get('anime-model'); // Is database valid? If not, set a flag so the // app can be run without a valid database diff --git a/src/Model/MangaCollection.php b/src/Model/MangaCollection.php new file mode 100644 index 00000000..37ae42e6 --- /dev/null +++ b/src/Model/MangaCollection.php @@ -0,0 +1,304 @@ + + * @copyright 2015 - 2017 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 4.0 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Model; + +use Aviat\AnimeClient\API\Kitsu; +use Aviat\Ion\Di\ContainerInterface; +use Aviat\Ion\Json; +use PDO; + +/** + * Model for getting anime collection data + */ +class MangaCollection extends Collection { + + /** + * Manga API Model + * @var object $mangaModel + */ + protected $mangaModel; + + /** + * Create the collection model + * + * @param ContainerInterface $container + */ + public function __construct(ContainerInterface $container) + { + parent::__construct($container); + $this->mangaModel = $container->get('manga-model'); + } + + /** + * Get collection from the database, and organize by media type + * + * @return array + */ + public function getCollection() + { + $rawCollection = $this->getCollectionFromDatabase(); + + $collection = []; + + foreach ($rawCollection as $row) + { + if (array_key_exists($row['media'], $collection)) + { + $collection[$row['media']][] = $row; + } + else + { + $collection[$row['media']] = [$row]; + } + } + + return $collection; + } + + /** + * Get list of media types + * + * @return array + */ + public function getMediaTypeList() + { + $output = []; + + $query = $this->db->select('id, type') + ->from('media') + ->get(); + + foreach ($query->fetchAll(PDO::FETCH_ASSOC) as $row) + { + $output[$row['id']] = $row['type']; + } + + return $output; + } + + /** + * Get item from collection for editing + * + * @param int $id + * @return array + */ + public function getCollectionEntry($id) + { + $query = $this->db->from('anime_set') + ->where('hummingbird_id', (int)$id) + ->get(); + + return $query->fetch(PDO::FETCH_ASSOC); + } + + /** + * Get full collection from the database + * + * @return array + */ + private function getCollectionFromDatabase() + { + if ( ! $this->validDatabase) + { + return []; + } + + $query = $this->db->select('hummingbird_id, slug, title, alternate_title, show_type, + age_rating, episode_count, episode_length, cover_image, notes, media.type as media') + ->from('manga_set a') + ->join('media', 'media.id=a.media_id', 'inner') + ->order_by('media') + ->order_by('title') + ->get(); + + return $query->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Add an item to the anime collection + * + * @param array $data + * @return void + */ + public function add($data) + { + $anime = (object)$this->mangaModel->getMangaById($data['id']); + $this->db->set([ + 'hummingbird_id' => $data['id'], + 'slug' => $anime->slug, + 'title' => array_shift($anime->titles), + 'alternate_title' => implode('
', $anime->titles), + 'show_type' => $anime->show_type, + 'age_rating' => $anime->age_rating, + 'cover_image' => $anime->cover_image, + 'episode_count' => $anime->episode_count, + 'episode_length' => $anime->episode_length, + 'media_id' => $data['media_id'], + 'notes' => $data['notes'] + ])->insert('manga_set'); + + $this->updateGenre($data['id']); + } + + /** + * Update a collection item + * + * @param array $data + * @return void + */ + public function update($data) + { + // If there's no id to update, don't update + if ( ! array_key_exists('hummingbird_id', $data)) + { + return; + } + + $id = $data['hummingbird_id']; + unset($data['hummingbird_id']); + + $this->db->set($data) + ->where('hummingbird_id', $id) + ->update('manga_set'); + } + + /** + * Remove a collection item + * + * @param array $data + * @return void + */ + public function delete($data) + { + // If there's no id to update, don't delete + if ( ! array_key_exists('hummingbird_id', $data)) + { + return; + } + + $this->db->where('hummingbird_id', $data['hummingbird_id']) + ->delete('genre_manga_set_link'); + + $this->db->where('hummingbird_id', $data['hummingbird_id']) + ->delete('manga_set'); + } + + /** + * Get the details of a collection item + * + * @param int $kitsuId + * @return array + */ + public function get($kitsuId) + { + $query = $this->db->from('manga_set') + ->where('hummingbird_id', $kitsuId) + ->get(); + + return $query->fetch(PDO::FETCH_ASSOC); + } + + /** + * Update genre information for selected manga + * + * @param int $mangaId The current manga + * @return void + */ + private function updateGenre($mangaId) + { + $genreInfo = $this->getGenreData(); + extract($genreInfo); + + // Get api information + $manga = $this->mangaModel->getMangaById($mangaId); + + foreach ($anime['genres'] as $genre) + { + // Add genres that don't currently exist + if ( ! in_array($genre, $genres)) + { + $this->db->set('genre', $genre) + ->insert('genres'); + + $genres[] = $genre; + } + + // Update link table + // Get id of genre to put in link table + $flippedGenres = array_flip($genres); + + $insertArray = [ + 'hummingbird_id' => $mangaId, + 'genre_id' => $flippedGenres[$genre] + ]; + + if (array_key_exists($mangaId, $links)) + { + if ( ! in_array($flippedGenres[$genre], $links[$mangaId])) + { + $this->db->set($insertArray)->insert('genre_manga_set_link'); + } + } + else + { + $this->db->set($insertArray)->insert('genre_manga_set_link'); + } + } + } + + /** + * Get list of existing genres + * + * @return array + */ + private function getGenreData() + { + $genres = []; + $links = []; + + // Get existing genres + $query = $this->db->select('id, genre') + ->from('genres') + ->get(); + foreach ($query->fetchAll(PDO::FETCH_ASSOC) as $genre) + { + $genres[$genre['id']] = $genre['genre']; + } + + // Get existing link table entries + $query = $this->db->select('hummingbird_id, genre_id') + ->from('genre_manga_set_link') + ->get(); + foreach ($query->fetchAll(PDO::FETCH_ASSOC) as $link) + { + if (array_key_exists($link['hummingbird_id'], $links)) + { + $links[$link['hummingbird_id']][] = $link['genre_id']; + } + else + { + $links[$link['hummingbird_id']] = [$link['genre_id']]; + } + } + + return [ + 'genres' => $genres, + 'links' => $links + ]; + } +} +// End of MangaCollectionModel.php \ No newline at end of file diff --git a/tests/ControllerTest.php b/tests/ControllerTest.php index 2215b1e2..8333cc62 100644 --- a/tests/ControllerTest.php +++ b/tests/ControllerTest.php @@ -22,7 +22,8 @@ use Aviat\AnimeClient\Controller; use Aviat\AnimeClient\Controller\{ Anime as AnimeController, Character as CharacterController, - Collection as CollectionController, + AnimeCollection as AnimeCollectionController, + MangaCollection as MangaCollectionController, Manga as MangaController }; @@ -71,7 +72,11 @@ class ControllerTest extends AnimeClientTestCase { ); $this->assertInstanceOf( 'Aviat\AnimeClient\Controller', - new CollectionController($this->container) + new AnimeCollectionController($this->container) + ); + $this->assertInstanceOf( + 'Aviat\AnimeClient\Controller', + new MangaCollectionController($this->container) ); } diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index ce1e3f1a..c64ec0f6 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -227,7 +227,8 @@ class DispatcherTest extends AnimeClientTestCase { 'expected' => [ 'anime' => 'Aviat\AnimeClient\Controller\Anime', 'manga' => 'Aviat\AnimeClient\Controller\Manga', - 'collection' => 'Aviat\AnimeClient\Controller\Collection', + 'anime-collection' => 'Aviat\AnimeClient\Controller\AnimeCollection', + 'manga-collection' => 'Aviat\AnimeClient\Controller\MangaCollection', 'character' => 'Aviat\AnimeClient\Controller\Character', 'index' => 'Aviat\AnimeClient\Controller\Index', ] @@ -248,7 +249,8 @@ class DispatcherTest extends AnimeClientTestCase { 'expected' => [ 'anime' => 'Aviat\AnimeClient\Controller\Anime', 'manga' => 'Aviat\AnimeClient\Controller\Manga', - 'collection' => 'Aviat\AnimeClient\Controller\Collection', + 'anime-collection' => 'Aviat\AnimeClient\Controller\AnimeCollection', + 'manga-collection' => 'Aviat\AnimeClient\Controller\MangaCollection', 'character' => 'Aviat\AnimeClient\Controller\Character', 'index' => 'Aviat\AnimeClient\Controller\Index', ]