186 lines
5.7 KiB
PHP
186 lines
5.7 KiB
PHP
<?php
|
||
|
||
namespace App\Entity;
|
||
|
||
//use App\Repository\NoteRepository;
|
||
//use Doctrine\ORM\Mapping as ORM;
|
||
|
||
//#[ORM\Entity(repositoryClass: NoteRepository::class)]
|
||
class Note
|
||
{
|
||
//#[ORM\Id]
|
||
//#[ORM\GeneratedValue]
|
||
//#[ORM\Column]
|
||
|
||
private int $id;
|
||
private int $mod;
|
||
private array $terms = [];
|
||
private string $profile;
|
||
private array $tags = [];
|
||
private string $model;
|
||
// Maybe these doesn't make sense to keep but leaving it here just in
|
||
// case for handiness' sake
|
||
private array $fields = [];
|
||
private ?array $mediaInfo = null;
|
||
private array $cardIds;
|
||
|
||
const HIGHLIGHT_PATTERN = '/<span\s+([^>]*)>(.*?)<\/span>/i';
|
||
const HIGHLIGHT_ATTR_KANJI = 'style="color: rgb(255, 78, 8);"';
|
||
|
||
public function getId(): int
|
||
{
|
||
return $this->id;
|
||
}
|
||
|
||
public function hasTerm(string $kanji): bool
|
||
{
|
||
foreach ($this->terms as $term) {
|
||
assert($term instanceof Term);
|
||
|
||
if ($term->kanji == $kanji) return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
public static function fromAnki(array $noteInfo): self
|
||
{
|
||
$note = new self();
|
||
|
||
[
|
||
'noteId' => $note->id,
|
||
'mod' => $note->mod,
|
||
'profile' => $note->profile,
|
||
'tags' => $note->tags,
|
||
'modelName' => $note->model,
|
||
'cards' => $note->cardIds,
|
||
] = $noteInfo;
|
||
|
||
// the fields array key value comes with an order fields that is
|
||
// already maintained by PHP since arrays are ordered dictionaries.
|
||
// So we can safely just drop it.
|
||
//
|
||
// REVIEW: Having said that, maybe ordering the array before throwing
|
||
// the order would be advisable.
|
||
$note->fields = array_map(fn($x) => $x['value'], $noteInfo['fields']);
|
||
|
||
$note->mediaInfo = $note->parseMediaInfo($note->fields['Notes']);
|
||
|
||
// Set VocabKanji field
|
||
$note->terms = Term::fromNoteFields($note->fields);
|
||
|
||
// If not defined, find them from the highlighted parts in the sentence
|
||
if (empty($note->terms)) {
|
||
// 1. Get all spans in the text
|
||
preg_match_all(
|
||
self::HIGHLIGHT_PATTERN,
|
||
$note->fields['SentKanji'],
|
||
$matches,
|
||
PREG_SET_ORDER,
|
||
);
|
||
// 2. Check the ones that match with the kanji color
|
||
foreach ($matches as $match) {
|
||
if ($match[1] === self::HIGHLIGHT_ATTR_KANJI) {
|
||
$term = new Term();
|
||
$term->kanji = mb_trim($match[2]);
|
||
$term->definitionEn = null;
|
||
$term->definitionJp = null;
|
||
$note->terms[] = $term;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Set to null whatever is null
|
||
$readings = array_map(
|
||
fn($x) => in_array($x, ['_', '_', '']) ? null : $x,
|
||
explode('|', $note->fields['VocabFurigana']),
|
||
);
|
||
|
||
// Set readings from furigana field
|
||
foreach ($note->terms as $key => &$term) {
|
||
if (null === $term->getReading()) {
|
||
if (null !== ($readings[$key] ?? null)) {
|
||
$term->kanji .= '[' . $readings[$key] . ']';
|
||
}
|
||
}
|
||
}
|
||
|
||
return $note;
|
||
}
|
||
|
||
public function toAnki(): array
|
||
{
|
||
return [
|
||
'id' => $this->id,
|
||
'fields' => [
|
||
'VocabKanji' => join('|', array_map(
|
||
fn(Term $x) => $x->getKanji(),
|
||
$this->terms,
|
||
)),
|
||
'VocabFurigana' => join('|', array_map(
|
||
fn(Term $x) => $x->getReading() ?? '_',
|
||
$this->terms,
|
||
)),
|
||
'VocabDef' => join("<br>\n", array_map(
|
||
fn(Term $x) => $x->toAnkiVocabDef(),
|
||
$this->terms,
|
||
)),
|
||
],
|
||
];
|
||
}
|
||
|
||
public function parseMediaInfo(string $notes): ?array
|
||
{
|
||
$matches = null;
|
||
|
||
// Parse the notes fields. It can be in the form of
|
||
// series-name_S01 EP07 (11h22m33s44ms)
|
||
// or
|
||
// movie-name EP (11h22m33s44ms)
|
||
if (1 !== preg_match(
|
||
'/(?<name>[a-z\-_]+)(_S)?(?<season>\d*) EP(?<episode>\d*) \((?<time>.*)\)/n',
|
||
$notes,
|
||
$matches,
|
||
)) {
|
||
return null;
|
||
}
|
||
|
||
// Remove number-indexed matches, cast numbers to integers
|
||
$matches = [
|
||
'name' => $matches['name'],
|
||
'time' => $matches['time'],
|
||
// NOTE: intval returns 0 if not a number, which is false-like
|
||
'season' => intval($matches['season']) ?: null,
|
||
'episode' => intval($matches['episode']) ?: null,
|
||
];
|
||
|
||
// Parse time into a DateInterval and replace it in the matches array
|
||
$time = new \DateInterval('PT0S');
|
||
|
||
preg_match('/(\d+)ms/', $matches['time'], $milliseconds);
|
||
preg_match('/(\d+)s/', $matches['time'], $seconds);
|
||
preg_match('/(\d+)m/', $matches['time'], $minutes);
|
||
preg_match('/(\d+)h/', $matches['time'], $hours);
|
||
|
||
if ($milliseconds[1] ?? false) $time->f = $milliseconds[1] * 1000;
|
||
if ($seconds[1] ?? false) $time->s = $seconds[1];
|
||
if ($minutes[1] ?? false) $time->i = $minutes[1];
|
||
if ($hours[1] ?? false) $time->h = $hours[1];
|
||
|
||
$matches['time'] = $time;
|
||
|
||
return $matches;
|
||
}
|
||
|
||
public function getCreatedAt(): \DateTimeImmutable
|
||
{
|
||
$timestamp = ceil($this->id / 1000);
|
||
return \DateTimeImmutable::createFromFormat('U', $timestamp);
|
||
}
|
||
|
||
public function getUpdatedAt(): \DateTimeImmutable
|
||
{
|
||
return \DateTimeImmutable::createFromFormat('U', $this->mod);
|
||
}
|
||
}
|