Phoenix 1.8 app that scrapes ammunition retailers (Lucky Gunner, SGAmmo) for price data and displays historical price trends. - Data model: retailers, calibers, products, price snapshots - Scraper infrastructure with Req, Floki, realistic browser headers - Oban-scheduled scrape jobs (every 4h with randomized delays) - LiveView pages: homepage with category cards, caliber detail with price table, Chart.js price history, and price stats banner - 18 seeded calibers across handgun/rifle/rimfire/shotgun categories - 77 tests
85 lines
2.9 KiB
Elixir
85 lines
2.9 KiB
Elixir
alias Ammoprices.Catalog.Caliber
|
|
alias Ammoprices.Catalog.Retailer
|
|
alias Ammoprices.Repo
|
|
|
|
# Retailers
|
|
retailers = [
|
|
%{
|
|
name: "Lucky Gunner",
|
|
slug: "lucky-gunner",
|
|
base_url: "https://www.luckygunner.com"
|
|
},
|
|
%{
|
|
name: "SGAmmo",
|
|
slug: "sgammo",
|
|
base_url: "https://www.sgammo.com"
|
|
}
|
|
]
|
|
|
|
for attrs <- retailers do
|
|
%Retailer{}
|
|
|> Retailer.changeset(attrs)
|
|
|> Repo.insert!(on_conflict: :nothing, conflict_target: :slug)
|
|
end
|
|
|
|
# Calibers by category
|
|
calibers = [
|
|
# Handgun
|
|
%{name: "9mm Luger", slug: "9mm-luger", category: "handgun", aliases: ["9mm", "9x19", "9mm parabellum", "9mm nato"]},
|
|
%{name: ".45 ACP", slug: "45-acp", category: "handgun", aliases: [".45 acp", "45 acp", ".45 auto", "45 auto"]},
|
|
%{name: ".380 ACP", slug: "380-acp", category: "handgun", aliases: [".380", "380 acp", ".380 auto", "380 auto"]},
|
|
%{name: ".40 S&W", slug: "40-sw", category: "handgun", aliases: [".40 s&w", "40 s&w", ".40 cal", "40 cal"]},
|
|
%{
|
|
name: ".38 Special",
|
|
slug: "38-special",
|
|
category: "handgun",
|
|
aliases: [".38 spl", "38 special", ".38 spc", "38 spl"]
|
|
},
|
|
%{name: ".357 Magnum", slug: "357-magnum", category: "handgun", aliases: [".357 mag", "357 mag", "357 magnum"]},
|
|
%{name: "10mm Auto", slug: "10mm-auto", category: "handgun", aliases: ["10mm", "10mm auto"]},
|
|
|
|
# Rifle
|
|
%{
|
|
name: "5.56x45 / .223 Rem",
|
|
slug: "556-223",
|
|
category: "rifle",
|
|
aliases: ["5.56", "5.56x45", ".223", ".223 rem", "223 remington", "5.56 nato"]
|
|
},
|
|
%{
|
|
name: ".308 Win / 7.62x51",
|
|
slug: "308-win",
|
|
category: "rifle",
|
|
aliases: [".308", ".308 win", "308 winchester", "7.62x51", "7.62 nato"]
|
|
},
|
|
%{name: "7.62x39", slug: "762x39", category: "rifle", aliases: ["7.62x39", "7.62x39mm"]},
|
|
%{
|
|
name: ".30-06 Springfield",
|
|
slug: "30-06",
|
|
category: "rifle",
|
|
aliases: [".30-06", "30-06", "30-06 springfield", ".30-06 sprg"]
|
|
},
|
|
%{
|
|
name: ".300 Blackout",
|
|
slug: "300-blackout",
|
|
category: "rifle",
|
|
aliases: [".300 blk", "300 blackout", ".300 aac blackout", "300 blk"]
|
|
},
|
|
%{name: "6.5 Creedmoor", slug: "65-creedmoor", category: "rifle", aliases: ["6.5 creedmoor", "6.5cm", "6.5 cm"]},
|
|
|
|
# Rimfire
|
|
%{name: ".22 LR", slug: "22-lr", category: "rimfire", aliases: [".22 lr", "22 lr", ".22 long rifle", "22 long rifle"]},
|
|
%{name: ".22 WMR", slug: "22-wmr", category: "rimfire", aliases: [".22 wmr", "22 wmr", ".22 magnum", "22 mag"]},
|
|
%{name: ".17 HMR", slug: "17-hmr", category: "rimfire", aliases: [".17 hmr", "17 hmr", ".17 hornady magnum"]},
|
|
|
|
# Shotgun
|
|
%{name: "12 Gauge", slug: "12-gauge", category: "shotgun", aliases: ["12 gauge", "12 ga", "12ga"]},
|
|
%{name: "20 Gauge", slug: "20-gauge", category: "shotgun", aliases: ["20 gauge", "20 ga", "20ga"]}
|
|
]
|
|
|
|
for attrs <- calibers do
|
|
%Caliber{}
|
|
|> Caliber.changeset(attrs)
|
|
|> Repo.insert!(on_conflict: :nothing, conflict_target: :slug)
|
|
end
|
|
|
|
IO.puts("Seeded #{length(retailers)} retailers and #{length(calibers)} calibers")
|