mirror of
https://github.com/sissbruecker/linkding.git
synced 2025-08-14 22:19:32 +02:00
Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
aac8bf39b8 | ||
![]() |
49f648a908 | ||
![]() |
68c3c27b38 | ||
![]() |
792a19d15e | ||
![]() |
2de6d8151b | ||
![]() |
9e9d7ae7d2 | ||
![]() |
4e8a183082 | ||
![]() |
7719c5b1ba | ||
![]() |
e08bf9fd03 | ||
![]() |
a9bf111ff1 | ||
![]() |
54b0b32b80 |
36
CHANGELOG.md
36
CHANGELOG.md
@@ -1,5 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
## v1.10.1 (21/05/2022)
|
||||
### What's Changed
|
||||
* Fake request headers to reduce bot detection by @sissbruecker in https://github.com/sissbruecker/linkding/pull/263
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.10.0...v1.10.1
|
||||
|
||||
---
|
||||
|
||||
## v1.10.0 (21/05/2022)
|
||||
### What's Changed
|
||||
* Add to managed hosting options by @m3nu in https://github.com/sissbruecker/linkding/pull/253
|
||||
* Add community reference to aiolinkding by @bachya in https://github.com/sissbruecker/linkding/pull/259
|
||||
* Improve import performance by @sissbruecker in https://github.com/sissbruecker/linkding/pull/261
|
||||
* Update how-to.md to fix unclear/paraphrased Safari action in IOS Shortcuts by @feoh in https://github.com/sissbruecker/linkding/pull/260
|
||||
* Allow searching for untagged bookmarks by @sissbruecker in https://github.com/sissbruecker/linkding/pull/226
|
||||
|
||||
### New Contributors
|
||||
* @m3nu made their first contribution in https://github.com/sissbruecker/linkding/pull/253
|
||||
* @bachya made their first contribution in https://github.com/sissbruecker/linkding/pull/259
|
||||
* @feoh made their first contribution in https://github.com/sissbruecker/linkding/pull/260
|
||||
|
||||
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.9.0...v1.10.0
|
||||
|
||||
---
|
||||
|
||||
## v1.9.0 (14/05/2022)
|
||||
### What's Changed
|
||||
* Scroll menu items into view when using keyboard by @sissbruecker in https://github.com/sissbruecker/linkding/pull/248
|
||||
@@ -171,13 +197,3 @@
|
||||
## v1.2.0 (09/01/2021)
|
||||
- [**closed**] Add Favicon [#58](https://github.com/sissbruecker/linkding/issues/58)
|
||||
- [**closed**] Make tags case-insensitive [#45](https://github.com/sissbruecker/linkding/issues/45)
|
||||
|
||||
---
|
||||
|
||||
## v1.1.1 (01/01/2021)
|
||||
- [**enhancement**] Add docker-compose support [#54](https://github.com/sissbruecker/linkding/pull/54)
|
||||
---
|
||||
|
||||
## v1.1.0 (31/12/2020)
|
||||
- [**enhancement**] Search autocomplete [#52](https://github.com/sissbruecker/linkding/issues/52)
|
||||
- [**enhancement**] Improve Netscape bookmarks file parsing [#50](https://github.com/sissbruecker/linkding/issues/50)
|
@@ -129,6 +129,7 @@ This section lists community projects around using linkding, in alphabetical ord
|
||||
- [linkding-extension](https://github.com/jeroenpardon/linkding-extension) Chromium compatible extension that wraps the linkding bookmarklet. Tested with Chrome, Edge, Brave. By [jeroenpardon](https://github.com/jeroenpardon)
|
||||
- [linkding-injector](https://github.com/Fivefold/linkding-injector) Injects search results from linkding into the sidebar of search pages like google and duckduckgo. Tested with Firefox and Chrome. By [Fivefold](https://github.com/Fivefold)
|
||||
- [aiolinkding](https://github.com/bachya/aiolinkding) A Python3, async library to interact with the linkding REST API. By [bachya](https://github.com/bachya)
|
||||
- [linkding-cli](https://github.com/bachya/linkding-cli) A command-line interface (CLI) to interact with the linkding REST API. Powered by [aiolinkding](https://github.com/bachya/aiolinkding). By [bachya](https://github.com/bachya)
|
||||
|
||||
## Development
|
||||
|
||||
|
@@ -4,7 +4,7 @@
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 1.9.x | :white_check_mark: |
|
||||
| 1.10.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
from background_task.admin import TaskAdmin, CompletedTaskAdmin
|
||||
from background_task.models import Task, CompletedTask
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.admin import AdminSite
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
@@ -107,3 +109,5 @@ linkding_admin_site.register(Tag, AdminTag)
|
||||
linkding_admin_site.register(User, AdminCustomUser)
|
||||
linkding_admin_site.register(TokenProxy, TokenAdmin)
|
||||
linkding_admin_site.register(Toast, AdminToast)
|
||||
linkding_admin_site.register(Task, TaskAdmin)
|
||||
linkding_admin_site.register(CompletedTask, CompletedTaskAdmin)
|
||||
|
@@ -19,6 +19,7 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
||||
'description',
|
||||
'website_title',
|
||||
'website_description',
|
||||
'is_archived',
|
||||
'tag_names',
|
||||
'date_added',
|
||||
'date_modified'
|
||||
@@ -33,6 +34,7 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
||||
# Override optional char fields to provide default value
|
||||
title = serializers.CharField(required=False, allow_blank=True, default='')
|
||||
description = serializers.CharField(required=False, allow_blank=True, default='')
|
||||
is_archived = serializers.BooleanField(required=False, default=False)
|
||||
# Override readonly tag_names property to allow passing a list of tag names to create/update
|
||||
tag_names = TagListField(required=False, default=[])
|
||||
|
||||
@@ -41,14 +43,21 @@ class BookmarkSerializer(serializers.ModelSerializer):
|
||||
bookmark.url = validated_data['url']
|
||||
bookmark.title = validated_data['title']
|
||||
bookmark.description = validated_data['description']
|
||||
bookmark.is_archived = validated_data['is_archived']
|
||||
tag_string = build_tag_string(validated_data['tag_names'])
|
||||
return create_bookmark(bookmark, tag_string, self.context['user'])
|
||||
|
||||
def update(self, instance: Bookmark, validated_data):
|
||||
instance.url = validated_data['url']
|
||||
instance.title = validated_data['title']
|
||||
instance.description = validated_data['description']
|
||||
tag_string = build_tag_string(validated_data['tag_names'])
|
||||
# Update fields if they were provided in the payload
|
||||
for key in ['url', 'title', 'description']:
|
||||
if key in validated_data:
|
||||
setattr(instance, key, validated_data[key])
|
||||
|
||||
# Use tag string from payload, or use bookmark's current tags as fallback
|
||||
tag_string = build_tag_string(instance.tag_names)
|
||||
if 'tag_names' in validated_data:
|
||||
tag_string = build_tag_string(validated_data['tag_names'])
|
||||
|
||||
return update_bookmark(instance, tag_string, self.context['user'])
|
||||
|
||||
|
||||
|
@@ -34,7 +34,8 @@ def load_website_metadata(url: str):
|
||||
|
||||
|
||||
def load_page(url: str):
|
||||
r = requests.get(url, timeout=10)
|
||||
headers = fake_request_headers()
|
||||
r = requests.get(url, timeout=10, headers=headers)
|
||||
|
||||
# Use charset_normalizer to determine encoding that best matches the response content
|
||||
# Several sites seem to specify the response encoding incorrectly, so we ignore it and use custom logic instead
|
||||
@@ -42,3 +43,13 @@ def load_page(url: str):
|
||||
# before trying to determine one
|
||||
results = from_bytes(r.content)
|
||||
return str(results.best())
|
||||
|
||||
|
||||
def fake_request_headers():
|
||||
return {
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Dnt": "1",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.0.0 Safari/537.36",
|
||||
}
|
||||
|
@@ -11,4 +11,8 @@
|
||||
.input-group > input[type=submit] {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
section.about table {
|
||||
max-width: 500px;
|
||||
}
|
||||
}
|
||||
|
@@ -98,13 +98,29 @@
|
||||
</section>
|
||||
|
||||
{# About section #}
|
||||
<section class="content-area">
|
||||
<section class="content-area about">
|
||||
<h2>About</h2>
|
||||
<p>Version: {{ app_version }}</p>
|
||||
<p>
|
||||
Code: <a href="https://github.com/sissbruecker/linkding/"
|
||||
target="_blank">GitHub</a>
|
||||
</p>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Version</td>
|
||||
<td>{{ version_info }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="3" style="vertical-align: top">Links</td>
|
||||
<td><a href="https://github.com/sissbruecker/linkding/"
|
||||
target="_blank">GitHub</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/sissbruecker/linkding#documentation"
|
||||
target="_blank">Documentation</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/sissbruecker/linkding/blob/master/CHANGELOG.md"
|
||||
target="_blank">Changelog</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
@@ -83,6 +83,11 @@ class LinkdingApiTestCase(APITestCase):
|
||||
self.assertEqual(response.status_code, expected_status_code)
|
||||
return response
|
||||
|
||||
def patch(self, url, data=None, expected_status_code=status.HTTP_200_OK):
|
||||
response = self.client.patch(url, data, format='json')
|
||||
self.assertEqual(response.status_code, expected_status_code)
|
||||
return response
|
||||
|
||||
def delete(self, url, expected_status_code=status.HTTP_200_OK):
|
||||
response = self.client.delete(url)
|
||||
self.assertEqual(response.status_code, expected_status_code)
|
||||
|
@@ -34,6 +34,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||
expectation['description'] = bookmark.description
|
||||
expectation['website_title'] = bookmark.website_title
|
||||
expectation['website_description'] = bookmark.website_description
|
||||
expectation['is_archived'] = bookmark.is_archived
|
||||
expectation['tag_names'] = tag_names
|
||||
expectation['date_added'] = bookmark.date_added.isoformat().replace('+00:00', 'Z')
|
||||
expectation['date_modified'] = bookmark.date_modified.isoformat().replace('+00:00', 'Z')
|
||||
@@ -49,6 +50,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||
'url': 'https://example.com/',
|
||||
'title': 'Test title',
|
||||
'description': 'Test description',
|
||||
'is_archived': False,
|
||||
'tag_names': ['tag1', 'tag2']
|
||||
}
|
||||
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
|
||||
@@ -56,6 +58,7 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||
self.assertEqual(bookmark.url, data['url'])
|
||||
self.assertEqual(bookmark.title, data['title'])
|
||||
self.assertEqual(bookmark.description, data['description'])
|
||||
self.assertFalse(bookmark.is_archived, data['is_archived'])
|
||||
self.assertEqual(bookmark.tags.count(), 2)
|
||||
self.assertEqual(bookmark.tags.filter(name=data['tag_names'][0]).count(), 1)
|
||||
self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)
|
||||
@@ -92,6 +95,30 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||
response = self.get(reverse('bookmarks:bookmark-archived') + '?q=#' + self.tag1.name, expected_status_code=status.HTTP_200_OK)
|
||||
self.assertBookmarkListEqual(response.data['results'], [self.archived_bookmark1])
|
||||
|
||||
def test_create_archived_bookmark(self):
|
||||
data = {
|
||||
'url': 'https://example.com/',
|
||||
'title': 'Test title',
|
||||
'description': 'Test description',
|
||||
'is_archived': True,
|
||||
'tag_names': ['tag1', 'tag2']
|
||||
}
|
||||
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
|
||||
bookmark = Bookmark.objects.get(url=data['url'])
|
||||
self.assertEqual(bookmark.url, data['url'])
|
||||
self.assertEqual(bookmark.title, data['title'])
|
||||
self.assertEqual(bookmark.description, data['description'])
|
||||
self.assertTrue(bookmark.is_archived)
|
||||
self.assertEqual(bookmark.tags.count(), 2)
|
||||
self.assertEqual(bookmark.tags.filter(name=data['tag_names'][0]).count(), 1)
|
||||
self.assertEqual(bookmark.tags.filter(name=data['tag_names'][1]).count(), 1)
|
||||
|
||||
def test_create_bookmark_minimal_payload_does_not_archive(self):
|
||||
data = {'url': 'https://example.com/'}
|
||||
self.post(reverse('bookmarks:bookmark-list'), data, status.HTTP_201_CREATED)
|
||||
bookmark = Bookmark.objects.get(url=data['url'])
|
||||
self.assertFalse(bookmark.is_archived)
|
||||
|
||||
def test_get_bookmark(self):
|
||||
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
|
||||
response = self.get(url, expected_status_code=status.HTTP_200_OK)
|
||||
@@ -104,6 +131,56 @@ class BookmarksApiTestCase(LinkdingApiTestCase, BookmarkFactoryMixin):
|
||||
updated_bookmark = Bookmark.objects.get(id=self.bookmark1.id)
|
||||
self.assertEqual(updated_bookmark.url, data['url'])
|
||||
|
||||
def test_update_bookmark_fails_without_required_fields(self):
|
||||
data = {'title': 'https://example.com/'}
|
||||
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
|
||||
self.put(url, data, expected_status_code=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def test_update_bookmark_with_minimal_payload_clears_all_fields(self):
|
||||
data = {'url': 'https://example.com/'}
|
||||
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
|
||||
self.put(url, data, expected_status_code=status.HTTP_200_OK)
|
||||
updated_bookmark = Bookmark.objects.get(id=self.bookmark1.id)
|
||||
self.assertEqual(updated_bookmark.url, data['url'])
|
||||
self.assertEqual(updated_bookmark.title, '')
|
||||
self.assertEqual(updated_bookmark.description, '')
|
||||
self.assertEqual(updated_bookmark.tag_names, [])
|
||||
|
||||
def test_patch_bookmark(self):
|
||||
data = {'url': 'https://example.com'}
|
||||
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
|
||||
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
|
||||
self.bookmark1.refresh_from_db()
|
||||
self.assertEqual(self.bookmark1.url, data['url'])
|
||||
|
||||
data = {'title': 'Updated title'}
|
||||
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
|
||||
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
|
||||
self.bookmark1.refresh_from_db()
|
||||
self.assertEqual(self.bookmark1.title, data['title'])
|
||||
|
||||
data = {'description': 'Updated description'}
|
||||
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
|
||||
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
|
||||
self.bookmark1.refresh_from_db()
|
||||
self.assertEqual(self.bookmark1.description, data['description'])
|
||||
|
||||
data = {'tag_names': ['updated-tag-1', 'updated-tag-2']}
|
||||
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
|
||||
self.patch(url, data, expected_status_code=status.HTTP_200_OK)
|
||||
self.bookmark1.refresh_from_db()
|
||||
tag_names = [tag.name for tag in self.bookmark1.tags.all()]
|
||||
self.assertListEqual(tag_names, ['updated-tag-1', 'updated-tag-2'])
|
||||
|
||||
def test_patch_with_empty_payload_does_not_modify_bookmark(self):
|
||||
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
|
||||
self.patch(url, {}, expected_status_code=status.HTTP_200_OK)
|
||||
updated_bookmark = Bookmark.objects.get(id=self.bookmark1.id)
|
||||
self.assertEqual(updated_bookmark.url, self.bookmark1.url)
|
||||
self.assertEqual(updated_bookmark.title, self.bookmark1.title)
|
||||
self.assertEqual(updated_bookmark.description, self.bookmark1.description)
|
||||
self.assertListEqual(updated_bookmark.tag_names, self.bookmark1.tag_names)
|
||||
|
||||
def test_delete_bookmark(self):
|
||||
url = reverse('bookmarks:bookmark-detail', args=[self.bookmark1.id])
|
||||
self.delete(url, expected_status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
@@ -1,8 +1,14 @@
|
||||
import random
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from unittest.mock import patch, Mock
|
||||
import requests
|
||||
from requests import RequestException
|
||||
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
from bookmarks.models import UserProfile
|
||||
from bookmarks.tests.helpers import BookmarkFactoryMixin
|
||||
from bookmarks.views.settings import app_version, get_version_info
|
||||
|
||||
|
||||
class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
@@ -38,3 +44,31 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
|
||||
self.assertEqual(self.user.profile.bookmark_date_display, form_data['bookmark_date_display'])
|
||||
self.assertEqual(self.user.profile.bookmark_link_target, form_data['bookmark_link_target'])
|
||||
self.assertEqual(self.user.profile.web_archive_integration, form_data['web_archive_integration'])
|
||||
|
||||
def test_about_shows_version_info(self):
|
||||
response = self.client.get(reverse('bookmarks:settings.general'))
|
||||
html = response.content.decode()
|
||||
|
||||
self.assertInHTML(f'''
|
||||
<tr>
|
||||
<td>Version</td>
|
||||
<td>{get_version_info(random.random())}</td>
|
||||
</tr>
|
||||
''', html)
|
||||
|
||||
def test_get_version_info_just_displays_latest_when_versions_are_equal(self):
|
||||
latest_version_response_mock = Mock(status_code=201, json=lambda: {'name': f'v{app_version}'})
|
||||
with patch.object(requests, 'get', return_value=latest_version_response_mock):
|
||||
version_info = get_version_info(random.random())
|
||||
self.assertEqual(version_info, f'{app_version} (latest)')
|
||||
|
||||
def test_get_version_info_shows_latest_version_when_versions_are_not_equal(self):
|
||||
latest_version_response_mock = Mock(status_code=201, json=lambda: {'name': f'v123.0.1'})
|
||||
with patch.object(requests, 'get', return_value=latest_version_response_mock):
|
||||
version_info = get_version_info(random.random())
|
||||
self.assertEqual(version_info, f'{app_version} (latest: 123.0.1)')
|
||||
|
||||
def test_get_version_info_silently_ignores_request_errors(self):
|
||||
with patch.object(requests, 'get', side_effect=RequestException()):
|
||||
version_info = get_version_info(random.random())
|
||||
self.assertEqual(version_info, f'{app_version}')
|
||||
|
@@ -1,5 +1,8 @@
|
||||
import logging
|
||||
import time
|
||||
from functools import lru_cache
|
||||
|
||||
import requests
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
@@ -17,10 +20,11 @@ logger = logging.getLogger(__name__)
|
||||
try:
|
||||
with open("version.txt", "r") as f:
|
||||
app_version = f.read().strip("\n")
|
||||
except Exception as exc:
|
||||
except Exception as exc:
|
||||
logging.exception(exc)
|
||||
pass
|
||||
|
||||
|
||||
@login_required
|
||||
def general(request):
|
||||
if request.method == 'POST':
|
||||
@@ -32,14 +36,41 @@ def general(request):
|
||||
|
||||
import_success_message = _find_message_with_tag(messages.get_messages(request), 'bookmark_import_success')
|
||||
import_errors_message = _find_message_with_tag(messages.get_messages(request), 'bookmark_import_errors')
|
||||
version_info = get_version_info(get_ttl_hash())
|
||||
return render(request, 'settings/general.html', {
|
||||
'form': form,
|
||||
'import_success_message': import_success_message,
|
||||
'import_errors_message': import_errors_message,
|
||||
'app_version': app_version
|
||||
'version_info': version_info,
|
||||
})
|
||||
|
||||
|
||||
# Cache API call response, for one hour when using get_ttl_hash with default params
|
||||
@lru_cache(maxsize=1)
|
||||
def get_version_info(ttl_hash=None):
|
||||
latest_version = None
|
||||
try:
|
||||
latest_version_url = 'https://api.github.com/repos/sissbruecker/linkding/releases/latest'
|
||||
response = requests.get(latest_version_url, timeout=5)
|
||||
json = response.json()
|
||||
latest_version = json['name'][1:]
|
||||
except requests.exceptions.RequestException:
|
||||
pass
|
||||
|
||||
latest_version_info = ''
|
||||
if latest_version == app_version:
|
||||
latest_version_info = ' (latest)'
|
||||
elif latest_version is not None:
|
||||
latest_version_info = f' (latest: {latest_version})'
|
||||
|
||||
return f'{app_version}{latest_version_info}'
|
||||
|
||||
|
||||
def get_ttl_hash(seconds=3600):
|
||||
"""Return the same value within `seconds` time period"""
|
||||
return round(time.time() / seconds)
|
||||
|
||||
|
||||
@login_required
|
||||
def integrations(request):
|
||||
application_url = request.build_absolute_uri("/bookmarks/new")
|
||||
|
@@ -24,7 +24,7 @@ The following resources are available:
|
||||
GET /api/bookmarks/
|
||||
```
|
||||
|
||||
List bookmarks.
|
||||
List bookmarks.
|
||||
|
||||
Parameters:
|
||||
|
||||
@@ -65,7 +65,7 @@ Example response:
|
||||
GET /api/bookmarks/archived/
|
||||
```
|
||||
|
||||
List archived bookmarks.
|
||||
List archived bookmarks.
|
||||
|
||||
Parameters and response are the same as for the regular list endpoint.
|
||||
|
||||
@@ -83,7 +83,8 @@ Retrieves a single bookmark by ID.
|
||||
POST /api/bookmarks/
|
||||
```
|
||||
|
||||
Creates a new bookmark. Tags are simply assigned using their names.
|
||||
Creates a new bookmark. Tags are simply assigned using their names. Including
|
||||
`is_archived: true` saves a bookmark directly to the archive.
|
||||
|
||||
Example payload:
|
||||
|
||||
@@ -92,6 +93,7 @@ Example payload:
|
||||
"url": "https://example.com",
|
||||
"title": "Example title",
|
||||
"description": "Example description",
|
||||
"is_archived": false,
|
||||
"tag_names": [
|
||||
"tag1",
|
||||
"tag2"
|
||||
@@ -201,4 +203,3 @@ Example payload:
|
||||
"name": "example"
|
||||
}
|
||||
```
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "linkding",
|
||||
"version": "1.10.0",
|
||||
"version": "1.11.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@@ -1 +1 @@
|
||||
1.10.0
|
||||
1.11.0
|
||||
|
Reference in New Issue
Block a user