Skip to content

Commit 23296f2

Browse files
authored
Merge branch 'fix-pocket-import' into fix-pocket-import
2 parents 14a3637 + e634b94 commit 23296f2

37 files changed

+1639
-193
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [unreleased]
99

10+
### Added
11+
12+
- ArchiveBox integration using @archivebox tag or folder
13+
- internal: call bookmark hooks from anywhere using bit flags
14+
- new export formats: pocket-html, json(pinboard/wallabag), generic XML-RSS
15+
- `suki search` command
16+
- `sudki fuzzy|search [term...] :tag1,tag2` to filter by tags with logical AND
17+
- `sudki fuzzy|search [term ...] :OR tag1,tag2` to filter by tags with logical OR
18+
- updated queries and api to filter by query and many tags
19+
20+
### Fixed
21+
22+
- html-autoimport: parse hash tags from title
23+
1024
## [1.2.1] 2025-08-09
1125

1226
### Added

bookmark.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@ type Bookmark struct {
3030
Module string `json:"module"`
3131
Version uint64 `json:"version"`
3232
Modified uint64 `json:"modified"`
33+
Xhsum string `json:"xhsum"`
3334
//flags int
3435
}

browsers/chrome/chrome.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"fmt"
3434
"os"
3535
"path/filepath"
36+
"strings"
3637
"time"
3738

3839
"github.com/OneOfOne/xxhash"
@@ -148,12 +149,26 @@ type Chrome struct {
148149

149150
func (ch *Chrome) Init(ctx *modules.Context, p *profiles.Profile) error {
150151
// NOTE: if called without profile setup default profile
152+
var err error
153+
var profile *profiles.Profile
154+
151155
if p == nil {
152-
prof, err := ProfileManager.GetProfileByID(BrowserName, ch.Profile)
156+
profileName := ch.Profile
157+
158+
// parse flavour
159+
parsedProfile := strings.SplitN(profileName, ":", 2)
160+
if len(parsedProfile) > 1 {
161+
flavour := parsedProfile[0]
162+
profileName = parsedProfile[1]
163+
profile, err = ProfileManager.GetProfileByID(flavour, profileName)
164+
} else {
165+
profile, err = ProfileManager.GetProfileByID(BrowserName, ch.Profile)
166+
}
167+
153168
if err != nil {
154169
return err
155170
}
156-
bookmarkDir, err := prof.AbsolutePath()
171+
bookmarkDir, err := profile.AbsolutePath()
157172
if err != nil {
158173
return err
159174
}

browsers/chrome/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func NewChromeConfig() *ChromeConfig {
5858
Type: tree.RootNode,
5959
},
6060
UseFileWatcher: true,
61-
UseHooks: []string{"node_tags_from_name", "node_marktab"},
61+
UseHooks: []string{"node_tags_from_name"},
6262
},
6363
ProfilePrefs: modules.ProfilePrefs{
6464
Profile: DefaultProfile,

browsers/firefox/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func NewFirefoxConfig() *FirefoxConfig {
9191
// NOTE: see parsing.Hook to add custom parsing logic for each
9292
// parsed bookmark node
9393
// UseHooks: []string{"node_notify_send"},
94-
UseHooks: []string{"node_tags_from_name", "node_marktab"},
94+
UseHooks: []string{"node_tags_from_name"},
9595
},
9696

9797
// Default data source name query options for `places.sqlite` db

browsers/firefox/firefox.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,12 +337,24 @@ func (f *Firefox) GetProfile() *profiles.Profile {
337337
}
338338

339339
func (f *Firefox) Init(ctx *modules.Context, p *profiles.Profile) error {
340+
var err error
341+
var profile *profiles.Profile
340342
if p == nil {
341-
// setup profile from config
342-
profile, err := FirefoxProfileManager.GetProfileByName(BrowserName, f.Profile)
343+
profileName := f.Profile
344+
345+
// parse flavour
346+
parsedProfile := strings.SplitN(profileName, ":", 2)
347+
if len(parsedProfile) > 1 {
348+
flavour := parsedProfile[0]
349+
profileName = parsedProfile[1]
350+
profile, err = FirefoxProfileManager.GetProfileByName(flavour, profileName)
351+
} else {
352+
profile, err = FirefoxProfileManager.GetProfileByName(BrowserName, f.Profile)
353+
}
343354
if err != nil {
344355
return err
345356
}
357+
346358
bookmarkDir, err := profile.AbsolutePath()
347359
if err != nil {
348360
return err

browsers/qute/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func NewQuteConfig() *QuteConfig {
5858
BkDir: baseDir + "/bookmarks",
5959
BaseDir: baseDir,
6060
UseFileWatcher: true,
61-
UseHooks: []string{"bk_tags_from_name", "bk_marktab"},
61+
UseHooks: []string{"bk_tags_from_name"},
6262
},
6363
ProfilePrefs: modules.ProfilePrefs{
6464
Profile: DefaultProfile,

cmd/export.go

Lines changed: 110 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -25,31 +25,40 @@ package cmd
2525
import (
2626
"context"
2727
"fmt"
28-
"html"
28+
"io"
2929
"os"
30-
"strings"
3130

31+
"github.com/jmoiron/sqlx"
3232
"github.com/urfave/cli/v3"
3333

34-
"github.com/blob42/gosuki"
3534
db "github.com/blob42/gosuki/internal/database"
35+
"github.com/blob42/gosuki/pkg/export"
3636
)
3737

3838
var ExportCmds = &cli.Command{
3939
Name: "export",
4040
Usage: "One-time export to other formats",
4141
Description: `The export command provides functionality to export bookmarks to other browser or application formats. `,
4242
Commands: []*cli.Command{
43-
exportHTMLCmd,
43+
exportNSHTMLCmd,
44+
exportPocketHTMLCmd,
45+
exportJSONCmd,
46+
exportRSSCmd,
4447
},
4548
}
4649

47-
var exportHTMLCmd = &cli.Command{
50+
var overwriteFlag = &cli.BoolFlag{
51+
Name: "force",
52+
Aliases: []string{"f"},
53+
Usage: "Overwrite existing files without prompting",
54+
}
55+
56+
var exportNSHTMLCmd = &cli.Command{
4857
Name: "html",
4958
Usage: "Export bookmarks to Netscape bookmark format (HTML)",
5059
Description: `Exports all bookmarks to a file in Netscape bookmark format, which is compatible with most modern browsers.`,
5160
ArgsUsage: "path/to/export.html",
52-
Action: exportToHTML,
61+
Action: exportToFormat(export.NetscapeHTML),
5362
Arguments: []cli.Argument{
5463
&cli.StringArg{
5564
Name: "path",
@@ -59,66 +68,115 @@ var exportHTMLCmd = &cli.Command{
5968
},
6069
},
6170
},
62-
Flags: []cli.Flag{
63-
&cli.BoolFlag{
64-
Name: "force",
65-
Aliases: []string{"f"},
66-
Usage: "Overwrite existing files without prompting",
71+
Flags: []cli.Flag{overwriteFlag},
72+
}
73+
74+
var exportJSONCmd = &cli.Command{
75+
Name: "json",
76+
Usage: "Export bookmarks to JSON format (Pinboard/Wallabag)",
77+
Description: `Exports all bookmarks to a file in JSON format compatible with Pinboard and Wallabag.`,
78+
ArgsUsage: "path/to/export.json",
79+
Action: exportToFormat(export.JSON),
80+
Arguments: []cli.Argument{
81+
&cli.StringArg{
82+
Name: "path",
83+
UsageText: "Export bookmarks to JSON format (Pinboard/Wallabag). The exported file can be imported into Pinboard, Wallabag and other applications that support this standard format.",
84+
Config: cli.StringConfig{
85+
TrimSpace: true,
86+
},
6787
},
6888
},
89+
Flags: []cli.Flag{overwriteFlag},
6990
}
7091

71-
func exportToHTML(ctx context.Context, c *cli.Command) error {
72-
path := c.StringArg("path")
92+
var exportRSSCmd = &cli.Command{
93+
Name: "rss",
94+
Usage: "Export bookmarks to generic RSS XML format",
95+
Description: `Exports all bookmarks to a generic RSS XML file, which can be imported into applications that support this standard format.`,
96+
ArgsUsage: "path/to/export.rss",
97+
Action: exportToFormat(export.RSS),
98+
Arguments: []cli.Argument{
99+
&cli.StringArg{
100+
Name: "path",
101+
UsageText: "Export bookmarks to RSS XML format. The exported file can be imported into applications that support this standard format.",
102+
Config: cli.StringConfig{
103+
TrimSpace: true,
104+
},
105+
},
106+
},
107+
Flags: []cli.Flag{overwriteFlag},
108+
}
73109

74-
if _, err := os.Stat(path); err == nil && !c.Bool("force") {
75-
return fmt.Errorf("file %s already exists. Use -f to overwrite", path)
76-
}
110+
// exports to pocket export html file format
111+
var exportPocketHTMLCmd = &cli.Command{
112+
Name: "pocket-html",
113+
Usage: "Export bookmarks to Pocket HTML format",
114+
Description: `Exports all bookmarks to a file in Pocket HTML format.`,
115+
ArgsUsage: "path/to/export.html",
116+
Action: exportToFormat(export.PocketHTML),
117+
Arguments: []cli.Argument{
118+
&cli.StringArg{
119+
Name: "path",
120+
UsageText: "Export bookmarks to Pocket HTML format. The exported file can be imported into Pocket and other applications that support this standard format.",
121+
Config: cli.StringConfig{
122+
TrimSpace: true,
123+
},
124+
},
125+
},
126+
Flags: []cli.Flag{overwriteFlag},
127+
}
77128

78-
db.Init(ctx, c)
79-
var rawResults db.RawBookmarks
129+
func exportToFormat(format int) cli.ActionFunc {
130+
return func(ctx context.Context, cmd *cli.Command) error {
131+
var rows *sqlx.Rows
132+
var exporter export.Exporter
133+
var output io.WriteCloser
134+
var err error
80135

81-
err := db.DiskDB.Handle.SelectContext(ctx,
82-
&rawResults,
83-
`SELECT * FROM gskbookmarks`)
84-
if err != nil {
85-
return err
86-
}
136+
path := cmd.StringArg("path")
137+
if path == "" {
138+
return fmt.Errorf("missing path: ... export %s", cmd.ArgsUsage)
139+
}
87140

88-
htmlContent := generateNetscapeHTML(rawResults.AsBookmarks())
141+
if _, err = os.Stat(path); err == nil && !cmd.Bool("force") {
142+
return fmt.Errorf("file %s already exists. Use -f to overwrite", path)
143+
}
89144

90-
if path == "-" {
91-
if _, err = fmt.Fprint(os.Stdout, htmlContent); err != nil {
145+
db.Init(ctx, cmd)
146+
if rows, err = db.DiskDB.Handle.QueryxContext(
147+
ctx,
148+
`SELECT * FROM gskbookmarks`,
149+
); err != nil {
92150
return err
93151
}
94-
} else {
95152

96-
if err := os.WriteFile(path, []byte(htmlContent), 0644); err != nil {
97-
return fmt.Errorf("failed to write to %s: %w", path, err)
153+
switch format {
154+
case export.NetscapeHTML:
155+
exporter = &export.NetscapeHTMLExporter{}
156+
case export.PocketHTML:
157+
exporter = &export.PocketHTMLExporter{}
158+
case export.JSON:
159+
exporter = &export.JSONExporter{}
160+
case export.RSS:
161+
exporter = &export.RSSXMLExporter{}
162+
default:
163+
panic(fmt.Sprintf("unsupported export format %#v", format))
98164
}
99-
}
100165

101-
return nil
102-
}
103-
104-
func generateNetscapeHTML(bookmarks []*gosuki.Bookmark) string {
105-
var sb strings.Builder
106-
sb.WriteString(`<!DOCTYPE NETSCAPE-Bookmark-file-1>
107-
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
108-
<TITLE>Bookmarks</TITLE>
109-
<H1>Bookmarks</H1>
110-
<DL><p>
111-
`)
112-
113-
for _, b := range bookmarks {
114-
sb.WriteString(fmt.Sprintf(` <DT><A HREF="%s" LAST_MODIFIED="%d">%s</A>
115-
`,
116-
html.EscapeString(b.URL),
117-
b.Modified,
118-
html.EscapeString(b.Title),
119-
))
166+
if path == "-" {
167+
output = os.Stdout
168+
} else {
169+
output, err = os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
170+
if err != nil {
171+
return err
172+
}
173+
defer output.Close()
174+
}
175+
bookExporter := export.NewBookmarksExporter(exporter, output)
176+
if format == export.JSON {
177+
bookExporter.Separator = ","
178+
}
179+
return bookExporter.ExportFromRows(rows)
120180
}
121181

122-
sb.WriteString("</DL>\n")
123-
return sb.String()
124182
}

cmd/pocket.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ func importFromPocketCSV(ctx context.Context, c *cli.Command) error {
116116
continue
117117
}
118118

119+
title := row[0]
119120
url := row[1]
120121
title := row[0]
121122
timeAdded := row[2]

0 commit comments

Comments
 (0)