Browse code
Make more robust and update interface
fiddlerwoaroof authored on 25/11/2016 10:17:55
Showing 9 changed files
Showing 9 changed files
- Makefile
- alimenta-feed-archive.asd
- archive-root/index.html
- archive-root/run.js
- archive-root/style.css
- deploy.lisp
- feed-archive.lisp
- tools.lisp
- yason-encoders.lisp
... | ... |
@@ -3,14 +3,16 @@ |
3 | 3 |
:author "Fiddlerwoaroof <fiddlerwoaroof@howit.is>" |
4 | 4 |
:license "MIT" |
5 | 5 |
:depends-on (#:alexandria |
6 |
- #:alimenta |
|
7 |
- #:fwoar.lisputils |
|
8 |
- #:ironclad |
|
9 |
- #:local-time |
|
10 |
- #:serapeum |
|
11 |
- #:uiop |
|
12 |
- #:ubiquitous |
|
13 |
- #:yason) |
|
6 |
+ #:alimenta |
|
7 |
+ #:fwoar.lisputils |
|
8 |
+ #:ironclad |
|
9 |
+ #:local-time |
|
10 |
+ #:serapeum |
|
11 |
+ #:ubiquitous |
|
12 |
+ #:uiop |
|
13 |
+ #:yason) |
|
14 | 14 |
:serial t |
15 |
- :components ((:file "feed-archive"))) |
|
15 |
+ :components ((:file "tools") |
|
16 |
+ (:file "yason-encoders") |
|
17 |
+ (:file "feed-archive"))) |
|
16 | 18 |
|
... | ... |
@@ -1,51 +1,58 @@ |
1 | 1 |
<!DOCTYPE html> |
2 | 2 |
<html lang="en"> |
3 |
-<head> |
|
4 |
- <meta charset="UTF-8"> |
|
5 |
- <title></title> |
|
6 |
- <base href="<XXX: YOUR BASE URL HERE>" /> |
|
7 |
- <link rel="stylesheet" href="style.css" /> |
|
8 |
-</head> |
|
9 |
-<body> |
|
10 |
- <div id="container" v-cloak> |
|
11 |
- <div class="left"> |
|
12 |
- <span class="pull-time">{{pull_time}}</span> |
|
13 |
- <!--<div v-for="url in feed_urls">{{url}}</div>--> |
|
14 |
- <div v-for="feed in feeds"> |
|
15 |
- <a v-on:click="get_feed(feed.path)"> |
|
16 |
- <span class="feed_url">{{feed.title}} <{{feed.url}}></span> |
|
17 |
- </a> |
|
18 |
- </div> |
|
3 |
+ <head> |
|
4 |
+ <meta charset="UTF-8"> |
|
5 |
+ <title></title> |
|
6 |
+ <base href="<XXX: YOUR BASE URL HERE>" /> |
|
7 |
+ <link rel="stylesheet" href="style.css" /> |
|
8 |
+ </head> |
|
9 |
+ <body> |
|
10 |
+ <div id="container" v-cloak> |
|
11 |
+ <div class="left"> |
|
12 |
+ <div class="feeds"> |
|
13 |
+ <span class="pull-time">{{pull_time}}</span> |
|
14 |
+ <!--<div v-for="url in feed_urls">{{url}}</div>--> |
|
15 |
+ <div v-for="feed in feeds"> |
|
16 |
+ <a class="feed-link" v-on:click="get_feed(feed.path)" v-if="has_items(feed)"> |
|
17 |
+ <div class="title">{{feed.title}}</div> |
|
18 |
+ <div class="url"> |
|
19 |
+ <{{feed.url}}> |
|
20 |
+ </div> |
|
21 |
+ </a> |
|
22 |
+ </div> |
|
23 |
+ </div> |
|
19 | 24 |
|
20 |
- <div v-if="current_feed.title !== null" class="current_feed"> |
|
21 |
- <h2>{{current_feed.title}}</h2> |
|
22 |
- <h3>{{current_feed.link}}</h3> |
|
23 |
- <div>{{current_feed.description}}</div> |
|
24 |
- <ul> |
|
25 |
- <li v-for="item in current_feed.items | orderBy 'path' -1"> |
|
26 |
- <a v-on:click="get_item(item.path)"> |
|
27 |
- <span class="feed_url">{{item.title}}</span> |
|
28 |
- </a> |
|
29 |
- </li> |
|
30 |
- </ul> |
|
25 |
+ <div v-if="current_feed.title !== null && current_feed.items.length > 0" class="current_feed"> |
|
26 |
+ <h2>{{current_feed.title}}</h2> |
|
27 |
+ <h3>{{current_feed.link}}</h3> |
|
28 |
+ <div>{{current_feed.description}}</div> |
|
29 |
+ <ul> |
|
30 |
+ <li v-for="item in current_feed.items | orderBy 'path' -1"> |
|
31 |
+ <a v-on:click="get_item(item.path)"> |
|
32 |
+ <span class="feed_url">{{item.title}}</span> |
|
33 |
+ </a> |
|
34 |
+ </li> |
|
35 |
+ </ul> |
|
36 |
+ </div> |
|
31 | 37 |
</div> |
32 |
- </div> |
|
33 |
- <div class="right"> |
|
34 |
- <div v-if="current_item.title !== null"> |
|
35 |
- <h2> |
|
36 |
- <a href="{{current_item.link}}">{{current_item.title}}</a> |
|
37 |
- </h2> |
|
38 |
- <h3>{{current_item.author}}</h3> |
|
39 |
- <div>{{{ item_content }}}</div> |
|
38 |
+ <div class="right"> |
|
39 |
+ <div v-if="current_item.title !== null"> |
|
40 |
+ <h2> |
|
41 |
+ <a href="{{current_item.link}}">{{current_item.title}}</a> |
|
42 |
+ </h2> |
|
43 |
+ <h3>{{current_item.author}}</h3> |
|
44 |
+ <div>{{{ item_content }}}</div> |
|
45 |
+ </div> |
|
40 | 46 |
</div> |
41 | 47 |
</div> |
42 |
- </div> |
|
43 | 48 |
|
44 |
- <script> |
|
45 |
- baseUrl = 'current' |
|
46 |
- </script> |
|
47 |
- <script src="vue.js"></script> |
|
48 |
- <script src="purify.js"></script> |
|
49 |
- <script src="run.js"></script> |
|
50 |
-</body> |
|
49 |
+ <script> |
|
50 |
+ baseUrl = 'current' |
|
51 |
+ </script> |
|
52 |
+ <script src="vue.js"></script> |
|
53 |
+ <script src="purify.js"></script> |
|
54 |
+ <script src="es6-promise.min.js"></script> |
|
55 |
+ <script src="fetch.js"></script> |
|
56 |
+ <script src="run.js"></script> |
|
57 |
+ </body> |
|
51 | 58 |
</html> |
... | ... |
@@ -1,97 +1,130 @@ |
1 | 1 |
root = new Vue({ |
2 |
- el: '#container', |
|
3 |
- data: { |
|
4 |
- "pull_time": null, |
|
5 |
- "feed_urls": [], |
|
6 |
- feeds: { |
|
7 |
- feeds: [] |
|
8 |
- }, |
|
2 |
+ el: '#container', |
|
3 |
+ data: { |
|
4 |
+ "pull_time": null, |
|
5 |
+ "feed_urls": [], |
|
6 |
+ feeds: { |
|
7 |
+ feeds: [] |
|
8 |
+ }, |
|
9 | 9 |
|
10 |
- current_feed: { |
|
11 |
- title: null, |
|
12 |
- fetch_url: null, |
|
13 |
- link: null, |
|
14 |
- description: null, |
|
15 |
- items: [], |
|
16 |
- base_path: null, |
|
17 |
- }, |
|
10 |
+ current_feed: { |
|
11 |
+ metadata: { |
|
12 |
+ description: null, |
|
13 |
+ fetch_url: null, |
|
14 |
+ link: null, |
|
15 |
+ title: null, |
|
16 |
+ }, |
|
17 |
+ base_path: null, |
|
18 |
+ items: [], |
|
19 |
+ }, |
|
18 | 20 |
|
19 |
- current_item: { |
|
20 |
- title: null, |
|
21 |
- date: null, |
|
22 |
- author: null, |
|
23 |
- id: null, |
|
24 |
- link: null, |
|
25 |
- content: null, |
|
26 |
- }, |
|
27 |
- }, |
|
21 |
+ current_item: { |
|
22 |
+ title: null, |
|
23 |
+ date: null, |
|
24 |
+ author: null, |
|
25 |
+ id: null, |
|
26 |
+ link: null, |
|
27 |
+ content: null, |
|
28 |
+ }, |
|
28 | 29 |
|
29 |
- computed: { |
|
30 |
- item_content() { |
|
31 |
- if (this.current_item.content !== null) { |
|
32 |
- return DOMPurify.sanitize(this.current_item.content); |
|
33 |
- } |
|
30 |
+ feed_item_counts: {}, |
|
34 | 31 |
}, |
35 |
- }, |
|
36 |
- |
|
37 |
- methods: { |
|
38 |
- sanitize(html) { |
|
39 |
- return DOMPurify.sanitize(html, { |
|
40 |
- FORBID_TAG: ['style'], |
|
41 |
- FORBID_ATTR: ['style'], |
|
42 |
- }); |
|
32 |
+ |
|
33 |
+ computed: { |
|
34 |
+ item_content() { |
|
35 |
+ let result = null; |
|
36 |
+ if (this.current_item.content !== null) { |
|
37 |
+ result = DOMPurify.sanitize(this.current_item.content); |
|
38 |
+ } |
|
39 |
+ return result; |
|
40 |
+ }, |
|
43 | 41 |
}, |
44 | 42 |
|
45 |
- get_feed: function (path) { |
|
46 |
- window.fetch(path+'index.json').then((resp) => resp.json()) |
|
47 |
- .then((data) => { |
|
48 |
- var result = Object.assign({}, data); |
|
49 |
- result.fetch_url = data['fetch-url']; |
|
50 |
- result.base_path = path; |
|
51 |
- window.history.pushState({ |
|
52 |
- 'current_feed': result |
|
53 |
- }, "", window.location.pathname); |
|
54 |
- Object.assign(root.current_feed, result); |
|
55 |
- }); |
|
43 |
+ methods: { |
|
44 |
+ sanitize(html) { |
|
45 |
+ return DOMPurify.sanitize(html, { |
|
46 |
+ FORBID_TAG: ['style'], |
|
47 |
+ FORBID_ATTR: ['style'], |
|
48 |
+ }); |
|
49 |
+ }, |
|
50 |
+ |
|
51 |
+ get_remote_feed: function (path) { |
|
52 |
+ var promise = new Promise((resolve, reject) => { |
|
53 |
+ window.fetch(path+'index.json').then((resp) => resp.json()) |
|
54 |
+ .then((data) => { |
|
55 |
+ var result = Object.assign({}, data); |
|
56 |
+ result.fetch_url = data['fetch-url']; |
|
57 |
+ result.base_path = path; |
|
58 |
+ window.history.pushState({ |
|
59 |
+ 'current_feed': result |
|
60 |
+ }, "", window.location.pathname); |
|
61 |
+ resolve(result); |
|
62 |
+ return promise; |
|
63 |
+ }, reject.bind(promise)); |
|
64 |
+ }); |
|
65 |
+ return promise; |
|
66 |
+ }, |
|
67 |
+ |
|
68 |
+ get_feed: function (path) { |
|
69 |
+ this.get_remote_feed(path) |
|
70 |
+ .then((result) => |
|
71 |
+ Object.assign(root.current_feed, result)); |
|
72 |
+ }, |
|
73 |
+ |
|
74 |
+ get_item(path) { |
|
75 |
+ window.fetch(this.current_feed.base_path + path).then((resp) => resp.json()) |
|
76 |
+ .then((data) => { |
|
77 |
+ window.history.pushState({ |
|
78 |
+ 'current_feed': root.current_feed, |
|
79 |
+ 'current_item': data |
|
80 |
+ }, "", window.location.pathname); |
|
81 |
+ Object.assign(this.current_item, data); |
|
82 |
+ }); |
|
83 |
+ }, |
|
84 |
+ |
|
85 |
+ has_items(feed) { |
|
86 |
+ var count = this.feed_item_counts[feed.path]; |
|
87 |
+ return count === undefined || (count > 0); |
|
88 |
+ }, |
|
89 |
+ |
|
56 | 90 |
}, |
57 | 91 |
|
58 |
- get_item(path) { |
|
59 |
- window.fetch(this.current_feed.base_path + path).then((resp) => resp.json()) |
|
60 |
- .then((data) => { |
|
61 |
- window.history.pushState({ |
|
62 |
- 'current_feed': root.current_feed, |
|
63 |
- 'current_item': data |
|
64 |
- }, "", window.location.pathname); |
|
65 |
- Object.assign(this.current_item, data); |
|
66 |
- }); |
|
67 |
- } |
|
92 |
+ ready() { |
|
93 |
+ window.fetch(baseUrl+'/index.json').then((resp) => resp.json()) |
|
94 |
+ .then(function (data) { |
|
95 |
+ root.pull_time = data['pull-time']; |
|
96 |
+ root.feed_urls = data['feed-urls']; |
|
97 |
+ root.feeds = data.feeds; |
|
98 |
+ return data; |
|
99 |
+ }) |
|
100 |
+ .then((data) => { |
|
101 |
+ root.feeds.forEach((feed) => { |
|
102 |
+ // console.log(feed); |
|
103 |
+ this.get_remote_feed(feed.path) |
|
104 |
+ .then((feed_index) => Vue.set(this.feed_item_counts, feed.path, feed_index.items.length)); |
|
105 |
+ }); |
|
106 |
+ }); |
|
68 | 107 |
|
69 |
- } |
|
70 |
-}); |
|
108 |
+ }, |
|
71 | 109 |
|
72 |
-window.fetch(baseUrl+'/index.json').then((resp) => resp.json()) |
|
73 |
-.then(function (data) { |
|
74 |
- root.pull_time = data['pull-time']; |
|
75 |
- root.feed_urls = data['feed-urls']; |
|
76 |
- root.feeds = data.feeds; |
|
77 | 110 |
}); |
78 | 111 |
|
79 | 112 |
window.onpopstate = function (ev) { |
80 |
- console.log(ev); |
|
81 |
- var current_feed = ev.state.current_feed, current_item = ev.state.current_item; |
|
113 |
+ // console.log(ev); |
|
114 |
+ var current_feed = ev.state.current_feed, current_item = ev.state.current_item; |
|
82 | 115 |
|
83 |
- Object.assign(root.current_feed, current_feed); |
|
116 |
+ Object.assign(root.current_feed, current_feed); |
|
84 | 117 |
|
85 |
- if (current_item !== undefined) { |
|
86 |
- Object.assign(root.current_item, current_item); |
|
87 |
- } |
|
118 |
+ if (current_item !== undefined) { |
|
119 |
+ Object.assign(root.current_item, current_item); |
|
120 |
+ } |
|
88 | 121 |
}; |
89 | 122 |
|
90 | 123 |
document.addEventListener('DOMContentLoaded', function (ev) { |
91 |
- if (window.history.state !== null) { |
|
92 |
- Object.assign(root.current_feed, window.history.state.current_feed); |
|
93 |
- if (window.history.state.current_item !== undefined) { |
|
94 |
- Object.assign(root.current_item, window.history.state.current_item); |
|
124 |
+ if (window.history.state !== null) { |
|
125 |
+ Object.assign(root.current_feed, window.history.state.current_feed); |
|
126 |
+ if (window.history.state.current_item !== undefined) { |
|
127 |
+ Object.assign(root.current_item, window.history.state.current_item); |
|
128 |
+ } |
|
95 | 129 |
} |
96 |
- } |
|
97 | 130 |
}); |
... | ... |
@@ -1,3 +1,7 @@ |
1 |
+* { |
|
2 |
+ box-sizing: border-box; |
|
3 |
+} |
|
4 |
+ |
|
1 | 5 |
body { |
2 | 6 |
margin: 0px; |
3 | 7 |
font-family: 'Lato', 'Alegreya Sans', sans-serif; |
... | ... |
@@ -17,20 +21,34 @@ body { |
17 | 21 |
|
18 | 22 |
a { |
19 | 23 |
color: #28b0b1; |
24 |
+ border: thin solid transparent; |
|
25 |
+} |
|
26 |
+ |
|
27 |
+.current_feed a, .feeds a { |
|
28 |
+ display: inline-block; |
|
29 |
+ vertical-align: middle; |
|
30 |
+ text-decoration: none; |
|
31 |
+ cursor: pointer; |
|
32 |
+ /*width: 100%;*/ |
|
33 |
+ margin-right: 1em; |
|
20 | 34 |
/*text-shadow: 0em 0em 0.1em #000*/ |
21 | 35 |
/*, 0em 0em 0.2em #888;*/ |
22 | 36 |
} |
23 | 37 |
|
24 |
-a:hover, a:focus { |
|
38 |
+a:hover { |
|
39 |
+ border-color: #28b0b1; |
|
40 |
+} |
|
41 |
+ |
|
42 |
+a:focus { |
|
25 | 43 |
text-decoration: underline; |
26 | 44 |
} |
27 | 45 |
|
28 |
-.left, .right { |
|
46 |
+.right { |
|
29 | 47 |
overflow-y: auto; |
30 | 48 |
} |
31 | 49 |
|
32 | 50 |
.right { |
33 |
- width: 66vw; |
|
51 |
+ width: 50vw; |
|
34 | 52 |
height: 100vh; |
35 | 53 |
box-sizing: border-box; |
36 | 54 |
position: absolute; |
... | ... |
@@ -48,11 +66,12 @@ h1, h2, h3, h4, h5, h6 { |
48 | 66 |
|
49 | 67 |
.left { |
50 | 68 |
border-right: 4px #28b0b1 double; |
51 |
- width: 33vw; |
|
69 |
+ box-shadow: 0px 0px 1em #28b0b1; |
|
70 |
+ width: 50vw; |
|
52 | 71 |
height: 100vh; |
72 |
+ overflow: hidden; |
|
53 | 73 |
box-sizing: border-box; |
54 | 74 |
position: absolute; |
55 |
- padding: 1em; |
|
56 | 75 |
display: block; |
57 | 76 |
left: 0; |
58 | 77 |
} |
... | ... |
@@ -63,3 +82,37 @@ h1, h2, h3, h4, h5, h6 { |
63 | 82 |
display: block; |
64 | 83 |
margin: auto; |
65 | 84 |
} |
85 |
+ |
|
86 |
+.current_feed, .feeds { |
|
87 |
+ overflow-x: hidden; |
|
88 |
+ overflow-y: scroll; |
|
89 |
+} |
|
90 |
+ |
|
91 |
+.current_feed { |
|
92 |
+ height: 100vh; |
|
93 |
+ width: 25vw; |
|
94 |
+ float: right; |
|
95 |
+} |
|
96 |
+ |
|
97 |
+.feeds { |
|
98 |
+ height: 100vh; |
|
99 |
+ width: 24vw; |
|
100 |
+ float: left; |
|
101 |
+ padding: 1em; |
|
102 |
+ border-bottom: 5px #28b0b1 double; |
|
103 |
+ box-shadow: 0px 0px 1em #28b0b1; |
|
104 |
+} |
|
105 |
+ |
|
106 |
+.feeds .title, .feeds .url { |
|
107 |
+ white-space: nowrap; |
|
108 |
+} |
|
109 |
+ |
|
110 |
+.feeds .title { |
|
111 |
+ font-size: 120%; |
|
112 |
+} |
|
113 |
+ |
|
114 |
+.feeds .url { |
|
115 |
+ font-size: 75%; |
|
116 |
+ padding-left: 2.5em; |
|
117 |
+ margin-bottom: 0.5em; |
|
118 |
+} |
66 | 119 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,4 @@ |
1 |
+(load (truename "~/quicklisp/setup.lisp")) |
|
2 |
+(push "/home/edwlan/github_repos/alimenta-feed-archive/" asdf:*central-registry*) |
|
3 |
+(ql:quickload :alimenta-feed-archive) |
|
4 |
+(save-lisp-and-die "feed-archiver" :executable t :toplevel #'alimenta.feed-archive::command-line-main) |
... | ... |
@@ -1,5 +1,6 @@ |
1 | 1 |
(defpackage :alimenta.feed-archive |
2 |
- (:use :cl :alexandria :serapeum :fw.lu)) |
|
2 |
+ (:use :cl :alexandria :serapeum :fw.lu :alimenta.feed-archive.tools) |
|
3 |
+ (:shadowing-import-from :alimenta.feed-archive.tools :->)) |
|
3 | 4 |
|
4 | 5 |
(in-package :alimenta.feed-archive) |
5 | 6 |
|
... | ... |
@@ -9,97 +10,59 @@ |
9 | 10 |
(defparameter +dirname-format+ |
10 | 11 |
'((:year 4) #\- (:month 2) #\- (:day 2) #\/ (:hour 2) #\- (:min 2) #\/)) |
11 | 12 |
|
12 |
-(defmethod yason:encode ((object pathname) &optional stream) |
|
13 |
- (yason:encode (princ-to-string (uiop:native-namestring object)) |
|
14 |
- stream) |
|
15 |
- object) |
|
16 |
- |
|
17 |
-(defmethod yason:encode ((object puri:uri) &optional stream) |
|
18 |
- (yason:encode (puri:render-uri object nil) |
|
19 |
- stream) |
|
20 |
- object) |
|
21 |
- |
|
22 |
-(defun sha256-string (string) |
|
23 |
- (let* ((digester (ironclad:make-digesting-stream :sha256)) |
|
24 |
- (digest-stream (flexi-streams:make-flexi-stream digester))) |
|
25 |
- (princ string digest-stream) |
|
26 |
- (crypto:byte-array-to-hex-string |
|
27 |
- (crypto:produce-digest |
|
28 |
- digester)))) |
|
29 |
- |
|
30 | 13 |
(defun get-store-directory-name (timestamp) |
31 |
- (car |
|
32 |
- (prog1-let ((result (merge-pathnames |
|
33 |
- (local-time:format-timestring |
|
34 |
- nil |
|
35 |
- (local-time:timestamp-minimize-part timestamp :sec) |
|
36 |
- :format +dirname-format+) |
|
37 |
- *feed-base*))) |
|
38 |
- (ensure-directories-exist result)))) |
|
39 |
- |
|
40 |
-(defun fix-pathname () |
|
41 |
- (let ((restart (find-restart 'fix-pathname))) |
|
42 |
- (when restart |
|
43 |
- (invoke-restart restart)))) |
|
14 |
+ (flet ((make-dirname (timestamp) |
|
15 |
+ (merge-pathnames (local-time:format-timestring nil (local-time:timestamp-minimize-part timestamp :sec) |
|
16 |
+ :format +dirname-format+) |
|
17 |
+ *feed-base*))) |
|
18 |
+ (-> (prog1-let ((result (make-dirname timestamp))) |
|
19 |
+ (ensure-directories-exist result)) |
|
20 |
+ (car)))) |
|
21 |
+ |
|
22 |
+(defun %encode-item (root-dir item) |
|
23 |
+ (let ((restarted nil)) |
|
24 |
+ (destructuring-bind (title path) item |
|
25 |
+ (tagbody start |
|
26 |
+ (format t "~&Restarted: ~a" restarted) |
|
27 |
+ (when restarted |
|
28 |
+ (format t " ~a~%"(namestring path))) |
|
29 |
+ (restart-case |
|
30 |
+ (progn (format t "~&encoding . . .~%") |
|
31 |
+ (let ((pathname (uiop:enough-pathname path root-dir))) |
|
32 |
+ (yason:with-object () |
|
33 |
+ (yason:encode-object-element "title" title) |
|
34 |
+ (yason:encode-object-element "path" pathname)))) |
|
35 |
+ (fix-pathname () |
|
36 |
+ (setf path |
|
37 |
+ (merge-pathnames path |
|
38 |
+ (make-pathname :type :unspecific))) |
|
39 |
+ (unless restarted |
|
40 |
+ (setf restarted t) |
|
41 |
+ (go start)))))))) |
|
44 | 42 |
|
45 | 43 |
(defun encode-feed-as-json (feed item-storage-info root-dir &optional stream) |
46 |
- (declare (optimize (debug 3))) |
|
47 |
- (flet ((encode-item (item) |
|
48 |
- (let ((restarted nil)) |
|
49 |
- (destructuring-bind (title path) item |
|
50 |
- (tagbody start |
|
51 |
- (format t "~&Restarted: ~a" restarted) |
|
52 |
- (when restarted |
|
53 |
- (format t " ~a~%"(namestring path))) |
|
54 |
- (restart-case |
|
55 |
- (progn (format t "~&encoding . . .~%") |
|
56 |
- (let ((pathname (uiop:enough-pathname path root-dir))) |
|
57 |
- (yason:with-object () |
|
58 |
- (yason:encode-object-element "title" title) |
|
59 |
- (yason:encode-object-element "path" pathname)))) |
|
60 |
- (fix-pathname () |
|
61 |
- (setf path |
|
62 |
- (merge-pathnames path |
|
63 |
- (make-pathname :type :unspecific))) |
|
64 |
- (unless restarted |
|
65 |
- (setf restarted t) |
|
66 |
- (go start))))))))) |
|
67 |
- (with-accessors ((description alimenta:description) |
|
68 |
- (feed-link alimenta:feed-link) |
|
69 |
- (items alimenta:items) |
|
70 |
- (link alimenta:link) |
|
71 |
- (source-type alimenta:source-type) |
|
72 |
- (title alimenta:title)) feed |
|
73 |
- (yason:with-output (stream :indent t) |
|
74 |
- (yason:with-object () |
|
75 |
- (yason:encode-object-element "title" title) |
|
76 |
- (yason:encode-object-element "fetch-url" |
|
77 |
- (puri:render-uri feed-link nil)) |
|
78 |
- (yason:encode-object-element "link" link) |
|
79 |
- ;(yason:encode-object-element "source-type" source-type) |
|
80 |
- (yason:encode-object-element "description" description) |
|
81 |
- (yason:with-object-element ("items") |
|
82 |
- (yason:with-array () |
|
83 |
- (dolist (item item-storage-info) |
|
84 |
- (with-simple-restart (continue "Skip item ~s" (car item)) |
|
85 |
- (encode-item item)))))))))) |
|
86 |
- |
|
87 |
-(defun older-than-a-week (date) |
|
88 |
- (let ((week-ago (local-time:timestamp- (local-time:now) |
|
89 |
- 7 :day))) |
|
90 |
- (local-time:timestamp< date week-ago))) |
|
91 |
- |
|
92 |
-(defmethod get-id ((feed alimenta:feed)) |
|
93 |
- (let* ((link (alimenta:feed-link feed)) |
|
94 |
- (host (puri:uri-host link))) |
|
95 |
- (concat host "-" (sha256-string link) "/"))) |
|
96 |
- |
|
97 |
-(defun get-feed-store-name (feed directory) |
|
98 |
- (merge-pathnames (get-id feed) |
|
99 |
- directory)) |
|
44 |
+ (with-accessors ((description alimenta:description) |
|
45 |
+ (feed-link alimenta:feed-link) |
|
46 |
+ (items alimenta:items) |
|
47 |
+ (link alimenta:link) |
|
48 |
+ (source-type alimenta:source-type) |
|
49 |
+ (title alimenta:title)) feed) |
|
50 |
+ (yason:with-output (stream :indent t) |
|
51 |
+ (yason:with-object () |
|
52 |
+ (yason:encode-object-element "metadata" feed) |
|
53 |
+ (yason:with-object-element ("items") |
|
54 |
+ (yason:with-array () |
|
55 |
+ (dolist (item item-storage-info) |
|
56 |
+ (with-simple-restart (continue "Skip item ~s" (car item)) |
|
57 |
+ (%encode-item root-dir item)))))))) |
|
58 |
+ |
|
59 |
+(defmethod store ((item alimenta:item) directory) |
|
60 |
+ (prog1-let ((item-title (alimenta:title item)) |
|
61 |
+ (fn (get-item-store-name item directory))) |
|
62 |
+ (with-open-file (item-f fn :direction :output) |
|
63 |
+ (yason:encode item item-f)))) |
|
100 | 64 |
|
101 | 65 |
(defmethod store ((feed alimenta:feed) directory) |
102 |
- (declare (optimize (debug 3))) |
|
103 | 66 |
(with-accessors ((description alimenta:description) |
104 | 67 |
(feed-link alimenta:feed-link) |
105 | 68 |
(items alimenta:items) |
... | ... |
@@ -116,7 +79,6 @@ |
116 | 79 |
index))))) |
117 | 80 |
|
118 | 81 |
(defmethod store ((items sequence) directory) |
119 |
- (declare (optimize (debug 3))) |
|
120 | 82 |
(map 'list (lambda (item) (store item directory)) |
121 | 83 |
(stable-sort |
122 | 84 |
(sort (remove-if #'older-than-a-week items :key #'alimenta:date) |
... | ... |
@@ -125,17 +87,6 @@ |
125 | 87 |
#'local-time:timestamp> |
126 | 88 |
:key #'alimenta:date))) |
127 | 89 |
|
128 |
-(defmethod get-id ((item alimenta:item)) |
|
129 |
- (concatenate 'string |
|
130 |
- (local-time:format-timestring nil (alimenta:date item)) |
|
131 |
- "-" |
|
132 |
- (sha256-string (alimenta:id item)) |
|
133 |
- ".json")) |
|
134 |
- |
|
135 |
-(defun get-item-store-name (item directory) |
|
136 |
- (let ((id (get-id item))) |
|
137 |
- (merge-pathnames (make-pathname :name id) directory))) |
|
138 |
- |
|
139 | 90 |
(defmethod yason:encode ((item alimenta:item) &optional stream) |
140 | 91 |
(with-accessors ((author alimenta::author) |
141 | 92 |
(content alimenta:content) |
... | ... |
@@ -154,13 +105,12 @@ |
154 | 105 |
(yason:encode-object-element "content" content))))) |
155 | 106 |
item) |
156 | 107 |
|
157 |
-(defmethod store ((item alimenta:item) directory) |
|
158 |
- (declare (optimize (debug 3))) |
|
159 |
- (prog1-let ((item-title (alimenta:title item)) |
|
160 |
- (fn (get-item-store-name item directory))) |
|
161 |
- (with-open-file (item-f fn :direction :output) |
|
162 |
- (yason:encode item item-f)))) |
|
163 | 108 |
|
109 |
+(defun test-feed-list () |
|
110 |
+ (values '("http://feeds.feedburner.com/GamasutraFeatureArticles/" |
|
111 |
+ "http://feeds.feedburner.com/GamasutraNews/" |
|
112 |
+ "http://feeds.feedburner.com/GamasutraColumns/") |
|
113 |
+ #p"/tmp/feed-archive/")) |
|
164 | 114 |
|
165 | 115 |
(defun init-feeds (&key feed-list archive-root) |
166 | 116 |
(ubiquitous:restore 'alimenta.feed-archiver) |
... | ... |
@@ -171,14 +121,12 @@ |
171 | 121 |
(ubiquitous:defaulted-value default-root :archive :root)))) |
172 | 122 |
|
173 | 123 |
(defun add-feed (feed) |
174 |
- (declare (optimize (debug 3))) |
|
175 | 124 |
(init-feeds) |
176 | 125 |
(pushnew feed |
177 | 126 |
(ubiquitous:value :feeds) |
178 | 127 |
:test #'equalp)) |
179 | 128 |
|
180 | 129 |
(defun safe-pull-feed (feed-url) |
181 |
- (declare (optimize (debug 3))) |
|
182 | 130 |
(let ((pop-times 0)) |
183 | 131 |
(handler-bind |
184 | 132 |
((condition |
... | ... |
@@ -196,42 +144,43 @@ |
196 | 144 |
(decf pop-times))))) |
197 | 145 |
|
198 | 146 |
(defun archive-feeds () |
199 |
- (declare (optimize (debug 3))) |
|
200 |
- (multiple-value-bind (*feeds* *feed-base*) (init-feeds) |
|
201 |
- (let ((pull-time (local-time:now))) |
|
202 |
- (alimenta.pull-feed::with-user-agent ("Feed Archiver v0.1b") |
|
203 |
- (let* ((pull-directory (get-store-directory-name pull-time)) |
|
204 |
- (paths (loop for feed-url in *feeds* collect |
|
205 |
- (with-simple-restart (continue "Skip ~a" feed-url) |
|
206 |
- (let ((feed (safe-pull-feed feed-url))) |
|
207 |
- (setf (alimenta:feed-link feed) |
|
208 |
- feed-url) |
|
209 |
- (store feed pull-directory)))))) |
|
210 |
- (with-open-file (index (merge-pathnames "index.json" pull-directory) :direction :output) |
|
211 |
- (yason:with-output (index :indent t) |
|
212 |
- (yason:with-object () |
|
213 |
- (yason:encode-object-element "pull-time" (local-time:format-timestring nil pull-time)) |
|
214 |
- (yason:encode-object-element "feed-urls" *feeds*) |
|
215 |
- (yason:with-object-element ("feeds") |
|
216 |
- (yason:with-array () |
|
217 |
- (mapcar (lambda (url feed-data) |
|
218 |
- (yason:with-object () |
|
219 |
- (yason:encode-object-element "url" url) |
|
220 |
- (when feed-data |
|
221 |
- (destructuring-bind (title path) feed-data |
|
222 |
- (yason:encode-object-element "title" title) |
|
223 |
- (yason:encode-object-element "path" |
|
224 |
- (princ-to-string |
|
225 |
- (uiop:enough-pathname path *feed-base*))))))) |
|
226 |
- *feeds* |
|
227 |
- paths))))))))))) |
|
228 |
- |
|
229 |
- |
|
230 |
-(defun command-line-main () |
|
231 |
- (handler-bind ((t (lambda (c) |
|
147 |
+ (let ((pull-time (local-time:now))) |
|
148 |
+ (alimenta.pull-feed::with-user-agent ("Feed Archiver v0.1b") |
|
149 |
+ (let* ((pull-directory (get-store-directory-name pull-time)) |
|
150 |
+ (paths (loop for feed-url in *feeds* collect |
|
151 |
+ (with-simple-restart (continue "Skip ~a" feed-url) |
|
152 |
+ (let ((feed (safe-pull-feed feed-url))) |
|
153 |
+ (setf (alimenta:feed-link feed) |
|
154 |
+ feed-url) |
|
155 |
+ (store feed pull-directory)))))) |
|
156 |
+ (with-open-file (index (merge-pathnames "index.json" pull-directory) :direction :output) |
|
157 |
+ (yason:with-output (index :indent t) |
|
158 |
+ (yason:with-object () |
|
159 |
+ (yason:encode-object-element "pull-time" (local-time:format-timestring nil pull-time)) |
|
160 |
+ (yason:encode-object-element "feed-urls" *feeds*) |
|
161 |
+ (yason:with-object-element ("feeds") |
|
162 |
+ (yason:with-array () |
|
163 |
+ (mapcar (lambda (url feed-data) |
|
164 |
+ (yason:with-object () |
|
165 |
+ (yason:encode-object-element "url" url) |
|
166 |
+ (when feed-data |
|
167 |
+ (destructuring-bind (title path) feed-data |
|
168 |
+ (yason:encode-object-element "title" title) |
|
169 |
+ (yason:encode-object-element "path" |
|
170 |
+ (princ-to-string |
|
171 |
+ (uiop:enough-pathname path *feed-base*))))))) |
|
172 |
+ *feeds* |
|
173 |
+ paths)))))))))) |
|
174 |
+ |
|
175 |
+ |
|
176 |
+(defun command-line-main (&optional (feed-list-initializer #'init-feeds)) |
|
177 |
+ (handler-bind ((error (lambda (c) |
|
232 | 178 |
c |
179 |
+ (format t "~&CONDITION RECEIVED: ~S~%RESTARTS: ~s~%" c (compute-restarts c)) |
|
233 | 180 |
(if (find-restart 'fix-pathname) |
234 | 181 |
(fix-pathname) |
235 | 182 |
(progn (format t "~&Skip a feed...~%") |
236 | 183 |
(continue)))))) |
237 |
- (archive-feeds))) |
|
184 |
+ (multiple-value-bind (*feeds* *feed-base*) (funcall feed-list-initializer) |
|
185 |
+ (archive-feeds)))) |
|
186 |
+ |
238 | 187 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,67 @@ |
1 |
+(defpackage :alimenta.feed-archive.tools |
|
2 |
+ (:use :cl :alexandria :serapeum :fw.lu) |
|
3 |
+ (:shadow :->) |
|
4 |
+ (:export :fix-pathname :sha256-string :get-id :older-than-a-week :-> :get-feed-store-name |
|
5 |
+ :store :get-item-store-name)) |
|
6 |
+ |
|
7 |
+(in-package :alimenta.feed-archive.tools) |
|
8 |
+ |
|
9 |
+(defgeneric store (item directory) |
|
10 |
+ (:documentation "Store an item in a directory")) |
|
11 |
+ |
|
12 |
+(defmacro -> (&rest forms) |
|
13 |
+ (let ((forms (mapcar (lambda (form) |
|
14 |
+ (typecase form |
|
15 |
+ (list form) |
|
16 |
+ (t (list form)))) |
|
17 |
+ forms))) |
|
18 |
+ (loop with result = (car forms) |
|
19 |
+ for form in (cdr forms) |
|
20 |
+ do |
|
21 |
+ (setf result `(,(car form) |
|
22 |
+ ,result |
|
23 |
+ ,@(cdr form))) |
|
24 |
+ finally |
|
25 |
+ (return result)))) |
|
26 |
+ |
|
27 |
+(defun fix-pathname () |
|
28 |
+ (let ((restart (find-restart 'fix-pathname))) |
|
29 |
+ (when restart |
|
30 |
+ (invoke-restart restart)))) |
|
31 |
+ |
|
32 |
+(defun sha256-string (string) |
|
33 |
+ (let* ((digester (ironclad:make-digesting-stream :sha256)) |
|
34 |
+ (digest-stream (flexi-streams:make-flexi-stream digester))) |
|
35 |
+ (princ string digest-stream) |
|
36 |
+ (crypto:byte-array-to-hex-string |
|
37 |
+ (crypto:produce-digest |
|
38 |
+ digester)))) |
|
39 |
+ |
|
40 |
+(defgeneric get-id (feed) |
|
41 |
+ (:documentation "Get an identifier for a feed")) |
|
42 |
+ |
|
43 |
+(defmethod get-id ((feed alimenta:feed)) |
|
44 |
+ (let* ((link (alimenta:feed-link feed)) |
|
45 |
+ (host (puri:uri-host link))) |
|
46 |
+ (concat host "-" (sha256-string link) "/"))) |
|
47 |
+ |
|
48 |
+(defmethod get-id ((item alimenta:item)) |
|
49 |
+ (concatenate 'string |
|
50 |
+ (local-time:format-timestring nil (alimenta:date item)) |
|
51 |
+ "-" |
|
52 |
+ (sha256-string (alimenta:id item)) |
|
53 |
+ ".json")) |
|
54 |
+ |
|
55 |
+(defun get-item-store-name (item directory) |
|
56 |
+ (let ((id (get-id item))) |
|
57 |
+ (merge-pathnames (make-pathname :name id) directory))) |
|
58 |
+ |
|
59 |
+(defun get-feed-store-name (feed directory) |
|
60 |
+ (merge-pathnames (get-id feed) |
|
61 |
+ directory)) |
|
62 |
+ |
|
63 |
+(defun older-than-a-week (date) |
|
64 |
+ (let ((week-ago (local-time:timestamp- (local-time:now) |
|
65 |
+ 7 :day))) |
|
66 |
+ (local-time:timestamp< date week-ago))) |
|
67 |
+ |
0 | 68 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,30 @@ |
1 |
+(defmethod yason:encode ((object pathname) &optional stream) |
|
2 |
+ (yason:encode (princ-to-string (uiop:native-namestring object)) |
|
3 |
+ stream) |
|
4 |
+ object) |
|
5 |
+ |
|
6 |
+(defmethod yason:encode ((object puri:uri) &optional stream) |
|
7 |
+ (yason:encode (puri:render-uri object nil) |
|
8 |
+ stream) |
|
9 |
+ object) |
|
10 |
+ |
|
11 |
+(defmethod yason:encode-slots progn ((feed alimenta:feed)) |
|
12 |
+ (with-accessors ((description alimenta:description) |
|
13 |
+ (feed-link alimenta:feed-link) |
|
14 |
+ (items alimenta:items) |
|
15 |
+ (link alimenta:link) |
|
16 |
+ (source-type alimenta:source-type) |
|
17 |
+ (title alimenta:title)) feed |
|
18 |
+ (yason:encode-object-element "title" title) |
|
19 |
+ (yason:encode-object-element "fetch-url" |
|
20 |
+ (puri:render-uri feed-link nil)) |
|
21 |
+ (yason:encode-object-element "link" link) |
|
22 |
+ ;;(yason:encode-object-element "source-type" source-type) |
|
23 |
+ (yason:encode-object-element "description" description)) |
|
24 |
+ feed) |
|
25 |
+ |
|
26 |
+ |
|
27 |
+(defmethod yason:encode ((feed alimenta:feed) &optional stream) |
|
28 |
+ (yason:with-output (stream :indent t) |
|
29 |
+ (yason:encode-object feed))) |
|
30 |
+ |