Skip to content

Commit e14de10

Browse files
committed
initial code to support play progress tracking via import.
1 parent 3961fbd commit e14de10

File tree

7 files changed

+122
-26
lines changed

7 files changed

+122
-26
lines changed

config/services.php

+16-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use App\Libs\Extends\ConsoleOutput;
1212
use App\Libs\Extends\HttpClient;
1313
use App\Libs\Extends\LogMessageProcessor;
14+
use App\Libs\Mappers\Import\DirectMapper;
1415
use App\Libs\Mappers\Import\MemoryMapper;
1516
use App\Libs\Mappers\ImportInterface as iImport;
1617
use App\Libs\QueueRequests;
@@ -167,13 +168,26 @@
167168
],
168169

169170
MemoryMapper::class => [
170-
'class' => function (iLogger $logger, iDB $db): iImport {
171-
return (new MemoryMapper(logger: $logger, db: $db))
171+
'class' => function (iLogger $logger, iDB $db, CacheInterface $cache): iImport {
172+
return (new MemoryMapper(logger: $logger, db: $db, cache: $cache))
172173
->setOptions(options: Config::get('mapper.import.opts', []));
173174
},
174175
'args' => [
175176
iLogger::class,
176177
iDB::class,
178+
CacheInterface::class
179+
],
180+
],
181+
182+
DirectMapper::class => [
183+
'class' => function (iLogger $logger, iDB $db, CacheInterface $cache): iImport {
184+
return (new DirectMapper(logger: $logger, db: $db, cache: $cache))
185+
->setOptions(options: Config::get('mapper.import.opts', []));
186+
},
187+
'args' => [
188+
iLogger::class,
189+
iDB::class,
190+
CacheInterface::class
177191
],
178192
],
179193

src/Commands/State/ImportCommand.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use App\Commands\Backend\Library\UnmatchedCommand;
99
use App\Commands\Config\EditCommand;
1010
use App\Libs\Config;
11+
use App\Libs\Container;
1112
use App\Libs\Database\DatabaseInterface as iDB;
1213
use App\Libs\Entity\StateInterface as iState;
1314
use App\Libs\Extends\StreamLogHandler;
@@ -250,7 +251,7 @@ protected function process(InputInterface $input, OutputInterface $output): int
250251
}
251252

252253
if ($input->getOption('direct-mapper')) {
253-
$this->mapper = new DirectMapper(logger: $this->logger, db: $this->db);
254+
$this->mapper = Container::get(DirectMapper::class);
254255
}
255256

256257
if (!empty($mapperOpts)) {

src/Commands/State/ProgressCommand.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ protected function configure(): void
5353
<error>***WARNING THIS COMMAND IS EXPERIMENTAL AND MAY NOT WORK AS EXPECTED***</error>
5454
<notice>THIS COMMAND ONLY WORKS CORRECTLY FOR PLEX & EMBY AT THE MOMENT.</notice>
5555
=================================================================================
56-
Jellyfin API has a bug which i cannot do anything about.
56+
Jellyfin API has a bug which I cannot do anything about.
5757
5858
This command push <notice>user</notice> watch progress to export enabled backends.
5959
You should not run this manually and instead rely on scheduled task to run this command.
@@ -100,6 +100,9 @@ protected function process(InputInterface $input, OutputInterface $output): int
100100
if (!empty($items)) {
101101
foreach ($items as $queueItem) {
102102
$dbItem = $this->db->get($queueItem);
103+
if ($dbItem->isWatched()) {
104+
continue;
105+
}
103106
$dbItem = $dbItem->apply($queueItem);
104107

105108
if (!$dbItem->hasPlayProgress()) {

src/Libs/Mappers/Import/DirectMapper.php

+46-10
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010
use App\Libs\Mappers\ImportInterface as iImport;
1111
use App\Libs\Message;
1212
use App\Libs\Options;
13+
use DateInterval;
1314
use DateTimeInterface as iDate;
1415
use Exception;
1516
use PDOException;
1617
use Psr\Log\LoggerInterface as iLogger;
18+
use Psr\SimpleCache\CacheInterface;
19+
use Psr\SimpleCache\InvalidArgumentException;
1720

1821
final class DirectMapper implements iImport
1922
{
@@ -43,8 +46,12 @@ final class DirectMapper implements iImport
4346
protected array $options = [];
4447

4548
protected bool $fullyLoaded = false;
49+
/**
50+
* @var array<string,iState> List of items with play progress.
51+
*/
52+
protected array $progressItems = [];
4653

47-
public function __construct(protected iLogger $logger, protected iDB $db)
54+
public function __construct(protected iLogger $logger, protected iDB $db, protected CacheInterface $cache)
4855
{
4956
}
5057

@@ -297,8 +304,12 @@ public function add(iState $entity, array $opts = []): self
297304
return $this;
298305
}
299306

307+
$newPlayProgress = (int)ag($entity->getMetadata($entity->via), iState::COLUMN_META_DATA_PROGRESS);
308+
$oldPlayProgress = (int)ag($cloned->getMetadata($entity->via), iState::COLUMN_META_DATA_PROGRESS);
309+
$playChanged = $newPlayProgress != $oldPlayProgress;
310+
300311
// -- this sometimes leads to never ending updates as data from backends conflicts.
301-
if (true === (bool)ag($this->options, Options::MAPPER_ALWAYS_UPDATE_META)) {
312+
if ($playChanged || true === (bool)ag($this->options, Options::MAPPER_ALWAYS_UPDATE_META)) {
302313
if (true === (clone $cloned)->apply(entity: $entity, fields: $keys)->isChanged(fields: $keys)) {
303314
try {
304315
$local = $local->apply(
@@ -311,16 +322,30 @@ public function add(iState $entity, array $opts = []): self
311322
$changes = $local->diff(fields: $keys);
312323

313324
if (count($changes) >= 1) {
314-
$this->logger->notice('MAPPER: [{backend}] updated [{title}] metadata.', [
315-
'id' => $cloned->id,
316-
'backend' => $entity->via,
317-
'title' => $cloned->getName(),
318-
'changes' => $changes,
319-
]);
325+
$this->logger->notice(
326+
$playChanged ? 'MAPPER: [{backend}] updated [{title}] due to play progress change.' : 'MAPPER: [{backend}] updated [{title}] metadata.',
327+
[
328+
'id' => $cloned->id,
329+
'backend' => $entity->via,
330+
'title' => $cloned->getName(),
331+
'changes' => $changes,
332+
]
333+
);
320334
}
321335

322336
if (false === $inDryRunMode) {
323337
$this->db->update($local);
338+
339+
if (true === $entity->hasPlayProgress()) {
340+
$itemId = r('{type}://{id}:{tainted}@{backend}', [
341+
'type' => $entity->type,
342+
'backend' => $entity->via,
343+
'tainted' => $entity->isTainted() ? 'tainted' : 'untainted',
344+
'id' => ag($entity->getMetadata($entity->via), iState::COLUMN_ID, '??'),
345+
]);
346+
347+
$this->progressItems[$itemId] = $entity;
348+
}
324349
}
325350

326351
if (null === ($this->changed[$local->id] ?? null)) {
@@ -510,8 +535,19 @@ public function remove(iState $entity): bool
510535
return $this->db->remove($entity);
511536
}
512537

513-
public function commit(): mixed
538+
public function commit(): array
514539
{
540+
if (count($this->progressItems) >= 1) {
541+
try {
542+
$progress = $this->cache->get('progress', []);
543+
foreach ($this->progressItems as $itemId => $entity) {
544+
$progress[$itemId] = $entity;
545+
}
546+
$this->cache->set('progress', $progress, new DateInterval('P1D'));
547+
} catch (InvalidArgumentException) {
548+
}
549+
}
550+
515551
$list = $this->actions;
516552

517553
$this->reset();
@@ -532,7 +568,7 @@ public function reset(): self
532568
];
533569

534570
$this->fullyLoaded = false;
535-
$this->changed = $this->objects = $this->pointers = [];
571+
$this->changed = $this->objects = $this->pointers = $this->progressItems = [];
536572

537573
return $this;
538574
}

src/Libs/Mappers/Import/MemoryMapper.php

+48-10
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
use App\Libs\Mappers\ImportInterface as iImport;
1010
use App\Libs\Message;
1111
use App\Libs\Options;
12+
use DateInterval;
1213
use DateTimeInterface as iDate;
1314
use PDOException;
1415
use Psr\Log\LoggerInterface as iLogger;
16+
use Psr\SimpleCache\CacheInterface;
17+
use Psr\SimpleCache\InvalidArgumentException;
1518

1619
final class MemoryMapper implements iImport
1720
{
@@ -32,11 +35,16 @@ final class MemoryMapper implements iImport
3235
*/
3336
protected array $changed = [];
3437

38+
/**
39+
* @var array<int,iState> List of items with play progress.
40+
*/
41+
protected array $progressItems = [];
42+
3543
protected array $options = [];
3644

3745
protected bool $fullyLoaded = false;
3846

39-
public function __construct(protected iLogger $logger, protected iDB $db)
47+
public function __construct(protected iLogger $logger, protected iDB $db, protected CacheInterface $cache)
4048
{
4149
}
4250

@@ -214,8 +222,12 @@ public function add(iState $entity, array $opts = []): self
214222
return $this;
215223
}
216224

225+
$newPlayProgress = (int)ag($entity->getMetadata($entity->via), iState::COLUMN_META_DATA_PROGRESS);
226+
$oldPlayProgress = (int)ag($cloned->getMetadata($entity->via), iState::COLUMN_META_DATA_PROGRESS);
227+
$playChanged = $newPlayProgress != $oldPlayProgress;
228+
217229
// -- this sometimes leads to never ending updates as data from backends conflicts.
218-
if (true === (bool)ag($this->options, Options::MAPPER_ALWAYS_UPDATE_META)) {
230+
if ($playChanged || true === (bool)ag($this->options, Options::MAPPER_ALWAYS_UPDATE_META)) {
219231
if (true === (clone $cloned)->apply(entity: $entity, fields: $keys)->isChanged(fields: $keys)) {
220232
$this->changed[$pointer] = $pointer;
221233
Message::increment("{$entity->via}.{$entity->type}.updated");
@@ -230,13 +242,26 @@ public function add(iState $entity, array $opts = []): self
230242
$changes = $this->objects[$pointer]->diff(fields: $keys);
231243

232244
if (count($changes) >= 1) {
233-
$this->logger->notice('MAPPER: [{backend}] updated [{title}] metadata.', [
234-
'id' => $cloned->id,
235-
'backend' => $entity->via,
236-
'title' => $cloned->getName(),
237-
'changes' => $changes,
238-
'fields' => implode(',', $keys),
239-
]);
245+
$this->logger->notice(
246+
$playChanged ? 'MAPPER: [{backend}] updated [{title}] due to play progress change.' : 'MAPPER: [{backend}] updated [{title}] metadata.',
247+
[
248+
'id' => $cloned->id,
249+
'backend' => $entity->via,
250+
'title' => $cloned->getName(),
251+
'changes' => $changes,
252+
'fields' => implode(',', $keys),
253+
]
254+
);
255+
if (true === $entity->hasPlayProgress()) {
256+
$itemId = r('{type}://{id}:{tainted}@{backend}', [
257+
'type' => $entity->type,
258+
'backend' => $entity->via,
259+
'tainted' => $entity->isTainted() ? 'tainted' : 'untainted',
260+
'id' => ag($entity->getMetadata($entity->via), iState::COLUMN_ID, '??'),
261+
]);
262+
263+
$this->progressItems[$itemId] = $entity;
264+
}
240265
}
241266

242267
return $this;
@@ -380,6 +405,19 @@ public function remove(iState $entity): bool
380405

381406
public function commit(): mixed
382407
{
408+
if (true !== $this->inDryRunMode()) {
409+
if (count($this->progressItems) >= 1) {
410+
try {
411+
$progress = $this->cache->get('progress', []);
412+
foreach ($this->progressItems as $itemId => $entity) {
413+
$progress[$itemId] = $entity;
414+
}
415+
$this->cache->set('progress', $progress, new DateInterval('P1D'));
416+
} catch (InvalidArgumentException) {
417+
}
418+
}
419+
}
420+
383421
$state = $this->db->transactional(function (iDB $db) {
384422
$list = [
385423
iState::TYPE_MOVIE => ['added' => 0, 'updated' => 0, 'failed' => 0],
@@ -437,7 +475,7 @@ public function has(iState $entity): bool
437475
public function reset(): self
438476
{
439477
$this->fullyLoaded = false;
440-
$this->objects = $this->changed = $this->pointers = [];
478+
$this->objects = $this->changed = $this->pointers = $this->progressItems = [];
441479

442480
return $this;
443481
}

tests/Mappers/Import/DirectMapperTest.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
use App\Libs\Entity\StateEntity;
88
use App\Libs\Mappers\Import\DirectMapper;
99
use App\Libs\Mappers\ImportInterface;
10+
use Symfony\Component\Cache\Adapter\NullAdapter;
11+
use Symfony\Component\Cache\Psr16Cache;
1012

1113
class DirectMapperTest extends AbstractTestsMapper
1214
{
1315
protected function setupMapper(): ImportInterface
1416
{
15-
$mapper = new DirectMapper($this->logger, $this->db);
17+
$mapper = new DirectMapper($this->logger, $this->db, cache: new Psr16Cache(new NullAdapter()));
1618
$mapper->setOptions(options: ['class' => new StateEntity([])]);
1719
return $mapper;
1820
}

tests/Mappers/Import/MemoryMapperTest.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
use App\Libs\Entity\StateEntity;
88
use App\Libs\Mappers\Import\MemoryMapper;
99
use App\Libs\Mappers\ImportInterface;
10+
use Symfony\Component\Cache\Adapter\NullAdapter;
11+
use Symfony\Component\Cache\Psr16Cache;
1012

1113
class MemoryMapperTest extends AbstractTestsMapper
1214
{
1315
protected function setupMapper(): ImportInterface
1416
{
15-
$mapper = new MemoryMapper($this->logger, $this->db);
17+
$mapper = new MemoryMapper($this->logger, $this->db, new Psr16Cache(new NullAdapter()));
1618
$mapper->setOptions(options: ['class' => new StateEntity([])]);
1719

1820
return $mapper;

0 commit comments

Comments
 (0)