fmog 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/exe/fmog +6 -0
- data/lib/fmog/cli.rb +166 -0
- data/lib/fmog/db.rb +54 -0
- data/lib/fmog/feed.rb +33 -0
- data/lib/fmog/fetcher.rb +111 -0
- data/lib/fmog/item.rb +54 -0
- data/lib/fmog/version.rb +5 -0
- data/lib/fmog.rb +7 -0
- data/sig/fmog/cli.rbs +25 -0
- data/sig/fmog/db.rbs +12 -0
- data/sig/fmog/feed.rbs +10 -0
- data/sig/fmog/fetcher.rbs +18 -0
- data/sig/fmog/item.rbs +11 -0
- data/sig/rss.rbs +5 -0
- data/sig/sqlite3.rbs +13 -0
- data/sig/terminal_table.rbs +6 -0
- data/sig/thor.rbs +9 -0
- metadata +189 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 30d24be1819b9fcbf9a414639dd4abe51873ca73f1563a67f7daf9844147a3e4
|
|
4
|
+
data.tar.gz: 609268fc98dd0c9fc4f4857b63fc2ab35f330f85985a2d041835a5bf5148b43f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1c3225d42416c692e36ad9d90f75164d25d688717072fe41305a98a9567221a50a812de2ca5cce6b45b77108df19a79cfdfea414f11fe85cb58405ff13fca6f2
|
|
7
|
+
data.tar.gz: 4a0bf86d883ebe793dfdfde3045dbd4b5fc910f99dc50283c0b3ea5bc71d9b836667d630ed88a25e6f2a5630261e4db19b299dec9a7cc6630d7fe1cda46fbc8b
|
data/exe/fmog
ADDED
data/lib/fmog/cli.rb
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require "json"
|
|
5
|
+
require "terminal-table"
|
|
6
|
+
|
|
7
|
+
module Fmog
|
|
8
|
+
class BaseCLI < Thor
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def tty?
|
|
12
|
+
$stdout.tty?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def output_single(hash, message)
|
|
16
|
+
if tty?
|
|
17
|
+
puts message
|
|
18
|
+
else
|
|
19
|
+
puts JSON.generate(hash)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class FeedCLI < BaseCLI
|
|
25
|
+
namespace "feed"
|
|
26
|
+
|
|
27
|
+
desc "add URL", "Add a feed"
|
|
28
|
+
def add(url)
|
|
29
|
+
id = Feed.add(url)
|
|
30
|
+
output_single({ id: id, url: url }, "Added feed ##{id}: #{url}")
|
|
31
|
+
rescue SQLite3::ConstraintException
|
|
32
|
+
$stderr.puts "Error: Feed already exists: #{url}"
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc "list", "List feeds"
|
|
37
|
+
def list
|
|
38
|
+
feeds = Feed.list
|
|
39
|
+
if tty?
|
|
40
|
+
if feeds.empty?
|
|
41
|
+
puts "No feeds."
|
|
42
|
+
return
|
|
43
|
+
end
|
|
44
|
+
table = Terminal::Table.new(
|
|
45
|
+
headings: ["ID", "Title", "URL", "Last Fetched"],
|
|
46
|
+
rows: feeds.map { |f| [f["id"], f["title"] || "-", f["url"], f["last_fetched_at"] || "-"] }
|
|
47
|
+
)
|
|
48
|
+
puts table
|
|
49
|
+
else
|
|
50
|
+
feeds.each { |f| puts JSON.generate(f) }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
desc "remove ID", "Remove a feed"
|
|
55
|
+
def remove(id)
|
|
56
|
+
count = Feed.remove(id.to_i)
|
|
57
|
+
if count > 0
|
|
58
|
+
output_single({ id: id.to_i }, "Removed feed ##{id}")
|
|
59
|
+
else
|
|
60
|
+
$stderr.puts "Error: Feed not found: #{id}"
|
|
61
|
+
exit 1
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
desc "fetch [ID]", "Fetch feed(s)"
|
|
66
|
+
def fetch(id = nil)
|
|
67
|
+
if id
|
|
68
|
+
count = Fetcher.fetch(id.to_i)
|
|
69
|
+
output_single({ id: id.to_i, count: count }, "Fetched #{count} items from feed ##{id}")
|
|
70
|
+
else
|
|
71
|
+
results = Fetcher.fetch_all
|
|
72
|
+
if tty?
|
|
73
|
+
results.each { |r| puts "Feed ##{r[:id]} (#{r[:url]}): #{r[:count]} items" }
|
|
74
|
+
else
|
|
75
|
+
results.each { |r| puts JSON.generate(r) }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
rescue => e
|
|
79
|
+
$stderr.puts "Error: #{e.message}"
|
|
80
|
+
exit 1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class ItemCLI < BaseCLI
|
|
86
|
+
namespace "item"
|
|
87
|
+
|
|
88
|
+
desc "list", "List items"
|
|
89
|
+
option :feed, type: :numeric, desc: "Filter by feed ID"
|
|
90
|
+
option :unread, type: :boolean, default: false, desc: "Show only unread items"
|
|
91
|
+
option :limit, type: :numeric, default: 50, desc: "Max items to show"
|
|
92
|
+
def list
|
|
93
|
+
items = Item.list(feed_id: options[:feed], unread: options[:unread], limit: options[:limit])
|
|
94
|
+
if tty?
|
|
95
|
+
if items.empty?
|
|
96
|
+
puts "No items."
|
|
97
|
+
return
|
|
98
|
+
end
|
|
99
|
+
table = Terminal::Table.new(
|
|
100
|
+
headings: ["ID", "Feed", "Title", "Published", "Read"],
|
|
101
|
+
rows: items.map { |i|
|
|
102
|
+
read_mark = i["read_at"] ? "Y" : ""
|
|
103
|
+
title = (i["title"] || "").slice(0, 60)
|
|
104
|
+
[i["id"], i["feed_id"], title, i["published_at"] || "-", read_mark]
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
puts table
|
|
108
|
+
else
|
|
109
|
+
items.each { |i| puts JSON.generate(i) }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
desc "show ID", "Show item details"
|
|
114
|
+
def show(id)
|
|
115
|
+
item = Item.find(id.to_i)
|
|
116
|
+
unless item
|
|
117
|
+
$stderr.puts "Error: Item not found: #{id}"
|
|
118
|
+
exit 1
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if tty?
|
|
122
|
+
puts "ID: #{item["id"]}"
|
|
123
|
+
puts "Feed: #{item["feed_id"]}"
|
|
124
|
+
puts "Title: #{item["title"]}"
|
|
125
|
+
puts "URL: #{item["url"]}"
|
|
126
|
+
puts "Published: #{item["published_at"]}"
|
|
127
|
+
puts "Read: #{item["read_at"] || "no"}"
|
|
128
|
+
puts "---"
|
|
129
|
+
puts item["body"] if item["body"]
|
|
130
|
+
else
|
|
131
|
+
puts JSON.generate(item)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
desc "read ID", "Mark item as read"
|
|
136
|
+
def read(id)
|
|
137
|
+
count = Item.mark_read(id.to_i)
|
|
138
|
+
if count > 0
|
|
139
|
+
output_single({ id: id.to_i, read: true }, "Marked item ##{id} as read")
|
|
140
|
+
else
|
|
141
|
+
$stderr.puts "Error: Item not found: #{id}"
|
|
142
|
+
exit 1
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
desc "unread ID", "Mark item as unread"
|
|
147
|
+
def unread(id)
|
|
148
|
+
count = Item.mark_unread(id.to_i)
|
|
149
|
+
if count > 0
|
|
150
|
+
output_single({ id: id.to_i, read: false }, "Marked item ##{id} as unread")
|
|
151
|
+
else
|
|
152
|
+
$stderr.puts "Error: Item not found: #{id}"
|
|
153
|
+
exit 1
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
class CLI < Thor
|
|
160
|
+
desc "feed SUBCOMMAND", "Manage feeds"
|
|
161
|
+
subcommand "feed", FeedCLI
|
|
162
|
+
|
|
163
|
+
desc "item SUBCOMMAND", "Manage items"
|
|
164
|
+
subcommand "item", ItemCLI
|
|
165
|
+
end
|
|
166
|
+
end
|
data/lib/fmog/db.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sqlite3"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "xdg"
|
|
6
|
+
|
|
7
|
+
module Fmog
|
|
8
|
+
module DB
|
|
9
|
+
DB_DIR = XDG.new.data_home.join("fmog").to_s
|
|
10
|
+
DB_PATH = File.join(DB_DIR, "fmog.db")
|
|
11
|
+
|
|
12
|
+
def self.connection
|
|
13
|
+
@connection ||= begin
|
|
14
|
+
FileUtils.mkdir_p(DB_DIR)
|
|
15
|
+
db = SQLite3::Database.new(DB_PATH)
|
|
16
|
+
db.results_as_hash = true
|
|
17
|
+
db.execute("PRAGMA foreign_keys = ON")
|
|
18
|
+
migrate(db)
|
|
19
|
+
db
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.migrate(db)
|
|
24
|
+
db.execute(<<~SQL)
|
|
25
|
+
CREATE TABLE IF NOT EXISTS feeds (
|
|
26
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
27
|
+
url TEXT NOT NULL UNIQUE,
|
|
28
|
+
title TEXT,
|
|
29
|
+
last_fetched_at DATETIME,
|
|
30
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
31
|
+
)
|
|
32
|
+
SQL
|
|
33
|
+
|
|
34
|
+
db.execute(<<~SQL)
|
|
35
|
+
CREATE TABLE IF NOT EXISTS items (
|
|
36
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
37
|
+
feed_id INTEGER NOT NULL REFERENCES feeds(id) ON DELETE CASCADE,
|
|
38
|
+
guid TEXT NOT NULL,
|
|
39
|
+
title TEXT,
|
|
40
|
+
url TEXT,
|
|
41
|
+
body TEXT,
|
|
42
|
+
published_at DATETIME,
|
|
43
|
+
read_at DATETIME,
|
|
44
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
45
|
+
UNIQUE(feed_id, guid)
|
|
46
|
+
)
|
|
47
|
+
SQL
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.reset!
|
|
51
|
+
@connection = nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/fmog/feed.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fmog
|
|
4
|
+
module Feed
|
|
5
|
+
def self.add(url)
|
|
6
|
+
db = DB.connection
|
|
7
|
+
db.execute("INSERT INTO feeds (url) VALUES (?)", [url])
|
|
8
|
+
db.last_insert_row_id
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.list
|
|
12
|
+
DB.connection.execute("SELECT id, url, title, last_fetched_at, created_at FROM feeds ORDER BY id")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.find(id)
|
|
16
|
+
DB.connection.execute("SELECT id, url, title, last_fetched_at, created_at FROM feeds WHERE id = ?", [id]).first
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.remove(id)
|
|
20
|
+
db = DB.connection
|
|
21
|
+
db.execute("DELETE FROM feeds WHERE id = ?", [id])
|
|
22
|
+
db.changes
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.update_title(id, title)
|
|
26
|
+
DB.connection.execute("UPDATE feeds SET title = ? WHERE id = ?", [title, id])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.touch_fetched(id)
|
|
30
|
+
DB.connection.execute("UPDATE feeds SET last_fetched_at = datetime('now') WHERE id = ?", [id])
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/fmog/fetcher.rb
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rss"
|
|
4
|
+
require "open-uri"
|
|
5
|
+
|
|
6
|
+
module Fmog
|
|
7
|
+
class Fetcher
|
|
8
|
+
def self.fetch(feed_id)
|
|
9
|
+
new(feed_id).call
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.fetch_all
|
|
13
|
+
Feed.list.map do |feed|
|
|
14
|
+
count = fetch(feed["id"])
|
|
15
|
+
{ id: feed["id"], url: feed["url"], count: count }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(feed_id)
|
|
20
|
+
@feed_id = feed_id
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call
|
|
24
|
+
feed = Feed.find(@feed_id)
|
|
25
|
+
raise "Feed not found: #{@feed_id}" unless feed
|
|
26
|
+
|
|
27
|
+
content = URI.open(feed["url"]).read
|
|
28
|
+
parsed = RSS::Parser.parse(content, false)
|
|
29
|
+
raise "Failed to parse feed: #{feed["url"]}" unless parsed
|
|
30
|
+
|
|
31
|
+
title = extract_feed_title(parsed)
|
|
32
|
+
Feed.update_title(@feed_id, title) if title
|
|
33
|
+
|
|
34
|
+
count = 0
|
|
35
|
+
items = parsed.items || []
|
|
36
|
+
items.each do |entry|
|
|
37
|
+
guid = extract_guid(entry)
|
|
38
|
+
next unless guid
|
|
39
|
+
|
|
40
|
+
Item.upsert(
|
|
41
|
+
feed_id: @feed_id,
|
|
42
|
+
guid: guid,
|
|
43
|
+
title: extract_title(entry),
|
|
44
|
+
url: extract_url(entry),
|
|
45
|
+
body: extract_body(entry),
|
|
46
|
+
published_at: extract_date(entry)&.strftime("%Y-%m-%d %H:%M:%S")
|
|
47
|
+
)
|
|
48
|
+
count += 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
Feed.touch_fetched(@feed_id)
|
|
52
|
+
count
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def extract_feed_title(parsed)
|
|
58
|
+
if parsed.respond_to?(:channel) && parsed.channel
|
|
59
|
+
parsed.channel.title
|
|
60
|
+
elsif parsed.title.respond_to?(:content)
|
|
61
|
+
parsed.title.content
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def extract_guid(entry)
|
|
66
|
+
# RSS 2.0
|
|
67
|
+
return entry.guid&.content if entry.respond_to?(:guid) && entry.guid
|
|
68
|
+
# Atom
|
|
69
|
+
return entry.id&.content if entry.respond_to?(:id) && entry.id.respond_to?(:content)
|
|
70
|
+
# fallback
|
|
71
|
+
extract_url(entry)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def extract_title(entry)
|
|
75
|
+
return entry.title.content if entry.title.respond_to?(:content)
|
|
76
|
+
entry.title.to_s
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def extract_url(entry)
|
|
80
|
+
# Atom
|
|
81
|
+
if entry.respond_to?(:link) && entry.link.respond_to?(:href)
|
|
82
|
+
return entry.link.href
|
|
83
|
+
end
|
|
84
|
+
# RSS 2.0
|
|
85
|
+
return entry.link if entry.respond_to?(:link)
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def extract_body(entry)
|
|
90
|
+
# Atom content
|
|
91
|
+
if entry.respond_to?(:content) && entry.content
|
|
92
|
+
return entry.content.content if entry.content.respond_to?(:content)
|
|
93
|
+
end
|
|
94
|
+
# RSS description
|
|
95
|
+
if entry.respond_to?(:description)
|
|
96
|
+
return entry.description
|
|
97
|
+
end
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def extract_date(entry)
|
|
102
|
+
# Atom
|
|
103
|
+
return entry.updated&.content if entry.respond_to?(:updated) && entry.updated
|
|
104
|
+
return entry.published&.content if entry.respond_to?(:published) && entry.published
|
|
105
|
+
# RSS 2.0
|
|
106
|
+
return entry.date if entry.respond_to?(:date)
|
|
107
|
+
return entry.pubDate if entry.respond_to?(:pubDate)
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
data/lib/fmog/item.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fmog
|
|
4
|
+
module Item
|
|
5
|
+
COLUMNS = "id, feed_id, guid, title, url, body, published_at, read_at, created_at"
|
|
6
|
+
|
|
7
|
+
def self.upsert(feed_id:, guid:, title:, url:, body:, published_at:)
|
|
8
|
+
DB.connection.execute(<<~SQL, [feed_id, guid, title, url, body, published_at])
|
|
9
|
+
INSERT INTO items (feed_id, guid, title, url, body, published_at)
|
|
10
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
11
|
+
ON CONFLICT(feed_id, guid) DO UPDATE SET
|
|
12
|
+
title = excluded.title,
|
|
13
|
+
url = excluded.url,
|
|
14
|
+
body = excluded.body,
|
|
15
|
+
published_at = excluded.published_at
|
|
16
|
+
SQL
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.list(feed_id: nil, unread: false, limit: 50)
|
|
20
|
+
conditions = []
|
|
21
|
+
params = []
|
|
22
|
+
|
|
23
|
+
if feed_id
|
|
24
|
+
conditions << "feed_id = ?"
|
|
25
|
+
params << feed_id
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if unread
|
|
29
|
+
conditions << "read_at IS NULL"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
where = conditions.empty? ? "" : "WHERE #{conditions.join(" AND ")}"
|
|
33
|
+
|
|
34
|
+
DB.connection.execute(
|
|
35
|
+
"SELECT #{COLUMNS} FROM items #{where} ORDER BY published_at DESC, id DESC LIMIT ?",
|
|
36
|
+
params + [limit]
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.find(id)
|
|
41
|
+
DB.connection.execute("SELECT #{COLUMNS} FROM items WHERE id = ?", [id]).first
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.mark_read(id)
|
|
45
|
+
DB.connection.execute("UPDATE items SET read_at = datetime('now') WHERE id = ?", [id])
|
|
46
|
+
DB.connection.changes
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.mark_unread(id)
|
|
50
|
+
DB.connection.execute("UPDATE items SET read_at = NULL WHERE id = ?", [id])
|
|
51
|
+
DB.connection.changes
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/fmog/version.rb
ADDED
data/lib/fmog.rb
ADDED
data/sig/fmog/cli.rbs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Fmog
|
|
2
|
+
class BaseCLI < Thor
|
|
3
|
+
private
|
|
4
|
+
|
|
5
|
+
def tty?: () -> bool
|
|
6
|
+
def output_single: (Hash[Symbol, untyped] hash, String message) -> void
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class FeedCLI < BaseCLI
|
|
10
|
+
def add: (String url) -> void
|
|
11
|
+
def list: () -> void
|
|
12
|
+
def remove: (String id) -> void
|
|
13
|
+
def fetch: (?String? id) -> void
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class ItemCLI < BaseCLI
|
|
17
|
+
def list: () -> void
|
|
18
|
+
def show: (String id) -> void
|
|
19
|
+
def read: (String id) -> void
|
|
20
|
+
def unread: (String id) -> void
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class CLI < Thor
|
|
24
|
+
end
|
|
25
|
+
end
|
data/sig/fmog/db.rbs
ADDED
data/sig/fmog/feed.rbs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Fmog
|
|
2
|
+
module Feed
|
|
3
|
+
def self.add: (String url) -> Integer
|
|
4
|
+
def self.list: () -> Array[Hash[String, untyped]]
|
|
5
|
+
def self.find: (Integer id) -> Hash[String, untyped]?
|
|
6
|
+
def self.remove: (Integer id) -> Integer
|
|
7
|
+
def self.update_title: (Integer id, String title) -> void
|
|
8
|
+
def self.touch_fetched: (Integer id) -> void
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Fmog
|
|
2
|
+
class Fetcher
|
|
3
|
+
def self.fetch: (Integer feed_id) -> Integer
|
|
4
|
+
def self.fetch_all: () -> Array[{ id: Integer, url: String, count: Integer }]
|
|
5
|
+
|
|
6
|
+
def initialize: (Integer feed_id) -> void
|
|
7
|
+
def call: () -> Integer
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def extract_feed_title: (untyped parsed) -> String?
|
|
12
|
+
def extract_guid: (untyped entry) -> String?
|
|
13
|
+
def extract_title: (untyped entry) -> String
|
|
14
|
+
def extract_url: (untyped entry) -> String?
|
|
15
|
+
def extract_body: (untyped entry) -> String?
|
|
16
|
+
def extract_date: (untyped entry) -> Time?
|
|
17
|
+
end
|
|
18
|
+
end
|
data/sig/fmog/item.rbs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Fmog
|
|
2
|
+
module Item
|
|
3
|
+
COLUMNS: String
|
|
4
|
+
|
|
5
|
+
def self.upsert: (feed_id: Integer, guid: String, title: String?, url: String?, body: String?, published_at: String?) -> void
|
|
6
|
+
def self.list: (?feed_id: Integer?, ?unread: bool, ?limit: Integer) -> Array[Hash[String, untyped]]
|
|
7
|
+
def self.find: (Integer id) -> Hash[String, untyped]?
|
|
8
|
+
def self.mark_read: (Integer id) -> Integer
|
|
9
|
+
def self.mark_unread: (Integer id) -> Integer
|
|
10
|
+
end
|
|
11
|
+
end
|
data/sig/rss.rbs
ADDED
data/sig/sqlite3.rbs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module SQLite3
|
|
2
|
+
class Database
|
|
3
|
+
attr_accessor results_as_hash: bool
|
|
4
|
+
|
|
5
|
+
def initialize: (String filename) -> void
|
|
6
|
+
def execute: (String sql, ?Array[untyped] bind_vars) -> Array[Hash[String, untyped]]
|
|
7
|
+
def last_insert_row_id: () -> Integer
|
|
8
|
+
def changes: () -> Integer
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class ConstraintException < Exception
|
|
12
|
+
end
|
|
13
|
+
end
|
data/sig/thor.rbs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class Thor
|
|
2
|
+
def self.desc: (String usage, String description) -> void
|
|
3
|
+
def self.option: (Symbol name, **untyped opts) -> void
|
|
4
|
+
def self.namespace: (String name) -> void
|
|
5
|
+
def self.subcommand: (String name, Class command_class) -> void
|
|
6
|
+
def self.start: (Array[String] args) -> void
|
|
7
|
+
|
|
8
|
+
def options: () -> Hash[String | Symbol, untyped]
|
|
9
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fmog
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kenichi Takahashi
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rss
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.3'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: sqlite3
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: thor
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.3'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.3'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: terminal-table
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: xdg
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '10.0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '10.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: minitest
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '5.0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '5.0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: rake
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '13.0'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '13.0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: rbs
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '3.0'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '3.0'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: steep
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - "~>"
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '1.9'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - "~>"
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '1.9'
|
|
138
|
+
description: fmog (Feed MogMog) is a command-line RSS/Atom feed aggregator. Subscribe
|
|
139
|
+
to feeds, fetch updates, and read items — all from your terminal, with JSON output
|
|
140
|
+
for piping.
|
|
141
|
+
email:
|
|
142
|
+
- kenichi.taka@gmail.com
|
|
143
|
+
executables:
|
|
144
|
+
- fmog
|
|
145
|
+
extensions: []
|
|
146
|
+
extra_rdoc_files: []
|
|
147
|
+
files:
|
|
148
|
+
- exe/fmog
|
|
149
|
+
- lib/fmog.rb
|
|
150
|
+
- lib/fmog/cli.rb
|
|
151
|
+
- lib/fmog/db.rb
|
|
152
|
+
- lib/fmog/feed.rb
|
|
153
|
+
- lib/fmog/fetcher.rb
|
|
154
|
+
- lib/fmog/item.rb
|
|
155
|
+
- lib/fmog/version.rb
|
|
156
|
+
- sig/fmog/cli.rbs
|
|
157
|
+
- sig/fmog/db.rbs
|
|
158
|
+
- sig/fmog/feed.rbs
|
|
159
|
+
- sig/fmog/fetcher.rbs
|
|
160
|
+
- sig/fmog/item.rbs
|
|
161
|
+
- sig/rss.rbs
|
|
162
|
+
- sig/sqlite3.rbs
|
|
163
|
+
- sig/terminal_table.rbs
|
|
164
|
+
- sig/thor.rbs
|
|
165
|
+
homepage: https://github.com/kenchan/fmog
|
|
166
|
+
licenses:
|
|
167
|
+
- MIT
|
|
168
|
+
metadata:
|
|
169
|
+
source_code_uri: https://github.com/kenchan/fmog
|
|
170
|
+
changelog_uri: https://github.com/kenchan/fmog/blob/main/CHANGELOG.md
|
|
171
|
+
rubygems_mfa_required: 'true'
|
|
172
|
+
rdoc_options: []
|
|
173
|
+
require_paths:
|
|
174
|
+
- lib
|
|
175
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
176
|
+
requirements:
|
|
177
|
+
- - ">="
|
|
178
|
+
- !ruby/object:Gem::Version
|
|
179
|
+
version: '4.0'
|
|
180
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
181
|
+
requirements:
|
|
182
|
+
- - ">="
|
|
183
|
+
- !ruby/object:Gem::Version
|
|
184
|
+
version: '0'
|
|
185
|
+
requirements: []
|
|
186
|
+
rubygems_version: 4.0.3
|
|
187
|
+
specification_version: 4
|
|
188
|
+
summary: Feed MogMog - CLI feed aggregator
|
|
189
|
+
test_files: []
|