searchlink 2.3.59
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/bin/searchlink +84 -0
- data/lib/searchlink/array.rb +7 -0
- data/lib/searchlink/config.rb +230 -0
- data/lib/searchlink/curl/html.rb +482 -0
- data/lib/searchlink/curl/json.rb +90 -0
- data/lib/searchlink/curl.rb +7 -0
- data/lib/searchlink/help.rb +103 -0
- data/lib/searchlink/output.rb +270 -0
- data/lib/searchlink/parse.rb +668 -0
- data/lib/searchlink/plist.rb +213 -0
- data/lib/searchlink/search.rb +70 -0
- data/lib/searchlink/searches/amazon.rb +25 -0
- data/lib/searchlink/searches/applemusic.rb +123 -0
- data/lib/searchlink/searches/bitly.rb +50 -0
- data/lib/searchlink/searches/definition.rb +67 -0
- data/lib/searchlink/searches/duckduckgo.rb +167 -0
- data/lib/searchlink/searches/github.rb +245 -0
- data/lib/searchlink/searches/google.rb +67 -0
- data/lib/searchlink/searches/helpers/chromium.rb +318 -0
- data/lib/searchlink/searches/helpers/firefox.rb +135 -0
- data/lib/searchlink/searches/helpers/safari.rb +133 -0
- data/lib/searchlink/searches/history.rb +166 -0
- data/lib/searchlink/searches/hook.rb +77 -0
- data/lib/searchlink/searches/itunes.rb +97 -0
- data/lib/searchlink/searches/lastfm.rb +41 -0
- data/lib/searchlink/searches/lyrics.rb +91 -0
- data/lib/searchlink/searches/pinboard.rb +183 -0
- data/lib/searchlink/searches/social.rb +105 -0
- data/lib/searchlink/searches/software.rb +27 -0
- data/lib/searchlink/searches/spelling.rb +59 -0
- data/lib/searchlink/searches/spotlight.rb +28 -0
- data/lib/searchlink/searches/stackoverflow.rb +31 -0
- data/lib/searchlink/searches/tmdb.rb +52 -0
- data/lib/searchlink/searches/twitter.rb +46 -0
- data/lib/searchlink/searches/wikipedia.rb +33 -0
- data/lib/searchlink/searches/youtube.rb +48 -0
- data/lib/searchlink/searches.rb +194 -0
- data/lib/searchlink/semver.rb +140 -0
- data/lib/searchlink/string.rb +469 -0
- data/lib/searchlink/url.rb +153 -0
- data/lib/searchlink/util.rb +87 -0
- data/lib/searchlink/version.rb +93 -0
- data/lib/searchlink/which.rb +175 -0
- data/lib/searchlink.rb +66 -0
- data/lib/tokens.rb +3 -0
- metadata +299 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
##
|
|
2
|
+
## Chromium (Chrome, Arc, Brave, Edge) search methods
|
|
3
|
+
##
|
|
4
|
+
module SL
|
|
5
|
+
# Chromium history search
|
|
6
|
+
class HistorySearch
|
|
7
|
+
class << self
|
|
8
|
+
## Search Arc history
|
|
9
|
+
##
|
|
10
|
+
## @param term The search term
|
|
11
|
+
##
|
|
12
|
+
## @return [Array] Single bookmark, [url, title, date]
|
|
13
|
+
##
|
|
14
|
+
def search_arc_history(term)
|
|
15
|
+
# Google history
|
|
16
|
+
history_file = File.expand_path('~/Library/Application Support/Arc/User Data/Default/History')
|
|
17
|
+
if File.exist?(history_file)
|
|
18
|
+
SL.notify('Searching Arc History', term)
|
|
19
|
+
search_chromium_history(history_file, term)
|
|
20
|
+
else
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
## Search Brave history
|
|
26
|
+
##
|
|
27
|
+
## @param term The search term
|
|
28
|
+
##
|
|
29
|
+
## @return [Array] Single bookmark, [url, title, date]
|
|
30
|
+
##
|
|
31
|
+
def search_brave_history(term)
|
|
32
|
+
# Google history
|
|
33
|
+
history_file = File.expand_path('~/Library/Application Support/BraveSoftware/Brave-Browser/Default/History')
|
|
34
|
+
if File.exist?(history_file)
|
|
35
|
+
SL.notify('Searching Brave History', term)
|
|
36
|
+
search_chromium_history(history_file, term)
|
|
37
|
+
else
|
|
38
|
+
false
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
## Search Edge history
|
|
43
|
+
##
|
|
44
|
+
## @param term The search term
|
|
45
|
+
##
|
|
46
|
+
## @return [Array] Single bookmark, [url, title, date]
|
|
47
|
+
##
|
|
48
|
+
def search_edge_history(term)
|
|
49
|
+
# Google history
|
|
50
|
+
history_file = File.expand_path('~/Library/Application Support/Microsoft Edge/Default/History')
|
|
51
|
+
if File.exist?(history_file)
|
|
52
|
+
SL.notify('Searching Edge History', term)
|
|
53
|
+
search_chromium_history(history_file, term)
|
|
54
|
+
else
|
|
55
|
+
false
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
## Search Chrome history
|
|
60
|
+
##
|
|
61
|
+
## @param term The search term
|
|
62
|
+
##
|
|
63
|
+
## @return [Array] Single bookmark, [url, title, date]
|
|
64
|
+
##
|
|
65
|
+
def search_chrome_history(term)
|
|
66
|
+
# Google history
|
|
67
|
+
history_file = File.expand_path('~/Library/Application Support/Google/Chrome/Default/History')
|
|
68
|
+
if File.exist?(history_file)
|
|
69
|
+
SL.notify('Searching Chrome History', term)
|
|
70
|
+
search_chromium_history(history_file, term)
|
|
71
|
+
else
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
##
|
|
77
|
+
## Generic chromium history search
|
|
78
|
+
##
|
|
79
|
+
## @param history_file [String] The history file
|
|
80
|
+
## path for the selected
|
|
81
|
+
## browser
|
|
82
|
+
## @param term [String] The search term
|
|
83
|
+
##
|
|
84
|
+
## @return [Array] Single bookmark, [url, title, date]
|
|
85
|
+
##
|
|
86
|
+
def search_chromium_history(history_file, term)
|
|
87
|
+
tmpfile = "#{history_file}.tmp"
|
|
88
|
+
FileUtils.cp(history_file, tmpfile)
|
|
89
|
+
|
|
90
|
+
exact_match = false
|
|
91
|
+
match_phrases = []
|
|
92
|
+
|
|
93
|
+
# If search terms start with ''term, only search for exact string matches
|
|
94
|
+
if term =~ /^ *'/
|
|
95
|
+
exact_match = true
|
|
96
|
+
term.gsub!(/(^ *'+|'+ *$)/, '')
|
|
97
|
+
elsif term =~ /%22(.*?)%22/
|
|
98
|
+
match_phrases = term.scan(/%22(\S.*?\S)%22/)
|
|
99
|
+
term.gsub!(/%22(\S.*?\S)%22/, '')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
terms = []
|
|
103
|
+
terms.push("(url NOT LIKE '%search/?%'
|
|
104
|
+
AND url NOT LIKE '%?q=%'
|
|
105
|
+
AND url NOT LIKE '%?s=%'
|
|
106
|
+
AND url NOT LIKE '%duckduckgo.com/?t%')")
|
|
107
|
+
if exact_match
|
|
108
|
+
terms.push("(url LIKE '%#{term.strip.downcase}%' OR title LIKE '%#{term.strip.downcase}%')")
|
|
109
|
+
else
|
|
110
|
+
terms.concat(term.split(/\s+/).map do |t|
|
|
111
|
+
"(url LIKE '%#{t.strip.downcase}%' OR title LIKE '%#{t.strip.downcase}%')"
|
|
112
|
+
end)
|
|
113
|
+
terms.concat(match_phrases.map do |t|
|
|
114
|
+
"(url LIKE '%#{t[0].strip.downcase}%' OR title LIKE '%#{t[0].strip.downcase}%')"
|
|
115
|
+
end)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
query = terms.join(' AND ')
|
|
119
|
+
most_recent = `sqlite3 -json '#{tmpfile}' "select title, url,
|
|
120
|
+
datetime(last_visit_time / 1000000 + (strftime('%s', '1601-01-01')), 'unixepoch') as datum
|
|
121
|
+
from urls where #{query} order by datum desc limit 1 COLLATE NOCASE;"`.strip
|
|
122
|
+
FileUtils.rm_f(tmpfile)
|
|
123
|
+
return false if most_recent.strip.empty?
|
|
124
|
+
|
|
125
|
+
bm = JSON.parse(most_recent)[0]
|
|
126
|
+
|
|
127
|
+
date = Time.parse(bm['datum'])
|
|
128
|
+
[bm['url'], bm['title'], date]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
##
|
|
132
|
+
## Search Arc bookmarks
|
|
133
|
+
##
|
|
134
|
+
## @param term [String] The search term
|
|
135
|
+
##
|
|
136
|
+
## @return [Array] single bookmark [url, title, date]
|
|
137
|
+
##
|
|
138
|
+
def search_arc_bookmarks(term)
|
|
139
|
+
bookmarks_file = File.expand_path('~/Library/Application Support/Arc/User Data/Default/Bookmarks')
|
|
140
|
+
|
|
141
|
+
if File.exist?(bookmarks_file)
|
|
142
|
+
SL.notify('Searching Brave Bookmarks', term)
|
|
143
|
+
return search_chromium_bookmarks(bookmarks_file, term)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
false
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
##
|
|
150
|
+
## Search Brave bookmarks
|
|
151
|
+
##
|
|
152
|
+
## @param term [String] The search term
|
|
153
|
+
##
|
|
154
|
+
## @return [Array] single bookmark [url, title, date]
|
|
155
|
+
##
|
|
156
|
+
def search_brave_bookmarks(term)
|
|
157
|
+
bookmarks_file = File.expand_path('~/Library/Application Support/BraveSoftware/Brave-Browser/Default/Bookmarks')
|
|
158
|
+
|
|
159
|
+
if File.exist?(bookmarks_file)
|
|
160
|
+
SL.notify('Searching Brave Bookmarks', term)
|
|
161
|
+
return search_chromium_bookmarks(bookmarks_file, term)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
false
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
##
|
|
168
|
+
## Search Ege bookmarks
|
|
169
|
+
##
|
|
170
|
+
## @param term [String] The search term
|
|
171
|
+
##
|
|
172
|
+
## @return [Array] single bookmark [url, title, date]
|
|
173
|
+
##
|
|
174
|
+
def search_edge_bookmarks(term)
|
|
175
|
+
bookmarks_file = File.expand_path('~/Library/Application Support/Microsoft Edge/Default/Bookmarks')
|
|
176
|
+
|
|
177
|
+
if File.exist?(bookmarks_file)
|
|
178
|
+
SL.notify('Searching Edge Bookmarks', term)
|
|
179
|
+
return search_chromium_bookmarks(bookmarks_file, term)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
false
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
##
|
|
186
|
+
## Search Chrome bookmarks
|
|
187
|
+
##
|
|
188
|
+
## @param term [String] The search term
|
|
189
|
+
##
|
|
190
|
+
## @return [Array] single bookmark [url, title, date]
|
|
191
|
+
##
|
|
192
|
+
def search_chrome_bookmarks(term)
|
|
193
|
+
bookmarks_file = File.expand_path('~/Library/Application Support/Google/Chrome/Default/Bookmarks')
|
|
194
|
+
|
|
195
|
+
if File.exist?(bookmarks_file)
|
|
196
|
+
SL.notify('Searching Chrome Bookmarks', term)
|
|
197
|
+
return search_chromium_bookmarks(bookmarks_file, term)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
false
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
##
|
|
204
|
+
## Generic chromium bookmark search
|
|
205
|
+
##
|
|
206
|
+
## @param bookmarks_file [String] The path to
|
|
207
|
+
## bookmarks file for
|
|
208
|
+
## selected browser
|
|
209
|
+
## @param term [String] The term
|
|
210
|
+
##
|
|
211
|
+
## @return [Array] single bookmark [url, title, date]
|
|
212
|
+
##
|
|
213
|
+
def search_chromium_bookmarks(bookmarks_file, term)
|
|
214
|
+
chrome_bookmarks = JSON.parse(IO.read(bookmarks_file))
|
|
215
|
+
|
|
216
|
+
exact_match = false
|
|
217
|
+
match_phrases = []
|
|
218
|
+
|
|
219
|
+
# If search terms start with ''term, only search for exact string matches
|
|
220
|
+
if term =~ /^ *'/
|
|
221
|
+
exact_match = true
|
|
222
|
+
term.gsub!(/(^ *'+|'+ *$)/, '')
|
|
223
|
+
elsif term =~ /%22(.*?)%22/
|
|
224
|
+
match_phrases = term.scan(/%22(\S.*?\S)%22/)
|
|
225
|
+
term.gsub!(/%22(\S.*?\S)%22/, '')
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
if chrome_bookmarks
|
|
229
|
+
roots = chrome_bookmarks['roots']
|
|
230
|
+
|
|
231
|
+
urls = extract_chrome_bookmarks(roots, [], term)
|
|
232
|
+
|
|
233
|
+
unless urls.empty?
|
|
234
|
+
urls.delete_if { |bm| !(bm[:url].matches_exact(term) || bm[:title].matches_exact(term)) } if exact_match
|
|
235
|
+
|
|
236
|
+
if match_phrases
|
|
237
|
+
match_phrases.map! { |phrase| phrase[0] }
|
|
238
|
+
urls.delete_if do |bm|
|
|
239
|
+
matched = true
|
|
240
|
+
match_phrases.each do |phrase|
|
|
241
|
+
matched = false unless bm[:url].matches_exact(phrase) || bm[:title].matches_exact(phrase)
|
|
242
|
+
end
|
|
243
|
+
!matched
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
return false if urls.empty?
|
|
248
|
+
|
|
249
|
+
lastest_bookmark = urls.max_by { |u| u[:score] }
|
|
250
|
+
|
|
251
|
+
return [lastest_bookmark[:url], lastest_bookmark[:title], lastest_bookmark[:date]]
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
false
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
##
|
|
259
|
+
## Extract chromium bookmarks from JSON file
|
|
260
|
+
##
|
|
261
|
+
## @param json [String] The json data
|
|
262
|
+
## @param urls [Array] The gathered urls,
|
|
263
|
+
## appended to recursively
|
|
264
|
+
## @param term [String] The search term
|
|
265
|
+
## (optional)
|
|
266
|
+
##
|
|
267
|
+
## @return [Array] array of bookmarks
|
|
268
|
+
##
|
|
269
|
+
def extract_chrome_bookmarks(json, urls = [], term = '')
|
|
270
|
+
if json.instance_of?(Array)
|
|
271
|
+
json.each { |item| urls = extract_chrome_bookmarks(item, urls, term) }
|
|
272
|
+
elsif json.instance_of?(Hash)
|
|
273
|
+
if json.key? 'children'
|
|
274
|
+
urls = extract_chrome_bookmarks(json['children'], urls, term)
|
|
275
|
+
elsif json['type'] == 'url'
|
|
276
|
+
date = Time.at(json['date_added'].to_i / 1000000 + (Time.new(1601, 01, 01).strftime('%s').to_i))
|
|
277
|
+
url = { url: json['url'], title: json['name'], date: date }
|
|
278
|
+
score = score_mark(url, term)
|
|
279
|
+
|
|
280
|
+
if score > 7
|
|
281
|
+
url[:score] = score
|
|
282
|
+
urls << url
|
|
283
|
+
end
|
|
284
|
+
else
|
|
285
|
+
json.each { |_, v| urls = extract_chrome_bookmarks(v, urls, term) }
|
|
286
|
+
end
|
|
287
|
+
else
|
|
288
|
+
return urls
|
|
289
|
+
end
|
|
290
|
+
urls
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
##
|
|
294
|
+
## Score bookmark for search term matches
|
|
295
|
+
##
|
|
296
|
+
## @param mark [Hash] The bookmark
|
|
297
|
+
## @param terms [String] The search terms
|
|
298
|
+
##
|
|
299
|
+
def score_mark(mark, terms)
|
|
300
|
+
return 0 unless mark[:url]
|
|
301
|
+
|
|
302
|
+
score = if mark[:title] && mark[:title].matches_exact(terms)
|
|
303
|
+
12 + mark[:url].matches_score(terms, start_word: false)
|
|
304
|
+
elsif mark[:url].matches_exact(terms)
|
|
305
|
+
11
|
|
306
|
+
elsif mark[:title] && mark[:title].matches_score(terms) > 5
|
|
307
|
+
mark[:title].matches_score(terms)
|
|
308
|
+
elsif mark[:url].matches_score(terms, start_word: false)
|
|
309
|
+
mark[:url].matches_score(terms, start_word: false)
|
|
310
|
+
else
|
|
311
|
+
0
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
score
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
module SL
|
|
2
|
+
class HistorySearch
|
|
3
|
+
class << self
|
|
4
|
+
def search_firefox_history(term)
|
|
5
|
+
# Firefox history
|
|
6
|
+
base = File.expand_path('~/Library/Application Support/Firefox/Profiles')
|
|
7
|
+
Dir.chdir(base)
|
|
8
|
+
profile = Dir.glob('*default-release')
|
|
9
|
+
return false unless profile
|
|
10
|
+
|
|
11
|
+
src = File.join(base, profile[0], 'places.sqlite')
|
|
12
|
+
|
|
13
|
+
exact_match = false
|
|
14
|
+
match_phrases = []
|
|
15
|
+
|
|
16
|
+
# If search terms start with ''term, only search for exact string matches
|
|
17
|
+
case term
|
|
18
|
+
when /^ *'/
|
|
19
|
+
exact_match = true
|
|
20
|
+
term.gsub!(/(^ *'+|'+ *$)/, '')
|
|
21
|
+
when /%22(.*?)%22/
|
|
22
|
+
match_phrases = term.scan(/%22(\S.*?\S)%22/)
|
|
23
|
+
term.gsub!(/%22(\S.*?\S)%22/, '')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if File.exist?(src)
|
|
27
|
+
SL.notify('Searching Firefox History', term)
|
|
28
|
+
tmpfile = "#{src}.tmp"
|
|
29
|
+
FileUtils.cp(src, tmpfile)
|
|
30
|
+
|
|
31
|
+
terms = []
|
|
32
|
+
terms.push("(moz_places.url NOT LIKE '%search/?%'
|
|
33
|
+
AND moz_places.url NOT LIKE '%?q=%'
|
|
34
|
+
AND moz_places.url NOT LIKE '%?s=%'
|
|
35
|
+
AND moz_places.url NOT LIKE '%duckduckgo.com/?t%')")
|
|
36
|
+
if exact_match
|
|
37
|
+
terms.push("(moz_places.url LIKE '%#{term.strip.downcase}%' OR moz_places.title LIKE '%#{term.strip.downcase}%')")
|
|
38
|
+
else
|
|
39
|
+
terms.concat(term.split(/\s+/).map do |t|
|
|
40
|
+
"(moz_places.url LIKE '%#{t.strip.downcase}%' OR moz_places.title LIKE '%#{t.strip.downcase}%')"
|
|
41
|
+
end)
|
|
42
|
+
terms.concat(match_phrases.map do |t|
|
|
43
|
+
"(moz_places.url LIKE '%#{t[0].strip.downcase}%' OR moz_places.title LIKE '%#{t[0].strip.downcase}%')"
|
|
44
|
+
end)
|
|
45
|
+
end
|
|
46
|
+
query = terms.join(' AND ')
|
|
47
|
+
most_recent = `sqlite3 -json '#{tmpfile}' "select moz_places.title, moz_places.url,
|
|
48
|
+
datetime(moz_historyvisits.visit_date/1000000, 'unixepoch', 'localtime') as datum
|
|
49
|
+
from moz_places, moz_historyvisits where moz_places.id = moz_historyvisits.place_id
|
|
50
|
+
and #{query} order by datum desc limit 1 COLLATE NOCASE;"`.strip
|
|
51
|
+
FileUtils.rm_f(tmpfile)
|
|
52
|
+
|
|
53
|
+
return false if most_recent.strip.empty?
|
|
54
|
+
|
|
55
|
+
marks = JSON.parse(most_recent)
|
|
56
|
+
|
|
57
|
+
marks.map! do |bm|
|
|
58
|
+
date = Time.parse(bm['datum'])
|
|
59
|
+
score = score_mark({url: bm['url'], title: bm['title']}, term)
|
|
60
|
+
{ url: bm['url'], title: bm['title'], date: date, score: score }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
m = marks.sort_by { |m| [m[:url].length * -1, m[:score]] }.last
|
|
65
|
+
|
|
66
|
+
[m[:url], m[:title], m[:date]]
|
|
67
|
+
else
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def search_firefox_bookmarks(term)
|
|
73
|
+
# Firefox history
|
|
74
|
+
base = File.expand_path('~/Library/Application Support/Firefox/Profiles')
|
|
75
|
+
Dir.chdir(base)
|
|
76
|
+
profile = Dir.glob('*default-release')
|
|
77
|
+
return false unless profile
|
|
78
|
+
|
|
79
|
+
src = File.join(base, profile[0], 'places.sqlite')
|
|
80
|
+
|
|
81
|
+
exact_match = false
|
|
82
|
+
match_phrases = []
|
|
83
|
+
|
|
84
|
+
# If search terms start with ''term, only search for exact string matches
|
|
85
|
+
if term =~ /^ *'/
|
|
86
|
+
exact_match = true
|
|
87
|
+
term.gsub!(/(^ *'+|'+ *$)/, '')
|
|
88
|
+
elsif term =~ /%22(.*?)%22/
|
|
89
|
+
match_phrases = term.scan(/%22(\S.*?\S)%22/)
|
|
90
|
+
term.gsub!(/%22(\S.*?\S)%22/, '')
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
if File.exist?(src)
|
|
94
|
+
SL.notify('Searching Firefox Bookmarks', term)
|
|
95
|
+
tmpfile = "#{src}.tmp"
|
|
96
|
+
FileUtils.cp(src, tmpfile)
|
|
97
|
+
|
|
98
|
+
terms = []
|
|
99
|
+
terms.push("(h.url NOT LIKE '%search/?%'
|
|
100
|
+
AND h.url NOT LIKE '%?q=%'
|
|
101
|
+
AND h.url NOT LIKE '%?s=%'
|
|
102
|
+
AND h.url NOT LIKE '%duckduckgo.com/?t%')")
|
|
103
|
+
if exact_match
|
|
104
|
+
terms.push("(h.url LIKE '%#{term.strip.downcase}%' OR h.title LIKE '%#{term.strip.downcase}%')")
|
|
105
|
+
else
|
|
106
|
+
terms.concat(term.split(/\s+/).map do |t|
|
|
107
|
+
"(h.url LIKE '%#{t.strip.downcase}%' OR h.title LIKE '%#{t.strip.downcase}%')"
|
|
108
|
+
end)
|
|
109
|
+
terms.concat(match_phrases.map do |t|
|
|
110
|
+
"(h.url LIKE '%#{t[0].strip.downcase}%' OR h.title LIKE '%#{t[0].strip.downcase}%')"
|
|
111
|
+
end)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
query = terms.join(' AND ')
|
|
115
|
+
|
|
116
|
+
most_recent = `sqlite3 -json '#{tmpfile}' "select h.url, b.title,
|
|
117
|
+
datetime(b.dateAdded/1000000, 'unixepoch', 'localtime') as datum
|
|
118
|
+
FROM moz_places h JOIN moz_bookmarks b ON h.id = b.fk
|
|
119
|
+
where #{query} order by datum desc limit 1 COLLATE NOCASE;"`.strip
|
|
120
|
+
FileUtils.rm_f(tmpfile)
|
|
121
|
+
|
|
122
|
+
return false if most_recent.strip.empty?
|
|
123
|
+
|
|
124
|
+
bm = JSON.parse(most_recent)[0]
|
|
125
|
+
|
|
126
|
+
date = Time.parse(bm['datum'])
|
|
127
|
+
score = score_mark({url: bm['url'], title: bm['title']}, term)
|
|
128
|
+
[bm['url'], bm['title'], date, score]
|
|
129
|
+
else
|
|
130
|
+
false
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
module SL
|
|
2
|
+
class HistorySearch
|
|
3
|
+
class << self
|
|
4
|
+
# Search Safari history for terms
|
|
5
|
+
#
|
|
6
|
+
# @param term The search term
|
|
7
|
+
#
|
|
8
|
+
def search_safari_history(term)
|
|
9
|
+
# Safari
|
|
10
|
+
src = File.expand_path('~/Library/Safari/History.db')
|
|
11
|
+
if File.exist?(src)
|
|
12
|
+
SL.notify('Searching Safari History', term)
|
|
13
|
+
|
|
14
|
+
exact_match = false
|
|
15
|
+
match_phrases = []
|
|
16
|
+
|
|
17
|
+
# If search terms start with ''term, only search for exact string matches
|
|
18
|
+
if term =~ /^ *'/
|
|
19
|
+
exact_match = true
|
|
20
|
+
term.gsub!(/(^ *'+|'+ *$)/, '')
|
|
21
|
+
elsif term =~ /%22(.*?)%22/
|
|
22
|
+
match_phrases = term.scan(/%22(\S.*?\S)%22/)
|
|
23
|
+
term.gsub!(/%22(\S.*?\S)%22/, '')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
terms = []
|
|
27
|
+
terms.push("(url NOT LIKE '%search/?%'
|
|
28
|
+
AND url NOT LIKE '%?q=%' AND url NOT LIKE '%?s=%'
|
|
29
|
+
AND url NOT LIKE '%duckduckgo.com/?t%')")
|
|
30
|
+
if exact_match
|
|
31
|
+
terms.push("(url LIKE '%#{term.strip.downcase}%' OR title LIKE '%#{term.strip.downcase}%')")
|
|
32
|
+
else
|
|
33
|
+
terms.concat(term.split(/\s+/).map do |t|
|
|
34
|
+
"(url LIKE '%#{t.strip.downcase}%' OR title LIKE '%#{t.strip.downcase}%')"
|
|
35
|
+
end)
|
|
36
|
+
terms.concat(match_phrases.map do |t|
|
|
37
|
+
"(url LIKE '%#{t[0].strip.downcase}%' OR title LIKE '%#{t[0].strip.downcase}%')"
|
|
38
|
+
end)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
query = terms.join(' AND ')
|
|
42
|
+
|
|
43
|
+
cmd = %(sqlite3 -json '#{src}' "select title, url,
|
|
44
|
+
datetime(visit_time/1000000, 'unixepoch', 'localtime') as datum
|
|
45
|
+
from history_visits INNER JOIN history_items ON history_items.id = history_visits.history_item
|
|
46
|
+
where #{query} order by datum desc limit 1 COLLATE NOCASE;")
|
|
47
|
+
|
|
48
|
+
most_recent = `#{cmd}`.strip
|
|
49
|
+
|
|
50
|
+
return false if most_recent.strip.empty?
|
|
51
|
+
|
|
52
|
+
bm = JSON.parse(most_recent)[0]
|
|
53
|
+
date = Time.parse(bm['datum'])
|
|
54
|
+
[bm['url'], bm['title'], date]
|
|
55
|
+
else
|
|
56
|
+
false
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
## Search Safari bookmarks for relevant search terms
|
|
62
|
+
##
|
|
63
|
+
## @param terms [String] The search terms
|
|
64
|
+
##
|
|
65
|
+
## @return [Array] [url, title, date]
|
|
66
|
+
##
|
|
67
|
+
def search_safari_bookmarks(terms)
|
|
68
|
+
data = `plutil -convert xml1 -o - ~/Library/Safari/Bookmarks.plist`.strip
|
|
69
|
+
parent = Plist.parse_xml(data)
|
|
70
|
+
results = get_safari_bookmarks(parent, terms)
|
|
71
|
+
return false if results.empty?
|
|
72
|
+
|
|
73
|
+
result = results.max_by { |res| [res[:score], res[:title].length] }
|
|
74
|
+
|
|
75
|
+
[result[:url], result[:title], Time.now]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
##
|
|
79
|
+
## Score bookmark for search term matches
|
|
80
|
+
##
|
|
81
|
+
## @param mark [Hash] The bookmark
|
|
82
|
+
## @param terms [String] The search terms
|
|
83
|
+
##
|
|
84
|
+
def score_bookmark(mark, terms)
|
|
85
|
+
score = if mark[:title].matches_exact(terms)
|
|
86
|
+
12 + mark[:url].matches_score(terms, start_word: false)
|
|
87
|
+
elsif mark[:url].matches_exact(terms)
|
|
88
|
+
11
|
|
89
|
+
elsif mark[:title].matches_score(terms) > 5
|
|
90
|
+
mark[:title].matches_score(terms)
|
|
91
|
+
elsif mark[:url].matches_score(terms, start_word: false)
|
|
92
|
+
mark[:url].matches_score(terms, start_word: false)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
{ url: mark[:url], title: mark[:title], score: score }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
##
|
|
99
|
+
## Recursively parse bookmarks hash and score
|
|
100
|
+
## bookmarks
|
|
101
|
+
##
|
|
102
|
+
## @param parent [Hash, Array] The parent
|
|
103
|
+
## bookmark item
|
|
104
|
+
## @param terms [String] The search terms
|
|
105
|
+
##
|
|
106
|
+
## @return [Array] array of scored bookmarks
|
|
107
|
+
##
|
|
108
|
+
def get_safari_bookmarks(parent, terms)
|
|
109
|
+
results = []
|
|
110
|
+
|
|
111
|
+
if parent.is_a?(Array)
|
|
112
|
+
parent.each do |c|
|
|
113
|
+
if c.is_a?(Hash)
|
|
114
|
+
if c.key?('Children')
|
|
115
|
+
results.concat(get_safari_bookmarks(c['Children'], terms))
|
|
116
|
+
elsif c.key?('URIDictionary')
|
|
117
|
+
title = c['URIDictionary']['title']
|
|
118
|
+
url = c['URLString']
|
|
119
|
+
scored = score_bookmark({ url: url, title: title }, terms)
|
|
120
|
+
|
|
121
|
+
results.push(scored) if scored[:score] > 7
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
elsif parent&.key?('Children')
|
|
126
|
+
results.concat(get_safari_bookmarks(parent['Children'], terms))
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
results.sort_by { |h| [h[:score], h[:title].length * -1] }.reverse
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|