├── tox.ini ├── .mypy.ini ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── questions-others-template.md │ └── bug_report.md └── workflows │ ├── install_calibre.sh │ └── build.yml ├── requirements.txt ├── static ├── opds_custom.scss ├── site_custom.scss ├── colours_custom.scss ├── reader_custom.scss ├── viewer-theme_custom.scss ├── OpenSans-Bold.ttf ├── OpenSans-Regular.ttf ├── OpenSans-Semibold.ttf ├── viewer-theme.scss ├── viewer-theme-light.scss ├── favicon.svg ├── colours.scss ├── viewer-theme-dark.scss ├── opds.scss ├── theme.js ├── index.html ├── reader_sprites.svg ├── reader.scss ├── reader.html └── opds.xsl ├── tests ├── __init__.py └── tests_recipe_utils.py ├── recipes ├── logos │ ├── knowable.png │ └── thirdpole.png ├── README.txt ├── joongangdaily.recipe.py ├── quanta-magazine.recipe.py ├── vox.recipe.py ├── politico-magazine.recipe.py ├── taipei-times.recipe.py ├── nytimes-books.recipe.py ├── knowable-magazine.recipe.py ├── mit-press-reader.recipe.py ├── asian-review.recipe.py ├── mollywhite-newsletter.recipe.py ├── channelnewsasia.recipe.py ├── korea-herald.recipe.py ├── restofworld.recipe.py ├── natesilver.recipe.py ├── bookforum-magazine.recipe.py ├── nine-dashline.recipe.py ├── nytimes-global.recipe.py ├── propublica.recipe.py ├── aeon.recipe.py ├── smithsonian-magazine.recipe.py ├── nautilus.recipe.py ├── paris-review-blog.recipe.py ├── world-today.recipe.py ├── harvard-intl-review.recipe.py ├── poetry.recipe.py ├── fivethirtyeight.recipe.py ├── fivebooks.recipe.py ├── lithub.recipe.py ├── kirkus.recipe.py ├── sydney-morning-herald.recipe.py ├── time-magazine.recipe.py ├── mit-tech-review.recipe.py ├── foreign-policy.recipe.py ├── longreads-features.recipe.py ├── thediplomat.recipe.py ├── forbes-editors-picks.recipe.py ├── noema-magazine.recipe.py ├── japan-times.recipe.py └── thirdpole.recipe.py ├── recipes_custom └── README.txt ├── babel.config.json ├── .flake8 ├── package.json ├── _recipes_custom.py ├── debug.sh ├── .gitignore ├── _opds.py └── README.md /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501,E722,E203,W503 3 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://buymeacoffee.com/ping/'] 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | humanize 3 | Pillow 4 | bleach 5 | -------------------------------------------------------------------------------- /static/opds_custom.scss: -------------------------------------------------------------------------------- 1 | // Define your own custom opds css here 2 | -------------------------------------------------------------------------------- /static/site_custom.scss: -------------------------------------------------------------------------------- 1 | // Define your own custom site css here 2 | -------------------------------------------------------------------------------- /static/colours_custom.scss: -------------------------------------------------------------------------------- 1 | // Define your own custom colours css here 2 | -------------------------------------------------------------------------------- /static/reader_custom.scss: -------------------------------------------------------------------------------- 1 | // Define your own custom reader viewer css here 2 | -------------------------------------------------------------------------------- /static/viewer-theme_custom.scss: -------------------------------------------------------------------------------- 1 | // Define your own custom viewer theme css here 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from .tests_recipe_utils import RecipeUtilsTests 3 | -------------------------------------------------------------------------------- /static/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ping/newsrack/HEAD/static/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /recipes/logos/knowable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ping/newsrack/HEAD/recipes/logos/knowable.png -------------------------------------------------------------------------------- /recipes/logos/thirdpole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ping/newsrack/HEAD/recipes/logos/thirdpole.png -------------------------------------------------------------------------------- /static/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ping/newsrack/HEAD/static/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /static/OpenSans-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ping/newsrack/HEAD/static/OpenSans-Semibold.ttf -------------------------------------------------------------------------------- /recipes_custom/README.txt: -------------------------------------------------------------------------------- 1 | Place your custom recipes (*.recipe.py or *.recipe) source files in this folder 2 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "minify" 5 | ], 6 | "comments": false 7 | } -------------------------------------------------------------------------------- /recipes/README.txt: -------------------------------------------------------------------------------- 1 | This folder holds the default recipes distributed with newsrack. 2 | 3 | Do not add your custom recipes to this folder. 4 | Put them in recipes_custom/ instead. 5 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | #max-line-length = 120 3 | extend-ignore = 4 | # let black determine line length 5 | E501, 6 | # ref https://github.com/psf/black/issues/1437 7 | E203 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@babel/cli": "^7.21.0", 4 | "@babel/core": "^7.21.3", 5 | "@babel/preset-env": "^7.20.2", 6 | "babel-preset-minify": "^0.5.2", 7 | "html-minifier-terser": "^7.1.0", 8 | "lunr": "^2.3.9", 9 | "sass": "^1.60.0", 10 | "terser": "^5.16.6" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a feature 4 | title: "[FEATURE] " 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please fill in the required information below: 11 | 12 | **Describe the Feature Request:** 13 | 14 | *Please make sure the description is worded well enough to be understood with as much context and examples as possible.* 15 | 16 | **Newsrack repository URL:** 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/questions-others-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Questions/Others 3 | about: Questions or other issues (not bug/feature request) 4 | title: "[OTHERS] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please fill in the required information below: 11 | 12 | **Describe the Issue:** 13 | 14 | *Please make sure the description is worded well enough to be understood with as much context and examples as possible.* 15 | 16 | **Newsrack repository URL:** 17 | -------------------------------------------------------------------------------- /_recipes_custom.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from _recipe_utils import Recipe 4 | 5 | # Define the categories display order, optional 6 | categories_sort: List[str] = [] 7 | 8 | # Define your custom recipes list here 9 | # Example: https://github.com/ping/newsrack-fork-test/blob/custom/_recipes_custom.py 10 | 11 | recipes: List[Recipe] = [ 12 | # Recipe( 13 | # recipe="example", 14 | # slug="example", 15 | # src_ext="epub", 16 | # category="example", 17 | # ), 18 | ] 19 | -------------------------------------------------------------------------------- /static/viewer-theme.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 https://github.com/ping/ 3 | 4 | This software is released under the GNU General Public License v3.0 5 | https://opensource.org/licenses/GPL-3.0 6 | */ 7 | 8 | @import 'colours'; 9 | 10 | .viewer-theme { 11 | font-family: Charter, 'Bitstream Charter', 'Sitka Text', Cambria, serif; 12 | line-height: 1.4; 13 | text-align: left; 14 | 15 | table.touchscreen_navbar { 16 | &:first-child { 17 | margin-bottom: 0.5rem; 18 | } 19 | } 20 | } 21 | 22 | @import 'viewer-theme_custom'; 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a problem 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please fill in the required information below: 11 | 12 | **Describe the Bug/Error:** 13 | 14 | *Please make sure the description is worded well enough to be understood with as much context and examples as possible.* 15 | 16 | **Newsrack repository URL:** 17 | 18 | **Error log (if relevant):** 19 | 20 | GitHub Action run url: 21 | 22 | ```text 23 | # paste error log here, do not use screenshots 24 | ``` 25 | -------------------------------------------------------------------------------- /static/viewer-theme-light.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 https://github.com/ping/ 3 | 4 | This software is released under the GNU General Public License v3.0 5 | https://opensource.org/licenses/GPL-3.0 6 | */ 7 | 8 | @import 'viewer-theme'; 9 | 10 | .viewer-theme { 11 | background-color: $base-bg-color; 12 | color: $base-color; 13 | 14 | a { 15 | color: $link-color; 16 | 17 | &:hover { 18 | color: $link-hover-color; 19 | } 20 | 21 | &:visited { 22 | color: $link-visited-color; 23 | } 24 | } 25 | 26 | table.touchscreen_navbar { 27 | background-color: $base-disabled-color; 28 | 29 | td { 30 | color: $base-color; 31 | background-color: $base-bg-color; 32 | 33 | a { 34 | color: $link-color; 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /static/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /debug.sh: -------------------------------------------------------------------------------- 1 | # helper script for debuging/developing new recipes 2 | if [ -z "$1" ]; 3 | then 4 | echo "No recipe specified." 5 | echo "Usage: sh debug.sh example" 6 | exit 9 7 | fi 8 | 9 | get_abs_dirname() { 10 | # $1 : relative filename 11 | echo "$(cd "$(dirname "$1")" && pwd)/" 12 | } 13 | # use to get shared code 14 | export recipes_includes=$(get_abs_dirname "recipes/includes/recipes_shared.py") 15 | 16 | for recipe_folder in 'recipes' 'recipes_custom' 17 | do 18 | if [ -f "$recipe_folder/$1.recipe.py" ]; then 19 | cp -p "$recipe_folder/$1.recipe.py" "$1.recipe" 20 | fi 21 | if [ -f "$recipe_folder/$1.recipe" ]; then 22 | cp -p "$recipe_folder/$1.recipe" "$1.recipe" 23 | fi 24 | done 25 | 26 | rm -rf debug 27 | ebook-convert "$1.recipe" .epub --test --debug-pipeline debug -vv && \ 28 | open debug/input/index.html 29 | 30 | if [ -f "$1.recipe" ]; then 31 | rm -f "$1.recipe" 32 | fi 33 | -------------------------------------------------------------------------------- /static/colours.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 https://github.com/ping/ 3 | 4 | This software is released under the GNU General Public License v3.0 5 | https://opensource.org/licenses/GPL-3.0 6 | */ 7 | 8 | $base-bg-color: #ffffff; 9 | $base-color: #000000; 10 | $base-disabled-color: #444956; 11 | $link-color: #0000e6; 12 | $link-hover-color: darken($link-color, 20%); 13 | $link-visited-color: #551a8a; 14 | $book-bg-color: #eaeaea; 15 | $error-color: #b90404; 16 | $tags-color: mix($base-disabled-color, $base-color); 17 | 18 | $dark-base-bg-color: #1e2228; 19 | $dark-base-color: #e7e9f2; 20 | $dark-base-disabled-color: #a2a5af; 21 | $dark-link-color: #5ccaec; 22 | $dark-link-hover-color: lighten($dark-link-color, 20%); 23 | $dark-link-visited-color: #bbb9ff; 24 | $dark-book-bg-color: lighten($dark-base-bg-color, 5%); 25 | $dark-error-color: #ff6464; 26 | $dark-tags-color: mix($dark-base-disabled-color, $dark-base-color); 27 | 28 | @import 'colours_custom'; 29 | 30 | @function url-friendly-colour($colour) { 31 | @return '%23' + str-slice('#{$colour}', 2, -1) 32 | } 33 | -------------------------------------------------------------------------------- /static/viewer-theme-dark.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2022 https://github.com/ping/ 3 | 4 | This software is released under the GNU General Public License v3.0 5 | https://opensource.org/licenses/GPL-3.0 6 | */ 7 | 8 | @import 'viewer-theme'; 9 | 10 | .viewer-theme { 11 | background-color: $dark-base-bg-color; 12 | color: $dark-base-color; 13 | 14 | // We overwrite the colour because the epub often assumes a light theme 15 | // and this can result in issues when a dark theme is applied. 16 | // This forcibly sets everything to the dark theme's base colour. 17 | * { 18 | color: $dark-base-color; 19 | } 20 | 21 | a { 22 | color: $dark-link-color; 23 | 24 | // These unsets are due to the styling generated by calibre when converting from mobi. 25 | // Without the unsets, the elements inherit from the *{} defined just above. 26 | * { 27 | color: unset; 28 | } 29 | 30 | &:hover { 31 | color: $dark-link-hover-color; 32 | 33 | * { 34 | color: unset; 35 | } 36 | } 37 | 38 | &:visited { 39 | color: $dark-link-visited-color; 40 | 41 | * { 42 | color: unset; 43 | } 44 | } 45 | } 46 | 47 | table.touchscreen_navbar { 48 | background-color: $dark-base-disabled-color; 49 | 50 | td { 51 | color: $dark-base-color; 52 | background-color: $dark-base-bg-color; 53 | 54 | a { 55 | color: $dark-link-color; 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | static/*.compiled.js 2 | 3 | .env 4 | meta/ 5 | *.recipe 6 | *.epub 7 | *.mobi 8 | public/ 9 | debug/ 10 | static/*.css 11 | job_summary.md 12 | 13 | # Node # 14 | # Dependency directories 15 | node_modules/ 16 | jspm_packages/ 17 | 18 | venv/ 19 | venv3/ 20 | .vscode/ 21 | 22 | coverage.sh 23 | cov_html/ 24 | 25 | *.iml 26 | .idea/ 27 | 28 | # OS generated files # 29 | .DS_Store 30 | .DS_Store? 31 | ._* 32 | .Spotlight-V100 33 | .Trashes 34 | ehthumbs.db 35 | Thumbs.db 36 | 37 | # Byte-compiled / optimized / DLL files 38 | __pycache__/ 39 | *.py[cod] 40 | *$py.class 41 | 42 | # C extensions 43 | *.so 44 | 45 | # Distribution / packaging 46 | .Python 47 | env/ 48 | build/ 49 | develop-eggs/ 50 | dist/ 51 | downloads/ 52 | eggs/ 53 | .eggs/ 54 | lib/ 55 | lib64/ 56 | parts/ 57 | sdist/ 58 | var/ 59 | *.egg-info/ 60 | .installed.cfg 61 | *.egg 62 | 63 | # PyInstaller 64 | # Usually these files are written by a python script from a template 65 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 66 | *.manifest 67 | *.spec 68 | 69 | # Installer logs 70 | pip-log.txt 71 | pip-delete-this-directory.txt 72 | 73 | # Unit test / coverage reports 74 | htmlcov/ 75 | .tox/ 76 | .coverage 77 | .coverage.* 78 | .cache 79 | nosetests.xml 80 | coverage.xml 81 | *,cover 82 | .hypothesis/ 83 | 84 | # Translations 85 | *.mo 86 | *.pot 87 | 88 | # Django stuff: 89 | *.log 90 | 91 | # Sphinx documentation 92 | docs/_build/ 93 | 94 | # PyBuilder 95 | target/ 96 | 97 | #Ipython Notebook 98 | .ipynb_checkpoints 99 | -------------------------------------------------------------------------------- /recipes/joongangdaily.recipe.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 https://github.com/ping/ 2 | # 3 | # This software is released under the GNU General Public License v3.0 4 | # https://opensource.org/licenses/GPL-3.0 5 | 6 | """ 7 | koreajoongangdaily.joins.com 8 | """ 9 | import os 10 | import sys 11 | 12 | # custom include to share code between recipes 13 | sys.path.append(os.environ["recipes_includes"]) 14 | from recipes_shared import BasicNewsrackRecipe, format_title 15 | 16 | from calibre.web.feeds.news import BasicNewsRecipe 17 | 18 | _name = "JoongAng Daily" 19 | 20 | 21 | class KoreaJoongAngDaily(BasicNewsrackRecipe, BasicNewsRecipe): 22 | title = _name 23 | description = "The Korea JoongAng Daily is an English-language daily published by the JoongAng Group, Korea’s leading media group, in association with The New York Times. https://koreajoongangdaily.joins.com/" 24 | language = "en" 25 | __author__ = "ping" 26 | publication_type = "newspaper" 27 | masthead_url = ( 28 | "https://koreajoongangdaily.joins.com/resources/images/common/logo.png" 29 | ) 30 | use_embedded_content = True 31 | auto_cleanup = True 32 | compress_news_images_auto_size = 10 33 | 34 | oldest_article = 1 # days 35 | max_articles_per_feed = 60 36 | 37 | extra_css = """ 38 | .caption { font-size: 0.8rem; margin: 0.5rem 0; } 39 | """ 40 | 41 | feeds = [ 42 | ("Korea JoongAng Daily", "https://koreajoongangdaily.joins.com/xmls/joins"), 43 | ] 44 | 45 | def populate_article_metadata(self, article, __, _): 46 | if (not self.pub_date) or article.utctime > self.pub_date: 47 | self.pub_date = article.utctime 48 | self.title = format_title(_name, article.utctime) 49 | 50 | def parse_feeds(self): 51 | return self.group_feeds_by_date(timezone_offset_hours=9) # Seoul time 52 | -------------------------------------------------------------------------------- /_opds.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 https://github.com/ping/ 2 | # 3 | # This software is released under the GNU General Public License v3.0 4 | # https://opensource.org/licenses/GPL-3.0 5 | 6 | # Helpers to generate opds xml - extremely minimal 7 | from datetime import datetime 8 | from typing import Dict, Optional 9 | from xml.dom import minidom 10 | 11 | extension_contenttype_map = { 12 | ".epub": "application/epub+zip", 13 | ".mobi": "application/x-mobipocket-ebook", 14 | ".azw": "application/x-mobipocket-ebook", 15 | ".azw3": "application/x-mobi8-ebook", 16 | ".pdf": "application/pdf", 17 | } 18 | 19 | 20 | def simple_tag( 21 | doc_root: minidom.Document, 22 | tag: str, 23 | value: Optional[str] = None, 24 | attributes: Optional[Dict] = None, 25 | ) -> minidom.Element: 26 | new_tag = doc_root.createElement(tag) 27 | if value: 28 | new_tag.appendChild(doc_root.createTextNode(value)) 29 | if attributes: 30 | for k, v in attributes.items(): 31 | new_tag.setAttribute(k, v) 32 | return new_tag 33 | 34 | 35 | def init_feed( 36 | doc: minidom.Document, publish_site: str, feed_id: str, title: str 37 | ) -> minidom.Element: 38 | feed = simple_tag( 39 | doc, 40 | "feed", 41 | attributes={ 42 | "xmlns": "http://www.w3.org/2005/Atom", 43 | "xmlns:dc": "http://purl.org/dc/terms/", 44 | "xmlns:opds": "http://opds-spec.org/2010/catalog", 45 | }, 46 | ) 47 | doc.appendChild(feed) 48 | feed.appendChild(simple_tag(doc, "id", feed_id)) 49 | feed.appendChild(simple_tag(doc, "title", title)) 50 | feed.appendChild(simple_tag(doc, "updated", f"{datetime.now():%Y-%m-%dT%H:%M:%SZ}")) 51 | feed_author = doc.createElement("author") 52 | feed_author.appendChild(simple_tag(doc, "name", publish_site)) 53 | feed_author.appendChild(simple_tag(doc, "uri", publish_site)) 54 | feed.appendChild(feed_author) 55 | return feed 56 | -------------------------------------------------------------------------------- /recipes/quanta-magazine.recipe.py: -------------------------------------------------------------------------------- 1 | # Original at https://raw.githubusercontent.com/kovidgoyal/calibre/1ca6887e6c9f83a05cafe1fba8bae6de9bd2773c/recipes/quanta_magazine.recipe 2 | 3 | import os 4 | import sys 5 | 6 | # custom include to share code between recipes 7 | sys.path.append(os.environ["recipes_includes"]) 8 | from recipes_shared import BasicNewsrackRecipe, format_title 9 | 10 | from calibre.web.feeds.news import BasicNewsRecipe 11 | 12 | _name = "Quanta Magazine" 13 | 14 | 15 | class QuantaMagazine(BasicNewsrackRecipe, BasicNewsRecipe): 16 | title = _name 17 | __author__ = "lui1" 18 | description = ( 19 | "Quanta Magazine is committed to in-depth, accurate journalism that " 20 | "serves the public interest. Each article braids the complexities of " 21 | "science with the malleable art of storytelling and is meticulously " 22 | "reported, edited and fact-checked. https://www.quantamagazine.org/" 23 | ) 24 | publication_type = "magazine" 25 | language = "en" 26 | masthead_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/1/1f/Quanta_Magazine_Logo_05.2022.svg/320px-Quanta_Magazine_Logo_05.2022.svg.png" 27 | 28 | oldest_article = 30 29 | max_articles_per_feed = 100 30 | 31 | keep_only_tags = [ 32 | dict(name="div", attrs={"id": "postBody"}), 33 | ] 34 | remove_tags = [ 35 | dict(name=["script", "noscript", "style", "svg", "form", "button"]), 36 | dict(class_=["post__title__actions", "post__sidebar__content", "video"]), 37 | ] 38 | 39 | extra_css = """ 40 | .component-img img { 41 | display: block; margin-bottom: 0.3rem; max-width: 100%; height: auto; 42 | box-sizing: border-box; 43 | } 44 | .caption, .attribution { font-size: 0.8rem; margin: 0; } 45 | """ 46 | 47 | feeds = [ 48 | (_name, "https://api.quantamagazine.org/feed/"), 49 | ] 50 | 51 | def populate_article_metadata(self, article, __, _): 52 | if (not self.pub_date) or article.utctime > self.pub_date: 53 | self.pub_date = article.utctime 54 | self.title = format_title(_name, article.utctime) 55 | 56 | def parse_feeds(self): 57 | return self.group_feeds_by_date() 58 | -------------------------------------------------------------------------------- /recipes/vox.recipe.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 https://github.com/ping/ 2 | # 3 | # This software is released under the GNU General Public License v3.0 4 | # https://opensource.org/licenses/GPL-3.0 5 | import os 6 | import sys 7 | 8 | # custom include to share code between recipes 9 | sys.path.append(os.environ["recipes_includes"]) 10 | from recipes_shared import BasicNewsrackRecipe, format_title, get_datetime_format 11 | 12 | from calibre.web.feeds.news import BasicNewsRecipe 13 | 14 | _name = "Vox" 15 | 16 | 17 | class Vox(BasicNewsrackRecipe, BasicNewsRecipe): 18 | title = _name 19 | language = "en" 20 | description = "General interest news site https://www.vox.com/" 21 | __author__ = "ping" 22 | publication_type = "magazine" 23 | masthead_url = "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a2/Vox_logo.svg/300px-Vox_logo.svg.png" 24 | oldest_article = 7 # days 25 | 26 | max_articles_per_feed = 25 27 | use_embedded_content = True 28 | scale_news_images = (600, 600) 29 | 30 | remove_attributes = ["style", "font"] 31 | 32 | feeds = [ 33 | ("Font Page", "https://www.vox.com/rss/front-page/index.xml"), 34 | ("All", "https://www.vox.com/rss/index.xml"), 35 | ] 36 | # e-image 37 | extra_css = """ 38 | h2 { font-size: 1.8rem; margin-bottom: 0.4rem; } 39 | .article-meta { padding-bottom: 0.5rem; } 40 | .article-meta .author { font-weight: bold; color: #444; margin-right: 0.5rem; } 41 | .e-image cite { display: block; } 42 | .e-image div, .e-image cite { font-size: 0.8rem; } 43 | """ 44 | 45 | def populate_article_metadata(self, article, __, _): 46 | if (not self.pub_date) or article.utctime > self.pub_date: 47 | self.pub_date = article.utctime 48 | self.title = format_title(_name, article.utctime) 49 | 50 | def parse_feeds(self): 51 | parsed_feeds = super().parse_feeds() 52 | for feed in parsed_feeds: 53 | for article in feed.articles: 54 | article.content = ( 55 | f""" 56 |
57 | {article.author} 58 | {article.utctime:{get_datetime_format()}} 59 |
60 | """ 61 | + article.content 62 | ) 63 | return parsed_feeds 64 | -------------------------------------------------------------------------------- /recipes/politico-magazine.recipe.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 https://github.com/ping/ 2 | # 3 | # This software is released under the GNU General Public License v3.0 4 | # https://opensource.org/licenses/GPL-3.0 5 | 6 | """ 7 | politico.com 8 | """ 9 | import os 10 | import sys 11 | 12 | # custom include to share code between recipes 13 | sys.path.append(os.environ["recipes_includes"]) 14 | from recipes_shared import BasicNewsrackRecipe, format_title 15 | 16 | from calibre.web.feeds.news import BasicNewsRecipe 17 | 18 | _name = "POLITICO Magazine" 19 | 20 | 21 | class PoliticoMagazine(BasicNewsrackRecipe, BasicNewsRecipe): 22 | title = _name 23 | __author__ = "ping" 24 | description = "News, Analysis and Opinion from POLITICO https://www.politico.com/" 25 | publisher = "Capitol News Company, LLC" 26 | category = "news, politics, USA" 27 | publication_type = "magazine" 28 | language = "en" 29 | masthead_url = "https://www.politico.com/dims4/default/bbb0fd2/2147483647/resize/1160x%3E/quality/90/?url=https%3A%2F%2Fstatic.politico.com%2F0e%2F5b%2F3cf3e0f04ca58370112ab667c255%2Fpolitico-logo.png" 30 | 31 | oldest_article = 7 32 | max_articles_per_feed = 25 33 | 34 | keep_only_tags = [dict(name=["main"])] 35 | remove_tags = [ 36 | dict( 37 | class_=[ 38 | "story-section", 39 | "social-tools", 40 | "below-article-section", 41 | "pop-up-bar", 42 | "inline-super-footer", 43 | ] 44 | ), 45 | dict(id=["weekend-promo"]), 46 | dict(name=["source"]), 47 | ] 48 | 49 | extra_css = """ 50 | .media-item__summary h2.headline { font-size: 1.8rem; margin-bottom: 0.4rem; } 51 | .media-item__summary p.dek { font-size: 1.2rem; font-style: italic; margin-bottom: 1rem; margin-top: 0; } 52 | .fig-graphic img, .story-photo__image img { max-width: 100%; height: auto; } 53 | .story-meta__credit, .story-photo__caption { font-size: 0.8rem; margin-top: 0.2rem; } 54 | """ 55 | 56 | feeds = [("Magazine", "https://rss.politico.com/magazine.xml")] 57 | 58 | def populate_article_metadata(self, article, __, _): 59 | if (not self.pub_date) or article.utctime > self.pub_date: 60 | self.pub_date = article.utctime 61 | self.title = format_title(_name, article.utctime) 62 | 63 | def parse_feeds(self): 64 | return self.group_feeds_by_date() 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # newsrack 2 | 3 | Generate an online "newsrack" of periodicals for your ereader. 4 | 5 | Features: 6 | - Download anywhere using your device browser 7 | - Subscribe via OPDS feeds 8 | 9 | Uses [calibre](https://calibre-ebook.com/) + [recipes](https://manual.calibre-ebook.com/news_recipe.html), [GitHub Actions](.github/workflows/build.yml), and hosted 10 | on [GitHub Pages](https://pages.github.com/). 11 | 12 | ![eInk Kindle Screenshot](https://github.com/ping/newsrack/assets/104607/475daa53-f2d5-4469-b88e-7d5463399d73) 13 | ![Mobile Screenshot](https://github.com/ping/newsrack/assets/104607/76ec3514-8d89-43bc-a68c-909df42971cb) 14 | 15 | [![Buy me a coffee](https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20coffee&emoji=&slug=ping&button_colour=FFDD00&font_colour=000000&font_family=Bree&outline_colour=000000&coffee_colour=ffffff)](https://www.buymeacoffee.com/ping) 16 | 17 | ## Running Your Own Instance 18 | 19 | ### General Steps 20 | 21 | 1. Fork this repository. 22 | 2. Create a new branch, for example `custom`. Using a new branch makes a few things, like contributing fixes for example, easier. 23 | 3. Add your own recipes to the [`recipes_custom/`](recipes_custom) folder and customise [_recipes_custom.py](_recipes_custom.py). Optional. 24 | 4. Customise the cron schedule and job run time in [.github/workflows/build.yml](.github/workflows/build.yml). Optional. 25 | 5. Set the new branch `custom` as default 26 | - from Settings > Branches > Default branch 27 | 6. Enable Pages in repository settings to deploy from `GitHub Actions` 28 | - from Settings > Pages > Build and deployment > Source 29 | 7. If needed, manually trigger the `Build` workflow from Actions to start your first build. 30 | 31 | ### What Can Be Customised 32 | 33 | `newsrack` supports extensive customisation such as: 34 | - add/remove recipes 35 | - the formats generated 36 | - when recipes are executed 37 | - cover colours and fonts 38 | 39 | Review the [wiki](https://github.com/ping/newsrack/wiki#customisation) page to understand what can be customised and how to do so. 40 | 41 | You can also refer to the [example fork repo](https://github.com/ping/newsrack-fork-test/) and see the [actual customisations](https://github.com/ping/newsrack-fork-test/compare/main...custom#files_bucket) in action. 42 | 43 | 44 | ## Available Recipes 45 | 46 | `newsrack` has its own set of customised recipes. The full list of available recipes can be viewed on [here](https://github.com/ping/newsrack/wiki/Available-Recipes). 47 | -------------------------------------------------------------------------------- /.github/workflows/install_calibre.sh: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------- 2 | # Install script for calibre 3 | # ------------------------------------------------------- 4 | 5 | bin_folder="$GITHUB_WORKSPACE/cache/calibre" 6 | mkdir -p "$bin_folder" 7 | platform='x86_64' 8 | bin_file="calibre-${platform}.txz" 9 | sig_file="calibre-${platform}.txz.sha512" 10 | 11 | if [ -f "${bin_folder}/${bin_file}" ]; then 12 | echo "Cached $bin_file exists." 13 | else 14 | echo "Cached $bin_file does not exist." 15 | rm -rf "${bin_folder}/calibre-*" 16 | tag="$(curl -L --retry 3 --show-error --silent --fail 'https://api.github.com/repos/kovidgoyal/calibre/releases/latest' | jq -r .tag_name)" && \ 17 | latest_version="${tag#*v}" && \ 18 | echo "Latest version: ${latest_version}" && \ 19 | dl_url="https://github.com/kovidgoyal/calibre/releases/download/${tag}/calibre-${latest_version}-${platform}.txz" && \ 20 | sig_url="https://calibre-ebook.com/signatures/calibre-${latest_version}-${platform}.txz.sha512" && \ 21 | sig2_url="https://code.calibre-ebook.com/signatures/calibre-${latest_version}-${platform}.txz.sha512" && \ 22 | { echo "Downloading sig $sig_url ..." && curl -L --retry 3 --show-error --silent --fail --output "${bin_folder}/${sig_file}" "$sig_url" || \ 23 | echo "Downloading sig $sig2_url ..." && curl -L --retry 3 --show-error --insecure --fail --silent --output "${bin_folder}/${sig_file}" "$sig2_url"; } && \ 24 | echo "Downloading bin $dl_url ..." 25 | curl -L --retry 3 --show-error --silent --fail --output "${bin_folder}/${bin_file}.part" "$dl_url" && \ 26 | echo "$(cat "${bin_folder}/${sig_file}") ${bin_folder}/${bin_file}.part" | sha512sum --check --status && \ 27 | mv "${bin_folder}/${bin_file}.part" "${bin_folder}/${bin_file}" 28 | fi 29 | 30 | if [ -f "${bin_folder}/${bin_file}" ]; then 31 | echo "Install from local..." 32 | mkdir -p "$HOME/calibre-bin/calibre" && \ 33 | tar xf "${bin_folder}/${bin_file}" -C "$HOME/calibre-bin/calibre" && \ 34 | "$HOME/calibre-bin/calibre/calibre_postinstall" && \ 35 | export PATH=$PATH:$HOME/calibre-bin/calibre && \ 36 | calibre --version && \ 37 | echo "$HOME/calibre-bin/calibre" >> $GITHUB_PATH 38 | fi 39 | 40 | calibre --version || { 41 | echo "Install latest from calibre servers..." 42 | mkdir -p ~/calibre-bin 43 | wget --tries=3 --timeout=30 -nv -O- https://download.calibre-ebook.com/linux-installer.sh | sh /dev/stdin install_dir=~/calibre-bin isolated=y 44 | export PATH=$PATH:$HOME/calibre-bin/calibre 45 | calibre --version 46 | echo "$HOME/calibre-bin/calibre" >> $GITHUB_PATH 47 | } 48 | -------------------------------------------------------------------------------- /recipes/taipei-times.recipe.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 https://github.com/ping/ 2 | # 3 | # This software is released under the GNU General Public License v3.0 4 | # https://opensource.org/licenses/GPL-3.0 5 | import os 6 | import sys 7 | from datetime import timezone, timedelta 8 | 9 | # custom include to share code between recipes 10 | sys.path.append(os.environ["recipes_includes"]) 11 | from recipes_shared import BasicNewsrackRecipe, format_title 12 | 13 | from calibre.web.feeds.news import BasicNewsRecipe 14 | 15 | _name = "Taipei Times" 16 | 17 | 18 | class TaipeiTimes(BasicNewsrackRecipe, BasicNewsRecipe): 19 | title = _name 20 | language = "en" 21 | __author__ = "ping" 22 | publication_type = "newspaper" 23 | description = "News from the Taipei Times https://www.taipeitimes.com/" 24 | masthead_url = "https://www.taipeitimes.com/assets/images/logo.gif" 25 | 26 | oldest_article = 1 # days 27 | max_articles_per_feed = 50 28 | ignore_duplicate_articles = {"title", "url"} 29 | 30 | keep_only_tags = [dict(name="div", class_="archives")] 31 | remove_tags = [dict(attrs={"class": ["ad_mg_t", "ad_mg_b", "sh"]})] 32 | 33 | extra_css = """ 34 | .archives h1 { font-size: 1.8rem; margin-bottom: 0.5rem; } 35 | .archives h2 { font-size: 1.2rem; margin-bottom: 0.5rem; font-weight: normal; font-style: italic; } 36 | p.byline { font-weight: bold; color: #444; display: block; margin-top: 1rem; } 37 | .imgboxa img { max-width: 100%; height: auto; } 38 | .imgboxa p { font-size: 0.8rem; margin-top: 0.2rem; display: inline-block; font-weight: normal; } 39 | """ 40 | 41 | feeds = [(_name, "https://www.taipeitimes.com/xml/index.rss")] 42 | 43 | def populate_article_metadata(self, article, _, __): 44 | if not self.pub_date or article.utctime > self.pub_date: 45 | self.pub_date = article.utctime 46 | post_date_local = article.utctime.astimezone(timezone(timedelta(hours=8))) 47 | self.title = format_title(_name, post_date_local) 48 | 49 | def preprocess_raw_html(self, raw_html, _): 50 | soup = self.soup(raw_html) 51 | 52 | # replace byline