├── models
├── __init__.py
├── source.py
└── article.py
├── .gitignore
├── db.py
├── app.py
├── README.md
├── templates
├── sources.html
└── index.html
├── feed.py
├── run.py
├── static
└── css
│ └── index.css
└── routes
└── __init__.py
/models/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | *.py[cod]
--------------------------------------------------------------------------------
/db.py:
--------------------------------------------------------------------------------
1 | from app import app
2 | from flask_sqlalchemy import SQLAlchemy
3 | db = SQLAlchemy(app)
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/templates/sources.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Sources
4 |
5 |
6 |
10 | {% for source in sources %}
11 |
14 | {% endfor %}
15 |
16 |
--------------------------------------------------------------------------------
/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/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
--------------------------------------------------------------------------------
/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 |
11 |
14 |
20 |
21 | {{ article.date_added }}
22 |
23 |
24 | {{ article.body }}
25 |
26 |
27 | {% endfor %}
28 |
29 |
30 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------