├── .gitignore ├── .vscode └── settings.json ├── bin └── chromedriver ├── browser.py ├── chromedriver.log ├── deploy.py ├── main.py ├── pdf.icns ├── readme.md ├── screenshot.png └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | *.pyc 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | .pytest_cache/ 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule.* 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # Environments 84 | .env 85 | .venv 86 | env/ 87 | venv/ 88 | ENV/ 89 | env.bak/ 90 | venv.bak/ 91 | 92 | # Spyder project settings 93 | .spyderproject 94 | .spyproject 95 | 96 | # Rope project settings 97 | .ropeproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | 105 | 106 | # End of https://www.gitignore.io/api/python 107 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/usr/local/bin/python3" 3 | } -------------------------------------------------------------------------------- /bin/chromedriver: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binho/notion-pdf/73362045a18f0d2ad2937800ffb80f9e48efc65d/bin/chromedriver -------------------------------------------------------------------------------- /browser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import contextlib 4 | import json 5 | import os 6 | import random 7 | import sys 8 | import time 9 | from contextlib import contextmanager 10 | from selenium import webdriver 11 | from selenium.common.exceptions import TimeoutException 12 | from selenium.webdriver.chrome.options import Options 13 | from selenium.webdriver.common.by import By 14 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities 15 | from selenium.webdriver.support import expected_conditions as EC 16 | from selenium.webdriver.support.expected_conditions import staleness_of 17 | from selenium.webdriver.support.ui import WebDriverWait 18 | 19 | user_agents = ( 20 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", 21 | "Mozilla/5.0 (Windows NT 5.1; rv:7.0.1) Gecko/20100101 Firefox/7.0.1", 22 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", 23 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/601.7.7 (KHTML, like Gecko) Version/9.1.2 Safari/601.7.7", 24 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36", 25 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/602.4.8 (KHTML, like Gecko) Version/10.0.3 Safari/602.4.8" 26 | ) 27 | 28 | def convert_to_pdf(urls, download_path): 29 | sleep = 6 30 | max_wait = 60 31 | width = 1280 32 | height = 720 33 | 34 | options = webdriver.ChromeOptions() 35 | # options.add_argument('--headless') 36 | options.add_argument('--ignore-certificate-errors') 37 | options.add_argument('window-size={},{}'.format(width, height)) 38 | options.add_argument('--user-agent={}'.format(random.choice(list(user_agents)))) 39 | options.add_argument('--no-sandbox') 40 | options.add_argument('--disable-extensions') 41 | options.add_argument('--kiosk-printing') 42 | options.add_argument('--test-type') 43 | # options.add_argument('--disable-infobars') 44 | options.add_argument('--disable-gpu') 45 | 46 | # Make sure "Save as PDF" is selected on print preview dialog 47 | # Ref: https://github.com/gingerbeardman/chrome-application-shortcuts/blob/master/Gmail.app/Contents/Profile/Default/Preferences 48 | appState = { 49 | "recentDestinations": [{ 50 | "id": "Save as PDF", 51 | "origin": "local" 52 | }], 53 | "selectedDestinationId": "Save as PDF", 54 | "version": 2, 55 | "isHeaderFooterEnabled": False 56 | } 57 | 58 | # Converts ~/Downloads to /Users//Downloads 59 | expanded_download_path = os.path.expanduser(download_path) 60 | 61 | prefs = { 62 | 'download.default_directory': os.path.normpath(expanded_download_path), 63 | 'savefile.default_directory': os.path.normpath(expanded_download_path), 64 | 'download.prompt_for_download': False, 65 | 'download.directory_upgrade': True, 66 | 'profile.default_content_settings.popups': 0, 67 | 'plugins.always_open_pdf_externally': True, 68 | 'printing.print_preview_sticky_settings.appState': json.dumps(appState), 69 | 'disk-cache-size': 4096 70 | } 71 | options.add_experimental_option('prefs', prefs) 72 | 73 | browser = webdriver.Chrome(executable_path='bin/chromedriver', 74 | chrome_options=options, 75 | service_args=['--verbose', '--log-path=/tmp/chromedriver.log']) 76 | browser.set_window_size(width, height) 77 | browser.set_page_load_timeout(max_wait) 78 | browser.set_script_timeout(max_wait) 79 | 80 | for url in urls: 81 | try: 82 | print('Loading URL: %s' % (url)) 83 | browser.get(url) 84 | except: 85 | print('Failed to load URL: %s' % (url)) 86 | 87 | try: 88 | notion_app_element = EC.presence_of_element_located((By.ID, 'notion-app')) 89 | WebDriverWait(browser, 10).until(notion_app_element) 90 | 91 | # add some extra time for images to finish loading 92 | time.sleep(5) 93 | 94 | # browser.execute_script(""" 95 | # var head = document.getElementsByTagName('head')[0]; 96 | # var style = '' 97 | # head.appendChild(style); 98 | # """) 99 | 100 | # Try to remove the login button from Notion before print 101 | try: 102 | login_button_element = browser.find_element_by_xpath("//a[@href='/login']") 103 | browser.execute_script('arguments[0].remove();', login_button_element) 104 | except: 105 | pass 106 | 107 | browser.execute_script('window.print();') 108 | except TimeoutException: 109 | print('Timed out waiting for page to load') 110 | 111 | browser.quit() 112 | -------------------------------------------------------------------------------- /chromedriver.log: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /deploy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | from distutils.dir_util import copy_tree 5 | 6 | app_deploy_name = 'NotionPDF.app' 7 | 8 | print('🚮 Cleaning...') 9 | os.system('rm -f *.pyc') 10 | os.system('rm -fr build') 11 | os.system('rm -fr dist') 12 | os.system('rm -fr __pycache__') 13 | os.system('echo > /tmp/chromedriver.log') 14 | 15 | print('⚙️ Running pyinstaller...') 16 | os.system('pyinstaller --onefile --windowed main.spec') 17 | 18 | print('🚢 Copying files from bin folder...') 19 | copy_tree('bin/', 'dist/{}/Contents/MacOS/bin'.format(app_deploy_name)) -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import browser 5 | from PyQt5.QtCore import pyqtSlot, Qt 6 | from PyQt5.QtGui import QColor, QFont, QFontDatabase, QFontInfo, QIcon 7 | from PyQt5.QtWidgets import (QAction, QApplication, QFileDialog, QLabel, 8 | QLineEdit, QMainWindow, QMessageBox, QPushButton, 9 | QTextEdit, QVBoxLayout, QWidget, QErrorMessage) 10 | 11 | class Window(QWidget): 12 | 13 | def __init__(self, parent=None): 14 | super(Window, self).__init__(parent) 15 | self.title = 'Notion to PDF' 16 | self.width = 840 17 | self.height = 600 18 | self.download_path = '~/Downloads' 19 | self.initUI() 20 | 21 | def initUI(self): 22 | self.setWindowTitle(self.title) 23 | 24 | self.setAutoFillBackground(True) 25 | 26 | self.font = QFont('Arial') 27 | self.font.setPointSize(15) 28 | self.font.setStyleStrategy(QFont.PreferAntialias) 29 | 30 | self.layout = QVBoxLayout() 31 | 32 | self.label = QLabel('1. Make sure the document is set as "Public Access" and "Can Read" on Notion.\n2. Add one Notion page URL per line and tap "Convert All" button.\n3. Google Chrome will open, load each URL and then save to PDF (Keep this window open while converting)') 33 | self.label.setFont(self.font) 34 | self.layout.addWidget(self.label) 35 | 36 | self.text_edit = QTextEdit(self) 37 | self.text_edit.setMinimumWidth(600) 38 | self.text_edit.setFont(self.font) 39 | # self.text_edit.setFont(QFontDatabase.systemFont(QFontDatabase.GeneralFont)) 40 | self.layout.addWidget(self.text_edit) 41 | 42 | self.directory_button = QPushButton('Select Destination Folder ({})'.format(self.download_path), self) 43 | self.directory_button.setMinimumHeight(42) 44 | self.directory_button.setStyleSheet('border: 0; border-radius: 6px; background-color: white;') 45 | self.directory_button.setFont(self.font) 46 | self.directory_button.clicked.connect(self.select_directory) 47 | self.layout.addWidget(self.directory_button) 48 | 49 | self.convert_button = QPushButton('Convert All', self) 50 | self.convert_button.setMinimumHeight(60) 51 | self.convert_button.setFont(self.font) 52 | self.convert_button.clicked.connect(self.convert_all) 53 | self.convert_button.setStyleSheet('border: 0; border-radius: 6px; background-color: #26D0B7;') 54 | self.layout.addWidget(self.convert_button) 55 | 56 | self.setLayout(self.layout) 57 | 58 | @pyqtSlot() 59 | def select_directory(self): 60 | dialog = QFileDialog() 61 | dialog.setFileMode(QFileDialog.DirectoryOnly) 62 | 63 | if dialog.exec_() == QFileDialog.Accepted: 64 | self.download_path = dialog.selectedFiles()[0] 65 | self.directory_button.setText('Select Destination Folder ({})'.format(self.download_path)) 66 | 67 | @pyqtSlot() 68 | def convert_all(self): 69 | value = self.text_edit.toPlainText() 70 | if not value: 71 | QMessageBox.warning(self, 'Error', 'Please enter a valid URL') 72 | else: 73 | urls = value.split('\n') 74 | browser.convert_to_pdf(urls, self.download_path) 75 | 76 | QMessageBox.information(self, 'Success', 'All URLs converted and saved to:\n{}'.format(self.download_path)) 77 | 78 | self.text_edit.setPlainText('') 79 | 80 | if __name__ == '__main__': 81 | import sys 82 | app = QApplication(sys.argv) 83 | window = Window() 84 | 85 | window.setAutoFillBackground(True) 86 | 87 | p = window.palette() 88 | p.setColor(window.backgroundRole(), QColor("#F5F5F5")) 89 | window.setPalette(p) 90 | 91 | window.show() 92 | sys.exit(app.exec_()) 93 | -------------------------------------------------------------------------------- /pdf.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binho/notion-pdf/73362045a18f0d2ad2937800ffb80f9e48efc65d/pdf.icns -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![Screenshot](https://github.com/binho/notion-pdf/blob/master/screenshot.png) 2 | 3 | ## Dependencies 4 | 5 | Recommended use of Python 3. 6 | 7 | [Chrome Browser](https://www.google.com/chrome) 8 | 9 | 10 | ### Python modules 11 | ``` 12 | pip3 install selenium 13 | pip3 install PyQt5 14 | ``` 15 | 16 | ## Running 17 | 18 | `python3 main.py` 19 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binho/notion-pdf/73362045a18f0d2ad2937800ffb80f9e48efc65d/screenshot.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | app_title = "Notion to PDF" 5 | app_description = "" 6 | main_python_file = "main.py" 7 | 8 | import sys 9 | from cx_Freeze import setup, Executable 10 | 11 | # Dependencies are automatically detected, but it might need 12 | # fine tuning. 13 | buildOptions = dict(packages = [], excludes = []) 14 | 15 | import sys 16 | base = 'Win32GUI' if sys.platform=='win32' else None 17 | 18 | executables = [ 19 | Executable(main_python_file, base=base) 20 | ] 21 | 22 | setup( 23 | name=app_title, 24 | version = '0.1', 25 | description = app_description, 26 | options = dict(build_exe = buildOptions), 27 | executables = executables 28 | ) --------------------------------------------------------------------------------