├── .gitignore ├── README.md ├── app.py ├── db.py ├── feed.py ├── models ├── __init__.py ├── article.py └── source.py ├── routes └── __init__.py ├── run.py ├── static └── css │ └── index.css └── templates ├── index.html └── sources.html /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-feedreader 2 | Building an RSS feed reader in Python. 3 | 4 | This is supplementary code for this tutorial series: https://www.youtube.com/playlist?list=PLmxT2pVYo5LBcv5nYKTIn-fblphtD_OJO 5 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | app = Flask(__name__) 4 | db_uri = 'mysql+pymysql://root:password@localhost/feedreader' 5 | app.config['SQLALCHEMY_DATABASE_URI'] = db_uri -------------------------------------------------------------------------------- /db.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | from flask_sqlalchemy import SQLAlchemy 3 | db = SQLAlchemy(app) -------------------------------------------------------------------------------- /feed.py: -------------------------------------------------------------------------------- 1 | import feedparser 2 | 3 | def parse(url): 4 | return feedparser.parse(url) 5 | 6 | def get_source(parsed): 7 | feed = parsed['feed'] 8 | return { 9 | 'link': feed['link'], 10 | 'title': feed['title'], 11 | 'subtitle': feed['subtitle'], 12 | } 13 | 14 | def get_articles(parsed): 15 | articles = [] 16 | entries = parsed['entries'] 17 | for entry in entries: 18 | articles.append({ 19 | 'id': entry['id'], 20 | 'link': entry['link'], 21 | 'title': entry['title'], 22 | 'summary': entry['summary'], 23 | 'published': entry['published_parsed'], 24 | }) 25 | return articles -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-tutorials/python-feedreader/b910d211ba4de079a5a7878a7cf89fa37b727bc9/models/__init__.py -------------------------------------------------------------------------------- /models/article.py: -------------------------------------------------------------------------------- 1 | from db import db 2 | import datetime 3 | 4 | class Article(db.Model): 5 | id = db.Column(db.Integer, primary_key=True) 6 | title = db.Column(db.Text, nullable=False) 7 | body = db.Column(db.Text, nullable=False) 8 | link = db.Column(db.Text, nullable=False) 9 | guid = db.Column(db.String(255), nullable=False) 10 | unread = db.Column(db.Boolean, default=True, nullable=False) 11 | source_id = db.Column(db.Integer, db.ForeignKey('source.id'), nullable=False) 12 | source = db.relationship('Source', backref=db.backref('articles', lazy=True)) 13 | date_added = db.Column(db.DateTime, default=datetime.datetime.utcnow) 14 | date_published = db.Column(db.DateTime) 15 | __table_args__ = ( 16 | db.UniqueConstraint('source_id', 'guid', name='uc_source_guid'), 17 | ) 18 | 19 | @classmethod 20 | def insert_from_feed(cls, source_id, feed_articles): 21 | stmt = Article.__table__.insert().prefix_with('IGNORE') 22 | articles = [] 23 | for article in feed_articles: 24 | articles.append({ 25 | 'title': article['title'], 26 | 'body': article['summary'], 27 | 'link': article['link'], 28 | 'guid': article['id'], 29 | 'source_id': source_id, 30 | 'date_published': article['published'], 31 | }) 32 | db.engine.execute(stmt, articles) 33 | -------------------------------------------------------------------------------- /models/source.py: -------------------------------------------------------------------------------- 1 | from db import db 2 | import datetime 3 | 4 | class Source(db.Model): 5 | id = db.Column(db.Integer, primary_key=True) 6 | title = db.Column(db.Text, nullable=False) 7 | subtitle = db.Column(db.Text, nullable=False) 8 | link = db.Column(db.Text, nullable=False) 9 | feed = db.Column(db.Text, nullable=False) 10 | date_added = db.Column(db.DateTime, default=datetime.datetime.utcnow) 11 | 12 | @classmethod 13 | def insert_from_feed(cls, feed, feed_source): 14 | link = feed_source['link'] 15 | title = feed_source['title'] 16 | subtitle = feed_source['subtitle'] 17 | source = Source(feed=feed, link=link, title=title, subtitle=subtitle) 18 | db.session.add(source) 19 | db.session.commit() 20 | return source -------------------------------------------------------------------------------- /routes/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import abort, redirect, request, render_template 2 | from app import app 3 | from db import db 4 | from models.article import Article 5 | from models.source import Source 6 | import feed 7 | 8 | @app.route('/', methods=['GET']) 9 | def index_get(): 10 | query = Article.query 11 | query = query.filter(Article.unread == True) 12 | orderby = request.args.get('orderby', 'added') 13 | if orderby == 'added': 14 | query = query.order_by(Article.date_added.desc()) 15 | elif orderby == 'published': 16 | query = query.order_by(Article.date_published.desc()) 17 | elif orderby == 'title': 18 | query = query.order_by(Article.title) 19 | elif orderby == 'source': 20 | query = query.join(Source).order_by(Source.title) 21 | articles = query.all() 22 | return render_template('index.html', articles=articles) 23 | 24 | @app.route('/read/', methods=['GET']) 25 | def read_article_get(article_id): 26 | article = Article.query.get(article_id) 27 | article.unread = False 28 | db.session.commit() 29 | return redirect(article.link) 30 | 31 | @app.route('/sources', methods=['GET']) 32 | def sources_get(): 33 | query = Source.query 34 | query = query.order_by(Source.title) 35 | sources = query.all() 36 | return render_template('sources.html', sources=sources) 37 | 38 | @app.route('/sources', methods=['POST']) 39 | def sources_post(): 40 | feed_url = request.form['feed'] 41 | parsed = feed.parse(feed_url) 42 | feed_source = feed.get_source(parsed) 43 | source = Source.insert_from_feed(feed_url, feed_source) 44 | feed_articles = feed.get_articles(parsed) 45 | Article.insert_from_feed(source.id, feed_articles) 46 | return redirect('/sources') 47 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | from db import db 3 | from models import article, source 4 | import routes 5 | import feed 6 | from threading import Thread 7 | import time 8 | 9 | with app.app_context(): 10 | db.create_all() 11 | 12 | def update_loop(): 13 | while True: 14 | with app.app_context(): 15 | query = source.Source.query 16 | for src in query.all(): 17 | try: 18 | update_source(src) 19 | except: 20 | continue 21 | time.sleep(60 * 15) 22 | 23 | def update_source(src): 24 | parsed = feed.parse(src.feed) 25 | feed_articles = feed.get_articles(parsed) 26 | article.Article.insert_from_feed(src.id, feed_articles) 27 | print('Updated ' + src.feed) 28 | 29 | thread = Thread(target=update_loop) 30 | thread.start() 31 | 32 | app.run() 33 | -------------------------------------------------------------------------------- /static/css/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | color: #333; 8 | background: #f1f1f1; 9 | font-family: Helvetica, Arial, sans-serif; 10 | } 11 | 12 | main { 13 | box-sizing: border-box; 14 | padding: 0.5em; 15 | margin: 0 auto; 16 | width: 100%; 17 | max-width: 640px; 18 | } 19 | 20 | main > h1 { 21 | font-size: 20px; 22 | margin: 0.5em 0; 23 | } 24 | 25 | article { 26 | font-size: 14px; 27 | padding: 0.5em; 28 | background: #fff; 29 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); 30 | } 31 | 32 | article + article { 33 | margin-top: 0.5em; 34 | } 35 | 36 | article > h1 { 37 | font-size: 18px; 38 | } 39 | 40 | article > h1 > a { 41 | color: inherit; 42 | text-decoration: none; 43 | } 44 | 45 | article > .added { 46 | margin-top: 0.25em; 47 | color: #777; 48 | } 49 | 50 | article > .body { 51 | margin-top: 0.5em; 52 | line-height: 1.3em; 53 | } -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Latest News 4 | 5 | 6 | 7 |
8 |

Latest News

9 | {% for article in articles %} 10 | 27 | {% endfor %} 28 |
29 | 30 | -------------------------------------------------------------------------------- /templates/sources.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sources 4 | 5 | 6 |
7 | 8 | 9 |
10 | {% for source in sources %} 11 |
12 | {{ source.title }} 13 |
14 | {% endfor %} 15 | 16 | --------------------------------------------------------------------------------