├── .gitignore
├── weread2notionpro
├── __init__.py
├── __main__.py
├── config.py
├── read_time.py
├── book.py
├── weread.py
├── weread_api.py
├── utils.py
└── notion_helper.py
├── asset
├── WechatIMG24.jpg
└── WechatIMG27.jpg
├── requirements.txt
├── setup.py
├── README.md
└── .github
└── workflows
├── weread.yml
└── read_time.yml
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
--------------------------------------------------------------------------------
/weread2notionpro/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/asset/WechatIMG24.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/malinkang/weread2notion-pro/HEAD/asset/WechatIMG24.jpg
--------------------------------------------------------------------------------
/asset/WechatIMG27.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/malinkang/weread2notion-pro/HEAD/asset/WechatIMG27.jpg
--------------------------------------------------------------------------------
/weread2notionpro/__main__.py:
--------------------------------------------------------------------------------
1 | from book import main
2 |
3 | if __name__ == "__main__":
4 | main()
5 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
2 | notion-client
3 | github-heatmap
4 | retrying
5 | pendulum
6 | python-dotenv
7 | weread2notionpro
--------------------------------------------------------------------------------
/weread2notionpro/config.py:
--------------------------------------------------------------------------------
1 |
2 | RICH_TEXT = "rich_text"
3 | URL = "url"
4 | RELATION = "relation"
5 | NUMBER = "number"
6 | DATE = "date"
7 | FILES = "files"
8 | STATUS = "status"
9 | TITLE = "title"
10 | SELECT = "select"
11 |
12 | book_properties_type_dict = {
13 | "书名":TITLE,
14 | "BookId":RICH_TEXT,
15 | "ISBN":RICH_TEXT,
16 | "链接":URL,
17 | "作者":RELATION,
18 | "Sort":NUMBER,
19 | "评分":NUMBER,
20 | "封面":FILES,
21 | "分类":RELATION,
22 | "阅读状态":STATUS,
23 | "阅读时长":NUMBER,
24 | "阅读进度":NUMBER,
25 | "阅读天数":NUMBER,
26 | "时间":DATE,
27 | "开始阅读时间":DATE,
28 | "最后阅读时间":DATE,
29 | "简介":RICH_TEXT,
30 | "书架分类":SELECT,
31 | "我的评分":SELECT,
32 | "豆瓣链接":URL,
33 | }
34 | tz='Asia/Shanghai'
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | setup(
4 | version="0.2.5",
5 | packages=find_packages(),
6 | install_requires=[
7 | "requests",
8 | "pendulum",
9 | "retrying",
10 | "notion-client",
11 | "github-heatmap",
12 | "github-heatmap",
13 | ],
14 | entry_points={
15 | "console_scripts": [
16 | "book = weread2notionpro.book:main",
17 | "weread = weread2notionpro.weread:main",
18 | "read_time = weread2notionpro.read_time:main",
19 | ],
20 | },
21 | author="malinkang",
22 | author_email="linkang.ma@gmail.com",
23 | description="自动将微信读书笔记和阅读记录同步到Notion",
24 | long_description=open("README.md").read(),
25 | long_description_content_type="text/markdown",
26 | url="https://github.com/malinkang/weread2notion-pro",
27 | classifiers=[
28 | "Programming Language :: Python :: 3",
29 | "License :: OSI Approved :: MIT License",
30 | "Operating System :: OS Independent",
31 | ],
32 | python_requires=">=3.6",
33 | )
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 将微信读书划线和笔记同步到Notion
2 |
3 |
4 | 本项目通过Github Action每天定时同步微信读书划线到Notion。
5 |
6 | 预览效果:[https://malinkang.notion.site/malinkang/534a7684b30e4a879269313f437f2185](https://malinkang.notion.site/9a311b7413b74c8788752249edd0b256?pvs=25)
7 |
8 |
9 | > [!CAUTION]
10 | > 目前项目已不能使用,可以通过chrome插件来使用:https://www.notionhub.app/docs/install.html
11 |
12 | ## 使用
13 |
14 | > [!IMPORTANT]
15 | > 关注公众号获取教程,后续有更新也会第一时间在公众号里同步。
16 |
17 | 
18 |
19 |
20 | ## 群
21 | > [!IMPORTANT]
22 | > 欢迎加入群讨论。可以讨论使用中遇到的任何问题,也可以讨论Notion使用,后续我也会在群中分享更多Notion自动化工具。微信群失效的话可以添加我的微信malinkang,我拉你入群。
23 |
24 | | 微信群 | QQ群 |
25 | | --- | --- |
26 | |

| 
|
27 |
28 |
29 | ## 捐赠
30 |
31 | 如果你觉得本项目帮助了你,请作者喝一杯咖啡,你的支持是作者最大的动力。本项目会持续更新。
32 |
33 | | 支付宝支付 | 微信支付 |
34 | | --- | --- |
35 | | 
| 
|
36 |
37 | ## 其他项目
38 | * [WeRead2Notion-Pro](https://github.com/malinkang/weread2notion-pro)
39 | * [WeRead2Notion](https://github.com/malinkang/weread2notion)
40 | * [Podcast2Notion](https://github.com/malinkang/podcast2notion)
41 | * [Douban2Notion](https://github.com/malinkang/douban2notion)
42 | * [Keep2Notion](https://github.com/malinkang/keep2notion)
43 |
--------------------------------------------------------------------------------
/.github/workflows/weread.yml:
--------------------------------------------------------------------------------
1 | name: weread note sync
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: '0 */2 * * *' # 每 2 小时执行一次
7 | concurrency:
8 | group: ${{ github.workflow }}-${{ github.ref }}
9 | cancel-in-progress: true
10 | jobs:
11 | sync:
12 | name: Sync
13 | runs-on: ubuntu-latest
14 | env:
15 | NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
16 | NOTION_PAGE: ${{ secrets.NOTION_PAGE }}
17 | WEREAD_COOKIE: ${{ secrets.WEREAD_COOKIE }}
18 | CC_URL: ${{ secrets.CC_URL }}
19 | CC_ID: ${{ secrets.CC_ID }}
20 | CC_PASSWORD: ${{ secrets.CC_PASSWORD }}
21 | BOOK_DATABASE_NAME: ${{ vars.BOOK_DATABASE_NAME }}
22 | AUTHOR_DATABASE_NAME: ${{ vars.AUTHOR_DATABASE_NAME }}
23 | CATEGORY_DATABASE_NAME: ${{ vars.CATEGORY_DATABASE_NAME }}
24 | BOOKMARK_DATABASE_NAME: ${{ vars.BOOKMARK_DATABASE_NAME }}
25 | REVIEW_DATABASE_NAME: ${{ vars.REVIEW_DATABASE_NAME }}
26 | CHAPTER_DATABASE_NAME: ${{ vars.CHAPTER_DATABASE_NAME }}
27 | YEAR_DATABASE_NAME: ${{ vars.YEAR_DATABASE_NAME }}
28 | WEEK_DATABASE_NAME: ${{ vars.WEEK_DATABASE_NAME }}
29 | MONTH_DATABASE_NAME: ${{ vars.MONTH_DATABASE_NAME }}
30 | DAY_DATABASE_NAME: ${{ vars.DAY_DATABASE_NAME }}
31 | REF: ${{ github.ref }}
32 | REPOSITORY: ${{ github.repository }}
33 | steps:
34 | - name: Checkout
35 | uses: actions/checkout@v4
36 | - name: Set up Python
37 | uses: actions/setup-python@v4
38 | with:
39 | python-version: 3.11
40 | - name: Install dependencies
41 | run: |
42 | python -m pip install --upgrade pip
43 | pip install -r requirements.txt
44 | - name: weread book sync
45 | run: |
46 | book
47 | - name: weread sync
48 | run: |
49 | weread
50 |
51 |
--------------------------------------------------------------------------------
/.github/workflows/read_time.yml:
--------------------------------------------------------------------------------
1 | name: read time sync
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: "0 */3 * * *"
7 | concurrency:
8 | group: ${{ github.workflow }}-${{ github.ref }}
9 | cancel-in-progress: true
10 | jobs:
11 | sync:
12 | name: Sync
13 | runs-on: ubuntu-latest
14 | env:
15 | NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
16 | NOTION_PAGE: ${{ secrets.NOTION_PAGE }}
17 | WEREAD_COOKIE: ${{ secrets.WEREAD_COOKIE }}
18 | CC_URL: ${{ secrets.CC_URL }}
19 | CC_ID: ${{ secrets.CC_ID }}
20 | CC_PASSWORD: ${{ secrets.CC_PASSWORD }}
21 | HEATMAP_BLOCK_ID: ${{ secrets.HEATMAP_BLOCK_ID }}
22 | BOOK_DATABASE_NAME: ${{ vars.BOOK_DATABASE_NAME }}
23 | AUTHOR_DATABASE_NAME: ${{ vars.AUTHOR_DATABASE_NAME }}
24 | CATEGORY_DATABASE_NAME: ${{ vars.CATEGORY_DATABASE_NAME }}
25 | BOOKMARK_DATABASE_NAME: ${{ vars.BOOKMARK_DATABASE_NAME }}
26 | REVIEW_DATABASE_NAME: ${{ vars.REVIEW_DATABASE_NAME }}
27 | CHAPTER_DATABASE_NAME: ${{ vars.CHAPTER_DATABASE_NAME }}
28 | YEAR_DATABASE_NAME: ${{ vars.YEAR_DATABASE_NAME }}
29 | WEEK_DATABASE_NAME: ${{ vars.WEEK_DATABASE_NAME }}
30 | MONTH_DATABASE_NAME: ${{ vars.MONTH_DATABASE_NAME }}
31 | DAY_DATABASE_NAME: ${{ vars.DAY_DATABASE_NAME }}
32 | REF: ${{ github.ref }}
33 | REPOSITORY: ${{ github.repository }}
34 | YEAR: ${{ vars.YEAR }}
35 | steps:
36 | - name: Checkout
37 | uses: actions/checkout@v4
38 | - name: Set up Python
39 | uses: actions/setup-python@v4
40 | with:
41 | python-version: 3.11
42 | - name: Install dependencies
43 | run: |
44 | python -m pip install --upgrade pip
45 | pip install -r requirements.txt
46 | - name: Remove folder
47 | run: rm -rf ./OUT_FOLDER
48 | - name: Set default year if not provided
49 | run: echo "YEAR=$(date +"%Y")" >> $GITHUB_ENV
50 | if: env.YEAR == ''
51 | - name: weread heatmap
52 | run: |
53 | github_heatmap weread --year $YEAR --me "${{secrets.NAME}}" --without-type-name --background-color=${{ vars.background_color||'#FFFFFF'}} --track-color=${{ vars.track_color||'#ACE7AE'}} --special-color1=${{ vars.special_color||'#69C16E'}} --special-color2=${{ vars.special_color2||'#549F57'}} --dom-color=${{ vars.dom_color||'#EBEDF0'}} --text-color=${{ vars.text_color||'#000000'}}
54 | - name: Rename weread.svg to a random name
55 | run: |
56 | cd OUT_FOLDER
57 | find . -type f ! -name "weread.svg" -exec rm -f {} +
58 | cd ..
59 | RANDOM_FILENAME=$(uuidgen).svg
60 | mv ./OUT_FOLDER/weread.svg ./OUT_FOLDER/$RANDOM_FILENAME
61 | echo "Renamed file to $RANDOM_FILENAME"
62 | - name: push
63 | run: |
64 | git config --local user.email "action@github.com"
65 | git config --local user.name "GitHub Action"
66 | git add .
67 | git commit -m 'add new heatmap' || echo "nothing to commit"
68 | git push || echo "nothing to push"
69 | - name: read time sync
70 | run: |
71 | read_time
72 |
73 |
--------------------------------------------------------------------------------
/weread2notionpro/read_time.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from datetime import timedelta
3 | import os
4 |
5 | import pendulum
6 |
7 | from weread2notionpro.weread_api import WeReadApi
8 | from weread2notionpro.notion_helper import NotionHelper
9 | from weread2notionpro.utils import (
10 | format_date,
11 | get_date,
12 | get_icon,
13 | get_number,
14 | get_relation,
15 | get_title,
16 | )
17 |
18 |
19 | def insert_to_notion(page_id, timestamp, duration):
20 | parent = {"database_id": notion_helper.day_database_id, "type": "database_id"}
21 | properties = {
22 | "标题": get_title(
23 | format_date(
24 | datetime.utcfromtimestamp(timestamp) + timedelta(hours=8),
25 | "%Y年%m月%d日",
26 | )
27 | ),
28 | "日期": get_date(
29 | start=format_date(datetime.utcfromtimestamp(timestamp) + timedelta(hours=8))
30 | ),
31 | "时长": get_number(duration),
32 | "时间戳": get_number(timestamp),
33 | "年": get_relation(
34 | [
35 | notion_helper.get_year_relation_id(
36 | datetime.utcfromtimestamp(timestamp) + timedelta(hours=8)
37 | ),
38 | ]
39 | ),
40 | "月": get_relation(
41 | [
42 | notion_helper.get_month_relation_id(
43 | datetime.utcfromtimestamp(timestamp) + timedelta(hours=8)
44 | ),
45 | ]
46 | ),
47 | "周": get_relation(
48 | [
49 | notion_helper.get_week_relation_id(
50 | datetime.utcfromtimestamp(timestamp) + timedelta(hours=8)
51 | ),
52 | ]
53 | ),
54 | }
55 | if page_id != None:
56 | notion_helper.client.pages.update(page_id=page_id, properties=properties)
57 | else:
58 | notion_helper.client.pages.create(
59 | parent=parent,
60 | icon=get_icon("https://www.notion.so/icons/target_red.svg"),
61 | properties=properties,
62 | )
63 |
64 |
65 | def get_file():
66 | # 设置文件夹路径
67 | folder_path = "./OUT_FOLDER"
68 |
69 | # 检查文件夹是否存在
70 | if os.path.exists(folder_path) and os.path.isdir(folder_path):
71 | entries = os.listdir(folder_path)
72 |
73 | file_name = entries[0] if entries else None
74 | return file_name
75 | else:
76 | print("OUT_FOLDER does not exist.")
77 | return None
78 |
79 | HEATMAP_GUIDE = "https://mp.weixin.qq.com/s?__biz=MzI1OTcxOTI4NA==&mid=2247484145&idx=1&sn=81752852420b9153fc292b7873217651&chksm=ea75ebeadd0262fc65df100370d3f983ba2e52e2fcde2deb1ed49343fbb10645a77570656728&token=157143379&lang=zh_CN#rd"
80 |
81 |
82 | notion_helper = NotionHelper()
83 | weread_api = WeReadApi()
84 | def main():
85 | image_file = get_file()
86 | if image_file:
87 | image_url = f"https://raw.githubusercontent.com/{os.getenv('REPOSITORY')}/{os.getenv('REF').split('/')[-1]}/OUT_FOLDER/{image_file}"
88 | heatmap_url = f"https://heatmap.malinkang.com/?image={image_url}"
89 | if notion_helper.heatmap_block_id:
90 | response = notion_helper.update_heatmap(
91 | block_id=notion_helper.heatmap_block_id, url=heatmap_url
92 | )
93 | else:
94 | print(f"更新热力图失败,没有添加热力图占位。具体参考:{HEATMAP_GUIDE}")
95 | else:
96 | print(f"更新热力图失败,没有生成热力图。具体参考:{HEATMAP_GUIDE}")
97 | api_data = weread_api.get_api_data()
98 | readTimes = {int(key): value for key, value in api_data.get("readTimes").items()}
99 | now = pendulum.now("Asia/Shanghai").start_of("day")
100 | today_timestamp = now.int_timestamp
101 | if today_timestamp not in readTimes:
102 | readTimes[today_timestamp] = 0
103 | readTimes = dict(sorted(readTimes.items()))
104 | results = notion_helper.query_all(database_id=notion_helper.day_database_id)
105 | for result in results:
106 | timestamp = result.get("properties").get("时间戳").get("number")
107 | duration = result.get("properties").get("时长").get("number")
108 | id = result.get("id")
109 | if timestamp in readTimes:
110 | value = readTimes.pop(timestamp)
111 | if value != duration:
112 | insert_to_notion(page_id=id, timestamp=timestamp, duration=value)
113 | for key, value in readTimes.items():
114 | insert_to_notion(None, int(key), value)
115 | if __name__ == "__main__":
116 | main()
117 |
--------------------------------------------------------------------------------
/weread2notionpro/book.py:
--------------------------------------------------------------------------------
1 | import pendulum
2 | from weread2notionpro.notion_helper import NotionHelper
3 | from weread2notionpro.weread_api import WeReadApi
4 | from weread2notionpro import utils
5 | from weread2notionpro.config import book_properties_type_dict, tz
6 |
7 | TAG_ICON_URL = "https://www.notion.so/icons/tag_gray.svg"
8 | USER_ICON_URL = "https://www.notion.so/icons/user-circle-filled_gray.svg"
9 | BOOK_ICON_URL = "https://www.notion.so/icons/book_gray.svg"
10 | rating = {"poor": "⭐️", "fair": "⭐️⭐️⭐️", "good": "⭐️⭐️⭐️⭐️⭐️"}
11 |
12 |
13 |
14 | def insert_book_to_notion(books, index, bookId):
15 | """插入Book到Notion"""
16 | book = {}
17 | if bookId in archive_dict:
18 | book["书架分类"] = archive_dict.get(bookId)
19 | if bookId in notion_books:
20 | book.update(notion_books.get(bookId))
21 | bookInfo = weread_api.get_bookinfo(bookId)
22 | if bookInfo != None:
23 | book.update(bookInfo)
24 | readInfo = weread_api.get_read_info(bookId)
25 | # 研究了下这个状态不知道什么情况有的虽然读了状态还是1 markedStatus = 1 想读 4 读完 其他为在读
26 | readInfo.update(readInfo.get("readDetail", {}))
27 | readInfo.update(readInfo.get("bookInfo", {}))
28 | book.update(readInfo)
29 | book["阅读进度"] = (
30 | 100 if (book.get("markedStatus") == 4) else book.get("readingProgress", 0)
31 | ) / 100
32 | markedStatus = book.get("markedStatus")
33 | status = "想读"
34 | if markedStatus == 4:
35 | status = "已读"
36 | elif book.get("readingTime", 0) >= 60:
37 | status = "在读"
38 | book["阅读状态"] = status
39 | book["阅读时长"] = book.get("readingTime")
40 | book["阅读天数"] = book.get("totalReadDay")
41 | book["评分"] = book.get("newRating")
42 | if book.get("newRatingDetail") and book.get("newRatingDetail").get("myRating"):
43 | book["我的评分"] = rating.get(book.get("newRatingDetail").get("myRating"))
44 | elif status == "已读":
45 | book["我的评分"] = "未评分"
46 | book["时间"] = (
47 | book.get("finishedDate")
48 | or book.get("lastReadingDate")
49 | or book.get("readingBookDate")
50 | )
51 | book["开始阅读时间"] = book.get("beginReadingDate")
52 | book["最后阅读时间"] = book.get("lastReadingDate")
53 | cover = book.get("cover").replace("/s_", "/t7_")
54 | if not cover or not cover.strip() or not cover.startswith("http"):
55 | cover = BOOK_ICON_URL
56 | if bookId not in notion_books:
57 | book["书名"] = book.get("title")
58 | book["BookId"] = book.get("bookId")
59 | book["ISBN"] = book.get("isbn")
60 | book["链接"] = weread_api.get_url(bookId)
61 | book["简介"] = book.get("intro")
62 | book["作者"] = [
63 | notion_helper.get_relation_id(
64 | x, notion_helper.author_database_id, USER_ICON_URL
65 | )
66 | for x in book.get("author").split(" ")
67 | ]
68 | if book.get("categories"):
69 | book["分类"] = [
70 | notion_helper.get_relation_id(
71 | x.get("title"), notion_helper.category_database_id, TAG_ICON_URL
72 | )
73 | for x in book.get("categories")
74 | ]
75 | properties = utils.get_properties(book, book_properties_type_dict)
76 | if book.get("时间"):
77 | notion_helper.get_date_relation(
78 | properties,
79 | pendulum.from_timestamp(book.get("时间"), tz="Asia/Shanghai"),
80 | )
81 |
82 | print(
83 | f"正在插入《{book.get('title')}》,一共{len(books)}本,当前是第{index+1}本。"
84 | )
85 | parent = {"database_id": notion_helper.book_database_id, "type": "database_id"}
86 | result = None
87 | if bookId in notion_books:
88 | result = notion_helper.update_page(
89 | page_id=notion_books.get(bookId).get("pageId"),
90 | properties=properties,
91 | cover=utils.get_icon(cover),
92 | )
93 | else:
94 | result = notion_helper.create_book_page(
95 | parent=parent,
96 | properties=properties,
97 | icon=utils.get_icon(cover),
98 | )
99 | page_id = result.get("id")
100 | if book.get("readDetail") and book.get("readDetail").get("data"):
101 | data = book.get("readDetail").get("data")
102 | data = {item.get("readDate"): item.get("readTime") for item in data}
103 | insert_read_data(page_id, data)
104 |
105 |
106 | def insert_read_data(page_id, readTimes):
107 | readTimes = dict(sorted(readTimes.items()))
108 | filter = {"property": "书架", "relation": {"contains": page_id}}
109 | results = notion_helper.query_all_by_book(notion_helper.read_database_id, filter)
110 | for result in results:
111 | timestamp = result.get("properties").get("时间戳").get("number")
112 | duration = result.get("properties").get("时长").get("number")
113 | id = result.get("id")
114 | if timestamp in readTimes:
115 | value = readTimes.pop(timestamp)
116 | if value != duration:
117 | insert_to_notion(
118 | page_id=id,
119 | timestamp=timestamp,
120 | duration=value,
121 | book_database_id=page_id,
122 | )
123 | for key, value in readTimes.items():
124 | insert_to_notion(None, int(key), value, page_id)
125 |
126 |
127 | def insert_to_notion(page_id, timestamp, duration, book_database_id):
128 | parent = {"database_id": notion_helper.read_database_id, "type": "database_id"}
129 | properties = {
130 | "标题": utils.get_title(
131 | pendulum.from_timestamp(timestamp, tz=tz).to_date_string()
132 | ),
133 | "日期": utils.get_date(
134 | start=pendulum.from_timestamp(timestamp, tz=tz).format(
135 | "YYYY-MM-DD HH:mm:ss"
136 | )
137 | ),
138 | "时长": utils.get_number(duration),
139 | "时间戳": utils.get_number(timestamp),
140 | "书架": utils.get_relation([book_database_id]),
141 | }
142 | if page_id != None:
143 | notion_helper.client.pages.update(page_id=page_id, properties=properties)
144 | else:
145 | notion_helper.client.pages.create(
146 | parent=parent,
147 | icon=utils.get_icon("https://www.notion.so/icons/target_red.svg"),
148 | properties=properties,
149 | )
150 |
151 |
152 | weread_api = WeReadApi()
153 | notion_helper = NotionHelper()
154 | archive_dict = {}
155 | notion_books = {}
156 |
157 |
158 | def main():
159 | global notion_books
160 | global archive_dict
161 | bookshelf_books = weread_api.get_bookshelf()
162 | notion_books = notion_helper.get_all_book()
163 | bookProgress = bookshelf_books.get("bookProgress")
164 | bookProgress = {book.get("bookId"): book for book in bookProgress}
165 | for archive in bookshelf_books.get("archive"):
166 | name = archive.get("name")
167 | bookIds = archive.get("bookIds")
168 | archive_dict.update({bookId: name for bookId in bookIds})
169 | not_need_sync = []
170 | for key, value in notion_books.items():
171 | if (
172 | (
173 | key not in bookProgress
174 | or value.get("readingTime") == bookProgress.get(key).get("readingTime")
175 | )
176 | and (archive_dict.get(key) == value.get("category"))
177 | and (value.get("cover") is not None)
178 | and (
179 | value.get("status") != "已读"
180 | or (value.get("status") == "已读" and value.get("myRating"))
181 | )
182 | ):
183 | not_need_sync.append(key)
184 | notebooks = weread_api.get_notebooklist()
185 | notebooks = [d["bookId"] for d in notebooks if "bookId" in d]
186 | books = bookshelf_books.get("books")
187 | books = [d["bookId"] for d in books if "bookId" in d]
188 | books = list((set(notebooks) | set(books)) - set(not_need_sync))
189 | for index, bookId in enumerate(books):
190 | insert_book_to_notion(books, index, bookId)
191 |
192 |
193 | if __name__ == "__main__":
194 | main()
195 |
--------------------------------------------------------------------------------
/weread2notionpro/weread.py:
--------------------------------------------------------------------------------
1 | from weread2notionpro.notion_helper import NotionHelper
2 | from weread2notionpro.weread_api import WeReadApi
3 |
4 | from weread2notionpro.utils import (
5 | get_block,
6 | get_heading,
7 | get_number,
8 | get_number_from_result,
9 | get_quote,
10 | get_rich_text_from_result,
11 | get_table_of_contents,
12 | )
13 |
14 |
15 | def get_bookmark_list(page_id, bookId):
16 | """获取我的划线"""
17 | filter = {
18 | "and": [
19 | {"property": "书籍", "relation": {"contains": page_id}},
20 | {"property": "blockId", "rich_text": {"is_not_empty": True}},
21 | ]
22 | }
23 | results = notion_helper.query_all_by_book(
24 | notion_helper.bookmark_database_id, filter
25 | )
26 | dict1 = {
27 | get_rich_text_from_result(x, "bookmarkId"): get_rich_text_from_result(
28 | x, "blockId"
29 | )
30 | for x in results
31 | }
32 | dict2 = {get_rich_text_from_result(x, "blockId"): x.get("id") for x in results}
33 | bookmarks = weread_api.get_bookmark_list(bookId)
34 | for i in bookmarks:
35 | if i.get("bookmarkId") in dict1:
36 | i["blockId"] = dict1.pop(i.get("bookmarkId"))
37 | for blockId in dict1.values():
38 | notion_helper.delete_block(blockId)
39 | notion_helper.delete_block(dict2.get(blockId))
40 | return bookmarks
41 |
42 |
43 | def get_review_list(page_id,bookId):
44 | """获取笔记"""
45 | filter = {
46 | "and": [
47 | {"property": "书籍", "relation": {"contains": page_id}},
48 | {"property": "blockId", "rich_text": {"is_not_empty": True}},
49 | ]
50 | }
51 | results = notion_helper.query_all_by_book(notion_helper.review_database_id, filter)
52 | dict1 = {
53 | get_rich_text_from_result(x, "reviewId"): get_rich_text_from_result(
54 | x, "blockId"
55 | )
56 | for x in results
57 | }
58 | dict2 = {get_rich_text_from_result(x, "blockId"): x.get("id") for x in results}
59 | reviews = weread_api.get_review_list(bookId)
60 | for i in reviews:
61 | if i.get("reviewId") in dict1:
62 | i["blockId"] = dict1.pop(i.get("reviewId"))
63 | for blockId in dict1.values():
64 | notion_helper.delete_block(blockId)
65 | notion_helper.delete_block(dict2.get(blockId))
66 | return reviews
67 |
68 |
69 | def check(bookId):
70 | """检查是否已经插入过"""
71 | filter = {"property": "BookId", "rich_text": {"equals": bookId}}
72 | response = notion_helper.query(
73 | database_id=notion_helper.book_database_id, filter=filter
74 | )
75 | if len(response["results"]) > 0:
76 | return response["results"][0]["id"]
77 | return None
78 |
79 |
80 | def get_sort():
81 | """获取database中的最新时间"""
82 | filter = {"property": "Sort", "number": {"is_not_empty": True}}
83 | sorts = [
84 | {
85 | "property": "Sort",
86 | "direction": "descending",
87 | }
88 | ]
89 | response = notion_helper.query(
90 | database_id=notion_helper.book_database_id,
91 | filter=filter,
92 | sorts=sorts,
93 | page_size=1,
94 | )
95 | if len(response.get("results")) == 1:
96 | return response.get("results")[0].get("properties").get("Sort").get("number")
97 | return 0
98 |
99 |
100 |
101 | def sort_notes(page_id, chapter, bookmark_list):
102 | """对笔记进行排序"""
103 | bookmark_list = sorted(
104 | bookmark_list,
105 | key=lambda x: (
106 | x.get("chapterUid", 1),
107 | 0
108 | if (x.get("range", "") == "" or x.get("range").split("-")[0] == "")
109 | else int(x.get("range").split("-")[0]),
110 | ),
111 | )
112 |
113 | notes = []
114 | if chapter != None:
115 | filter = {"property": "书籍", "relation": {"contains": page_id}}
116 | results = notion_helper.query_all_by_book(
117 | notion_helper.chapter_database_id, filter
118 | )
119 | dict1 = {
120 | get_number_from_result(x, "chapterUid"): get_rich_text_from_result(
121 | x, "blockId"
122 | )
123 | for x in results
124 | }
125 | dict2 = {get_rich_text_from_result(x, "blockId"): x.get("id") for x in results}
126 | d = {}
127 | for data in bookmark_list:
128 | chapterUid = data.get("chapterUid", 1)
129 | if chapterUid not in d:
130 | d[chapterUid] = []
131 | d[chapterUid].append(data)
132 | for key, value in d.items():
133 | if key in chapter:
134 | if key in dict1:
135 | chapter.get(key)["blockId"] = dict1.pop(key)
136 | notes.append(chapter.get(key))
137 | notes.extend(value)
138 | for blockId in dict1.values():
139 | notion_helper.delete_block(blockId)
140 | notion_helper.delete_block(dict2.get(blockId))
141 | else:
142 | notes.extend(bookmark_list)
143 | return notes
144 |
145 |
146 | def append_blocks(id, contents):
147 | print(f"笔记数{len(contents)}")
148 | before_block_id = ""
149 | block_children = notion_helper.get_block_children(id)
150 | if len(block_children) > 0 and block_children[0].get("type") == "table_of_contents":
151 | before_block_id = block_children[0].get("id")
152 | else:
153 | response = notion_helper.append_blocks(
154 | block_id=id, children=[get_table_of_contents()]
155 | )
156 | before_block_id = response.get("results")[0].get("id")
157 | blocks = []
158 | sub_contents = []
159 | l = []
160 | for content in contents:
161 | if len(blocks) == 100:
162 | results = append_blocks_to_notion(id, blocks, before_block_id, sub_contents)
163 | before_block_id = results[-1].get("blockId")
164 | l.extend(results)
165 | blocks.clear()
166 | sub_contents.clear()
167 | if not notion_helper.sync_bookmark and content.get("type")==0:
168 | continue
169 | blocks.append(content_to_block(content))
170 | sub_contents.append(content)
171 | elif "blockId" in content:
172 | if len(blocks) > 0:
173 | l.extend(
174 | append_blocks_to_notion(id, blocks, before_block_id, sub_contents)
175 | )
176 | blocks.clear()
177 | sub_contents.clear()
178 | before_block_id = content["blockId"]
179 | else:
180 | if not notion_helper.sync_bookmark and content.get("type")==0:
181 | continue
182 | blocks.append(content_to_block(content))
183 | sub_contents.append(content)
184 |
185 | if len(blocks) > 0:
186 | l.extend(append_blocks_to_notion(id, blocks, before_block_id, sub_contents))
187 | for index, value in enumerate(l):
188 | print(f"正在插入第{index+1}条笔记,共{len(l)}条")
189 | if "bookmarkId" in value:
190 | notion_helper.insert_bookmark(id, value)
191 | elif "reviewId" in value:
192 | notion_helper.insert_review(id, value)
193 | else:
194 | notion_helper.insert_chapter(id, value)
195 |
196 |
197 | def content_to_block(content):
198 | if "bookmarkId" in content:
199 | return get_block(
200 | content.get("markText",""),
201 | notion_helper.block_type,
202 | notion_helper.show_color,
203 | content.get("style"),
204 | content.get("colorStyle"),
205 | content.get("reviewId"),
206 | )
207 | elif "reviewId" in content:
208 | return get_block(
209 | content.get("content",""),
210 | notion_helper.block_type,
211 | notion_helper.show_color,
212 | content.get("style"),
213 | content.get("colorStyle"),
214 | content.get("reviewId"),
215 | )
216 | else:
217 | return get_heading(content.get("level"), content.get("title"))
218 |
219 |
220 | def append_blocks_to_notion(id, blocks, after, contents):
221 | response = notion_helper.append_blocks_after(
222 | block_id=id, children=blocks, after=after
223 | )
224 | results = response.get("results")
225 | l = []
226 | for index, content in enumerate(contents):
227 | result = results[index]
228 | if content.get("abstract") != None and content.get("abstract") != "":
229 | notion_helper.append_blocks(
230 | block_id=result.get("id"), children=[get_quote(content.get("abstract"))]
231 | )
232 | content["blockId"] = result.get("id")
233 | l.append(content)
234 | return l
235 |
236 | weread_api = WeReadApi()
237 | notion_helper = NotionHelper()
238 | def main():
239 | notion_books = notion_helper.get_all_book()
240 | books = weread_api.get_notebooklist()
241 | if books != None:
242 | for index, book in enumerate(books):
243 | bookId = book.get("bookId")
244 | title = book.get("book").get("title")
245 | sort = book.get("sort")
246 | if bookId not in notion_books:
247 | continue
248 | if sort == notion_books.get(bookId).get("Sort"):
249 | continue
250 | pageId = notion_books.get(bookId).get("pageId")
251 | print(f"正在同步《{title}》,一共{len(books)}本,当前是第{index+1}本。")
252 | chapter = weread_api.get_chapter_info(bookId)
253 | bookmark_list = get_bookmark_list(pageId, bookId)
254 | reviews = get_review_list(pageId,bookId)
255 | bookmark_list.extend(reviews)
256 | content = sort_notes(pageId, chapter, bookmark_list)
257 | append_blocks(pageId, content)
258 | properties = {
259 | "Sort":get_number(sort)
260 | }
261 | notion_helper.update_book_page(page_id=pageId,properties=properties)
262 |
263 | if __name__ == "__main__":
264 | main()
265 |
266 |
--------------------------------------------------------------------------------
/weread2notionpro/weread_api.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import json
3 | import os
4 | import re
5 |
6 | import requests
7 | from requests.utils import cookiejar_from_dict
8 | from retrying import retry
9 | from urllib.parse import quote
10 | from dotenv import load_dotenv
11 |
12 | load_dotenv()
13 | WEREAD_URL = "https://weread.qq.com/"
14 | WEREAD_NOTEBOOKS_URL = "https://i.weread.qq.com/user/notebooks"
15 | WEREAD_BOOKMARKLIST_URL = "https://i.weread.qq.com/book/bookmarklist"
16 | WEREAD_CHAPTER_INFO = "https://i.weread.qq.com/book/chapterInfos"
17 | WEREAD_READ_INFO_URL = "https://i.weread.qq.com/book/readinfo"
18 | WEREAD_REVIEW_LIST_URL = "https://i.weread.qq.com/review/list"
19 | WEREAD_BOOK_INFO = "https://i.weread.qq.com/book/info"
20 | WEREAD_READDATA_DETAIL = "https://i.weread.qq.com/readdata/detail"
21 | WEREAD_HISTORY_URL = "https://i.weread.qq.com/readdata/summary?synckey=0"
22 |
23 |
24 | class WeReadApi:
25 | def __init__(self):
26 | self.cookie = self.get_cookie()
27 | self.session = requests.Session()
28 | self.session.cookies = self.parse_cookie_string()
29 |
30 | def try_get_cloud_cookie(self, url, id, password):
31 | if url.endswith("/"):
32 | url = url[:-1]
33 | req_url = f"{url}/get/{id}"
34 | data = {"password": password}
35 | result = None
36 | response = requests.post(req_url, data=data)
37 | if response.status_code == 200:
38 | data = response.json()
39 | cookie_data = data.get("cookie_data")
40 | if cookie_data and "weread.qq.com" in cookie_data:
41 | cookies = cookie_data["weread.qq.com"]
42 | cookie_str = "; ".join(
43 | [f"{cookie['name']}={cookie['value']}" for cookie in cookies]
44 | )
45 | result = cookie_str
46 | return result
47 |
48 | def get_cookie(self):
49 | url = os.getenv("CC_URL")
50 | if not url:
51 | url = "https://cookiecloud.malinkang.com/"
52 | id = os.getenv("CC_ID")
53 | password = os.getenv("CC_PASSWORD")
54 | cookie = os.getenv("WEREAD_COOKIE")
55 | if url and id and password:
56 | cookie = self.try_get_cloud_cookie(url, id, password)
57 | if not cookie or not cookie.strip():
58 | raise Exception("没有找到cookie,请按照文档填写cookie")
59 | return cookie
60 |
61 | def parse_cookie_string(self):
62 | cookies_dict = {}
63 |
64 | # 使用正则表达式解析 cookie 字符串
65 | pattern = re.compile(r'([^=]+)=([^;]+);?\s*')
66 | matches = pattern.findall(self.cookie)
67 |
68 | for key, value in matches:
69 | cookies_dict[key] = value.encode('unicode_escape').decode('ascii')
70 | # 直接使用 cookies_dict 创建 cookiejar
71 | cookiejar = cookiejar_from_dict(cookies_dict)
72 |
73 | return cookiejar
74 |
75 | def get_bookshelf(self):
76 | self.session.get(WEREAD_URL)
77 | r = self.session.get(
78 | "https://i.weread.qq.com/shelf/sync?synckey=0&teenmode=0&album=1&onlyBookid=0"
79 | )
80 | if r.ok:
81 | return r.json()
82 | else:
83 | errcode = r.json().get("errcode",0)
84 | self.handle_errcode(errcode)
85 | raise Exception(f"Could not get bookshelf {r.text}")
86 |
87 | def handle_errcode(self,errcode):
88 | if( errcode== -2012 or errcode==-2010):
89 | print(f"::error::微信读书Cookie过期了,请参考文档重新设置。https://mp.weixin.qq.com/s/B_mqLUZv7M1rmXRsMlBf7A")
90 |
91 | @retry(stop_max_attempt_number=3, wait_fixed=5000)
92 | def get_notebooklist(self):
93 | """获取笔记本列表"""
94 | self.session.get(WEREAD_URL)
95 | r = self.session.get(WEREAD_NOTEBOOKS_URL)
96 | if r.ok:
97 | data = r.json()
98 | books = data.get("books")
99 | books.sort(key=lambda x: x["sort"])
100 | return books
101 | else:
102 | errcode = r.json().get("errcode",0)
103 | self.handle_errcode(errcode)
104 | raise Exception(f"Could not get notebook list {r.text}")
105 |
106 | @retry(stop_max_attempt_number=3, wait_fixed=5000)
107 | def get_bookinfo(self, bookId):
108 | """获取书的详情"""
109 | self.session.get(WEREAD_URL)
110 | params = dict(bookId=bookId)
111 | r = self.session.get(WEREAD_BOOK_INFO, params=params)
112 | if r.ok:
113 | return r.json()
114 | else:
115 | errcode = r.json().get("errcode",0)
116 | self.handle_errcode(errcode)
117 | print(f"Could not get book info {r.text}")
118 |
119 |
120 | @retry(stop_max_attempt_number=3, wait_fixed=5000)
121 | def get_bookmark_list(self, bookId):
122 | self.session.get(WEREAD_URL)
123 | params = dict(bookId=bookId)
124 | r = self.session.get(WEREAD_BOOKMARKLIST_URL, params=params)
125 | if r.ok:
126 | with open("bookmark.json","w") as f:
127 | f.write(json.dumps(r.json(),indent=4,ensure_ascii=False))
128 | bookmarks = r.json().get("updated")
129 | return bookmarks
130 | else:
131 | errcode = r.json().get("errcode",0)
132 | self.handle_errcode(errcode)
133 | raise Exception(f"Could not get {bookId} bookmark list")
134 |
135 | @retry(stop_max_attempt_number=3, wait_fixed=5000)
136 | def get_read_info(self, bookId):
137 | self.session.get(WEREAD_URL)
138 | params = dict(
139 | noteCount=1,
140 | readingDetail=1,
141 | finishedBookIndex=1,
142 | readingBookCount=1,
143 | readingBookIndex=1,
144 | finishedBookCount=1,
145 | bookId=bookId,
146 | finishedDate=1,
147 | )
148 | headers = {
149 | "baseapi":"32",
150 | "appver":"8.2.5.10163885",
151 | "basever":"8.2.5.10163885",
152 | "osver":"12",
153 | "User-Agent": "WeRead/8.2.5 WRBrand/xiaomi Dalvik/2.1.0 (Linux; U; Android 12; Redmi Note 7 Pro Build/SQ3A.220705.004)",
154 | }
155 | r = self.session.get(WEREAD_READ_INFO_URL,headers=headers, params=params)
156 | if r.ok:
157 | return r.json()
158 | else:
159 | errcode = r.json().get("errcode",0)
160 | self.handle_errcode(errcode)
161 | raise Exception(f"get {bookId} read info failed {r.text}")
162 |
163 | @retry(stop_max_attempt_number=3, wait_fixed=5000)
164 | def get_review_list(self, bookId):
165 | self.session.get(WEREAD_URL)
166 | params = dict(bookId=bookId, listType=11, mine=1, syncKey=0)
167 | r = self.session.get(WEREAD_REVIEW_LIST_URL, params=params)
168 | if r.ok:
169 | reviews = r.json().get("reviews")
170 | reviews = list(map(lambda x: x.get("review"), reviews))
171 | reviews = [
172 | {"chapterUid": 1000000, **x} if x.get("type") == 4 else x
173 | for x in reviews
174 | ]
175 | return reviews
176 | else:
177 | errcode = r.json().get("errcode",0)
178 | self.handle_errcode(errcode)
179 | raise Exception(f"get {bookId} review list failed {r.text}")
180 |
181 |
182 |
183 |
184 | def get_api_data(self):
185 | self.session.get(WEREAD_URL)
186 | r = self.session.get(WEREAD_HISTORY_URL)
187 | if r.ok:
188 | return r.json()
189 | else:
190 | errcode = r.json().get("errcode",0)
191 | self.handle_errcode(errcode)
192 | raise Exception(f"get history data failed {r.text}")
193 |
194 |
195 |
196 | @retry(stop_max_attempt_number=3, wait_fixed=5000)
197 | def get_chapter_info(self, bookId):
198 | self.session.get(WEREAD_URL)
199 | body = {"bookIds": [bookId], "synckeys": [0], "teenmode": 0}
200 | r = self.session.post(WEREAD_CHAPTER_INFO, json=body)
201 | if (
202 | r.ok
203 | and "data" in r.json()
204 | and len(r.json()["data"]) == 1
205 | and "updated" in r.json()["data"][0]
206 | ):
207 | update = r.json()["data"][0]["updated"]
208 | update.append(
209 | {
210 | "chapterUid": 1000000,
211 | "chapterIdx": 1000000,
212 | "updateTime": 1683825006,
213 | "readAhead": 0,
214 | "title": "点评",
215 | "level": 1,
216 | }
217 | )
218 | return {item["chapterUid"]: item for item in update}
219 | else:
220 | raise Exception(f"get {bookId} chapter info failed {r.text}")
221 |
222 | def transform_id(self, book_id):
223 | id_length = len(book_id)
224 | if re.match("^\\d*$", book_id):
225 | ary = []
226 | for i in range(0, id_length, 9):
227 | ary.append(format(int(book_id[i : min(i + 9, id_length)]), "x"))
228 | return "3", ary
229 |
230 | result = ""
231 | for i in range(id_length):
232 | result += format(ord(book_id[i]), "x")
233 | return "4", [result]
234 |
235 | def calculate_book_str_id(self, book_id):
236 | md5 = hashlib.md5()
237 | md5.update(book_id.encode("utf-8"))
238 | digest = md5.hexdigest()
239 | result = digest[0:3]
240 | code, transformed_ids = self.transform_id(book_id)
241 | result += code + "2" + digest[-2:]
242 |
243 | for i in range(len(transformed_ids)):
244 | hex_length_str = format(len(transformed_ids[i]), "x")
245 | if len(hex_length_str) == 1:
246 | hex_length_str = "0" + hex_length_str
247 |
248 | result += hex_length_str + transformed_ids[i]
249 |
250 | if i < len(transformed_ids) - 1:
251 | result += "g"
252 |
253 | if len(result) < 20:
254 | result += digest[0 : 20 - len(result)]
255 |
256 | md5 = hashlib.md5()
257 | md5.update(result.encode("utf-8"))
258 | result += md5.hexdigest()[0:3]
259 | return result
260 |
261 | def get_url(self, book_id):
262 | return f"https://weread.qq.com/web/reader/{self.calculate_book_str_id(book_id)}"
263 |
--------------------------------------------------------------------------------
/weread2notionpro/utils.py:
--------------------------------------------------------------------------------
1 | import calendar
2 | from datetime import datetime
3 | from datetime import timedelta
4 | import hashlib
5 | import os
6 | import re
7 | import requests
8 | import base64
9 | from weread2notionpro.config import (
10 | RICH_TEXT,
11 | URL,
12 | RELATION,
13 | NUMBER,
14 | DATE,
15 | FILES,
16 | STATUS,
17 | TITLE,
18 | SELECT,
19 | )
20 | import pendulum
21 |
22 | MAX_LENGTH = (
23 | 1024 # NOTION 2000个字符限制https://developers.notion.com/reference/request-limits
24 | )
25 |
26 |
27 | def get_heading(level, content):
28 | if level == 1:
29 | heading = "heading_1"
30 | elif level == 2:
31 | heading = "heading_2"
32 | else:
33 | heading = "heading_3"
34 | return {
35 | "type": heading,
36 | heading: {
37 | "rich_text": [
38 | {
39 | "type": "text",
40 | "text": {
41 | "content": content[:MAX_LENGTH],
42 | },
43 | }
44 | ],
45 | "color": "default",
46 | "is_toggleable": False,
47 | },
48 | }
49 |
50 |
51 | def get_table_of_contents():
52 | """获取目录"""
53 | return {"type": "table_of_contents", "table_of_contents": {"color": "default"}}
54 |
55 |
56 | def get_title(content):
57 | return {"title": [{"type": "text", "text": {"content": content[:MAX_LENGTH]}}]}
58 |
59 |
60 | def get_rich_text(content):
61 | return {"rich_text": [{"type": "text", "text": {"content": content[:MAX_LENGTH]}}]}
62 |
63 |
64 | def get_url(url):
65 | return {"url": url}
66 |
67 |
68 | def get_file(url):
69 | return {"files": [{"type": "external", "name": "Cover", "external": {"url": url}}]}
70 |
71 |
72 | def get_multi_select(names):
73 | return {"multi_select": [{"name": name} for name in names]}
74 |
75 |
76 | def get_relation(ids):
77 | return {"relation": [{"id": id} for id in ids]}
78 |
79 |
80 | def get_date(start, end=None):
81 | return {
82 | "date": {
83 | "start": start,
84 | "end": end,
85 | "time_zone": "Asia/Shanghai",
86 | }
87 | }
88 |
89 |
90 | def get_icon(url):
91 | return {"type": "external", "external": {"url": url}}
92 |
93 |
94 | def get_select(name):
95 | return {"select": {"name": name}}
96 |
97 |
98 | def get_number(number):
99 | return {"number": number}
100 |
101 |
102 | def get_quote(content):
103 | return {
104 | "type": "quote",
105 | "quote": {
106 | "rich_text": [
107 | {
108 | "type": "text",
109 | "text": {"content": content[:MAX_LENGTH]},
110 | }
111 | ],
112 | "color": "default",
113 | },
114 | }
115 |
116 |
117 | def get_block(content,type,show_color, style, colorStyle, reviewId):
118 | color = "default"
119 | if show_color:
120 | # 根据划线颜色设置文字的颜色
121 | if colorStyle == 1:
122 | color = "red"
123 | elif colorStyle == 2:
124 | color = "purple"
125 | elif colorStyle == 3:
126 | color = "blue"
127 | elif colorStyle == 4:
128 | color = "green"
129 | elif colorStyle == 5:
130 | color = "yellow"
131 | block = {
132 | "type": type,
133 | type: {
134 | "rich_text": [
135 | {
136 | "type": "text",
137 | "text": {
138 | "content": content[:MAX_LENGTH],
139 | },
140 | }
141 | ],
142 | "color": color,
143 | },
144 | }
145 | if(type=="callout"):
146 | # 根据不同的划线样式设置不同的emoji 直线type=0 背景颜色是1 波浪线是2
147 | emoji = "〰️"
148 | if style == 0:
149 | emoji = "💡"
150 | elif style == 1:
151 | emoji = "⭐"
152 | # 如果reviewId不是空说明是笔记
153 | if reviewId != None:
154 | emoji = "✍️"
155 | block[type]["icon"] = {"emoji": emoji}
156 | return block
157 |
158 |
159 | def get_rich_text_from_result(result, name):
160 | return result.get("properties").get(name).get("rich_text")[0].get("plain_text")
161 |
162 |
163 | def get_number_from_result(result, name):
164 | return result.get("properties").get(name).get("number")
165 |
166 |
167 | def format_time(time):
168 | """将秒格式化为 xx时xx分格式"""
169 | result = ""
170 | hour = time // 3600
171 | if hour > 0:
172 | result += f"{hour}时"
173 | minutes = time % 3600 // 60
174 | if minutes > 0:
175 | result += f"{minutes}分"
176 | return result
177 |
178 |
179 | def format_date(date, format="%Y-%m-%d %H:%M:%S"):
180 | return date.strftime(format)
181 |
182 |
183 | def timestamp_to_date(timestamp):
184 | """时间戳转化为date"""
185 | return datetime.utcfromtimestamp(timestamp) + timedelta(hours=8)
186 |
187 |
188 | def get_first_and_last_day_of_month(date):
189 | # 获取给定日期所在月的第一天
190 | first_day = date.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
191 |
192 | # 获取给定日期所在月的最后一天
193 | _, last_day_of_month = calendar.monthrange(date.year, date.month)
194 | last_day = date.replace(
195 | day=last_day_of_month, hour=0, minute=0, second=0, microsecond=0
196 | )
197 |
198 | return first_day, last_day
199 |
200 |
201 | def get_first_and_last_day_of_year(date):
202 | # 获取给定日期所在年的第一天
203 | first_day = date.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
204 |
205 | # 获取给定日期所在年的最后一天
206 | last_day = date.replace(month=12, day=31, hour=0, minute=0, second=0, microsecond=0)
207 |
208 | return first_day, last_day
209 |
210 |
211 | def get_first_and_last_day_of_week(date):
212 | # 获取给定日期所在周的第一天(星期一)
213 | first_day_of_week = (date - timedelta(days=date.weekday())).replace(
214 | hour=0, minute=0, second=0, microsecond=0
215 | )
216 |
217 | # 获取给定日期所在周的最后一天(星期日)
218 | last_day_of_week = first_day_of_week + timedelta(days=6)
219 |
220 | return first_day_of_week, last_day_of_week
221 |
222 | def get_properties(dict1, dict2):
223 | properties = {}
224 | for key, value in dict1.items():
225 | type = dict2.get(key)
226 | if value == None:
227 | continue
228 | property = None
229 | if type == TITLE:
230 | property = {
231 | "title": [{"type": "text", "text": {"content": value[:MAX_LENGTH]}}]
232 | }
233 | elif type == RICH_TEXT:
234 | property = {
235 | "rich_text": [{"type": "text", "text": {"content": value[:MAX_LENGTH]}}]
236 | }
237 | elif type == NUMBER:
238 | property = {"number": value}
239 | elif type == STATUS:
240 | property = {"status": {"name": value}}
241 | elif type == FILES:
242 | property = {
243 | "files": [
244 | {"type": "external", "name": "Cover", "external": {"url": value}}
245 | ]
246 | }
247 | elif type == DATE:
248 | property = {
249 | "date": {
250 | "start": pendulum.from_timestamp(
251 | value, tz="Asia/Shanghai"
252 | ).to_datetime_string(),
253 | "time_zone": "Asia/Shanghai",
254 | }
255 | }
256 | elif type == URL:
257 | property = {"url": value}
258 | elif type == SELECT:
259 | property = {"select": {"name": value}}
260 | elif type == RELATION:
261 | property = {"relation": [{"id": id} for id in value]}
262 | if property:
263 | properties[key] = property
264 | return properties
265 |
266 |
267 | def get_property_value(property):
268 | """从Property中获取值"""
269 | type = property.get("type")
270 | content = property.get(type)
271 | if content is None:
272 | return None
273 | if type == "title" or type == "rich_text":
274 | if len(content) > 0:
275 | return content[0].get("plain_text")
276 | else:
277 | return None
278 | elif type == "status" or type == "select":
279 | return content.get("name")
280 | elif type == "files":
281 | # 不考虑多文件情况
282 | if len(content) > 0 and content[0].get("type") == "external":
283 | return content[0].get("external").get("url")
284 | else:
285 | return None
286 | elif type == "date":
287 | return str_to_timestamp(content.get("start"))
288 | else:
289 | return content
290 |
291 |
292 |
293 |
294 | def str_to_timestamp(date):
295 | if date == None:
296 | return 0
297 | dt = pendulum.parse(date)
298 | # 获取时间戳
299 | return int(dt.timestamp())
300 |
301 |
302 | upload_url = "https://wereadassets.malinkang.com/"
303 |
304 |
305 | def upload_image(folder_path, filename, file_path):
306 | # 将文件内容编码为Base64
307 | with open(file_path, "rb") as file:
308 | content_base64 = base64.b64encode(file.read()).decode("utf-8")
309 |
310 | # 构建请求的JSON数据
311 | data = {"file": content_base64, "filename": filename, "folder": folder_path}
312 |
313 | response = requests.post(upload_url, json=data)
314 |
315 | if response.status_code == 200:
316 | print("File uploaded successfully.")
317 | return response.text
318 | else:
319 | return None
320 |
321 |
322 | def url_to_md5(url):
323 | # 创建一个md5哈希对象
324 | md5_hash = hashlib.md5()
325 |
326 | # 对URL进行编码,准备进行哈希处理
327 | # 默认使用utf-8编码
328 | encoded_url = url.encode("utf-8")
329 |
330 | # 更新哈希对象的状态
331 | md5_hash.update(encoded_url)
332 |
333 | # 获取十六进制的哈希表示
334 | hex_digest = md5_hash.hexdigest()
335 |
336 | return hex_digest
337 |
338 |
339 | def download_image(url, save_dir="cover"):
340 | # 确保目录存在,如果不存在则创建
341 | if not os.path.exists(save_dir):
342 | os.makedirs(save_dir)
343 |
344 | file_name = url_to_md5(url) + ".jpg"
345 | save_path = os.path.join(save_dir, file_name)
346 |
347 | # 检查文件是否已经存在,如果存在则不进行下载
348 | if os.path.exists(save_path):
349 | print(f"File {file_name} already exists. Skipping download.")
350 | return save_path
351 |
352 | response = requests.get(url, stream=True)
353 | if response.status_code == 200:
354 | with open(save_path, "wb") as file:
355 | for chunk in response.iter_content(chunk_size=128):
356 | file.write(chunk)
357 | print(f"Image downloaded successfully to {save_path}")
358 | else:
359 | print(f"Failed to download image. Status code: {response.status_code}")
360 | return save_path
361 |
362 |
363 |
364 | def get_embed(url):
365 | return {"type": "embed", "embed": {"url": url}}
366 |
--------------------------------------------------------------------------------
/weread2notionpro/notion_helper.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import re
4 | import time
5 |
6 | from notion_client import Client
7 | import pendulum
8 | from retrying import retry
9 | from datetime import timedelta
10 | from dotenv import load_dotenv
11 |
12 | load_dotenv()
13 | from weread2notionpro.utils import (
14 | format_date,
15 | get_date,
16 | get_first_and_last_day_of_month,
17 | get_first_and_last_day_of_week,
18 | get_first_and_last_day_of_year,
19 | get_icon,
20 | get_number,
21 | get_relation,
22 | get_rich_text,
23 | get_title,
24 | timestamp_to_date,
25 | get_property_value,
26 | )
27 |
28 | TAG_ICON_URL = "https://www.notion.so/icons/tag_gray.svg"
29 | USER_ICON_URL = "https://www.notion.so/icons/user-circle-filled_gray.svg"
30 | TARGET_ICON_URL = "https://www.notion.so/icons/target_red.svg"
31 | BOOKMARK_ICON_URL = "https://www.notion.so/icons/bookmark_gray.svg"
32 |
33 |
34 | class NotionHelper:
35 | database_name_dict = {
36 | "BOOK_DATABASE_NAME": "书架",
37 | "REVIEW_DATABASE_NAME": "笔记",
38 | "BOOKMARK_DATABASE_NAME": "划线",
39 | "DAY_DATABASE_NAME": "日",
40 | "WEEK_DATABASE_NAME": "周",
41 | "MONTH_DATABASE_NAME": "月",
42 | "YEAR_DATABASE_NAME": "年",
43 | "CATEGORY_DATABASE_NAME": "分类",
44 | "AUTHOR_DATABASE_NAME": "作者",
45 | "CHAPTER_DATABASE_NAME": "章节",
46 | "READ_DATABASE_NAME": "阅读记录",
47 | "SETTING_DATABASE_NAME": "设置",
48 | }
49 | database_id_dict = {}
50 | heatmap_block_id = None
51 | show_color = True
52 | block_type = "callout"
53 | sync_bookmark = True
54 | def __init__(self):
55 | self.client = Client(auth=os.getenv("NOTION_TOKEN"), log_level=logging.ERROR)
56 | self.__cache = {}
57 | self.page_id = self.extract_page_id(os.getenv("NOTION_PAGE"))
58 | self.search_database(self.page_id)
59 | for key in self.database_name_dict.keys():
60 | if os.getenv(key) != None and os.getenv(key) != "":
61 | self.database_name_dict[key] = os.getenv(key)
62 | self.book_database_id = self.database_id_dict.get(
63 | self.database_name_dict.get("BOOK_DATABASE_NAME")
64 | )
65 | self.review_database_id = self.database_id_dict.get(
66 | self.database_name_dict.get("REVIEW_DATABASE_NAME")
67 | )
68 | self.bookmark_database_id = self.database_id_dict.get(
69 | self.database_name_dict.get("BOOKMARK_DATABASE_NAME")
70 | )
71 | self.day_database_id = self.database_id_dict.get(
72 | self.database_name_dict.get("DAY_DATABASE_NAME")
73 | )
74 | self.week_database_id = self.database_id_dict.get(
75 | self.database_name_dict.get("WEEK_DATABASE_NAME")
76 | )
77 | self.month_database_id = self.database_id_dict.get(
78 | self.database_name_dict.get("MONTH_DATABASE_NAME")
79 | )
80 | self.year_database_id = self.database_id_dict.get(
81 | self.database_name_dict.get("YEAR_DATABASE_NAME")
82 | )
83 | self.category_database_id = self.database_id_dict.get(
84 | self.database_name_dict.get("CATEGORY_DATABASE_NAME")
85 | )
86 | self.author_database_id = self.database_id_dict.get(
87 | self.database_name_dict.get("AUTHOR_DATABASE_NAME")
88 | )
89 | self.chapter_database_id = self.database_id_dict.get(
90 | self.database_name_dict.get("CHAPTER_DATABASE_NAME")
91 | )
92 | self.read_database_id = self.database_id_dict.get(
93 | self.database_name_dict.get("READ_DATABASE_NAME")
94 | )
95 | self.setting_database_id = self.database_id_dict.get(
96 | self.database_name_dict.get("SETTING_DATABASE_NAME")
97 | )
98 | self.update_book_database()
99 | if self.read_database_id is None:
100 | self.create_database()
101 | if self.setting_database_id is None:
102 | self.create_setting_database()
103 | if self.setting_database_id:
104 | self.insert_to_setting_database()
105 |
106 | def extract_page_id(self, notion_url):
107 | # 正则表达式匹配 32 个字符的 Notion page_id
108 | match = re.search(
109 | r"([a-f0-9]{32}|[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})",
110 | notion_url,
111 | )
112 | if match:
113 | return match.group(0)
114 | else:
115 | raise Exception(f"获取NotionID失败,请检查输入的Url是否正确")
116 |
117 | def search_database(self, block_id):
118 | children = self.client.blocks.children.list(block_id=block_id)["results"]
119 | # 遍历子块
120 | for child in children:
121 | # 检查子块的类型
122 | if child["type"] == "child_database":
123 | self.database_id_dict[child.get("child_database").get("title")] = (
124 | child.get("id")
125 | )
126 | elif child["type"] == "embed" and child.get("embed").get("url"):
127 | if child.get("embed").get("url").startswith("https://heatmap.malinkang.com/"):
128 | self.heatmap_block_id = child.get("id")
129 | # 如果子块有子块,递归调用函数
130 | if "has_children" in child and child["has_children"]:
131 | self.search_database(child["id"])
132 |
133 | def update_book_database(self):
134 | """更新数据库"""
135 | response = self.client.databases.retrieve(database_id=self.book_database_id)
136 | id = response.get("id")
137 | properties = response.get("properties")
138 | update_properties = {}
139 | if (
140 | properties.get("阅读时长") is None
141 | or properties.get("阅读时长").get("type") != "number"
142 | ):
143 | update_properties["阅读时长"] = {"number": {}}
144 | if (
145 | properties.get("书架分类") is None
146 | or properties.get("书架分类").get("type") != "select"
147 | ):
148 | update_properties["书架分类"] = {"select": {}}
149 | if (
150 | properties.get("豆瓣链接") is None
151 | or properties.get("豆瓣链接").get("type") != "url"
152 | ):
153 | update_properties["豆瓣链接"] = {"url": {}}
154 | if (
155 | properties.get("我的评分") is None
156 | or properties.get("我的评分").get("type") != "select"
157 | ):
158 | update_properties["我的评分"] = {"select": {}}
159 | if (
160 | properties.get("豆瓣短评") is None
161 | or properties.get("豆瓣短评").get("type") != "rich_text"
162 | ):
163 | update_properties["豆瓣短评"] = {"rich_text": {}}
164 | """NeoDB先不添加了,现在受众还不广,可能有的小伙伴不知道是干什么的"""
165 | if len(update_properties) > 0:
166 | self.client.databases.update(database_id=id, properties=update_properties)
167 |
168 | def create_database(self):
169 | title = [
170 | {
171 | "type": "text",
172 | "text": {
173 | "content": self.database_name_dict.get("READ_DATABASE_NAME"),
174 | },
175 | },
176 | ]
177 | properties = {
178 | "标题": {"title": {}},
179 | "时长": {"number": {}},
180 | "时间戳": {"number": {}},
181 | "日期": {"date": {}},
182 | "书架": {
183 | "relation": {
184 | "database_id": self.book_database_id,
185 | "single_property": {},
186 | }
187 | },
188 | }
189 | parent = parent = {"page_id": self.page_id, "type": "page_id"}
190 | self.read_database_id = self.client.databases.create(
191 | parent=parent,
192 | title=title,
193 | icon=get_icon("https://www.notion.so/icons/target_gray.svg"),
194 | properties=properties,
195 | ).get("id")
196 |
197 | def create_setting_database(self):
198 | title = [
199 | {
200 | "type": "text",
201 | "text": {
202 | "content": self.database_name_dict.get("SETTING_DATABASE_NAME"),
203 | },
204 | },
205 | ]
206 | properties = {
207 | "标题": {"title": {}},
208 | "NotinToken": {"rich_text": {}},
209 | "NotinPage": {"rich_text": {}},
210 | "WeReadCookie": {"rich_text": {}},
211 | "根据划线颜色设置文字颜色": {"checkbox": {}},
212 | "同步书签": {"checkbox": {}},
213 | # "Cookie状态": {
214 | # "select": {
215 | # "options": [
216 | # {"name": "可用", "color": "green"},
217 | # {"name": "过期", "color": "red"},
218 | # ]
219 | # }
220 | # },
221 | "样式": {
222 | "select": {
223 | "options": [
224 | {"name": "callout", "color": "blue"},
225 | {"name": "quote", "color": "green"},
226 | {"name": "paragraph", "color": "purple"},
227 | {"name": "bulleted_list_item", "color": "yellow"},
228 | {"name": "numbered_list_item", "color": "pink"},
229 | ]
230 | }
231 | },
232 | "最后同步时间": {"date": {}},
233 | }
234 | parent = parent = {"page_id": self.page_id, "type": "page_id"}
235 | self.setting_database_id = self.client.databases.create(
236 | parent=parent,
237 | title=title,
238 | icon=get_icon("https://www.notion.so/icons/gear_gray.svg"),
239 | properties=properties,
240 | ).get("id")
241 |
242 | def insert_to_setting_database(self):
243 | existing_pages = self.query(database_id=self.setting_database_id, filter={"property": "标题", "title": {"equals": "设置"}}).get("results")
244 | properties = {
245 | "标题": {"title": [{"type": "text", "text": {"content": "设置"}}]},
246 | "最后同步时间": {"date": {"start": pendulum.now("Asia/Shanghai").isoformat()}},
247 | "NotinToken": {"rich_text": [{"type": "text", "text": {"content": os.getenv("NOTION_TOKEN")}}]},
248 | "NotinPage": {"rich_text": [{"type": "text", "text": {"content": os.getenv("NOTION_PAGE")}}]},
249 | "WeReadCookie": {"rich_text": [{"type": "text", "text": {"content": os.getenv("WEREAD_COOKIE")}}]},
250 | }
251 | if existing_pages:
252 | remote_properties = existing_pages[0].get("properties")
253 | self.show_color = get_property_value(remote_properties.get("根据划线颜色设置文字颜色"))
254 | self.sync_bookmark = get_property_value(remote_properties.get("同步书签"))
255 | self.block_type = get_property_value(remote_properties.get("样式"))
256 | page_id = existing_pages[0].get("id")
257 | self.client.pages.update(page_id=page_id, properties=properties)
258 | else:
259 | properties["根据划线颜色设置文字颜色"] = {"checkbox": True}
260 | properties["同步书签"] = {"checkbox": True}
261 | properties["样式"] = {"select": {"name": "callout"}}
262 | self.client.pages.create(
263 | parent={"database_id": self.setting_database_id},
264 | properties=properties,
265 | )
266 |
267 |
268 |
269 | def update_heatmap(self, block_id, url):
270 | # 更新 image block 的链接
271 | return self.client.blocks.update(block_id=block_id, embed={"url": url})
272 |
273 | def get_week_relation_id(self, date):
274 | year = date.isocalendar().year
275 | week = date.isocalendar().week
276 | week = f"{year}年第{week}周"
277 | start, end = get_first_and_last_day_of_week(date)
278 | properties = {"日期": get_date(format_date(start), format_date(end))}
279 | return self.get_relation_id(
280 | week, self.week_database_id, TARGET_ICON_URL, properties
281 | )
282 |
283 | def get_month_relation_id(self, date):
284 | month = date.strftime("%Y年%-m月")
285 | start, end = get_first_and_last_day_of_month(date)
286 | properties = {"日期": get_date(format_date(start), format_date(end))}
287 | return self.get_relation_id(
288 | month, self.month_database_id, TARGET_ICON_URL, properties
289 | )
290 |
291 | def get_year_relation_id(self, date):
292 | year = date.strftime("%Y")
293 | start, end = get_first_and_last_day_of_year(date)
294 | properties = {"日期": get_date(format_date(start), format_date(end))}
295 | return self.get_relation_id(
296 | year, self.year_database_id, TARGET_ICON_URL, properties
297 | )
298 |
299 | def get_day_relation_id(self, date):
300 | new_date = date.replace(hour=0, minute=0, second=0, microsecond=0)
301 | timestamp = (new_date - timedelta(hours=8)).timestamp()
302 | day = new_date.strftime("%Y年%m月%d日")
303 | properties = {
304 | "日期": get_date(format_date(date)),
305 | "时间戳": get_number(timestamp),
306 | }
307 | properties["年"] = get_relation(
308 | [
309 | self.get_year_relation_id(new_date),
310 | ]
311 | )
312 | properties["月"] = get_relation(
313 | [
314 | self.get_month_relation_id(new_date),
315 | ]
316 | )
317 | properties["周"] = get_relation(
318 | [
319 | self.get_week_relation_id(new_date),
320 | ]
321 | )
322 | return self.get_relation_id(
323 | day, self.day_database_id, TARGET_ICON_URL, properties
324 | )
325 |
326 | def get_relation_id(self, name, id, icon, properties={}):
327 | key = f"{id}{name}"
328 | if key in self.__cache:
329 | return self.__cache.get(key)
330 | filter = {"property": "标题", "title": {"equals": name}}
331 | response = self.client.databases.query(database_id=id, filter=filter)
332 | if len(response.get("results")) == 0:
333 | parent = {"database_id": id, "type": "database_id"}
334 | properties["标题"] = get_title(name)
335 | page_id = self.client.pages.create(
336 | parent=parent, properties=properties, icon=get_icon(icon)
337 | ).get("id")
338 | else:
339 | page_id = response.get("results")[0].get("id")
340 | self.__cache[key] = page_id
341 | return page_id
342 |
343 | def insert_bookmark(self, id, bookmark):
344 | icon = get_icon(BOOKMARK_ICON_URL)
345 | properties = {
346 | "Name": get_title(bookmark.get("markText", "")),
347 | "bookId": get_rich_text(bookmark.get("bookId")),
348 | "range": get_rich_text(bookmark.get("range")),
349 | "bookmarkId": get_rich_text(bookmark.get("bookmarkId")),
350 | "blockId": get_rich_text(bookmark.get("blockId")),
351 | "chapterUid": get_number(bookmark.get("chapterUid")),
352 | "bookVersion": get_number(bookmark.get("bookVersion")),
353 | "colorStyle": get_number(bookmark.get("colorStyle")),
354 | "type": get_number(bookmark.get("type")),
355 | "style": get_number(bookmark.get("style")),
356 | "书籍": get_relation([id]),
357 | }
358 | if "createTime" in bookmark:
359 | create_time = timestamp_to_date(int(bookmark.get("createTime")))
360 | properties["Date"] = get_date(create_time.strftime("%Y-%m-%d %H:%M:%S"))
361 | self.get_date_relation(properties, create_time)
362 | parent = {"database_id": self.bookmark_database_id, "type": "database_id"}
363 | self.create_page(parent, properties, icon)
364 |
365 | def insert_review(self, id, review):
366 | time.sleep(0.1)
367 | icon = get_icon(TAG_ICON_URL)
368 | properties = {
369 | "Name": get_title(review.get("content", "")),
370 | "bookId": get_rich_text(review.get("bookId")),
371 | "reviewId": get_rich_text(review.get("reviewId")),
372 | "blockId": get_rich_text(review.get("blockId")),
373 | "chapterUid": get_number(review.get("chapterUid")),
374 | "bookVersion": get_number(review.get("bookVersion")),
375 | "type": get_number(review.get("type")),
376 | "书籍": get_relation([id]),
377 | }
378 | if "range" in review:
379 | properties["range"] = get_rich_text(review.get("range"))
380 | if "star" in review:
381 | properties["star"] = get_number(review.get("star"))
382 | if "abstract" in review:
383 | properties["abstract"] = get_rich_text(review.get("abstract"))
384 | if "createTime" in review:
385 | create_time = timestamp_to_date(int(review.get("createTime")))
386 | properties["Date"] = get_date(create_time.strftime("%Y-%m-%d %H:%M:%S"))
387 | self.get_date_relation(properties, create_time)
388 | parent = {"database_id": self.review_database_id, "type": "database_id"}
389 | self.create_page(parent, properties, icon)
390 |
391 | def insert_chapter(self, id, chapter):
392 | time.sleep(0.1)
393 | icon = {"type": "external", "external": {"url": TAG_ICON_URL}}
394 | properties = {
395 | "Name": get_title(chapter.get("title")),
396 | "blockId": get_rich_text(chapter.get("blockId")),
397 | "chapterUid": {"number": chapter.get("chapterUid")},
398 | "chapterIdx": {"number": chapter.get("chapterIdx")},
399 | "readAhead": {"number": chapter.get("readAhead")},
400 | "updateTime": {"number": chapter.get("updateTime")},
401 | "level": {"number": chapter.get("level")},
402 | "书籍": {"relation": [{"id": id}]},
403 | }
404 | parent = {"database_id": self.chapter_database_id, "type": "database_id"}
405 | self.create_page(parent, properties, icon)
406 |
407 | @retry(stop_max_attempt_number=3, wait_fixed=5000)
408 | def update_book_page(self, page_id, properties):
409 | return self.client.pages.update(page_id=page_id, properties=properties)
410 |
411 | @retry(stop_max_attempt_number=3, wait_fixed=5000)
412 | def update_page(self, page_id, properties, cover):
413 | return self.client.pages.update(
414 | page_id=page_id, properties=properties, cover=cover
415 | )
416 |
417 |
418 | @retry(stop_max_attempt_number=3, wait_fixed=5000)
419 | def create_page(self, parent, properties, icon):
420 | return self.client.pages.create(parent=parent, properties=properties, icon=icon)
421 |
422 | @retry(stop_max_attempt_number=3, wait_fixed=5000)
423 | def create_book_page(self, parent, properties, icon):
424 | return self.client.pages.create(
425 | parent=parent, properties=properties, icon=icon, cover=icon
426 | )
427 |
428 | @retry(stop_max_attempt_number=3, wait_fixed=5000)
429 | def query(self, **kwargs):
430 | kwargs = {k: v for k, v in kwargs.items() if v}
431 | return self.client.databases.query(**kwargs)
432 |
433 | @retry(stop_max_attempt_number=3, wait_fixed=5000)
434 | def get_block_children(self, id):
435 | response = self.client.blocks.children.list(id)
436 | return response.get("results")
437 |
438 | @retry(stop_max_attempt_number=3, wait_fixed=5000)
439 | def append_blocks(self, block_id, children):
440 | return self.client.blocks.children.append(block_id=block_id, children=children)
441 |
442 | @retry(stop_max_attempt_number=3, wait_fixed=5000)
443 | def append_blocks_after(self, block_id, children, after):
444 | #奇怪不知道为什么会多插入一个children,没找到问题,先暂时这么解决,搜索是否有parent
445 | parent = self.client.blocks.retrieve(after).get("parent")
446 | if(parent.get("type")=="block_id"):
447 | after = parent.get("block_id")
448 | return self.client.blocks.children.append(
449 | block_id=block_id, children=children, after=after
450 | )
451 |
452 | @retry(stop_max_attempt_number=3, wait_fixed=5000)
453 | def delete_block(self, block_id):
454 | return self.client.blocks.delete(block_id=block_id)
455 |
456 | @retry(stop_max_attempt_number=3, wait_fixed=5000)
457 | def get_all_book(self):
458 | """从Notion中获取所有的书籍"""
459 | results = self.query_all(self.book_database_id)
460 | books_dict = {}
461 | for result in results:
462 | bookId = get_property_value(result.get("properties").get("BookId"))
463 | books_dict[bookId] = {
464 | "pageId": result.get("id"),
465 | "readingTime": get_property_value(
466 | result.get("properties").get("阅读时长")
467 | ),
468 | "category": get_property_value(
469 | result.get("properties").get("书架分类")
470 | ),
471 | "Sort": get_property_value(result.get("properties").get("Sort")),
472 | "douban_url": get_property_value(
473 | result.get("properties").get("豆瓣链接")
474 | ),
475 | "cover": result.get("cover"),
476 | "myRating": get_property_value(
477 | result.get("properties").get("我的评分")
478 | ),
479 | "comment": get_property_value(result.get("properties").get("豆瓣短评")),
480 | "status": get_property_value(result.get("properties").get("阅读状态")),
481 | }
482 | return books_dict
483 |
484 | @retry(stop_max_attempt_number=3, wait_fixed=5000)
485 | def query_all_by_book(self, database_id, filter):
486 | results = []
487 | has_more = True
488 | start_cursor = None
489 | while has_more:
490 | response = self.client.databases.query(
491 | database_id=database_id,
492 | filter=filter,
493 | start_cursor=start_cursor,
494 | page_size=100,
495 | )
496 | start_cursor = response.get("next_cursor")
497 | has_more = response.get("has_more")
498 | results.extend(response.get("results"))
499 | return results
500 |
501 | @retry(stop_max_attempt_number=3, wait_fixed=5000)
502 | def query_all(self, database_id):
503 | """获取database中所有的数据"""
504 | results = []
505 | has_more = True
506 | start_cursor = None
507 | while has_more:
508 | response = self.client.databases.query(
509 | database_id=database_id,
510 | start_cursor=start_cursor,
511 | page_size=100,
512 | )
513 | start_cursor = response.get("next_cursor")
514 | has_more = response.get("has_more")
515 | results.extend(response.get("results"))
516 | return results
517 |
518 | def get_date_relation(self, properties, date):
519 | properties["年"] = get_relation(
520 | [
521 | self.get_year_relation_id(date),
522 | ]
523 | )
524 | properties["月"] = get_relation(
525 | [
526 | self.get_month_relation_id(date),
527 | ]
528 | )
529 | properties["周"] = get_relation(
530 | [
531 | self.get_week_relation_id(date),
532 | ]
533 | )
534 | properties["日"] = get_relation(
535 | [
536 | self.get_day_relation_id(date),
537 | ]
538 | )
--------------------------------------------------------------------------------