├── douban2notion ├── __init__.py ├── __main__.py ├── config.py ├── update_heatmap.py ├── utils.py ├── notion_helper.py └── douban.py ├── douban2notion.egg-info ├── dependency_links.txt ├── top_level.txt ├── entry_points.txt ├── requires.txt ├── SOURCES.txt └── PKG-INFO ├── .gitignore ├── dist ├── douban2notion-0.0.10.tar.gz ├── douban2notion-0.0.11.tar.gz ├── douban2notion-0.0.12.tar.gz ├── douban2notion-0.0.13.tar.gz ├── douban2notion-0.0.14.tar.gz ├── douban2notion-0.0.8.tar.gz ├── douban2notion-0.0.9.tar.gz ├── douban2notion-0.0.10-py3-none-any.whl ├── douban2notion-0.0.11-py3-none-any.whl ├── douban2notion-0.0.12-py3-none-any.whl ├── douban2notion-0.0.13-py3-none-any.whl ├── douban2notion-0.0.14-py3-none-any.whl ├── douban2notion-0.0.8-py3-none-any.whl └── douban2notion-0.0.9-py3-none-any.whl ├── requirements.txt ├── setup.py ├── README.md ├── .github └── workflows │ ├── douban_book.yml │ └── douban_movie.yml └── OUT_FOLDER ├── book └── 1766506028.svg └── movie └── 1766506353.svg /douban2notion/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /douban2notion.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | douban2notion/__pycache__/ 2 | .env 3 | a -------------------------------------------------------------------------------- /douban2notion.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | douban2notion 2 | -------------------------------------------------------------------------------- /douban2notion/__main__.py: -------------------------------------------------------------------------------- 1 | from book import main 2 | 3 | if __name__ == "__main__": 4 | main() 5 | -------------------------------------------------------------------------------- /dist/douban2notion-0.0.10.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malinkang/douban2notion/HEAD/dist/douban2notion-0.0.10.tar.gz -------------------------------------------------------------------------------- /dist/douban2notion-0.0.11.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malinkang/douban2notion/HEAD/dist/douban2notion-0.0.11.tar.gz -------------------------------------------------------------------------------- /dist/douban2notion-0.0.12.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malinkang/douban2notion/HEAD/dist/douban2notion-0.0.12.tar.gz -------------------------------------------------------------------------------- /dist/douban2notion-0.0.13.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malinkang/douban2notion/HEAD/dist/douban2notion-0.0.13.tar.gz -------------------------------------------------------------------------------- /dist/douban2notion-0.0.14.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malinkang/douban2notion/HEAD/dist/douban2notion-0.0.14.tar.gz -------------------------------------------------------------------------------- /dist/douban2notion-0.0.8.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malinkang/douban2notion/HEAD/dist/douban2notion-0.0.8.tar.gz -------------------------------------------------------------------------------- /dist/douban2notion-0.0.9.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malinkang/douban2notion/HEAD/dist/douban2notion-0.0.9.tar.gz -------------------------------------------------------------------------------- /dist/douban2notion-0.0.10-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malinkang/douban2notion/HEAD/dist/douban2notion-0.0.10-py3-none-any.whl -------------------------------------------------------------------------------- /dist/douban2notion-0.0.11-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malinkang/douban2notion/HEAD/dist/douban2notion-0.0.11-py3-none-any.whl -------------------------------------------------------------------------------- /dist/douban2notion-0.0.12-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malinkang/douban2notion/HEAD/dist/douban2notion-0.0.12-py3-none-any.whl -------------------------------------------------------------------------------- /dist/douban2notion-0.0.13-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malinkang/douban2notion/HEAD/dist/douban2notion-0.0.13-py3-none-any.whl -------------------------------------------------------------------------------- /dist/douban2notion-0.0.14-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malinkang/douban2notion/HEAD/dist/douban2notion-0.0.14-py3-none-any.whl -------------------------------------------------------------------------------- /dist/douban2notion-0.0.8-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malinkang/douban2notion/HEAD/dist/douban2notion-0.0.8-py3-none-any.whl -------------------------------------------------------------------------------- /dist/douban2notion-0.0.9-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/malinkang/douban2notion/HEAD/dist/douban2notion-0.0.9-py3-none-any.whl -------------------------------------------------------------------------------- /douban2notion.egg-info/entry_points.txt: -------------------------------------------------------------------------------- 1 | [console_scripts] 2 | douban = douban2notion.douban:main 3 | heatmap = douban2notion.update_heatmap:main 4 | -------------------------------------------------------------------------------- /douban2notion.egg-info/requires.txt: -------------------------------------------------------------------------------- 1 | requests 2 | pendulum 3 | retrying 4 | notion-client 5 | github-heatmap 6 | python-dotenv 7 | beautifulsoup4 8 | lxml 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.32.5 2 | notion-client==2.5.0 3 | github-heatmap==1.3.8 4 | retrying==1.4.2 5 | pendulum==3.1.0 6 | python-dotenv==1.2.1 7 | douban2notion -------------------------------------------------------------------------------- /douban2notion.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | README.md 2 | setup.py 3 | douban2notion/__init__.py 4 | douban2notion/__main__.py 5 | douban2notion/config.py 6 | douban2notion/douban.py 7 | douban2notion/notion_helper.py 8 | douban2notion/update_heatmap.py 9 | douban2notion/utils.py 10 | douban2notion.egg-info/PKG-INFO 11 | douban2notion.egg-info/SOURCES.txt 12 | douban2notion.egg-info/dependency_links.txt 13 | douban2notion.egg-info/entry_points.txt 14 | douban2notion.egg-info/requires.txt 15 | douban2notion.egg-info/top_level.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | version="0.0.14", 5 | packages=find_packages(), 6 | install_requires=[ 7 | "requests", 8 | "pendulum", 9 | "retrying", 10 | "notion-client", 11 | "github-heatmap", 12 | "python-dotenv", 13 | "beautifulsoup4", 14 | "lxml", 15 | ], 16 | entry_points={ 17 | "console_scripts": [ 18 | "douban = douban2notion.douban:main", 19 | "heatmap = douban2notion.update_heatmap:main", 20 | ], 21 | }, 22 | author="malinkang", 23 | author_email="linkang.ma@gmail.com", 24 | description="自动将豆瓣电影和豆瓣读书同步到Notion", 25 | long_description=open("README.md").read(), 26 | long_description_content_type="text/markdown", 27 | url="https://github.com/malinkang/weread2notion-pro", 28 | classifiers=[ 29 | "Programming Language :: Python :: 3", 30 | "License :: OSI Approved :: MIT License", 31 | "Operating System :: OS Independent", 32 | ], 33 | python_requires=">=3.6", 34 | ) 35 | -------------------------------------------------------------------------------- /douban2notion/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 | MULTI_SELECT = "multi_select" 12 | 13 | book_properties_type_dict = { 14 | "书名":TITLE, 15 | "短评":RICH_TEXT, 16 | "ISBN":RICH_TEXT, 17 | "豆瓣链接":URL, 18 | "作者":RELATION, 19 | "评分":SELECT, 20 | "封面":FILES, 21 | "分类":RELATION, 22 | "状态":STATUS, 23 | "日期":DATE, 24 | "简介":RICH_TEXT, 25 | "豆瓣链接":URL, 26 | "出版社":MULTI_SELECT, 27 | } 28 | 29 | TAG_ICON_URL = "https://www.notion.so/icons/tag_gray.svg" 30 | USER_ICON_URL = "https://www.notion.so/icons/user-circle-filled_gray.svg" 31 | BOOK_ICON_URL = "https://www.notion.so/icons/book_gray.svg" 32 | 33 | 34 | movie_properties_type_dict = { 35 | "电影名":TITLE, 36 | "短评":RICH_TEXT, 37 | # "ISBN":RICH_TEXT, 38 | # "链接":URL, 39 | "导演":RELATION, 40 | "演员":RELATION, 41 | # "Sort":NUMBER, 42 | "封面":FILES, 43 | "分类":RELATION, 44 | "状态":STATUS, 45 | "类型":SELECT, 46 | "评分":SELECT, 47 | # "阅读时长":NUMBER, 48 | # "阅读进度":NUMBER, 49 | # "阅读天数":NUMBER, 50 | "日期":DATE, 51 | "简介":RICH_TEXT, 52 | "IMDB":RICH_TEXT, 53 | # "开始阅读时间":DATE, 54 | # "最后阅读时间":DATE, 55 | # "简介":RICH_TEXT, 56 | # "书架分类":SELECT, 57 | # "我的评分":SELECT, 58 | "豆瓣链接":URL, 59 | } 60 | -------------------------------------------------------------------------------- /douban2notion/update_heatmap.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import glob 3 | import os 4 | import shutil 5 | import time 6 | from douban2notion.notion_helper import NotionHelper 7 | 8 | def move_and_rename_file(type): 9 | 10 | # 确保目标目录存在 11 | source_path = os.path.join("./OUT_FOLDER", 'notion.svg') 12 | target_dir = os.path.join("./OUT_FOLDER", type) 13 | os.makedirs(target_dir, exist_ok=True) 14 | 15 | # 生成时间戳命名的文件名 16 | timestamp = int(time.time()) 17 | new_filename = f"{timestamp}.svg" 18 | target_path = os.path.join(target_dir, new_filename) 19 | 20 | # 移动并重命名文件 21 | shutil.move(source_path, target_path) 22 | # 返回移动后的文件路径 23 | return target_path 24 | 25 | def main(): 26 | parser = argparse.ArgumentParser() 27 | parser.add_argument("type") 28 | options = parser.parse_args() 29 | type = options.type 30 | notion_helper = NotionHelper(type) 31 | image_file = move_and_rename_file(type) 32 | if image_file: 33 | image_url = f"https://raw.githubusercontent.com/{os.getenv('REPOSITORY')}/{os.getenv('REF').split('/')[-1]}/{image_file[2:]}" 34 | heatmap_url = f"https://heatmap.malinkang.com/?image={image_url}" 35 | if notion_helper.heatmap_block_id: 36 | response = notion_helper.update_heatmap( 37 | block_id=notion_helper.heatmap_block_id, url=heatmap_url 38 | ) 39 | if __name__ == "__main__": 40 | main() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 将豆瓣电影和读书同步到Notion 2 | 3 | 4 | 本项目通过Github Action每天定时同步豆瓣电影和读书到Notion。 5 | 6 | * 豆瓣电影预览效果:https://douban-movie.malinkang.com/ 7 | * 豆瓣图书预览效果:https://douban-book.malinkang.com/ 8 | 9 | 10 | ## 使用 11 | 12 | > [!IMPORTANT] 13 | > 关注公众号回复豆瓣获取教程,后续有更新也会第一时间在公众号里同步。 14 | 15 | ![扫码_搜索联合传播样式-标准色版](https://github.com/malinkang/weread2notion/assets/3365208/191900c6-958e-4f9b-908d-a40a54889b5e) 16 | 17 | 18 | ## 群 19 | > [!IMPORTANT] 20 | > 欢迎加入群讨论。可以讨论使用中遇到的任何问题,也可以讨论Notion使用,后续我也会在群中分享更多Notion自动化工具。微信群失效的话可以添加我的微信malinkang,我拉你入群。 21 | 22 | | 微信群 | QQ群 | 23 | | --- | --- | 24 | |
|
| 25 | 26 | 27 | ## 捐赠 28 | 29 | 如果你觉得本项目帮助了你,请作者喝一杯咖啡,你的支持是作者最大的动力。本项目会持续更新。 30 | 31 | | 支付宝支付 | 微信支付 | 32 | | --- | --- | 33 | |
|
| 34 | 35 | ## 其他项目 36 | * [WeRead2Notion-Pro](https://github.com/malinkang/weread2notion-pro) 37 | * [WeRead2Notion](https://github.com/malinkang/weread2notion) 38 | * [Podcast2Notion](https://github.com/malinkang/podcast2notion) 39 | * [Douban2Notion](https://github.com/malinkang/douban2notion) 40 | * [Keep2Notion](https://github.com/malinkang/keep2notion) 41 | -------------------------------------------------------------------------------- /douban2notion.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.1 2 | Name: douban2notion 3 | Version: 0.0.14 4 | Summary: 自动将豆瓣电影和豆瓣读书同步到Notion 5 | Home-page: https://github.com/malinkang/weread2notion-pro 6 | Author: malinkang 7 | Author-email: linkang.ma@gmail.com 8 | Classifier: Programming Language :: Python :: 3 9 | Classifier: License :: OSI Approved :: MIT License 10 | Classifier: Operating System :: OS Independent 11 | Requires-Python: >=3.6 12 | Description-Content-Type: text/markdown 13 | Requires-Dist: requests 14 | Requires-Dist: pendulum 15 | Requires-Dist: retrying 16 | Requires-Dist: notion-client 17 | Requires-Dist: github-heatmap 18 | Requires-Dist: python-dotenv 19 | Requires-Dist: beautifulsoup4 20 | Requires-Dist: lxml 21 | 22 | # 将豆瓣电影和读书同步到Notion 23 | 24 | 25 | 本项目通过Github Action每天定时同步豆瓣电影和读书到Notion。 26 | 27 | * 豆瓣电影预览效果:https://douban-movie.malinkang.com/ 28 | * 豆瓣图书预览效果:https://douban-book.malinkang.com/ 29 | 30 | 31 | ## 使用 32 | 33 | > [!IMPORTANT] 34 | > 关注公众号回复豆瓣获取教程,后续有更新也会第一时间在公众号里同步。 35 | 36 | ![扫码_搜索联合传播样式-标准色版](https://github.com/malinkang/weread2notion/assets/3365208/191900c6-958e-4f9b-908d-a40a54889b5e) 37 | 38 | 39 | ## 群 40 | > [!IMPORTANT] 41 | > 欢迎加入群讨论。可以讨论使用中遇到的任何问题,也可以讨论Notion使用,后续我也会在群中分享更多Notion自动化工具。微信群失效的话可以添加我的微信malinkang,我拉你入群。 42 | 43 | | 微信群 | QQ群 | 44 | | --- | --- | 45 | |
|
| 46 | 47 | 48 | ## 捐赠 49 | 50 | 如果你觉得本项目帮助了你,请作者喝一杯咖啡,你的支持是作者最大的动力。本项目会持续更新。 51 | 52 | | 支付宝支付 | 微信支付 | 53 | | --- | --- | 54 | |
|
| 55 | 56 | ## 其他项目 57 | * [WeRead2Notion-Pro](https://github.com/malinkang/weread2notion-pro) 58 | * [WeRead2Notion](https://github.com/malinkang/weread2notion) 59 | * [Podcast2Notion](https://github.com/malinkang/podcast2notion) 60 | * [Douban2Notion](https://github.com/malinkang/douban2notion) 61 | * [Keep2Notion](https://github.com/malinkang/keep2notion) 62 | -------------------------------------------------------------------------------- /.github/workflows/douban_book.yml: -------------------------------------------------------------------------------- 1 | name: douban book sync 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 16 * * *" 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 | BOOK_NOTION_TOKEN: ${{ secrets.BOOK_NOTION_TOKEN }} 17 | NOTION_BOOK_URL: ${{ secrets.NOTION_BOOK_URL }} 18 | DOUBAN_NAME: ${{ secrets.DOUBAN_NAME }} 19 | AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }} 20 | YEAR: ${{ vars.YEAR }} 21 | REF: ${{ github.ref }} 22 | REPOSITORY: ${{ github.repository }} 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | 27 | - name: Set up Python 28 | uses: actions/setup-python@v4 29 | with: 30 | python-version: 3.9 31 | - name: Install dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip install -r requirements.txt 35 | - name: douban book sync 36 | run: | 37 | douban "book" 38 | - name: Remove folder 39 | run: rm -rf ./OUT_FOLDER/book 40 | - name: Set default year if not provided 41 | run: echo "YEAR=$(date +"%Y")" >> $GITHUB_ENV 42 | if: env.YEAR == '' 43 | - name: notion heatmap 44 | run: | 45 | github_heatmap notion --notion_token "${{ secrets.BOOK_NOTION_TOKEN || secrets.NOTION_TOKEN }}" --database_id "${{ env.DATABASE_ID }}" --date_prop_name "日期" --value_prop_name "读过" --unit "本" --year $YEAR --me "${{secrets.BOOK_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'}} 46 | - name: udpate heatmap 47 | run: | 48 | heatmap "book" 49 | - name: push 50 | run: | 51 | git config --local user.email "action@github.com" 52 | git config --local user.name "GitHub Action" 53 | git add . 54 | git commit -m 'add new heatmap' || echo "nothing to commit" 55 | git pull --rebase origin main 56 | git push || echo "nothing to push" 57 | -------------------------------------------------------------------------------- /.github/workflows/douban_movie.yml: -------------------------------------------------------------------------------- 1 | name: douban movie sync 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 16 * * *" 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 | MOVIE_NOTION_TOKEN: ${{ secrets.MOVIE_NOTION_TOKEN }} 17 | NOTION_MOVIE_URL: ${{ secrets.NOTION_MOVIE_URL }} 18 | DOUBAN_NAME: ${{ secrets.DOUBAN_NAME }} 19 | AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }} 20 | YEAR: ${{ vars.YEAR }} 21 | REF: ${{ github.ref }} 22 | REPOSITORY: ${{ github.repository }} 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | 27 | - name: Set up Python 28 | uses: actions/setup-python@v4 29 | with: 30 | python-version: 3.9 31 | - name: Install dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip install -r requirements.txt 35 | - name: douban movie sync 36 | run: | 37 | douban "movie" 38 | - name: Remove folder 39 | run: rm -rf ./OUT_FOLDER/movie 40 | - name: Set default year if not provided 41 | run: echo "YEAR=$(date +"%Y")" >> $GITHUB_ENV 42 | if: env.YEAR == '' 43 | - name: notion heatmap 44 | run: | 45 | github_heatmap notion --notion_token "${{secrets.MOVIE_NOTION_TOKEN || secrets.NOTION_TOKEN}}" --database_id "${{ env.DATABASE_ID }}" --date_prop_name "日期" --value_prop_name "看过" --unit "部" --year $YEAR --me "${{secrets.MOVIE_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'}} 46 | - name: udpate heatmap 47 | run: | 48 | heatmap "movie" 49 | - name: push 50 | run: | 51 | git config --local user.email "action@github.com" 52 | git config --local user.name "GitHub Action" 53 | git add . 54 | git commit -m 'add new heatmap' || echo "nothing to commit" 55 | git pull --rebase origin main 56 | git push || echo "nothing to push" 57 | -------------------------------------------------------------------------------- /douban2notion/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 douban2notion.config import ( 10 | RICH_TEXT, 11 | URL, 12 | RELATION, 13 | NUMBER, 14 | DATE, 15 | FILES, 16 | STATUS, 17 | TITLE, 18 | SELECT, 19 | MULTI_SELECT 20 | ) 21 | import pendulum 22 | 23 | MAX_LENGTH = ( 24 | 1024 # NOTION 2000个字符限制https://developers.notion.com/reference/request-limits 25 | ) 26 | 27 | tz = "Asia/Shanghai" 28 | def get_heading(level, content): 29 | if level == 1: 30 | heading = "heading_1" 31 | elif level == 2: 32 | heading = "heading_2" 33 | else: 34 | heading = "heading_3" 35 | return { 36 | "type": heading, 37 | heading: { 38 | "rich_text": [ 39 | { 40 | "type": "text", 41 | "text": { 42 | "content": content[:MAX_LENGTH], 43 | }, 44 | } 45 | ], 46 | "color": "default", 47 | "is_toggleable": False, 48 | }, 49 | } 50 | 51 | 52 | def get_table_of_contents(): 53 | """获取目录""" 54 | return {"type": "table_of_contents", "table_of_contents": {"color": "default"}} 55 | 56 | 57 | def get_title(content): 58 | return {"title": [{"type": "text", "text": {"content": content[:MAX_LENGTH]}}]} 59 | 60 | 61 | def get_rich_text(content): 62 | return {"rich_text": [{"type": "text", "text": {"content": content[:MAX_LENGTH]}}]} 63 | 64 | 65 | def get_url(url): 66 | return {"url": url} 67 | 68 | 69 | def get_file(url): 70 | return {"files": [{"type": "external", "name": "Cover", "external": {"url": url}}]} 71 | 72 | 73 | def get_multi_select(names): 74 | return {"multi_select": [{"name": name} for name in names]} 75 | 76 | 77 | def get_relation(ids): 78 | return {"relation": [{"id": id} for id in ids]} 79 | 80 | 81 | def get_date(start, end=None): 82 | return { 83 | "date": { 84 | "start": start, 85 | "end": end, 86 | "time_zone": "Asia/Shanghai", 87 | } 88 | } 89 | 90 | 91 | def get_icon(url): 92 | return {"type": "external", "external": {"url": url}} 93 | 94 | 95 | def get_select(name): 96 | return {"select": {"name": name}} 97 | 98 | 99 | def get_number(number): 100 | return {"number": number} 101 | 102 | 103 | def get_quote(content): 104 | return { 105 | "type": "quote", 106 | "quote": { 107 | "rich_text": [ 108 | { 109 | "type": "text", 110 | "text": {"content": content[:MAX_LENGTH]}, 111 | } 112 | ], 113 | "color": "default", 114 | }, 115 | } 116 | 117 | 118 | def get_callout(content, style, colorStyle, reviewId): 119 | # 根据不同的划线样式设置不同的emoji 直线type=0 背景颜色是1 波浪线是2 120 | emoji = "〰️" 121 | if style == 0: 122 | emoji = "💡" 123 | elif style == 1: 124 | emoji = "⭐" 125 | # 如果reviewId不是空说明是笔记 126 | if reviewId != None: 127 | emoji = "✍️" 128 | color = "default" 129 | # 根据划线颜色设置文字的颜色 130 | if colorStyle == 1: 131 | color = "red" 132 | elif colorStyle == 2: 133 | color = "purple" 134 | elif colorStyle == 3: 135 | color = "blue" 136 | elif colorStyle == 4: 137 | color = "green" 138 | elif colorStyle == 5: 139 | color = "yellow" 140 | return { 141 | "type": "callout", 142 | "callout": { 143 | "rich_text": [ 144 | { 145 | "type": "text", 146 | "text": { 147 | "content": content[:MAX_LENGTH], 148 | }, 149 | } 150 | ], 151 | "icon": {"emoji": emoji}, 152 | "color": color, 153 | }, 154 | } 155 | 156 | 157 | def get_rich_text_from_result(result, name): 158 | return result.get("properties").get(name).get("rich_text")[0].get("plain_text") 159 | 160 | 161 | def get_number_from_result(result, name): 162 | return result.get("properties").get(name).get("number") 163 | 164 | 165 | def format_time(time): 166 | """将秒格式化为 xx时xx分格式""" 167 | result = "" 168 | hour = time // 3600 169 | if hour > 0: 170 | result += f"{hour}时" 171 | minutes = time % 3600 // 60 172 | if minutes > 0: 173 | result += f"{minutes}分" 174 | return result 175 | 176 | 177 | def format_date(date, format="%Y-%m-%d %H:%M:%S"): 178 | return date.strftime(format) 179 | 180 | 181 | def timestamp_to_date(timestamp): 182 | """时间戳转化为date""" 183 | return datetime.utcfromtimestamp(timestamp) + timedelta(hours=8) 184 | 185 | 186 | def get_first_and_last_day_of_month(date): 187 | # 获取给定日期所在月的第一天 188 | first_day = date.replace(day=1, hour=0, minute=0, second=0, microsecond=0) 189 | 190 | # 获取给定日期所在月的最后一天 191 | _, last_day_of_month = calendar.monthrange(date.year, date.month) 192 | last_day = date.replace( 193 | day=last_day_of_month, hour=0, minute=0, second=0, microsecond=0 194 | ) 195 | 196 | return first_day, last_day 197 | 198 | 199 | def get_first_and_last_day_of_year(date): 200 | # 获取给定日期所在年的第一天 201 | first_day = date.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0) 202 | 203 | # 获取给定日期所在年的最后一天 204 | last_day = date.replace(month=12, day=31, hour=0, minute=0, second=0, microsecond=0) 205 | 206 | return first_day, last_day 207 | 208 | 209 | def get_first_and_last_day_of_week(date): 210 | # 获取给定日期所在周的第一天(星期一) 211 | first_day_of_week = (date - timedelta(days=date.weekday())).replace( 212 | hour=0, minute=0, second=0, microsecond=0 213 | ) 214 | 215 | # 获取给定日期所在周的最后一天(星期日) 216 | last_day_of_week = first_day_of_week + timedelta(days=6) 217 | 218 | return first_day_of_week, last_day_of_week 219 | 220 | 221 | def get_properties(dict1, dict2): 222 | properties = {} 223 | for key, value in dict1.items(): 224 | type = dict2.get(key) 225 | if value == None: 226 | continue 227 | property = None 228 | if type == TITLE: 229 | property = { 230 | "title": [ 231 | {"type": "text", "text": {"content": value[:MAX_LENGTH]}} 232 | ] 233 | } 234 | elif type == RICH_TEXT: 235 | property = { 236 | "rich_text": [ 237 | {"type": "text", "text": {"content": value[:MAX_LENGTH]}} 238 | ] 239 | } 240 | elif type == NUMBER: 241 | property = {"number": value} 242 | elif type == STATUS: 243 | property = {"status": {"name": value}} 244 | elif type == FILES: 245 | property = {"files": [{"type": "external", "name": "Cover", "external": {"url": value}}]} 246 | elif type == DATE: 247 | property = { 248 | "date": { 249 | "start": pendulum.from_timestamp( 250 | value, tz="Asia/Shanghai" 251 | ).to_datetime_string(), 252 | "time_zone": "Asia/Shanghai", 253 | } 254 | } 255 | elif type==URL: 256 | property = {"url": value} 257 | elif type==SELECT: 258 | property = {"select": {"name": value}} 259 | elif type==MULTI_SELECT: 260 | property = {"multi_select": [{"name": name} for name in value]} 261 | elif type == RELATION: 262 | property = {"relation": [{"id": id} for id in value]} 263 | if property: 264 | properties[key] = property 265 | return properties 266 | 267 | 268 | def get_property_value(property): 269 | """从Property中获取值""" 270 | type = property.get("type") 271 | content = property.get(type) 272 | if content is None: 273 | return None 274 | if type == "title" or type == "rich_text": 275 | if(len(content)>0): 276 | return content[0].get("plain_text") 277 | else: 278 | return None 279 | elif type == "status" or type == "select": 280 | return content.get("name") 281 | elif type == "files": 282 | # 不考虑多文件情况 283 | if len(content) > 0 and content[0].get("type") == "external": 284 | return content[0].get("external").get("url") 285 | else: 286 | return None 287 | elif type == "date": 288 | return str_to_timestamp(content.get("start")) 289 | else: 290 | return content 291 | 292 | 293 | def calculate_book_str_id(book_id): 294 | md5 = hashlib.md5() 295 | md5.update(book_id.encode("utf-8")) 296 | digest = md5.hexdigest() 297 | result = digest[0:3] 298 | code, transformed_ids = transform_id(book_id) 299 | result += code + "2" + digest[-2:] 300 | 301 | for i in range(len(transformed_ids)): 302 | hex_length_str = format(len(transformed_ids[i]), "x") 303 | if len(hex_length_str) == 1: 304 | hex_length_str = "0" + hex_length_str 305 | 306 | result += hex_length_str + transformed_ids[i] 307 | 308 | if i < len(transformed_ids) - 1: 309 | result += "g" 310 | 311 | if len(result) < 20: 312 | result += digest[0 : 20 - len(result)] 313 | md5 = hashlib.md5() 314 | md5.update(result.encode("utf-8")) 315 | result += md5.hexdigest()[0:3] 316 | return result 317 | 318 | def transform_id(book_id): 319 | id_length = len(book_id) 320 | if re.match("^\d*$", book_id): 321 | ary = [] 322 | for i in range(0, id_length, 9): 323 | ary.append(format(int(book_id[i : min(i + 9, id_length)]), "x")) 324 | return "3", ary 325 | 326 | result = "" 327 | for i in range(id_length): 328 | result += format(ord(book_id[i]), "x") 329 | return "4", [result] 330 | 331 | def get_weread_url(book_id): 332 | return f"https://weread.qq.com/web/reader/{calculate_book_str_id(book_id)}" 333 | 334 | def str_to_timestamp(date): 335 | if date == None: 336 | return 0 337 | dt = pendulum.parse(date) 338 | # 获取时间戳 339 | return int(dt.timestamp()) 340 | 341 | upload_url = 'https://wereadassets.malinkang.com/' 342 | 343 | 344 | def upload_image(folder_path, filename,file_path): 345 | # 将文件内容编码为Base64 346 | with open(file_path, 'rb') as file: 347 | content_base64 = base64.b64encode(file.read()).decode('utf-8') 348 | 349 | # 构建请求的JSON数据 350 | data = { 351 | 'file': content_base64, 352 | 'filename': filename, 353 | 'folder': folder_path 354 | } 355 | 356 | response = requests.post(upload_url, json=data) 357 | 358 | if response.status_code == 200: 359 | print('File uploaded successfully.') 360 | return response.text 361 | else: 362 | return None 363 | 364 | def url_to_md5(url): 365 | # 创建一个md5哈希对象 366 | md5_hash = hashlib.md5() 367 | 368 | # 对URL进行编码,准备进行哈希处理 369 | # 默认使用utf-8编码 370 | encoded_url = url.encode('utf-8') 371 | 372 | # 更新哈希对象的状态 373 | md5_hash.update(encoded_url) 374 | 375 | # 获取十六进制的哈希表示 376 | hex_digest = md5_hash.hexdigest() 377 | 378 | return hex_digest 379 | 380 | def download_image(url, save_dir="cover"): 381 | # 确保目录存在,如果不存在则创建 382 | if not os.path.exists(save_dir): 383 | os.makedirs(save_dir) 384 | 385 | file_name = url_to_md5(url) + ".jpg" 386 | save_path = os.path.join(save_dir, file_name) 387 | 388 | # 检查文件是否已经存在,如果存在则不进行下载 389 | if os.path.exists(save_path): 390 | print(f"File {file_name} already exists. Skipping download.") 391 | return save_path 392 | 393 | response = requests.get(url, stream=True) 394 | if response.status_code == 200: 395 | with open(save_path, "wb") as file: 396 | for chunk in response.iter_content(chunk_size=128): 397 | file.write(chunk) 398 | print(f"Image downloaded successfully to {save_path}") 399 | else: 400 | print(f"Failed to download image. Status code: {response.status_code}") 401 | return save_path 402 | 403 | def upload_cover(url): 404 | cover_file = download_image(url) 405 | return upload_image("cover",f"{cover_file.split('/')[-1]}",cover_file) 406 | 407 | def get_embed(url): 408 | return {"type": "embed", "embed": {"url": url}} 409 | -------------------------------------------------------------------------------- /douban2notion/notion_helper.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import re 4 | 5 | from notion_client import Client 6 | from retrying import retry 7 | 8 | from douban2notion.utils import ( 9 | format_date, 10 | get_date, 11 | get_first_and_last_day_of_month, 12 | get_first_and_last_day_of_week, 13 | get_first_and_last_day_of_year, 14 | get_icon, 15 | get_relation, 16 | get_title, 17 | ) 18 | 19 | TAG_ICON_URL = "https://www.notion.so/icons/tag_gray.svg" 20 | USER_ICON_URL = "https://www.notion.so/icons/user-circle-filled_gray.svg" 21 | TARGET_ICON_URL = "https://www.notion.so/icons/target_red.svg" 22 | BOOKMARK_ICON_URL = "https://www.notion.so/icons/bookmark_gray.svg" 23 | 24 | 25 | class NotionHelper: 26 | database_name_dict = { 27 | "MOVIE_DATABASE_NAME": "电影", 28 | "BOOK_DATABASE_NAME": "书架", 29 | "DAY_DATABASE_NAME": "日", 30 | "WEEK_DATABASE_NAME": "周", 31 | "MONTH_DATABASE_NAME": "月", 32 | "YEAR_DATABASE_NAME": "年", 33 | "CATEGORY_DATABASE_NAME": "分类", 34 | "DIRECTOR_DATABASE_NAME": "导演", 35 | "ACTOR_DATABASE_NAME": "演员", 36 | "AUTHOR_DATABASE_NAME": "作者", 37 | } 38 | database_id_dict = {} 39 | image_dict = {} 40 | def __init__(self,type): 41 | is_movie = True if type=="movie" else False 42 | page_url = os.getenv("NOTION_MOVIE_URL") if is_movie else os.getenv("NOTION_BOOK_URL") 43 | notion_token = os.getenv("NOTION_TOKEN") 44 | if not notion_token: 45 | if is_movie: 46 | notion_token = os.getenv("MOVIE_NOTION_TOKEN") 47 | else: 48 | notion_token = os.getenv("BOOK_NOTION_TOKEN") 49 | self.client = Client(auth=notion_token, log_level=logging.ERROR) 50 | self.__cache = {} 51 | self.page_id = self.extract_page_id(page_url) 52 | self.search_database(self.page_id) 53 | for key in self.database_name_dict.keys(): 54 | if os.getenv(key) != None and os.getenv(key) != "": 55 | self.database_name_dict[key] = os.getenv(key) 56 | self.book_database_id = self.database_id_dict.get( 57 | self.database_name_dict.get("BOOK_DATABASE_NAME") 58 | ) 59 | self.movie_database_id = self.database_id_dict.get( 60 | self.database_name_dict.get("MOVIE_DATABASE_NAME") 61 | ) 62 | self.day_database_id = self.database_id_dict.get( 63 | self.database_name_dict.get("DAY_DATABASE_NAME") 64 | ) 65 | self.week_database_id = self.database_id_dict.get( 66 | self.database_name_dict.get("WEEK_DATABASE_NAME") 67 | ) 68 | self.month_database_id = self.database_id_dict.get( 69 | self.database_name_dict.get("MONTH_DATABASE_NAME") 70 | ) 71 | self.year_database_id = self.database_id_dict.get( 72 | self.database_name_dict.get("YEAR_DATABASE_NAME") 73 | ) 74 | self.category_database_id = self.database_id_dict.get( 75 | self.database_name_dict.get("CATEGORY_DATABASE_NAME") 76 | ) 77 | self.director_database_id = self.database_id_dict.get( 78 | self.database_name_dict.get("DIRECTOR_DATABASE_NAME") 79 | ) 80 | self.author_database_id = self.database_id_dict.get( 81 | self.database_name_dict.get("AUTHOR_DATABASE_NAME") 82 | ) 83 | self.actor_database_id = self.database_id_dict.get( 84 | self.database_name_dict.get("ACTOR_DATABASE_NAME") 85 | ) 86 | if self.day_database_id: 87 | self.write_database_id(self.day_database_id) 88 | if is_movie: 89 | self.update_movie_database() 90 | 91 | def write_database_id(self, database_id): 92 | env_file = os.getenv('GITHUB_ENV') 93 | # 将值写入环境文件 94 | with open(env_file, "a") as file: 95 | file.write(f"DATABASE_ID={database_id}\n") 96 | def extract_page_id(self, notion_url): 97 | # 正则表达式匹配 32 个字符的 Notion page_id 98 | match = re.search( 99 | 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})", 100 | notion_url, 101 | ) 102 | if match: 103 | return match.group(0) 104 | else: 105 | raise Exception(f"获取NotionID失败,请检查输入的Url是否正确") 106 | 107 | @retry(stop_max_attempt_number=3, wait_fixed=5000) 108 | def search_database(self, block_id): 109 | children = self.client.blocks.children.list(block_id=block_id)["results"] 110 | # 遍历子块 111 | for child in children: 112 | # 检查子块的类型 113 | 114 | if child["type"] == "child_database": 115 | self.database_id_dict[ 116 | child.get("child_database").get("title") 117 | ] = child.get("id") 118 | elif child["type"] == "embed" and child.get("embed").get("url"): 119 | if child.get("embed").get("url").startswith("https://heatmap.malinkang.com/"): 120 | self.heatmap_block_id = child.get("id") 121 | # 如果子块有子块,递归调用函数 122 | if "has_children" in child and child["has_children"]: 123 | self.search_database(child["id"]) 124 | 125 | @retry(stop_max_attempt_number=3, wait_fixed=5000) 126 | def update_heatmap(self, block_id, url): 127 | # 更新 image block 的链接 128 | return self.client.blocks.update(block_id=block_id, embed={"url": url}) 129 | 130 | def get_week_relation_id(self, date): 131 | year = date.isocalendar().year 132 | week = date.isocalendar().week 133 | week = f"{year}年第{week}周" 134 | start, end = get_first_and_last_day_of_week(date) 135 | properties = {"日期": get_date(format_date(start), format_date(end))} 136 | return self.get_relation_id( 137 | week, self.week_database_id, TARGET_ICON_URL, properties 138 | ) 139 | 140 | def get_month_relation_id(self, date): 141 | month = date.strftime("%Y年%-m月") 142 | start, end = get_first_and_last_day_of_month(date) 143 | properties = {"日期": get_date(format_date(start), format_date(end))} 144 | return self.get_relation_id( 145 | month, self.month_database_id, TARGET_ICON_URL, properties 146 | ) 147 | 148 | def get_year_relation_id(self, date): 149 | year = date.strftime("%Y") 150 | start, end = get_first_and_last_day_of_year(date) 151 | properties = {"日期": get_date(format_date(start), format_date(end))} 152 | return self.get_relation_id( 153 | year, self.year_database_id, TARGET_ICON_URL, properties 154 | ) 155 | 156 | def get_day_relation_id(self, date): 157 | new_date = date.replace(hour=0, minute=0, second=0, microsecond=0) 158 | day = new_date.strftime("%Y年%m月%d日") 159 | properties = { 160 | "日期": get_date(format_date(date)), 161 | } 162 | properties["年"] = get_relation( 163 | [ 164 | self.get_year_relation_id(new_date), 165 | ] 166 | ) 167 | properties["月"] = get_relation( 168 | [ 169 | self.get_month_relation_id(new_date), 170 | ] 171 | ) 172 | properties["周"] = get_relation( 173 | [ 174 | self.get_week_relation_id(new_date), 175 | ] 176 | ) 177 | return self.get_relation_id( 178 | day, self.day_database_id, TARGET_ICON_URL, properties 179 | ) 180 | 181 | def update_movie_database(self): 182 | """更新数据库""" 183 | response = self.client.databases.retrieve(database_id=self.movie_database_id) 184 | id = response.get("id") 185 | properties = response.get("properties") 186 | update_properties = {} 187 | if ( 188 | properties.get("演员") is None 189 | or properties.get("演员").get("type") != "relation" 190 | ): 191 | update_properties["演员"] = {"relation": {"database_id": self.actor_database_id,"dual_property":{}}} 192 | if ( 193 | properties.get("IMDB") is None 194 | or properties.get("IMDB").get("type") != "rich_text" 195 | ): 196 | update_properties["IMDB"] = {"rich_text": {}} 197 | if len(update_properties) > 0: 198 | self.client.databases.update(database_id=id, properties=update_properties) 199 | 200 | @retry(stop_max_attempt_number=3, wait_fixed=5000) 201 | def get_relation_id(self, name, id, icon, properties={}): 202 | key = f"{id}{name}" 203 | if key in self.__cache: 204 | return self.__cache.get(key) 205 | filter = {"property": "标题", "title": {"equals": name}} 206 | response = self.client.databases.query(database_id=id, filter=filter) 207 | if len(response.get("results")) == 0: 208 | parent = {"database_id": id, "type": "database_id"} 209 | properties["标题"] = get_title(name) 210 | page_id = self.client.pages.create( 211 | parent=parent, properties=properties, icon=get_icon(icon) 212 | ).get("id") 213 | else: 214 | page_id = response.get("results")[0].get("id") 215 | self.__cache[key] = page_id 216 | return page_id 217 | 218 | 219 | 220 | @retry(stop_max_attempt_number=3, wait_fixed=5000) 221 | def update_book_page(self, page_id, properties): 222 | return self.client.pages.update(page_id=page_id, properties=properties) 223 | 224 | @retry(stop_max_attempt_number=3, wait_fixed=5000) 225 | def update_page(self, page_id, properties): 226 | return self.client.pages.update( 227 | page_id=page_id, properties=properties 228 | ) 229 | 230 | @retry(stop_max_attempt_number=3, wait_fixed=5000) 231 | def create_page(self, parent, properties, icon): 232 | return self.client.pages.create(parent=parent, properties=properties, icon=icon) 233 | 234 | @retry(stop_max_attempt_number=3, wait_fixed=5000) 235 | def query(self, **kwargs): 236 | kwargs = {k: v for k, v in kwargs.items() if v} 237 | return self.client.databases.query(**kwargs) 238 | 239 | @retry(stop_max_attempt_number=3, wait_fixed=5000) 240 | def get_block_children(self, id): 241 | response = self.client.blocks.children.list(id) 242 | return response.get("results") 243 | 244 | @retry(stop_max_attempt_number=3, wait_fixed=5000) 245 | def append_blocks(self, block_id, children): 246 | return self.client.blocks.children.append(block_id=block_id, children=children) 247 | 248 | @retry(stop_max_attempt_number=3, wait_fixed=5000) 249 | def append_blocks_after(self, block_id, children, after): 250 | return self.client.blocks.children.append( 251 | block_id=block_id, children=children, after=after 252 | ) 253 | 254 | @retry(stop_max_attempt_number=3, wait_fixed=5000) 255 | def delete_block(self, block_id): 256 | return self.client.blocks.delete(block_id=block_id) 257 | 258 | 259 | @retry(stop_max_attempt_number=3, wait_fixed=5000) 260 | def query_all_by_book(self, database_id, filter): 261 | results = [] 262 | has_more = True 263 | start_cursor = None 264 | while has_more: 265 | response = self.client.databases.query( 266 | database_id=database_id, 267 | filter=filter, 268 | start_cursor=start_cursor, 269 | page_size=100, 270 | ) 271 | start_cursor = response.get("next_cursor") 272 | has_more = response.get("has_more") 273 | results.extend(response.get("results")) 274 | return results 275 | 276 | @retry(stop_max_attempt_number=3, wait_fixed=5000) 277 | def query_all(self, database_id): 278 | """获取database中所有的数据""" 279 | results = [] 280 | has_more = True 281 | start_cursor = None 282 | while has_more: 283 | response = self.client.databases.query( 284 | database_id=database_id, 285 | start_cursor=start_cursor, 286 | page_size=100, 287 | ) 288 | start_cursor = response.get("next_cursor") 289 | has_more = response.get("has_more") 290 | results.extend(response.get("results")) 291 | return results 292 | 293 | def get_date_relation(self, properties, date): 294 | properties["年"] = get_relation( 295 | [ 296 | self.get_year_relation_id(date), 297 | ] 298 | ) 299 | properties["月"] = get_relation( 300 | [ 301 | self.get_month_relation_id(date), 302 | ] 303 | ) 304 | properties["周"] = get_relation( 305 | [ 306 | self.get_week_relation_id(date), 307 | ] 308 | ) 309 | properties["日"] = get_relation( 310 | [ 311 | self.get_day_relation_id(date), 312 | ] 313 | ) 314 | -------------------------------------------------------------------------------- /douban2notion/douban.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from email import feedparser 3 | import json 4 | import os 5 | import re 6 | from bs4 import BeautifulSoup 7 | import pendulum 8 | from retrying import retry 9 | import requests 10 | from douban2notion.notion_helper import NotionHelper 11 | from douban2notion import utils 12 | DOUBAN_API_HOST = os.getenv("DOUBAN_API_HOST", "frodo.douban.com") 13 | DOUBAN_API_KEY = os.getenv("DOUBAN_API_KEY", "0ac44ae016490db2204ce0a042db2916") 14 | 15 | from douban2notion.config import movie_properties_type_dict,book_properties_type_dict, TAG_ICON_URL, USER_ICON_URL 16 | from douban2notion.utils import get_icon 17 | from dotenv import load_dotenv 18 | load_dotenv() 19 | rating = { 20 | 1: "⭐️", 21 | 2: "⭐️⭐️", 22 | 3: "⭐️⭐️⭐️", 23 | 4: "⭐️⭐️⭐️⭐️", 24 | 5: "⭐️⭐️⭐️⭐️⭐️", 25 | } 26 | movie_status = { 27 | "mark": "想看", 28 | "doing": "在看", 29 | "done": "看过", 30 | } 31 | book_status = { 32 | "mark": "想读", 33 | "doing": "在读", 34 | "done": "读过", 35 | } 36 | AUTH_TOKEN = os.getenv("AUTH_TOKEN") 37 | 38 | headers = { 39 | "host": DOUBAN_API_HOST, 40 | "authorization": f"Bearer {AUTH_TOKEN}" if AUTH_TOKEN else "", 41 | "user-agent": "User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 15_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.16(0x18001023) NetType/WIFI Language/zh_CN", 42 | "referer": "https://servicewechat.com/wx2f9b06c1de1ccfca/84/page-frame.html", 43 | } 44 | @retry(stop_max_attempt_number=3, wait_fixed=5000) 45 | def fetch_subjects(user, type_, status): 46 | offset = 0 47 | page = 0 48 | url = f"https://{DOUBAN_API_HOST}/api/v2/user/{user}/interests" 49 | total = 0 50 | results = [] 51 | while True: 52 | params = { 53 | "type": type_, 54 | "count": 50, 55 | "status": status, 56 | "start": offset, 57 | "apiKey": DOUBAN_API_KEY, 58 | } 59 | response = requests.get(url, headers=headers, params=params) 60 | 61 | if response.ok: 62 | response = response.json() 63 | interests = response.get("interests") 64 | if len(interests)==0: 65 | break 66 | results.extend(interests) 67 | print(f"total = {total}") 68 | print(f"size = {len(results)}") 69 | page += 1 70 | offset = page * 50 71 | return results 72 | 73 | 74 | 75 | def insert_movie(douban_name,notion_helper): 76 | notion_movies = notion_helper.query_all(database_id=notion_helper.movie_database_id) 77 | notion_movie_dict = {} 78 | for i in notion_movies: 79 | movie = {} 80 | for key, value in i.get("properties").items(): 81 | movie[key] = utils.get_property_value(value) 82 | notion_movie_dict[movie.get("豆瓣链接")] = { 83 | "短评": movie.get("短评"), 84 | "状态": movie.get("状态"), 85 | "日期": movie.get("日期"), 86 | "评分": movie.get("评分"), 87 | "演员": movie.get("演员"), 88 | "IMDB": movie.get("IMDB"), 89 | "page_id": i.get("id") 90 | } 91 | results = [] 92 | for i in movie_status.keys(): 93 | results.extend(fetch_subjects(douban_name, "movie", i)) 94 | for result in results: 95 | movie = {} 96 | if not result: 97 | print(result) 98 | continue 99 | subject = result.get("subject") 100 | movie["电影名"] = subject.get("title") 101 | create_time = result.get("create_time") 102 | create_time = pendulum.parse(create_time,tz=utils.tz) 103 | #时间上传到Notion会丢掉秒的信息,这里直接将秒设置为0 104 | create_time = create_time.replace(second=0) 105 | movie["日期"] = create_time.int_timestamp 106 | movie["豆瓣链接"] = subject.get("url") 107 | movie["状态"] = movie_status.get(result.get("status")) 108 | if result.get("rating"): 109 | movie["评分"] = rating.get(result.get("rating").get("value")) 110 | if result.get("comment"): 111 | movie["短评"] = result.get("comment") 112 | if notion_movie_dict.get(movie.get("豆瓣链接")): 113 | notion_movive = notion_movie_dict.get(movie.get("豆瓣链接")) 114 | if ( 115 | notion_movive.get("日期") != movie.get("日期") 116 | or notion_movive.get("短评") != movie.get("短评") 117 | or notion_movive.get("状态") != movie.get("状态") 118 | or notion_movive.get("评分") != movie.get("评分") 119 | or not notion_movive.get("演员") 120 | or not notion_movive.get("IMDB") 121 | ): 122 | if not notion_movive.get("演员") and subject.get("actors"): 123 | l = [] 124 | actors = subject.get("actors")[0:5] 125 | for actor in actors: 126 | if actor.get("name"): 127 | if "/" in actor.get("name"): 128 | l.extend(actor.get("name").split("/")) 129 | else: 130 | l.append(actor.get("name")) 131 | movie["演员"] = [ 132 | notion_helper.get_relation_id( 133 | x.get("name"), notion_helper.actor_database_id, USER_ICON_URL 134 | ) 135 | for x in actors 136 | ] 137 | if not notion_movive.get("IMDB"): 138 | movie["IMDB"] = get_imdb(movie.get("豆瓣链接")) 139 | properties = utils.get_properties(movie, movie_properties_type_dict) 140 | print(movie.get("电影名")) 141 | notion_helper.get_date_relation(properties,create_time) 142 | notion_helper.update_page( 143 | page_id=notion_movive.get("page_id"), 144 | properties=properties 145 | ) 146 | 147 | else: 148 | print(f"插入{movie.get('电影名')}") 149 | cover = subject.get("pic").get("normal") 150 | if not cover.endswith('.webp'): 151 | cover = cover.rsplit('.', 1)[0] + '.webp' 152 | movie["封面"] = cover 153 | movie["类型"] = subject.get("type") 154 | if subject.get("genres"): 155 | movie["分类"] = [ 156 | notion_helper.get_relation_id( 157 | x, notion_helper.category_database_id, TAG_ICON_URL 158 | ) 159 | for x in subject.get("genres") 160 | ] 161 | if subject.get("actors"): 162 | l = [] 163 | actors = subject.get("actors")[0:5] 164 | for actor in actors: 165 | if actor.get("name"): 166 | if "/" in actor.get("name"): 167 | l.extend(actor.get("name").split("/")) 168 | else: 169 | l.append(actor.get("name")) 170 | movie["演员"] = [ 171 | notion_helper.get_relation_id( 172 | x.get("name"), notion_helper.actor_database_id, USER_ICON_URL 173 | ) 174 | for x in actors 175 | ] 176 | if subject.get("directors"): 177 | movie["导演"] = [ 178 | notion_helper.get_relation_id( 179 | x.get("name"), notion_helper.director_database_id, USER_ICON_URL 180 | ) 181 | for x in subject.get("directors")[0:5] 182 | ] 183 | properties = utils.get_properties(movie, movie_properties_type_dict) 184 | notion_helper.get_date_relation(properties,create_time) 185 | parent = { 186 | "database_id": notion_helper.movie_database_id, 187 | "type": "database_id", 188 | } 189 | notion_helper.create_page( 190 | parent=parent, properties=properties, icon=get_icon(cover) 191 | ) 192 | def get_imdb(link): 193 | headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36'} 194 | response = requests.get(link, headers=headers) 195 | soup = BeautifulSoup(response.content) 196 | info = soup.find(id='info') 197 | if info: 198 | for span in info.find_all('span', {'class': 'pl'}): 199 | if ('IMDb:' == span.string): 200 | return span.next_sibling.string.strip() 201 | 202 | def insert_book(douban_name,notion_helper): 203 | notion_books = notion_helper.query_all(database_id=notion_helper.book_database_id) 204 | notion_book_dict = {} 205 | for i in notion_books: 206 | book = {} 207 | for key, value in i.get("properties").items(): 208 | book[key] = utils.get_property_value(value) 209 | notion_book_dict[book.get("豆瓣链接")] = { 210 | "短评": book.get("短评"), 211 | "状态": book.get("状态"), 212 | "日期": book.get("日期"), 213 | "评分": book.get("评分"), 214 | "封面": book.get("封面"), 215 | "page_id": i.get("id"), 216 | } 217 | print(i) 218 | print(f"notion {len(notion_book_dict)}") 219 | results = [] 220 | for i in book_status.keys(): 221 | results.extend(fetch_subjects(douban_name, "book", i)) 222 | for result in results: 223 | book = {} 224 | if not result: 225 | continue 226 | subject = result.get("subject") 227 | book["书名"] = subject.get("title") 228 | create_time = result.get("create_time") 229 | create_time = pendulum.parse(create_time,tz=utils.tz) 230 | #时间上传到Notion会丢掉秒的信息,这里直接将秒设置为0 231 | create_time = create_time.replace(second=0) 232 | book["日期"] = create_time.int_timestamp 233 | book["豆瓣链接"] = subject.get("url") 234 | book["状态"] = book_status.get(result.get("status")) 235 | cover = subject.get("pic").get("large") 236 | if not cover.endswith('.webp'): 237 | cover = cover.rsplit('.', 1)[0] + '.webp' 238 | book["封面"] = cover 239 | if result.get("rating"): 240 | book["评分"] = rating.get(result.get("rating").get("value")) 241 | if result.get("comment"): 242 | book["短评"] = result.get("comment") 243 | if notion_book_dict.get(book.get("豆瓣链接")): 244 | notion_movive = notion_book_dict.get(book.get("豆瓣链接")) 245 | if ( 246 | notion_movive.get("封面") is None 247 | or notion_movive.get("封面") != book.get("封面") 248 | or notion_movive.get("日期") != book.get("日期") 249 | or notion_movive.get("短评") != book.get("短评") 250 | or notion_movive.get("状态") != book.get("状态") 251 | or notion_movive.get("评分") != book.get("评分") 252 | ): 253 | print(f"更新{book.get('书名')}") 254 | properties = utils.get_properties(book, book_properties_type_dict) 255 | notion_helper.get_date_relation(properties,create_time) 256 | notion_helper.update_page( 257 | page_id=notion_movive.get("page_id"), 258 | properties=properties 259 | ) 260 | 261 | else: 262 | print(f"插入{book.get('书名')}") 263 | book["简介"] = subject.get("intro") 264 | press = [] 265 | for i in subject.get("press"): 266 | press.extend(i.split(",")) 267 | book["出版社"] = press 268 | book["类型"] = subject.get("type") 269 | if result.get("tags"): 270 | book["分类"] = [ 271 | notion_helper.get_relation_id( 272 | x, notion_helper.category_database_id, TAG_ICON_URL 273 | ) 274 | for x in result.get("tags") 275 | ] 276 | if subject.get("author"): 277 | book["作者"] = [ 278 | notion_helper.get_relation_id( 279 | x, notion_helper.author_database_id, USER_ICON_URL 280 | ) 281 | for x in subject.get("author")[0:100] 282 | ] 283 | properties = utils.get_properties(book, book_properties_type_dict) 284 | notion_helper.get_date_relation(properties,create_time) 285 | parent = { 286 | "database_id": notion_helper.book_database_id, 287 | "type": "database_id", 288 | } 289 | notion_helper.create_page( 290 | parent=parent, properties=properties, icon=get_icon(cover) 291 | ) 292 | 293 | 294 | def main(): 295 | parser = argparse.ArgumentParser() 296 | parser.add_argument("type") 297 | options = parser.parse_args() 298 | type = options.type 299 | notion_helper = NotionHelper(type) 300 | is_movie = True if type=="movie" else False 301 | douban_name = os.getenv("DOUBAN_NAME", None) 302 | if is_movie: 303 | insert_movie(douban_name,notion_helper) 304 | else: 305 | insert_book(douban_name,notion_helper) 306 | if __name__ == "__main__": 307 | main() -------------------------------------------------------------------------------- /OUT_FOLDER/book/1766506028.svg: -------------------------------------------------------------------------------- 1 | 2 | 马林康的读书记录2025: 0 本Jan2024-12-302024-12-312025-01-012025-01-022025-01-032025-01-042025-01-052025-01-062025-01-072025-01-082025-01-092025-01-102025-01-112025-01-122025-01-132025-01-142025-01-152025-01-162025-01-172025-01-182025-01-192025-01-202025-01-212025-01-222025-01-232025-01-242025-01-252025-01-262025-01-272025-01-282025-01-292025-01-302025-01-312025-02-012025-02-02Feb2025-02-032025-02-042025-02-052025-02-062025-02-072025-02-082025-02-092025-02-102025-02-112025-02-122025-02-132025-02-142025-02-152025-02-162025-02-172025-02-182025-02-192025-02-202025-02-212025-02-222025-02-232025-02-242025-02-252025-02-262025-02-272025-02-282025-03-012025-03-02Mar2025-03-032025-03-042025-03-052025-03-062025-03-072025-03-082025-03-092025-03-102025-03-112025-03-122025-03-132025-03-142025-03-152025-03-162025-03-172025-03-182025-03-192025-03-202025-03-212025-03-222025-03-232025-03-242025-03-252025-03-262025-03-272025-03-282025-03-292025-03-302025-03-312025-04-012025-04-022025-04-032025-04-042025-04-052025-04-06Apr2025-04-072025-04-082025-04-092025-04-102025-04-112025-04-122025-04-132025-04-142025-04-152025-04-162025-04-172025-04-182025-04-192025-04-202025-04-212025-04-222025-04-232025-04-242025-04-252025-04-262025-04-272025-04-282025-04-292025-04-302025-05-012025-05-022025-05-032025-05-04May2025-05-052025-05-062025-05-072025-05-082025-05-092025-05-102025-05-112025-05-122025-05-132025-05-142025-05-152025-05-162025-05-172025-05-182025-05-192025-05-202025-05-212025-05-222025-05-232025-05-242025-05-252025-05-262025-05-272025-05-282025-05-292025-05-302025-05-312025-06-01Jun2025-06-022025-06-032025-06-042025-06-052025-06-062025-06-072025-06-082025-06-092025-06-102025-06-112025-06-122025-06-132025-06-142025-06-152025-06-162025-06-172025-06-182025-06-192025-06-202025-06-212025-06-222025-06-232025-06-242025-06-252025-06-262025-06-272025-06-282025-06-292025-06-302025-07-012025-07-022025-07-032025-07-042025-07-052025-07-06Jul2025-07-072025-07-082025-07-092025-07-102025-07-112025-07-122025-07-132025-07-142025-07-152025-07-162025-07-172025-07-182025-07-192025-07-202025-07-212025-07-222025-07-232025-07-242025-07-252025-07-262025-07-272025-07-282025-07-292025-07-302025-07-312025-08-012025-08-022025-08-03Aug2025-08-042025-08-052025-08-062025-08-072025-08-082025-08-092025-08-102025-08-112025-08-122025-08-132025-08-142025-08-152025-08-162025-08-172025-08-182025-08-192025-08-202025-08-212025-08-222025-08-232025-08-242025-08-252025-08-262025-08-272025-08-282025-08-292025-08-302025-08-31Sep2025-09-012025-09-022025-09-032025-09-042025-09-052025-09-062025-09-072025-09-082025-09-092025-09-102025-09-112025-09-122025-09-132025-09-142025-09-152025-09-162025-09-172025-09-182025-09-192025-09-202025-09-212025-09-222025-09-232025-09-242025-09-252025-09-262025-09-272025-09-282025-09-292025-09-302025-10-012025-10-022025-10-032025-10-042025-10-05Oct2025-10-062025-10-072025-10-082025-10-092025-10-102025-10-112025-10-122025-10-132025-10-142025-10-152025-10-162025-10-172025-10-182025-10-192025-10-202025-10-212025-10-222025-10-232025-10-242025-10-252025-10-262025-10-272025-10-282025-10-292025-10-302025-10-312025-11-012025-11-02Nov2025-11-032025-11-042025-11-052025-11-062025-11-072025-11-082025-11-092025-11-102025-11-112025-11-122025-11-132025-11-142025-11-152025-11-162025-11-172025-11-182025-11-192025-11-202025-11-212025-11-222025-11-232025-11-242025-11-252025-11-262025-11-272025-11-282025-11-292025-11-30Dec2025-12-012025-12-022025-12-032025-12-042025-12-052025-12-062025-12-072025-12-082025-12-092025-12-102025-12-112025-12-122025-12-132025-12-142025-12-152025-12-162025-12-172025-12-182025-12-192025-12-202025-12-212025-12-222025-12-232025-12-242025-12-252025-12-262025-12-272025-12-282025-12-292025-12-302025-12-31 -------------------------------------------------------------------------------- /OUT_FOLDER/movie/1766506353.svg: -------------------------------------------------------------------------------- 1 | 2 | 马林康的观影记录2025: 38 部Jan2024-12-302024-12-312025-01-012025-01-022025-01-03 1.0 部2025-01-042025-01-052025-01-062025-01-072025-01-082025-01-09 1.0 部2025-01-102025-01-112025-01-122025-01-132025-01-142025-01-15 1.0 部2025-01-162025-01-172025-01-182025-01-192025-01-202025-01-212025-01-222025-01-232025-01-242025-01-252025-01-262025-01-272025-01-282025-01-292025-01-302025-01-312025-02-01 1.0 部2025-02-02 1.0 部Feb2025-02-032025-02-042025-02-052025-02-062025-02-072025-02-082025-02-092025-02-102025-02-112025-02-122025-02-132025-02-142025-02-152025-02-162025-02-172025-02-182025-02-192025-02-202025-02-212025-02-222025-02-232025-02-242025-02-252025-02-262025-02-272025-02-282025-03-012025-03-02Mar2025-03-032025-03-042025-03-052025-03-062025-03-072025-03-082025-03-092025-03-102025-03-112025-03-122025-03-132025-03-142025-03-152025-03-162025-03-172025-03-182025-03-192025-03-202025-03-212025-03-222025-03-232025-03-242025-03-252025-03-262025-03-272025-03-282025-03-292025-03-302025-03-312025-04-012025-04-022025-04-032025-04-042025-04-052025-04-06Apr2025-04-072025-04-082025-04-092025-04-102025-04-112025-04-122025-04-13 1.0 部2025-04-142025-04-152025-04-162025-04-172025-04-182025-04-192025-04-202025-04-212025-04-222025-04-232025-04-242025-04-252025-04-262025-04-272025-04-282025-04-292025-04-302025-05-012025-05-022025-05-032025-05-04May2025-05-052025-05-062025-05-072025-05-082025-05-092025-05-102025-05-112025-05-122025-05-132025-05-142025-05-152025-05-162025-05-172025-05-182025-05-192025-05-202025-05-212025-05-222025-05-232025-05-242025-05-252025-05-262025-05-272025-05-282025-05-292025-05-302025-05-312025-06-01Jun2025-06-022025-06-032025-06-042025-06-052025-06-062025-06-072025-06-082025-06-092025-06-102025-06-112025-06-122025-06-132025-06-142025-06-152025-06-162025-06-172025-06-182025-06-192025-06-202025-06-212025-06-222025-06-232025-06-242025-06-252025-06-262025-06-272025-06-282025-06-29 2.0 部2025-06-302025-07-012025-07-022025-07-032025-07-042025-07-052025-07-06Jul2025-07-07 1.0 部2025-07-082025-07-092025-07-102025-07-112025-07-122025-07-132025-07-142025-07-152025-07-162025-07-172025-07-182025-07-192025-07-202025-07-212025-07-22 1.0 部2025-07-232025-07-242025-07-25 2.0 部2025-07-262025-07-272025-07-282025-07-292025-07-302025-07-312025-08-012025-08-022025-08-03Aug2025-08-042025-08-052025-08-062025-08-072025-08-082025-08-092025-08-102025-08-112025-08-122025-08-132025-08-142025-08-152025-08-162025-08-172025-08-182025-08-192025-08-202025-08-21 1.0 部2025-08-222025-08-232025-08-242025-08-252025-08-262025-08-272025-08-282025-08-292025-08-30 1.0 部2025-08-31Sep2025-09-012025-09-022025-09-032025-09-042025-09-052025-09-062025-09-072025-09-082025-09-09 1.0 部2025-09-102025-09-112025-09-122025-09-132025-09-14 1.0 部2025-09-152025-09-16 1.0 部2025-09-172025-09-182025-09-192025-09-20 10.0 部2025-09-21 5.0 部2025-09-22 1.0 部2025-09-232025-09-242025-09-25 1.0 部2025-09-262025-09-272025-09-282025-09-292025-09-302025-10-012025-10-022025-10-032025-10-042025-10-05Oct2025-10-062025-10-072025-10-082025-10-092025-10-102025-10-112025-10-122025-10-132025-10-142025-10-152025-10-162025-10-172025-10-182025-10-192025-10-202025-10-212025-10-222025-10-232025-10-242025-10-252025-10-262025-10-272025-10-282025-10-292025-10-302025-10-312025-11-012025-11-02Nov2025-11-032025-11-042025-11-052025-11-062025-11-072025-11-082025-11-09 1.0 部2025-11-102025-11-112025-11-122025-11-132025-11-142025-11-152025-11-162025-11-172025-11-182025-11-192025-11-202025-11-212025-11-22 1.0 部2025-11-232025-11-242025-11-252025-11-262025-11-272025-11-28 1.0 部2025-11-292025-11-30Dec2025-12-012025-12-02 1.0 部2025-12-032025-12-042025-12-052025-12-062025-12-072025-12-082025-12-092025-12-102025-12-112025-12-122025-12-132025-12-142025-12-152025-12-162025-12-172025-12-182025-12-192025-12-202025-12-212025-12-222025-12-232025-12-242025-12-252025-12-262025-12-272025-12-282025-12-292025-12-302025-12-31 --------------------------------------------------------------------------------