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,166 @@
|
|
1
|
+
#import
|
2
|
+
require_relative 'helpers/chromium'
|
3
|
+
|
4
|
+
#import
|
5
|
+
require_relative 'helpers/firefox'
|
6
|
+
|
7
|
+
#import
|
8
|
+
require_relative 'helpers/safari'
|
9
|
+
|
10
|
+
module SL
|
11
|
+
# Browser history/bookmark search
|
12
|
+
class HistorySearch
|
13
|
+
class << self
|
14
|
+
def settings
|
15
|
+
{
|
16
|
+
trigger: 'h(([scfabe])([hb])?)*',
|
17
|
+
searches: [
|
18
|
+
['h', 'Browser History/Bookmark Search'],
|
19
|
+
['hsh', 'Safari History Search'],
|
20
|
+
['hsb', 'Safari Bookmark Search'],
|
21
|
+
['hshb', nil],
|
22
|
+
['hsbh', nil],
|
23
|
+
['hch', 'Chrome History Search'],
|
24
|
+
['hcb', 'Chrome Bookmark Search'],
|
25
|
+
['hchb', nil],
|
26
|
+
['hcbh', nil],
|
27
|
+
['hfh', 'Firefox History Search'],
|
28
|
+
['hfb', 'Firefox Bookmark Search'],
|
29
|
+
['hfhb', nil],
|
30
|
+
['hfbh', nil],
|
31
|
+
['hah', 'Arc History Search'],
|
32
|
+
['hab', 'Arc Bookmark Search'],
|
33
|
+
['hahb', nil],
|
34
|
+
['habh', nil],
|
35
|
+
['hbh', 'Brave History Search'],
|
36
|
+
['hbb', 'Brave Bookmark Search'],
|
37
|
+
['hbhb', nil],
|
38
|
+
['hbbh', nil],
|
39
|
+
['heh', 'Edge History Search'],
|
40
|
+
['heb', 'Edge Bookmark Search'],
|
41
|
+
['hehb', nil],
|
42
|
+
['hebh', nil]
|
43
|
+
]
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def search(search_type, search_terms, link_text)
|
48
|
+
str = search_type.match(/^h(([scfabe])([hb])?)*$/)[1]
|
49
|
+
|
50
|
+
types = []
|
51
|
+
while str && str.length.positive?
|
52
|
+
if str =~ /^s([hb]*)/
|
53
|
+
t = Regexp.last_match(1)
|
54
|
+
if t.length > 1 || t.empty?
|
55
|
+
types.push('safari_history')
|
56
|
+
types.push('safari_bookmarks')
|
57
|
+
elsif t == 'h'
|
58
|
+
types.push('safari_history')
|
59
|
+
elsif t == 'b'
|
60
|
+
types.push('safari_bookmarks')
|
61
|
+
end
|
62
|
+
str.sub!(/^s([hb]*)/, '')
|
63
|
+
end
|
64
|
+
|
65
|
+
if str =~ /^c([hb]*)/
|
66
|
+
t = Regexp.last_match(1)
|
67
|
+
if t.length > 1 || t.empty?
|
68
|
+
types.push('chrome_bookmarks')
|
69
|
+
types.push('chrome_history')
|
70
|
+
elsif t == 'h'
|
71
|
+
types.push('chrome_history')
|
72
|
+
elsif t == 'b'
|
73
|
+
types.push('chrome_bookmarks')
|
74
|
+
end
|
75
|
+
str.sub!(/^c([hb]*)/, '')
|
76
|
+
end
|
77
|
+
|
78
|
+
if str =~ /^f([hb]*)$/
|
79
|
+
t = Regexp.last_match(1)
|
80
|
+
if t.length > 1 || t.empty?
|
81
|
+
types.push('firefox_bookmarks')
|
82
|
+
types.push('firefox_history')
|
83
|
+
elsif t == 'h'
|
84
|
+
types.push('firefox_history')
|
85
|
+
elsif t == 'b'
|
86
|
+
types.push('firefox_bookmarks')
|
87
|
+
end
|
88
|
+
str.sub!(/^f([hb]*)/, '')
|
89
|
+
end
|
90
|
+
|
91
|
+
if str =~ /^e([hb]*)$/
|
92
|
+
t = Regexp.last_match(1)
|
93
|
+
if t.length > 1 || t.empty?
|
94
|
+
types.push('edge_bookmarks')
|
95
|
+
types.push('edge_history')
|
96
|
+
elsif t == 'h'
|
97
|
+
types.push('edge_history')
|
98
|
+
elsif t == 'b'
|
99
|
+
types.push('edge_bookmarks')
|
100
|
+
end
|
101
|
+
str.sub!(/^e([hb]*)/, '')
|
102
|
+
end
|
103
|
+
|
104
|
+
if str =~ /^b([hb]*)$/
|
105
|
+
t = Regexp.last_match(1)
|
106
|
+
if t.length > 1 || t.empty?
|
107
|
+
types.push('brave_bookmarks')
|
108
|
+
types.push('brave_history')
|
109
|
+
elsif t == 'h'
|
110
|
+
types.push('brave_history')
|
111
|
+
elsif t == 'b'
|
112
|
+
types.push('brave_bookmarks')
|
113
|
+
end
|
114
|
+
str.sub!(/^b([hb]*)/, '')
|
115
|
+
end
|
116
|
+
|
117
|
+
next unless str =~ /^a([hb]*)$/
|
118
|
+
|
119
|
+
t = Regexp.last_match(1)
|
120
|
+
if t.length > 1 || t.empty?
|
121
|
+
types.push('arc_bookmarks')
|
122
|
+
types.push('arc_history')
|
123
|
+
elsif t == 'h'
|
124
|
+
types.push('arc_history')
|
125
|
+
elsif t == 'b'
|
126
|
+
types.push('arc_bookmarks')
|
127
|
+
end
|
128
|
+
str.sub!(/^a([hb]*)/, '')
|
129
|
+
end
|
130
|
+
|
131
|
+
url, title = search_history(search_terms, types)
|
132
|
+
link_text = title if link_text == '' || link_text == search_terms
|
133
|
+
[url, title, link_text]
|
134
|
+
end
|
135
|
+
|
136
|
+
def search_history(term, types = [])
|
137
|
+
if types.empty?
|
138
|
+
return false unless SL.config['history_types']
|
139
|
+
|
140
|
+
types = SL.config['history_types']
|
141
|
+
end
|
142
|
+
|
143
|
+
results = []
|
144
|
+
|
145
|
+
if !types.empty?
|
146
|
+
types.each do |type|
|
147
|
+
url, title, date = send("search_#{type}", term)
|
148
|
+
|
149
|
+
results << { 'url' => url, 'title' => title, 'date' => date } if url
|
150
|
+
end
|
151
|
+
|
152
|
+
if results.empty?
|
153
|
+
false
|
154
|
+
else
|
155
|
+
out = results.sort_by! { |r| r['date'] }.last
|
156
|
+
[out['url'], out['title']]
|
157
|
+
end
|
158
|
+
else
|
159
|
+
false
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
SL::Searches.register 'history', :search, self
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module SL
|
2
|
+
#
|
3
|
+
# Hookmark String helpers
|
4
|
+
#
|
5
|
+
class ::String
|
6
|
+
def split_hook
|
7
|
+
elements = split(/\|\|/)
|
8
|
+
{
|
9
|
+
name: elements[0].nil_if_missing,
|
10
|
+
url: elements[1].nil_if_missing,
|
11
|
+
path: elements[2].nil_if_missing
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def split_hooks
|
16
|
+
split(/\^\^/).map(&:split_hook)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
##
|
21
|
+
## Hookmark Search
|
22
|
+
##
|
23
|
+
class HookSearch
|
24
|
+
class << self
|
25
|
+
def settings
|
26
|
+
{
|
27
|
+
trigger: 'hook',
|
28
|
+
searches: [
|
29
|
+
['hook', 'Hookmark Bookmark Search']
|
30
|
+
]
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
# Main search method
|
35
|
+
def search(_, search_terms, link_text)
|
36
|
+
url, title = search_hook(search_terms)
|
37
|
+
[url, title, link_text]
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
## Run the AppleScript Hookmark query
|
42
|
+
##
|
43
|
+
## @param query [String] The query
|
44
|
+
##
|
45
|
+
def run_query(query)
|
46
|
+
`osascript <<'APPLESCRIPT'
|
47
|
+
tell application "Hook"
|
48
|
+
set _marks to every bookmark whose #{query}
|
49
|
+
set _out to {}
|
50
|
+
repeat with _hook in _marks
|
51
|
+
set _out to _out & (name of _hook & "||" & address of _hook & "||" & path of _hook)
|
52
|
+
end repeat
|
53
|
+
set {astid, AppleScript's text item delimiters} to {AppleScript's text item delimiters, "^^"}
|
54
|
+
set _output to _out as string
|
55
|
+
set AppleScript's text item delimiters to astid
|
56
|
+
return _output
|
57
|
+
end tell
|
58
|
+
APPLESCRIPT`.strip.split_hooks
|
59
|
+
end
|
60
|
+
|
61
|
+
# Search bookmark paths and addresses. Return array of bookmark hashes.
|
62
|
+
def search_hook(search)
|
63
|
+
types = %w[name path address]
|
64
|
+
query = search.strip.split(' ').map { |s| types.map { |t| %(#{t} contains "#{s}") }.join(' or ') }
|
65
|
+
query = query.map { |q| "(#{q})" }.join(' and ')
|
66
|
+
path_matches = run_query(query)
|
67
|
+
|
68
|
+
top_match = path_matches.uniq.first
|
69
|
+
return false unless top_match
|
70
|
+
|
71
|
+
[top_match[:url], top_match[:name]]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
SL::Searches.register 'hook', :search, self
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# title: iTunes Search
|
2
|
+
# description: Search iTunes
|
3
|
+
module SL
|
4
|
+
class ITunesSearch
|
5
|
+
class << self
|
6
|
+
def settings
|
7
|
+
{
|
8
|
+
trigger: '(i(pod|art|alb|song|tud?)|masd?)',
|
9
|
+
searches: [
|
10
|
+
['ipod', 'iTunes podcast'],
|
11
|
+
['iart', 'iTunes artist'],
|
12
|
+
['ialb', 'iTunes album'],
|
13
|
+
['isong', 'iTunes song'],
|
14
|
+
['itu', 'iOS App Store Search'],
|
15
|
+
['itud', 'iOS App Store Developer Link'],
|
16
|
+
['mas', 'Mac App Store Search'],
|
17
|
+
['masd', 'Mac App Store Developer Link']
|
18
|
+
]
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def search(search_type, search_terms, link_text)
|
23
|
+
case search_type
|
24
|
+
when /^ialb$/ # iTunes Album Search
|
25
|
+
url, title = search_itunes('album', search_terms, false)
|
26
|
+
when /^iart$/ # iTunes Artist Search
|
27
|
+
url, title = search_itunes('musicArtist', search_terms, false)
|
28
|
+
when /^imov?$/ # iTunes movie search
|
29
|
+
dev = false
|
30
|
+
url, title = search_itunes('movie', search_terms, dev, SL.config['itunes_affiliate'])
|
31
|
+
when /^ipod$/
|
32
|
+
url, title = search_itunes('podcast', search_terms, false)
|
33
|
+
when /^isong$/ # iTunes Song Search
|
34
|
+
url, title = search_itunes('song', search_terms, false)
|
35
|
+
when /^itud?$/ # iTunes app search
|
36
|
+
dev = search_type =~ /d$/
|
37
|
+
url, title = search_itunes('iPadSoftware', search_terms, dev, SL.config['itunes_affiliate'])
|
38
|
+
when /^masd?$/ # Mac App Store search (mas = itunes link, masd = developer link)
|
39
|
+
dev = search_type =~ /d$/
|
40
|
+
url, title = search_itunes('macSoftware', search_terms, dev, SL.config['itunes_affiliate'])
|
41
|
+
end
|
42
|
+
|
43
|
+
[url, title, link_text]
|
44
|
+
end
|
45
|
+
|
46
|
+
def search_itunes(entity, terms, dev, aff = nil)
|
47
|
+
aff ||= SL.config['itunes_affiliate']
|
48
|
+
|
49
|
+
url = "http://itunes.apple.com/search?term=#{terms.url_encode}&country=#{SL.config['country_code']}&entity=#{entity}&limit=1"
|
50
|
+
|
51
|
+
begin
|
52
|
+
page = Curl::Json.new(url, compressed: true)
|
53
|
+
json = page.json
|
54
|
+
rescue StandardError => e
|
55
|
+
SL.add_error('Invalid response', "Search for #{terms}: (#{e})")
|
56
|
+
return false
|
57
|
+
end
|
58
|
+
return false unless json
|
59
|
+
|
60
|
+
return false unless json['resultCount']&.positive?
|
61
|
+
|
62
|
+
result = json['results'][0]
|
63
|
+
case entity
|
64
|
+
when /movie/
|
65
|
+
# dev parameter probably not necessary in this case
|
66
|
+
output_url = result['trackViewUrl']
|
67
|
+
output_title = result['trackName']
|
68
|
+
when /(mac|iPad)Software/
|
69
|
+
output_url = dev && result['sellerUrl'] ? result['sellerUrl'] : result['trackViewUrl']
|
70
|
+
output_title = result['trackName']
|
71
|
+
when /(musicArtist|song|album)/
|
72
|
+
case result['wrapperType']
|
73
|
+
when 'track'
|
74
|
+
output_url = result['trackViewUrl']
|
75
|
+
output_title = "#{result['trackName']} by #{result['artistName']}"
|
76
|
+
when 'collection'
|
77
|
+
output_url = result['collectionViewUrl']
|
78
|
+
output_title = "#{result['collectionName']} by #{result['artistName']}"
|
79
|
+
when 'artist'
|
80
|
+
output_url = result['artistLinkUrl']
|
81
|
+
output_title = result['artistName']
|
82
|
+
end
|
83
|
+
when /podcast/
|
84
|
+
output_url = result['collectionViewUrl']
|
85
|
+
output_title = result['collectionName']
|
86
|
+
end
|
87
|
+
return false unless output_url && output_title
|
88
|
+
|
89
|
+
return [output_url, output_title] if dev
|
90
|
+
|
91
|
+
[output_url + aff, output_title]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
SL::Searches.register 'itunes', :search, self
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module SL
|
2
|
+
class LastFMSearch
|
3
|
+
class << self
|
4
|
+
def settings
|
5
|
+
{
|
6
|
+
trigger: 'l(art|song)',
|
7
|
+
searches: [
|
8
|
+
['lart', 'Last.fm Artist Search'],
|
9
|
+
['lsong', 'Last.fm Song Search']
|
10
|
+
]
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
def search(search_type, search_terms, link_text)
|
15
|
+
type = search_type =~ /art$/ ? 'artist' : 'track'
|
16
|
+
|
17
|
+
url = "http://ws.audioscrobbler.com/2.0/?method=#{type}.search&#{type}=#{search_terms.url_encode}&api_key=2f3407ec29601f97ca8a18ff580477de&format=json"
|
18
|
+
json = Curl::Json.new(url).json
|
19
|
+
return false unless json['results']
|
20
|
+
|
21
|
+
begin
|
22
|
+
case type
|
23
|
+
when 'track'
|
24
|
+
result = json['results']['trackmatches']['track'][0]
|
25
|
+
url = result['url']
|
26
|
+
title = "#{result['name']} by #{result['artist']}"
|
27
|
+
when 'artist'
|
28
|
+
result = json['results']['artistmatches']['artist'][0]
|
29
|
+
url = result['url']
|
30
|
+
title = result['name']
|
31
|
+
end
|
32
|
+
[url, title, link_text]
|
33
|
+
rescue StandardError
|
34
|
+
false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
SL::Searches.register 'lastfm', :search, self
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# Always start with module SL
|
2
|
+
module SL
|
3
|
+
# Give it a unique class name
|
4
|
+
class LyricsSearch
|
5
|
+
class << self
|
6
|
+
# Settings block is required with `trigger` and `searches`
|
7
|
+
def settings
|
8
|
+
{
|
9
|
+
# `trigger` is A regular expression that will trigger this plugin
|
10
|
+
# when used with a bang. The one below will trigger on !lyrics or
|
11
|
+
# !lyricse.
|
12
|
+
trigger: 'lyrics?e?',
|
13
|
+
# Every search that the plugin should execute should be individually
|
14
|
+
# listed and described in the searches array. This is used for
|
15
|
+
# completion and help generation. Do not include the bang (!) in the
|
16
|
+
# search keyword.
|
17
|
+
searches: [
|
18
|
+
['lyric', 'Song Lyrics Search'],
|
19
|
+
['lyrice', 'Song Lyrics Embed']
|
20
|
+
]
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
# Every plugin must contain a #search method that takes 3 arguments:
|
25
|
+
#
|
26
|
+
# - `search_type` will contain the !search trigger that was used (minus the !)
|
27
|
+
# - `search_terms` will include everything that came after the !search
|
28
|
+
# - `link_text` will contain the text that will be used for the linked
|
29
|
+
# text portion of the link. This can usually remain untouched but must
|
30
|
+
# be passed back at the end of the function.
|
31
|
+
def search(search_type, search_terms, link_text)
|
32
|
+
# You can branch to multiple searches by testing the search_type
|
33
|
+
case search_type
|
34
|
+
when /e$/
|
35
|
+
url, title = SL.ddg("site:genius.com #{search_terms}", link_text)
|
36
|
+
if url
|
37
|
+
title = get_lyrics(url)
|
38
|
+
# To return an embed, set url (first parameter in the return
|
39
|
+
# array) to 'embed', and put the embed contents in the second
|
40
|
+
# parameter.
|
41
|
+
title ? ['embed', title, link_text] : false
|
42
|
+
else
|
43
|
+
# Use `SL#add_error(title, text)` to add errors to the HTML
|
44
|
+
# report. The report will only be shown if errors have been added.
|
45
|
+
SL.add_error('No lyrics found', "Song lyrics for #{search_terms} not found")
|
46
|
+
false
|
47
|
+
end
|
48
|
+
else
|
49
|
+
# You can perform a DuckDuckGo search using SL#ddg, passing the
|
50
|
+
# search terms and link_text. It will return url, title, and
|
51
|
+
# link_text. SL#ddg will add its own errors, and if it returns false
|
52
|
+
# that will automatically be tested for, no additional error
|
53
|
+
# handling is required.
|
54
|
+
url, title, link_text = SL.ddg("site:genius.com #{search_terms}", link_text)
|
55
|
+
# Always return an array containing the resulting URL, the title,
|
56
|
+
# and the link_text variable that was passed in, even if it's
|
57
|
+
# unmodified.
|
58
|
+
[url, title, link_text]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Any additional helper methods can be defined after #search
|
63
|
+
def get_lyrics(url)
|
64
|
+
if SL::URL.valid_link?(url)
|
65
|
+
# You can use Ruby's net/http methods for retrieving pages, but
|
66
|
+
# `curl -SsL` is faster and easier. Curl::Html.new(url) returns a
|
67
|
+
# new object containing :body
|
68
|
+
body = Curl::Html.new(url).body
|
69
|
+
|
70
|
+
matches = body.scan(%r{class="Lyrics__Container-.*?>(.*?)</div><div class="LyricsFooter})
|
71
|
+
|
72
|
+
lyrics = matches.join("\n")
|
73
|
+
|
74
|
+
if lyrics
|
75
|
+
"```\n#{CGI.unescape(lyrics).gsub(%r{<br/?>}, " \n").gsub(%r{</?.*?>}, '').gsub(/'/, "'")}\n```"
|
76
|
+
else
|
77
|
+
false
|
78
|
+
end
|
79
|
+
else
|
80
|
+
false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# At the end of the search class, you must register it as a plugin. This
|
86
|
+
# method takes a title, a type (:search for a search plugin), and the
|
87
|
+
# unique class. When running #register within the search class itself,
|
88
|
+
# you can just use `self`.
|
89
|
+
SL::Searches.register 'lyrics', :search, self
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
module SL
|
2
|
+
class PinboardSearch
|
3
|
+
PINBOARD_CACHE = SL::Util.cache_file_for('pinboard')
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def settings
|
7
|
+
{
|
8
|
+
trigger: 'pb',
|
9
|
+
searches: [
|
10
|
+
['pb', 'Pinboard Bookmark Search']
|
11
|
+
]
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def pinboard_bookmarks
|
16
|
+
curl = TTY::Which.which('curl')
|
17
|
+
bookmarks = `#{curl} -sSL "https://api.pinboard.in/v1/posts/all?auth_token=#{SL.config['pinboard_api_key']}&format=json"`
|
18
|
+
bookmarks = bookmarks.force_encoding('utf-8')
|
19
|
+
bookmarks.gsub!(/[^[:ascii:]]/) do |non_ascii|
|
20
|
+
non_ascii.force_encoding('utf-8')
|
21
|
+
.encode('utf-16be')
|
22
|
+
.unpack('H*')
|
23
|
+
.gsub(/(....)/, '\u\1')
|
24
|
+
end
|
25
|
+
|
26
|
+
bookmarks.gsub!(/[\u{1F600}-\u{1F6FF}]/, '')
|
27
|
+
|
28
|
+
bookmarks = JSON.parse(bookmarks)
|
29
|
+
updated = Time.now
|
30
|
+
{ 'update_time' => updated, 'bookmarks' => bookmarks }
|
31
|
+
end
|
32
|
+
|
33
|
+
def save_pinboard_cache(cache)
|
34
|
+
cachefile = PINBOARD_CACHE
|
35
|
+
|
36
|
+
# file = File.new(cachefile,'w')
|
37
|
+
# file = Zlib::GzipWriter.new(File.new(cachefile,'w'))
|
38
|
+
begin
|
39
|
+
File.open(cachefile, 'wb') { |f| f.write(Marshal.dump(cache)) }
|
40
|
+
rescue IOError
|
41
|
+
SL.add_error('Pinboard cache error', 'Failed to write stash to disk')
|
42
|
+
return false
|
43
|
+
end
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
def load_pinboard_cache
|
48
|
+
refresh_cache = false
|
49
|
+
cachefile = PINBOARD_CACHE
|
50
|
+
|
51
|
+
if File.exist?(cachefile)
|
52
|
+
begin
|
53
|
+
# file = IO.read(cachefile) # Zlib::GzipReader.open(cachefile)
|
54
|
+
# cache = Marshal.load file
|
55
|
+
cache = Marshal.load(File.binread(cachefile))
|
56
|
+
# file.close
|
57
|
+
rescue IOError # Zlib::GzipFile::Error
|
58
|
+
SL.add_error('Error loading pinboard cache', "IOError reading #{cachefile}")
|
59
|
+
cache = pinboard_bookmarks
|
60
|
+
save_pinboard_cache(cache)
|
61
|
+
rescue StandardError
|
62
|
+
SL.add_error('Error loading pinboard cache', "StandardError reading #{cachefile}")
|
63
|
+
cache = pinboard_bookmarks
|
64
|
+
save_pinboard_cache(cache)
|
65
|
+
end
|
66
|
+
curl = TTY::Which.which('curl')
|
67
|
+
updated = JSON.parse(`#{curl} -SsL 'https://api.pinboard.in/v1/posts/update?auth_token=#{SL.config['pinboard_api_key']}&format=json'`)
|
68
|
+
last_bookmark = Time.parse(updated['update_time'])
|
69
|
+
if cache&.key?('update_time')
|
70
|
+
last_update = cache['update_time']
|
71
|
+
refresh_cache = true if last_update < last_bookmark
|
72
|
+
else
|
73
|
+
refresh_cache = true
|
74
|
+
end
|
75
|
+
else
|
76
|
+
refresh_cache = true
|
77
|
+
end
|
78
|
+
|
79
|
+
if refresh_cache
|
80
|
+
cache = pinboard_bookmarks
|
81
|
+
save_pinboard_cache(cache)
|
82
|
+
end
|
83
|
+
|
84
|
+
cache
|
85
|
+
end
|
86
|
+
|
87
|
+
# Search pinboard bookmarks
|
88
|
+
# Begin query with '' to force exact matching (including description text)
|
89
|
+
# Regular matching searches for each word of query and scores the bookmarks
|
90
|
+
# exact matches in title get highest score
|
91
|
+
# exact matches in description get second highest score
|
92
|
+
# other bookmarks are scored based on the number of words that match
|
93
|
+
#
|
94
|
+
# After sorting by score, bookmarks will be sorted by date and the most recent
|
95
|
+
# will be returned
|
96
|
+
#
|
97
|
+
# Exact matching is case and punctuation insensitive
|
98
|
+
def search(_, search_terms, link_text)
|
99
|
+
unless SL.config['pinboard_api_key']
|
100
|
+
SL.add_error('Missing Pinboard API token',
|
101
|
+
'Find your api key at https://pinboard.in/settings/password and add it
|
102
|
+
to your configuration (pinboard_api_key: YOURKEY)')
|
103
|
+
return false
|
104
|
+
end
|
105
|
+
|
106
|
+
exact_match = false
|
107
|
+
match_phrases = []
|
108
|
+
|
109
|
+
# If search terms start with ''term, only search for exact string matches
|
110
|
+
case search_terms
|
111
|
+
when /^ *'/
|
112
|
+
exact_match = true
|
113
|
+
search_terms.gsub!(/(^ *'+|'+ *$)/, '')
|
114
|
+
when /%22(.*?)%22/
|
115
|
+
match_phrases = search_terms.scan(/%22(\S.*?\S)%22/)
|
116
|
+
search_terms.gsub!(/%22(\S.*?\S)%22/, '')
|
117
|
+
end
|
118
|
+
|
119
|
+
cache = load_pinboard_cache
|
120
|
+
# cache = pinboard_bookmarks
|
121
|
+
bookmarks = cache['bookmarks']
|
122
|
+
|
123
|
+
if exact_match
|
124
|
+
bookmarks.each do |bm|
|
125
|
+
text = [bm['description'], bm['extended'], bm['tags']].join(' ')
|
126
|
+
|
127
|
+
return [bm['href'], bm['description']] if text.matches_exact(search_terms)
|
128
|
+
end
|
129
|
+
|
130
|
+
return false
|
131
|
+
end
|
132
|
+
|
133
|
+
unless match_phrases.empty?
|
134
|
+
bookmarks.delete_if do |bm|
|
135
|
+
matched = tru
|
136
|
+
full_text = [bm['description'], bm['extended'], bm['tags']].join(' ')
|
137
|
+
match_phrases.each do |phrase|
|
138
|
+
matched = false unless full_text.matches_exact(phrase)
|
139
|
+
end
|
140
|
+
!matched
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
matches = []
|
145
|
+
bookmarks.each do |bm|
|
146
|
+
title_tags = [bm['description'], bm['tags']].join(' ')
|
147
|
+
full_text = [bm['description'], bm['extended'], bm['tags']].join(' ')
|
148
|
+
|
149
|
+
score = if title_tags.matches_exact(search_terms)
|
150
|
+
14.0
|
151
|
+
elsif full_text.matches_exact(search_terms)
|
152
|
+
13.0
|
153
|
+
elsif full_text.matches_any(search_terms)
|
154
|
+
full_text.matches_score(search_terms)
|
155
|
+
else
|
156
|
+
0
|
157
|
+
end
|
158
|
+
|
159
|
+
return [bm['href'], bm['description']] if score == 14
|
160
|
+
|
161
|
+
next unless score.positive?
|
162
|
+
|
163
|
+
matches.push({
|
164
|
+
score: score,
|
165
|
+
href: bm['href'],
|
166
|
+
title: bm['description'],
|
167
|
+
date: bm['time']
|
168
|
+
})
|
169
|
+
end
|
170
|
+
|
171
|
+
return false if matches.empty?
|
172
|
+
|
173
|
+
top = matches.max_by { |bm| [bm[:score], bm[:date]] }
|
174
|
+
|
175
|
+
return false unless top
|
176
|
+
|
177
|
+
[top[:href], top[:title], link_text]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
SL::Searches.register 'pinboard', :search, self
|
182
|
+
end
|
183
|
+
end
|