Source code for this website is 46 | on GitHub 48 |
49 |├── .gitignore ├── LICENSE ├── app.py ├── fetch_data.py ├── readme.md ├── requirements.txt ├── static ├── css │ └── style.css └── img │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── cover.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ └── profile.png └── templates └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | __pycache__/ 3 | *.py[cod] 4 | venv/ 5 | 6 | data/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sagun Shrestha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | from datetime import datetime 4 | 5 | import humanize 6 | from flask import Flask, render_template 7 | 8 | app = Flask(__name__) 9 | 10 | 11 | @app.route('/') 12 | def index(): 13 | data_dir = 'data' 14 | stories = {} 15 | files = {'top': 'Top', 'new': 'New', 'ask': 'Ask HN', 'show': 'Show HN', 'jobs': 'Jobs'} 16 | 17 | for key, value in files.items(): 18 | stories[value] = [] 19 | file_path = os.path.join(data_dir, f'{key}.csv') 20 | if not os.path.isfile(file_path): 21 | continue 22 | 23 | with open(os.path.join(data_dir, f'{key}.csv')) as file: 24 | reader = csv.DictReader(file) 25 | for item in list(reader)[:30]: 26 | time = int(item.pop('time')) 27 | ago_time = humanize.naturaltime(datetime.utcnow() - datetime.utcfromtimestamp(time)) 28 | item['time'] = ago_time 29 | item['hn_url'] = f'https://news.ycombinator.com/item?id={item["id"]}' 30 | item['user_url'] = f'https://news.ycombinator.com/user?id={item["by"]}' 31 | stories[value].append(item) 32 | 33 | return render_template('index.html', stories=stories) 34 | 35 | 36 | if __name__ == '__main__': 37 | app.run(host='0.0.0.0', port=5000, debug=True) 38 | -------------------------------------------------------------------------------- /fetch_data.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | from urllib.parse import urlparse 4 | 5 | import requests 6 | 7 | 8 | def scrape_item(story): 9 | try: 10 | item_url = f'https://hacker-news.firebaseio.com/v0/item/{story}.json?print=pretty' 11 | item_response = requests.get(item_url, timeout=180) 12 | item = item_response.json() 13 | except: 14 | return 15 | 16 | if not item: 17 | return 18 | item.pop('kids', None) 19 | fields = list(item.keys()) 20 | if 'url' not in fields: 21 | fields.append('url') 22 | item['url'] = '' 23 | if 'text' not in fields: 24 | fields.append('text') 25 | item['text'] = '' 26 | 27 | fields.append('domain') 28 | item['domain'] = '' 29 | if item['url']: 30 | parsed_uri = urlparse(item['url']) 31 | domain = parsed_uri.netloc 32 | if domain.count('.') > 1: 33 | if domain.startswith(sub_domains): 34 | domain = domain.split('.', 1)[1] 35 | item['domain'] = domain 36 | return item 37 | 38 | 39 | if __name__ == '__main__': 40 | data_dir = 'data' 41 | os.makedirs(data_dir, exist_ok=True) 42 | 43 | urls = { 44 | 'top': 'https://hacker-news.firebaseio.com/v0/topstories.json?print=pretty', 45 | 'new': 'https://hacker-news.firebaseio.com/v0/newstories.json?print=pretty', 46 | 'ask': 'https://hacker-news.firebaseio.com/v0/askstories.json?print=pretty', 47 | 'show': 'https://hacker-news.firebaseio.com/v0/showstories.json?print=pretty', 48 | 'jobs': 'https://hacker-news.firebaseio.com/v0/jobstories.json?print=pretty', 49 | } 50 | 51 | sub_domains = ('www.', 'mail.', 'blog.', 'ns.', 'smtp.', 'webmail.', 'docs.', 'jobs.', 'cs.', 'apply.', 'boards.') 52 | 53 | for key, value in urls.items(): 54 | print(f'Scraping {key}') 55 | 56 | try: 57 | response = requests.get(value, timeout=180) 58 | stories = response.json() 59 | except: 60 | continue 61 | 62 | items = [] 63 | for story in stories[:30]: 64 | item = scrape_item(story) 65 | if item: 66 | print(item) 67 | items.append(item) 68 | 69 | if not items: 70 | continue 71 | 72 | with open(os.path.join(data_dir, f'{key}.csv'), 'w') as file: 73 | writer = csv.DictWriter(file, fieldnames=items[0].keys()) 74 | writer.writeheader() 75 | writer.writerows(items) 76 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # HN Deck 2 | 3 | A tweetdeck like interface to browse Hacker News 4 | 5 | Live https://hndeck.sagunshrestha.com/ 6 | 7 | ## Pre requisites 8 | 9 | * Python 3.6+ 10 | * pip 11 | 12 | ## Install Requirments 13 | 14 | $ pip install -r requirements.txt 15 | 16 | ## Run App 17 | 18 | Collect data, alternatively schedule fetch_data.py as cron job 19 | 20 | $ python fetch_data.py 21 | 22 | Run server 23 | 24 | $ python app.py 25 | 26 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.31.0 2 | humanize==4.10.0 3 | Flask==3.0.3 -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | .jumbotron { 2 | padding-top: 3rem; 3 | padding-bottom: 3rem; 4 | margin-bottom: 0; 5 | background-color: #fff; 6 | } 7 | 8 | @media (min-width: 768px) { 9 | .jumbotron { 10 | padding-top: 6rem; 11 | padding-bottom: 6rem; 12 | } 13 | } 14 | 15 | .jumbotron p:last-child { 16 | margin-bottom: 0; 17 | } 18 | 19 | .jumbotron h1 { 20 | font-weight: 300; 21 | } 22 | 23 | .jumbotron .container { 24 | max-width: 40rem; 25 | } 26 | 27 | /*footer {*/ 28 | /* padding-top: 3rem;*/ 29 | /* padding-bottom: 3rem;*/ 30 | /*}*/ 31 | 32 | /*footer p {*/ 33 | /* margin-bottom: .25rem;*/ 34 | /*}*/ 35 | 36 | .bd-placeholder-img { 37 | font-size: 1.125rem; 38 | text-anchor: middle; 39 | -webkit-user-select: none; 40 | -moz-user-select: none; 41 | -ms-user-select: none; 42 | user-select: none; 43 | } 44 | 45 | @media (min-width: 768px) { 46 | .bd-placeholder-img-lg { 47 | font-size: 3.5rem; 48 | } 49 | } 50 | 51 | .col:first-child { 52 | padding-left: 5px; 53 | } 54 | 55 | .col:last-child { 56 | padding-right: 5px; 57 | } 58 | 59 | .col { 60 | /*min-width: 100%;*/ 61 | width: 100%; 62 | text-align: center; 63 | margin-top: 5px; 64 | padding-top: 4px; 65 | padding-bottom: 20px; 66 | box-sizing: border-box; 67 | padding-left: 0; 68 | padding-right: 5px; 69 | } 70 | 71 | @media (max-width: 767px) { 72 | .col { 73 | min-width: 100% 74 | } 75 | } 76 | 77 | .stories { 78 | overflow-y: scroll; 79 | height: 100vh; 80 | padding-top: 4px; 81 | } 82 | 83 | .stories::-webkit-scrollbar { 84 | width: 6px; 85 | } 86 | 87 | .stories::-webkit-scrollbar-thumb { 88 | background: #d8d8d8; 89 | border-radius: 5px; 90 | } 91 | 92 | .stories::-webkit-scrollbar-track { 93 | background: #ffffff; 94 | } 95 | 96 | .item { 97 | width: 90%; 98 | /*background: #ffffff;*/ 99 | height: 90px; 100 | padding: 4px; 101 | } 102 | 103 | .item-wrapper { 104 | border-radius: 4px; 105 | display: block; 106 | width: 100%; 107 | height: 92px; 108 | position: relative; 109 | max-width: 600px; 110 | background: #f0f0f0; 111 | border: 1px solid #f0f0f0; 112 | margin-bottom: 10px; 113 | } 114 | 115 | p.title { 116 | text-align: left; 117 | margin-bottom: 0; 118 | font-size: 10pt; 119 | } 120 | 121 | a { 122 | text-decoration: none; 123 | color: #828282; 124 | } 125 | 126 | a:hover { 127 | text-decoration: underline; 128 | color: #828282; 129 | } 130 | 131 | .col-header { 132 | font-size: 24px; 133 | background-color: #ff6600; 134 | font-family: Verdana, Geneva, sans-serif; 135 | padding-top: 10px; 136 | padding-bottom: 10px; 137 | } 138 | 139 | .title > a { 140 | color: #212529; 141 | } 142 | 143 | .title > a:hover { 144 | color: #212529; 145 | } 146 | 147 | footer { 148 | padding: 50px 0 65px; 149 | background-color: #f0f0f0; 150 | } 151 | 152 | footer .copyright { 153 | font-size: 14px; 154 | margin-bottom: 0; 155 | text-align: center; 156 | } 157 | 158 | footer a { 159 | color: #007bff; 160 | } 161 | 162 | footer a:hover { 163 | color: #0062cc; 164 | text-decoration: underline; 165 | } 166 | 167 | .share-button { 168 | color: #212529; 169 | } 170 | 171 | .share-button:hover { 172 | color: #0f2af7; 173 | } -------------------------------------------------------------------------------- /static/img/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagunsh/hndeck/268cc9a47e5829e9d576be9b2cfa2ab6fc6826d6/static/img/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/img/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagunsh/hndeck/268cc9a47e5829e9d576be9b2cfa2ab6fc6826d6/static/img/android-chrome-512x512.png -------------------------------------------------------------------------------- /static/img/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagunsh/hndeck/268cc9a47e5829e9d576be9b2cfa2ab6fc6826d6/static/img/apple-touch-icon.png -------------------------------------------------------------------------------- /static/img/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagunsh/hndeck/268cc9a47e5829e9d576be9b2cfa2ab6fc6826d6/static/img/cover.png -------------------------------------------------------------------------------- /static/img/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagunsh/hndeck/268cc9a47e5829e9d576be9b2cfa2ab6fc6826d6/static/img/favicon-16x16.png -------------------------------------------------------------------------------- /static/img/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagunsh/hndeck/268cc9a47e5829e9d576be9b2cfa2ab6fc6826d6/static/img/favicon-32x32.png -------------------------------------------------------------------------------- /static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagunsh/hndeck/268cc9a47e5829e9d576be9b2cfa2ab6fc6826d6/static/img/favicon.ico -------------------------------------------------------------------------------- /static/img/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sagunsh/hndeck/268cc9a47e5829e9d576be9b2cfa2ab6fc6826d6/static/img/profile.png -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 |Source code for this website is 46 | on GitHub 48 |
49 |