diff --git a/app/helpers/character_parser.rb b/app/helpers/character_parser.rb new file mode 100644 index 0000000..a3c48ad --- /dev/null +++ b/app/helpers/character_parser.rb @@ -0,0 +1,252 @@ +# frozen_string_literal: true + +require 'pry' + +# CharacterParser parses character data from gbf.wiki +class CharacterParser + attr_reader :granblue_id + + def initialize(granblue_id: String, debug: false) + @character = Character.find_by(granblue_id: granblue_id) + @wiki = GranblueWiki.new + @debug = debug || false + end + + # Fetches using @wiki and then processes the response + # Returns true if successful, false if not + # Raises an exception if something went wrong + def fetch(save: false) + response = fetch_wiki_info + return false if response.nil? + + redirect = handle_redirected_string(response) + return fetch(save: save) unless redirect.nil? + + handle_fetch_success(response, save) + end + + private + + # Determines whether or not the response is a redirect + # If it is, it will update the character's wiki_en value + def handle_redirected_string(response) + redirect = extract_redirected_string(response) + return unless redirect + + @character.wiki_en = redirect + if @character.save! + ap "Saved new wiki_en value for #{@granblue_id}: #{redirect}" if @debug + redirect + else + ap "Unable to save new wiki_en value for #{@granblue_id}: #{redirect}" if @debug + nil + end + end + + # Handle the response from the wiki if the response is successful + # If the save flag is set, it will persist the data to the database + def handle_fetch_success(response, save) + ap "#{@granblue_id}: Successfully fetched info for #{@character.wiki_en}" if @debug + extracted = parse_string(response) + info = parse(extracted) + persist(info) if save + true + end + + # Determines whether the response string + # should be treated as a redirect + def extract_redirected_string(string) + string.match(/#REDIRECT \[\[(.*?)\]\]/)&.captures&.first + end + + # Parses the response string into a hash + def parse_string(string) + lines = string.split("\n") + data = {} + stop_loop = false + + lines.each do |line| + next if stop_loop + + if line.include?('Gameplay Notes') + stop_loop = true + next + end + + next unless line[0] == '|' && line.size > 2 + + key, value = line[1..].split('=', 2).map(&:strip) + data[key] = value if value + end + + data + end + + # Fetches data from the GranblueWiki object + def fetch_wiki_info + @wiki.fetch(@character.wiki_en) + rescue WikiError => e + ap "There was an error fetching #{e.page}: #{e.message}" if @debug + nil + end + + # Iterates over all characters in the database and fetches their data + # If the save flag is set, data is saved to the database + # If the overwrite flag is set, data is fetched even if it already exists + # If the debug flag is set, additional information is printed to the console + def self.fetch_all(save: false, overwrite: false, debug: false) + errors = [] + + count = Character.count + Character.all.each_with_index do |c, i| + percentage = ((i + 1) / count.to_f * 100).round(2) + ap "#{percentage}%: Fetching #{c.name_en}... (#{i + 1}/#{count})" if debug + next unless c.release_date.nil? || overwrite + + begin + CharacterParser.new(granblue_id: c.granblue_id, + debug: debug).fetch(save: save) + rescue WikiError => e + errors.push(e.page) + end + end + + ap 'The following pages were unable to be fetched:' + ap errors + end + + # Parses the hash into a format that can be saved to the database + def parse(hash) + info = {} + + info[:name] = { en: hash['name'], ja: hash['jpname'] } + info[:id] = hash['id'] + info[:charid] = hash['charid'].scan(/\b\d{4}\b/) + + info[:flb] = GranblueWiki.boolean.fetch(hash['5star'], false) + info[:ulb] = hash['max_evo'].to_i == 6 + + info[:rarity] = GranblueWiki.rarities.fetch(hash['rarity'], 0) + info[:element] = GranblueWiki.elements.fetch(hash['element'], 0) + info[:gender] = GranblueWiki.genders.fetch(hash['gender'], 0) + + info[:proficiencies] = proficiencies_from_hash(hash['weapon']) + info[:races] = races_from_hash(hash['race']) + + info[:hp] = { + min_hp: hash['min_hp'].to_i, + max_hp: hash['max_hp'].to_i, + max_hp_flb: hash['flb_hp'].to_i + } + + info[:atk] = { + min_atk: hash['min_atk'].to_i, + max_atk: hash['max_atk'].to_i, + max_atk_flb: hash['flb_atk'].to_i + } + + info[:dates] = { + release_date: parse_date(hash['release_date']), + flb_date: parse_date(hash['5star_date']), + ulb_date: parse_date(hash['6star_date']) + } + + info[:links] = { + wiki: { en: hash['name'], ja: hash['link_jpwiki'] }, + gamewith: hash['link_gamewith'], + kamigame: hash['link_kamigame'] + } + + info.compact + end + + # Saves select fields to the database + def persist(hash) + @character.release_date = hash[:dates][:release_date] + @character.flb_date = hash[:dates][:flb_date] if hash[:dates].key?(:flb_date) + @character.ulb_date = hash[:dates][:ulb_date] if hash[:dates].key?(:ulb_date) + + @character.wiki_ja = hash[:links][:wiki][:ja] if hash[:links].key?(:wiki) && hash[:links][:wiki].key?(:ja) + @character.gamewith = hash[:links][:gamewith] if hash[:links].key?(:gamewith) + @character.kamigame = hash[:links][:kamigame] if hash[:links].key?(:kamigame) + + if @character.save + ap "#{@character.granblue_id}: Successfully saved info for #{@character.name_en}" if @debug + puts + true + end + + false + end + + # Converts proficiencies from a string to a hash + def proficiencies_from_hash(weapon) + weapon.to_s.split(',').map.with_index do |prof, i| + { "proficiency#{i + 1}" => GranblueWiki.proficiencies[prof] } + end.reduce({}, :merge) + end + + # Converts races from a string to a hash + def races_from_hash(race) + race.to_s.split(',').map.with_index do |r, i| + { "race#{i + 1}" => GranblueWiki.races[r] } + end.reduce({}, :merge) + end + + # Parses a date string into a Date object + def parse_date(date_str) + Date.parse(date_str) unless date_str.blank? + end + + # Unused methods for now + def extract_abilities(hash) + abilities = [] + hash.each do |key, value| + next unless key =~ /^a(\d+)_/ + + ability_number = Regexp.last_match(1).to_i + abilities[ability_number] ||= {} + + case key.gsub(/^a\d+_/, '') + when 'cd' + cooldown = parse_substring(value) + abilities[ability_number]['cooldown'] = cooldown + when 'dur' + duration = parse_substring(value) + abilities[ability_number]['duration'] = duration + when 'oblevel' + obtained = parse_substring(value) + abilities[ability_number]['obtained'] = obtained + else + abilities[ability_number][key.gsub(/^a\d+_/, '')] = value + end + end + + { 'abilities' => abilities.compact } + end + + def parse_substring(string) + hash = {} + + string.scan(/\|([^|=]+?)=([^|]+)/) do |key, value| + value.gsub!(/\}\}$/, '') if value.include?('}}') + hash[key] = value + end + + hash + end + + def extract_ougis(hash) + ougi = [] + hash.each do |key, value| + next unless key =~ /^ougi(\d*)_(.*)/ + + ougi_number = Regexp.last_match(1) + ougi_key = Regexp.last_match(2) + ougi[ougi_number.to_i] ||= {} + ougi[ougi_number.to_i][ougi_key] = value + end + + { 'ougis' => ougi.compact } + end +end