import argparse import re import functools from datetime import datetime from pathlib import Path import aiohttp import jinja2 from aiohttp import web BASE_DIR = Path(__file__).parent env = jinja2.Environment( loader=jinja2.FileSystemLoader(BASE_DIR / 'templates'), autoescape=jinja2.select_autoescape(default=True), ) def async_cache(maxsize=128): cache = {} def decorator(func): async def wrapper(*args, **kwargs): key = functools._make_key(args, kwargs, False) if key not in cache: if len(cache) >= maxsize: del cache[next(iter(cache))] cache[key] = await func(*args, **kwargs) return cache[key] return wrapper return decorator def relative_datetime(value): dt = datetime.fromtimestamp(value) delta = datetime.now() - dt if delta.days > 365: return '%i years ago' % (delta.days // 365) elif delta.days > 0: return '%i days ago' % delta.days elif delta.seconds > 3600: return '%i hours ago' % (delta.seconds // 3600) elif delta.seconds > 60: return '%i minutes ago' % (delta.seconds // 60) else: return '%i seconds ago' % delta.seconds env.filters['dt'] = relative_datetime def format_number(value): if value > 100000000: return '%im' % (value // 1000000) elif value > 1000000: return '%.1fm' % (value / 1000000) if value > 100000: return '%ik' % (value // 1000) elif value > 1000: return '%.1fk' % (value / 1000) else: return str(value) env.filters['k'] = format_number def normalize_link(value): if value.startswith('https://www.reddit.com'): value = value[22:] return value env.filters['link'] = normalize_link def dash_resize(url): return re.sub('DASH_[0-9]*', 'DASH_220', url) env.filters['dash_resize'] = dash_resize async def fetch(url, **params): async with aiohttp.ClientSession() as session: async with session.get( url, headers={'User-agent': 'neddit'}, params={**params, 'raw_json': 1}, ) as response: if response.status == 404: raise web.HTTPNotFound if response.status == 429: raise web.HTTPServiceUnavailable response.raise_for_status() return await response.json() @async_cache() async def fetch_subreddit(name): return await fetch('https://www.reddit.com/r/%s/about.json' % name) async def generic(request): if not request.path.endswith('/'): raise web.HTTPPermanentRedirect(f'{request.raw_path}/') path = request.path.strip('/') or 'hot' context = { 'params': request.query, } if path.startswith('r/'): subreddit = path.split('/')[1] context['subreddit'] = await fetch_subreddit(subreddit) a = await fetch('https://www.reddit.com/%s/.json' % path, **request.query) if isinstance(a, list): tpl = env.get_template('comments.html') context['post'] = a[0]['data']['children'][0] context['comments'] = a[1] else: tpl = env.get_template('listing.html') context.update(a) return web.Response(body=tpl.render(**context), content_type='text/html') if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--port', type=int, default=8000) args = parser.parse_args() app = web.Application() app.router.add_static('/static', BASE_DIR / 'static') app.router.add_route('GET', '', generic) app.router.add_route('GET', '/{path:.*}', generic) web.run_app(app, host='localhost', port=args.port)