Commit
+825 -0 +/-16 browse
1 | diff --git a/.gitignore b/.gitignore |
2 | new file mode 100644 |
3 | index 0000000..ba077a4 |
4 | --- /dev/null |
5 | +++ b/.gitignore |
6 | @@ -0,0 +1 @@ |
7 | + bin |
8 | diff --git a/Makefile b/Makefile |
9 | new file mode 100644 |
10 | index 0000000..1f46faf |
11 | --- /dev/null |
12 | +++ b/Makefile |
13 | @@ -0,0 +1,12 @@ |
14 | + .PHONY: bin/lingua |
15 | + |
16 | + default: bin/lingua |
17 | + |
18 | + bin: |
19 | + mkdir -p $@ |
20 | + |
21 | + bin/lingua: bin |
22 | + go build -o $@ |
23 | + |
24 | + example: bin/lingua |
25 | + bin/lingua --config linga.example.toml |
26 | diff --git a/README.md b/README.md |
27 | new file mode 100644 |
28 | index 0000000..85ccfd9 |
29 | --- /dev/null |
30 | +++ b/README.md |
31 | @@ -0,0 +1,50 @@ |
32 | + # Lingua |
33 | + |
34 | + Lingua is an [ollama](https://github.com/ollama/ollama) powered language |
35 | + learning tool. It downloads foreign language news articles from RSS feeds and |
36 | + asks you to translate them ranking your accuracy from 1 to 10. Lingua also |
37 | + supports using [Miniflux](https://miniflux.app) as a feed source. |
38 | + |
39 | + ## Building From Source |
40 | + |
41 | + ```sh |
42 | + make |
43 | + ``` |
44 | + |
45 | + ## Usage |
46 | + |
47 | + ```sh |
48 | + lingua --config linga.example.toml |
49 | + # Or load from ~/.config/lingua/config.toml |
50 | + lingua |
51 | + ``` |
52 | + |
53 | + ## Configuration |
54 | + |
55 | + Configuration is defined in a TOML file, see `linga.example.toml`. |
56 | + |
57 | + ### Source |
58 | + |
59 | + A source can be either a plain RSS feed or a [Miniflux](https://miniflux.app) |
60 | + feed ID. |
61 | + |
62 | + #### Plain RSS Feed |
63 | + |
64 | + ```toml |
65 | + [[sources]] |
66 | + name = "Fanpage" |
67 | + language = "IT" |
68 | + kind = "RSS" |
69 | + url = "https://www.fanpage.it/feed/" |
70 | + ``` |
71 | + |
72 | + #### Miniflux Feed Item |
73 | + |
74 | + ```toml |
75 | + [[sources]] |
76 | + name = "Fanpage" |
77 | + language = "IT" |
78 | + kind = "Miniflux" |
79 | + # Miniflux Feed ID: |
80 | + id = 111 |
81 | + ``` |
82 | diff --git a/client.go b/client.go |
83 | new file mode 100644 |
84 | index 0000000..a3e26ae |
85 | --- /dev/null |
86 | +++ b/client.go |
87 | @@ -0,0 +1,105 @@ |
88 | + package main |
89 | + |
90 | + import ( |
91 | + "context" |
92 | + "fmt" |
93 | + "strconv" |
94 | + |
95 | + "github.com/ollama/ollama/api" |
96 | + ) |
97 | + |
98 | + type Client struct { |
99 | + c *api.Client |
100 | + debug bool |
101 | + model string |
102 | + } |
103 | + |
104 | + func NewClient(model string, debug bool) (*Client, error) { |
105 | + |
106 | + client, err := api.ClientFromEnvironment() |
107 | + if err != nil { |
108 | + return nil, err |
109 | + } |
110 | + |
111 | + return &Client{ |
112 | + c: client, |
113 | + debug: debug, |
114 | + model: model, |
115 | + }, nil |
116 | + } |
117 | + |
118 | + func (c Client) Translate(ctx context.Context, input string, local, remote Language) (string, error) { |
119 | + messages := []api.Message{ |
120 | + { |
121 | + Role: "system", |
122 | + Content: fmt.Sprintf( |
123 | + TRANSLATE_TEMPLATE, remote.String(), local.String()), |
124 | + }, |
125 | + { |
126 | + Role: "user", |
127 | + Content: input, |
128 | + }, |
129 | + } |
130 | + |
131 | + text := "" |
132 | + |
133 | + req := &api.ChatRequest{ |
134 | + Model: c.model, |
135 | + Messages: messages, |
136 | + } |
137 | + |
138 | + respFunc := func(resp api.ChatResponse) error { |
139 | + if c.debug { |
140 | + fmt.Print(resp.Message.Content) |
141 | + } |
142 | + text += resp.Message.Content |
143 | + return nil |
144 | + } |
145 | + |
146 | + err := c.c.Chat(ctx, req, respFunc) |
147 | + if err != nil { |
148 | + return "", nil |
149 | + } |
150 | + |
151 | + return text, nil |
152 | + } |
153 | + |
154 | + func (c Client) Compare(ctx context.Context, t1, t2 string) (int, error) { |
155 | + promptInput := fmt.Sprintf(COMPARISON_TEMPLATE, t1, t2) |
156 | + log.Debugf("input: %s", promptInput) |
157 | + messages := []api.Message{ |
158 | + { |
159 | + Role: "system", |
160 | + Content: COMPARISION_TEMPLATE_SYSTEM, |
161 | + }, |
162 | + { |
163 | + Role: "user", |
164 | + Content: promptInput, |
165 | + }, |
166 | + } |
167 | + |
168 | + text := "" |
169 | + |
170 | + req := &api.ChatRequest{ |
171 | + Model: c.model, |
172 | + Messages: messages, |
173 | + } |
174 | + |
175 | + respFunc := func(resp api.ChatResponse) error { |
176 | + text += resp.Message.Content |
177 | + return nil |
178 | + } |
179 | + |
180 | + err := c.c.Chat(ctx, req, respFunc) |
181 | + if err != nil { |
182 | + return -1, err |
183 | + } |
184 | + |
185 | + score, err := strconv.Atoi(text) |
186 | + |
187 | + if err != nil { |
188 | + log.Errorf("Failed to extract numeric score from model output: %s", err) |
189 | + } |
190 | + |
191 | + return score, nil |
192 | + } |
193 | diff --git a/config.go b/config.go |
194 | new file mode 100644 |
195 | index 0000000..e4dbd02 |
196 | --- /dev/null |
197 | +++ b/config.go |
198 | @@ -0,0 +1,75 @@ |
199 | + package main |
200 | + |
201 | + import ( |
202 | + "errors" |
203 | + "fmt" |
204 | + "os" |
205 | + "path" |
206 | + |
207 | + "github.com/BurntSushi/toml" |
208 | + ) |
209 | + |
210 | + func defaultConfigPath() string { |
211 | + xdg_home := os.Getenv("XDG_CONFIG_HOME") |
212 | + if xdg_home != "" { |
213 | + return path.Join(xdg_home, "lingua/config.toml") |
214 | + } |
215 | + return "config.toml" |
216 | + } |
217 | + |
218 | + type Config struct { |
219 | + Sources []Source |
220 | + Model string |
221 | + Language Language |
222 | + } |
223 | + |
224 | + func (c *Config) ByName(name string) Source { |
225 | + for i := range c.Sources { |
226 | + if c.Sources[i].Name() == name { |
227 | + return c.Sources[i] |
228 | + } |
229 | + } |
230 | + return nil |
231 | + } |
232 | + |
233 | + type SourceConfig struct { |
234 | + Name string `toml:"name"` |
235 | + Language string `toml:"language"` |
236 | + Kind string `toml:"kind"` |
237 | + Url string `toml:"url"` |
238 | + Id int64 `toml:"id"` |
239 | + } |
240 | + |
241 | + type innerConfig struct { |
242 | + MinifluxEndpoint string `toml:"miniflux-endpoint"` |
243 | + Language string `toml:"language"` |
244 | + Model string `toml:"model"` |
245 | + Sources []SourceConfig `toml:"sources"` |
246 | + } |
247 | + |
248 | + func load_config(path string) (*Config, error) { |
249 | + var cfg innerConfig |
250 | + if _, err := toml.DecodeFile(path, &cfg); err != nil { |
251 | + return nil, err |
252 | + } |
253 | + localLang, ok := Languages[cfg.Language] |
254 | + if !ok { |
255 | + return nil, errors.New(fmt.Sprintf("Unsupported local language: %s", cfg.Language)) |
256 | + } |
257 | + sources := []Source{} |
258 | + for _, sourceCfg := range cfg.Sources { |
259 | + language, ok := Languages[sourceCfg.Language] |
260 | + if !ok { |
261 | + return nil, errors.New(fmt.Sprintf("Unsupported remote language: %s", sourceCfg.Language)) |
262 | + } |
263 | + if sourceCfg.Kind == "RSS" || sourceCfg.Kind == "rss" { |
264 | + sources = append(sources, &RssFeed{ |
265 | + url: sourceCfg.Url, name: sourceCfg.Name, language: language}) |
266 | + } else if sourceCfg.Kind == "Miniflux" || sourceCfg.Kind == "miniflux" { |
267 | + sources = append(sources, &MinifluxFeed{ |
268 | + endpoint: cfg.MinifluxEndpoint, name: sourceCfg.Name, language: language, feedId: sourceCfg.Id, |
269 | + }) |
270 | + } |
271 | + } |
272 | + return &Config{Sources: sources, Model: cfg.Model, Language: localLang}, nil |
273 | + } |
274 | diff --git a/demo.mp4 b/demo.mp4 |
275 | new file mode 100644 |
276 | index 0000000..3133e37 |
277 | Binary files /dev/null and b/demo.mp4 differ |
278 | diff --git a/extract.go b/extract.go |
279 | new file mode 100644 |
280 | index 0000000..d0893f5 |
281 | --- /dev/null |
282 | +++ b/extract.go |
283 | @@ -0,0 +1,24 @@ |
284 | + package main |
285 | + |
286 | + import ( |
287 | + "strings" |
288 | + |
289 | + "golang.org/x/net/html" |
290 | + ) |
291 | + |
292 | + func extractText(input string) string { |
293 | + buf := strings.NewReader(input) |
294 | + z := html.NewTokenizer(buf) |
295 | + content := "" |
296 | + loop: |
297 | + for { |
298 | + tt := z.Next() |
299 | + switch tt { |
300 | + case html.ErrorToken: |
301 | + break loop |
302 | + case html.TextToken: |
303 | + content = content + string(z.Text()) |
304 | + } |
305 | + } |
306 | + return content |
307 | + } |
308 | diff --git a/go.mod b/go.mod |
309 | new file mode 100644 |
310 | index 0000000..54a67da |
311 | --- /dev/null |
312 | +++ b/go.mod |
313 | @@ -0,0 +1,47 @@ |
314 | + module main |
315 | + |
316 | + go 1.24.0 |
317 | + |
318 | + require ( |
319 | + github.com/BurntSushi/toml v1.4.0 |
320 | + github.com/charmbracelet/huh v0.6.0 |
321 | + github.com/charmbracelet/huh/spinner v0.0.0-20250228115947-334300b07206 |
322 | + github.com/mmcdole/gofeed v1.3.0 |
323 | + github.com/ollama/ollama v0.5.12 |
324 | + go.uber.org/zap v1.27.0 |
325 | + golang.org/x/net v0.35.0 |
326 | + miniflux.app v1.0.46 |
327 | + ) |
328 | + |
329 | + require ( |
330 | + github.com/PuerkitoBio/goquery v1.10.0 // indirect |
331 | + github.com/andybalholm/cascadia v1.3.2 // indirect |
332 | + github.com/atotto/clipboard v0.1.4 // indirect |
333 | + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect |
334 | + github.com/catppuccin/go v0.2.0 // indirect |
335 | + github.com/charmbracelet/bubbles v0.20.0 // indirect |
336 | + github.com/charmbracelet/bubbletea v1.3.4 // indirect |
337 | + github.com/charmbracelet/lipgloss v1.0.0 // indirect |
338 | + github.com/charmbracelet/x/ansi v0.8.0 // indirect |
339 | + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect |
340 | + github.com/charmbracelet/x/term v0.2.1 // indirect |
341 | + github.com/dustin/go-humanize v1.0.1 // indirect |
342 | + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect |
343 | + github.com/json-iterator/go v1.1.12 // indirect |
344 | + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect |
345 | + github.com/mattn/go-isatty v0.0.20 // indirect |
346 | + github.com/mattn/go-localereader v0.0.1 // indirect |
347 | + github.com/mattn/go-runewidth v0.0.16 // indirect |
348 | + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect |
349 | + github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 // indirect |
350 | + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect |
351 | + github.com/modern-go/reflect2 v1.0.2 // indirect |
352 | + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect |
353 | + github.com/muesli/cancelreader v0.2.2 // indirect |
354 | + github.com/muesli/termenv v0.16.0 // indirect |
355 | + github.com/rivo/uniseg v0.4.7 // indirect |
356 | + go.uber.org/multierr v1.10.0 // indirect |
357 | + golang.org/x/sync v0.11.0 // indirect |
358 | + golang.org/x/sys v0.30.0 // indirect |
359 | + golang.org/x/text v0.22.0 // indirect |
360 | + ) |
361 | diff --git a/go.sum b/go.sum |
362 | new file mode 100644 |
363 | index 0000000..86eb099 |
364 | --- /dev/null |
365 | +++ b/go.sum |
366 | @@ -0,0 +1,132 @@ |
367 | + github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= |
368 | + github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= |
369 | + github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= |
370 | + github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= |
371 | + github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4= |
372 | + github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4= |
373 | + github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= |
374 | + github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= |
375 | + github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= |
376 | + github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= |
377 | + github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= |
378 | + github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= |
379 | + github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= |
380 | + github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= |
381 | + github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= |
382 | + github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= |
383 | + github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= |
384 | + github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= |
385 | + github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8= |
386 | + github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU= |
387 | + github.com/charmbracelet/huh/spinner v0.0.0-20250228115947-334300b07206 h1:f9BE18tYgP4l5AhrCBO0Ff/3dNff1soAjezGeIpVU0I= |
388 | + github.com/charmbracelet/huh/spinner v0.0.0-20250228115947-334300b07206/go.mod h1:SMBco6myAJ5hS5DKgoAbocROODtaikOnGYLL8eLh3VE= |
389 | + github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= |
390 | + github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= |
391 | + github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= |
392 | + github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= |
393 | + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= |
394 | + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= |
395 | + github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= |
396 | + github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= |
397 | + github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
398 | + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
399 | + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
400 | + github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= |
401 | + github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= |
402 | + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= |
403 | + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= |
404 | + github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= |
405 | + github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= |
406 | + github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= |
407 | + github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= |
408 | + github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= |
409 | + github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= |
410 | + github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= |
411 | + github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= |
412 | + github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= |
413 | + github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= |
414 | + github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= |
415 | + github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= |
416 | + github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= |
417 | + github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= |
418 | + github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= |
419 | + github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4= |
420 | + github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE= |
421 | + github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 h1:Zr92CAlFhy2gL+V1F+EyIuzbQNbSgP4xhTODZtrXUtk= |
422 | + github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8= |
423 | + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= |
424 | + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= |
425 | + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= |
426 | + github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= |
427 | + github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= |
428 | + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= |
429 | + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= |
430 | + github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= |
431 | + github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= |
432 | + github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= |
433 | + github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= |
434 | + github.com/ollama/ollama v0.5.12 h1:qM+k/ozyHLJzEQoAEPrUQ0qXqsgDEEdpIVwuwScrd2U= |
435 | + github.com/ollama/ollama v0.5.12/go.mod h1:ibdmDvb/TjKY1OArBWIazL3pd1DHTk8eG2MMjEkWhiI= |
436 | + github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
437 | + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
438 | + github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= |
439 | + github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= |
440 | + github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= |
441 | + github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
442 | + github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= |
443 | + github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= |
444 | + github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= |
445 | + github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= |
446 | + go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= |
447 | + go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= |
448 | + go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= |
449 | + go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= |
450 | + go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= |
451 | + go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= |
452 | + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
453 | + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= |
454 | + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= |
455 | + golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= |
456 | + golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
457 | + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= |
458 | + golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= |
459 | + golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= |
460 | + golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= |
461 | + golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= |
462 | + golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= |
463 | + golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
464 | + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
465 | + golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
466 | + golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= |
467 | + golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= |
468 | + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
469 | + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
470 | + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
471 | + golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
472 | + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
473 | + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
474 | + golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
475 | + golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
476 | + golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
477 | + golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= |
478 | + golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= |
479 | + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= |
480 | + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= |
481 | + golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= |
482 | + golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= |
483 | + golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
484 | + golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
485 | + golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= |
486 | + golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= |
487 | + golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= |
488 | + golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= |
489 | + golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= |
490 | + golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
491 | + golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
492 | + golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= |
493 | + golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= |
494 | + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
495 | + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= |
496 | + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
497 | + miniflux.app v1.0.46 h1:2/xrbXiEoQlj/bAZ8MT+fPN1+gmYDYuXVCOhvSskns4= |
498 | + miniflux.app v1.0.46/go.mod h1:YtEJIO1vMCvZgyzDbds7II0W/H7sGpo3auFCQscuMrE= |
499 | diff --git a/language.go b/language.go |
500 | new file mode 100644 |
501 | index 0000000..5f3d2c5 |
502 | --- /dev/null |
503 | +++ b/language.go |
504 | @@ -0,0 +1,52 @@ |
505 | + package main |
506 | + |
507 | + type Language int |
508 | + |
509 | + func (l Language) String() string { |
510 | + switch l { |
511 | + case English: |
512 | + return "english" |
513 | + case Italian: |
514 | + return "italian" |
515 | + case Portuguese: |
516 | + return "portuguese" |
517 | + case Spanish: |
518 | + return "spanish" |
519 | + } |
520 | + panic("unreachable") |
521 | + } |
522 | + |
523 | + const ( |
524 | + English = Language(iota) |
525 | + Italian |
526 | + Portuguese |
527 | + Spanish |
528 | + ) |
529 | + |
530 | + // See: https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes |
531 | + var Languages = map[string]Language{ |
532 | + "EN": English, |
533 | + "IT": Italian, |
534 | + "PT": Portuguese, |
535 | + "ES": Spanish, |
536 | + } |
537 | + |
538 | + var Introduction = map[Language]string { |
539 | + English: "Hello! Welcome to Lingua!", |
540 | + Italian: "Salve! Benvenuti a Lingua!", |
541 | + } |
542 | + |
543 | + var SelectNewsSource = map[Language]string { |
544 | + English: "Select a New Source", |
545 | + Italian: "Seleziona un canale", |
546 | + } |
547 | + |
548 | + var SummarizeText = map[Language]string { |
549 | + English: "Summarize the text above into %s", |
550 | + Italian: "Riassumi il testo seguente in %s", |
551 | + } |
552 | + |
553 | + var ScoreText = map[Language]string { |
554 | + English: "Your score was: %d", |
555 | + Italian: "La tua valutazione รจ stata: %d", |
556 | + } |
557 | diff --git a/lingua.example.toml b/lingua.example.toml |
558 | new file mode 100644 |
559 | index 0000000..ace6284 |
560 | --- /dev/null |
561 | +++ b/lingua.example.toml |
562 | @@ -0,0 +1,15 @@ |
563 | + # Language which translations are written in. English is default, choose |
564 | + # the same as your source remote language for a more challenging conversation. |
565 | + language = "EN" |
566 | + model = "mistral-nemo:latest" |
567 | + # miniflux-endpoint = "https://miniflux.example.org" |
568 | + [[sources]] |
569 | + name = "Fanpage" |
570 | + language = "IT" |
571 | + kind = "RSS" |
572 | + url = "https://www.fanpage.it/feed/" |
573 | + [[sources]] |
574 | + name = "La Stampa" |
575 | + language = "IT" |
576 | + kind = "RSS" |
577 | + url = "https://www.lastampa.it/rss/copertina.xml" |
578 | diff --git a/main.go b/main.go |
579 | new file mode 100644 |
580 | index 0000000..05cd0fc |
581 | --- /dev/null |
582 | +++ b/main.go |
583 | @@ -0,0 +1,152 @@ |
584 | + package main |
585 | + |
586 | + import ( |
587 | + "context" |
588 | + "flag" |
589 | + |
590 | + "fmt" |
591 | + |
592 | + "github.com/charmbracelet/huh" |
593 | + "github.com/charmbracelet/huh/spinner" |
594 | + "go.uber.org/zap" |
595 | + "go.uber.org/zap/zapcore" |
596 | + ) |
597 | + |
598 | + var log *zap.SugaredLogger |
599 | + |
600 | + func setupLogging(debug bool) { |
601 | + config := zap.NewDevelopmentConfig() |
602 | + config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder |
603 | + if debug { |
604 | + config.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel) |
605 | + } else { |
606 | + config.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel) |
607 | + } |
608 | + logger, _ := config.Build() |
609 | + log = logger.Sugar() |
610 | + } |
611 | + |
612 | + func main() { |
613 | + |
614 | + cfgPath := flag.String("config", "", "Configuration File") |
615 | + nArticles := flag.Int("n", 5, "Number of articles to translate") |
616 | + debug := flag.Bool("debug", false, "Show debugging information") |
617 | + flag.Parse() |
618 | + |
619 | + setupLogging(*debug) |
620 | + |
621 | + configFile := *cfgPath |
622 | + |
623 | + if configFile == "" { |
624 | + configFile = defaultConfigPath() |
625 | + } |
626 | + |
627 | + cfg, err := load_config(configFile) |
628 | + if err != nil { |
629 | + log.Fatal(err) |
630 | + } |
631 | + |
632 | + localLanguage := cfg.Language |
633 | + |
634 | + fmt.Println(Introduction[localLanguage]) |
635 | + |
636 | + client, err := NewClient(cfg.Model, *debug) |
637 | + if err != nil { |
638 | + log.Fatal(err) |
639 | + } |
640 | + |
641 | + var sourceName string |
642 | + |
643 | + sources := []huh.Option[string]{} |
644 | + for i := range cfg.Sources { |
645 | + sourceName := cfg.Sources[i].Name() |
646 | + sources = append(sources, huh.NewOption(sourceName, sourceName)) |
647 | + } |
648 | + |
649 | + form := huh.NewForm( |
650 | + huh.NewGroup( |
651 | + huh.NewSelect[string]().Title(SelectNewsSource[localLanguage]). |
652 | + Options(sources...).Value(&sourceName), |
653 | + ), |
654 | + ).WithTheme(huh.ThemeBase16()) |
655 | + |
656 | + if err := form.Run(); err != nil { |
657 | + log.Fatal(err) |
658 | + } |
659 | + |
660 | + source := cfg.ByName(sourceName) |
661 | + |
662 | + ctx := context.Background() |
663 | + |
664 | + var nArticlesRemote int |
665 | + |
666 | + task := spinner.New().Title("Downloading Article Source...").Action(func() { |
667 | + n, err := source.Len(ctx) |
668 | + if err != nil { |
669 | + log.Fatal(err) |
670 | + } |
671 | + nArticlesRemote = n |
672 | + }) |
673 | + |
674 | + if err := task.Run(); err != nil { |
675 | + log.Fatal(err) |
676 | + } |
677 | + |
678 | + count := min(*nArticles, nArticlesRemote) |
679 | + |
680 | + for i := range count { |
681 | + |
682 | + article, err := source.Get(ctx, i) |
683 | + if err != nil { |
684 | + log.Fatal(err) |
685 | + } |
686 | + |
687 | + for _, chunk := range article.Chunks() { |
688 | + |
689 | + var inputText string |
690 | + |
691 | + form = huh.NewForm( |
692 | + huh.NewGroup( |
693 | + huh.NewText().Title(chunk). |
694 | + Description( |
695 | + fmt.Sprintf( |
696 | + SummarizeText[localLanguage], |
697 | + localLanguage)).Value(&inputText), |
698 | + ), |
699 | + ).WithTheme(huh.ThemeBase16()) |
700 | + |
701 | + if err := form.Run(); err != nil { |
702 | + log.Fatal(err) |
703 | + } |
704 | + |
705 | + sourceText := chunk |
706 | + if localLanguage != source.Language() { |
707 | + // translate the remote language to the local language before doing |
708 | + // the comparision. |
709 | + translated, err := client.Translate(ctx, chunk, localLanguage, source.Language()) |
710 | + if err != nil { |
711 | + log.Fatal(err) |
712 | + } |
713 | + log.Debugf("Translated source:\n%s\nInto:\n%s", sourceText, translated) |
714 | + sourceText = translated |
715 | + } |
716 | + |
717 | + var score int |
718 | + |
719 | + task := spinner.New().Title("Comparing text").Action(func() { |
720 | + n, err := client.Compare(ctx, sourceText, inputText) |
721 | + if err != nil { |
722 | + log.Fatal(err) |
723 | + } |
724 | + score = n |
725 | + }) |
726 | + |
727 | + if err := task.Run(); err != nil { |
728 | + log.Fatal(err) |
729 | + } |
730 | + |
731 | + log.Infof("Accuracy: %d", score) |
732 | + |
733 | + } |
734 | + } |
735 | + } |
736 | diff --git a/prompt.go b/prompt.go |
737 | new file mode 100644 |
738 | index 0000000..eac1e07 |
739 | --- /dev/null |
740 | +++ b/prompt.go |
741 | @@ -0,0 +1,21 @@ |
742 | + package main |
743 | + |
744 | + const ( |
745 | + COMPARISION_TEMPLATE_SYSTEM = ` |
746 | + The user will input two texts where the first text begins at "TEXT ONE:" and the |
747 | + second text bagins at "TEXT TWO:". Compare the two texts for conceptual similarity |
748 | + and rate them on a scale of 1 to 10 where 1 is not at all similar and 10 is almost |
749 | + identical and output only the number.` |
750 | + COMPARISON_TEMPLATE = ` |
751 | + TEXT ONE: |
752 | + %s |
753 | + |
754 | + TEXT TWO: |
755 | + %s |
756 | + ` |
757 | + |
758 | + TRANSLATE_TEMPLATE = ` |
759 | + Translate all input text from %s to %s, return aboslutely no other text except |
760 | + the translation. |
761 | + ` |
762 | + ) |
763 | diff --git a/source.go b/source.go |
764 | new file mode 100644 |
765 | index 0000000..ef03ba6 |
766 | --- /dev/null |
767 | +++ b/source.go |
768 | @@ -0,0 +1,21 @@ |
769 | + package main |
770 | + |
771 | + import "context" |
772 | + |
773 | + |
774 | + type Article struct { |
775 | + raw string |
776 | + } |
777 | + |
778 | + // Return the source text separated by paragraphs |
779 | + func (a Article) Chunks() []string { |
780 | + return []string{a.raw} // FIXME |
781 | + } |
782 | + |
783 | + // Article source |
784 | + type Source interface { |
785 | + Name() string |
786 | + Language() Language |
787 | + Len(context.Context) (int, error) |
788 | + Get(context.Context, int) (*Article, error) |
789 | + } |
790 | diff --git a/source_miniflux.go b/source_miniflux.go |
791 | new file mode 100644 |
792 | index 0000000..741de26 |
793 | --- /dev/null |
794 | +++ b/source_miniflux.go |
795 | @@ -0,0 +1,63 @@ |
796 | + package main |
797 | + |
798 | + import ( |
799 | + "context" |
800 | + "errors" |
801 | + "os" |
802 | + |
803 | + miniflux "miniflux.app/client" |
804 | + ) |
805 | + |
806 | + var _ Source = &MinifluxFeed{} |
807 | + |
808 | + type MinifluxFeed struct { |
809 | + name string |
810 | + endpoint string |
811 | + feedId int64 |
812 | + language Language |
813 | + client *miniflux.Client |
814 | + } |
815 | + |
816 | + func (m *MinifluxFeed) getClient() error { |
817 | + token := os.Getenv("MINIFLUX_SECRET_TOKEN") |
818 | + if token == "" { |
819 | + return errors.New("MINIFLUX_SECRET_TOKEN not set") |
820 | + } |
821 | + m.client = miniflux.New(m.endpoint, token) |
822 | + return nil |
823 | + } |
824 | + |
825 | + func (m *MinifluxFeed) Name() string { |
826 | + return m.name |
827 | + } |
828 | + |
829 | + func (m *MinifluxFeed) Language() Language { |
830 | + return m.language |
831 | + } |
832 | + |
833 | + func (m *MinifluxFeed) Len(ctx context.Context) (int, error) { |
834 | + if m.client == nil { |
835 | + if err := m.getClient(); err != nil { |
836 | + return -1, err |
837 | + } |
838 | + } |
839 | + entries, err := m.client.FeedEntries(m.feedId, &miniflux.Filter{Status: miniflux.EntryStatusUnread}) |
840 | + if err != nil { |
841 | + return -1, err |
842 | + } |
843 | + return entries.Total, nil |
844 | + } |
845 | + |
846 | + func (m *MinifluxFeed) Get(ctx context.Context, i int) (*Article, error) { |
847 | + if m.client == nil { |
848 | + if err := m.getClient(); err != nil { |
849 | + return nil, err |
850 | + } |
851 | + } |
852 | + entries, err := m.client.FeedEntries(m.feedId, &miniflux.Filter{Status: miniflux.EntryStatusUnread}) |
853 | + if err != nil { |
854 | + return nil, err |
855 | + } |
856 | + entry := entries.Entries[i] |
857 | + return &Article{raw: extractText(entry.Content)}, nil |
858 | + } |
859 | diff --git a/source_rss.go b/source_rss.go |
860 | new file mode 100644 |
861 | index 0000000..e043825 |
862 | --- /dev/null |
863 | +++ b/source_rss.go |
864 | @@ -0,0 +1,55 @@ |
865 | + package main |
866 | + |
867 | + import ( |
868 | + "context" |
869 | + |
870 | + "github.com/mmcdole/gofeed" |
871 | + ) |
872 | + |
873 | + var _ Source = &RssFeed{} |
874 | + |
875 | + type RssFeed struct { |
876 | + name string |
877 | + url string |
878 | + content string |
879 | + language Language |
880 | + feed *gofeed.Feed |
881 | + } |
882 | + |
883 | + func (r *RssFeed) Name() string { |
884 | + return r.name |
885 | + } |
886 | + |
887 | + func (r *RssFeed) Language() Language { |
888 | + return r.language |
889 | + } |
890 | + |
891 | + func (r *RssFeed) getFeed(ctx context.Context) error { |
892 | + fp := gofeed.NewParser() |
893 | + feed, err := fp.ParseURLWithContext(r.url, ctx) |
894 | + if err != nil { |
895 | + return err |
896 | + } |
897 | + r.feed = feed |
898 | + return nil |
899 | + } |
900 | + |
901 | + func (r RssFeed) Len(ctx context.Context) (int, error) { |
902 | + if r.feed == nil { |
903 | + if err := r.getFeed(ctx); err != nil { |
904 | + return -1, err |
905 | + } |
906 | + } |
907 | + return r.feed.Len(), nil |
908 | + } |
909 | + |
910 | + func (r *RssFeed) Get(ctx context.Context, i int) (*Article, error) { |
911 | + if r.feed == nil { |
912 | + if err := r.getFeed(ctx); err != nil { |
913 | + return nil, err |
914 | + } |
915 | + } |
916 | + item := r.feed.Items[i] |
917 | + textOnly := extractText(item.Content) |
918 | + return &Article{raw: textOnly}, nil |
919 | + } |