-
简介
作为一个热衷于阅读和收集网络资源的人,我经常面临着管理大量URL的问题。过去我一直使用TickTick来管理这些URL,但是随着时间的推移,有些链接可能会失效,这是互联网的常态。很久以前我就知道了Linkwarden这个项目,但那时候这个项目并不成熟。最近我重新审视了这个项目,发现它已经足够成熟,支持浏览器扩展,iOS上支持快捷指令导入URL,支持PWA,几乎可以说是完美的解决方案。
搭建postgresql
Linkwarden使用PostgreSQL作为数据库存储。我通常会将其部署在Kubernetes环境中,下面是一个PostgreSQL的Kubernetes配置示例:
首先是sts.yaml
:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgresql
namespace: app
spec:
selector:
matchLabels:
app: postgresql
serviceName: postgresql
replicas: 1
template:
metadata:
labels:
app: postgresql
spec:
hostNetwork: true
containers:
- name: postgresql
image: postgres:14.11-alpine3.19
ports:
- containerPort: 5432
name: postgresql
env:
- name: POSTGRES_PASSWORD
value: 'xxxxxx'
- name: POSTGRES_HOST_AUTH_METHOD
value: trust
volumeMounts:
- name: postgresql-data
mountPath: /var/lib/postgresql/data
- name: timezone
mountPath: /etc/localtime
readOnly: true
volumes:
- name: timezone
hostPath:
path: /usr/share/zoneinfo/Asia/Shanghai
volumeClaimTemplates:
- metadata:
name: postgresql-data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 200Gi
然后是svc.yaml
:
apiVersion: v1
kind: Service
metadata:
name: postgresql
namespace: app
spec:
selector:
app: postgresql
ports:
- port: 5432
targetPort: 5432
搭建linkwarden
接下来是Linkwarden的Kubernetes配置示例:
首先是sts.yaml
:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: linkwarden
namespace: app
spec:
selector:
matchLabels:
app: linkwarden
serviceName: linkwarden
replicas: 1
template:
metadata:
labels:
app: linkwarden
spec:
containers:
- name: linkwarden
image: ghcr.io/linkwarden/linkwarden:v2.5.1
ports:
- containerPort: 3000
name: linkwarden
env:
- name: NEXTAUTH_SECRET
value: 'xxxxxxxxxxxxxxx'
- name: NEXTAUTH_URL
value: https://linkwarden.your-domain.com/api/v1/auth
- name: POSTGRES_PASSWORD
value: 'xxxxxxxxxxxxxxxx'
- name: DATABASE_URL
value: postgresql://postgres:${POSTGRES_PASSWORD}@postgresql:5432/linkwarden
volumeMounts:
- name: linkwarden-data
mountPath: /data/data
- name: timezone
mountPath: /etc/localtime
readOnly: true
volumes:
- name: timezone
hostPath:
path: /usr/share/zoneinfo/Asia/Shanghai
volumeClaimTemplates:
- metadata:
name: linkwarden-data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 200Gi
环境变量说明:
NEXTAUTH_SECRET
: 一个随机字符串,了解我的人都知道我会随机创建 一个文件夹,然后获取他的md5作为这个随机字符串NEXTAUTH_URL
: linkwarden的访问urlPOSTGRES_PASSWORD
: pg数据库的密码DATABASE_URL
: 数据库链接地址
其他的环境变量可以参考Linkwarden官方文档:
https://docs.linkwarden.app/self-hosting/environment-variables
Linkwarden还支持多种第三方登录方式,具体可以参考:
https://docs.linkwarden.app/self-hosting/sso-oauth
然后是svc.yaml
:
apiVersion: v1
kind: Service
metadata:
name: linkwarden
namespace: app
spec:
selector:
app: linkwarden
ports:
- port: 3000
targetPort: 3000
最后是ingress.yaml
:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: linkwarden-ingress
namespace: app
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
rules:
- host: "your-domain.com"
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: linkwarden
port:
number: 3000
tls:
- hosts:
- your-domain.com
secretName: your-tls-secret
从ticktick导出数据
由于我收集的URL数量接近800个,手动添加显然是不切实际的。因此,我需要先从TickTick获取历史数据。TickTick并没有提供数据导出功能,但通过分析网络请求,我发现使用以下URL可以获取所有数据:
https://api.ticktick.com/api/v2/batch/check/0
返回的数据是JSON格式,其中projectProfiles
键包含清单的详细信息,syncTaskBean
键包含任务的详细信息。如果你想获取特定清单下的所有数据,只需将syncTaskBean
中的JSON数据转换成CSV格式,然后使用Excel筛选projectId
字段即可。projectProfiles
中的id
就是syncTaskBean
中的projectId
。
筛选完成后,获取所有的title
和content
字段的内容,使用URL提取工具(如:https://www.67tool.com/text/extract-urls
)即可获取所有的URL。
导入数据到linkwarden
接下来就是导入数据的步骤。我编写了一个Python脚本来完成这个任务。首先,创建一个urls.txt
文件,然后将刚才获取的所有URL按行写入。登录Linkwarden,获取你登录后的cookie,修改下面脚本中的cookie和对应的Linkwarden地址,然后运行脚本即可。
import requests
import json
# 读取文本文件中的URL
with open('urls.txt', 'r') as file:
urls = file.readlines()
# 设置请求头
headers = {
'authority': 'your-linkwarden-domain',
'accept': '*/*',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
'content-type': 'application/json',
'cookie': 'your-cookie', # 填入你的cookie
'origin': 'https://your-linkwarden-domain',
'referer': 'https://your-linkwarden-domain/dashboard',
'sec-ch-ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
}
# 遍历URL,发送POST请求
for url in urls:
url = url.strip()
data = {
"name": "",
"url": url, # 导入的url
"description": "",
"type": "url",
"tags": [],
"preview": "",
"image": "",
"pdf": "",
"readable": "",
"textContent": "",
"collection": {
"id": "your-collection-id", # 这里是你linkwarden的collection的id
"name": "your-collection-name", # 这里是你linkwarden的collection名字
"ownerId": "your-user-id" # 这里是你linkwarden的用户id
}
}
response = requests.post('https://your-linkwarden-domain/api/v1/links', headers=headers, data=json.dumps(data))
# 检查响应状态
if response.status_code == 200:
print(f'Successfully posted URL: {url}')
else:
print(f'Failed to post URL: {url}. Status code: {response.status_code}')
注意替换脚本中的your-linkwarden-domain
,your-cookie
,your-collection-id
,your-user-id
和your-collection-name
为你实际的值。
这样,所有的URL就都成功导入到Linkwarden中了。
欢迎关注我的博客www.bboy.app
Have Fun