diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..5857937 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,26 @@ +AllCops: + TargetRubyVersion: 2.4.1 + UseCache: true + Exclude: + - vendor/**/* + - db/schema.rb + +Rails: + Enabled: true + +Metrics/LineLength: + Max: 100 +Metrics/MethodLength: + Max: 20 +Metrics/AbcSize: + Max: 20 + +Style/TrailingCommaInLiteral: + EnforcedStyleForMultiline: comma + +Style/FrozenStringLiteralComment: + Enabled: false +Style/Documentation: + Enabled: false +Style/AmbiguousBlockAssociation: + Enabled: false diff --git a/Gemfile b/Gemfile index 541d521..75fb0dd 100644 --- a/Gemfile +++ b/Gemfile @@ -12,15 +12,15 @@ gem 'countries' gem 'httparty' gem 'money-rails' gem 'slim-rails' -# gem 'sass-rails', '~> 5.0' group :development, :test do - gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] + gem 'byebug', platforms: %i[mri mingw x64_mingw] gem 'jazz_fingers' end group :development do gem 'listen', '>= 3.0.5', '< 3.2' + gem 'rubocop', require: false gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' end diff --git a/Gemfile.lock b/Gemfile.lock index 73e5a54..6c03857 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -39,6 +39,7 @@ GEM minitest (~> 5.1) tzinfo (~> 1.1) arel (8.0.0) + ast (2.3.0) awesome_print (1.7.0) bugsnag (5.3.2) builder (3.2.3) @@ -95,7 +96,10 @@ GEM nio4r (2.0.0) nokogiri (1.7.2) mini_portile2 (~> 2.1.0) + parser (2.4.0.0) + ast (~> 2.2) pg (0.20.0) + powerpack (0.1.1) pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) @@ -135,11 +139,20 @@ GEM method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) + rainbow (2.2.2) + rake rake (12.0.0) rb-fsevent (0.9.8) rb-inotify (0.9.8) ffi (>= 0.5.0) redis (3.3.3) + rubocop (0.48.1) + parser (>= 2.3.3.1, < 3.0) + powerpack (~> 0.1) + rainbow (>= 1.99.1, < 3.0) + ruby-progressbar (~> 1.7) + unicode-display_width (~> 1.0, >= 1.0.1) + ruby-progressbar (1.8.1) ruby_dep (1.5.0) sixarm_ruby_unaccent (1.1.1) slim (3.0.8) @@ -168,6 +181,7 @@ GEM tilt (2.0.7) tzinfo (1.2.3) thread_safe (~> 0.1) + unicode-display_width (1.2.1) unicode_utils (1.4.0) websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) @@ -189,6 +203,7 @@ DEPENDENCIES puma (~> 3.7) rails (~> 5.1.1) redis (~> 3.2) + rubocop slim-rails spring spring-watcher-listen (~> 2.0.0) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4f531a2..7b0f42e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,61 +1,18 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception - def prices - games = Game.includes(:prices).by_game_code - countries = Price.distinct.pluck(:country).sort - currency = Price.pluck(:currency).include?(params[:currency]) ? params[:currency] : nil - - csv = CSV.generate(headers: true) do |rows| - rows << %w(Title).concat(countries) - games.each do |code, games| - game = games.first - prices = games.flat_map(&:prices) - row = [] - row << game.title - countries.each do |country| - price = prices.detect { |p| p.country === country } - row << (price ? (currency ? price.value.exchange_to(currency) : price.value) : 'NA') - end - # csv << attributes.map{ |attr| user.send(attr) } - rows << row - end - end - - send_data csv, type: 'text/csv; charset=utf-8; header=present' - end - def best_deals - games = Game.includes(:prices).by_game_code - currencies = Price.distinct.pluck(:currency).sort + all_games = Game.includes(:prices).by_game_code + currencies = Price.distinct.pluck(:currency).sort csv = CSV.generate(headers: true) do |rows| - rows << ['Title', 'Country'].concat(currencies) + rows << %w[Title Country].concat(currencies) - games.each do |code, games| - game = games.first + all_games.each do |_code, games| lowest = games.flat_map(&:prices).min_by { |price| price.value.exchange_to('USD') } - next if !lowest.present? + next if lowest.blank? rates = currencies.map { |c| lowest.value.exchange_to(c) } - rows << [game.title, ISO3166::Country[lowest.country].translation('en')].concat(rates) - end - end - - send_data csv, type: 'text/csv; charset=utf-8; header=present' - end - - def rates - currencies = Price.distinct.pluck(:currency).sort - - csv = CSV.generate(headers: true) do |rows| - rows << [''].concat(currencies) - - currencies.each do |from| - row = [from] - currencies.each do |to| - row << Money.default_bank.get_rate(from, to) - end - rows << row + rows << [game.first.title, ISO3166::Country[lowest.country].translation('en')].concat(rates) end end @@ -83,7 +40,6 @@ def glossary currency = ISO3166::Country.find_country_by_currency(code).currency rows << [code, currency.name] end - end send_data csv, type: 'text/csv; charset=utf-8; header=present' diff --git a/app/controllers/prices_controller.rb b/app/controllers/prices_controller.rb new file mode 100644 index 0000000..b82d6a9 --- /dev/null +++ b/app/controllers/prices_controller.rb @@ -0,0 +1,31 @@ +class PricesController < ApplicationController + def index + @current_page = 'prices' + @games = Game.includes(:prices).by_game_code + @countries = Price.distinct.pluck(:country).sort + @currencies = Price.distinct.pluck(:currency).sort + @currency = @currencies.include?(params[:currency]) ? params[:currency] : nil + + respond_to do |format| + format.html + format.csv { send_data(render_csv, type: 'text/csv; charset=utf-8; header=present') } + end + end + + private + + def render_csv + CSV.generate(headers: true) do |csv| + csv << %w[Title].concat(@countries) + + @games.each do |_code, games| + prices = games.flat_map(&:prices) + csv << [games.first.title] + @countries.map do |country| + price = prices.detect { |p| p.country == country } + next 'NA' unless price + @currency ? price.value.exchange_to(@currency) : price.value + end + end + end + end +end diff --git a/app/models/game.rb b/app/models/game.rb index 4ae5f46..a7f9218 100644 --- a/app/models/game.rb +++ b/app/models/game.rb @@ -1,7 +1,7 @@ class Game < ApplicationRecord - REGIONS = %w(europe americas asia).freeze - GAME_CODE_FORMAT = /\A[0-9A-Z]{4}\z/.freeze - NSUID_CODE_FORMAT = /\A7001000000[0-9]{4}\z/.freeze + REGIONS = %w[europe americas asia].freeze + GAME_CODE_FORMAT = /\A[0-9A-Z]{4}\z/ + NSUID_CODE_FORMAT = /\A7001000000[0-9]{4}\z/ attribute :game_code, :string attribute :region, :string @@ -17,11 +17,9 @@ class Game < ApplicationRecord has_many :prices - scope :order_by_title, -> { - order('LOWER(title COLLATE "C")') - } + scope :order_by_title, -> { order('LOWER(title COLLATE "C")') } - scope :order_by_region, -> { + scope :order_by_region, lambda { order_by = ['case'] REGIONS.each_with_index do |region, index| order_by << "WHEN games.region='#{region}' THEN #{index}" @@ -30,6 +28,11 @@ class Game < ApplicationRecord order(order_by.join(' ')) } - scope :by_game_code, -> { order_by_title.group_by(&:game_code).each { |_,games| games.sort_by! { |game| REGIONS.index(game.region) } } } - scope :by_region, -> { order_by_title.order_by_region.group_by(&:region) } + scope :by_game_code, lambda { + order_by_title.group_by(&:game_code).each do |_, games| + games.sort_by! { |game| REGIONS.index(game.region) } + end + } + + scope :by_region, -> { order_by_title.order_by_region.group_by(&:region) } end diff --git a/app/views/games/index.slim b/app/views/games/index.slim index 13bbf78..d24c380 100644 --- a/app/views/games/index.slim +++ b/app/views/games/index.slim @@ -17,7 +17,7 @@ table.table.table-responsive = cover ? image_tag(cover, style: 'max-width:120px;max-height:120px;', class: 'rounded mx-auto d-block', title: 'wat') : nil td.game--code rowspan=games.count = game.game_code td.game--region = game.region - td.game--title.text-overflow style="width:100%;max-width:0" = game.title + td.game--title.text-overflow style="width:100%;max-width:0" title=games.first.title = game.title td.game--nsuid = game.nsuid || 'NA' td.game--release-date time.text-overflow datetime=game.release_date.iso8601 = game.release_date.strftime("%b #{game.release_date.day.ordinalize}, %Y") diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 5defc37..57558a6 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -16,6 +16,9 @@ html overflow: hidden; text-overflow: ellipsis; } + .text-muted { + opacity: .2; + } body nav.navbar.navbar-toggleable-sm.navbar-light.bg-faded.sticky-top style="width: 100%" @@ -28,8 +31,10 @@ html #navbarSupportedContent.navbar-collapse.collapse ul.navbar-nav.mr-auto li.nav-item class=(@current_page == 'games' ? 'active' : nil) - = link_to 'Game list', '#', class: 'nav-link' - li.nav-item - = link_to 'All prices', '#', class: 'nav-link disabled' + = link_to 'Game list', games_url, class: 'nav-link' + li.nav-item class=(@current_page == 'prices' ? 'active' : nil) + = link_to 'All prices', prices_url, class: 'nav-link' + + = content_for :navbar_right = yield diff --git a/bin/setup b/bin/setup index 78c4e86..ca842c1 100755 --- a/bin/setup +++ b/bin/setup @@ -21,7 +21,6 @@ chdir APP_ROOT do # Install JavaScript dependencies if using Yarn # system('bin/yarn') - # puts "\n== Copying sample files ==" # unless File.exist?('config/database.yml') # cp 'config/database.yml.sample', 'config/database.yml' diff --git a/bin/yarn b/bin/yarn index c2bacef..329aee9 100755 --- a/bin/yarn +++ b/bin/yarn @@ -2,10 +2,10 @@ VENDOR_PATH = File.expand_path('..', __dir__) Dir.chdir(VENDOR_PATH) do begin - exec "yarnpkg #{ARGV.join(" ")}" + exec "yarnpkg #{ARGV.join(' ')}" rescue Errno::ENOENT - $stderr.puts "Yarn executable was not detected in the system." - $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" + $stderr.puts 'Yarn executable was not detected in the system.' + $stderr.puts 'Download Yarn at https://yarnpkg.com/en/docs/install' exit 1 end end diff --git a/config/environments/development.rb b/config/environments/development.rb index 5187e22..30dc47c 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -13,12 +13,12 @@ config.consider_all_requests_local = true # Enable/disable caching. By default caching is disabled. - if Rails.root.join('tmp/caching-dev.txt').exist? + if Rails.root.join('tmp', 'caching-dev.txt').exist? config.action_controller.perform_caching = true config.cache_store = :memory_store config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}" + 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}", } else config.action_controller.perform_caching = false diff --git a/config/environments/production.rb b/config/environments/production.rb index 6bb6ce1..8f7777f 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -30,7 +30,8 @@ # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false - # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb + # `config.assets.precompile` and `config.assets.version` have moved to + # config/initializers/assets.rb # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = 'http://assets.example.com' @@ -52,7 +53,7 @@ config.log_level = :debug # Prepend all log lines with the following tags. - config.log_tags = [ :request_id ] + config.log_tags = [:request_id] # Use a different cache store in production. # config.cache_store = :mem_cache_store @@ -80,7 +81,7 @@ # require 'syslog/logger' # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') - if ENV["RAILS_LOG_TO_STDOUT"].present? + if ENV['RAILS_LOG_TO_STDOUT'].present? logger = ActiveSupport::Logger.new(STDOUT) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) diff --git a/config/environments/test.rb b/config/environments/test.rb index 8e5cbde..befcc06 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -15,7 +15,7 @@ # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}" + 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}", } # Show full error reports and disable caching. diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb index 59385cd..78c4f58 100644 --- a/config/initializers/backtrace_silencers.rb +++ b/config/initializers/backtrace_silencers.rb @@ -1,7 +1,9 @@ # Be sure to restart your server when you modify this file. -# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# You can add backtrace silencers for libraries that you're using but don't wish to see in your +# backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } -# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# You can also remove all the silencers if you're trying to debug a problem that might stem from +# framework code. # Rails.backtrace_cleaner.remove_silencers! diff --git a/config/initializers/bugsnag.rb b/config/initializers/bugsnag.rb index 149cc43..5132167 100644 --- a/config/initializers/bugsnag.rb +++ b/config/initializers/bugsnag.rb @@ -1,3 +1,3 @@ Bugsnag.configure do |config| - config.api_key = ENV.fetch('BUGSNAG_API_KEY', '') + config.api_key = ENV.fetch('BUGSNAG_API_KEY') { '' } end diff --git a/config/initializers/money.rb b/config/initializers/money.rb index bd291dd..664303c 100644 --- a/config/initializers/money.rb +++ b/config/initializers/money.rb @@ -1,4 +1,4 @@ MoneyRails.configure do |config| - REDIS_URL = ENV.fetch('REDIS_URL', 'redis://localhost:6379') + REDIS_URL = ENV.fetch('REDIS_URL') { 'redis://localhost:6379' } config.default_bank = Money::Bank::VariableExchange.new(ExchangeRate.new(url: REDIS_URL)) end diff --git a/config/puma.rb b/config/puma.rb index 1e19380..ccda173 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -4,16 +4,16 @@ # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. # -threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 } threads threads_count, threads_count # Specifies the `port` that Puma will listen on to receive requests; default is 3000. # -port ENV.fetch("PORT") { 3000 } +port ENV.fetch('PORT') { 3000 } # Specifies the `environment` that Puma will run in. # -environment ENV.fetch("RAILS_ENV") { "development" } +environment ENV.fetch('RAILS_ENV') { 'development' } # Specifies the number of `workers` to boot in clustered mode. # Workers are forked webserver processes. If using threads and workers together diff --git a/config/routes.rb b/config/routes.rb index 0912115..1ec1538 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,11 +1,11 @@ Rails.application.routes.draw do # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html - get :prices, to: 'application#prices' get :best_deals, to: 'application#best_deals' get :rates, to: 'application#rates' get :glossary, to: 'application#glossary' get :about, to: 'application#about' resources :games, only: :index + resources :prices, only: :index end diff --git a/config/spring.rb b/config/spring.rb index c9119b4..9fa7863 100644 --- a/config/spring.rb +++ b/config/spring.rb @@ -1,6 +1,6 @@ -%w( +%w[ .ruby-version .rbenv-vars tmp/restart.txt tmp/caching-dev.txt -).each { |path| Spring.watch(path) } +].each { |path| Spring.watch(path) } diff --git a/db/migrate/20170514194430_create_games.rb b/db/migrate/20170514194430_create_games.rb index d4b7d63..f7f6853 100644 --- a/db/migrate/20170514194430_create_games.rb +++ b/db/migrate/20170514194430_create_games.rb @@ -12,7 +12,7 @@ def change t.timestamps end - add_index :games, [:game_code, :nsuid], unique: true + add_index :games, %i[game_code nsuid], unique: true add_index :games, :parsed_game_code add_index :games, :nsuid end diff --git a/db/migrate/20170514195632_create_prices.rb b/db/migrate/20170514195632_create_prices.rb index e904a0b..a233c3c 100644 --- a/db/migrate/20170514195632_create_prices.rb +++ b/db/migrate/20170514195632_create_prices.rb @@ -11,6 +11,6 @@ def change t.timestamps end - add_index :prices, [:nsuid, :country], unique: true + add_index :prices, %i[nsuid country], unique: true end end diff --git a/db/migrate/20170515170820_drop_exchange_rates.rb b/db/migrate/20170515170820_drop_exchange_rates.rb index 0c235e5..0028668 100644 --- a/db/migrate/20170515170820_drop_exchange_rates.rb +++ b/db/migrate/20170515170820_drop_exchange_rates.rb @@ -1,5 +1,11 @@ class DropExchangeRates < ActiveRecord::Migration[5.1] def change - drop_table :exchange_rates + drop_table :exchange_rates do |t| + t.string :from, limit: 3 + t.string :to, limit: 3 + t.float :rate + + t.timestamps + end end end diff --git a/db/migrate/20170520131614_change_game_columns_name.rb b/db/migrate/20170520131614_change_game_columns_name.rb index 471f684..478d294 100644 --- a/db/migrate/20170520131614_change_game_columns_name.rb +++ b/db/migrate/20170520131614_change_game_columns_name.rb @@ -1,7 +1,7 @@ class ChangeGameColumnsName < ActiveRecord::Migration[5.1] def change # Undo previous indexes, we are gonna replace them - remove_index :games, column: [:game_code, :nsuid] + remove_index :games, column: %i[game_code nsuid] remove_index :games, column: :parsed_game_code # Rename actual columns @@ -10,6 +10,6 @@ def change # Rebuild better indexes add_index :games, :game_code - add_index :games, [:game_code, :region], unique: true + add_index :games, %i[game_code region], unique: true end end diff --git a/db/seeds.rb b/db/seeds.rb index 14d35c1..b976da2 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,5 +1,7 @@ -# This file should contain all the record creation needed to seed the database with its default values. -# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). +# This file should contain all the record creation needed to seed the database +# with its default values. +# The data can then be loaded with the rails db:seed command (or created +# alongside the database with db:setup). # # Examples: # @@ -10,5 +12,6 @@ GAME_CSV_FILE = Rails.root.join('db', 'seeds', 'games.csv') CSV.foreach(GAME_CSV_FILE, headers: true) do |row| - Game.find_or_create_by(region: row['region'], game_code: row['game_code']).update_attributes(row.to_hash) + Game.find_or_create_by(region: row['region'], game_code: row['game_code']) + .update_attributes(row.to_hash) end diff --git a/lib/eshop.rb b/lib/eshop.rb index bd090ca..d8942b0 100644 --- a/lib/eshop.rb +++ b/lib/eshop.rb @@ -1,137 +1,23 @@ require 'active_support/core_ext/array/grouping' require 'active_support/core_ext/hash/slice' require 'httparty' -require 'countries' -module Eshop +require_relative 'eshop/countries' +require_relative 'eshop/games' +require_relative 'eshop/prices' +module Eshop # Nintendo split countries this way. So yes, africa and oceania are in europe... REGIONS = { - asia: %w(AE AZ CY HK IN JP KR MY SA SG TR TW), - europe: %w(AD AL AT AU BA BE BG BW CH CZ DE DJ DK EE ER ES FI FR GB GG GI GR HR HU IE IM IS IT - JE LI LS LT LU LV MC ME MK ML MR MT MZ NA NE NL NO NZ PL PT RO RS RU SD SE SI SK SM SO SZ TD VA - ZA ZM ZW), - americas: %w(AG AI AR AW BB BM BO BR BS BZ CA CL CO CR DM DO EC GD GF GP GT GY HN HT JM KN KY LC - MQ MS MX NI PA PE PY SR SV TC TT US UY VC VE VG VI), + asia: %w[AE AZ CY HK IN JP KR MY SA SG TR TW], + europe: %w[ + AD AL AT AU BA BE BG BW CH CZ DE DJ DK EE ER ES FI FR GB GG GI GR HR HU IE IM IS IT JE LI LS + LT LU LV MC ME MK ML MR MT MZ NA NE NL NO NZ PL PT RO RS RU SD SE SI SK SM SO SZ TD VA ZA ZM + ZW + ], + americas: %w[ + AG AI AR AW BB BM BO BR BS BZ CA CL CO CR DM DO EC GD GF GP GT GY HN HT JM KN KY LC MQ MS MX + NI PA PE PY SR SV TC TT US UY VC VE VG VI + ], }.freeze - - class Countries - include HTTParty - - URL = 'https://api.ec.nintendo.com/v1/price'.freeze - DEFAULT_PARAMS = { - lang: 'en', - ids: '70010000000000' - } - - def self.list - ISO3166::Country.all.map(&:alpha2).map do |country| - response = get(URL, query: DEFAULT_PARAMS.merge(country: country)) - response.code == 200 ? country : nil - end.compact - end - end - - class Games - include HTTParty - - def self.list - list_americas + list_europe - end - - def self.list_europe - url = 'http://search.nintendo-europe.com/en/select' - default_params = { - fl: 'product_code_txt,title,date_from,nsuid_txt,image_url_sq_s', - fq: 'type:GAME AND (system_type:"nintendoswitch_gamecard" OR system_type:"nintendoswitch_downloadsoftware" OR system_type:"nintendoswitch_digitaldistribution") AND product_code_txt:*', - q: '*', - rows: 9999, - sort: 'sorting_title asc', - start: 0, - wt: 'json' - } - - response = get(url, query: default_params) - games = JSON.parse(response.body, symbolize_names: true)[:response][:docs] - - return games.map do |game| - { - region: 'europe', - game_code: game.dig(:product_code_txt, 0).match(/\AHAC\w?(\w{4})\w\Z/)[1], - raw_game_code: game.dig(:product_code_txt, 0), - title: game[:title], - release_date: Date.parse(game[:date_from]), - nsuid: game.dig(:nsuid_txt, 0), - cover_url: game[:image_url_sq_s], - } - end - end - - def self.list_asia - - end - - def self.list_americas - url = 'http://www.nintendo.com/json/content/get/filter/game' - default_params = { - limit: 40, - system: 'switch', - sort: 'title', - direction: 'asc', - shop: 'ncom', - }.freeze - - response = get(url, query: default_params.merge(limit: 1, offset: 99999)) - games = JSON.parse(response.body, symbolize_names: true)[:games][:game] - raise 'Nintendo fixed the pagination bug!' if games.count == 1 - - # Use this when Nintendo will fix the pagination bug - # offset = 0 - # games = [] - # - # loop do - # response = get(GAMES_URL, query: GAMES_DEFAULT_PARAMS.merge(offset: offset)) - # offset, new_games, limit = JSON.parse(response.body, symbolize_names: true)[:games].values_at(:offset, :game, :limit) - # games = games + new_games - # break if new_games.count != limit - # offset += limit - # end - return games.map do |game| - next if (game[:game_code] =~ /\AHAC\w?(\w{4})\w\Z/).nil? - { - region: 'americas', - game_code: game[:game_code].match(/\AHAC\w?(\w{4})\w\Z/)[1], - raw_game_code: game[:game_code], - title: game[:title], - release_date: Date.parse(game[:release_date]), - nsuid: game[:nsuid], - cover_url: game[:front_box_art], - } - end.compact - end - end - - class Prices - include HTTParty - - URL = 'https://api.ec.nintendo.com/v1/price'.freeze - DEFAULT_PARAMS = { - lang: 'en', - }.freeze - - def self.list(country: 'US', ids: [], limit: 50) - ids.in_groups_of(limit).flat_map do |ids_to_fetch| - response = get(URL, query: DEFAULT_PARAMS.merge(country: country, ids: ids_to_fetch.join(','))) - JSON.parse(response.body, symbolize_names: true)[:prices].select { |p| p.include? :regular_price } - end.map do |price| - { - nsuid: price[:title_id], - country: country, - status: price[:sales_status], - currency: price.dig(:regular_price, :currency), - value_in_cents: Money.from_amount(price.dig(:regular_price, :raw_value).to_f, price.dig(:regular_price, :currency)).cents, - } - end - end - end end diff --git a/lib/eshop/countries.rb b/lib/eshop/countries.rb new file mode 100644 index 0000000..43752fe --- /dev/null +++ b/lib/eshop/countries.rb @@ -0,0 +1,21 @@ +require 'httparty' +require 'countries' + +module Eshop + class Countries + include HTTParty + + URL = 'https://api.ec.nintendo.com/v1/price'.freeze + DEFAULT_PARAMS = { + lang: 'en', + ids: '70010000000000', + }.freeze + + def self.list + ISO3166::Country.all.map(&:alpha2).map do |country| + response = get(URL, query: DEFAULT_PARAMS.merge(country: country)) + response.code == 200 ? country : nil + end.compact + end + end +end diff --git a/lib/eshop/games.rb b/lib/eshop/games.rb new file mode 100644 index 0000000..b38bfe0 --- /dev/null +++ b/lib/eshop/games.rb @@ -0,0 +1,11 @@ +require 'httparty' +require_relative 'games/americas' +require_relative 'games/europe' + +module Eshop + class Games + def self.list + Americas.list + Europe.list + end + end +end diff --git a/lib/eshop/games/americas.rb b/lib/eshop/games/americas.rb new file mode 100644 index 0000000..251256c --- /dev/null +++ b/lib/eshop/games/americas.rb @@ -0,0 +1,41 @@ +require 'httparty' + +module Eshop + class Games + class Americas + include HTTParty + + URL = 'http://www.nintendo.com/json/content/get/filter/game'.freeze + DEFAULT_PARAMS = { + limit: 40, + system: 'switch', + sort: 'title', + direction: 'asc', + shop: 'ncom', + }.freeze + + def self.list + response = get(URL, query: DEFAULT_PARAMS.merge(limit: 1, offset: 99_999)) + games = JSON.parse(response.body, symbolize_names: true)[:games][:game] + raise 'Nintendo fixed the pagination bug!' if games.count == 1 + + games.map do |game| + next if (game[:game_code] =~ /\AHAC\w?(\w{4})\w\Z/).nil? + coerce(game) + end.compact + end + + def self.coerce(game) + { + region: 'americas', + game_code: game[:game_code].match(/\AHAC\w?(\w{4})\w\Z/)[1], + raw_game_code: game[:game_code], + title: game[:title], + release_date: Date.parse(game[:release_date]), + nsuid: game[:nsuid], + cover_url: game[:front_box_art], + } + end + end + end +end diff --git a/lib/eshop/games/asia.rb b/lib/eshop/games/asia.rb new file mode 100644 index 0000000..9398c66 --- /dev/null +++ b/lib/eshop/games/asia.rb @@ -0,0 +1,13 @@ +require 'httparty' + +module Eshop + class Games + class Asia + include HTTParty + + def self.list + # TODO + end + end + end +end diff --git a/lib/eshop/games/europe.rb b/lib/eshop/games/europe.rb new file mode 100644 index 0000000..a7e4007 --- /dev/null +++ b/lib/eshop/games/europe.rb @@ -0,0 +1,47 @@ +require 'httparty' + +module Eshop + class Games + class Europe + include HTTParty + + URL = 'http://search.nintendo-europe.com/en/select'.freeze + DEFAULT_PARAMS = { + fl: 'product_code_txt,title,date_from,nsuid_txt,image_url_sq_s', + fq: [ + 'type:GAME', + "(#{[ + 'system_type:"nintendoswitch_gamecard"', + 'system_type:"nintendoswitch_downloadsoftware"', + 'system_type:"nintendoswitch_digitaldistribution"', + ].join(' OR ')})", + 'product_code_txt:*', + ].join(' AND '), + q: '*', + rows: 9999, + sort: 'sorting_title asc', + start: 0, + wt: 'json', + }.freeze + + def self.list + response = get(URL, query: DEFAULT_PARAMS) + games = JSON.parse(response.body, symbolize_names: true)[:response][:docs] + + games.map { |game| coerce(game) } + end + + def self.coerce(game) + { + region: 'europe', + game_code: game.dig(:product_code_txt, 0).match(/\AHAC\w?(\w{4})\w\Z/)[1], + raw_game_code: game.dig(:product_code_txt, 0), + title: game[:title], + release_date: Date.parse(game[:date_from]), + nsuid: game.dig(:nsuid_txt, 0), + cover_url: game[:image_url_sq_s], + } + end + end + end +end diff --git a/lib/eshop/prices.rb b/lib/eshop/prices.rb new file mode 100644 index 0000000..fc6a2e7 --- /dev/null +++ b/lib/eshop/prices.rb @@ -0,0 +1,35 @@ +require 'active_support/core_ext/array/grouping' +require 'httparty' + +module Eshop + class Prices + include HTTParty + + URL = 'https://api.ec.nintendo.com/v1/price'.freeze + DEFAULT_PARAMS = { + lang: 'en', + }.freeze + + def self.list(country: 'US', ids: [], limit: 50) + prices = ids.in_groups_of(limit).flat_map do |ids_to_fetch| + query = DEFAULT_PARAMS.merge(country: country, ids: ids_to_fetch.join(',')) + response = get(URL, query: query) + JSON.parse(response.body, symbolize_names: true)[:prices] + end + prices.select! { |p| p.include? :regular_price } + prices.map { |price| coerce(price, country) } + end + + def self.coerce(price, country) + value = price.dig(:regular_price, :raw_value).to_f + currency = price.dig(:regular_price, :currency) + { + nsuid: price[:title_id], + country: country, + status: price[:sales_status], + currency: price.dig(:regular_price, :currency), + value_in_cents: Money.from_amount(value, currency).cents, + } + end + end +end diff --git a/lib/tasks/currencies.rake b/lib/tasks/currencies.rake index 7734eab..620a2ac 100644 --- a/lib/tasks/currencies.rake +++ b/lib/tasks/currencies.rake @@ -5,11 +5,11 @@ API_URL = 'http://api.fixer.io/latest'.freeze namespace :currencies do desc 'Get all currencies exchange rates' task retrieve_all: :environment do - currencies = Price.pluck(:currency).uniq.sort + currencies = Price.distinct.pluck(:currency).sort currencies.each do |from| puts "Retieving #{currencies.count} rates for #{from} currency..." - response = HTTParty.get(API_URL, query: { base: from, symbols: currencies.join(',')}) + response = HTTParty.get(API_URL, query: { base: from, symbols: currencies.join(',') }) rates = JSON.parse(response.body, symbolize_names: true)[:rates] rates.each do |to, rate| Money.add_rate(from, to, rate) diff --git a/lib/tasks/eshop.rake b/lib/tasks/eshop.rake index 83f5435..b757436 100644 --- a/lib/tasks/eshop.rake +++ b/lib/tasks/eshop.rake @@ -4,7 +4,8 @@ namespace :eshop do desc 'Get all games from eShop API' task retrieve_games: :environment do Eshop::Games.list.map do |raw_game| - Game.find_or_create_by!(region: raw_game[:region], game_code: raw_game[:game_code]).update_attributes!(raw_game) + Game.find_or_create_by!(region: raw_game[:region], game_code: raw_game[:game_code]) + .update_attributes!(raw_game) end end @@ -17,7 +18,8 @@ namespace :eshop do print " #{ISO3166::Country[country]}" Eshop::Prices.list(country: country, ids: ids).map do |price| price[:game] = Game.find_by(region: region, nsuid: price[:nsuid]) - Price.find_or_initialize_by(nsuid: price[:nsuid], country: country).update_attributes!(price) + Price.find_or_initialize_by(nsuid: price[:nsuid], country: country) + .update_attributes!(price) end print " OK\n" end @@ -25,6 +27,5 @@ namespace :eshop do end desc 'Get all prices from eShop API' - task retrieve_all: [:retrieve_games, :retrieve_prices] do - end + task retrieve_all: %i[retrieve_games retrieve_prices] end