<commit_before><?php declare(strict_types=1);
/**
 * Hummingbird Anime List Client
 *
 * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
 *
 * PHP version 7
 *
 * @package     HummingbirdAnimeClient
 * @author      <NAME> <<EMAIL>>
 * @copyright   2015 - 2017  <NAME>
 * @license     http://www.opensource.org/licenses/mit-license.html  MIT License
 * @version     4.0
 * @link        https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
 */

namespace Aviat\AnimeClient\Command;

use function Amp\{all, wait};

use Amp\Artax\Client;
use Aviat\AnimeClient\API\{
	JsonAPI,
	ParallelAPIRequest,
	Mapping\AnimeWatchingStatus,
	Mapping\MangaReadingStatus
};
use Aviat\AnimeClient\API\MAL\Transformer\{
	AnimeListTransformer as ALT,
	MangaListTransformer as MLT
};
use Aviat\Ion\Json;

/**
 * Clears the API Cache
 */
class SyncKitsuWithMal extends BaseCommand {

	/**
	 * Model for making requests to Kitsu API
	 * @var \Aviat\AnimeClient\API\Kitsu\Model
	 */
	protected $kitsuModel;

	/**
	 * Model for making requests to MAL API
	 * @var \Aviat\AnimeClient\API\MAL\Model
	 */
	protected $malModel;

	/**
	 * Run the image conversion script
	 *
	 * @param array $args
	 * @param array $options
	 * @return void
	 * @throws \ConsoleKit\ConsoleException
	 */
	public function execute(array $args, array $options = [])
	{
		$this->setContainer($this->setupContainer());
		$this->setCache($this->container->get('cache'));
		$this->kitsuModel = $this->container->get('kitsu-model');
		$this->malModel = $this->container->get('mal-model');

		$this->syncAnime();
		$this->syncManga();
	}

	public function syncAnime()
	{
		$malCount = count($this->malModel->getAnimeList());
		$kitsuCount = $this->kitsuModel->getAnimeListCount();

		$this->echoBox("Number of MAL anime list items: {$malCount}");
		$this->echoBox("Number of Kitsu anime list items: {$kitsuCount}");

		$data = $this->diffAnimeLists();

		$this->echoBox("Number of anime items that need to be added to MAL: " . count($data['addToMAL']));

		if ( ! empty($data['addToMAL']))
		{
			$this->echoBox("Adding missing anime list items to MAL");
			$this->createMALListItems($data['addToMAL'], 'anime');
		}

		$this->echoBox('Number of anime items that need to be added to Kitsu: ' . count($data['addToKitsu']));

		if ( ! empty($data['addToKitsu']))
		{
			$this->echoBox("Adding missing anime list items to Kitsu");
			$this->createKitusListItems($data['addToKitsu'], 'anime');
		}
	}

	public function syncManga()
	{
		$malCount =  count($this->malModel->getMangaList());
		$kitsuCount = $this->kitsuModel->getMangaListCount();

		$this->echoBox("Number of MAL manga list items: {$malCount}");
		$this->echoBox("Number of Kitsu manga list items: {$kitsuCount}");

		$data = $this->diffMangaLists();

		$this->echoBox("Number of manga items that need to be added to MAL: " . count($data['addToMAL']));

		if ( ! empty($data['addToMAL']))
		{
			$this->echoBox("Adding missing manga list items to MAL");
			$this->createMALListItems($data['addToMAL'], 'manga');
		}

		$this->echoBox('Number of manga items that need to be added to Kitsu: ' . count($data['addToKitsu']));

		if ( ! empty($data['addToKitsu']))
		{
			$this->echoBox("Adding missing manga list items to Kitsu");
			$this->createKitsuListItems($data['addToKitsu'], 'manga');
		}
	}

	public function filterMappings(array $includes, string $type = 'anime'): array
	{
		$output = [];

		foreach($includes as $id => $mapping)
		{
			if ($mapping['externalSite'] === "myanimelist/{$type}")
			{
				$output[$id] = $mapping;
			}
		}

		return $output;
	}

	public function formatMALAnimeList()
	{
		$orig = $this->malModel->getAnimeList();
		$output = [];

		foreach($orig as $item)
		{
			$output[$item['series_animedb_id']] = [
				'id' => $item['series_animedb_id'],
				'data' => [
					'status' => AnimeWatchingStatus::MAL_TO_KITSU[$item['my_status']],
					'progress' => $item['my_watched_episodes'],
					'reconsuming' => (bool) $item['my_rewatching'],
					'reconsumeCount' => array_key_exists('times_rewatched', $item)
						? $item['times_rewatched']
						: 0,
					// 'notes' => ,
					'rating' => $item['my_score'] / 2,
					'updatedAt' => (new \DateTime())
						->setTimestamp((int)$item['my_last_updated'])
						->format(\DateTime::W3C),
				]
			];
		}

		return $output;
	}

	public function formatMALMangaList()
	{
		$orig = $this->malModel->getMangaList();
		$output = [];

		foreach($orig as $item)
		{
			$output[$item['series_mangadb_id']] = [
				'id' => $item['series_mangadb_id'],
				'data' => [
					'my_status' => $item['my_status'],
					'status' => MangaReadingStatus::MAL_TO_KITSU[$item['my_status']],
					'progress' => $item['my_read_chapters'],
					'volumes' => $item['my_read_volumes'],
					'reconsuming' => (bool) $item['my_rereadingg'],
					/* 'reconsumeCount' => array_key_exists('times_rewatched', $item)
						? $item['times_rewatched']
						: 0, */
					// 'notes' => ,
					'rating' => $item['my_score'] / 2,
					'updatedAt' => (new \DateTime())
						->setTimestamp((int)$item['my_last_updated'])
						->format(\DateTime::W3C),
				]
			];
		}

		return $output;
	}

	public function filterKitsuAnimeList()
	{
		$data = $this->kitsuModel->getFullAnimeList();
		$includes = JsonAPI::organizeIncludes($data['included']);
		$includes['mappings'] = $this->filterMappings($includes['mappings']);

		$output = [];

		foreach($data['data'] as $listItem)
		{
			$animeId = $listItem['relationships']['anime']['data']['id'];
			$potentialMappings = $includes['anime'][$animeId]['relationships']['mappings'];
			$malId = NULL;

			foreach ($potentialMappings as $mappingId)
			{
				if (array_key_exists($mappingId, $includes['mappings']))
				{
					$malId = $includes['mappings'][$mappingId]['externalId'];
				}
			}

			// Skip to the next item if there isn't a MAL ID
			if (is_null($malId))
			{
				continue;
			}

			$output[$listItem['id']] = [
				'id' => $listItem['id'],
				'malId' => $malId,
				'data' => $listItem['attributes'],
			];
		}

		return $output;
	}

	public function filterKitsuMangaList()
	{
		$data = $this->kitsuModel->getFullMangaList();
		$includes = JsonAPI::organizeIncludes($data['included']);
		$includes['mappings'] = $this->filterMappings($includes['mappings'], 'manga');

		$output = [];

		foreach($data['data'] as $listItem)
		{
			$mangaId = $listItem['relationships']['manga']['data']['id'];
			$potentialMappings = $includes['manga'][$mangaId]['relationships']['mappings'];
			$malId = NULL;

			foreach ($potentialMappings as $mappingId)
			{
				if (array_key_exists($mappingId, $includes['mappings']))
				{
					$malId = $includes['mappings'][$mappingId]['externalId'];
				}
			}

			// Skip to the next item if there isn't a MAL ID
			if (is_null($malId))
			{
				continue;
			}

			$output[$listItem['id']] = [
				'id' => $listItem['id'],
				'malId' => $malId,
				'data' => $listItem['attributes'],
			];
		}

		return $output;
	}

	public function diffMangaLists()
	{
		$kitsuList = $this->filterKitsuMangaList();
		$malList = $this->formatMALMangaList();

		$itemsToAddToMAL =  [];
		$itemsToAddToKitsu = [];

		$malIds = array_column($malList, 'id');
		$kitsuMalIds = array_column($kitsuList, 'malId');
		$missingMalIds = array_diff($malIds, $kitsuMalIds);

		foreach($missingMalIds as $mid)
		{
			$itemsToAddToKitsu[] = array_merge($malList[$mid]['data'], [
				'id' => $this->kitsuModel->getKitsuIdFromMALId($mid, 'manga'),
				'type' => 'manga'
			]);
		}

		foreach($kitsuList as $kitsuItem)
		{
			if (in_array($kitsuItem['malId'], $malIds))
			{
				// Eventually, compare the list entries, and determine which
				// needs to be updated
				continue;
			}

			// Looks like this item only exists on Kitsu
			$itemsToAddToMAL[] = [
				'mal_id' => $kitsuItem['malId'],
				'data' => $kitsuItem['data']
			];
		}

		return [
			'addToMAL' => $itemsToAddToMAL,
			'addToKitsu' => $itemsToAddToKitsu
		];
	}

	public function diffAnimeLists()
	{
		// Get libraryEntries with media.mappings from Kitsu
		// Organize mappings, and ignore entries without mappings
		$kitsuList = $this->filterKitsuAnimeList();

		// Get MAL list data
		$malList = $this->formatMALAnimeList();

		$itemsToAddToMAL = [];
		$itemsToAddToKitsu = [];
		$malUpdateItems = [];
		$kitsuUpdateItems = [];

		$malIds = array_column($malList, 'id');
		$kitsuMalIds = array_column($kitsuList, 'malId');
		$missingMalIds = array_diff($malIds, $kitsuMalIds);

		foreach($missingMalIds as $mid)
		{
			// print_r($malList[$mid]);
			$itemsToAddToKitsu[] = array_merge($malList[$mid]['data'], [
				'id' => $this->kitsuModel->getKitsuIdFromMALId($mid),
				'type' => 'anime'
			]);
		}

		foreach($kitsuList as $kitsuItem)
		{
			if (in_array($kitsuItem['malId'], $malIds))
			{
				// Eventually, compare the list entries, and determine which
				// needs to be updated
				continue;
			}

			// Looks like this item only exists on Kitsu
			$itemsToAddToMAL[] = [
				'mal_id' => $kitsuItem['malId'],
				'data' => $kitsuItem['data']
			];

		}

		// Compare each list entry
			// If a list item exists only on MAL, create it on Kitsu with the existing data from MAL
			// If a list item exists only on Kitsu, create it on MAL with the existing data from Kitsu
			// If an item already exists on both APIS:
				// Compare last updated dates, and use the later one
				// Otherwise, use rewatch count, then episode progress as critera for selecting the more up
				// to date entry
				// Based on the 'newer' entry, update the other api list item

		return [
			'addToMAL' => $itemsToAddToMAL,
			'updateMAL' => $malUpdateItems,
			'addToKitsu' => $itemsToAddToKitsu,
			'updateKitsu' => $kitsuUpdateItems
		];
	}

	public function createKitusAnimeListItems($itemsToAdd, $type = 'anime')
	{
		$requester = new ParallelAPIRequest();
		foreach($itemsToAdd as $item)
		{
			$requester->addRequest($this->kitsuModel->createListItem($item));
		}

		$responses = $requester->makeRequests();

		foreach($responses as $key => $response)
		{
			$id = $itemsToAdd[$key]['id'];
			if ($response->getStatus() === 201)
			{
				$this->echoBox("Successfully created Kitsu {$type} list item with id: {$id}");
			}
			else
			{
				echo $response->getBody();
				$this->echoBox("Failed to create Kitsu {$type} list item with id: {$id}");<commit_msg>Fix creating missing Kitsu items<commit_after><?php declare(strict_types=1);
/**
 * Hummingbird Anime List Client
 *
 * An API client for Kitsu and MyAnimeList to manage anime and manga watch lists
 *
 * PHP version 7
 *
 * @package     HummingbirdAnimeClient
 * @author      <NAME> <<EMAIL>>
 * @copyright   2015 - 2017  <NAME>
 * @license     http://www.opensource.org/licenses/mit-license.html  MIT License
 * @version     4.0
 * @link        https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
 */

namespace Aviat\AnimeClient\Command;

use function Amp\{all, wait};

use Amp\Artax\Client;
use Aviat\AnimeClient\API\{
	JsonAPI,
	ParallelAPIRequest,
	Mapping\AnimeWatchingStatus,
	Mapping\MangaReadingStatus
};
use Aviat\AnimeClient\API\MAL\Transformer\{
	AnimeListTransformer as ALT,
	MangaListTransformer as MLT
};
use Aviat\Ion\Json;

/**
 * Clears the API Cache
 */
class SyncKitsuWithMal extends BaseCommand {

	/**
	 * Model for making requests to Kitsu API
	 * @var \Aviat\AnimeClient\API\Kitsu\Model
	 */
	protected $kitsuModel;

	/**
	 * Model for making requests to MAL API
	 * @var \Aviat\AnimeClient\API\MAL\Model
	 */
	protected $malModel;

	/**
	 * Run the image conversion script
	 *
	 * @param array $args
	 * @param array $options
	 * @return void
	 * @throws \ConsoleKit\ConsoleException
	 */
	public function execute(array $args, array $options = [])
	{
		$this->setContainer($this->setupContainer());
		$this->setCache($this->container->get('cache'));
		$this->kitsuModel = $this->container->get('kitsu-model');
		$this->malModel = $this->container->get('mal-model');

		$this->syncAnime();
		$this->syncManga();
	}

	public function syncAnime()
	{
		$malCount = count($this->malModel->getAnimeList());
		$kitsuCount = $this->kitsuModel->getAnimeListCount();

		$this->echoBox("Number of MAL anime list items: {$malCount}");
		$this->echoBox("Number of Kitsu anime list items: {$kitsuCount}");

		$data = $this->diffAnimeLists();

		$this->echoBox("Number of anime items that need to be added to MAL: " . count($data['addToMAL']));

		if ( ! empty($data['addToMAL']))
		{
			$this->echoBox("Adding missing anime list items to MAL");
			$this->createMALListItems($data['addToMAL'], 'anime');
		}

		$this->echoBox('Number of anime items that need to be added to Kitsu: ' . count($data['addToKitsu']));

		if ( ! empty($data['addToKitsu']))
		{
			$this->echoBox("Adding missing anime list items to Kitsu");
			$this->createKitsuListItems($data['addToKitsu'], 'anime');
		}
	}

	public function syncManga()
	{
		$malCount =  count($this->malModel->getMangaList());
		$kitsuCount = $this->kitsuModel->getMangaListCount();

		$this->echoBox("Number of MAL manga list items: {$malCount}");
		$this->echoBox("Number of Kitsu manga list items: {$kitsuCount}");

		$data = $this->diffMangaLists();

		$this->echoBox("Number of manga items that need to be added to MAL: " . count($data['addToMAL']));

		if ( ! empty($data['addToMAL']))
		{
			$this->echoBox("Adding missing manga list items to MAL");
			$this->createMALListItems($data['addToMAL'], 'manga');
		}

		$this->echoBox('Number of manga items that need to be added to Kitsu: ' . count($data['addToKitsu']));

		if ( ! empty($data['addToKitsu']))
		{
			$this->echoBox("Adding missing manga list items to Kitsu");
			$this->createKitsuListItems($data['addToKitsu'], 'manga');
		}
	}

	public function filterMappings(array $includes, string $type = 'anime'): array
	{
		$output = [];

		foreach($includes as $id => $mapping)
		{
			if ($mapping['externalSite'] === "myanimelist/{$type}")
			{
				$output[$id] = $mapping;
			}
		}

		return $output;
	}

	public function formatMALAnimeList()
	{
		$orig = $this->malModel->getAnimeList();
		$output = [];

		foreach($orig as $item)
		{
			$output[$item['series_animedb_id']] = [
				'id' => $item['series_animedb_id'],
				'data' => [
					'status' => AnimeWatchingStatus::MAL_TO_KITSU[$item['my_status']],
					'progress' => $item['my_watched_episodes'],
					'reconsuming' => (bool) $item['my_rewatching'],
					'reconsumeCount' => array_key_exists('times_rewatched', $item)
						? $item['times_rewatched']
						: 0,
					// 'notes' => ,
					'rating' => $item['my_score'] / 2,
					'updatedAt' => (new \DateTime())
						->setTimestamp((int)$item['my_last_updated'])
						->format(\DateTime::W3C),
				]
			];
		}

		return $output;
	}

	public function formatMALMangaList()
	{
		$orig = $this->malModel->getMangaList();
		$output = [];

		foreach($orig as $item)
		{
			$output[$item['series_mangadb_id']] = [
				'id' => $item['series_mangadb_id'],
				'data' => [
					'my_status' => $item['my_status'],
					'status' => MangaReadingStatus::MAL_TO_KITSU[$item['my_status']],
					'progress' => $item['my_read_chapters'],
					'volumes' => $item['my_read_volumes'],
					'reconsuming' => (bool) $item['my_rereadingg'],
					/* 'reconsumeCount' => array_key_exists('times_rewatched', $item)
						? $item['times_rewatched']
						: 0, */
					// 'notes' => ,
					'rating' => $item['my_score'] / 2,
					'updatedAt' => (new \DateTime())
						->setTimestamp((int)$item['my_last_updated'])
						->format(\DateTime::W3C),
				]
			];
		}

		return $output;
	}

	public function filterKitsuAnimeList()
	{
		$data = $this->kitsuModel->getFullAnimeList();
		$includes = JsonAPI::organizeIncludes($data['included']);
		$includes['mappings'] = $this->filterMappings($includes['mappings']);

		$output = [];

		foreach($data['data'] as $listItem)
		{
			$animeId = $listItem['relationships']['anime']['data']['id'];
			$potentialMappings = $includes['anime'][$animeId]['relationships']['mappings'];
			$malId = NULL;

			foreach ($potentialMappings as $mappingId)
			{
				if (array_key_exists($mappingId, $includes['mappings']))
				{
					$malId = $includes['mappings'][$mappingId]['externalId'];
				}
			}

			// Skip to the next item if there isn't a MAL ID
			if (is_null($malId))
			{
				continue;
			}

			$output[$listItem['id']] = [
				'id' => $listItem['id'],
				'malId' => $malId,
				'data' => $listItem['attributes'],
			];
		}

		return $output;
	}

	public function filterKitsuMangaList()
	{
		$data = $this->kitsuModel->getFullMangaList();
		$includes = JsonAPI::organizeIncludes($data['included']);
		$includes['mappings'] = $this->filterMappings($includes['mappings'], 'manga');

		$output = [];

		foreach($data['data'] as $listItem)
		{
			$mangaId = $listItem['relationships']['manga']['data']['id'];
			$potentialMappings = $includes['manga'][$mangaId]['relationships']['mappings'];
			$malId = NULL;

			foreach ($potentialMappings as $mappingId)
			{
				if (array_key_exists($mappingId, $includes['mappings']))
				{
					$malId = $includes['mappings'][$mappingId]['externalId'];
				}
			}

			// Skip to the next item if there isn't a MAL ID
			if (is_null($malId))
			{
				continue;
			}

			$output[$listItem['id']] = [
				'id' => $listItem['id'],
				'malId' => $malId,
				'data' => $listItem['attributes'],
			];
		}

		return $output;
	}

	public function diffMangaLists()
	{
		$kitsuList = $this->filterKitsuMangaList();
		$malList = $this->formatMALMangaList();

		$itemsToAddToMAL =  [];
		$itemsToAddToKitsu = [];

		$malIds = array_column($malList, 'id');
		$kitsuMalIds = array_column($kitsuList, 'malId');
		$missingMalIds = array_diff($malIds, $kitsuMalIds);

		foreach($missingMalIds as $mid)
		{
			$itemsToAddToKitsu[] = array_merge($malList[$mid]['data'], [
				'id' => $this->kitsuModel->getKitsuIdFromMALId($mid, 'manga'),
				'type' => 'manga'
			]);
		}

		foreach($kitsuList as $kitsuItem)
		{
			if (in_array($kitsuItem['malId'], $malIds))
			{
				// Eventually, compare the list entries, and determine which
				// needs to be updated
				continue;
			}

			// Looks like this item only exists on Kitsu
			$itemsToAddToMAL[] = [
				'mal_id' => $kitsuItem['malId'],
				'data' => $kitsuItem['data']
			];
		}

		return [
			'addToMAL' => $itemsToAddToMAL,
			'addToKitsu' => $itemsToAddToKitsu
		];
	}

	public function diffAnimeLists()
	{
		// Get libraryEntries with media.mappings from Kitsu
		// Organize mappings, and ignore entries without mappings
		$kitsuList = $this->filterKitsuAnimeList();

		// Get MAL list data
		$malList = $this->formatMALAnimeList();

		$itemsToAddToMAL = [];
		$itemsToAddToKitsu = [];
		$malUpdateItems = [];
		$kitsuUpdateItems = [];

		$malIds = array_column($malList, 'id');
		$kitsuMalIds = array_column($kitsuList, 'malId');
		$missingMalIds = array_diff($malIds, $kitsuMalIds);

		foreach($missingMalIds as $mid)
		{
			// print_r($malList[$mid]);
			$itemsToAddToKitsu[] = array_merge($malList[$mid]['data'], [
				'id' => $this->kitsuModel->getKitsuIdFromMALId($mid),
				'type' => 'anime'
			]);
		}

		foreach($kitsuList as $kitsuItem)
		{
			if (in_array($kitsuItem['malId'], $malIds))
			{
				// Eventually, compare the list entries, and determine which
				// needs to be updated
				continue;
			}

			// Looks like this item only exists on Kitsu
			$itemsToAddToMAL[] = [
				'mal_id' => $kitsuItem['malId'],
				'data' => $kitsuItem['data']
			];

		}

		// Compare each list entry
			// If a list item exists only on MAL, create it on Kitsu with the existing data from MAL
			// If a list item exists only on Kitsu, create it on MAL with the existing data from Kitsu
			// If an item already exists on both APIS:
				// Compare last updated dates, and use the later one
				// Otherwise, use rewatch count, then episode progress as critera for selecting the more up
				// to date entry
				// Based on the 'newer' entry, update the other api list item

		return [
			'addToMAL' => $itemsToAddToMAL,
			'updateMAL' => $malUpdateItems,
			'addToKitsu' => $itemsToAddToKitsu,
			'updateKitsu' => $kitsuUpdateItems
		];
	}

	public function createKitsuListItems($itemsToAdd, $type = 'anime')
	{
		$requester = new ParallelAPIRequest();
		foreach($itemsToAdd as $item)
		{
			$requester->addRequest($this->kitsuModel->createListItem($item));
		}

		$responses = $requester->makeRequests();

		foreach($responses as $key => $response)
		{
			$id = $itemsToAdd[$key]['id'];
			if ($response->getStatus() === 201)
			{
				$this->echoBox("Successfully created Kitsu {$type} list item with id: {$id}");
			}
			else
			{
				echo $response->getBody();
				$this->echoBox("Failed to create Kitsu {$type} list item with id: {$id}");
{'fraction_non_alphanumeric': 0.17528957528957528, 'fraction_numerical': 0.0021235521235521237, 'mean_word_length': 3.8379640438944667, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 233, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7369511', 'n_tokens_mistral': 8557, 'n_tokens_neox': 7133, 'n_words': 1489}