impress/lib/tasks/neopets/import/nc_mall.rake
Emi Matchu 3582229b47 Update NC Mall scraping for new redesign
First actual feature I'm letting Claude run! We worked the exploration of the updated API together, then it ran with the implementation.

I left this hanging for a long time.... good to finally have it updated!
2025-10-30 12:43:14 +00:00

102 lines
3.6 KiB
Ruby

namespace "neopets:import" do
desc "Sync our NCMallRecord table with the live NC Mall"
task :nc_mall => :environment do
# Log to STDOUT.
Rails.logger = Logger.new(STDOUT)
puts "Importing from NC Mall…"
# First, load all records of what's being sold in the live NC Mall. We load
# all categories from the menu and fetch all items from each. (We also
# de-duplicate the items, which is important because the same item can
# appear in multiple categories!)
live_item_records = load_all_nc_mall_items.uniq { |item| item[:id] }
# Then, get the existing NC Mall records in our database. (We include the
# items, to be able to output the item name during logging.)
existing_records = NCMallRecord.includes(:item).all
existing_records_by_item_id = existing_records.to_h { |r| [r.item_id, r] }
# Additionally, check which of the item IDs in the live records are items
# we've seen before. (We'll skip records for items we don't know.)
live_item_ids = live_item_records.map { |r| r[:id] }
recognized_item_ids = Item.where(id: live_item_ids).pluck(:id).to_set
Rails.logger.debug "We found #{live_item_records.size} items, and we " +
"recognize #{recognized_item_ids.size} of them."
# For each record in the live NC Mall, check if there's an existing record.
# If so, update it, and remove it from the existing records hash. If not,
# create it.
live_item_records.each do |record_data|
# If we don't recognize this item ID in our database already, skip it.
next unless recognized_item_ids.include?(record_data[:id])
record = existing_records_by_item_id.delete(record_data[:id]) ||
NCMallRecord.new
record.item_id = record_data[:id]
record.price = record_data[:price]
record.discount_price = record_data.dig(:discount, :price)
record.discount_begins_at = record_data.dig(:discount, :begins_at)
record.discount_ends_at = record_data.dig(:discount, :ends_at)
if !record.changed?
Rails.logger.info "Skipping record for item #{record_data[:name]} " +
"(unchanged)"
next
end
if record.save
if record.previously_new_record?
Rails.logger.info "Created record for item #{record_data[:name]}"
else
Rails.logger.info "Updated record for item #{record_data[:name]}"
end
else
Rails.logger.error "Failed to save record for item " +
"#{record_data[:name]}: " +
"#{record.errors.full_messages.join("; ")}: " +
"#{record.inspect}"
end
end
# For each existing record remaining in the existing records hash, this
# means there was no live record corresponding to it during this sync.
# Delete it!
existing_records_by_item_id.values.each do |record|
item_name = record.item&.name || "<item not found>"
if record.destroy
Rails.logger.info "Destroyed record #{record.id} for item " +
"#{item_name}"
else
Rails.logger.error "Failed to destroy record #{record.id} for " +
"item #{item_name}: #{record.inspect}"
end
end
end
end
def load_all_nc_mall_items
Sync do
# Load all categories from the menu JSON
categories = Neopets::NCMall.load_categories
# Load all pages for each category, 10 categories at a time
category_item_tasks = DTIRequests.load_many(max_at_once: 10) do |task|
categories.map do |category|
task.async do
type = category["type"]
cat_id = category["cat_id"]
Rails.logger.debug "Loading category: #{category["cat_name"]} " +
"(type=#{type}, cat=#{cat_id})"
Neopets::NCMall.load_category_all_pages(type, cat_id)
end
end
end
# Flatten all items from all categories and return as a single array
# (We'll de-duplicate in the main task)
category_item_tasks.map(&:wait).flatten
end
end