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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/bin/searchlink +84 -0
  3. data/lib/searchlink/array.rb +7 -0
  4. data/lib/searchlink/config.rb +230 -0
  5. data/lib/searchlink/curl/html.rb +482 -0
  6. data/lib/searchlink/curl/json.rb +90 -0
  7. data/lib/searchlink/curl.rb +7 -0
  8. data/lib/searchlink/help.rb +103 -0
  9. data/lib/searchlink/output.rb +270 -0
  10. data/lib/searchlink/parse.rb +668 -0
  11. data/lib/searchlink/plist.rb +213 -0
  12. data/lib/searchlink/search.rb +70 -0
  13. data/lib/searchlink/searches/amazon.rb +25 -0
  14. data/lib/searchlink/searches/applemusic.rb +123 -0
  15. data/lib/searchlink/searches/bitly.rb +50 -0
  16. data/lib/searchlink/searches/definition.rb +67 -0
  17. data/lib/searchlink/searches/duckduckgo.rb +167 -0
  18. data/lib/searchlink/searches/github.rb +245 -0
  19. data/lib/searchlink/searches/google.rb +67 -0
  20. data/lib/searchlink/searches/helpers/chromium.rb +318 -0
  21. data/lib/searchlink/searches/helpers/firefox.rb +135 -0
  22. data/lib/searchlink/searches/helpers/safari.rb +133 -0
  23. data/lib/searchlink/searches/history.rb +166 -0
  24. data/lib/searchlink/searches/hook.rb +77 -0
  25. data/lib/searchlink/searches/itunes.rb +97 -0
  26. data/lib/searchlink/searches/lastfm.rb +41 -0
  27. data/lib/searchlink/searches/lyrics.rb +91 -0
  28. data/lib/searchlink/searches/pinboard.rb +183 -0
  29. data/lib/searchlink/searches/social.rb +105 -0
  30. data/lib/searchlink/searches/software.rb +27 -0
  31. data/lib/searchlink/searches/spelling.rb +59 -0
  32. data/lib/searchlink/searches/spotlight.rb +28 -0
  33. data/lib/searchlink/searches/stackoverflow.rb +31 -0
  34. data/lib/searchlink/searches/tmdb.rb +52 -0
  35. data/lib/searchlink/searches/twitter.rb +46 -0
  36. data/lib/searchlink/searches/wikipedia.rb +33 -0
  37. data/lib/searchlink/searches/youtube.rb +48 -0
  38. data/lib/searchlink/searches.rb +194 -0
  39. data/lib/searchlink/semver.rb +140 -0
  40. data/lib/searchlink/string.rb +469 -0
  41. data/lib/searchlink/url.rb +153 -0
  42. data/lib/searchlink/util.rb +87 -0
  43. data/lib/searchlink/version.rb +93 -0
  44. data/lib/searchlink/which.rb +175 -0
  45. data/lib/searchlink.rb +66 -0
  46. data/lib/tokens.rb +3 -0
  47. metadata +299 -0
@@ -0,0 +1,153 @@
1
+ module SL
2
+ module URL
3
+ class << self
4
+ # Validates that a link exists and returns 200
5
+ def valid_link?(uri_str, limit = 5)
6
+ return false unless uri_str
7
+
8
+ SL.notify('Validating', uri_str)
9
+ return false if limit.zero?
10
+
11
+ url = URI(uri_str)
12
+ return true unless url.scheme
13
+
14
+ url.path = '/' if url.path == ''
15
+ # response = Net::HTTP.get_response(URI(uri_str))
16
+ response = false
17
+
18
+ Net::HTTP.start(url.host, url.port, use_ssl: url.scheme == 'https') do |http|
19
+ response = http.request_head(url.path)
20
+ end
21
+
22
+ case response
23
+ when Net::HTTPMethodNotAllowed, Net::HTTPServiceUnavailable
24
+ unless /amazon\.com/ =~ url.host
25
+ SL.add_error('link validation', "Validation blocked: #{uri_str} (#{e})")
26
+ end
27
+ SL.notify('Error validating', uri_str)
28
+ true
29
+ when Net::HTTPSuccess
30
+ true
31
+ when Net::HTTPRedirection
32
+ location = response['location']
33
+ valid_link?(location, limit - 1)
34
+ else
35
+ SL.notify('Error validating', uri_str)
36
+ false
37
+ end
38
+ rescue StandardError => e
39
+ SL.notify('Error validating', uri_str)
40
+ SL.add_error('link validation', "Possibly invalid => #{uri_str} (#{e})")
41
+ true
42
+ end
43
+
44
+ def url?(input)
45
+ input =~ %r{^(#.*|https?://\S+|/\S+|\S+/|[^!]\S+\.\S+)(\s+".*?")?$}
46
+ end
47
+
48
+ def only_url?(input)
49
+ input =~ %r{(?i)^((http|https)://)?([\w\-_]+(\.[\w\-_]+)+)([\w\-.,@?^=%&amp;:/~+#]*[\w\-@^=%&amp;/~+#])?$}
50
+ end
51
+
52
+ def ref_title_for_url(url)
53
+ url = URI.parse(url) if url.is_a?(String)
54
+
55
+ parts = url.hostname.split(/\./)
56
+ domain = if parts.count > 1
57
+ parts.slice(-2, 1).join('')
58
+ else
59
+ parts.join('')
60
+ end
61
+
62
+ path = url.path.split(%r{/}).last
63
+ if path
64
+ path.gsub!(/-/, ' ').gsub!(/\.\w{2-4}$/, '')
65
+ else
66
+ path = domain
67
+ end
68
+
69
+ path.length > domain.length ? path : domain
70
+ end
71
+
72
+ def url_to_link(url, type)
73
+ input = url.dup
74
+
75
+ if only_url?(input)
76
+ input.sub!(%r{(?mi)^(?!https?://)(.*?)$}, 'https://\1')
77
+ url = URI.parse(input.downcase)
78
+
79
+ title = if type == :ref_title
80
+ ref_title_for_url(url)
81
+ else
82
+ title(url.to_s) || input.sub(%r{^https?://}, '')
83
+ end
84
+
85
+ return [url.to_s, title] if url.hostname
86
+ end
87
+ false
88
+ end
89
+
90
+ def amazon_affiliatize(url, amazon_partner)
91
+ return url if amazon_partner.nil? || amazon_partner.empty?
92
+
93
+ unless url =~ %r{https?://(?<subdomain>.*?)amazon.com/(?:(?<title>.*?)/)?(?<type>[dg])p/(?<id>[^?]+)}
94
+ return [url, '']
95
+ end
96
+
97
+ m = Regexp.last_match
98
+ sd = m['subdomain']
99
+ title = m['title'].gsub(/-/, ' ')
100
+ t = m['type']
101
+ id = m['id']
102
+ ["https://#{sd}amazon.com/#{t}p/#{id}/?ref=as_li_ss_tl&ie=UTF8&linkCode=sl1&tag=#{amazon_partner}", title]
103
+ end
104
+
105
+ def title(url)
106
+ title = nil
107
+
108
+ ## Gather proving too inexact
109
+ # gather = false
110
+ # ['/usr/local/bin', '/opt/homebrew/bin'].each do |root|
111
+ # if File.exist?(File.join(root, 'gather')) && File.executable?(File.join(root, 'gather'))
112
+ # gather = File.join(root, 'gather')
113
+ # break
114
+ # end
115
+ # end
116
+
117
+ # if gather
118
+ # cmd = %(#{gather} --title-only '#{url.strip}' --fallback-title 'Unknown')
119
+ # title = SL::Util.exec_with_timeout(cmd, 15)
120
+ # if title
121
+ # title = title.strip.gsub(/\n+/, ' ').gsub(/ +/, ' ')
122
+ # title.remove_seo!(url) if SL.config['remove_seo']
123
+ # return title.remove_protocol
124
+ # else
125
+ # SL.add_error('Error retrieving title', "Gather timed out on #{url}")
126
+ # SL.notify('Error retrieving title', 'Gather timed out')
127
+ # end
128
+ # end
129
+
130
+ begin
131
+ page = Curl::Html.new(url)
132
+
133
+ title = page.title || nil
134
+
135
+ if title.nil? || title =~ /^\s*$/
136
+ SL.add_error('Title not found', "Warning: missing title for #{url.strip}")
137
+ title = url.gsub(%r{(^https?://|/.*$)}, '').gsub(/-/, ' ').strip
138
+ else
139
+ title = title.gsub(/\n/, ' ').gsub(/\s+/, ' ').strip # .sub(/[^a-z]*$/i,'')
140
+ title.remove_seo!(url) if SL.config['remove_seo']
141
+ end
142
+ title.gsub!(/\|/, '—')
143
+ title.remove_seo!(url.strip) if SL.config['remove_seo']
144
+ title.remove_protocol
145
+ rescue StandardError
146
+ SL.add_error('Error retrieving title', "Error determining title for #{url.strip}")
147
+ warn "Error retrieving title for #{url.strip}"
148
+ url.remove_protocol
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,87 @@
1
+ module SL
2
+ module Util
3
+ class << self
4
+ ## Execute system command with deadman's switch
5
+ ##
6
+ ## <https://stackoverflow.com/questions/8292031/ruby-timeouts-and-system-commands>
7
+ ## <https://stackoverflow.com/questions/12189904/fork-child-process-with-timeout-and-capture-output>
8
+ ##
9
+ ## @param cmd The command to execute
10
+ ## @param timeout The timeout
11
+ ##
12
+ ## @return [String] STDOUT output
13
+ ##
14
+ def exec_with_timeout(cmd, timeout)
15
+ begin
16
+ # stdout, stderr pipes
17
+ rout, wout = IO.pipe
18
+ rerr, werr = IO.pipe
19
+ stdout, stderr = nil
20
+
21
+ pid = Process.spawn(cmd, pgroup: true, out: wout, err: werr)
22
+
23
+ Timeout.timeout(timeout) do
24
+ Process.waitpid(pid)
25
+
26
+ # close write ends so we can read from them
27
+ wout.close
28
+ werr.close
29
+
30
+ stdout = rout.readlines.join
31
+ stderr = rerr.readlines.join
32
+ end
33
+ rescue Timeout::Error
34
+ Process.kill(-9, pid)
35
+ Process.detach(pid)
36
+ ensure
37
+ wout.close unless wout.closed?
38
+ werr.close unless werr.closed?
39
+ # dispose the read ends of the pipes
40
+ rout.close
41
+ rerr.close
42
+ end
43
+
44
+ stdout&.strip
45
+ end
46
+
47
+ ##
48
+ ## Execute a search with deadman's switch
49
+ ##
50
+ ## @param search [Proc] The search command
51
+ ## @param timeout [Number] The timeout
52
+ ##
53
+ ## @return [Array] url, title, link_text
54
+ ##
55
+ def search_with_timeout(search, timeout)
56
+ url = nil
57
+ title = nil
58
+ link_text = nil
59
+
60
+ begin
61
+ Timeout.timeout(timeout) do
62
+ url, title, link_text = search.call
63
+ end
64
+ rescue Timeout::Error
65
+ SL.add_error('Timeout', 'Search timed out')
66
+ url, title, link_text = false
67
+ end
68
+
69
+ [url, title, link_text]
70
+ end
71
+
72
+ ##
73
+ ## Get the path for a cache file
74
+ ##
75
+ ## @param filename [String] The filename to
76
+ ## generate the cache for
77
+ ##
78
+ ## @return [String] path to new cache file
79
+ ##
80
+ def cache_file_for(filename)
81
+ cache_folder = File.expand_path('~/.local/share/searchlink/cache')
82
+ FileUtils.mkdir_p(cache_folder) unless File.directory?(cache_folder)
83
+ File.join(cache_folder, filename.sub(/(\.cache)?$/, '.cache'))
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,93 @@
1
+ module SL
2
+ VERSION = '2.3.59'
3
+ end
4
+
5
+ module SL
6
+ class << self
7
+ def version_check
8
+ cachefile = File.expand_path('~/.searchlink_update_check')
9
+ if File.exist?(cachefile)
10
+ last_check, latest_tag = IO.read(cachefile).strip.split(/\|/)
11
+ last_time = Time.parse(last_check)
12
+ else
13
+ latest_tag = new_version?
14
+ last_time = Time.now
15
+ end
16
+
17
+ if last_time + (24 * 60 * 60) < Time.now
18
+ latest_tag = new_version?
19
+ last_time = Time.now
20
+ end
21
+
22
+ latest_tag ||= SL::VERSION
23
+ latest = SemVer.new(latest_tag)
24
+ current = SemVer.new(SL::VERSION)
25
+
26
+ File.open(cachefile, 'w') { |f| f.puts("#{last_time.strftime('%c')}|#{latest.to_s}") }
27
+
28
+ return "SearchLink v#{current.to_s}, #{latest.to_s} available. Run 'update' to download." if latest_tag && current.older_than(latest)
29
+
30
+ "SearchLink v#{current.to_s}"
31
+ end
32
+
33
+ # Check for a newer version than local copy using GitHub release tag
34
+ #
35
+ # @return false if no new version, or semantic version of latest release
36
+ def new_version?
37
+ headers = {
38
+ 'Accept' => 'application/vnd.github+json',
39
+ 'X-GitHub-Api-Version' => '2022-11-28'
40
+ }
41
+ headers['Authorization'] = "Bearer #{Secrets::GH_AUTH_TOKEN}" if defined? Secrets::GH_AUTH_TOKEN
42
+
43
+ url = 'https://api.github.com/repos/ttscoff/searchlink/releases/latest'
44
+ page = Curl::Json.new(url, headers: headers)
45
+ result = page.json
46
+
47
+ if result
48
+ latest_tag = result['tag_name']
49
+
50
+ return false unless latest_tag
51
+
52
+ return false if latest_tag =~ /^#{Regexp.escape(SL::VERSION)}$/
53
+
54
+ latest = SemVer.new(latest_tag)
55
+ current = SemVer.new(SL::VERSION)
56
+
57
+ return latest_tag if current.older_than(latest)
58
+ else
59
+ warn 'Check for new version failed.'
60
+ end
61
+
62
+ false
63
+ end
64
+
65
+ def update_searchlink
66
+ new_version = SL.new_version?
67
+ if new_version
68
+ folder = File.expand_path('~/Downloads')
69
+ services = File.expand_path('~/Library/Services')
70
+ dl = File.join(folder, 'SearchLink.zip')
71
+ curl = TTY::Which.which('curl')
72
+ `#{curl} -SsL -o "#{dl}" https://github.com/ttscoff/searchlink/releases/latest/download/SearchLink.zip`
73
+ Dir.chdir(folder)
74
+ `unzip -qo #{dl} -d #{folder}`
75
+ FileUtils.rm(dl)
76
+
77
+ ['SearchLink', 'SearchLink File', 'Jump to SearchLink Error'].each do |workflow|
78
+ wflow = "#{workflow}.workflow"
79
+ src = File.join(folder, 'SearchLink Services', wflow)
80
+ dest = File.join(services, wflow)
81
+ if File.exist?(src) && File.exist?(dest)
82
+ FileUtils.rm_rf(dest)
83
+ FileUtils.mv(src, dest, force: true)
84
+ end
85
+ end
86
+ add_output("Installed SearchLink #{new_version}")
87
+ FileUtils.rm_rf('SearchLink Services')
88
+ else
89
+ add_output('Already up to date.')
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ # From <https://github.com/piotrmurach/tty-which/tree/master>
4
+ # [MIT License](https://github.com/piotrmurach/tty-which/blob/master/LICENSE.txt)
5
+
6
+ module TTY
7
+ module Which
8
+ VERSION = "0.5.0"
9
+ end # Which
10
+ end # TTY
11
+
12
+ module TTY
13
+ # A class responsible for finding an executable in the PATH
14
+ module Which
15
+ # Find an executable in a platform independent way
16
+ #
17
+ # @param [String] cmd
18
+ # the command to search for
19
+ # @param [Array<String>] paths
20
+ # the paths to look through
21
+ #
22
+ # @example
23
+ # which("ruby") # => "/usr/local/bin/ruby"
24
+ # which("/usr/local/bin/ruby") # => "/usr/local/bin/ruby"
25
+ # which("foo") # => nil
26
+ #
27
+ # @example
28
+ # which("ruby", paths: ["/usr/locale/bin", "/usr/bin", "/bin"])
29
+ #
30
+ # @return [String, nil]
31
+ # the absolute path to executable if found, `nil` otherwise
32
+ #
33
+ # @api public
34
+ def which(cmd, paths: search_paths)
35
+ if file_with_path?(cmd)
36
+ return cmd if executable_file?(cmd)
37
+
38
+ extensions.each do |ext|
39
+ exe = "#{cmd}#{ext}"
40
+ return ::File.absolute_path(exe) if executable_file?(exe)
41
+ end
42
+ return nil
43
+ end
44
+
45
+ paths.each do |path|
46
+ if file_with_exec_ext?(cmd)
47
+ exe = ::File.join(path, cmd)
48
+ return ::File.absolute_path(exe) if executable_file?(exe)
49
+ end
50
+ extensions.each do |ext|
51
+ exe = ::File.join(path, "#{cmd}#{ext}")
52
+ return ::File.absolute_path(exe) if executable_file?(exe)
53
+ end
54
+ end
55
+ nil
56
+ end
57
+ module_function :which
58
+
59
+ # Check if executable exists in the path
60
+ #
61
+ # @param [String] cmd
62
+ # the executable to check
63
+ #
64
+ # @param [Array<String>] paths
65
+ # paths to check
66
+ #
67
+ # @return [Boolean]
68
+ #
69
+ # @api public
70
+ def exist?(cmd, paths: search_paths)
71
+ !which(cmd, paths: paths).nil?
72
+ end
73
+ module_function :exist?
74
+
75
+ # Find default system paths
76
+ #
77
+ # @param [String] path
78
+ # the path to search through
79
+ #
80
+ # @example
81
+ # search_paths("/usr/local/bin:/bin")
82
+ # # => ["/bin"]
83
+ #
84
+ # @return [Array<String>]
85
+ # the array of paths to search
86
+ #
87
+ # @api private
88
+ def search_paths(path = ENV["PATH"])
89
+ paths = if path && !path.empty?
90
+ path.split(::File::PATH_SEPARATOR)
91
+ else
92
+ %w[/usr/local/bin /usr/ucb /usr/bin /bin /opt/homebrew/bin]
93
+ end
94
+ paths.select(&Dir.method(:exist?))
95
+ end
96
+ module_function :search_paths
97
+
98
+ # All possible file extensions
99
+ #
100
+ # @example
101
+ # extensions(".exe;cmd;.bat")
102
+ # # => [".exe", ".bat"]
103
+ #
104
+ # @param [String] path_ext
105
+ # a string of semicolon separated filename extensions
106
+ #
107
+ # @return [Array<String>]
108
+ # an array with valid file extensions
109
+ #
110
+ # @api private
111
+ def extensions(path_ext = ENV["PATHEXT"])
112
+ return [""] unless path_ext
113
+
114
+ path_ext.split(::File::PATH_SEPARATOR).select { |part| part.include?(".") }
115
+ end
116
+ module_function :extensions
117
+
118
+ # Determines if filename is an executable file
119
+ #
120
+ # @example Basic usage
121
+ # executable_file?("/usr/bin/less") # => true
122
+ #
123
+ # @example Executable in directory
124
+ # executable_file?("less", "/usr/bin") # => true
125
+ # executable_file?("less", "/usr") # => false
126
+ #
127
+ # @param [String] filename
128
+ # the path to file
129
+ # @param [String] dir
130
+ # the directory within which to search for filename
131
+ #
132
+ # @return [Boolean]
133
+ #
134
+ # @api private
135
+ def executable_file?(filename, dir = nil)
136
+ path = ::File.join(dir, filename) if dir
137
+ path ||= filename
138
+ ::File.file?(path) && ::File.executable?(path)
139
+ end
140
+ module_function :executable_file?
141
+
142
+ # Check if command itself has executable extension
143
+ #
144
+ # @param [String] filename
145
+ # the path to executable file
146
+ #
147
+ # @example
148
+ # file_with_exec_ext?("file.bat")
149
+ # # => true
150
+ #
151
+ # @return [Boolean]
152
+ #
153
+ # @api private
154
+ def file_with_exec_ext?(filename)
155
+ extension = ::File.extname(filename)
156
+ return false if extension.empty?
157
+
158
+ extensions.any? { |ext| extension.casecmp(ext).zero? }
159
+ end
160
+ module_function :file_with_exec_ext?
161
+
162
+ # Check if executable file is part of absolute/relative path
163
+ #
164
+ # @param [String] cmd
165
+ # the executable to check
166
+ #
167
+ # @return [Boolean]
168
+ #
169
+ # @api private
170
+ def file_with_path?(cmd)
171
+ ::File.expand_path(cmd) == cmd
172
+ end
173
+ module_function :file_with_path?
174
+ end # Which
175
+ end # TTY
data/lib/searchlink.rb ADDED
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SearchLink by Brett Terpstra 2015 <http://brettterpstra.com/projects/searchlink/>
4
+ # MIT License, please maintain attribution
5
+ require 'net/https'
6
+ require 'uri'
7
+ require 'rexml/document'
8
+ require 'shellwords'
9
+ require 'yaml'
10
+ require 'cgi'
11
+ require 'fileutils'
12
+ require 'tempfile'
13
+ require 'zlib'
14
+ require 'time'
15
+ require 'json'
16
+ require 'erb'
17
+
18
+ # import
19
+ require 'tokens' if File.exist?('lib/tokens.rb')
20
+
21
+ # import
22
+ require 'searchlink/util'
23
+
24
+ # import
25
+ require 'searchlink/curl'
26
+
27
+ # import
28
+ require 'searchlink/semver'
29
+
30
+ # import
31
+ require 'searchlink/version'
32
+
33
+ # import
34
+ require 'searchlink/array'
35
+
36
+ # import
37
+ require 'searchlink/string'
38
+
39
+ # import
40
+ require 'searchlink/plist'
41
+
42
+ # import
43
+ require 'searchlink/config'
44
+
45
+ # import
46
+ require 'searchlink/searches'
47
+
48
+ # import
49
+ require 'searchlink/url'
50
+
51
+ # import
52
+ require 'searchlink/search'
53
+
54
+ # import
55
+ require 'searchlink/help'
56
+
57
+ # import
58
+ require 'searchlink/parse'
59
+
60
+ # import
61
+ require 'searchlink/output'
62
+
63
+ # import
64
+ require 'searchlink/which'
65
+
66
+ module Secrets; end
data/lib/tokens.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Secrets
2
+ GH_AUTH_TOKEN = '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'
3
+ end