diff --git a/README.md b/README.md index 51668d0..d84d40b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # python-feedreader Building an RSS feed reader in Python. + +This is supplementary code for this tutorial series: https://www.youtube.com/playlist?list=PLmxT2pVYo5LBcv5nYKTIn-fblphtD_OJO diff --git a/app.py b/app.py new file mode 100644 index 0000000..3131ee8 --- /dev/null +++ b/app.py @@ -0,0 +1,5 @@ +from flask import Flask + +app = Flask(__name__) +db_uri = 'mysql+pymysql://root:password@localhost/feedreader' +app.config['SQLALCHEMY_DATABASE_URI'] = db_uri \ No newline at end of file diff --git a/db.py b/db.py new file mode 100644 index 0000000..f74f1ad --- /dev/null +++ b/db.py @@ -0,0 +1,3 @@ +from app import app +from flask_sqlalchemy import SQLAlchemy +db = SQLAlchemy(app) \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/article.py b/models/article.py new file mode 100644 index 0000000..463ccc0 --- /dev/null +++ b/models/article.py @@ -0,0 +1,32 @@ +from db import db +import datetime + +class Article(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.Text, nullable=False) + body = db.Column(db.Text, nullable=False) + link = db.Column(db.Text, nullable=False) + guid = db.Column(db.String(255), nullable=False) + unread = db.Column(db.Boolean, default=True, nullable=False) + source_id = db.Column(db.Integer, db.ForeignKey('source.id'), nullable=False) + source = db.relationship('Source', backref=db.backref('articles', lazy=True)) + date_added = db.Column(db.DateTime, default=datetime.datetime.utcnow) + date_published = db.Column(db.DateTime) + __table_args__ = ( + db.UniqueConstraint('source_id', 'guid', name='uc_source_guid'), + ) + + @classmethod + def insert_from_feed(cls, source_id, feed_articles): + stmt = Article.__table__.insert().prefix_with('IGNORE') + articles = [] + for article in feed_articles: + articles.append({ + 'title': article['title'], + 'body': article['summary'], + 'link': article['link'], + 'guid': article['id'], + 'source_id': source_id, + 'date_published': article['published'], + }) + db.engine.execute(stmt, articles) diff --git a/models/source.py b/models/source.py new file mode 100644 index 0000000..b668340 --- /dev/null +++ b/models/source.py @@ -0,0 +1,20 @@ +from db import db +import datetime + +class Source(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.Text, nullable=False) + subtitle = db.Column(db.Text, nullable=False) + link = db.Column(db.Text, nullable=False) + feed = db.Column(db.Text, nullable=False) + date_added = db.Column(db.DateTime, default=datetime.datetime.utcnow) + + @classmethod + def insert_from_feed(cls, feed, feed_source): + link = feed_source['link'] + title = feed_source['title'] + subtitle = feed_source['subtitle'] + source = Source(feed=feed, link=link, title=title, subtitle=subtitle) + db.session.add(source) + db.session.commit() + return source \ No newline at end of file diff --git a/routes/__init__.py b/routes/__init__.py new file mode 100644 index 0000000..8a8063c --- /dev/null +++ b/routes/__init__.py @@ -0,0 +1,46 @@ +from flask import abort, redirect, request, render_template +from app import app +from db import db +from models.article import Article +from models.source import Source +import feed + +@app.route('/', methods=['GET']) +def index_get(): + query = Article.query + query = query.filter(Article.unread == True) + orderby = request.args.get('orderby', 'added') + if orderby == 'added': + query = query.order_by(Article.date_added.desc()) + elif orderby == 'published': + query = query.order_by(Article.date_published.desc()) + elif orderby == 'title': + query = query.order_by(Article.title) + elif orderby == 'source': + query = query.join(Source).order_by(Source.title) + articles = query.all() + return render_template('index.html', articles=articles) + +@app.route('/read/', methods=['GET']) +def read_article_get(article_id): + article = Article.query.get(article_id) + article.unread = False + db.session.commit() + return redirect(article.link) + +@app.route('/sources', methods=['GET']) +def sources_get(): + query = Source.query + query = query.order_by(Source.title) + sources = query.all() + return render_template('sources.html', sources=sources) + +@app.route('/sources', methods=['POST']) +def sources_post(): + feed_url = request.form['feed'] + parsed = feed.parse(feed_url) + feed_source = feed.get_source(parsed) + source = Source.insert_from_feed(feed_url, feed_source) + feed_articles = feed.get_articles(parsed) + Article.insert_from_feed(source.id, feed_articles) + return redirect('/sources') diff --git a/run.py b/run.py new file mode 100644 index 0000000..7b85aca --- /dev/null +++ b/run.py @@ -0,0 +1,32 @@ +from app import app +from db import db +from models import article, source +import routes +import feed +from threading import Thread +import time + +with app.app_context(): + db.create_all() + +def update_loop(): + while True: + with app.app_context(): + query = source.Source.query + for src in query.all(): + try: + update_source(src) + except: + continue + time.sleep(60 * 15) + +def update_source(src): + parsed = feed.parse(src.feed) + feed_articles = feed.get_articles(parsed) + article.Article.insert_from_feed(src.id, feed_articles) + print('Updated ' + src.feed) + +thread = Thread(target=update_loop) +thread.start() + +app.run() diff --git a/static/css/index.css b/static/css/index.css new file mode 100644 index 0000000..57858e5 --- /dev/null +++ b/static/css/index.css @@ -0,0 +1,53 @@ +* { + margin: 0; + padding: 0; +} + +body { + color: #333; + background: #f1f1f1; + font-family: Helvetica, Arial, sans-serif; +} + +main { + box-sizing: border-box; + padding: 0.5em; + margin: 0 auto; + width: 100%; + max-width: 640px; +} + +main > h1 { + font-size: 20px; + margin: 0.5em 0; +} + +article { + font-size: 14px; + padding: 0.5em; + background: #fff; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +article + article { + margin-top: 0.5em; +} + +article > h1 { + font-size: 18px; +} + +article > h1 > a { + color: inherit; + text-decoration: none; +} + +article > .added { + margin-top: 0.25em; + color: #777; +} + +article > .body { + margin-top: 0.5em; + line-height: 1.3em; +} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..f6e6c62 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,30 @@ + + + Latest News + + + +
+

Latest News

+ {% for article in articles %} + + {% endfor %} +
+ + \ No newline at end of file diff --git a/templates/sources.html b/templates/sources.html new file mode 100644 index 0000000..080a4aa --- /dev/null +++ b/templates/sources.html @@ -0,0 +1,16 @@ + + + Sources + + +
+ + +
+ {% for source in sources %} +
+ {{ source.title }} +
+ {% endfor %} + + \ No newline at end of file