searchlink 2.3.59

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