searchlink 2.3.71 → 2.3.72

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55880b0891d029bf3dde58968a5b55c4cb302b14ceecd8e5f279338abc070895
4
- data.tar.gz: 7ea4da442f494ce7c956d9510f7eb20f295de133d91d7abe538c380bfd7b4d79
3
+ metadata.gz: d6e6eec4235b7ab5dd818a599ccd7ece852d6328233d2a81b8738447a581e046
4
+ data.tar.gz: 2e11a49af2da708369bde197c02b247875c41d6591fedbbb8e2b80a339819332
5
5
  SHA512:
6
- metadata.gz: 61efe73ac610acaea6d949186358bb863df9ff10d9fd0698f6b5510c3128c4833e2904347deccdb335573e1dcc4793739ae801150e97ba4bbf1e0ade8544d979
7
- data.tar.gz: 0dc5b9f05b691833a8a515f186513a4ee7c281d8358e81f1abdaf1f7412d855480ce8d34f4bbfd383682d811eeb016fbb1868cd6ff165b5cca36afd50eaea87b
6
+ metadata.gz: a7c755596c600f2ce59a02a7b4e4199efa726613ae085cb0cd1f9998fec0fae7d384597f57d5b1c5258ba45d9d2abfb7cd148d533ec35406d01fcb186385b77c
7
+ data.tar.gz: 461e781699e0f1f04eaf46d28215b0fecf1447da6784025505c2d4c1f700ca768a513ea07c2660cd8cd983d8704d63230689637f061d4f4ec4defbdb1c655ecc
@@ -133,17 +133,15 @@ module SL
133
133
  SL::Util.search_with_timeout(search, timeout)
134
134
  end
135
135
 
136
- # Performs a DuckDuckGo search with the given search
137
- # terms and link text. If link text is not provided, the
138
- # first result will be returned. The search will timeout
139
- # after the given number of seconds.
136
+ # Performs a DuckDuckGo search with the given search terms and link text. If
137
+ # link text is not provided, the first result will be returned. The search
138
+ # will timeout after the given number of seconds.
140
139
  #
141
- # @param search_terms [String] The search terms to
142
- # use
143
- # @param link_text [String] The text of the
144
- # link to search for
145
- # @param timeout [Integer] The timeout for
146
- # the search in seconds
140
+ # @param search_terms [String] The search terms to use
141
+ # @param link_text [String] The text of the link to search for
142
+ # @param timeout [Integer] The timeout for the search in seconds
143
+ # @param google [Boolean] Use Google if API key installed
144
+ # @param image [Boolean] Image search
147
145
  # @return [SL::Searches::Result] The search result
148
146
  #
149
147
  def ddg(search_terms, link_text = nil, timeout: SL.config['timeout'], google: true, image: false)
@@ -159,6 +157,17 @@ module SL
159
157
  SL::Util.search_with_timeout(search, timeout)
160
158
  end
161
159
 
160
+ ##
161
+ ## Perform a site-specific search
162
+ ##
163
+ ## @param site [String] The site to search
164
+ ## @param search_terms [String] The search terms
165
+ ## @param link_text [String] The link text
166
+ ##
167
+ def site_search(site, search_terms, link_text)
168
+ ddg("site:#{site} #{search_terms}", link_text)
169
+ end
170
+
162
171
  def first_image(url)
163
172
  images = Curl::Html.new(url).images
164
173
  images.filter { |img| img[:type] == 'img' }.first[:src]
@@ -29,7 +29,7 @@ module SL
29
29
  terms.push("(url NOT LIKE '%search/?%'
30
30
  AND url NOT LIKE '%?q=%' AND url NOT LIKE '%?s=%'
31
31
  AND url NOT LIKE '%/search?%'
32
- AND url NOT LIKE '%duckduckgo.com/?t%')")
32
+ AND url NOT LIKE '%duckduckgo.com%')")
33
33
  if exact_match
34
34
  terms.push("(url LIKE '%#{term.strip.downcase}%' OR title LIKE '%#{term.strip.downcase}%')")
35
35
  else
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SL
4
+ class LinkdingSearch
5
+ LINKDING_CACHE = SL::Util.cache_file_for("linkding")
6
+
7
+ class << self
8
+ def settings
9
+ {
10
+ trigger: "(ld|ding)",
11
+ searches: [
12
+ [["ld", "ding"], "Linkding Bookmark Search"],
13
+ ],
14
+ }
15
+ end
16
+
17
+ def get_json(call)
18
+ curl = TTY::Which.which("curl")
19
+ bookmarks = `#{curl} -SsL -H "Authorization: Token #{SL.config["linkding_api_key"]}" "#{SL.config["linkding_server"]}#{call}"`
20
+
21
+ bookmarks = bookmarks.force_encoding("utf-8")
22
+ bookmarks.gsub!(/[^[:ascii:]]/) do |non_ascii|
23
+ non_ascii.force_encoding("utf-8")
24
+ .encode("utf-16be")
25
+ .unpack("H*")[0]
26
+ .gsub(/(....)/, '\u\1')
27
+ end
28
+
29
+ bookmarks.gsub!(/[\u{1F600}-\u{1F6FF}]/, "")
30
+
31
+ JSON.parse(bookmarks)
32
+ end
33
+
34
+ def get_linkding_bookmarks
35
+ curl = TTY::Which.which("curl")
36
+ call = "/api/bookmarks/?limit=8000&format=json"
37
+
38
+ json = get_json(call)
39
+ bookmarks = json["results"]
40
+ offset = 0
41
+
42
+ while json["next"]
43
+ offset += 8000
44
+ json = get_json(call + "&offset=#{offset}")
45
+ bookmarks.concat(json["results"])
46
+ end
47
+
48
+ bookmarks
49
+ end
50
+
51
+ def linkding_bookmarks
52
+ bookmarks = get_linkding_bookmarks
53
+ updated = Time.now
54
+ { "update_time" => updated, "bookmarks" => bookmarks }
55
+ end
56
+
57
+ def save_linkding_cache(cache)
58
+ cachefile = LINKDING_CACHE
59
+
60
+ # file = File.new(cachefile,'w')
61
+ # file = Zlib::GzipWriter.new(File.new(cachefile,'w'))
62
+ begin
63
+ File.open(cachefile, "wb") { |f| f.write(Marshal.dump(cache)) }
64
+ rescue IOError
65
+ SL.add_error("Linkding cache error", "Failed to write stash to disk")
66
+ return false
67
+ end
68
+ true
69
+ end
70
+
71
+ def linkding_cache
72
+ refresh_cache = false
73
+ cachefile = LINKDING_CACHE
74
+
75
+ if File.exist?(cachefile)
76
+ begin
77
+ # file = IO.read(cachefile) # Zlib::GzipReader.open(cachefile)
78
+ # cache = Marshal.load file
79
+ cache = Marshal.load(File.binread(cachefile))
80
+ # file.close
81
+ rescue IOError # Zlib::GzipFile::Error
82
+ SL.add_error("Error loading linkding cache", "IOError reading #{cachefile}")
83
+ cache = linkding_bookmarks
84
+ save_linkding_cache(cache)
85
+ rescue StandardError
86
+ SL.add_error("Error loading linkding cache", "StandardError reading #{cachefile}")
87
+ cache = linkding_bookmarks
88
+ save_linkding_cache(cache)
89
+ end
90
+ curl = TTY::Which.which("curl")
91
+ updated = get_json("/api/bookmarks/?limit=1&format=json")["results"][0]
92
+ last_bookmark = Time.parse(updated["date_modified"])
93
+ if cache&.key?("update_time")
94
+ last_update = cache["update_time"]
95
+ refresh_cache = true if last_update < last_bookmark
96
+ else
97
+ refresh_cache = true
98
+ end
99
+ else
100
+ refresh_cache = true
101
+ end
102
+
103
+ if refresh_cache
104
+ cache = linkding_bookmarks
105
+ save_linkding_cache(cache)
106
+ end
107
+
108
+ cache
109
+ end
110
+
111
+ # Search pinboard bookmarks
112
+ # Begin query with '' to force exact matching (including description text)
113
+ # Regular matching searches for each word of query and scores the bookmarks
114
+ # exact matches in title get highest score
115
+ # exact matches in description get second highest score
116
+ # other bookmarks are scored based on the number of words that match
117
+ #
118
+ # After sorting by score, bookmarks will be sorted by date and the most recent
119
+ # will be returned
120
+ #
121
+ # Exact matching is case and punctuation insensitive
122
+ def search(_, search_terms, link_text)
123
+ unless SL.config["linkding_server"]
124
+ SL.add_error("Missing Linkding server",
125
+ "add it to your configuration (linkding_server: https://YOUR_SERVER)")
126
+ return false
127
+ end
128
+
129
+ unless SL.config["linkding_api_key"]
130
+ SL.add_error("Missing Linkding API token",
131
+ "Find your api key at https://your_server/settings/integrations and add it
132
+ to your configuration (linkding_api_key: YOURKEY)")
133
+ return false
134
+ end
135
+
136
+ exact_match = false
137
+ match_phrases = []
138
+
139
+ # If search terms start with ''term, only search for exact string matches
140
+ case search_terms
141
+ when /^ *'/
142
+ exact_match = true
143
+ search_terms.gsub!(/(^ *'+|'+ *$)/, "")
144
+ when /%22(.*?)%22/
145
+ match_phrases = search_terms.scan(/%22(\S.*?\S)%22/)
146
+ search_terms.gsub!(/%22(\S.*?\S)%22/, "")
147
+ end
148
+
149
+ cache = linkding_cache
150
+ # cache = linkding_bookmarks
151
+ bookmarks = cache["bookmarks"]
152
+
153
+ if exact_match
154
+ bookmarks.each do |bm|
155
+ text = [bm["title"], bm["description"], bm["tag_names"].join(" ")].join(" ")
156
+
157
+ return [bm["url"], bm["title"]] if text.matches_exact(search_terms)
158
+ end
159
+
160
+ return false
161
+ end
162
+
163
+ unless match_phrases.empty?
164
+ bookmarks.delete_if do |bm|
165
+ matched = tru
166
+ full_text = [bm["title"], bm["description"], bm["tag_names"].join(" ")].join(" ")
167
+ match_phrases.each do |phrase|
168
+ matched = false unless full_text.matches_exact(phrase)
169
+ end
170
+ !matched
171
+ end
172
+ end
173
+
174
+ matches = []
175
+ bookmarks.each do |bm|
176
+ title_tags = [bm["title"], bm["description"]].join(" ")
177
+ full_text = [bm["title"], bm["description"], bm["tag_names"].join(" ")].join(" ")
178
+
179
+ score = if title_tags.matches_exact(search_terms)
180
+ 14.0
181
+ elsif full_text.matches_exact(search_terms)
182
+ 13.0
183
+ elsif full_text.matches_any(search_terms)
184
+ full_text.matches_score(search_terms)
185
+ else
186
+ 0
187
+ end
188
+
189
+ return [bm["url"], bm["title"]] if score == 14
190
+
191
+ next unless score.positive?
192
+
193
+ matches.push({
194
+ score: score,
195
+ href: bm["url"],
196
+ title: bm["title"],
197
+ date: bm["date_added"],
198
+ })
199
+ end
200
+
201
+ return false if matches.empty?
202
+
203
+ top = matches.max_by { |bm| [bm[:score], bm[:date]] }
204
+
205
+ return false unless top
206
+
207
+ [top[:href], top[:title], link_text]
208
+ end
209
+ end
210
+
211
+ SL::Searches.register "linkding", :search, self
212
+ end
213
+ end
@@ -9,7 +9,7 @@ module SL
9
9
  end
10
10
 
11
11
  def load_searches
12
- Dir.glob(File.join(File.dirname(__FILE__), 'searches', '*.rb')).sort.each { |f| require f }
12
+ Dir.glob(File.join(File.dirname(__FILE__), "searches", "*.rb")).sort.each { |f| require f }
13
13
  end
14
14
 
15
15
  #
@@ -43,21 +43,21 @@ module SL
43
43
  #
44
44
  def available_searches_html
45
45
  searches = plugins[:search]
46
- .flat_map { |_, plugin| plugin[:searches] }
47
- .reject { |s| s[1].nil? }
48
- .sort_by { |s| s[0].is_a?(Array) ? s[0][0] : s[0] }
46
+ .flat_map { |_, plugin| plugin[:searches] }
47
+ .reject { |s| s[1].nil? }
48
+ .sort_by { |s| s[0].is_a?(Array) ? s[0][0] : s[0] }
49
49
  out = ['<table id="searches">',
50
- '<thead><td>Shortcut</td><td>Search Type</td></thead>',
51
- '<tbody>']
50
+ "<thead><td>Shortcut</td><td>Search Type</td></thead>",
51
+ "<tbody>"]
52
52
 
53
53
  searches.each do |s|
54
54
  out << "<tr>
55
55
  <td>
56
- <code>!#{s[0].is_a?(Array) ? "#{s[0][0]} (#{s[0][1..-1].join(',')})" : s[0]}
56
+ <code>!#{s[0].is_a?(Array) ? "#{s[0][0]} (#{s[0][1..-1].join(",")})" : s[0]}
57
57
  </code>
58
58
  </td><td>#{s[1]}</td></tr>"
59
59
  end
60
- out.concat(['</tbody>', '</table>']).join("\n")
60
+ out.concat(["</tbody>", "</table>"]).join("\n")
61
61
  end
62
62
 
63
63
  #
@@ -69,26 +69,33 @@ module SL
69
69
  searches = []
70
70
  plugins[:search].each_value { |plugin| searches.concat(plugin[:searches].delete_if { |s| s[1].nil? }) }
71
71
  out = []
72
+
72
73
  searches.each do |s|
73
- out += "!#{s[0].is_a?(Array) ? "#{s[0][0]} (#{s[0][1..-1].join(',')})" : s[0]}#{s[0].spacer}#{s[1]}"
74
+ shortcut = if s[0].is_a?(Array)
75
+ "#{s[0][0]} (#{s[0][1..-1].join(",")})"
76
+ else
77
+ s[0]
78
+ end
79
+
80
+ out << "!#{shortcut}#{shortcut.spacer}#{s[1]}"
74
81
  end
75
82
  out.join("\n")
76
83
  end
77
84
 
78
85
  def best_search_match(term)
79
86
  searches = all_possible_searches.dup
80
- searches.flatten.select { |s| s.matches_score(term, separator: '', start_word: false) > 8 }
87
+ searches.flatten.select { |s| s.matches_score(term, separator: "", start_word: false) > 8 }
81
88
  end
82
89
 
83
90
  def all_possible_searches
84
91
  searches = []
85
92
  plugins[:search].each_value { |plugin| plugin[:searches].each { |s| searches.push(s[0]) } }
86
- searches.concat(SL.config['custom_site_searches'].keys.sort)
93
+ searches.concat(SL.config["custom_site_searches"].keys.sort)
87
94
  end
88
95
 
89
96
  def did_you_mean(term)
90
97
  matches = best_search_match(term)
91
- matches.empty? ? '' : ", did you mean #{matches.map { |m| "!#{m}" }.join(', ')}?"
98
+ matches.empty? ? "" : ", did you mean #{matches.map { |m| "!#{m}" }.join(", ")}?"
92
99
  end
93
100
 
94
101
  def valid_searches
@@ -99,14 +106,14 @@ module SL
99
106
 
100
107
  def valid_search?(term)
101
108
  valid = false
102
- valid = true if term =~ /^(#{valid_searches.join('|')})$/
103
- valid = true if SL.config['custom_site_searches'].keys.include? term
109
+ valid = true if term =~ /^(#{valid_searches.join("|")})$/
110
+ valid = true if SL.config["custom_site_searches"].keys.include? term
104
111
  # SL.notify("Invalid search#{did_you_mean(term)}", term) unless valid
105
112
  valid
106
113
  end
107
114
 
108
115
  def register_plugin(title, type, klass)
109
- raise PluginError.new("Plugin has no settings method", plugin: title) unless klass.respond_to? :settings
116
+ raise PluginError.new("Plugin has no settings method", plugin: title) unless klass.respond_to? :settings
110
117
 
111
118
  settings = klass.settings
112
119
 
@@ -116,16 +123,16 @@ module SL
116
123
  plugins[type][title] = {
117
124
  trigger: settings.fetch(:trigger, title).normalize_trigger,
118
125
  searches: settings[:searches],
119
- class: klass
126
+ class: klass,
120
127
  }
121
128
  end
122
129
 
123
130
  def load_custom
124
- plugins_folder = File.expand_path('~/.local/searchlink/plugins')
125
- new_plugins_folder = File.expand_path('~/.config/searchlink/plugins')
131
+ plugins_folder = File.expand_path("~/.local/searchlink/plugins")
132
+ new_plugins_folder = File.expand_path("~/.config/searchlink/plugins")
126
133
 
127
134
  if File.directory?(plugins_folder) && !File.directory?(new_plugins_folder)
128
- Dir.glob(File.join(plugins_folder, '**/*.rb')).sort.each do |plugin|
135
+ Dir.glob(File.join(plugins_folder, "**/*.rb")).sort.each do |plugin|
129
136
  require plugin
130
137
  end
131
138
 
@@ -134,7 +141,7 @@ module SL
134
141
 
135
142
  return unless File.directory?(new_plugins_folder)
136
143
 
137
- Dir.glob(File.join(new_plugins_folder, '**/*.rb')).sort.each do |plugin|
144
+ Dir.glob(File.join(new_plugins_folder, "**/*.rb")).sort.each do |plugin|
138
145
  require plugin
139
146
  end
140
147
 
@@ -142,25 +149,25 @@ module SL
142
149
  end
143
150
 
144
151
  def load_custom_scripts(plugins_folder)
145
- Dir.glob(File.join(plugins_folder, '**/*.{json,yml,yaml}')).each do |file|
146
- ext = File.extname(file).sub(/^\./, '')
152
+ Dir.glob(File.join(plugins_folder, "**/*.{json,yml,yaml}")).each do |file|
153
+ ext = File.extname(file).sub(/^\./, "")
147
154
  config = IO.read(file)
148
155
 
149
156
  cfg = case ext
150
- when /^y/i
151
- YAML.safe_load(config)
152
- else
153
- JSON.parse(config)
154
- end
155
- cfg['filename'] = File.basename(file)
156
- cfg['path'] = file.shorten_path
157
+ when /^y/i
158
+ YAML.safe_load(config)
159
+ else
160
+ JSON.parse(config)
161
+ end
162
+ cfg["filename"] = File.basename(file)
163
+ cfg["path"] = file.shorten_path
157
164
  SL::ScriptSearch.new(cfg)
158
165
  end
159
166
  end
160
167
 
161
- def do_search(search_type, search_terms, link_text, timeout: SL.config['timeout'])
168
+ def do_search(search_type, search_terms, link_text, timeout: SL.config["timeout"])
162
169
  plugins[:search].each do |_title, plugin|
163
- trigger = plugin[:trigger].gsub(/(^\^|\$$)/, '')
170
+ trigger = plugin[:trigger].gsub(/(^\^|\$$)/, "")
164
171
  if search_type =~ /^#{trigger}$/
165
172
  search = proc { plugin[:class].search(search_type, search_terms, link_text) }
166
173
  return SL::Util.search_with_timeout(search, timeout)
@@ -172,64 +179,67 @@ module SL
172
179
  end
173
180
 
174
181
  # import
175
- require_relative 'searches/applemusic'
182
+ require_relative "searches/applemusic"
176
183
 
177
184
  # import
178
- require_relative 'searches/itunes'
185
+ require_relative "searches/itunes"
179
186
 
180
187
  # import
181
- require_relative 'searches/amazon'
188
+ require_relative "searches/amazon"
182
189
 
183
190
  # import
184
- require_relative 'searches/bitly'
191
+ require_relative "searches/bitly"
185
192
 
186
193
  # import
187
- require_relative 'searches/definition'
194
+ require_relative "searches/definition"
188
195
 
189
196
  # import
190
- require_relative 'searches/duckduckgo'
197
+ require_relative "searches/duckduckgo"
191
198
 
192
199
  # import
193
- require_relative 'searches/github'
200
+ require_relative "searches/github"
194
201
 
195
202
  # import
196
- require_relative 'searches/google'
203
+ require_relative "searches/google"
197
204
 
198
205
  # import
199
- require_relative 'searches/history'
206
+ require_relative "searches/history"
200
207
 
201
208
  # import
202
- require_relative 'searches/hook'
209
+ require_relative "searches/hook"
203
210
 
204
211
  # import
205
- require_relative 'searches/lastfm'
212
+ require_relative "searches/lastfm"
206
213
 
207
214
  # import
208
- require_relative 'searches/pinboard'
215
+ require_relative "searches/pinboard"
209
216
 
210
217
  # import
211
- require_relative 'searches/social'
218
+ require_relative "searches/social"
212
219
 
213
220
  # import
214
- require_relative 'searches/software'
221
+ require_relative "searches/software"
215
222
 
216
223
  # import
217
- require_relative 'searches/spelling'
224
+ require_relative "searches/spelling"
218
225
 
219
226
  # import
220
- require_relative 'searches/spotlight'
227
+ require_relative "searches/spotlight"
221
228
 
222
229
  # import
223
- require_relative 'searches/tmdb'
230
+ require_relative "searches/tmdb"
224
231
 
225
232
  # import
226
- require_relative 'searches/twitter'
233
+ require_relative "searches/twitter"
227
234
 
228
235
  # import
229
- require_relative 'searches/wikipedia'
236
+ require_relative "searches/wikipedia"
230
237
 
231
238
  # import
232
- require_relative 'searches/youtube'
239
+ require_relative "searches/youtube"
233
240
 
234
241
  # import
235
- require_relative 'searches/stackoverflow'
242
+ require_relative "searches/stackoverflow"
243
+
244
+ #import
245
+ require_relative "searches/linkding"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SL
4
- VERSION = '2.3.71'
4
+ VERSION = '2.3.72'
5
5
  end
6
6
 
7
7
  # Main module
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: searchlink
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.71
4
+ version: 2.3.72
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-11 00:00:00.000000000 Z
11
+ date: 2024-11-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -220,6 +220,20 @@ dependencies:
220
220
  - - "~>"
221
221
  - !ruby/object:Gem::Version
222
222
  version: 0.9.5
223
+ - !ruby/object:Gem::Dependency
224
+ name: plist
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - "~>"
228
+ - !ruby/object:Gem::Version
229
+ version: 3.7.1
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - "~>"
235
+ - !ruby/object:Gem::Version
236
+ version: 3.7.1
223
237
  description: macOS System Service for inline web searches
224
238
  email:
225
239
  - me@brettterpstra.com
@@ -258,6 +272,7 @@ files:
258
272
  - lib/searchlink/searches/hook.rb
259
273
  - lib/searchlink/searches/itunes.rb
260
274
  - lib/searchlink/searches/lastfm.rb
275
+ - lib/searchlink/searches/linkding.rb
261
276
  - lib/searchlink/searches/lyrics.rb
262
277
  - lib/searchlink/searches/pinboard.rb
263
278
  - lib/searchlink/searches/social.rb
@@ -295,7 +310,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
295
310
  - !ruby/object:Gem::Version
296
311
  version: '0'
297
312
  requirements: []
298
- rubygems_version: 3.2.15
313
+ rubygems_version: 3.2.16
299
314
  signing_key:
300
315
  specification_version: 4
301
316
  summary: Create Markdown links from web searches without leaving your editor.