anker/src/Service/AnkiService.php

158 lines
4.5 KiB
PHP

<?php
namespace App\Service;
use App\Entity\Note;
use App\Entity\SentenceListeningNote;
use App\Entity\SentenceNote;
use App\Utils\Japanese;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class AnkiService
{
function __construct(
private HttpClientInterface $client,
) {}
private function request(string $action, array $params): mixed
{
['error' => $error, 'result' => $result] = $this->client->request(
'POST',
'http://localhost:8765',
['json' => [
'action' => $action,
'version' => 6,
'params' => $params,
]]
)->toArray();
if ($error != null) {
throw new \Exception('AnkiConnect returned error: ' . $error);
}
return $result;
}
/** The note's id is updated on success.
* @return bool True on success
*/
public function addNote(Note &$note, string $deckName): bool
{
$note->setId($this->request('addNote', ['note' => [
'deckName' => $deckName,
'modelName' => $note->getModel(),
'fields' => $note->getFields(),
'options' => ['allowDuplicate' => false],
'tags' => array_merge($note->getTags(), ['anker_made']),
]]));
return $note->getId() !== null;
}
public function findNotesIds(string $query): array
{
return $this->request('findNotes', ['query' => $query]);
}
public function getAllSentenceListeningNoteIds(): array
{
$query = sprintf(
'"note:%s"',
SentenceListeningNote::MODEL_NAME,
);
return $this->request('findNotes', ['query' => $query]);
}
public function getAllSentenceNoteIds(): array
{
return $this->request(
'findNotes',
['query' => '"note:Japanese sentences" -is:suspended'],
);
}
/** Give an array of IDs, the note Infos are returned. if info for a given
* doesn't exist, it is assigned to null instead of the default [].
*/
public function getNotesInfo(array $noteIds): array
{
$noteInfos = $this->request('notesInfo', ['notes' => $noteIds]);
foreach ($noteInfos as &$noteInfo) {
if ([] === $noteInfo) $noteInfo = null;
}
return $noteInfos;
}
/** Returns info form note given an ID, returns null if it doesn't exist */
public function getNoteInfo(int $noteId): ?array
{
return $this->getNotesInfo([$noteId])[0];
}
public function getNotes(array $nids): array
{
return array_map(self::parseNoteInfo(...), $this->getNotesInfo($nids));
}
public function getNote(int $nid): ?Note
{
$noteInfo = $this->getNoteInfo($nid)
?? throw new \Exception("Note $nid not found.");
return self::parseNoteInfo($noteInfo);
}
private static function parseNoteInfo(array $noteInfo): Note
{
return match ($noteInfo['modelName']) {
SentenceNote::MODEL_NAME => SentenceNote::fromAnki($noteInfo),
SentenceListeningNote::MODEL_NAME => SentenceListeningNote::fromAnki($noteInfo),
default => throw new \Exception(sprintf(
'Unrecognized Note "%s" of type "%s"',
$noteInfo['noteId'],
$noteInfo['modelName'],
))
};
}
public function getLatestNote(): ?Note
{
// NoteIDs are just timestamps in milliseconds, so the latest is just
// the biggest numerically
$latestId = max($this->getAllSentenceNoteIds());
return $this->getNote($latestId);
}
public function updateNote(Note $note)
{
$this->request('guiBrowse', ['query' => 'nid:1']);
$this->request('updateNoteFields', ['note' => $note->toAnki()]);
$this->request('guiBrowse', ['query' => 'nid:' . $note->getId()]);
}
/** @return array<string, int> */
public function getKnownSlnKanjiCounts(): array
{
$allListeningIds = $this->getAllSentenceListeningNoteIds();
$ret = [];
foreach ($this->getNotes($allListeningIds) as $slNote) {
assert($slNote instanceof SentenceListeningNote);
$termKanji = Japanese::getOnlyKanji($slNote->getTerm()->getKanji());
$len = mb_strlen($termKanji);
for ($i = 0; $i < $len; $i++) {
$kanji = mb_substr($termKanji, $i, 1);
$ret[$kanji] ??= 0;
$ret[$kanji]++;
}
}
return $ret;
}
}