├── README.rst ├── webkin ├── __init__.py ├── config.py ├── send_email.py ├── scaffold.py └── webkin.py ├── requirements.txt ├── MANIFEST.in ├── LICENSE ├── .gitignore ├── setup.py └── README.md /README.rst: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /webkin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | olefile==0.44 2 | Pillow==9.0.1 3 | requests==2.20.0 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.in 2 | include *.md 3 | include *.rst 4 | include *.txt 5 | include LICENSE 6 | -------------------------------------------------------------------------------- /webkin/config.py: -------------------------------------------------------------------------------- 1 | from os import getenv 2 | 3 | KINDLE_EMAIL = getenv('KINDLE_EMAIL') 4 | AMAZON_EMAIL = getenv('AMAZON_EMAIL') 5 | 6 | EMAIL_PASSWORD = getenv('EMAIL_PASSWORD') 7 | SMTP_HOST_NAME = getenv('SMTP_HOST_NAME') 8 | 9 | # DEFAULT SMTP PORT IS 587 for non SSL in GMail, SET YOURS HERE TO CHANGE 10 | 11 | SMTP_PORT = getenv('SMTP_PORT', 587) 12 | MERCURY_API_KEY = getenv('MERCURY_API_KEY') 13 | 14 | 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Karan Sharma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | .vscode/ 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | .idea/* 92 | 93 | .html 94 | .png 95 | .mobi -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | try: 6 | import pypandoc 7 | long_description = pypandoc.convert('README.md', 'rst') 8 | except (IOError, ImportError): 9 | long_description = open('README.rst', encoding="utf8").read() 10 | 11 | with open('requirements.txt') as f: 12 | requirements = f.read().splitlines() 13 | 14 | 15 | version = '1.0.3' 16 | 17 | setup( 18 | name='webkin', 19 | version=version, 20 | install_requires=requirements, 21 | author='Karan Sharma', 22 | author_email='karansharma1295@gmail.com', 23 | packages=find_packages(), 24 | include_package_data=True, 25 | url='https://github.com/mr-karan/webkin/', 26 | license='MIT', 27 | description='Send webpages to your Kindle through terminal', 28 | long_description=long_description, 29 | entry_points={ 30 | 'console_scripts': [ 31 | 'webkin=webkin.webkin:main', 32 | ], 33 | }, 34 | classifiers=[ 35 | 'Development Status :: 5 - Production/Stable', 36 | 'License :: OSI Approved :: MIT License', 37 | 'Operating System :: POSIX', 38 | 'Intended Audience :: Developers', 39 | 'Intended Audience :: End Users/Desktop', 40 | 'Natural Language :: English', 41 | 'Operating System :: OS Independent', 42 | 'Programming Language :: Python :: 3 :: Only', 43 | 'Programming Language :: Python :: 3.3', 44 | 'Programming Language :: Python :: 3.4', 45 | 'Programming Language :: Python :: 3.5', 46 | 'Topic :: Software Development :: Libraries', 47 | 'Topic :: Software Development :: Libraries :: Python Modules', 48 | ], 49 | ) 50 | -------------------------------------------------------------------------------- /webkin/send_email.py: -------------------------------------------------------------------------------- 1 | from email import encoders 2 | from email.mime.multipart import MIMEMultipart 3 | from email.mime.base import MIMEBase 4 | from email.mime.text import MIMEText 5 | from .config import KINDLE_EMAIL,AMAZON_EMAIL,SMTP_HOST_NAME,SMTP_PORT,EMAIL_PASSWORD 6 | from .scaffold import * 7 | from sys import exit 8 | 9 | import smtplib 10 | 11 | 12 | def setup_email(SMTP_PORT=587): 13 | ''' 14 | 15 | :param SMTP_PORT: Default port used by GMail is 587 for non SSL. User can set a custom port. 16 | :return: SMTP Object to send email. 17 | 18 | ''' 19 | try: 20 | smtpObj = smtplib.SMTP(SMTP_HOST_NAME, SMTP_PORT) 21 | if smtpObj.ehlo()[0]==250: 22 | try: 23 | smtpObj.starttls() 24 | log.info('Connection successful... Attempting to login...') 25 | smtpObj.login(AMAZON_EMAIL, EMAIL_PASSWORD) 26 | return smtpObj 27 | 28 | except smtplib.SMTPAuthenticationError: 29 | log.error('Please Check Authentication details') 30 | exit() 31 | except: 32 | log.error('Nodename nor servername not identified.') 33 | exit() 34 | 35 | def send_email(mobi_file): 36 | ''' 37 | 38 | :param mobi_file: path to mobi file generated by `ebook-convert` 39 | ''' 40 | 41 | log.info('Attempting to establish connection with SMTP server : {} {}'.format(SMTP_HOST_NAME,SMTP_PORT)) 42 | smtpObj = setup_email(SMTP_PORT) 43 | msg = MIMEMultipart() 44 | msg['From'] = AMAZON_EMAIL 45 | msg['To'] = KINDLE_EMAIL 46 | # As suggested here https://www.amazon.com/gp/help/customer/display.html?nodeId=201974220 47 | msg['Subject'] = "Convert" 48 | body = "" 49 | msg.attach(MIMEText(body, 'plain')) 50 | attachment = open(mobi_file, "rb") 51 | part = MIMEBase('application', 'octet-stream') 52 | part.set_payload((attachment).read()) 53 | encoders.encode_base64(part) 54 | part.add_header('Content-Disposition', "attachment; filename= %s" % mobi_file) 55 | msg.attach(part) 56 | text = msg.as_string() 57 | 58 | try: 59 | log.info('Sending mail') 60 | smtpObj.sendmail(AMAZON_EMAIL, KINDLE_EMAIL, text) 61 | except: 62 | log.info('Cannot sent email.') 63 | exit() 64 | 65 | log.info('Sent') 66 | log.info('Quitting SMTP Connection') 67 | return smtpObj.quit() 68 | 69 | -------------------------------------------------------------------------------- /webkin/scaffold.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from os import getenv 3 | 4 | __all__ = ['log', 'check_for_tokens'] 5 | 6 | logging.basicConfig(level=logging.INFO, 7 | format='%(levelname)s: %(asctime)s -' 8 | ' %(funcName)s - %(message)s') 9 | 10 | log = logging.getLogger('webkin') 11 | 12 | 13 | def check_for_tokens(): 14 | log.debug('Checking for tokens') 15 | KINDLE_EMAIL = getenv('KINDLE_EMAIL') 16 | AMAZON_EMAIL = getenv('AMAZON_EMAIL') 17 | 18 | EMAIL_PASSWORD = getenv('EMAIL_PASSWORD') 19 | SMTP_HOST_NAME = getenv('SMTP_HOST_NAME') 20 | 21 | # DEFAULT SMTP PORT IS 587 for non SSL in GMail, SET YOURS HERE TO CHANGE 22 | 23 | SMTP_PORT = getenv('SMTP_PORT',587) 24 | MERCURY_API_KEY = getenv('MERCURY_API_KEY') 25 | log.debug("Tokens fetched: {} {} {} {} {} ".format(KINDLE_EMAIL, AMAZON_EMAIL, 26 | EMAIL_PASSWORD,SMTP_HOST_NAME,SMTP_PORT)) 27 | 28 | if KINDLE_EMAIL is None or AMAZON_EMAIL is None: 29 | print(''' 30 | You need to add your Kindle Verified Email Address, \n 31 | along with Amazon EMail ID. 32 | 33 | export KINDLE_EMAIL='your-kindle-email' 34 | export AMAZON_EMAIL='your-email-id' 35 | 36 | Ensure that your email address has been added to your Approved Personal Document Email List. \n 37 | You can check add it here : https://www.amazon.com/gp/help/customer/display.html?nodeId=201974220 38 | ''') 39 | return False 40 | 41 | if EMAIL_PASSWORD is None or SMTP_HOST_NAME is None: 42 | print(''' 43 | You need to add your email password along with SMTP Host Name, \n 44 | 45 | export EMAIL_PASSWORD='your-email-password' or 'your-application-password' 46 | export SMTP_HOST_NAME='your-smtp-host-name-' 47 | 48 | NOTE: If you're using GMail and have turned on 2FA, you need to put in Application Password 49 | Set a new application password here : https://security.google.com/settings/security/apppasswords 50 | ''') 51 | return False 52 | 53 | log.debug("Mercury API Key: {}".format(MERCURY_API_KEY)) 54 | if MERCURY_API_KEY is None: 55 | print(''' 56 | Please set up a new API token for Mercury Web Parser API. \n You can do this by 57 | setting environment variables like so: 58 | export MERCURY_API_KEY='your-mercury-api-key' 59 | Generate the key from 60 | https://mercury.postlight.com/web-parser/ 61 | ''') 62 | return False 63 | return True 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webkin 2 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/mr-karan/webkin/master/LICENSE) 3 | [![saythanks](https://img.shields.io/badge/say-thanks-ff69b4.svg)](https://saythanks.io/to/mr-karan) 4 | 5 | webkin lets you send webpages to your kindle device through terminal. 6 | 7 | Note : Python3+ only. 8 | 9 | ### Installation : 10 | 11 | pip install webkin 12 | 13 | [![asciicast](https://asciinema.org/a/101549.png)](https://asciinema.org/a/101549) 14 | 15 | ### Avilable commands : 16 | 17 | #### To parse URL and send it to your kindle email id. 18 | 19 | webkin --url 20 | 21 | #### To change the default directory. 22 | 23 | webkin --url= --path= 24 | 25 | Example : 26 | 27 | webkin -u=https://medium.com/@mrkaran/my-development-setup-7e767d33fc41 --verbose` 28 | 29 | ![img](http://i.imgur.com/aeIzhPQ.jpg) 30 | 31 | ### Pre Installation 32 | 33 | `webkin` depends on calibre CLI tools and uses `ebook-convert` to convert `html` to `mobi` format. Please ensure that you have Calibre installed alongwith CLI tools and `ebook-convert` is present in your PATH. 34 | 35 | For OSX users, you don't need to do anything besides [installing Calibre](http://calibre-ebook.com/download_osx). 36 | I have tested it on Ubuntu 16.04 fresh VM and after installing Calibre, it worked fine. If you install using [this](http://calibre-ebook.com/download_linux) method, you need to manually add `ebook-convert` to your path, while if you install it from PPA, it's automatically in your PATH. 37 | 38 | 39 | sudo add-apt-repository ppa:n-muench/calibre2 40 | sudo apt-get update 41 | sudo apt-get install calibre 42 | 43 | [Source](http://askubuntu.com/questions/338172/how-to-install-calibre-on-ubuntu-12-04) 44 | 45 | ### First Time Setup 46 | You need to export tokens to add your `Amazon Email Address` (should be present in your [Approved Personal Document Email List](https://www.amazon.com/gp/help/customer/display.html?nodeId=2019742) ), `Kindle Email Address`, `Mercury Web Parser API Key` ,`SMTP Hostname and SMTP Port`. The first time setup will guide you on how to do that. 47 | 48 | - To obtain your Mercury Web API key, signup [here](https://mercury.postlight.com/web-parser/) 49 | - If you're using GMail, you need to add `smtp.gmail.com` as your `SMTP_Host_NAME` while the default port is `587` so you can skip adding that. If you're using any other email provider, you can find a comprehensive list over [here](https://www.arclab.com/en/kb/email/list-of-smtp-and-pop3-servers-mailserver-list.html) and add accordingly 50 | - If you're using GMail and have turned on 2FA (which you must absolutely), you rather need to add an Application Password instead of your email password. Set a new one over [here](https://security.google.com/settings/security/apppasswords) 51 | 52 | ## Credits 53 | 54 | - [calibre](http://calibre-ebook.com/) 55 | - [Mercury Web Parser](https://mercury.postlight.com/web-parser/) 56 | - [@sathyabhat](https://github.com/SathyaBhat/spotify-dl/blob/master/spotify_dl/scaffold.py) for his clean implementation of fetching tokens in a CLI program, which I have shamlelessly adapted for webkin. 57 | 58 | ## Contributing 59 | 60 | Feel free to report any issues and/or send PRs for additional features. 61 | 62 | ## Why? 63 | 64 | Well, there are a couple of tools to already do this task, but I couldn't find any Open source tool which does it. Though Kindle uses a `MOBI` format which itself is closed source, I found the need of a CLI application to automate this boring task for me. If you're looking for a tool to do this but don't wanna use a terminal, you can also take a look at [this](https://chrome.google.com/webstore/detail/send-to-kindle-for-google/cgdjpilhipecahhcilnafpblkieebhea?hl=en) chrome extension. I like my stuff in the Terminal so I did it :) 65 | 66 | ### License 67 | 68 | MIT © Karan Sharma 69 | [LICENSE included here](LICENSE) 70 | -------------------------------------------------------------------------------- /webkin/webkin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | 4 | from .config import MERCURY_API_KEY 5 | from .scaffold import * 6 | from .send_email import send_email 7 | 8 | from PIL import Image 9 | from shlex import split, quote 10 | from sys import platform as _platform, exit 11 | from unicodedata import normalize 12 | from logging import DEBUG 13 | 14 | import os 15 | import requests 16 | import subprocess 17 | import argparse 18 | 19 | parser = argparse.ArgumentParser(prog='webkin') 20 | parser.add_argument('-u', '--url', action='store', type=str, 21 | help='Specify a url to parse', required=True) 22 | parser.add_argument('-V', '--verbose', action='store_true', 23 | help='Show more information on what''s happening.') 24 | parser.add_argument('-p', '--path', metavar="path", type=str, 25 | help="Path where your files will be generated") 26 | 27 | args = parser.parse_args() 28 | 29 | if args.verbose: 30 | log.setLevel(DEBUG) 31 | 32 | 33 | def main(): 34 | parse_url(url=args.url) 35 | 36 | 37 | def parse_url(url): 38 | directory = args.path or os.path.abspath(os.path.expanduser(os.curdir)) 39 | try: 40 | os.makedirs(directory, exist_ok=True) 41 | except OSError as e: 42 | log.exception('Cannot create directory: {}'.format(e)) 43 | exit() 44 | 45 | log.debug('Parsing {}'.format(url)) 46 | parse_url = 'https://mercury.postlight.com/parser?url=' + url 47 | data = requests.get(parse_url, headers={ 48 | 'x-api-key': MERCURY_API_KEY}).json() 49 | if data is None: 50 | log.info("Couldn't parse the webpage.") 51 | exit() 52 | return convert_html(data, directory) 53 | 54 | 55 | def convert_html(data, directory): 56 | command = '' 57 | try: 58 | title = normalize('NFKD', data['title']) 59 | except: 60 | title = 'webpage' 61 | filename = os.path.join(directory, title.replace(" ", "_")) 62 | html_file = filename + ".html" 63 | mobi_file = filename + ".mobi" 64 | img_file = filename + ".png" 65 | 66 | command += ' --title {} '.format(quote(title)) 67 | 68 | if data['lead_image_url'] is not None: 69 | 70 | img_url = data['lead_image_url'] 71 | try: 72 | im = Image.open(requests.get(img_url, stream=True).raw).resize( 73 | (1200, 1600),).save('{}'.format(img_file)) 74 | command += ' --cover {} '.format(img_file) 75 | 76 | except: 77 | pass 78 | 79 | if data['excerpt'] is not None: 80 | excerpt = normalize('NFKD', data['excerpt']) 81 | command += ' --comments {} --output-profile kindle'.format( 82 | quote(excerpt)) 83 | 84 | with open(os.path.join(directory, html_file), 'w') as template: 85 | clean_html = normalize('NFKD', data['content']) 86 | template.write(clean_html) 87 | 88 | if _platform == "darwin": 89 | try: 90 | build_line = ' ./ebook-convert ' + html_file + " " + mobi_file + command 91 | args = split(build_line) 92 | subprocess.call( 93 | args, cwd='/Applications/calibre.app/Contents/console.app/Contents/MacOS/') 94 | except: 95 | log.error('Please check if calibre CLI tools are installed.') 96 | exit() 97 | else: 98 | try: 99 | build_line = 'ebook-convert ' + html_file + " " + mobi_file + command 100 | args = split(build_line) 101 | subprocess.call(args) 102 | except: 103 | log.error('Please check if ebook-convert is added in your path.') 104 | exit() 105 | 106 | return send_email(mobi_file) 107 | 108 | 109 | if __name__ == '__main__': 110 | log.info('Initiating Web-Kindle...') 111 | log.debug('Initiating Web-Kindle with DEBUG mode') 112 | if not check_for_tokens(): 113 | exit() 114 | exit(main()) 115 | --------------------------------------------------------------------------------