Compare commits

...

5 Commits

Author SHA1 Message Date
Jason Schwarzenberger
9cee370a25 tvnz icon 2020-11-10 14:10:02 +13:00
Jason Schwarzenberger
5efc6ef2d3 add related stories (in api only) 2020-11-10 14:09:56 +13:00
Jason Schwarzenberger
4ec50e20cb feed thread loop. 2020-11-10 10:10:38 +13:00
Jason Schwarzenberger
c1b7877f4b remove limit. 2020-11-09 17:54:50 +13:00
Jason Schwarzenberger
7b8cbfc9b9 try to make feed only determined by the max age. 2020-11-09 17:50:58 +13:00
6 changed files with 73 additions and 63 deletions

View File

@ -1,5 +1,4 @@
import json
from datetime import datetime, timedelta
from sqlalchemy import create_engine, Column, String, ForeignKey, Integer
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
@ -66,18 +65,26 @@ def get_story_by_ref(ref):
session = Session()
return session.query(Story).filter(Story.ref==ref).first()
def get_reflist(amount):
def get_stories_by_url(url):
session = Session()
q = session.query(Reflist).order_by(Reflist.rid.desc()).limit(amount)
return session.query(Story).\
filter(Story.title != None).\
filter(Story.meta['url'].as_string() == url).\
order_by(Story.meta['date'].desc())
def get_reflist():
session = Session()
q = session.query(Reflist).order_by(Reflist.rid.desc())
return [dict(ref=x.ref, sid=x.sid, source=x.source) for x in q.all()]
def get_stories(amount):
def get_stories(maxage=60*60*24*2):
time = datetime.now().timestamp() - maxage
session = Session()
q = session.query(Reflist, Story.meta).\
join(Story).\
filter(Story.title != None).\
order_by(Story.meta['date'].desc()).\
limit(amount)
filter(Story.meta['date'] > time).\
order_by(Story.meta['date'].desc())
return [x[1] for x in q]
def put_ref(ref, sid, source):

View File

@ -6,16 +6,13 @@ logging.basicConfig(
import requests
import time
from bs4 import BeautifulSoup
import itertools
import settings
from feeds import hackernews, reddit, tildes, substack, manual, news
from scrapers import outline, declutter, local
ONE_HOUR = 60*60
ONE_DAY = 24*ONE_HOUR
INVALID_DOMAINS = ['youtube.com', 'bloomberg.com', 'wsj.com']
MAX_AGE_IN_DAYS = 3*ONE_DAY
substacks = {}
for key, value in settings.SUBSTACK.items():
@ -27,36 +24,39 @@ sitemaps = {}
for key, value in settings.SITEMAP.items():
sitemaps[key] = news.Sitemap(value['url'], value.get('tz'))
def list():
feed = []
def get_list():
feeds = {}
if settings.NUM_HACKERNEWS:
feed += [(x, 'hackernews') for x in hackernews.feed()[:settings.NUM_HACKERNEWS]]
feeds['hackernews'] = [(x, 'hackernews') for x in hackernews.feed()[:settings.NUM_HACKERNEWS]]
if settings.NUM_REDDIT:
feed += [(x, 'reddit') for x in reddit.feed()[:settings.NUM_REDDIT]]
feeds['reddit'] = [(x, 'reddit') for x in reddit.feed()[:settings.NUM_REDDIT]]
if settings.NUM_TILDES:
feed += [(x, 'tildes') for x in tildes.feed()[:settings.NUM_TILDES]]
feeds['tildes'] = [(x, 'tildes') for x in tildes.feed()[:settings.NUM_TILDES]]
if settings.NUM_SUBSTACK:
feed += [(x, 'substack') for x in substack.top.feed()[:settings.NUM_SUBSTACK]]
feeds['substack'] = [(x, 'substack') for x in substack.top.feed()[:settings.NUM_SUBSTACK]]
for key, publication in substacks.items():
count = settings.SUBSTACK[key]['count']
feed += [(x, key) for x in publication.feed()[:count]]
feeds[key] = [(x, key) for x in publication.feed()[:count]]
for key, sites in categories.items():
count = settings.CATEGORY[key].get('count') or 0
excludes = settings.CATEGORY[key].get('excludes')
tz = settings.CATEGORY[key].get('tz')
feed += [(x, key) for x in sites.feed(excludes)[:count]]
feeds[key] = [(x, key) for x in sites.feed(excludes)[:count]]
for key, sites in sitemaps.items():
count = settings.SITEMAP[key].get('count') or 0
excludes = settings.SITEMAP[key].get('excludes')
feed += [(x, key) for x in sites.feed(excludes)[:count]]
feeds[key] = [(x, key) for x in sites.feed(excludes)[:count]]
values = feeds.values()
feed = itertools.chain.from_iterable(itertools.zip_longest(*values, fillvalue=None))
feed = list(filter(None, feed))
return feed
def get_article(url):
@ -124,7 +124,7 @@ def update_story(story, is_manual=False):
logging.info('Story not ready yet')
return False
if story['date'] and not is_manual and story['date'] + MAX_AGE_IN_DAYS < time.time():
if story['date'] and not is_manual and story['date'] + settings.MAX_STORY_AGE < time.time():
logging.info('Story too old, removing')
return False

View File

@ -163,6 +163,8 @@ def get_sitemap_date(a):
return a.find('lastmod').text
if a.find('news:publication_date'):
return a.find('news:publication_date').text
if a.find('ns2:publication_date'):
return a.find('ns2:publication_date').text
return ''
class Sitemap(_Base):

View File

@ -15,6 +15,7 @@ import traceback
import time
from urllib.parse import urlparse, parse_qs
import settings
import database
import search
import feed
@ -27,9 +28,6 @@ from flask_cors import CORS
database.init()
search.init()
FEED_LENGTH = 75
news_index = 0
def new_id():
nid = gen_rand_id()
while database.get_story(nid):
@ -42,7 +40,7 @@ cors = CORS(flask_app)
@flask_app.route('/api')
def api():
stories = database.get_stories(FEED_LENGTH)
stories = database.get_stories(settings.MAX_STORY_AGE)
res = Response(json.dumps({"stories": stories}))
res.headers['content-type'] = 'application/json'
return res
@ -101,7 +99,9 @@ def submit():
def story(sid):
story = database.get_story(sid)
if story:
res = Response(json.dumps({"story": story.data}))
related = database.get_stories_by_url(story.meta['url'])
related = [r.meta for r in related]
res = Response(json.dumps({"story": story.data, "related": related}))
res.headers['content-type'] = 'application/json'
return res
else:
@ -144,51 +144,49 @@ def static_story(sid):
http_server = WSGIServer(('', 33842), flask_app)
def feed_thread():
global news_index
def _add_new_refs():
for ref, source in feed.get_list():
if database.get_story_by_ref(ref):
continue
try:
nid = new_id()
database.put_ref(ref, nid, source)
logging.info('Added ref ' + ref)
except database.IntegrityError:
continue
def _update_current_story(item):
try:
story = database.get_story(item['sid']).data
except AttributeError:
story = dict(id=item['sid'], ref=item['ref'], source=item['source'])
logging.info('Updating story: {}'.format(str(story['ref'])))
valid = feed.update_story(story)
if valid:
database.put_story(story)
search.put_story(story)
else:
database.del_ref(item['ref'])
logging.info('Removed ref {}'.format(item['ref']))
def feed_thread():
ref_list = []
try:
while True:
# onboard new stories
if news_index == 0:
for ref, source in feed.list():
if database.get_story_by_ref(ref):
continue
try:
nid = new_id()
database.put_ref(ref, nid, source)
logging.info('Added ref ' + ref)
except database.IntegrityError:
continue
ref_list = database.get_reflist(FEED_LENGTH)
if not len(ref_list):
_add_new_refs()
ref_list = database.get_reflist()
# update current stories
if news_index < len(ref_list):
item = ref_list[news_index]
try:
story = database.get_story(item['sid']).data
except AttributeError:
story = dict(id=item['sid'], ref=item['ref'], source=item['source'])
logging.info('Updating story: ' + str(story['ref']) + ', index: ' + str(news_index))
valid = feed.update_story(story)
if valid:
database.put_story(story)
search.put_story(story)
else:
database.del_ref(item['ref'])
logging.info('Removed ref {}'.format(item['ref']))
else:
logging.info('Skipping index: ' + str(news_index))
if len(ref_list):
item = ref_list.pop(0)
_update_current_story(item)
gevent.sleep(6)
news_index += 1
if news_index == FEED_LENGTH: news_index = 0
except KeyboardInterrupt:
logging.info('Ending feed thread...')
except ValueError as e:

View File

@ -1,6 +1,8 @@
# QotNews settings
# edit this file and save it as settings.py
MAX_STORY_AGE = 3*24*60*60
# Feed Lengths
# Number of top items from each site to pull
# set to 0 to disable that site

View File

@ -72,5 +72,6 @@ export const logos = {
stuff: "data:image/png;base64,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",
substack: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAB80lEQVR4Ae3BwUtTcQAH8O/vt98L3/YYYm2TDusUZCBURlTHSV3yTzA8iHcVaYID50HQIWM3BY8P/QdaIAV2a2HIKDGhukgjtqEi7+3trb3X79c7jijbRr2fBz8fAk8sGIiVn0bL8FG/Xu2v1H9UKCSjkIzB43DhFCpOAT5yuHBw4Twg8PT10L6XTy6/go8evzh+dNLgJwwehRJl6Aq7Ax8plCjwUEjG4BECosnRhI+EgMCF84DAE1ZoOPswnIWPpt8Y04bDDQaPqhB1/IY6Dh/NvTPnDAcGhWQUklFIRiEZhWQUklFIRiEZQ5fKdV6e3zXnCQhZuKstxFQaQxcYOlR3RT37wcpm3lsZs8lNeDa/2JvJW6Hk1GBoKshIEB1gaBMX4PpnW0/tGKmSxUtoYTa5mdoxU2v71trivfDi6HV1lBJQtIGhDdvfmtszBWOmeOQUcYaSxUtjr0/HcntWbuVBeCVx9VICf8Fwho+nzkHybe1Z/rCRRweKR05x+Pnx8Mi1npHl+1rmZq8ygD9g+I2qzavp3Vp6/aC+7nLhokv5w0Z+6+v3rYmB4ER6SEtHVRrFLxha2C7s3J6VWyrWlgyHG/gHXC7c1X1rdeOTvTF7W5udHAxNqgwqWkXUQERP9OpxjcXxn8U1FtcTvXpEDUTg+QkFxcDjVd0KBQAAAABJRU5ErkJggg==",
"the bulletin": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADwElEQVR4AcXBW2iWBRzH8e/v/z7vfH3du5rOU7p0w8PwkIlM2kIsSbSoGdhFXkRkh0sp6qLool0EddnJKIT0oqguVDDEUjRRYSbOssY2zWpbs3Ind3Dnd8+/HQij8H0f9WKfjwq27HImkTHJjElmTDLjlghHTBDXiZsVcFOccYJEzCjITzAlDkPD0N45QO+wY4wR4EQREJEAR2wqncuzm0tYu7yQRNyQwIGhdEh9Qyv7Tv3GZ4cv0j0QIkQ2ARHl5Bjv7yijomwJZsa/CUjEY9y7+C5WLZrN848uY8c7JzhZ04KZkYmRlRHivP3CGraUL8HMyESKMa/gTjaVFSKJbAKyCllZdAdPbliBZIxJj6T5/Fg9R84209M/zIxUghVF+TyxfhGz86ex+1ANb+w+hySyCYhgU+kCTGKcOx8d+InKPdWYGU7ImANVTby79zyPlS3gy28bcDeQk01ABEVzc8EBgcvZf+pXMEY5Qoxz6B1wvjjWADIQkRgRhCEIMc6haE4uLsa5O9cJZExwogjIwh1+vtyFA2LCW8+VEY8FHDvXTFvPEIZAjHLAESKqgCzc4WBVI69uKyUnCJCMgvwUO196kJGRkLauXhpbrvHL5aucudDK4e+aaO9JE1UsWVJRSQYSdFxLgw9SvnIeJiGEJGJmpJIJ5s/M457iWWwuXcBTm0sYHOqn+mI7ILKJJUsqKslCwOnaVppaOlhZPJ1UcgqS+C9JJOJxHlhdSHdvH9UX2kEik1iypKKSSERtQxe7D9Vx4sdm6ps6aGrppuvaAEEAuYkcxCgJSaxZOotPvq5nOB2SScBNGk5DVU0bp2taQYwShGnuXzaLXa9tpCCVCxJ5yanct7yAo2dbgJAbMW6BBEiAGKcYp2rb+PSbOpyQMZK4e2Yu4GRiROQ4IsRx/kcCE119wzjXuYtsjIie3riIj19Zz4y8OI7jDo5wHHCm5sSoKCvEZIxxD2m80kM2ARFsf2Qxb24vx8zYsLqQI9WNfH+pnZaOfgyxcG6Kx9cVs3T+dP7xV2cfVTV/AiKTgIyMVcV5VD5TTswMychLTmXruhK2ruOG0iMjvL7rBINDApGRkVHI+UtXefG9o7T19OEekk1P/yAvf3icA1V/4CKrgGxM7DvZzPEf9rLtocU8vHY+yxbOITklhgB3ETr8fqWTr840sOdgLU1tgxgiioCI2nvS7Nxfxwf764jHQmakEkxLGCHiavcgXX1pQIAwoguISEwQkB4xrnQO4ThjhADjVgTcBiFulzHJjElmTLK/AU9/SZ4HPrs9AAAAAElFTkSuQmCC",
tvnz: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAASAAAAEgARslrPgAABWVJREFUWMOVl1+MlNUZxn/PmYGFXSgw7kJbiPzZVVkBaaSicTclFWyMJjaxiUkTNd6ZEOMF8cZojIlR74xpb9pb7AW98KY3VAu0Wi0IasVVEQPpAqIBZgd2gF3izJzHi++bmW92/rg9yTeZ88133vd33vO87/eOmOf4cMcmbHJBLJdYL7FGaKkEEleBc0H8D8IVIG498Nm87KrXj4d/dTs2AMsEY5J2StwhsVYwKGmRBIIbEpckzggdlzggOGwoB2DzPyb+f4B/j90OMCB4UOJxiXHBCkmku0ZAY05yLwiESoj3BG9K/N1mZksXiDaAd+8dRQbDrRJ7JD2aOO7gtDcEEiVgn8TrNqdDgE1vT3QH+Oc9owRExGMSrwrGJYKkhvHkmjPvDRGBf0k8Z3NUgs3vTLQDHLh7Y33n9wbpDxLbsga7OsXgSAgBhUDrmgYEgqMST9scy0YiAHw4/gtihJq51fBKtLfZYENsXE6uGInVGrFWwxL5whADo1vJD67EMeJkE9TXG9fvbbd5RWLYhslHtgCQB7h8YxagX2gPsAOBbUjpnVoMCxfSN7SSgbUbGLhlI/3rNrD45vX0/XQ11z7/L2feeJlqqQghJGFwPc4GC4ldNs9IPHf1KjMA+f133gaJ/YeQH8WSACvdRmqpb+Uq1j75FMvv3E7f0Cpy/QNJjNOxZNNWFg79jErxYnpfbRBGEjxmcxD428T9W8hHgMgywWPAivTBZGEaCcXI0o2bWfXAb8n193dJaGGpXjea8BkIJ0YLiMcF7wLTIT2rMcN449zsxjk2FuYXJNLuMUyil6YO3KKJ2NTEfYa7DIRajLK906bQ8uBcMcVab+eu6yYrwFYIUkBwwWYspz4CqGC4o502Ub3TnVXKZWoz14mVCpUrJVytdgSJdtNpNqKZjaWRuKcSbxTyNuuBtWRFlzm3iJECV76Y4MRrLxHyORYsWcrw7j0sWFHoEIV0aZutNtvDwLq8YQ1msFGWOkAgqM5c58Khd6BWpbDtLhxjl2PIJk9PiEGJ1XnbS0GLmmruDqGQSzMsdBVhVrxttlojswizJJ/MnSb+j0Ek8TV0B0h1QyeIOXMDeUMZM4u8eF4QvQAyoktKaE+IWYmrAXPOUGyp2z1yuT7vCpB9bo4w58yLNueDYdLmTNvLowtE7BkF94ZoTdFTNpOhWvNl28c7vsE6QTiF6Oa841u0o60jZy9duxxCwIYDtkvzhei0/UaIaR5Ri63W8E/Z/uDnNw0Q0lWHbd5rP7cOEKmx9qHM7jtAtNo+aPgIIKThLBvetJlqPEgXCEOs1TLJ3lRgvSGBnhBFm721SDkawu9PnKo73G/Yl+hkjpgyEChw/dxZSp9+TKU8TWV6mkp5mtInx5j59jxWyDjOQCS2ou29hkMA4+9/mSTmICP8cRSAYcGfBLsafWC2x2v0d2bxylX0r1lDCAFsZr85y/dTl5LesGVNSxe9X7AbmNxx+ASQtmRFTuE4Qshx2uZ54CeY7Y3a3TjmtGJKzF68wOyF75oOQiAEEe2k0UwLkQWyCeg/wAvRTGYrea7+5a2pEr+7qUBYyHnXOI7YAKyj3hg2tZZ8pE6TTlhJs9XWrwggIt4Gnq3W/IkC7DzyVTsAwFvFEo8sKxByfGv4ILVwC7C4I0TLvA7W4r+I9GfgRcMJgvjN0a+yLrv/Nds3OoKhX7BL8ITEryUVOvb87X9QpgQHJfZKHAJmH/joZJfk7TH+snGEvgDRLEP8Umhc4m6JYcGQxKIUYlaiKHFKcETS+xIfS5SrFXj4s5NdffTuMjPjr6MjACEELResQ6wOYonASNcE30icCWLaEB/+9Ot52f0B/MOqaxJsqlAAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTYtMDktMjhUMjA6NDA6MjkrMDA6MDD37mKbAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE2LTA5LTI4VDIwOjQwOjI5KzAwOjAwhrPaJwAAAEZ0RVh0c29mdHdhcmUASW1hZ2VNYWdpY2sgNi43LjgtOSAyMDE0LTA1LTEyIFExNiBodHRwOi8vd3d3LmltYWdlbWFnaWNrLm9yZ9yG7QAAAAAYdEVYdFRodW1iOjpEb2N1bWVudDo6UGFnZXMAMaf/uy8AAAAYdEVYdFRodW1iOjpJbWFnZTo6aGVpZ2h0ADE5Mg8AcoUAAAAXdEVYdFRodW1iOjpJbWFnZTo6V2lkdGgAMTky06whCAAAABl0RVh0VGh1bWI6Ok1pbWV0eXBlAGltYWdlL3BuZz+yVk4AAAAXdEVYdFRodW1iOjpNVGltZQAxNDc1MDk1MjI5BqTtUAAAAA90RVh0VGh1bWI6OlNpemUAMEJClKI+7AAAAFZ0RVh0VGh1bWI6OlVSSQBmaWxlOi8vL21udGxvZy9mYXZpY29ucy8yMDE2LTA5LTI4LzgzZWFmZGYwYzNkNzQzZDY4YzI0NzE0ZTVhMjY4YWY2Lmljby5wbmcMVz3tAAAAAElFTkSuQmCC",
webworm: "data:image/png;base64,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",
};