news4 - RSS aggrigation system
Revision | dc4c829ca4a9caf6d049130634e7d5c7335d0438 (tree) |
---|---|
Zeit | 2012-11-06 20:30:32 |
Autor | hylom <hylom@user...> |
Commiter | hylom |
Merge remote branch 'remotes/origin/live' into live
@@ -14,9 +14,12 @@ | ||
14 | 14 | gnewsでサイトを生成するために、下記の設定ファイルが必要です。 |
15 | 15 | |
16 | 16 | === config.py === |
17 | - HTMLの生成先やサイト名、RSSの取得先などを設定するファイルです。PythonのDictionaryおよびArray形式で記述されています。 | |
17 | + HTMLの生成先やサイト名などを設定するファイルです。PythonのDictionaryおよびArray形式で記述されています。 | |
18 | 18 | config.py.sampleをコピーしてconfig.pyを作成し、編集します。 |
19 | 19 | |
20 | +=== sources.ini === | |
21 | + RSSの取得先を設定するファイルです。ini形式で記述されています。セクション名が表示されるサイト名、urlパラメータがサイトのURL、sourcesパラメータがRSSの取得先、filtersが使用するフィルタ一覧(カンマ区切り)となります。 | |
22 | + | |
20 | 23 | === install.conf === |
21 | 24 | 関連ファイルのコピー先を指定するファイルです。 |
22 | 25 | install.conf.sampleをコピーしてintall.confを作成し、編集します。 |
@@ -35,81 +35,3 @@ config = { | ||
35 | 35 | ], |
36 | 36 | } |
37 | 37 | |
38 | -target_rss = [ | |
39 | - { | |
40 | - 'name': 'SourceForge.JP Magazine', | |
41 | - 'url': 'http://rss.rssad.jp/rss/sourceforge/magazine/rss', | |
42 | - 'source_url': 'http://sourceforge.jp/magazine/', | |
43 | - }, | |
44 | - { | |
45 | - 'name': 'Slashdot Japan', | |
46 | - 'url': 'http://rss.rssad.jp/rss/slashdot/slashdot.rss', | |
47 | - 'source_url': 'http://slashdot.jp/', | |
48 | - 'filter': ['slashdotjp',], | |
49 | - }, | |
50 | - { | |
51 | - 'name': 'ITmedia', | |
52 | - 'url': 'http://rss.rssad.jp/rss/itmtop/2.0/itmedia_all.xml', | |
53 | - 'source_url': 'http://www.itmedia.co.jp/', | |
54 | - 'filter': ['tagging', 'itmedia'], | |
55 | - }, | |
56 | - { | |
57 | - 'name': 'So-netセキュリティ通信', | |
58 | - 'url': 'http://security-t.blog.so-net.ne.jp/index.rdf', | |
59 | - 'source_url': 'http://security-t.blog.so-net.ne.jp/', | |
60 | - 'filter': ['tagging',], | |
61 | - }, | |
62 | - { | |
63 | - 'name': 'Engadget Japanese', | |
64 | - 'url': 'http://japanese.engadget.com/rss.xml', | |
65 | - 'source_url': 'http://japanese.engadget.com/', | |
66 | - 'filter': ['tagging',], | |
67 | - }, | |
68 | - { | |
69 | - 'name': 'ギズモード・ジャパン', | |
70 | - 'url': 'http://feeds.gizmodo.jp/rss/gizmodo/index.xml', | |
71 | - 'source_url': 'http://www.gizmodo.jp/', | |
72 | - 'filter': ['tagging',], | |
73 | - }, | |
74 | - { | |
75 | - 'name': 'TechCrunch Japan', | |
76 | - 'url': 'http://jp.techcrunch.com/feed/', | |
77 | - 'source_url': 'http://jp.techcrunch.com/', | |
78 | - 'filter': ['tagging',], | |
79 | - }, | |
80 | - { | |
81 | - 'name': 'Impress Watch', | |
82 | - 'url': 'http://rss.rssad.jp/rss/headline/headline.rdf', | |
83 | - 'source_url': 'http://www.watch.impress.co.jp/', | |
84 | - 'filter': ['tagging',], | |
85 | - }, | |
86 | - { | |
87 | - 'name': 'WIRED.jp', | |
88 | - 'url': 'ihttp://rss.rssad.jp/rss/h/wired/feed.rdf', | |
89 | - 'source_url': 'http://wired.jp/', | |
90 | - 'filter': ['tagging',], | |
91 | - }, | |
92 | - { | |
93 | - 'name': 'CNET Japan', | |
94 | - 'url': 'http://feeds.japan.cnet.com/rss/cnet/all.rdf', | |
95 | - 'source_url': 'http://japan.cnet.com/', | |
96 | - 'filter': ['tagging',], | |
97 | - }, | |
98 | - { | |
99 | - 'name': 'japan.internet.com', | |
100 | - 'url': 'http://rss.internetcom.jp/rss/japaninternetcom/index.rdf', | |
101 | - 'source_url': 'http://japan.internet.com/', | |
102 | - 'filter': ['tagging',], | |
103 | - }, | |
104 | - | |
105 | - ] | |
106 | - | |
107 | - | |
108 | -"""Template: | |
109 | - { | |
110 | - 'name': '', | |
111 | - 'url': '', | |
112 | - 'source_url': '', | |
113 | - 'filter': ['tagging',], | |
114 | - }, | |
115 | -""" |
@@ -22,17 +22,21 @@ a { | ||
22 | 22 | margin-bottom: 1em; |
23 | 23 | } |
24 | 24 | .entry-footer{ |
25 | - color: gray; | |
25 | + color: #888; | |
26 | 26 | } |
27 | 27 | |
28 | 28 | #site-header { |
29 | - border-bottom: 1px solid gray; | |
29 | + border-bottom: 1px solid #888; | |
30 | 30 | margin-bottom: 10px; |
31 | 31 | } |
32 | 32 | |
33 | +#site-header .last-update { | |
34 | + color: #888; | |
35 | +} | |
36 | + | |
33 | 37 | #site-footer { |
34 | 38 | margin-top: 10px; |
35 | - color: gray; | |
39 | + color: #888; | |
36 | 40 | text-align: center; |
37 | 41 | } |
38 | 42 |
@@ -21,7 +21,7 @@ class FeedFetcher(object): | ||
21 | 21 | entry = { |
22 | 22 | # 'title': e.title.decode('utf8') if isinstance(e.title, str) else e.title, |
23 | 23 | 'title': e.title, |
24 | - 'link': e.link, | |
24 | + 'url': e.link, | |
25 | 25 | 'body': e.description, |
26 | 26 | 'date': dateutil.parser.parse(e.updated), |
27 | 27 | 'feed': self._feed, |
@@ -15,7 +15,6 @@ def main(): | ||
15 | 15 | "gnews's main function" |
16 | 16 | # TODO: argv check |
17 | 17 | |
18 | - | |
19 | 18 | # fetch RSS feed |
20 | 19 | entries = [] |
21 | 20 | for feed in sources: |
@@ -49,8 +48,14 @@ def main(): | ||
49 | 48 | for e in entries: |
50 | 49 | log(e["date"]) |
51 | 50 | |
51 | + call_plugin('pre_render', entries) | |
52 | + | |
52 | 53 | # do rendering |
53 | - params = {'tags':tags, 'page':{}, 'sorted_tags':sorted_tags} | |
54 | + params = { | |
55 | + 'tags':tags, | |
56 | + 'page':{}, | |
57 | + 'sorted_tags':sorted_tags | |
58 | + } | |
54 | 59 | |
55 | 60 | # render index page |
56 | 61 | do_rendering('index', 'index%s.html', entries, params) |
@@ -61,6 +66,38 @@ def main(): | ||
61 | 66 | do_rendering('tags', tag + '%s.html', subentries, params) |
62 | 67 | |
63 | 68 | |
69 | +def call_plugin(function_name, entries): | |
70 | + "call plugin" | |
71 | + for plugin in config['plugins']: | |
72 | + mod = _get_plugin(plugin) | |
73 | + f = mod.__getattribute__(function_name) | |
74 | + f(entries) | |
75 | + | |
76 | +class PluginError(Exception): | |
77 | + def __init__(self, value): | |
78 | + self.value = value | |
79 | + def __str__(self): | |
80 | + return 'plugin "' + self.value + '" is not found.' | |
81 | + | |
82 | +def _get_plugin(plugin_name): | |
83 | + 'load plugin by config settings' | |
84 | + | |
85 | + # fallback when filter isn't defined | |
86 | + if plugin_name is None: | |
87 | + return lambda x:x | |
88 | + | |
89 | + # import module | |
90 | + mods = __import__(config['plugin_directory'], | |
91 | + globals(), | |
92 | + locals(), | |
93 | + [plugin_name,]) | |
94 | + try: | |
95 | + mod = mods.__getattribute__(plugin_name) | |
96 | + except AttributeError: | |
97 | + raise PluginError(plugin_name) | |
98 | + | |
99 | + return mod | |
100 | + | |
64 | 101 | |
65 | 102 | def do_rendering(page_type, filename, entries, params): |
66 | 103 | "rendering page" |
@@ -0,0 +1,40 @@ | ||
1 | +#!/usr/bin/python | |
2 | +# -*- coding: utf-8 | |
3 | +'plugin for hatena bookmark counter' | |
4 | + | |
5 | +#from __future__ import with_statement | |
6 | + | |
7 | +import xmlrpclib | |
8 | +import datetime | |
9 | +import time | |
10 | +import sys | |
11 | + | |
12 | +# see http://d.hatena.ne.jp/keyword/%a4%cf%a4%c6%a4%ca%a5%d6%a5%c3%a5%af%a5%de%a1%bc%a5%af%b7%ef%bf%f4%bc%e8%c6%c0API?kid=146686 | |
13 | + | |
14 | +urls = [] | |
15 | +counts = {} | |
16 | + | |
17 | +def pre_fetch(): | |
18 | + pass | |
19 | + | |
20 | +def pre_tag_aggregate(entries): | |
21 | + pass | |
22 | + | |
23 | +def pre_render(entries): | |
24 | + counts = [] | |
25 | + for i in range(0, len(entries), 50): | |
26 | + urls = [x['url'] for x in entries[i:i+50]] | |
27 | + c = _get_count(urls) | |
28 | + for j in range(0, len(c)): | |
29 | + entries[i+j]['url'] = c[j] | |
30 | + | |
31 | +def pre_quit(entries): | |
32 | + pass | |
33 | + | |
34 | +def _get_count(urls): | |
35 | + # urls can have max 50 items | |
36 | + uri = "http://b.hatena.ne.jp/xmlrpc" | |
37 | + server = xmlrpclib.ServerProxy(uri) | |
38 | + t = server.bookmark.getCount(*urls) | |
39 | + return t | |
40 | + |
@@ -12,7 +12,6 @@ from propertizer import propertize | ||
12 | 12 | from logger import log |
13 | 13 | |
14 | 14 | def date_format(date): |
15 | - #dt = dateutil.parser.parse(date) | |
16 | 15 | return date.strftime('%Y/%m/%d %H:%M') |
17 | 16 | |
18 | 17 | class Renderer(object): |
@@ -41,6 +40,7 @@ class Renderer(object): | ||
41 | 40 | 'site': config['site_parameter'], |
42 | 41 | 'sources': self._sources, |
43 | 42 | } |
43 | + kwargs['site']['last_update'] = datetime.datetime.utcnow() | |
44 | 44 | for key in kwargs: |
45 | 45 | d = propertize(kwargs[key]) |
46 | 46 | kwargs[key] = d |
@@ -25,14 +25,17 @@ s.parentNode.insertBefore(ga, s); | ||
25 | 25 | <div class="container"> |
26 | 26 | |
27 | 27 | <!-- タイトル --> |
28 | - <div class="row"> | |
29 | - <div class="span12"> | |
30 | - <header id="site-header"> | |
28 | + <div class="row" id="site-header"> | |
29 | + <div class="span9"> | |
30 | + <header> | |
31 | 31 | <a href="${site.root}"> |
32 | 32 | <img id="sitelogo" src="${site.img_directory}/themesjp.png" alt="Themes.JP"> α |
33 | 33 | </a> |
34 | 34 | </header> |
35 | 35 | </div> |
36 | + <div class="span3 last-update"> | |
37 | + last update: ${date_format(site.last_update)} | |
38 | + </div> | |
36 | 39 | </div> |
37 | 40 | |
38 | 41 | <!-- コンテンツ本体 --> |
@@ -63,7 +66,7 @@ s.parentNode.insertBefore(ga, s); | ||
63 | 66 | <img class="thumbnail" src="${entry.images[0]}"> |
64 | 67 | % endif |
65 | 68 | <h3> |
66 | - <a href='${entry.link}' target="_blank_">${entry.title}</a> | |
69 | + <a href='${entry.url}' target="_blank_">${entry.title}</a> | |
67 | 70 | </h3> |
68 | 71 | </div> |
69 | 72 |
@@ -76,7 +79,7 @@ s.parentNode.insertBefore(ga, s); | ||
76 | 79 | <!-- フッタ --> |
77 | 80 | <div class="entry-footer"> |
78 | 81 | <div class="entry-continue"> |
79 | - <a href='${entry.link}'>[続きを読む]</a> | |
82 | + <a href='${entry.url}'>[続きを読む]</a> | |
80 | 83 | </div> |
81 | 84 | <div class="information"> |
82 | 85 | <span>情報元:<a href='${entry.feed.url}'>${entry.feed.name}</a></span> |