├── .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 | ![扫码_搜索联合传播样式-标准色版](https://github.com/malinkang/weread2notion/assets/3365208/191900c6-958e-4f9b-908d-a40a54889b5e) 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 | ) --------------------------------------------------------------------------------