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