module WinehqSearch
Constants
- BASE_URL
- DESCRIPTION_LIMIT
- ENTRY_ID_PATTERN
- MAX_LIST_RESULTS
- MAX_VERSION_LINES
- QUERY_PATTERN
- RATINGS
- RATING_HINTS
- RATING_ORDER
- STYLE_TITLE
- USER_AGENT
Public Instance Methods
Source
# File lib/winehq_search.rb, line 69 def appdb_link(app_id) application_url(app_id) end
Source
# File lib/winehq_search.rb, line 65 def application_url(app_id) "#{BASE_URL}/objectManager.php?sClass=application&iId=#{app_id.to_i}" end
Source
# File lib/winehq_search.rb, line 163 def best_rating(versions) versions.min_by { |entry| RATING_ORDER.fetch(entry[:rating], RATINGS.length) } end
Source
# File lib/winehq_search.rb, line 321 def default_fetch lambda do |url| RabbotHttp.fetch(url, user_agent: USER_AGENT) end end
Source
# File lib/winehq_search.rb, line 146 def empty_detail(app_id) { name: nil, app_id: app_id, description: nil, versions: [] } end
Source
# File lib/winehq_search.rb, line 150 def extract_app_id(href) match = href.to_s.match(/[?&;]iId=(\d+)/) match&.[](1)&.to_i end
Source
# File lib/winehq_search.rb, line 284 def fetch_application(app_id, fetch:) html = fetch.call(application_url(app_id)) detail = parse_application_detail(html, app_id: app_id) detail[:app_id] = app_id detail end
Source
# File lib/winehq_search.rb, line 199 def format_detail_reply(detail) app_id = detail[:app_id] title = app_id ? "#{detail[:name]} ##{app_id}" : detail[:name].to_s lines = [IrcFormat.report_header(tag: "WINE", title: title, style: STYLE_TITLE)] versions = detail[:versions] || [] if versions.empty? lines << "No compatibility ratings listed yet on AppDB." else spotlight = versions.last best = best_rating(versions) lines << "Latest โ #{format_version_line(spotlight)}" if best && best != spotlight lines << "Best rated โ #{format_version_line(best)}" end extra = versions.length - MAX_VERSION_LINES lines << "+#{extra} more version#{'s' if extra != 1} on AppDB" if extra.positive? hint_rating = best&.dig(:rating) || spotlight[:rating] lines << "Rating โ #{hint_rating} (#{rating_hint(hint_rating)})" end lines << detail[:description] unless detail[:description].to_s.strip.empty? lines << run_instructions(versions.last&.dig(:rating)) footer_bits = ["appdb.winehq.org"] footer_bits << "community test data" if versions.any? lines << IrcFormat.report_footer(*footer_bits) lines.join("\n") end
Source
# File lib/winehq_search.rb, line 276 def format_fetch_error [ IrcFormat.report_header(tag: "WINE", title: "lookup failed", style: STYLE_TITLE), "Could not reach WineHQ AppDB โ try again shortly.", IrcFormat.report_footer("winehq.org") ].join("\n") end
Source
# File lib/winehq_search.rb, line 268 def format_invalid_query [ IrcFormat.report_header(tag: "WINE", title: "invalid query", style: STYLE_TITLE), "Use letters, numbers, spaces, and simple punctuation only.", IrcFormat.report_footer(usage_message) ].join("\n") end
Source
# File lib/winehq_search.rb, line 171 def format_list_line(index, entry) "#{index}. #{entry[:name]} ##{entry[:app_id]} โ #{entry[:description]}" end
Source
# File lib/winehq_search.rb, line 260 def format_no_results(query) [ IrcFormat.report_header(tag: "WINE", title: query.to_s.strip, style: STYLE_TITLE), "No AppDB matches for that name โ try a shorter keyword or browse appdb.winehq.org", IrcFormat.report_footer("winehq.org") ].join("\n") end
Source
# File lib/winehq_search.rb, line 228 def format_search_reply(query, results, detail: nil) count = results.length title = "search โ #{query} ยท #{count} match#{'es' if count != 1}" lines = [IrcFormat.report_header(tag: "WINE", title: title, style: STYLE_TITLE)] results.first(MAX_LIST_RESULTS).each_with_index do |entry, index| lines << format_list_line(index + 1, entry) end lines << "... #{count - MAX_LIST_RESULTS} more on AppDB" if count > MAX_LIST_RESULTS if detail && !detail[:name].to_s.strip.empty? best = pick_best_match(results, query) label = best ? best[:name] : detail[:name] versions = detail[:versions] || [] if versions.any? spotlight = versions.last lines << "Top pick โ #{label}: #{spotlight[:rating]} on Wine #{spotlight[:wine_version]}" else lines << "Top pick โ #{label}: no ratings yet" end end if results.length > 1 alt = results[1] lines << "Tip: !wine #{alt[:app_id]} for #{alt[:name]}" else lines << "Tip: !wine <entry#> for a specific AppDB listing" end lines << IrcFormat.report_footer("winehq.org", "AppDB") lines.join("\n") end
Source
# File lib/winehq_search.rb, line 175 def format_version_line(version) parts = ["#{version[:version]} โ #{version[:rating]}"] parts << "Wine #{version[:wine_version]}" unless version[:wine_version].to_s.strip.empty? tests = version[:test_results].to_i parts << "#{tests} test#{'s' if tests != 1}" if tests.positive? parts.join(" ยท ") end
Source
# File lib/winehq_search.rb, line 291 def lookup(text, fetch: nil) parsed = parse_query(text) return usage_message if parsed[:query].nil? && parsed[:entry_id].nil? && !parsed[:invalid] return format_invalid_query if parsed[:invalid] http_fetch = fetch || default_fetch if parsed[:entry_id] detail = fetch_application(parsed[:entry_id], fetch: http_fetch) return format_no_results("##{parsed[:entry_id]}") if detail[:name].to_s.strip.empty? return format_detail_reply(detail) end search_html = http_fetch.call(search_url(parsed[:query])) results = parse_search_results(search_html) return format_no_results(parsed[:query]) if results.empty? best = pick_best_match(results, parsed[:query]) detail = fetch_application(best[:app_id], fetch: http_fetch) if results.length == 1 format_detail_reply(detail) else format_search_reply(parsed[:query], results, detail: detail) end rescue StandardError format_fetch_error end
Source
# File lib/winehq_search.rb, line 100 def parse_application_detail(html, app_id: nil) doc = Nokogiri.HTML(html.to_s) name = doc.at_css("h1.whq-app-title")&.text.to_s.strip return empty_detail(app_id) if name.empty? description_node = doc.at_css("div.col-xs-7") if description_node description_node.css(".whq-shot, .whq-shot-link, img").remove description = TextUtil.squish(description_node.text) description = description.sub(/\AView Screenshots\s*/i, "") description = truncate_text(description, DESCRIPTION_LIMIT) unless description.empty? else description = nil end versions = doc.css("table.whq-table tr").filter_map do |row| next if row.at_css("thead") cells = row.css("td") next if cells.length < 4 version = cells[0].text.to_s.strip rating_cell = cells[2] rating = rating_cell.text.to_s.strip rating = rating_cell["class"].to_s.split.first if rating.empty? next unless RATINGS.include?(rating) { version: version, rating: rating, wine_version: cells[3].text.to_s.strip, test_results: cells[4]&.text.to_s.strip.to_i, comments: cells[5]&.text.to_s.strip.to_i } end { name: name, app_id: app_id, description: description, versions: versions } rescue StandardError empty_detail(app_id) end
Source
# File lib/winehq_search.rb, line 35 def parse_query(text) stripped = text.to_s.strip return { query: nil, entry_id: nil, invalid: false } if stripped.empty? if stripped.match?(ENTRY_ID_PATTERN) return { query: nil, entry_id: stripped.to_i, invalid: false } end return { query: nil, entry_id: nil, invalid: true } unless stripped.match?(QUERY_PATTERN) { query: stripped, entry_id: nil, invalid: false } end
Source
# File lib/winehq_search.rb, line 73 def parse_search_results(html) doc = Nokogiri.HTML(html.to_s) table = doc.at_css("table.whq-table-full") return [] unless table table.css("tr").filter_map do |row| link = row.at_css("td a[href*='iId=']") next unless link app_id = extract_app_id(link["href"]) next unless app_id cells = row.css("td") name = link.text.to_s.strip description = cells[2]&.text.to_s.strip next if name.empty? { name: name, app_id: app_id, description: truncate_text(description, DESCRIPTION_LIMIT) } end rescue StandardError [] end
Source
# File lib/winehq_search.rb, line 155 def pick_best_match(results, query) return nil if results.empty? needle = query.to_s.strip.downcase exact = results.find { |entry| entry[:name].to_s.downcase == needle } exact || results.first end
Source
# File lib/winehq_search.rb, line 183 def rating_hint(rating) RATING_HINTS.fetch(rating.to_s, "see AppDB test notes") end
Source
# File lib/winehq_search.rb, line 187 def run_instructions(rating) base = "Install Wine, then run: wine /path/to/program.exe" case rating.to_s when "Platinum", "Gold" base when "Silver", "Bronze" "#{base} โ try winetricks for missing DLLs; read test notes on AppDB" else "#{base} โ may need a custom Wine build or workarounds; check AppDB comments" end end
Source
# File lib/winehq_search.rb, line 48 def search_url(query) params = { bIsQueue: "false", bIsRejected: "false", sClass: "application", sTitle: "application name", iItemsPerPage: "25", iPage: "1", "iappFamily-appNameOp0": "2", "sappFamily-appNameData0": query.to_s.strip, sOrderBy: "appName", bAscending: "true" } query_string = params.map { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join("&") "#{BASE_URL}/objectManager.php?#{query_string}" end
Source
# File lib/winehq_search.rb, line 167 def truncate_text(text, max_length) TextUtil.truncate(text, max_length) end
Source
# File lib/winehq_search.rb, line 31 def usage_message "Usage: !wine <Windows app name> or !wine <AppDB entry#> โ ratings from WineHQ AppDB" end