Add Delicious import

Since 2021, you can export again your data \o/

Also fix indentation in json fixtures files.
This commit is contained in:
Jeremy Benoist 2021-02-08 09:08:12 +01:00
parent 890c7d0bfa
commit dd9d6a4c64
No known key found for this signature in database
GPG key ID: BCA73962457ACC3C
23 changed files with 984 additions and 370 deletions

View file

@ -248,6 +248,11 @@ old_sound_rabbit_mq:
exchange_options:
name: 'wallabag.import.pinboard'
type: topic
import_delicious:
connection: default
exchange_options:
name: 'wallabag.import.delicious'
type: topic
import_instapaper:
connection: default
exchange_options:
@ -315,6 +320,15 @@ old_sound_rabbit_mq:
name: 'wallabag.import.pinboard'
callback: wallabag_import.consumer.amqp.pinboard
qos_options: {prefetch_count: "%rabbitmq_prefetch_count%"}
import_delicious:
connection: default
exchange_options:
name: 'wallabag.import.delicious'
type: topic
queue_options:
name: 'wallabag.import.delicious'
callback: wallabag_import.consumer.amqp.delicious
qos_options: {prefetch_count: "%rabbitmq_prefetch_count%"}
import_wallabag_v1:
connection: default
exchange_options:

View file

@ -517,6 +517,10 @@ import:
page_title: Import > Pinboard
description: This importer will import all your Pinboard articles. On the backup (https://pinboard.in/settings/backup) page, click on "JSON" in the "Bookmarks" section. A JSON file will be downloaded (like "pinboard_export").
how_to: Please select your Pinboard export and click on the below button to upload and import it.
delicious:
page_title: Import > del.icio.us
description: This importer will import all your Delicious bookmarks. Since 2021, you can export again your data from it using the export page (https://del.icio.us/export). Choose the "JSON" format and download it (like "delicious_export.2021.02.06_21.10.json").
how_to: Please select your Delicious export and click on the below button to upload and import it.
developer:
page_title: API clients management
welcome_message: Welcome to the wallabag API

View file

@ -19,7 +19,7 @@ class ImportCommand extends ContainerAwareCommand
->setDescription('Import entries from a JSON export')
->addArgument('username', InputArgument::REQUIRED, 'User to populate')
->addArgument('filepath', InputArgument::REQUIRED, 'Path to the JSON file')
->addOption('importer', null, InputOption::VALUE_OPTIONAL, 'The importer to use: v1, v2, instapaper, pinboard, readability, firefox or chrome', 'v1')
->addOption('importer', null, InputOption::VALUE_OPTIONAL, 'The importer to use: v1, v2, instapaper, pinboard, delicious, readability, firefox or chrome', 'v1')
->addOption('markAsRead', null, InputOption::VALUE_OPTIONAL, 'Mark all entries as read', false)
->addOption('useUserId', null, InputOption::VALUE_NONE, 'Use user id instead of username to find account')
->addOption('disableContentUpdate', null, InputOption::VALUE_NONE, 'Disable fetching updated content from URL')
@ -77,6 +77,9 @@ class ImportCommand extends ContainerAwareCommand
case 'pinboard':
$import = $this->getContainer()->get('wallabag_import.pinboard.import');
break;
case 'delicious':
$import = $this->getContainer()->get('wallabag_import.delicious.import');
break;
default:
$import = $this->getContainer()->get('wallabag_import.wallabag_v1.import');
}

View file

@ -17,7 +17,7 @@ class RedisWorkerCommand extends ContainerAwareCommand
$this
->setName('wallabag:import:redis-worker')
->setDescription('Launch Redis worker')
->addArgument('serviceName', InputArgument::REQUIRED, 'Service to use: wallabag_v1, wallabag_v2, pocket, readability, pinboard, firefox, chrome or instapaper')
->addArgument('serviceName', InputArgument::REQUIRED, 'Service to use: wallabag_v1, wallabag_v2, pocket, readability, pinboard, delicious, firefox, chrome or instapaper')
->addOption('maxIterations', '', InputOption::VALUE_OPTIONAL, 'Number of iterations before stoping', false)
;
}

View file

@ -0,0 +1,77 @@
<?php
namespace Wallabag\ImportBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Wallabag\ImportBundle\Form\Type\UploadImportType;
class DeliciousController extends Controller
{
/**
* @Route("/delicious", name="import_delicious")
*/
public function indexAction(Request $request)
{
$form = $this->createForm(UploadImportType::class);
$form->handleRequest($request);
$delicious = $this->get('wallabag_import.delicious.import');
$delicious->setUser($this->getUser());
if ($this->get('craue_config')->get('import_with_rabbitmq')) {
$delicious->setProducer($this->get('old_sound_rabbit_mq.import_delicious_producer'));
} elseif ($this->get('craue_config')->get('import_with_redis')) {
$delicious->setProducer($this->get('wallabag_import.producer.redis.delicious'));
}
if ($form->isSubmitted() && $form->isValid()) {
$file = $form->get('file')->getData();
$markAsRead = $form->get('mark_as_read')->getData();
$name = 'delicious_' . $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 = $delicious
->setFilepath($this->getParameter('wallabag_import.resource_dir') . '/' . $name)
->setMarkAsRead($markAsRead)
->import();
$message = 'flashes.import.notice.failed';
if (true === $res) {
$summary = $delicious->getSummary();
$message = $this->get('translator')->trans('flashes.import.notice.summary', [
'%imported%' => $summary['imported'],
'%skipped%' => $summary['skipped'],
]);
if (0 < $summary['queued']) {
$message = $this->get('translator')->trans('flashes.import.notice.summary_with_queue', [
'%queued%' => $summary['queued'],
]);
}
unlink($this->getParameter('wallabag_import.resource_dir') . '/' . $name);
}
$this->get('session')->getFlashBag()->add(
'notice',
$message
);
return $this->redirect($this->generateUrl('homepage'));
}
$this->get('session')->getFlashBag()->add(
'notice',
'flashes.import.notice.failed_on_file'
);
}
return $this->render('WallabagImportBundle:Delicious:index.html.twig', [
'form' => $form->createView(),
'import' => $delicious,
]);
}
}

View file

@ -43,6 +43,7 @@ class ImportController extends Controller
+ $this->getTotalMessageInRabbitQueue('chrome')
+ $this->getTotalMessageInRabbitQueue('instapaper')
+ $this->getTotalMessageInRabbitQueue('pinboard')
+ $this->getTotalMessageInRabbitQueue('delicious')
+ $this->getTotalMessageInRabbitQueue('elcurator')
;
} catch (\Exception $e) {
@ -60,6 +61,7 @@ class ImportController extends Controller
+ $redis->llen('wallabag.import.chrome')
+ $redis->llen('wallabag.import.instapaper')
+ $redis->llen('wallabag.import.pinboard')
+ $redis->llen('wallabag.import.delicious')
+ $redis->llen('wallabag.import.elcurator')
;
} catch (\Exception $e) {

View file

@ -0,0 +1,151 @@
<?php
namespace Wallabag\ImportBundle\Import;
use Wallabag\CoreBundle\Entity\Entry;
class DeliciousImport extends AbstractImport
{
private $filepath;
/**
* {@inheritdoc}
*/
public function getName()
{
return 'Delicious';
}
/**
* {@inheritdoc}
*/
public function getUrl()
{
return 'import_delicious';
}
/**
* {@inheritdoc}
*/
public function getDescription()
{
return 'import.delicious.description';
}
/**
* Set file path to the json file.
*
* @param string $filepath
*/
public function setFilepath($filepath)
{
$this->filepath = $filepath;
return $this;
}
/**
* {@inheritdoc}
*/
public function import()
{
if (!$this->user) {
$this->logger->error('DeliciousImport: user is not defined');
return false;
}
if (!file_exists($this->filepath) || !is_readable($this->filepath)) {
$this->logger->error('DeliciousImport: unable to read file', ['filepath' => $this->filepath]);
return false;
}
$data = json_decode(file_get_contents($this->filepath), true);
if (empty($data)) {
$this->logger->error('DeliciousImport: 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('WallabagCoreBundle:Entry')
->findByUrlAndUserId($importedEntry['url'], $this->user->getId());
if (false !== $existingEntry) {
++$this->skippedEntries;
return;
}
$data = [
'title' => $importedEntry['title'],
'url' => $importedEntry['url'],
'is_archived' => $this->markAsRead,
'is_starred' => false,
'created_at' => $importedEntry['created'],
'tags' => $importedEntry['tags'],
];
$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->setStarred($data['is_starred']);
$entry->setCreatedAt(\DateTime::createFromFormat('U', $data['created_at']));
$this->em->persist($entry);
++$this->importedEntries;
return $entry;
}
/**
* {@inheritdoc}
*/
protected function setEntryAsRead(array $importedEntry)
{
return $importedEntry;
}
}

View file

@ -32,6 +32,14 @@ services:
- "@wallabag_import.pinboard.import"
- "@event_dispatcher"
- "@logger"
wallabag_import.consumer.amqp.delicious:
class: Wallabag\ImportBundle\Consumer\AMQPEntryConsumer
arguments:
- "@doctrine.orm.entity_manager"
- "@wallabag_user.user_repository"
- "@wallabag_import.delicious.import"
- "@event_dispatcher"
- "@logger"
wallabag_import.consumer.amqp.wallabag_v1:
class: Wallabag\ImportBundle\Consumer\AMQPEntryConsumer
arguments:

View file

@ -63,6 +63,27 @@ services:
- "@event_dispatcher"
- "@logger"
# delicious
wallabag_import.queue.redis.delicious:
class: Simpleue\Queue\RedisQueue
arguments:
- "@wallabag_core.redis.client"
- "wallabag.import.delicious"
wallabag_import.producer.redis.delicious:
class: Wallabag\ImportBundle\Redis\Producer
arguments:
- "@wallabag_import.queue.redis.delicious"
wallabag_import.consumer.redis.delicious:
class: Wallabag\ImportBundle\Consumer\RedisEntryConsumer
arguments:
- "@doctrine.orm.entity_manager"
- "@wallabag_user.user_repository"
- "@wallabag_import.delicious.import"
- "@event_dispatcher"
- "@logger"
# pocket
wallabag_import.queue.redis.pocket:
class: Simpleue\Queue\RedisQueue

View file

@ -96,6 +96,18 @@ services:
tags:
- { name: wallabag_import.import, alias: pinboard }
wallabag_import.delicious.import:
class: Wallabag\ImportBundle\Import\DeliciousImport
arguments:
- "@doctrine.orm.entity_manager"
- "@wallabag_core.content_proxy"
- "@wallabag_core.tags_assigner"
- "@event_dispatcher"
calls:
- [ setLogger, [ "@logger" ]]
tags:
- { name: wallabag_import.import, alias: delicious }
wallabag_import.firefox.import:
class: Wallabag\ImportBundle\Import\FirefoxImport
arguments:

View file

@ -0,0 +1,45 @@
{% extends "WallabagCoreBundle::layout.html.twig" %}
{% block title %}{{ 'import.delicious.page_title'|trans }}{% endblock %}
{% block content %}
<div class="row">
<div class="col s12">
<div class="card-panel settings">
{% include 'WallabagImportBundle:Import:_information.html.twig' %}
<div class="row">
<blockquote>{{ import.description|trans }}</blockquote>
<p>{{ 'import.delicious.how_to'|trans }}</p>
<div class="col s12">
{{ form_start(form, {'method': 'POST'}) }}
{{ form_errors(form) }}
<div class="row">
<div class="file-field input-field col s12">
{{ form_errors(form.file) }}
<div class="btn">
<span>{{ form.file.vars.label|trans }}</span>
{{ form_widget(form.file) }}
</div>
<div class="file-path-wrapper">
<input class="file-path validate" type="text">
</div>
</div>
<div class="input-field col s6 with-checkbox">
<h6>{{ 'import.form.mark_as_read_title'|trans }}</h6>
{{ form_widget(form.mark_as_read) }}
{{ form_label(form.mark_as_read) }}
</div>
</div>
{{ form_widget(form.save, { 'attr': {'class': 'btn waves-effect waves-light'} }) }}
{{ form_rest(form) }}
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,199 @@
<?php
namespace Tests\Wallabag\ImportBundle\Controller;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Tests\Wallabag\CoreBundle\WallabagCoreTestCase;
class DeliciousControllerTest extends WallabagCoreTestCase
{
public function testImportDelicious()
{
$this->logInAs('admin');
$client = $this->getClient();
$crawler = $client->request('GET', '/import/delicious');
$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 testImportDeliciousWithRabbitEnabled()
{
$this->logInAs('admin');
$client = $this->getClient();
$client->getContainer()->get('craue_config')->set('import_with_rabbitmq', 1);
$crawler = $client->request('GET', '/import/delicious');
$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('craue_config')->set('import_with_rabbitmq', 0);
}
public function testImportDeliciousBadFile()
{
$this->logInAs('admin');
$client = $this->getClient();
$crawler = $client->request('GET', '/import/delicious');
$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 testImportDeliciousWithRedisEnabled()
{
$this->checkRedis();
$this->logInAs('admin');
$client = $this->getClient();
$client->getContainer()->get('craue_config')->set('import_with_redis', 1);
$crawler = $client->request('GET', '/import/delicious');
$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/delicious_export.2021.02.06_21.10.json', 'delicious.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('wallabag_core.redis.client')->lpop('wallabag.import.delicious'));
$client->getContainer()->get('craue_config')->set('import_with_redis', 0);
}
public function testImportDeliciousWithFile()
{
$this->logInAs('admin');
$client = $this->getClient();
$crawler = $client->request('GET', '/import/delicious');
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$file = new UploadedFile(__DIR__ . '/../fixtures/delicious_export.2021.02.06_21.10.json', 'delicious.json');
$data = [
'upload_import_file[file]' => $file,
];
$client->submit($form, $data);
$this->assertSame(302, $client->getResponse()->getStatusCode());
$crawler = $client->followRedirect();
$content = $client->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry')
->findByUrlAndUserId(
'https://feross.org/spoofmac/',
$this->getLoggedInUserId()
);
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('flashes.import.notice.summary', $body[0]);
$this->assertInstanceOf('Wallabag\CoreBundle\Entity\Entry', $content);
$tags = $content->getTags();
$this->assertContains('osx', $tags, 'It includes the "osx" tag');
$this->assertGreaterThanOrEqual(4, count($tags));
$this->assertInstanceOf(\DateTime::class, $content->getCreatedAt());
$this->assertSame('2013-01-17', $content->getCreatedAt()->format('Y-m-d'));
}
public function testImportDeliciousWithFileAndMarkAllAsRead()
{
$this->logInAs('admin');
$client = $this->getClient();
$crawler = $client->request('GET', '/import/delicious');
$form = $crawler->filter('form[name=upload_import_file] > button[type=submit]')->form();
$file = new UploadedFile(__DIR__ . '/../fixtures/delicious_export.2021.02.06_21.10.json', 'delicious-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('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry')
->findByUrlAndUserId(
'https://stackoverflow.com/review/',
$this->getLoggedInUserId()
);
$this->assertInstanceOf('Wallabag\CoreBundle\Entity\Entry', $content1);
$content2 = $client->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry')
->findByUrlAndUserId(
'https://addyosmani.com/basket.js/',
$this->getLoggedInUserId()
);
$this->assertInstanceOf('Wallabag\CoreBundle\Entity\Entry', $content2);
$this->assertGreaterThan(1, $body = $crawler->filter('body')->extract(['_text']));
$this->assertStringContainsString('flashes.import.notice.summary', $body[0]);
}
public function testImportDeliciousWithEmptyFile()
{
$this->logInAs('admin');
$client = $this->getClient();
$crawler = $client->request('GET', '/import/delicious');
$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]);
}
}

View file

@ -1,36 +1,38 @@
{
"checksum": "f3aa0e9c0edad632a246f7e98ec64918",
"roots": {
"bookmark_bar": {
"children": [ {
"date_added": "13118850929335823",
"id": "6",
"name": "\"La multiplication des chefs de projet est une catastrophe managériale majeure\", affirme le sociologue François Dupuy - Ressources humaines",
"type": "url",
"url": "http://www.usinenouvelle.com/article/la-multiplication-des-chefs-de-projet-est-une-catastrophe-manageriale-majeure-affirme-le-sociologue-francois-dupuy.N307730"
} ],
"date_added": "13118829474385693",
"date_modified": "13118850929335823",
"id": "1",
"name": "Barre de favoris",
"type": "folder"
},
"other": {
"children": [ ],
"date_added": "13118829474385701",
"date_modified": "0",
"id": "2",
"name": "Autres favoris",
"type": "folder"
},
"synced": {
"children": [ ],
"date_added": "13118829474385702",
"date_modified": "0",
"id": "3",
"name": "Favoris sur mobile",
"type": "folder"
}
},
"version": 1
"checksum": "f3aa0e9c0edad632a246f7e98ec64918",
"roots": {
"bookmark_bar": {
"children": [
{
"date_added": "13118850929335823",
"id": "6",
"name": "\"La multiplication des chefs de projet est une catastrophe managériale majeure\", affirme le sociologue François Dupuy - Ressources humaines",
"type": "url",
"url": "http://www.usinenouvelle.com/article/la-multiplication-des-chefs-de-projet-est-une-catastrophe-manageriale-majeure-affirme-le-sociologue-francois-dupuy.N307730"
}
],
"date_added": "13118829474385693",
"date_modified": "13118850929335823",
"id": "1",
"name": "Barre de favoris",
"type": "folder"
},
"other": {
"children": [],
"date_added": "13118829474385701",
"date_modified": "0",
"id": "2",
"name": "Autres favoris",
"type": "folder"
},
"synced": {
"children": [],
"date_added": "13118829474385702",
"date_modified": "0",
"id": "3",
"name": "Favoris sur mobile",
"type": "folder"
}
},
"version": 1
}

View file

@ -0,0 +1,44 @@
[
{
"title": "basket.js - a simple script loader that caches scripts with localStorage",
"tags": [
"basket",
"javascript",
"loader",
"localStorage"
],
"url": "https://addyosmani.com/basket.js/",
"description": "\"A simple (proof-of-concept) script loader that caches scripts with localStorage\"",
"created": "1358531607",
"others": 9,
"owner": "maciej",
"private": "0"
},
{
"title": "Review - Stack Overflow",
"tags": [
""
],
"url": "https://stackoverflow.com/review/",
"description": "",
"created": "1358457437",
"others": 84,
"owner": "maciej",
"private": "0"
},
{
"title": "Announcing SpoofMAC - Spoof your MAC address in Mac OS X",
"tags": [
"MAC_address",
"osx",
"mac",
"spoof"
],
"url": "https://feross.org/spoofmac/",
"description": "",
"created": "1358425796",
"others": 6,
"owner": "maciej",
"private": "0"
}
]

View file

@ -1,13 +1,13 @@
[
{
"created_at": "2015-09-09 11:10:32 UTC",
"title": "Qualité de code - Intégration de php-git-hooks dans Symfony2 - Experts Symfony et Drupal - Lexik",
"url": "https://devblog.lexik.fr/git/qualite-de-code-integration-de-php-git-hooks-dans-symfony2-2842",
"description": null,
"tags": [
"tag1",
"tag2"
],
"is_saved": true
}
{
"created_at": "2015-09-09 11:10:32 UTC",
"title": "Qualité de code - Intégration de php-git-hooks dans Symfony2 - Experts Symfony et Drupal - Lexik",
"url": "https://devblog.lexik.fr/git/qualite-de-code-integration-de-php-git-hooks-dans-symfony2-2842",
"description": null,
"tags": [
"tag1",
"tag2"
],
"is_saved": true
}
]

View file

@ -1,63 +1,63 @@
{
"guid": "root________",
"title": "",
"index": 0,
"dateAdded": 1388166091504000,
"lastModified": 1472897622350000,
"id": 1,
"type": "text/x-moz-place-container",
"root": "placesRoot",
"children": [
"guid": "root________",
"title": "",
"index": 0,
"dateAdded": 1388166091504000,
"lastModified": 1472897622350000,
"id": 1,
"type": "text/x-moz-place-container",
"root": "placesRoot",
"children": [
{
"guid": "toolbar_____",
"title": "Barre personnelle",
"index": 1,
"dateAdded": 1388166091504000,
"lastModified": 1472897622263000,
"id": 3,
"annos": [
{
"guid": "toolbar_____",
"title": "Barre personnelle",
"index": 1,
"dateAdded": 1388166091504000,
"lastModified": 1472897622263000,
"id": 3,
"annos": [
{
"name": "bookmarkProperties/description",
"flags": 0,
"expires": 4,
"value": "Ajoutez des marque-pages dans ce dossier pour les voir apparaître sur votre barre personnelle"
}
],
"type": "text/x-moz-place-container",
"root": "toolbarFolder",
"children": [
{
"guid": "tard77lzbC5H",
"title": "Orange offre un meilleur réseau mobile que Bouygues et SFR, Free derrière - L'Express L'Expansion",
"index": 1,
"dateAdded": 1388166091644000,
"lastModified": 1388166091644000,
"tags":"test,tag",
"id": 4,
"type": "text/x-moz-place",
"uri": "http://lexpansion.lexpress.fr/high-tech/orange-offre-un-meilleur-reseau-mobile-que-bouygues-et-sfr-free-derriere_1811554.html"
},
{
"guid": "E385l9vZ_LVn",
"title": "Le journaliste et cinéaste Claude Lanzmann est mort",
"index": 1,
"dateAdded": 1388166091544000,
"lastModified": 1388166091545000,
"id": 5,
"type": "text/x-moz-place",
"uri": "https://www.lemonde.fr/disparitions/article/2018/07/05/le-journaliste-et-cineaste-claude-lanzmann-est-mort_5326313_3382.html"
}
]
"name": "bookmarkProperties/description",
"flags": 0,
"expires": 4,
"value": "Ajoutez des marque-pages dans ce dossier pour les voir apparaître sur votre barre personnelle"
}
],
"type": "text/x-moz-place-container",
"root": "toolbarFolder",
"children": [
{
"guid": "tard77lzbC5H",
"title": "Orange offre un meilleur réseau mobile que Bouygues et SFR, Free derrière - L'Express L'Expansion",
"index": 1,
"dateAdded": 1388166091644000,
"lastModified": 1388166091644000,
"tags": "test,tag",
"id": 4,
"type": "text/x-moz-place",
"uri": "http://lexpansion.lexpress.fr/high-tech/orange-offre-un-meilleur-reseau-mobile-que-bouygues-et-sfr-free-derriere_1811554.html"
},
{
"guid": "unfiled_____",
"title": "Autres marque-pages",
"index": 3,
"dateAdded": 1388166091504000,
"lastModified": 1388166091542000,
"id": 6,
"type": "text/x-moz-place-container",
"root": "unfiledBookmarksFolder"
"guid": "E385l9vZ_LVn",
"title": "Le journaliste et cinéaste Claude Lanzmann est mort",
"index": 1,
"dateAdded": 1388166091544000,
"lastModified": 1388166091545000,
"id": 5,
"type": "text/x-moz-place",
"uri": "https://www.lemonde.fr/disparitions/article/2018/07/05/le-journaliste-et-cineaste-claude-lanzmann-est-mort_5326313_3382.html"
}
]
]
},
{
"guid": "unfiled_____",
"title": "Autres marque-pages",
"index": 3,
"dateAdded": 1388166091504000,
"lastModified": 1388166091542000,
"id": 6,
"type": "text/x-moz-place-container",
"root": "unfiledBookmarksFolder"
}
]
}

View file

@ -1,3 +1,35 @@
[{"href":"https:\/\/developers.google.com\/web\/updates\/2016\/07\/infinite-scroller","description":"Complexities of an Infinite Scroller","extended":"TL;DR: Re-use your DOM elements and remove the ones that are far away from the viewport. Use placeholders to account for delayed data","meta":"21ff61c6f648901168f9e6119f53df7d","hash":"e69b65724cca1c585b446d4c47865d76","time":"2016-10-31T15:57:56Z","shared":"yes","toread":"no","tags":"infinite dom performance scroll"},
{"href":"https:\/\/ma.ttias.be\/varnish-explained\/","description":"Varnish (explained) for PHP developers","extended":"A few months ago, I gave a presentation at LaraconEU in Amsterdam titled \"Varnish for PHP developers\". The generic title of that presentation is actually Varnish Explained and this is a write-up of that presentation, the video and the slides.","meta":"d32ad9fac2ed29da4aec12c562e9afb1","hash":"21dd6bdda8ad62666a2c9e79f6e80f98","time":"2016-10-26T06:43:03Z","shared":"yes","toread":"no","tags":"varnish PHP"},
{"href":"https:\/\/ilia.ws\/files\/nginx_torontophpug.pdf","description":"Nginx Tricks for PHP Developers","extended":"","meta":"9adbb5c4ca6760e335b920800d88c70a","hash":"0189bb08f8bd0122c6544bed4624c546","time":"2016-10-05T07:11:27Z","shared":"yes","toread":"no","tags":"nginx PHP best_practice"}]
[
{
"href": "https://developers.google.com/web/updates/2016/07/infinite-scroller",
"description": "Complexities of an Infinite Scroller",
"extended": "TL;DR: Re-use your DOM elements and remove the ones that are far away from the viewport. Use placeholders to account for delayed data",
"meta": "21ff61c6f648901168f9e6119f53df7d",
"hash": "e69b65724cca1c585b446d4c47865d76",
"time": "2016-10-31T15:57:56Z",
"shared": "yes",
"toread": "no",
"tags": "infinite dom performance scroll"
},
{
"href": "https://ma.ttias.be/varnish-explained/",
"description": "Varnish (explained) for PHP developers",
"extended": "A few months ago, I gave a presentation at LaraconEU in Amsterdam titled \"Varnish for PHP developers\". The generic title of that presentation is actually Varnish Explained and this is a write-up of that presentation, the video and the slides.",
"meta": "d32ad9fac2ed29da4aec12c562e9afb1",
"hash": "21dd6bdda8ad62666a2c9e79f6e80f98",
"time": "2016-10-26T06:43:03Z",
"shared": "yes",
"toread": "no",
"tags": "varnish PHP"
},
{
"href": "https://ilia.ws/files/nginx_torontophpug.pdf",
"description": "Nginx Tricks for PHP Developers",
"extended": "",
"meta": "9adbb5c4ca6760e335b920800d88c70a",
"hash": "0189bb08f8bd0122c6544bed4624c546",
"time": "2016-10-05T07:11:27Z",
"shared": "yes",
"toread": "no",
"tags": "nginx PHP best_practice"
}
]

View file

@ -1,25 +1,25 @@
{
"bookmarks": [
{
"article__excerpt": "This is a guest post from Moritz Beller from the Delft University of Technology in The Netherlands. His team produced amazing research on several million Travis CI builds, creating invaluable&hellip;",
"favorite": false,
"date_archived": "2016-08-02T06:49:30",
"article__url": "https://blog.travis-ci.com/2016-07-28-what-we-learned-from-analyzing-2-million-travis-builds/",
"date_added": "2016-08-01T05:24:16",
"date_favorited": null,
"article__title": "Travis",
"archive": true
},
{
"article__excerpt": "The GraphQL Type system describes the capabilities of a GraphQL server and is used to determine if a query is valid. The type system also describes the input types of query variables to determine if&hellip;",
"favorite": false,
"date_archived": "2016-07-19T06:48:31",
"article__url": "https://facebook.github.io/graphql/October2016/",
"date_added": "2016-06-24T17:50:16",
"date_favorited": null,
"article__title": "GraphQL",
"archive": true
}
],
"recommendations": []
"bookmarks": [
{
"article__excerpt": "This is a guest post from Moritz Beller from the Delft University of Technology in The Netherlands. His team produced amazing research on several million Travis CI builds, creating invaluable&hellip;",
"favorite": false,
"date_archived": "2016-08-02T06:49:30",
"article__url": "https://blog.travis-ci.com/2016-07-28-what-we-learned-from-analyzing-2-million-travis-builds/",
"date_added": "2016-08-01T05:24:16",
"date_favorited": null,
"article__title": "Travis",
"archive": true
},
{
"article__excerpt": "The GraphQL Type system describes the capabilities of a GraphQL server and is used to determine if a query is valid. The type system also describes the input types of query variables to determine if&hellip;",
"favorite": false,
"date_archived": "2016-07-19T06:48:31",
"article__url": "https://facebook.github.io/graphql/October2016/",
"date_added": "2016-06-24T17:50:16",
"date_favorited": null,
"article__title": "GraphQL",
"archive": true
}
],
"recommendations": []
}

View file

@ -1,29 +1,29 @@
{
"bookmarks": [
{
"article__excerpt": "When Twitter started it had so much promise to change the way we communicate. But now it has been ruined by the amount of garbage and hate we have to wade through. It&#x2019;s like that polluted&hellip;",
"favorite": false,
"date_archived": null,
"article__url": "https://venngage.com/blog/hashtags-are-worthless/",
"date_added": "2016-08-25T12:05:00",
"date_favorited": null,
"article__title": "We Looked At 167,943 Tweets & Found Out Hashtags Are Worthless",
"archive": false
},
{
"article__title": "No title found",
"article__url": "http://news.nationalgeographic.com/2016/02/160211-albatrosses-mothers-babies-animals-science/&sf20739758=1",
"archive": false,
"date_added": "2016-09-08T11:55:58+0200",
"favorite": true
},
{
"archive": 0,
"date_added": "2016-09-08T11:55:58+0200",
"favorite": 0,
"article__title": "Bordeaux: Poche, chocolatine… Une association traduit aux étudiants étrangers les mots du Sud-Ouest",
"article__url": "https://www.20minutes.fr/bordeaux/2120479-20170823-bordeaux-poche-chocolatine-association-traduit-etudiants-etrangers-mots-sud-ouest"
}
],
"recommendations": []
"bookmarks": [
{
"article__excerpt": "When Twitter started it had so much promise to change the way we communicate. But now it has been ruined by the amount of garbage and hate we have to wade through. It&#x2019;s like that polluted&hellip;",
"favorite": false,
"date_archived": null,
"article__url": "https://venngage.com/blog/hashtags-are-worthless/",
"date_added": "2016-08-25T12:05:00",
"date_favorited": null,
"article__title": "We Looked At 167,943 Tweets & Found Out Hashtags Are Worthless",
"archive": false
},
{
"article__title": "No title found",
"article__url": "http://news.nationalgeographic.com/2016/02/160211-albatrosses-mothers-babies-animals-science/&sf20739758=1",
"archive": false,
"date_added": "2016-09-08T11:55:58+0200",
"favorite": true
},
{
"archive": 0,
"date_added": "2016-09-08T11:55:58+0200",
"favorite": 0,
"article__title": "Bordeaux: Poche, chocolatine… Une association traduit aux étudiants étrangers les mots du Sud-Ouest",
"article__url": "https://www.20minutes.fr/bordeaux/2120479-20170823-bordeaux-poche-chocolatine-association-traduit-etudiants-etrangers-mots-sud-ouest"
}
],
"recommendations": []
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long