Skip to content

Commit 442d29b

Browse files
authored
Merge pull request #361 from ArabCoders/dev
Fixed bug preventing emby/jellyfin from adding new backends.
2 parents a5f75d7 + e14de10 commit 442d29b

17 files changed

+221
-40
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/Backends/Emby/EmbyClient.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public function withContext(Context $context): self
9090
'token' => $context->backendToken,
9191
'app' => Config::get('name') . '/' . static::CLIENT_NAME,
9292
'os' => PHP_OS,
93-
'id' => md5($context->backendUser),
93+
'id' => md5(Config::get('name') . '/' . static::CLIENT_NAME . $context->backendUser),
9494
'version' => getAppVersion(),
9595
'user' => $context->backendUser,
9696
]

src/Backends/Jellyfin/JellyfinClient.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public function withContext(Context $context): self
105105
'token' => $context->backendToken,
106106
'app' => Config::get('name') . '/' . static::CLIENT_NAME,
107107
'os' => PHP_OS,
108-
'id' => md5($context->backendUser),
108+
'id' => md5(Config::get('name') . '/' . static::CLIENT_NAME . $context->backendUser),
109109
'version' => getAppVersion(),
110110
'user' => $context->backendUser,
111111
]

src/Backends/Jellyfin/JellyfinManage.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ public function manage(array $backend, array $opts = []): array
3737
);
3838

3939
$question->setValidator(function ($answer) {
40-
$host = parse_url($answer, PHP_URL_HOST);
41-
if (false === is_string($answer)) {
40+
if (false === isValidURL($answer)) {
4241
throw new RuntimeException(
4342
'Invalid backend URL was given. Expecting something like http://example.com:8096/'
4443
);

src/Backends/Plex/PlexManage.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,7 @@ public function manage(array $backend, array $opts = []): array
156156
);
157157
158158
$question->setValidator(function ($answer) {
159-
$host = parse_url($answer, PHP_URL_HOST);
160-
if (false === is_string($answer)) {
159+
if (false === isValidURL($answer)) {
161160
throw new RuntimeException(
162161
'Invalid URL was selected/given. Expecting something like http://plex:32400.'
163162
);

src/Commands/Config/AddCommand.php

+72-2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66

77
use App\Command;
88
use App\Libs\Routable;
9+
use RuntimeException;
910
use Symfony\Component\Console\Exception\ExceptionInterface;
11+
use Symfony\Component\Console\Helper\QuestionHelper;
1012
use Symfony\Component\Console\Input\ArrayInput;
1113
use Symfony\Component\Console\Input\InputArgument;
1214
use Symfony\Component\Console\Input\InputInterface;
1315
use Symfony\Component\Console\Input\InputOption;
1416
use Symfony\Component\Console\Output\OutputInterface;
17+
use Symfony\Component\Console\Question\Question;
1518

1619
#[Routable(command: self::ROUTE)]
1720
final class AddCommand extends Command
@@ -23,7 +26,7 @@ protected function configure(): void
2326
$this->setName(self::ROUTE)
2427
->setDescription('Add new backend.')
2528
->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use Alternative config file.')
26-
->addArgument('backend', InputArgument::REQUIRED, 'Backend name')
29+
->addArgument('backend', InputArgument::OPTIONAL, 'Backend name', null)
2730
->setHelp(
2831
r(
2932
<<<HELP
@@ -50,6 +53,31 @@ protected function configure(): void
5053
*/
5154
protected function runCommand(InputInterface $input, OutputInterface $output): int
5255
{
56+
if (function_exists('stream_isatty') && defined('STDERR')) {
57+
$tty = stream_isatty(STDERR);
58+
} else {
59+
$tty = true;
60+
}
61+
62+
if (false === $tty || $input->getOption('no-interaction')) {
63+
$output->writeln(
64+
r(
65+
<<<ERROR
66+
67+
<error>ERROR:</error> This command require <notice>interaction</notice>. For example:
68+
69+
{cmd} <cmd>{route}</cmd>
70+
71+
ERROR,
72+
[
73+
'cmd' => trim(commandContext()),
74+
'route' => self::ROUTE,
75+
]
76+
)
77+
);
78+
return self::FAILURE;
79+
}
80+
5381
$opts = [
5482
'--add' => true,
5583
];
@@ -61,7 +89,49 @@ protected function runCommand(InputInterface $input, OutputInterface $output): i
6189
$opts['--' . $option] = $val;
6290
}
6391

64-
$opts['backend'] = strtolower($input->getArgument('backend'));
92+
$backend = $input->getArgument('backend');
93+
94+
if (null !== $backend) {
95+
$opts['backend'] = strtolower($backend);
96+
} else {
97+
// -- $backend.token
98+
$opts['backend'] = (function () use (&$opts, $input, $output) {
99+
$chosen = ag($opts, 'backend');
100+
101+
$question = new Question(
102+
<<<HELP
103+
<question>What should we be calling this <value>backend</value>?</question>
104+
------------------
105+
Backend name is used to identify the backend. The backend name must only contains
106+
<value>lower case a-z</value>, <value>0-9</value> and <value>_</value>.
107+
------------------
108+
<notice>Choose good name to identify your backend. For example, <value>home_plex</value>.</notice>
109+
HELP. PHP_EOL . '> ',
110+
$chosen
111+
);
112+
113+
$question->setValidator(function ($answer) {
114+
if (empty($answer)) {
115+
throw new RuntimeException('Backend Name cannot be empty.');
116+
}
117+
if (!isValidName($answer) || strtolower($answer) !== $answer) {
118+
throw new RuntimeException(
119+
r(
120+
'<error>ERROR:</error> Invalid [<value>{name}</value>] name was given. Only [<value>a-z, 0-9, _</value>] are allowed.',
121+
[
122+
'name' => $answer
123+
],
124+
)
125+
);
126+
}
127+
return $answer;
128+
});
129+
130+
return (new QuestionHelper())->ask($input, $output, $question);
131+
})();
132+
$output->writeln('');
133+
$opts['backend'] = strtolower($opts['backend']);
134+
}
65135

66136
return $this->getApplication()?->find(ManageCommand::ROUTE)->run(new ArrayInput($opts), $output) ?? 1;
67137
}

src/Commands/Config/ManageCommand.php

-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ protected function runCommand(InputInterface $input, OutputInterface $output, nu
7070
[
7171
'cmd' => trim(commandContext()),
7272
'route' => self::ROUTE,
73-
'backend' => $input->getArgument('backend'),
7473
]
7574
)
7675

src/Commands/State/BackupCommand.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ protected function process(InputInterface $input, OutputInterface $output): int
177177
continue;
178178
}
179179

180-
if (null === ($url = ag($backend, 'url')) || true !== is_string(parse_url($url, PHP_URL_HOST))) {
180+
if (null === ($url = ag($backend, 'url')) || false === isValidURL($url)) {
181181
$this->logger->error('SYSTEM: Ignoring [{backend}] because of invalid URL.', [
182182
'backend' => $backendName,
183183
'url' => $url ?? 'None',

src/Commands/State/ExportCommand.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ protected function process(InputInterface $input, OutputInterface $output): int
164164
continue;
165165
}
166166

167-
if (null === ($url = ag($backend, 'url')) || true !== is_string(parse_url($url, PHP_URL_HOST))) {
167+
if (null === ($url = ag($backend, 'url')) || false === isValidURL($url)) {
168168
$this->logger->error(
169169
sprintf('%s: Backend does not have valid url.', $backendName),
170170
['url' => $url ?? 'None']

src/Commands/State/ImportCommand.php

+3-2
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)) {
@@ -298,7 +299,7 @@ protected function process(InputInterface $input, OutputInterface $output): int
298299
continue;
299300
}
300301

301-
if (null === ($url = ag($backend, 'url')) || true !== is_string(parse_url($url, PHP_URL_HOST))) {
302+
if (null === ($url = ag($backend, 'url')) || false === isValidURL($url)) {
302303
$this->logger->error('SYSTEM: Ignoring [{backend}] because of invalid URL.', [
303304
'backend' => $backendName,
304305
'url' => $url ?? 'None',

src/Commands/State/ProgressCommand.php

+5-2
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()) {
@@ -146,7 +149,7 @@ protected function process(InputInterface $input, OutputInterface $output): int
146149
continue;
147150
}
148151

149-
if (null === ($url = ag($backend, 'url')) || true !== is_string(parse_url($url, PHP_URL_HOST))) {
152+
if (null === ($url = ag($backend, 'url')) || false === isValidURL($url)) {
150153
$this->logger->error('SYSTEM: [{backend}] Invalid url.', [
151154
'backend' => $backendName,
152155
'url' => $url ?? 'None',

src/Commands/State/PushCommand.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ protected function process(InputInterface $input): int
133133
continue;
134134
}
135135

136-
if (null === ($url = ag($backend, 'url')) || true !== is_string(parse_url($url, PHP_URL_HOST))) {
136+
if (null === ($url = ag($backend, 'url')) || false === isValidURL($url)) {
137137
$this->logger->error('SYSTEM: [{backend}] Invalid url.', [
138138
'backend' => $backendName,
139139
'url' => $url ?? 'None',

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
}

0 commit comments

Comments
 (0)