├── 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 | 
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 | 
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 |
--------------------------------------------------------------------------------
/OUT_FOLDER/movie/1766506353.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------