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,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
|