From bd8ccf924f4ae78be407b151b668679edd511e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20L=C5=93uillet?= Date: Thu, 31 Oct 2024 08:10:33 +0100 Subject: [PATCH] Added Omnivore Import --- app/config/config.yml | 14 + app/config/services.yml | 9 + app/config/services_rabbit.yml | 6 + app/config/services_redis.yml | 16 + .../ImportBundle/Command/ImportCommand.php | 8 +- .../Command/RedisWorkerCommand.php | 2 +- .../Consumer/RabbitMQConsumerTotalProxy.php | 7 +- .../Controller/ImportController.php | 2 + .../Controller/OmnivoreController.php | 84 +++++ .../ImportBundle/Import/OmnivoreImport.php | 158 ++++++++ .../Resources/views/Omnivore/index.html.twig | 45 +++ .../Controller/ImportControllerTest.php | 2 +- .../Controller/OmnivoreControllerTest.php | 203 +++++++++++ .../ImportBundle/fixtures/omnivore.json | 343 ++++++++++++++++++ translations/messages.en.yml | 4 + web/wallassets/41a1d465797f51702171.otf | Bin 0 -> 49652 bytes 16 files changed, 899 insertions(+), 4 deletions(-) create mode 100644 src/Wallabag/ImportBundle/Controller/OmnivoreController.php create mode 100644 src/Wallabag/ImportBundle/Import/OmnivoreImport.php create mode 100644 src/Wallabag/ImportBundle/Resources/views/Omnivore/index.html.twig create mode 100644 tests/Wallabag/ImportBundle/Controller/OmnivoreControllerTest.php create mode 100644 tests/Wallabag/ImportBundle/fixtures/omnivore.json create mode 100644 web/wallassets/41a1d465797f51702171.otf diff --git a/app/config/config.yml b/app/config/config.yml index 2155a2017..064df3623 100644 --- a/app/config/config.yml +++ b/app/config/config.yml @@ -267,6 +267,11 @@ old_sound_rabbit_mq: exchange_options: name: 'wallabag.import.elcurator' type: topic + import_omnivore: + connection: default + exchange_options: + name: 'wallabag.import.omnivore' + type: topic import_firefox: connection: default exchange_options: @@ -350,6 +355,15 @@ old_sound_rabbit_mq: name: 'wallabag.import.elcurator' callback: wallabag_import.consumer.amqp.elcurator qos_options: {prefetch_count: "%rabbitmq_prefetch_count%"} + import_omnivore: + connection: default + exchange_options: + name: 'wallabag.import.omnivore' + type: topic + queue_options: + name: 'wallabag.import.omnivore' + callback: wallabag_import.consumer.amqp.omnivore + qos_options: {prefetch_count: "%rabbitmq_prefetch_count%"} import_firefox: connection: default exchange_options: diff --git a/app/config/services.yml b/app/config/services.yml index 270e79d9d..47f9b3aef 100644 --- a/app/config/services.yml +++ b/app/config/services.yml @@ -81,6 +81,11 @@ services: $rabbitMqProducer: '@old_sound_rabbit_mq.import_elcurator_producer' $redisProducer: '@wallabag_import.producer.redis.elcurator' + Wallabag\ImportBundle\Controller\OmnivoreController: + arguments: + $rabbitMqProducer: '@old_sound_rabbit_mq.import_omnivore_producer' + $redisProducer: '@wallabag_import.producer.redis.omnivore' + Wallabag\ImportBundle\Controller\FirefoxController: arguments: $rabbitMqProducer: '@old_sound_rabbit_mq.import_firefox_producer' @@ -377,6 +382,10 @@ services: tags: - { name: wallabag_import.import, alias: delicious } + Wallabag\ImportBundle\Import\OmnivoreImport: + tags: + - { name: wallabag_import.import, alias: omnivore } + Wallabag\ImportBundle\Import\FirefoxImport: tags: - { name: wallabag_import.import, alias: firefox } diff --git a/app/config/services_rabbit.yml b/app/config/services_rabbit.yml index 26e02a784..b6f9f1d52 100644 --- a/app/config/services_rabbit.yml +++ b/app/config/services_rabbit.yml @@ -18,6 +18,7 @@ services: $pinboardConsumer: '@old_sound_rabbit_mq.import_pinboard_consumer' $deliciousConsumer: '@old_sound_rabbit_mq.import_delicious_consumer' $elcuratorConsumer: '@old_sound_rabbit_mq.import_elcurator_consumer' + $omnivoreConsumer: '@old_sound_rabbit_mq.import_omnivore_consumer' wallabag_import.consumer.amqp.pocket: class: Wallabag\ImportBundle\Consumer\AMQPEntryConsumer @@ -44,6 +45,11 @@ services: arguments: $import: '@Wallabag\ImportBundle\Import\DeliciousImport' + wallabag_import.consumer.amqp.omnivore: + class: Wallabag\ImportBundle\Consumer\AMQPEntryConsumer + arguments: + $import: '@Wallabag\ImportBundle\Import\OmnivoreImport' + wallabag_import.consumer.amqp.wallabag_v1: class: Wallabag\ImportBundle\Consumer\AMQPEntryConsumer arguments: diff --git a/app/config/services_redis.yml b/app/config/services_redis.yml index 02c7eba95..2f0f1ac48 100644 --- a/app/config/services_redis.yml +++ b/app/config/services_redis.yml @@ -69,6 +69,22 @@ services: arguments: $import: '@Wallabag\ImportBundle\Import\DeliciousImport' + # Omnivore + wallabag_import.queue.redis.omnivore: + class: Simpleue\Queue\RedisQueue + arguments: + $queueName: "wallabag.import.omnivore" + + wallabag_import.producer.redis.omnivore: + class: Wallabag\ImportBundle\Redis\Producer + arguments: + - "@wallabag_import.queue.redis.omnivore" + + wallabag_import.consumer.redis.omnivore: + class: Wallabag\ImportBundle\Consumer\RedisEntryConsumer + arguments: + $import: '@Wallabag\ImportBundle\Import\OmnivoreImport' + # pocket wallabag_import.queue.redis.pocket: class: Simpleue\Queue\RedisQueue diff --git a/src/Wallabag/ImportBundle/Command/ImportCommand.php b/src/Wallabag/ImportBundle/Command/ImportCommand.php index 1031e5675..edc85c01d 100644 --- a/src/Wallabag/ImportBundle/Command/ImportCommand.php +++ b/src/Wallabag/ImportBundle/Command/ImportCommand.php @@ -15,6 +15,7 @@ use Wallabag\ImportBundle\Import\ChromeImport; use Wallabag\ImportBundle\Import\DeliciousImport; use Wallabag\ImportBundle\Import\FirefoxImport; use Wallabag\ImportBundle\Import\InstapaperImport; +use Wallabag\ImportBundle\Import\OmnivoreImport; use Wallabag\ImportBundle\Import\PinboardImport; use Wallabag\ImportBundle\Import\ReadabilityImport; use Wallabag\ImportBundle\Import\WallabagV1Import; @@ -34,9 +35,10 @@ class ImportCommand extends Command private InstapaperImport $instapaperImport; private PinboardImport $pinboardImport; private DeliciousImport $deliciousImport; + private OmnivoreImport $omnivoreImport; private WallabagV1Import $wallabagV1Import; - public function __construct(EntityManagerInterface $entityManager, TokenStorageInterface $tokenStorage, UserRepository $userRepository, WallabagV2Import $wallabagV2Import, FirefoxImport $firefoxImport, ChromeImport $chromeImport, ReadabilityImport $readabilityImport, InstapaperImport $instapaperImport, PinboardImport $pinboardImport, DeliciousImport $deliciousImport, WallabagV1Import $wallabagV1Import) + public function __construct(EntityManagerInterface $entityManager, TokenStorageInterface $tokenStorage, UserRepository $userRepository, WallabagV2Import $wallabagV2Import, FirefoxImport $firefoxImport, ChromeImport $chromeImport, ReadabilityImport $readabilityImport, InstapaperImport $instapaperImport, PinboardImport $pinboardImport, DeliciousImport $deliciousImport, OmnivoreImport $omnivoreImport, WallabagV1Import $wallabagV1Import) { $this->entityManager = $entityManager; $this->tokenStorage = $tokenStorage; @@ -48,6 +50,7 @@ class ImportCommand extends Command $this->instapaperImport = $instapaperImport; $this->pinboardImport = $pinboardImport; $this->deliciousImport = $deliciousImport; + $this->omnivoreImport = $omnivoreImport; $this->wallabagV1Import = $wallabagV1Import; parent::__construct(); @@ -120,6 +123,9 @@ class ImportCommand extends Command case 'delicious': $import = $this->deliciousImport; break; + case 'omnivore': + $import = $this->omnivoreImport; + break; default: $import = $this->wallabagV1Import; } diff --git a/src/Wallabag/ImportBundle/Command/RedisWorkerCommand.php b/src/Wallabag/ImportBundle/Command/RedisWorkerCommand.php index 5c19d30b3..179e213de 100644 --- a/src/Wallabag/ImportBundle/Command/RedisWorkerCommand.php +++ b/src/Wallabag/ImportBundle/Command/RedisWorkerCommand.php @@ -27,7 +27,7 @@ class RedisWorkerCommand extends Command $this ->setName('wallabag:import:redis-worker') ->setDescription('Launch Redis worker') - ->addArgument('serviceName', InputArgument::REQUIRED, 'Service to use: wallabag_v1, wallabag_v2, pocket, readability, pinboard, delicious, firefox, chrome or instapaper') + ->addArgument('serviceName', InputArgument::REQUIRED, 'Service to use: wallabag_v1, wallabag_v2, pocket, readability, pinboard, delicious, omnivore, firefox, chrome or instapaper') ->addOption('maxIterations', '', InputOption::VALUE_OPTIONAL, 'Number of iterations before stopping', false) ; } diff --git a/src/Wallabag/ImportBundle/Consumer/RabbitMQConsumerTotalProxy.php b/src/Wallabag/ImportBundle/Consumer/RabbitMQConsumerTotalProxy.php index 443d167cd..096d2b9a6 100644 --- a/src/Wallabag/ImportBundle/Consumer/RabbitMQConsumerTotalProxy.php +++ b/src/Wallabag/ImportBundle/Consumer/RabbitMQConsumerTotalProxy.php @@ -20,8 +20,9 @@ class RabbitMQConsumerTotalProxy private Consumer $pinboardConsumer; private Consumer $deliciousConsumer; private Consumer $elcuratorConsumer; + private Consumer $omnivoreConsumer; - public function __construct(Consumer $pocketConsumer, Consumer $readabilityConsumer, Consumer $wallabagV1Consumer, Consumer $wallabagV2Consumer, Consumer $firefoxConsumer, Consumer $chromeConsumer, Consumer $instapaperConsumer, Consumer $pinboardConsumer, Consumer $deliciousConsumer, Consumer $elcuratorConsumer) + public function __construct(Consumer $pocketConsumer, Consumer $readabilityConsumer, Consumer $wallabagV1Consumer, Consumer $wallabagV2Consumer, Consumer $firefoxConsumer, Consumer $chromeConsumer, Consumer $instapaperConsumer, Consumer $pinboardConsumer, Consumer $deliciousConsumer, Consumer $elcuratorConsumer, Consumer $omnivoreConsumer) { $this->pocketConsumer = $pocketConsumer; $this->readabilityConsumer = $readabilityConsumer; @@ -33,6 +34,7 @@ class RabbitMQConsumerTotalProxy $this->pinboardConsumer = $pinboardConsumer; $this->deliciousConsumer = $deliciousConsumer; $this->elcuratorConsumer = $elcuratorConsumer; + $this->omnivoreConsumer = $omnivoreConsumer; } /** @@ -77,6 +79,9 @@ class RabbitMQConsumerTotalProxy case 'elcurator': $consumer = $this->elcuratorConsumer; break; + case 'omnivore': + $consumer = $this->omnivoreConsumer; + break; default: return 0; } diff --git a/src/Wallabag/ImportBundle/Controller/ImportController.php b/src/Wallabag/ImportBundle/Controller/ImportController.php index 9309d7d4e..16414d885 100644 --- a/src/Wallabag/ImportBundle/Controller/ImportController.php +++ b/src/Wallabag/ImportBundle/Controller/ImportController.php @@ -57,6 +57,7 @@ class ImportController extends AbstractController + $this->rabbitMQConsumerTotalProxy->getTotalMessage('pinboard') + $this->rabbitMQConsumerTotalProxy->getTotalMessage('delicious') + $this->rabbitMQConsumerTotalProxy->getTotalMessage('elcurator') + + $this->rabbitMQConsumerTotalProxy->getTotalMessage('omnivore') ; } catch (\Exception $e) { $rabbitNotInstalled = true; @@ -75,6 +76,7 @@ class ImportController extends AbstractController + $redis->llen('wallabag.import.pinboard') + $redis->llen('wallabag.import.delicious') + $redis->llen('wallabag.import.elcurator') + + $redis->llen('wallabag.import.omnivore') ; } catch (\Exception $e) { $redisNotInstalled = true; diff --git a/src/Wallabag/ImportBundle/Controller/OmnivoreController.php b/src/Wallabag/ImportBundle/Controller/OmnivoreController.php new file mode 100644 index 000000000..13bc12922 --- /dev/null +++ b/src/Wallabag/ImportBundle/Controller/OmnivoreController.php @@ -0,0 +1,84 @@ +rabbitMqProducer = $rabbitMqProducer; + $this->redisProducer = $redisProducer; + } + + /** + * @Route("/omnivore", name="import_omnivore") + */ + public function indexAction(Request $request, OmnivoreImport $omnivore, Config $craueConfig, TranslatorInterface $translator) + { + $form = $this->createForm(UploadImportType::class); + $form->handleRequest($request); + + $omnivore->setUser($this->getUser()); + + if ($craueConfig->get('import_with_rabbitmq')) { + $omnivore->setProducer($this->rabbitMqProducer); + } elseif ($craueConfig->get('import_with_redis')) { + $omnivore->setProducer($this->redisProducer); + } + + if ($form->isSubmitted() && $form->isValid()) { + $file = $form->get('file')->getData(); + $markAsRead = $form->get('mark_as_read')->getData(); + $name = 'omnivore_' . $this->getUser()->getId() . '.json'; + + if (null !== $file && \in_array($file->getClientMimeType(), $this->getParameter('wallabag_import.allow_mimetypes'), true) && $file->move($this->getParameter('wallabag_import.resource_dir'), $name)) { + $res = $omnivore + ->setFilepath($this->getParameter('wallabag_import.resource_dir') . '/' . $name) + ->setMarkAsRead($markAsRead) + ->import(); + + $message = 'flashes.import.notice.failed'; + + if (true === $res) { + $summary = $omnivore->getSummary(); + $message = $translator->trans('flashes.import.notice.summary', [ + '%imported%' => $summary['imported'], + '%skipped%' => $summary['skipped'], + ]); + + if (0 < $summary['queued']) { + $message = $translator->trans('flashes.import.notice.summary_with_queue', [ + '%queued%' => $summary['queued'], + ]); + } + + unlink($this->getParameter('wallabag_import.resource_dir') . '/' . $name); + } + + $this->addFlash('notice', $message); + + return $this->redirect($this->generateUrl('homepage')); + } + + $this->addFlash('notice', 'flashes.import.notice.failed_on_file'); + } + + return $this->render('@WallabagImport/Omnivore/index.html.twig', [ + 'form' => $form->createView(), + 'import' => $omnivore, + ]); + } +} diff --git a/src/Wallabag/ImportBundle/Import/OmnivoreImport.php b/src/Wallabag/ImportBundle/Import/OmnivoreImport.php new file mode 100644 index 000000000..d4b9e8110 --- /dev/null +++ b/src/Wallabag/ImportBundle/Import/OmnivoreImport.php @@ -0,0 +1,158 @@ +filepath = $filepath; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function import() + { + if (!$this->user) { + $this->logger->error('OmnivoreImport: user is not defined'); + + return false; + } + + if (!file_exists($this->filepath) || !is_readable($this->filepath)) { + $this->logger->error('OmnivoreImport: unable to read file', ['filepath' => $this->filepath]); + + return false; + } + + $data = json_decode(file_get_contents($this->filepath), true); + + if (empty($data)) { + $this->logger->error('OmnivoreImport: no entries in imported file'); + + return false; + } + + if ($this->producer) { + $this->parseEntriesForProducer($data); + + return true; + } + + $this->parseEntries($data); + + return true; + } + + /** + * {@inheritdoc} + */ + public function validateEntry(array $importedEntry) + { + if (empty($importedEntry['url'])) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function parseEntry(array $importedEntry) + { + $existingEntry = $this->em + ->getRepository(Entry::class) + ->findByUrlAndUserId($importedEntry['url'], $this->user->getId()); + + if (false !== $existingEntry) { + ++$this->skippedEntries; + + return; + } + + $data = [ + 'title' => $importedEntry['title'], + 'url' => $importedEntry['url'], + 'is_archived' => ('Archived' === $importedEntry['state']) || $this->markAsRead, + 'is_starred' => false, + 'created_at' => $importedEntry['savedAt'], + 'tags' => $importedEntry['labels'], + 'published_by' => [$importedEntry['author']], + 'published_at' => $importedEntry['publishedAt'], + 'preview_picture' => $importedEntry['thumbnail'], + ]; + + $entry = new Entry($this->user); + $entry->setUrl($data['url']); + $entry->setTitle($data['title']); + + // update entry with content (in case fetching failed, the given entry will be return) + $this->fetchContent($entry, $data['url'], $data); + + if (!empty($data['tags'])) { + $this->tagsAssigner->assignTagsToEntry( + $entry, + $data['tags'], + $this->em->getUnitOfWork()->getScheduledEntityInsertions() + ); + } + + $entry->updateArchived($data['is_archived']); + $entry->setCreatedAt(\DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $data['created_at'])); + if (null !== $data['published_at']) { + $entry->setPublishedAt(\DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $data['published_at'])); + } + $entry->setPublishedBy($data['published_by']); + $entry->setPreviewPicture($data['preview_picture']); + + $this->em->persist($entry); + ++$this->importedEntries; + + return $entry; + } + + /** + * {@inheritdoc} + */ + protected function setEntryAsRead(array $importedEntry) + { + return $importedEntry; + } +} diff --git a/src/Wallabag/ImportBundle/Resources/views/Omnivore/index.html.twig b/src/Wallabag/ImportBundle/Resources/views/Omnivore/index.html.twig new file mode 100644 index 000000000..4ce940d47 --- /dev/null +++ b/src/Wallabag/ImportBundle/Resources/views/Omnivore/index.html.twig @@ -0,0 +1,45 @@ +{% extends "@WallabagCore/layout.html.twig" %} + +{% block title %}{{ 'import.omnivore.page_title'|trans }}{% endblock %} + +{% block content %} +
+
+
+ {% include '@WallabagImport/Import/_information.html.twig' %} + +
+
{{ import.description|trans }}
+

{{ 'import.omnivore.how_to'|trans }}

+ +
+ {{ form_start(form, {'method': 'POST'}) }} + {{ form_errors(form) }} +
+
+ {{ form_errors(form.file) }} +
+ {{ form.file.vars.label|trans }} + {{ form_widget(form.file) }} +
+
+ +
+
+
+
{{ 'import.form.mark_as_read_title'|trans }}
+ {{ form_widget(form.mark_as_read) }} + {{ form_label(form.mark_as_read) }} +
+
+ + {{ form_widget(form.save, {'attr': {'class': 'btn waves-effect waves-light'}}) }} + + {{ form_rest(form) }} + +
+
+
+
+
+{% endblock %} diff --git a/tests/Wallabag/ImportBundle/Controller/ImportControllerTest.php b/tests/Wallabag/ImportBundle/Controller/ImportControllerTest.php index 06a2a2e21..cf1739841 100644 --- a/tests/Wallabag/ImportBundle/Controller/ImportControllerTest.php +++ b/tests/Wallabag/ImportBundle/Controller/ImportControllerTest.php @@ -24,6 +24,6 @@ class ImportControllerTest extends WallabagCoreTestCase $crawler = $client->request('GET', '/import/'); $this->assertSame(200, $client->getResponse()->getStatusCode()); - $this->assertSame(10, $crawler->filter('blockquote')->count()); + $this->assertSame(11, $crawler->filter('blockquote')->count()); } } diff --git a/tests/Wallabag/ImportBundle/Controller/OmnivoreControllerTest.php b/tests/Wallabag/ImportBundle/Controller/OmnivoreControllerTest.php new file mode 100644 index 000000000..9d10048bf --- /dev/null +++ b/tests/Wallabag/ImportBundle/Controller/OmnivoreControllerTest.php @@ -0,0 +1,203 @@ +logInAs('admin'); + $client = $this->getTestClient(); + + $crawler = $client->request('GET', '/import/omnivore'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + $this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count()); + $this->assertSame(1, $crawler->filter('input[type=file]')->count()); + } + + public function testImportOmnivoreWithRabbitEnabled() + { + $this->logInAs('admin'); + $client = $this->getTestClient(); + + $client->getContainer()->get(Config::class)->set('import_with_rabbitmq', 1); + + $crawler = $client->request('GET', '/import/omnivore'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + $this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count()); + $this->assertSame(1, $crawler->filter('input[type=file]')->count()); + + $client->getContainer()->get(Config::class)->set('import_with_rabbitmq', 0); + } + + public function testImportOmnivoreBadFile() + { + $this->logInAs('admin'); + $client = $this->getTestClient(); + + $crawler = $client->request('GET', '/import/omnivore'); + $form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form(); + + $data = [ + 'upload_import_file[file]' => '', + ]; + + $client->submit($form, $data); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + } + + public function testImportOmnivoreWithRedisEnabled() + { + $this->checkRedis(); + $this->logInAs('admin'); + $client = $this->getTestClient(); + $client->getContainer()->get(Config::class)->set('import_with_redis', 1); + + $crawler = $client->request('GET', '/import/omnivore'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + $this->assertSame(1, $crawler->filter('form[name=upload_import_file] > button[type=submit]')->count()); + $this->assertSame(1, $crawler->filter('input[type=file]')->count()); + + $form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form(); + + $file = new UploadedFile(__DIR__ . '/../fixtures/omnivore.json', 'omnivore.json'); + + $data = [ + 'upload_import_file[file]' => $file, + ]; + + $client->submit($form, $data); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + + $crawler = $client->followRedirect(); + + $this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text'])); + $this->assertStringContainsString('flashes.import.notice.summary', $body[0]); + + $this->assertNotEmpty($client->getContainer()->get(Client::class)->lpop('wallabag.import.omnivore')); + + $client->getContainer()->get(Config::class)->set('import_with_redis', 0); + } + + public function testImportOmnivoreWithFile() + { + $this->logInAs('admin'); + $client = $this->getTestClient(); + + $crawler = $client->request('GET', '/import/omnivore'); + $form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form(); + + $file = new UploadedFile(__DIR__ . '/../fixtures/omnivore.json', 'omnivore.json'); + + $data = [ + 'upload_import_file[file]' => $file, + ]; + + $client->submit($form, $data); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + + $crawler = $client->followRedirect(); + + $content = $client->getContainer() + ->get(EntityManagerInterface::class) + ->getRepository(Entry::class) + ->findByUrlAndUserId( + 'https://www.lemonde.fr/economie/article/2024/10/29/malgre-la-crise-du-marche-des-montres-breitling-etend-son-reseau-commercial-et-devoile-ses-ambitions_6365425_3234.html', + $this->getLoggedInUserId() + ); + + $this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text'])); + $this->assertStringContainsString('flashes.import.notice.summary', $body[0]); + + $this->assertInstanceOf(Entry::class, $content); + + $tags = $content->getTagsLabel(); + $this->assertContains('rss', $tags, 'It includes the "rss" tag'); + $this->assertGreaterThanOrEqual(2, \count($tags)); + + $this->assertInstanceOf(\DateTime::class, $content->getCreatedAt()); + $this->assertSame('2024-10-29', $content->getCreatedAt()->format('Y-m-d')); + } + + public function testImportOmnivoreWithFileAndMarkAllAsRead() + { + $this->logInAs('admin'); + $client = $this->getTestClient(); + + $crawler = $client->request('GET', '/import/omnivore'); + $form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form(); + + $file = new UploadedFile(__DIR__ . '/../fixtures/omnivore.json', 'omnivore-read.json'); + + $data = [ + 'upload_import_file[file]' => $file, + 'upload_import_file[mark_as_read]' => 1, + ]; + + $client->submit($form, $data); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + + $crawler = $client->followRedirect(); + + $content1 = $client->getContainer() + ->get(EntityManagerInterface::class) + ->getRepository(Entry::class) + ->findByUrlAndUserId( + 'https://www.lemonde.fr/economie/article/2024/10/29/l-union-europeenne-adopte-jusqu-a-35-de-surtaxes-sur-les-voitures-electriques-importees-de-chine_6365258_3234.html', + $this->getLoggedInUserId() + ); + + $this->assertInstanceOf(Entry::class, $content1); + + $content2 = $client->getContainer() + ->get(EntityManagerInterface::class) + ->getRepository(Entry::class) + ->findByUrlAndUserId( + 'https://www.lemonde.fr/les-decodeurs/article/2024/10/29/presidentielle-americaine-2024-comment-le-calendrier-de-l-election-et-des-affaires-judiciaires-de-trump-s-entremelent_6210916_3211.html', + $this->getLoggedInUserId() + ); + + $this->assertInstanceOf(Entry::class, $content2); + + $this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text'])); + $this->assertStringContainsString('flashes.import.notice.summary', $body[0]); + } + + public function testImportOmnivoreWithEmptyFile() + { + $this->logInAs('admin'); + $client = $this->getTestClient(); + + $crawler = $client->request('GET', '/import/omnivore'); + $form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form(); + + $file = new UploadedFile(__DIR__ . '/../fixtures/test.txt', 'test.txt'); + + $data = [ + 'upload_import_file[file]' => $file, + ]; + + $client->submit($form, $data); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + + $crawler = $client->followRedirect(); + + $this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text'])); + $this->assertStringContainsString('flashes.import.notice.failed', $body[0]); + } +} diff --git a/tests/Wallabag/ImportBundle/fixtures/omnivore.json b/tests/Wallabag/ImportBundle/fixtures/omnivore.json new file mode 100644 index 000000000..6e36e7150 --- /dev/null +++ b/tests/Wallabag/ImportBundle/fixtures/omnivore.json @@ -0,0 +1,343 @@ +[ + { + "id": "20db074a-34e1-4f55-b0e9-161e367946f6", + "slug": "malgre-la-crise-du-marche-des-montres-breitling-etend-son-reseau-192daf3a84e", + "title": "Malgré la crise du marché des montres, Breitling étend son réseau commercial et dévoile ses ambitions", + "description": "La marque suisse – peu présente en Chine – veut s’étendre ailleurs en Asie et n’exclut pas des acquisitions. En France, après s’être installée sur les Champs-Elysées, elle ouvre boutique à Lille et à Monaco.", + "author": "Juliette Garnier", + "url": "https://www.lemonde.fr/economie/article/2024/10/29/malgre-la-crise-du-marche-des-montres-breitling-etend-son-reseau-commercial-et-devoile-ses-ambitions_6365425_3234.html", + "state": "Archived", + "readingProgress": 0, + "thumbnail": "https://img.lemde.fr/2024/10/29/234/0/6192/3096/1440/720/60/0/5ee3f32_1730201062245-063-1432929408.jpg", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T16:30:11.000Z", + "updatedAt": "2024-10-31T13:01:28.320Z", + "publishedAt": "2024-10-29T16:30:11.000Z" + }, + { + "id": "5ace624c-ba30-48cc-82ba-5a4d585e0cd4", + "slug": "espagne-l-enquete-visant-l-epouse-de-pedro-sanchez-elargie-ce-de-192d9fe5ee4", + "title": "Espagne : l’enquête visant l’épouse de Pedro Sanchez élargie, ce dernier fait part de sa « tranquillité absolue »", + "description": "Begoña Gomez fait l’objet d’une enquête pour corruption et trafic d’influence, ouverte après des plaintes déposées par deux associations réputées proches de l’extrême droite.", + "author": "Le Monde avec AFP", + "url": "https://www.lemonde.fr/international/article/2024/10/29/espagne-l-enquete-visant-l-epouse-de-pedro-sanchez-elargie-ce-dernier-fait-part-de-sa-tranquillite-absolue_6365392_3210.html", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://img.lemde.fr/2024/10/29/385/0/3266/1633/1440/720/60/0/54ca5e3_2024-10-29t141242z-866115530-rc2g8aaxq4l9-rtrmadp-3-spain-argentina.JPG", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T16:21:32.000Z", + "updatedAt": "2024-10-29T20:36:19.400Z", + "publishedAt": "2024-10-29T16:21:32.000Z" + }, + { + "id": "1d72bb2e-2b3d-4d2d-8f17-90f441024358", + "slug": "montpellier-enquete-ouverte-apres-la-mort-d-une-jeune-femme-des--192d9fe4e61", + "title": "Montpellier : enquête ouverte après la mort d’une jeune femme des suites d’une méningite, malgré des appels au SAMU et aux pompiers", + "description": "Malgré deux appels aux secours, l’un au 15 et l’autre au 18, ce sont deux amis qui ont conduit la femme de 25 ans à une clinique montpelliéraine, avant qu’elle ne soit transférée au CHU et ne meure.", + "author": "Le Monde", + "url": "https://www.lemonde.fr/societe/article/2024/10/29/montpellier-enquete-ouverte-apres-la-mort-d-une-jeune-femme-des-suites-d-une-meningite-malgre-des-appels-au-samu-et-aux-pompiers_6365359_3224.html", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://img.lemde.fr/2024/10/24/187/0/2250/1125/1440/720/60/0/4e171be_1729786778722-frame-159.jpg", + "labels": [ + "RSS", + "TEST" + ], + "savedAt": "2024-10-29T16:09:19.000Z", + "updatedAt": "2024-10-31T13:03:21.779Z", + "publishedAt": "2024-10-29T16:09:19.000Z" + }, + { + "id": "5ac06f9c-52e8-47df-984c-038c501819cb", + "slug": "tuberculose-le-nombre-de-cas-dans-le-monde-se-stabilise-apres-le-192daa35669", + "title": "Tuberculose : le nombre de cas dans le monde se stabilise après le regain des années Covid", + "description": "L’incidence de la maladie est en baisse de 8,3 % par rapport aux chiffres de 2015, mais reste loin de l’objectif initialement fixé de diviser par deux le nombre de malades d’ici à 2025.", + "author": "Delphine Roucaute", + "url": "https://www.lemonde.fr/planete/article/2024/10/29/tuberculose-le-nombre-de-cas-dans-le-monde-se-stabilise-apres-le-regain-des-annees-covid_6365326_3244.html", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://img.lemde.fr/2024/10/29/501/0/6009/3004/1440/720/60/0/01bc1cc_1730215288364-000-34ne6t2.jpg", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T16:02:03.000Z", + "updatedAt": "2024-10-29T23:36:30.655Z", + "publishedAt": "2024-10-29T16:02:03.000Z" + }, + { + "id": "2f870f6d-79a9-45a4-8a64-e4cd3e7c4488", + "slug": "l-union-europeenne-adopte-jusqu-a-35-de-surtaxes-sur-les-voiture-192d9fe4fa0", + "title": "L’Union européenne adopte jusqu’à 35 % de surtaxes sur les voitures électriques importées de Chine", + "description": "L’objectif affiché est de rétablir des conditions de concurrence équitables avec des constructeurs accusés de profiter de subventions publiques massives.", + "author": "Le Monde avec AFP", + "url": "https://www.lemonde.fr/economie/article/2024/10/29/l-union-europeenne-adopte-jusqu-a-35-de-surtaxes-sur-les-voitures-electriques-importees-de-chine_6365258_3234.html", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://img.lemde.fr/2024/10/03/131/0/2405/1202/1440/720/60/0/9f89e19_5860742-01-06.jpg", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T15:36:53.000Z", + "updatedAt": "2024-10-29T20:36:15.460Z", + "publishedAt": "2024-10-29T15:36:53.000Z" + }, + { + "id": "fe9efcb1-1782-46c3-805d-c9b8074bee3b", + "slug": "l-ex-patron-de-la-dgse-bernard-bajolet-sera-juge-pour-complicite-192da0f4619", + "title": "L’ex-patron de la DGSE Bernard Bajolet sera jugé pour complicité de tentative d’extorsion", + "description": "L’homme d’affaires Alain Duménil accuse le service de renseignement d’avoir fait usage de la contrainte pour lui réclamer de l’argent en 2016.", + "author": "Le Monde avec AFP", + "url": "https://www.lemonde.fr/societe/article/2024/10/29/l-ex-patron-de-la-dgse-bernard-bajolet-sera-juge-pour-complicite-de-tentative-d-extorsion_6365225_3224.html", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://img.lemde.fr/2024/10/29/99/0/4804/2402/1440/720/60/0/e26408c_1730213831930-000-32hg2qa.jpg", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T15:32:35.000Z", + "updatedAt": "2024-10-29T20:54:47.245Z", + "publishedAt": "2024-10-29T15:32:35.000Z" + }, + { + "id": "885df5b1-b564-4535-8ad7-f65bb934375d", + "slug": "la-cour-d-appel-de-paris-confirme-le-proces-pour-viol-du-rappeur-192da4a24d7", + "title": "La cour d’appel de Paris confirme le procès pour viol du rappeur Naps", + "description": "L’artiste est soupçonné d’avoir violé une jeune femme pendant son sommeil à l’automne 2021.", + "author": "Le Monde avec AFP", + "url": "https://www.lemonde.fr/societe/article/2024/10/29/la-cour-d-appel-de-paris-confirme-le-proces-pour-viol-du-rappeur-naps_6365192_3224.html", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://img.lemde.fr/2024/10/29/369/0/4430/2215/1440/720/60/0/78d02d3_1730214584579-000-9t294r.jpg", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T15:26:15.000Z", + "updatedAt": "2024-10-29T21:59:05.276Z", + "publishedAt": "2024-10-29T15:26:15.000Z" + }, + { + "id": "647e36c0-2782-42b0-8694-8599b61e912a", + "slug": "les-serbes-apres-leur-medaille-de-bronze-aux-jo-on-a-bu-pendant--192d9ed5eef", + "title": "Les Serbes après leur médaille de bronze aux JO : \"On a bu pendant huit heures !\"", + "description": "Lors de la cérémonie de remise de médailles des Jeux Olympiques de Paris, les Serbes se sont fait remarquer en titubant sur le podium. La raison est simple...", + "author": "La rédaction", + "url": "https://www.basketeurope.com/les-serbes-apres-leur-medaille-de-bronze-aux-j0-on-a-bu-pendant-huit-heures/", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://www.basketeurope.com/content/images/2024/10/Marinkovic.webp", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T13:59:23.000Z", + "updatedAt": "2024-10-29T20:17:45.252Z", + "publishedAt": "2024-10-29T13:59:23.000Z" + }, + { + "id": "40c17460-d4bc-4408-aafb-3f6ffbe8d413", + "slug": "a-quoi-ressemble-le-parcours-du-tour-de-france-2025-192d9fe6f1d", + "title": "A quoi ressemble le parcours du Tour de France 2025 ?", + "description": "Le parcours de la prochaine Grande Boucle cycliste a été dévoilé mardi. Le peloton s’élancera de Lille pour retrouver, trois semaines plus tard, la traditionnelle arrivée sur les Champs-Elysées, à Paris. Entre-temps, il lui faudra enchaîner les cols mythiques.", + "author": "Valentin Moinard", + "url": "https://www.lemonde.fr/sport/article/2024/10/29/cyclisme-le-tour-de-france-2025-fera-la-part-belle-aux-grimpeurs_6364955_3242.html", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://img.lemde.fr/2024/07/07/410/0/5994/2997/1440/720/60/0/1df95b2_5013615-01-06.jpg", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T13:12:05.000Z", + "updatedAt": "2024-10-29T20:36:23.653Z", + "publishedAt": "2024-10-29T13:12:05.000Z" + }, + { + "id": "d4eb58ef-1cab-4aef-aee1-89dca60b8a80", + "slug": "arrets-maladie-des-fonctionnaires-les-arguments-discutables-du-g-192d9fe680a", + "title": "Arrêts maladie des fonctionnaires : les arguments discutables du gouvernement pour justifier sa réforme", + "description": "Alors que l’exécutif souhaite ne plus payer les trois premiers jours d’absence des agents publics, le fait qu’il prenne peu en compte l’amélioration de la qualité de vie au travail lui vaut de vives critiques, de la part des syndicats, mais aussi de personnalités ayant l’expérience du terrain.", + "author": "Bertrand Bissuel", + "url": "https://www.lemonde.fr/politique/article/2024/10/29/arrets-maladie-des-fonctionnaires-les-arguments-discutables-du-gouvernement-pour-justifier-sa-reforme_6364919_823448.html", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://img.lemde.fr/2024/10/29/265/0/6720/3360/1440/720/60/0/2f2f937_1730191618529-cbi1223010.jpg", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T13:00:08.000Z", + "updatedAt": "2024-10-29T20:36:21.703Z", + "publishedAt": "2024-10-29T13:00:08.000Z" + }, + { + "id": "ff9c411a-e91f-424b-a570-f92b311026a5", + "slug": "premium-jean-denys-choulet-selectionneur-du-kosovo-ma-nationalit-192db836507", + "title": "[Premium] Jean-Denys Choulet, sélectionneur du Kosovo : \"Ma nationalité, c'est le basket\"", + "description": "Huit mois après son licenciement de la Chorale de Roanne, Jean-Denys Choulet a retrouvé un poste en tant que sélectionneur du Kosovo. À 66 ans, il ne se voyait pas quitter le milieu du basket et reste ouvert à l'idée de diriger un club.", + "author": "Morgan Parmentier", + "url": "https://www.basketeurope.com/premium-jean-denys-choulet-ma-nationalite-cest-le-basket/", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://www.basketeurope.com/content/images/size/w1200/2024/10/choulet-jd-chorale-roanne-tuan-nguyen.webp", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T12:31:47.000Z", + "updatedAt": "2024-10-30T03:41:14.412Z", + "publishedAt": "2024-10-29T12:31:47.000Z" + }, + { + "id": "54ae4346-03c0-407e-b204-b09351174365", + "slug": "pedocriminalite-l-eglise-doit-mieux-sanctionner-les-auteurs-et-a-192da2d8b8f", + "title": "Pédocriminalité : l’Eglise doit mieux sanctionner les auteurs et aider les victimes, selon un rapport du Vatican", + "description": "En avril 2022, le pape François avait demandé à une commission pontificale un rapport sur la protection des mineurs dans l’Eglise. Très attendu, il vient d’être publié par le Saint-Siège.", + "author": "Le Monde avec AFP", + "url": "https://www.lemonde.fr/international/article/2024/10/29/pedocriminalite-l-eglise-doit-mieux-sanctionner-les-auteurs-et-aider-les-victimes-selon-un-rapport-du-vatican_6364916_3210.html", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://img.lemde.fr/2024/10/27/688/0/8256/4128/1440/720/60/0/a7b4e07_5084272-01-06.jpg", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T12:26:36.000Z", + "updatedAt": "2024-10-29T21:27:50.999Z", + "publishedAt": "2024-10-29T12:26:36.000Z" + }, + { + "id": "b2d30e8c-c344-4460-a583-4a2955fdc197", + "slug": "cybercriminalite-les-stealers-redline-et-meta-vises-par-une-oper-192d90d1624", + "title": "Cybercriminalité : les « stealers » Redline et META visés par une opération policière internationale", + "description": "Le marché des identifiants dérobés est devenu un secteur central de la cybercriminalité. Les deux virus visés par cette opération policière, baptisée « Magnus », ont permis le vol de plus de 227 millions de mots de passe en 2024.", + "author": "Florian Reynaud", + "url": "https://www.lemonde.fr/pixels/article/2024/10/29/cybercriminalite-les-stealers-redline-et-meta-vises-par-une-operation-policiere-internationale_6364915_4408996.html", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://img.lemde.fr/2024/10/10/252/0/3008/1504/1440/720/60/0/b3dbd8e_1728546840972-papier-271-16.jpg", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T12:21:06.000Z", + "updatedAt": "2024-10-29T16:12:46.658Z", + "publishedAt": "2024-10-29T12:21:06.000Z" + }, + { + "id": "a4a6e509-beab-4577-99db-2f1db819d669", + "slug": "au-maroc-emmanuel-macron-appelle-a-plus-de-resultats-contre-l-im-192d90d159a", + "title": "Au Maroc, Emmanuel Macron appelle à plus de « résultats » contre l’immigration illégale et réaffirme son soutien à la « souveraineté marocaine » au Sahara occidental", + "description": "Le chef de l’Etat français a également proposé au roi du Maroc, Mohammed VI, de signer un nouveau « cadre stratégique » bilatéral en 2025 à Paris, soixante-dix ans après la déclaration de la Celle-Saint-Cloud qui scella l’indépendance du Maroc de la France.", + "author": "Le Monde avec AFP", + "url": "https://www.lemonde.fr/international/article/2024/10/29/au-maroc-emmanuel-macron-appelle-a-plus-de-resultats-contre-l-immigration-illegale-et-reaffirme-son-soutien-a-la-souverainete-marocaine-au-sahara-occidental_6364914_3210.html", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://img.lemde.fr/2024/10/29/350/0/4201/2100/1440/720/60/0/c37301c_5103313-01-06.jpg", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T12:14:53.000Z", + "updatedAt": "2024-10-29T16:12:46.456Z", + "publishedAt": "2024-10-29T12:14:53.000Z" + }, + { + "id": "0ab9e8c7-c88b-4a31-af3e-62ec4766b99d", + "slug": "prison-de-noumea-l-etat-condamne-car-trop-lent-a-ameliorer-les-c-192d90d0722", + "title": "Prison de Nouméa : l’Etat condamné car trop lent à améliorer les conditions de détention", + "description": "En 2020, le Conseil d’Etat avait exigé des mesures urgentes pour les droits des détenus au Camp-Est, mais l’administration a pris du retard et les travaux n’auront pas lieu avant 2028, selon l’Observatoire international des prisons.", + "author": "Le Monde avec AFP", + "url": "https://www.lemonde.fr/societe/article/2024/10/29/prison-de-noumea-l-etat-condamne-car-trop-lent-a-ameliorer-les-conditions-de-detention_6364912_3224.html", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://img.lemde.fr/2024/10/29/520/0/4160/2080/1440/720/60/0/9ec59fb_1730203377797-cdo-cellule-case-g-cp-nouma-a-2.jpg", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T12:08:18.000Z", + "updatedAt": "2024-10-29T16:12:42.744Z", + "publishedAt": "2024-10-29T12:08:18.000Z" + }, + { + "id": "fe79a70e-d1e1-43fe-8555-094209d1b48d", + "slug": "tour-de-france-femmes-2025-la-course-traversera-l-hexagone-d-oue-192da131a9a", + "title": "Tour de France Femmes 2025 : la course traversera l’Hexagone d’Ouest en Est, de la Bretagne aux Alpes", + "description": "Le parcours de la quatrième édition de la course cycliste a été présenté mardi. Du 26 juillet au 3 août, il fera la part belle aux grimpeuses, qui auront trois étapes finales dans le massif alpin pour s’illustrer.", + "author": "Valentin Moinard", + "url": "https://www.lemonde.fr/sport/article/2024/10/29/tour-de-france-femmes-2025-de-la-bretagne-aux-alpes-la-course-traversera-la-france-d-ouest-en-est_6364908_3242.html", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://img.lemde.fr/2024/08/17/1194/0/7268/3634/1440/720/60/0/8bafa27_5442255-01-06.jpg", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T11:52:45.000Z", + "updatedAt": "2024-10-29T20:58:58.164Z", + "publishedAt": "2024-10-29T11:52:45.000Z" + }, + { + "id": "54687e28-21e1-42b4-bb92-cc5f39716464", + "slug": "en-direct-guerre-au-proche-orient-le-bombardement-israelien-sur--192da214287", + "title": "En direct, guerre au Proche-Orient : le bombardement israélien sur le nord de la bande de Gaza a fait 93 morts, selon un nouveau bilan de la défense civile", + "description": "Un précédent bilan, établi par la même source, faisait état de 55 morts. L’attaque aérienne, qui a eu lieu dans la nuit de lundi à mardi, a visé la ville de Beit Lahya, dans le nord de la bande de Gaza.", + "author": "Seb2000", + "url": "https://www.lemonde.fr/international/live/2024/10/29/en-direct-guerre-au-proche-orient-le-bombardement-israelien-sur-le-nord-de-la-bande-de-gaza-a-fait-93-morts-selon-un-nouveau-bilan-de-la-defense-civile_6362390_3210.html", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://img.lemde.fr/2024/10/29/303/0/3644/1822/1440/720/60/0/6b6faba_1730210101426-468617.jpg", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T11:52:34.000Z", + "updatedAt": "2024-10-29T21:14:26.095Z", + "publishedAt": "2024-10-29T11:52:34.000Z" + }, + { + "id": "a2b54509-62ad-4ab7-a68a-2a970ac25952", + "slug": "au-chili-le-gouvernement-de-gauche-malmene-aux-elections-locales-192da0e7d74", + "title": "Au Chili, le gouvernement de gauche malmené aux élections locales", + "description": "La droite de la coalition Chile Vamos sort renforcée des élections municipales et régionales, tandis que l’extrême droite progresse, sans enregistrer la percée qu’elle espérait.", + "author": "Flora Genoux", + "url": "https://www.lemonde.fr/international/article/2024/10/29/au-chili-le-gouvernement-de-gauche-malmene-aux-elections-locales_6364875_3210.html", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://img.lemde.fr/2024/10/29/720/0/8640/4320/1440/720/60/0/3632d71_1730193220296-852876.jpg", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T11:31:53.000Z", + "updatedAt": "2024-10-29T20:53:55.779Z", + "publishedAt": "2024-10-29T11:31:53.000Z" + }, + { + "id": "9c68ff83-1927-4377-a3b0-a048516a725d", + "slug": "en-isere-lyes-louffok-est-le-candidat-insoumis-investi-a-l-elect-192d90d319e", + "title": "En Isère, Lyes Louffok est le candidat « insoumis » investi à l’élection législative partielle", + "description": "Le siège est vacant depuis la démission d’Hugo Prevost (LFI), accusé de violences sexistes et sexuelles.", + "author": "Le Monde avec AFP", + "url": "https://www.lemonde.fr/politique/article/2024/10/29/en-isere-lyes-louffok-est-le-candidat-insoumis-investi-a-l-election-legislative-partielle_6364842_823448.html", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://img.lemde.fr/2024/10/29/552/0/6628/3314/1440/720/60/0/607f0f6_1730199775062-000-34r33w7.jpg", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T11:24:36.000Z", + "updatedAt": "2024-10-29T16:12:53.630Z", + "publishedAt": "2024-10-29T11:24:36.000Z" + }, + { + "id": "1015c224-bd79-4ed9-9268-8fa9803a52cd", + "slug": "presidentielle-americaine-2024-comment-le-calendrier-de-l-electi-192d90d3440", + "title": "Présidentielle américaine 2024 : comment le calendrier de l’élection et des affaires judiciaires de Trump s’entremêlent", + "description": "Le verdict du procès visant l’ancien président ne devrait être connu qu’après l’élection, et le reste des poursuites pénales reste incertain. « Le Monde » vous propose de suivre le déroulé, mis à jour continuellement, de cette année décisive pour les Etats-Unis.", + "author": "Gary Dagorn, Jean-Philippe Lefief", + "url": "https://www.lemonde.fr/les-decodeurs/article/2024/10/29/presidentielle-americaine-2024-comment-le-calendrier-de-l-election-et-des-affaires-judiciaires-de-trump-s-entremelent_6210916_3211.html", + "state": "Active", + "readingProgress": 0, + "thumbnail": "https://img.lemde.fr/2024/01/12/0/0/1500/750/1440/720/60/0/e9367e3_1705065713486-chrono-media-appel.png", + "labels": [ + "RSS" + ], + "savedAt": "2024-10-29T11:17:19.000Z", + "updatedAt": "2024-10-29T16:12:54.352Z", + "publishedAt": "2024-10-29T11:17:19.000Z" + } +] \ No newline at end of file diff --git a/translations/messages.en.yml b/translations/messages.en.yml index e06387742..312581153 100644 --- a/translations/messages.en.yml +++ b/translations/messages.en.yml @@ -503,6 +503,10 @@ import: page_title: 'Import > elCurator' description: 'This importer will import all your elCurator articles.' how_to: Please select your elCurator export and click on the button below to upload and import it. + omnivore: + page_title: 'Import > Omnivore' + description: 'This importer will import all your Omnivore articles.' + how_to: Please unzip your Omnivore export, then upload each JSON file named "metadata_x_to_y.json" one by one. readability: page_title: Import > Readability description: This importer will import all your Readability articles. diff --git a/web/wallassets/41a1d465797f51702171.otf b/web/wallassets/41a1d465797f51702171.otf new file mode 100644 index 0000000000000000000000000000000000000000..1a3027223aab6cc62f0855a7d4d064669705b81b GIT binary patch literal 49652 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 literal 0 HcmV?d00001