diff options
author | Case Duckworth | 2022-07-17 23:10:22 -0500 |
---|---|---|
committer | Case Duckworth | 2022-07-17 23:10:22 -0500 |
commit | 6aa64d77e0f8ef2e42bdb90bc5ed3f6557e3ed81 (patch) | |
tree | d54f6332408be6add8865714cb2bf1ef54160bdf | |
download | sfeed-6aa64d77e0f8ef2e42bdb90bc5ed3f6557e3ed81.tar.gz sfeed-6aa64d77e0f8ef2e42bdb90bc5ed3f6557e3ed81.zip |
Initial commit
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | mars-eyes.png | bin | 0 -> 4907 bytes | |||
-rwxr-xr-x | runsfeed | 77 | ||||
-rwxr-xr-x | scripts/get-feed.sh | 59 | ||||
-rwxr-xr-x | sfeed_html.sh | 148 | ||||
-rwxr-xr-x | sfeed_update_xargs | 45 | ||||
-rw-r--r-- | sfeedrc | 225 | ||||
-rw-r--r-- | style.css | 176 |
8 files changed, 731 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..865369a --- /dev/null +++ b/.gitignore | |||
@@ -0,0 +1 @@ | |||
feeds/ \ No newline at end of file | |||
diff --git a/mars-eyes.png b/mars-eyes.png new file mode 100644 index 0000000..2e62c21 --- /dev/null +++ b/mars-eyes.png | |||
Binary files differ | |||
diff --git a/runsfeed b/runsfeed new file mode 100755 index 0000000..f987a92 --- /dev/null +++ b/runsfeed | |||
@@ -0,0 +1,77 @@ | |||
1 | #!/usr/bin/env bash | ||
2 | # Run sfeed | ||
3 | |||
4 | # set -euo pipefail | ||
5 | |||
6 | main() { | ||
7 | export SFEED_CONFIG="$HOME/.sfeed" | ||
8 | # SFEED_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/sfeed" | ||
9 | export SFEED_DATA="$HOME/.sfeed" | ||
10 | # SFEED_DATA="${XDG_DATA_HOME:-$HOME/.local/share}/sfeed" | ||
11 | export SFEED_OUTPUT="$HOME/.sfeed" | ||
12 | # SFEED_OUTPUT=/var/www/sfeed | ||
13 | export sfeedrc="$SFEED_CONFIG/sfeedrc" | ||
14 | export sfeedpath="$SFEED_DATA/feeds" | ||
15 | test -d "$(dirname "$sfeedrc")" || mkdir -p "$(dirname "$sfeedrc")" | ||
16 | test -d "$sfeedpath" || mkdir -p "$sfeedpath" | ||
17 | |||
18 | log Updating feeds... | ||
19 | update "$sfeedrc" | ||
20 | log Generating HTML... | ||
21 | html "$sfeedpath"/* >"$SFEED_OUTPUT/index.html" | ||
22 | log | ||
23 | LIMIT=0 html "$sfeedpath"/* >"$SFEED_OUTPUT/feeds.html" | ||
24 | log | ||
25 | log Generating RSS... | ||
26 | atom "$sfeedpath" >"$SFEED_OUTPUT/feeds.xml" | ||
27 | atom "$sfeedpath" 7 >"$SFEED_OUTPUT/feeds-short.xml" | ||
28 | log Generating OPML... | ||
29 | opml "$sfeedrc" >"$SFEED_OUTPUT/feeds.opml" | ||
30 | log Done. | ||
31 | } | ||
32 | |||
33 | log() { | ||
34 | printf '%s\n' "$*" >&2 | ||
35 | } | ||
36 | |||
37 | update() { | ||
38 | cmd="$(command -v sfeed_update_xargs || echo ./sfeed_update_xargs)" | ||
39 | "$cmd" "$@" | ||
40 | } | ||
41 | |||
42 | opml() { | ||
43 | sfeed_opml_export "$@" | ||
44 | } | ||
45 | |||
46 | html() { | ||
47 | converter="$(command -v sfeed_html.sh || echo ./sfeed_html.sh)" | ||
48 | "$converter" "$@" | ||
49 | } | ||
50 | |||
51 | atom() ( # atom DIRECTORY [DAYS] | ||
52 | curd="$PWD" | ||
53 | cd "$1" || return 1 | ||
54 | if [ $# -eq 2 ]; then | ||
55 | old=$(($(date +%s) - ($2 * 24 * 3600))) | ||
56 | else | ||
57 | old=0 | ||
58 | fi | ||
59 | awk -F $'\t' -v old="$old" \ | ||
60 | 'BEGIN{OFS="\t"} int($1)>=old{$2="["FILENAME"] "$2;print}' \ | ||
61 | * | | ||
62 | sort -k1,1rn | | ||
63 | sfeed_atom | ||
64 | ) | ||
65 | |||
66 | sfeed_archive() ( # sfeed_archive FEED ... | ||
67 | for feed; do | ||
68 | awk -v old="$(($(date +%s) - (15 * 24 * 3600)))" \ | ||
69 | -F '\t' 'int($1) > old' <"$feed" >"$feed.new" | ||
70 | mv "$feed" "$feed.old" | ||
71 | mv "$feed.new" "$feed" | ||
72 | done | ||
73 | ) | ||
74 | |||
75 | if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then | ||
76 | main "$@" | ||
77 | fi | ||
diff --git a/scripts/get-feed.sh b/scripts/get-feed.sh new file mode 100755 index 0000000..f000fee --- /dev/null +++ b/scripts/get-feed.sh | |||
@@ -0,0 +1,59 @@ | |||
1 | #!/bin/sh | ||
2 | |||
3 | main() { | ||
4 | # get-feed.sh URL(str) => DIRECTIVE(feed_directive) | ||
5 | url="$1" | ||
6 | wp="$(mktemp /tmp/get-feed.XXXXXX)" | ||
7 | curl -sL "$url" >"$wp" | ||
8 | case "$url" in | ||
9 | *html) # We know it's a webpage | ||
10 | type=html | ||
11 | ;; | ||
12 | *xml) # We know it's a feed | ||
13 | type=xml | ||
14 | ;; | ||
15 | *) # Not sure | ||
16 | type="$(head -n1 "$wp")" | ||
17 | ;; | ||
18 | esac | ||
19 | case "$type" in | ||
20 | *xml*) # a feed | ||
21 | title="$(get_title_xml <"$wp")" | ||
22 | output_feed "$title" "$url" | ||
23 | ;; | ||
24 | *html*) # a webpage | ||
25 | cat "$wp" | sfeed_web | cut -f1 | | ||
26 | while read u; do | ||
27 | title="$(curl -sL "$u" | get_title_xml)" | ||
28 | output_feed "$title" "$u" | ||
29 | done | ||
30 | ;; | ||
31 | *) | ||
32 | echo >&2 "Don't know type \"$type\"." | ||
33 | exit 1 | ||
34 | ;; | ||
35 | esac | ||
36 | } | ||
37 | |||
38 | output_feed() { | ||
39 | ## output_feed TITLE(str) URL(str) => FEED_DIRECTIVE(str) | ||
40 | printf "feed \"%s\" '%s'\n" "$1" "$2" | ||
41 | } | ||
42 | |||
43 | get_title_xml() { | ||
44 | ## get_title_xml < FILE => TITLE(str) | ||
45 | awk ' | ||
46 | /<channel>/ { channel = 1; }/<item>/{ channel = 0; } | ||
47 | channel && $0 ~ /<title>/ { title = 1; } | ||
48 | title { | ||
49 | if (match($0,/<\/title>/)) title = 0; | ||
50 | gsub(/<\/?title>/,""); | ||
51 | sub(/^[ \t]*/,""); | ||
52 | sub(/[ \t]*$/,""); | ||
53 | print; | ||
54 | } | ||
55 | channel && $0 ~ /<\/title>/ { title = 0; } | ||
56 | ' | ||
57 | } | ||
58 | |||
59 | main "$@" | ||
diff --git a/sfeed_html.sh b/sfeed_html.sh new file mode 100755 index 0000000..58fce30 --- /dev/null +++ b/sfeed_html.sh | |||
@@ -0,0 +1,148 @@ | |||
1 | #!/usr/bin/env bash | ||
2 | |||
3 | echo() { printf '%s\n' "$*"; } | ||
4 | |||
5 | html() { | ||
6 | : "${LIMIT:=1}" | ||
7 | aside="$(mktemp /tmp/sfeed_html_aside.XXXXXX)" | ||
8 | cat <<EOF | ||
9 | $(html_head) | ||
10 | <body> | ||
11 | <header> | ||
12 | <h1> | ||
13 | <a href="index.html"><img src="mars-eyes.png" | ||
14 | title="$(fortune)" | ||
15 | width="40" height="39" | ||
16 | alt="mars, but with eyes" /></a> | ||
17 | Planet ACDW</h1> | ||
18 | <p class="last-updated">last updated at <time>$(date -R)</time></p> | ||
19 | </header> | ||
20 | <nav> | ||
21 | <a href="feeds.html">all feeds</a> | ||
22 | // | ||
23 | <a href="feeds.xml">rss (full)</a> | ||
24 | // | ||
25 | <a href="feeds-short.xml">rss (short)</a> | ||
26 | // | ||
27 | <a href="feeds.opml">opml</a> | ||
28 | </nav> | ||
29 | <main> | ||
30 | $(html_main "$@") | ||
31 | <aside><ul>$(cat "$aside")</ul></aside> | ||
32 | </main> | ||
33 | </body> | ||
34 | </html> | ||
35 | EOF | ||
36 | rm "$aside" | ||
37 | } | ||
38 | |||
39 | html_head() { | ||
40 | cat <<EOF | ||
41 | <!DOCTYPE html> | ||
42 | <html> | ||
43 | <head> | ||
44 | <meta charset="utf-8"> | ||
45 | <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||
46 | <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> | ||
47 | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
48 | <title>Planet ACDW</title> | ||
49 | <link rel="stylesheet" type="text/css" href="style.css"> | ||
50 | <link rel="shortcut icon" type="image/png" href="mars-eyes.png"> | ||
51 | <link rel="alternate" type="application/atom+xml" title="rss (full)" href="feeds.xml"> | ||
52 | <link rel="alternate" type="application/atom+xml" title="rss (short)" href="feeds-short.xml"> | ||
53 | <link rel="alternate" type="application/xml" title="opml" href="feeds.opml"> | ||
54 | </head> | ||
55 | EOF | ||
56 | } | ||
57 | |||
58 | html_main() { | ||
59 | cat <<EOF | ||
60 | <section id="list"> | ||
61 | $(for file in "$@"; do html_feed "$file"; done) | ||
62 | </section> | ||
63 | EOF | ||
64 | } | ||
65 | |||
66 | html_feed() { # html_feed FEED(file) => HTML | ||
67 | filename="$(basename "$1")" | ||
68 | now="$(date +%s)" | ||
69 | fresh_days=7 | ||
70 | fresh_secs="$((fresh_days * 24 * 60 * 60))" | ||
71 | |||
72 | ## ENTRIES | ||
73 | entries="$(awk -v NOW="$now" -v FRESH_SECS="$fresh_secs" \ | ||
74 | -v NAME="$filename" -v ASIDE="$aside" -v limit="$LIMIT" \ | ||
75 | 'BEGIN { FS="\t"; fresh_feed = 0; FRESH = (NOW - FRESH_SECS); } | ||
76 | function unescape(t) { | ||
77 | t = html_escape(t); | ||
78 | gsub(/\\\t/,"\t",t); | ||
79 | gsub(/\\\n/,"\n",t); | ||
80 | gsub(/\\\\/,"\\",t); | ||
81 | return t | ||
82 | } | ||
83 | function html_escape(t) { | ||
84 | gsub(/</,"\\<",t); | ||
85 | gsub(/>/,"\\>",t); | ||
86 | gsub(/&/,"\\&",t); | ||
87 | return t | ||
88 | } | ||
89 | { | ||
90 | timestamp=$1; | ||
91 | title=html_escape($2); | ||
92 | link=$3; | ||
93 | content=unescape($4); | ||
94 | content_type=$5; | ||
95 | id=$6; | ||
96 | author=$7; | ||
97 | enclosure=$8; | ||
98 | category=$9; | ||
99 | |||
100 | if (limit && (timestamp < (NOW - (FRESH_SECS * 3)))) next; | ||
101 | show_in_sidebar = 1; | ||
102 | #print timestamp, title, link > "/dev/stderr"; | ||
103 | |||
104 | date_cmd = "date -d \"@" timestamp "\" +\"%F %R\"" | ||
105 | if (timestamp) { | ||
106 | date_cmd | getline ts; | ||
107 | close(date_cmd); | ||
108 | } | ||
109 | |||
110 | fresh = (timestamp >= FRESH) | ||
111 | if (fresh) fresh_feed = 1; | ||
112 | |||
113 | print "<tr class=\"entry " (fresh ? "fresh" : "") "\">" | ||
114 | print "<td class=\"entry-timestamp\">" ts "</td>" | ||
115 | printf "%s", "<td class=\"entry-extra\">" | ||
116 | if (enclosure) { | ||
117 | stamp = "@" | ||
118 | printf "%s", "<a href=\"" enclosure "\" target=\"_blank\">" stamp "</a>" | ||
119 | } | ||
120 | if ((link != id) && (id != enclosure) && (id ~ /^https?:/)) { | ||
121 | stamp = "#" | ||
122 | printf "%s", "<a href=\"" id "\" target=\"_blank\">" stamp "</a>" | ||
123 | } | ||
124 | print "</td>" | ||
125 | print "<td class=\"entry-title\"><a href=\"" link "\" target=\"_blank\">" title "</a></td>" | ||
126 | print "</tr>" | ||
127 | } | ||
128 | END { | ||
129 | if (show_in_sidebar) { | ||
130 | printf "<li%s>", (fresh_feed?" class=\"fresh\"":"") >> ASIDE | ||
131 | printf "<a href=\"#%s\">%s</a></li>\n", NAME, NAME >> ASIDE | ||
132 | } | ||
133 | printf "%s", (stamp ? stamp : ".") > "/dev/stderr" | ||
134 | }' "$1")" | ||
135 | if [ -z "$entries" ]; then return 1; fi | ||
136 | echo "<section id=\"$filename\">" | ||
137 | # TODO: Include a link back to the website | ||
138 | printf '<header><h2><a href="#%s">#</a> %s</h2>\n' "$filename" "$filename" | ||
139 | printf '<a class="top" href="#">%s</a></header>' "[back to top]" | ||
140 | echo "<table class=\"entries\">" | ||
141 | echo "$entries" | ||
142 | echo "</table>" | ||
143 | echo "</section>" | ||
144 | } | ||
145 | |||
146 | if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then | ||
147 | html "$@" | ||
148 | fi | ||
diff --git a/sfeed_update_xargs b/sfeed_update_xargs new file mode 100755 index 0000000..f8ee8e7 --- /dev/null +++ b/sfeed_update_xargs | |||
@@ -0,0 +1,45 @@ | |||
1 | #!/bin/sh | ||
2 | # -*- sh -*- | ||
3 | # update feeds, merge with old feeds using xargs in parallel mode (non-POSIX). | ||
4 | |||
5 | # include script and reuse its functions, but do not start main(). | ||
6 | SFEED_UPDATE_INCLUDE="1" . sfeed_update | ||
7 | # load config file, sets $config. | ||
8 | loadconfig "$1" | ||
9 | |||
10 | # process a single feed. | ||
11 | # args are: config, tmpdir, name, feedurl, basesiteurl, encoding | ||
12 | if [ "${SFEED_UPDATE_CHILD}" = "1" ]; then | ||
13 | sfeedtmpdir="$2" | ||
14 | _feed "$3" "$4" "$5" "$6" | ||
15 | exit $? | ||
16 | fi | ||
17 | |||
18 | # ...else parent mode: | ||
19 | |||
20 | # feed(name, feedurl, basesiteurl, encoding) | ||
21 | feed() { | ||
22 | # workaround: *BSD xargs doesn't handle empty fields in the middle. | ||
23 | name="${1:-$$}" | ||
24 | feedurl="${2:-http://}" | ||
25 | basesiteurl="${3:-${feedurl}}" | ||
26 | encoding="$4" | ||
27 | |||
28 | printf '%s\0%s\0%s\0%s\0%s\0%s\0' "${config}" "${sfeedtmpdir}" \ | ||
29 | "${name}" "${feedurl}" "${basesiteurl}" "${encoding}" | ||
30 | } | ||
31 | |||
32 | # fetch feeds and store in temporary directory. | ||
33 | sfeedtmpdir="$(mktemp -d '/tmp/sfeed_XXXXXX')" | ||
34 | mkdir -p "${sfeedtmpdir}/feeds" | ||
35 | touch "${sfeedtmpdir}/ok" | ||
36 | # make sure path exists. | ||
37 | mkdir -p "${sfeedpath}" | ||
38 | # print feeds for parallel processing with xargs. | ||
39 | feeds | SFEED_UPDATE_CHILD="1" xargs -r -0 -P "${maxjobs}" -L 6 "$(readlink -f "$0")" | ||
40 | status=$? | ||
41 | # check error exit status indicator for parallel jobs. | ||
42 | test -f "${sfeedtmpdir}/ok" || status=1 | ||
43 | # cleanup temporary files etc. | ||
44 | cleanup | ||
45 | exit ${status} | ||
diff --git a/sfeedrc b/sfeedrc new file mode 100644 index 0000000..c5e3001 --- /dev/null +++ b/sfeedrc | |||
@@ -0,0 +1,225 @@ | |||
1 | # -*- sh -*- | ||
2 | |||
3 | # SFEED="$HOME/.sfeed" | ||
4 | USER_AGENT='Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101 Firefox/78.0' | ||
5 | # sfeedpath="$SFEED/feeds" | ||
6 | maxjobs="$(nproc)" | ||
7 | |||
8 | ### Feeds ####################################################### | ||
9 | |||
10 | feeds() { | ||
11 | feeds_planets | ||
12 | feeds_youtube | ||
13 | feeds_podcasts | ||
14 | feeds_friends | ||
15 | feeds_news | ||
16 | feeds_smolweb | ||
17 | feeds_comics | ||
18 | feeds_misc | ||
19 | feeds_me | ||
20 | } | ||
21 | |||
22 | feeds_planets() { | ||
23 | feed "Planet Emacs" "https://planet.emacslife.com/atom.xml" | ||
24 | feed "Planet Lisp" "https://planet.lisp.org/rss20.xml" | ||
25 | feed "Planet Scheme" "https://planet.scheme.org/atom.xml" | ||
26 | } | ||
27 | |||
28 | feeds_youtube() { | ||
29 | feed "3Blue1Brown" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCYO_jab_esuFRV4b17AJtAw' | ||
30 | feed "AB - Ancienne Belgique" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCUVAw2kdxJlcfCdEcdgXv5A' | ||
31 | feed "Abraham Moller" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCMfIwe2KHD2XoBO2lWqeFXg' | ||
32 | feed "Adam Ragusea" 'https://www.youtube.com/feeds/videos.xml?channel_id=UC9_p50tH3WmMslWRWKnM7dQ' | ||
33 | feed "Babish Culinary Universe" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCJHA_jMfCvEnv-3kRjTCQXw' | ||
34 | feed "Baggers" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCMV8p6Lb-bd6UZtTc_QD4zA' | ||
35 | feed "Case Duckworth" 'https://www.youtube.com/feeds/videos.xml?channel_id=UC92gRJdnUYklVu4pvj9n0Lw' | ||
36 | feed "Claire Saffitz x Dessert Person" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCvw6Y1kr_8bp6B5m1dqNyiw' | ||
37 | feed "Computerphile" 'https://www.youtube.com/feeds/videos.xml?channel_id=UC9-y-6csu5WGm29I7JiwpnA' | ||
38 | feed "EBRPL Career Center" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCIvntuaxP7PyaJDeHE_9E8Q' | ||
39 | feed "EmacsConf and Emacs hangouts" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCwuyodzTl_KdEKNuJmeo99A' | ||
40 | feed "freeCodeCamp.org" 'https://www.youtube.com/feeds/videos.xml?channel_id=UC8butISFwT-Wl7EV0hUK0BQ' | ||
41 | feed "Gavin Freeborn" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCJetJ7nDNLlEzDLXv7KIo0w' | ||
42 | feed "Henry Homesweet" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCZqjwc1Wy5t1rsviYYsJiYg' | ||
43 | feed "Howard Abrams" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCVHICXXtKG7rZgtC5xonNdQ' | ||
44 | feed "Ignite Talks" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCZotK8ZPTUNLMeW5Q6T0cKg' | ||
45 | feed "Jake B" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCBMMB7Yi0eyFuY95Qn2o0Yg' | ||
46 | feed "James Tomasino" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCbTp1BYjpuhDRG5OmgIT8iw' | ||
47 | feed "jan Misali" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCJOh5FKisc0hUlEeWFBlD-w' | ||
48 | feed "J Duckworth Animations" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCtAEaNVrNxAUy2VSRPD_PYQ' | ||
49 | feed "Jelle's Marble Runs" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCYJdpnjuSWVOLgGT9fIzL0g' | ||
50 | feed "John Kitchin" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCQp2VLAOlvq142YN3JO3y8w' | ||
51 | feed "karthik" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCbh_g91w0T6OYp40xFrtnhA' | ||
52 | feed "Ken Forkish" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCvVvFZd0e86bLbd5FdgYiUg' | ||
53 | feed "Lex Fridman" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCSHZKyawb77ixDdsGog4iWA' | ||
54 | feed "LockPickingLawyer" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCm9K6rby98W8JigLoZOh6FQ' | ||
55 | feed "Maangchi" 'https://www.youtube.com/feeds/videos.xml?channel_id=UC8gFadPgK2r1ndqLI04Xvvw' | ||
56 | feed "Mike Zamansky" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCxkMDXQ5qzYOgXPRnOBrp1w' | ||
57 | feed "MIT OpenCourseWare" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCEBb1b_L6zDS3xTUrIALZOw' | ||
58 | feed "My Analog Journal" 'https://www.youtube.com/feeds/videos.xml?channel_id=UC8TZwtZ17WKFJSmwTZQpBTA' | ||
59 | feed "Nat's What I Reckon" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCEFW1E8QzP-hKxjO2Rj68wg' | ||
60 | feed "Now You See It" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCWTFGPpNQ0Ms6afXhaWDiRw' | ||
61 | feed "Numberphile" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCoxcjq-8xIDTYp3uz647V5A' | ||
62 | feed "Philosophy Tube" 'https://www.youtube.com/feeds/videos.xml?channel_id=UC2PA-AKmVpU6NKCGtZq_rKQ' | ||
63 | feed "PronunciationManual" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCqDSLtXeZsGc3dtVb5MW13g' | ||
64 | feed "Protesilaos Stavrou" 'https://www.youtube.com/feeds/videos.xml?channel_id=UC0uTPqBCFIpZxlz_Lv1tk_g' | ||
65 | feed "RailCowGirl" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCj-Xm8j6WBgKY8OG7s9r2vQ' | ||
66 | feed "Simone Giertz" 'https://www.youtube.com/feeds/videos.xml?channel_id=UC3KEoMzNz8eYnwBC34RaKCQ' | ||
67 | feed "Steve Yegge" 'https://www.youtube.com/feeds/videos.xml?channel_id=UC2RCcnTltR3HMQOYVqwmweA' | ||
68 | feed "System Crafters" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCAiiOTio8Yu69c3XnR7nQBQ' | ||
69 | feed "Tasting History with Max Miller" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCsaGKqPZnGp_7N80hcHySGQ' | ||
70 | feed "Technology Connections" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCy0tKL1T7wFoYcxCe0xjN6Q' | ||
71 | feed "Too Many Zooz" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCtjXVqMVzBIgU0SO8AV0vPg' | ||
72 | feed "Townsends" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCxr2d4As312LulcajAkKJYw' | ||
73 | feed "Unitarian Church of Baton Rouge" 'https://www.youtube.com/feeds/videos.xml?channel_id=UClrqHvbiFM-1hn931ZmAPFw' | ||
74 | feed "Vulf" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCtWuB1D_E3mcyYThA9iKggQ' | ||
75 | feed "WFTDA: Women's Flat Track Derby Association" 'https://www.youtube.com/feeds/videos.xml?channel_id=UC7eMWpvytqd3gYAqxTl9w7g' | ||
76 | feed "Zach Anner" 'https://www.youtube.com/feeds/videos.xml?channel_id=UCPTVYxUoYWhNa8J7GzIGnyQ' | ||
77 | } | ||
78 | |||
79 | feeds_podcasts() { | ||
80 | feed "Tilde Whirl Tildeverse Podcast" 'https://tilde.town/~dozens/podcast/rss.xml' | ||
81 | feed "trash cat tech cat" 'https://podcast.librepunk.club/tctc/ogg.xml' | ||
82 | feed "Hacker Public Radio" 'https://hackerpublicradio.org/hpr_ogg_rss.php' | ||
83 | } | ||
84 | |||
85 | feeds_friends() { | ||
86 | # nihilazo | ||
87 | feed "lipu pi jan Niko" 'https://tilde.town/~nihilazo/index.xml' | ||
88 | # dozens | ||
89 | feed "chrismanbrown.gitlab.io (dozens)" 'https://chrismanbrown.gitlab.io/rss.xml' | ||
90 | feed "Dozens and Dragons" 'https://dozensanddragons.neocities.org/rss.xml' | ||
91 | feed "dozens: Society For Putting Things On Top Of Other Things" 'https://society.neocities.org/rss.xml' | ||
92 | feed "dozens: vgnfdblg" 'https://supervegan.neocities.org/feed.xml' | ||
93 | feed "dozens: backgammon" 'http://tilde.town/~dozens/backgammon/rss.xml' | ||
94 | feed "dozens: It's Pro Toad and Superb Owl" 'https://git.tilde.town/dozens/protoadandsuperbowl/raw/branch/master/feed.xml' | ||
95 | feed "dozens dreams" 'https://tilde.team/~dozens/dreams/rss.xml' | ||
96 | feed "dozens: write.as" 'https://write.tildeverse.org/dozens/feed/' | ||
97 | feed "dozens css art" 'http://tilde.town/~dozens/cssart/feed.xml' | ||
98 | ### | ||
99 | feed "Benjamin Wil" 'https://benjaminwil.info/feed.xml' | ||
100 | feed "(lambda (x) (create x))" 'http://lambdacreate.com/static/feed.rss' | ||
101 | feed "m455.casa" 'https://m455.casa/feed.rss' | ||
102 | feed "Oatmeal" 'https://eli.li/feed.rss' | ||
103 | feed "RSRSSS" 'https://envs.net/~lucidiot/rsrsss/feed.xml' | ||
104 | feed "Tomasino Blog" 'https://blog.tomasino.org/index.xml' | ||
105 | feed "Tomasino Labs" 'https://labs.tomasino.org/index.xml' | ||
106 | feed "Will's Blog" 'https://wflewis.com/feed.xml' | ||
107 | feed "Rick Carlino's Blog" 'https://rickcarlino.com/rss/feed.rss' | ||
108 | feed "Causal Agency" 'https://text.causal.agency/feed.atom' | ||
109 | feed "Benoit Joly" 'https://blog.benoitj.ca/posts/index.xml' | ||
110 | feed "p1k3::feed" 'https://p1k3.com/feed' | ||
111 | feed "linkbudz" 'https://linkbudz.m455.casa/feed.rss' | ||
112 | feed "Alex Schroeder" "https://alexschroeder.ch/wiki/feed/full/" | ||
113 | feed "Björn Wärmedal" "https://warmedal.se/~bjorn/atom.xml" | ||
114 | feed "a rickety bridge of impossible crossing" "https://bluelander.bearblog.dev/feed/" | ||
115 | } | ||
116 | |||
117 | feeds_comics() { | ||
118 | feed "Cat and Girl" 'https://catandgirl.com/feed/' | ||
119 | feed "Dinosaur Comics!" 'https://qwantz.com/rssfeed.php' | ||
120 | feed "False Knees" 'https://falseknees.tumblr.com/rss' | ||
121 | feed "Saturday Morning Breakfast Cereal" 'https://www.smbc-comics.com/comic/rss' | ||
122 | feed "xkcd" 'https://xkcd.com/atom.xml' | ||
123 | } | ||
124 | |||
125 | feeds_news() { | ||
126 | feed "tilde news: Private feed for acdw" \ | ||
127 | 'https://tilde.news/rss?token=FvdFj8rQkhrBy9j1yON1t6RYKDdcuG1MoUlyvRICmbgDGCf2JTWAEObDhdgt' | ||
128 | feed "Tildes Atom feed" 'https://tildes.net/topics.atom' | ||
129 | feed "NPR" "https://feeds.npr.org/1001/rss.xml" | ||
130 | } | ||
131 | |||
132 | feeds_me() { | ||
133 | : | ||
134 | } | ||
135 | |||
136 | feeds_smolweb() { | ||
137 | feed "~town friday postcard" 'https://tilde.town/~lucidiot/fridaypostcard.xml' | ||
138 | feed "Cosmic Voyage" 'https://cosmic.voyage/rss.xml' | ||
139 | feed "plan.cat" 'https://plan.cat/rss' | ||
140 | } | ||
141 | |||
142 | feeds_misc() { | ||
143 | feed "Crystalverse" 'https://crystalverse.com/feed/' | ||
144 | feed "Hetzner" 'https://status.hetzner.com/en.atom' | ||
145 | feed "LOW-TECH MAGAZINE" 'https://feeds2.feedburner.com/typepad/krisdedecker/lowtechmagazineenglish' | ||
146 | } | ||
147 | |||
148 | ### Filter ###################################################### | ||
149 | |||
150 | filter() { | ||
151 | case "$1" in | ||
152 | # Filter items based on feed name. | ||
153 | *NPR*) | ||
154 | sed 's@www\.npr\.org@text.npr.org@' | ||
155 | ;; | ||
156 | *) cat ;; | ||
157 | esac | | ||
158 | filter_add_empties | | ||
159 | filter_html_entities | | ||
160 | filter_embed_youtube | | ||
161 | filter_filter_links | ||
162 | } | ||
163 | |||
164 | filter_add_empties() { | ||
165 | awk 'BEGIN{FS="\t";OFS=FS;} | ||
166 | { $2 = $2 ? $2 : "[empty]" } | ||
167 | { print $1,$2,$3,$4,$5,$6,$7,$8,$9; } | ||
168 | ' | ||
169 | } | ||
170 | |||
171 | filter_embed_youtube() { | ||
172 | # replace youtube links with embed links | ||
173 | sed 's@www.youtube.com/watch?v=@www.youtube.com/embed/@g' | ||
174 | } | ||
175 | |||
176 | filter_filter_links() { | ||
177 | # shorten feedburner links and strip tracking parameters and pixels | ||
178 | awk -F '\t' 'BEGIN { OFS = "\t"; } | ||
179 | function filterlink(s) { | ||
180 | # protocol must start with http, https or gopher. | ||
181 | if (match(s, /^(http|https|gopher):\/\//) == 0) { | ||
182 | return ""; | ||
183 | } | ||
184 | # shorten feedburner links. | ||
185 | if (match(s, /^(http|https):\/\/[^\/]+\/~r\/.*\/~3\/[^\/]+\//)) { | ||
186 | s = substr($3, RSTART, RLENGTH); | ||
187 | } | ||
188 | # strip tracking parameters | ||
189 | # urchin, facebook, piwik, webtrekk and generic. | ||
190 | gsub(/\?(ad|campaign|fbclid|pk|tm|utm|wt)_([^&]+)/, "?", s); | ||
191 | gsub(/&(ad|campaign|fbclid|pk|tm|utm|wt)_([^&]+)/, "", s); | ||
192 | gsub(/\?&/, "?", s); | ||
193 | gsub(/[\?&]+$/, "", s); | ||
194 | return s | ||
195 | } | ||
196 | { | ||
197 | $3 = filterlink($3); # link | ||
198 | $8 = filterlink($8); # enclosure | ||
199 | # try to remove tracking pixels: <img/> tags with 1px width or height. | ||
200 | gsub("<img[^>]*(width|height)[[:space:]]*=[[:space:]]*[\"'"'"' ]?1[\"'"'"' ]?[^0-9>]+[^>]*>", "", $4); | ||
201 | print $0; | ||
202 | }' | ||
203 | } | ||
204 | |||
205 | filter_html_entities() { | ||
206 | # convert HTML entities into dumb counterparts | ||
207 | awk '{ | ||
208 | gsub(/"/,"\""); gsub(/"/,"\""); | ||
209 | gsub(/'/,"'\''"); gsub(/'/,"'\''"); | ||
210 | gsub(/&/,"\\&"); # MUST BE LAST!; | ||
211 | |||
212 | }' | ||
213 | } | ||
214 | |||
215 | # Fetch ######################################################### | ||
216 | |||
217 | fetch() { # fetch(name, url, feedfile) | ||
218 | # return | ||
219 | curl -s -L \ | ||
220 | --max-redirs 3 \ | ||
221 | --header "'User-Agent: $USER_AGENT'" \ | ||
222 | --fail \ | ||
223 | --max-time 15 \ | ||
224 | "$2" | ||
225 | } | ||
diff --git a/style.css b/style.css new file mode 100644 index 0000000..0fa9d6b --- /dev/null +++ b/style.css | |||
@@ -0,0 +1,176 @@ | |||
1 | body { | ||
2 | background: #888; | ||
3 | } | ||
4 | |||
5 | main { | ||
6 | position: relative; | ||
7 | display: flex; | ||
8 | flex: column nowrap; | ||
9 | } | ||
10 | |||
11 | #items { | ||
12 | max-width: 70ch; | ||
13 | } | ||
14 | |||
15 | .entries tr { | ||
16 | vertical-align: baseline; | ||
17 | } | ||
18 | |||
19 | .fresh { | ||
20 | font-weight: bold; | ||
21 | } | ||
22 | |||
23 | .entry-timestamp { | ||
24 | font-family: monospace; | ||
25 | padding-right: 8px; | ||
26 | } | ||
27 | |||
28 | #list { | ||
29 | padding: 2ch; | ||
30 | } | ||
31 | |||
32 | aside { | ||
33 | flex-grow: 1; | ||
34 | } | ||
35 | |||
36 | aside li { | ||
37 | list-style: none; | ||
38 | text-align: right; | ||
39 | } | ||
40 | |||
41 | aside li:nth-child(even), | ||
42 | tr:nth-child(even) | ||
43 | { | ||
44 | background: inherit; | ||
45 | } | ||
46 | |||
47 | aside li:nth-child(odd), | ||
48 | tr:nth-child(odd) | ||
49 | { | ||
50 | background: #aaa; | ||
51 | } | ||
52 | |||
53 | a { display: block; } | ||
54 | |||
55 | a:link { | ||
56 | text-decoration: none; | ||
57 | color: black; | ||
58 | } | ||
59 | a:visited { | ||
60 | font-style: italic; | ||
61 | color: inherit; | ||
62 | } | ||
63 | aside a:visited { font-style: normal; } | ||
64 | a:hover { | ||
65 | background: yellow; | ||
66 | } | ||
67 | a:active{ | ||
68 | background: cyan; | ||
69 | } | ||
70 | |||
71 | header { | ||
72 | display: flex; | ||
73 | flex: row wrap; | ||
74 | margin: 1ch 0; | ||
75 | align-items: baseline; | ||
76 | justify-content: space-between; | ||
77 | } | ||
78 | header h2, header h1 { | ||
79 | margin: 0; | ||
80 | } | ||
81 | header h2 a { display: inline; } | ||
82 | header .top { | ||
83 | font-size: 80%; | ||
84 | text-align: right; | ||
85 | flex-grow: 1; | ||
86 | } | ||
87 | |||
88 | body>nav { | ||
89 | border-bottom: 1px solid black; | ||
90 | margin-top: 0; | ||
91 | text-align: right; | ||
92 | } | ||
93 | body>nav a { display: inline; } | ||
94 | |||
95 | .last-updated { | ||
96 | font-style: italic; | ||
97 | font-size: 80%; | ||
98 | text-align: right; | ||
99 | } | ||
100 | |||
101 | .entries { border-collapse: collapse; } | ||
102 | |||
103 | .entries, .entry-title { width: 100%; } | ||
104 | .entry-extra a { color: blue; display: inline; } | ||
105 | |||
106 | header a { display: inline; } | ||
107 | |||
108 | @media screen and (max-width: 720px) { | ||
109 | main, header { | ||
110 | flex-flow: row wrap; | ||
111 | padding:0; margin: 0; | ||
112 | } | ||
113 | * { border: none; } | ||
114 | .entries, .entry-title { width: 100%; } | ||
115 | html, body, #list, #items { padding: 0; margin: 0; max-width: 100%; } | ||
116 | header h2, header p { margin: 4px; } | ||
117 | aside a { display: inline; } | ||
118 | aside { | ||
119 | padding: 2ch; | ||
120 | order: 1; | ||
121 | border-bottom: 1px solid black; | ||
122 | } | ||
123 | aside ul { margin: 0; padding: 0; } | ||
124 | #list { order: 2; } | ||
125 | aside li { | ||
126 | display: inline; | ||
127 | background: inherit !important; | ||
128 | } | ||
129 | aside li::after { | ||
130 | background: inherit; | ||
131 | font-weight: normal; | ||
132 | content: " //"; | ||
133 | } | ||
134 | aside li:last-child::after { | ||
135 | content: ""; | ||
136 | } | ||
137 | .entry-timestamp { | ||
138 | display: none; | ||
139 | } | ||
140 | } | ||
141 | |||
142 | @media (prefers-color-scheme: light) { | ||
143 | body { background: white; color: black; } | ||
144 | aside li:nth-child(even), | ||
145 | tr:nth-child(even) { background: white; } | ||
146 | aside li:nth-child(odd), | ||
147 | tr:nth-child(odd) { background: #eee; } | ||
148 | a:link { color: black; } | ||
149 | a:hover { background: yellow; } | ||
150 | a:active { background: cyan; } | ||
151 | .entry-extra a { color: blue; } | ||
152 | body>nav { border-bottom: 1px solid black; } | ||
153 | } | ||
154 | |||
155 | @media (prefers-color-scheme: dark) { | ||
156 | body { background: black; color: white; } | ||
157 | aside li:nth-child(even), | ||
158 | tr:nth-child(even) { background: black; } | ||
159 | aside li:nth-child(odd), | ||
160 | tr:nth-child(odd) { background: #222; } | ||
161 | a:link { color: white; } | ||
162 | a:hover { color: yellow; background: inherit; } | ||
163 | a:active { color: cyan; background: inherit; } | ||
164 | .entry-extra a { color: cyan; } | ||
165 | body>nav { border-bottom: 1px solid white; } | ||
166 | } | ||
167 | |||
168 | @media screen and (max-width: 720px) and (prefers-color-scheme: dark) { | ||
169 | aside { border-bottom: 1px solid white; } | ||
170 | aside li { background: black !important; } | ||
171 | } | ||
172 | |||
173 | @media screen and (max-width: 720px) and (prefers-color-scheme: light) { | ||
174 | aside { border-bottom: 1px solid black; } | ||
175 | aside li { background: white !important; } | ||
176 | } | ||