searchlink 2.3.71 → 2.3.72

Sign up to get free protection for your applications and to get access to all the features.
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.