12 Commits

Author SHA1 Message Date
dcb3c2102e ecosystem.config.js tweaks
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-09 21:24:09 +03:00
1e6be9bfd9 tweaks
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-09 21:09:12 +03:00
ff444d6de2 SEO
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-09 19:57:29 +03:00
382ee2f39a Got rid of cringe /index path
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-09 19:43:09 +03:00
d56c127306 i18n fix and Ukranian lang
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-08 12:06:28 +03:00
fc01b7e7b9 i18n
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-08 03:00:46 +03:00
edb82fdcfa Image optimisation
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-07 03:20:49 +03:00
b57e3e5cba pm2 config in file
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-07 03:12:56 +03:00
fd32ca704a Filesystem org
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-07 02:07:16 +03:00
76fdee2cfc Merge pull request 'use-api' (#1) from use-api into master
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: anatolykopyl/warframe-market-bot#1
2022-03-06 22:37:17 +00:00
13a1bd9ab0 Added a rate limiter
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build encountered an error
2022-03-07 01:36:25 +03:00
7bfb00ad77 Switched to api from scraping
Some checks failed
continuous-integration/drone/push Build is failing
2022-03-07 01:17:48 +03:00
36 changed files with 2980 additions and 776 deletions

View File

@@ -18,7 +18,3 @@ steps:
commands:
- cd /home/pi/warframe-market-bot
- npm install
- name: restart
commands:
- export PM2_HOME=/home/pi/.pm2
- pm2 restart warframe-market-bot

2
.gitignore vendored
View File

@@ -2,4 +2,4 @@ node_modules
.DS_Store
.vscode
.env
public/index.html
public/*.html

View File

@@ -3,3 +3,5 @@
Список Прайм предметов: https://warframe.fandom.com/wiki/Prime
Маркет: https://warframe.market
Документация API маркета: https://warframe.market/api_docs

9
ecosystem.config.js Normal file
View File

@@ -0,0 +1,9 @@
module.exports = {
apps: [{
name: 'warframe-market-bot',
script: './src/index.js',
watch: true,
ignore_watch: ['node_modules', 'public'],
restart_delay: 1 * 60 * 1000
}]
}

View File

@@ -1,38 +0,0 @@
require('dotenv').config()
const puppeteer = require('puppeteer')
const items = require('./items')
const output = require('./output')
async function initPuppeteer (page) {
await page.setDefaultNavigationTimeout(0)
await page.setRequestInterception(true)
page.on('request', (request) => {
if (['image'].includes(request.resourceType())) request.abort()
else request.continue()
})
}
(async () => {
const browser = await puppeteer.launch({
executablePath: process.env.EXECUTABLE
})
const page = await browser.newPage()
await initPuppeteer(page)
for (const item of items) {
console.log(`Looking at ${item.name}`)
let partsPrice = 0
for (const part of item.parts) {
partsPrice += await part.getPrice(page)
}
const setPrice = await item.set.getPrice(page)
if (partsPrice < setPrice) {
output.addItem(item.name, partsPrice, setPrice)
}
}
await browser.close()
output.submit()
})()

View File

@@ -1,16 +0,0 @@
module.exports = class Part {
constructor (set, part, amount) {
this.part = part
this.url = `${set}_prime_${part}`
this.amount = amount ?? 1
}
async getPrice (page) {
const marketUrl = 'https://warframe.market/items/'
await page.goto(marketUrl + this.url)
const element = await page.waitForSelector('b.price')
const value = await element.evaluate(el => el.textContent)
return Number(value) * this.amount
}
}

View File

@@ -1,39 +0,0 @@
const fs = require('fs')
const Handlebars = require('handlebars')
const OutputItem = require('./OutputItem')
class Output {
constructor () {
this.items = []
this.timestamp = null
}
addItem (...p) {
this.items.push(new OutputItem(...p))
}
async compileTemplate () {
const templateFile = await fs.readFileSync('./output/template.hbs', 'utf8')
const template = Handlebars.compile(templateFile)
return template(this)
}
async writeToFile (content) {
const filename = './public/index.html'
try {
await fs.unlinkSync(filename)
} catch {
console.log('File probably doesnt exist')
}
fs.writeFileSync(filename, content, 'utf-8')
}
async submit () {
this.timestamp = new Date()
const content = await this.compileTemplate()
await this.writeToFile(content)
}
}
module.exports = new Output()

3411
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,11 +9,13 @@
"author": "Anatoly Kopyl",
"license": "ISC",
"dependencies": {
"axios": "^0.26.0",
"dotenv": "^16.0.0",
"handlebars": "^4.7.7",
"puppeteer": "^13.3.2"
"limiter": "^2.1.0"
},
"devDependencies": {
"eslint-config-standard": "^16.0.3"
"eslint-config-standard": "^16.0.3",
"pm2": "^5.2.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 KiB

After

Width:  |  Height:  |  Size: 234 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -60,6 +60,7 @@ a {
background: white;
color: black;
text-align: center;
overflow: hidden;
}
.hero > .main {
@@ -93,6 +94,31 @@ a {
right: 0;
}
.languages {
position: absolute;
left: 0;
top: 0;
margin: 10px;
z-index: 20;
}
.languages > a {
position: relative;
display: inline-block;
padding: 5px;
height: 32px;
border-radius: 50%;
}
.languages img {
max-height: 100%;
}
.languages .selected {
background: rgb(233, 233, 233);
box-shadow: 0 0 5px rgba(0, 0, 0, 50%);
}
.controls {
margin: 32px auto;
display: flex;

31
src/api.js Normal file
View File

@@ -0,0 +1,31 @@
const axios = require('axios')
const { RateLimiter } = require('limiter')
class Api {
constructor () {
this.baseUrl = 'https://api.warframe.market/v1/'
this.delay = process.env.API_DELAY ?? 3000
this.limiter = new RateLimiter({ tokensPerInterval: 1, interval: Number(this.delay) })
}
async _get (url) {
await this.limiter.removeTokens(1)
return axios.get(url)
}
async getOrders (url) {
const response = await this._get(this.baseUrl + 'items/' + url + '/orders')
return response.data.payload.orders.filter(function (order) {
return order.order_type === 'sell' && order.user.status !== 'offline'
})
}
async getSortedOrders (url) {
const orders = await this.getOrders(url)
return orders.sort(function (a, b) {
return a.platinum - b.platinum
})
}
}
module.exports = new Api()

21
src/index.js Normal file
View File

@@ -0,0 +1,21 @@
require('dotenv').config()
const items = require('./items')
const output = require('./output');
(async () => {
for (const item of items) {
console.log(`Looking at ${item.name}`)
let partsPrice = 0
for (const part of item.parts) {
partsPrice += await part.getPrice()
}
const setPrice = await item.set.getPrice()
if (partsPrice < setPrice) {
output.addItem(item.name, partsPrice, setPrice)
}
}
output.submit()
})()

14
src/items/Part.js Normal file
View File

@@ -0,0 +1,14 @@
const Api = require('../api')
module.exports = class Part {
constructor (set, part, amount) {
this.part = part
this.url = `${set}_prime_${part}`
this.amount = amount ?? 1
}
async getPrice () {
const orders = await Api.getSortedOrders(this.url)
return Number(orders[0].platinum) * this.amount
}
}

View File

@@ -1,4 +1,4 @@
module.exports = class OutputItem {
module.exports = class ListingSet {
constructor (name, parts, set) {
this.name = name + ' prime'
this.parts = parts

9
src/output/helpers.js Normal file
View File

@@ -0,0 +1,9 @@
function ifEq (a, b, opts) {
if (a === b) {
return opts.fn(this)
} else {
return opts.inverse(this)
}
}
module.exports = { ifEq }

35
src/output/i18n.json Normal file
View File

@@ -0,0 +1,35 @@
{
"en": {
"link": "",
"title": "Market Gaps",
"description": "Find a profitable difference between the price of the set and the price of the sum of it's parts.",
"filter_by_difference": "Filter by difference:",
"name": "Name",
"parts_price": "Parts price",
"set_price": "Set price",
"difference": "Difference",
"generated_at": "Generated at"
},
"ru": {
"link": "ru",
"title": "Market Gaps",
"description": "Находите выгоду между покупкой сета по отдельности и продажей его в собранном виде.",
"filter_by_difference": "Фильтровать по разнице:",
"name": "Название",
"parts_price": "Стоимость частей",
"set_price": "Стоимость сета",
"difference": "Разница",
"generated_at": "Сгенерировано в"
},
"ua": {
"link": "ua",
"title": "Market Gaps",
"description": "Знаходьте вигоду між покупкою сета окремо та продажем його в зібраному вигляді.",
"filter_by_difference": "Фільтрувати за різницею:",
"name": "Назва",
"parts_price": "Вартість частин",
"set_price": "Вартість сета",
"difference": "Різниця",
"generated_at": "Згенеровано в"
}
}

48
src/output/index.js Normal file
View File

@@ -0,0 +1,48 @@
const fs = require('fs')
const Handlebars = require('handlebars')
const { ifEq } = require('./helpers')
const ListingSet = require('./ListingSet')
const i18n = require('./i18n.json')
Handlebars.registerHelper('if_eq', ifEq)
class Output {
constructor () {
this.items = []
this.timestamp = null
}
addItem (...p) {
this.items.push(new ListingSet(...p))
}
async _compileTemplate () {
const templateFile = await fs.readFileSync('./src/output/template.hbs', 'utf8')
return Handlebars.compile(templateFile)
}
async _writeToFile (filename, content) {
try {
await fs.unlinkSync(filename)
} catch {
console.log('File probably doesnt exist')
}
fs.writeFileSync(filename, content, 'utf8')
}
async submit () {
this.timestamp = new Date()
const template = await this._compileTemplate()
Object.keys(i18n).forEach(locale => {
const filename = i18n[locale].link || 'index'
this._writeToFile(`./public/${filename}.html`, template({
...this,
t: i18n[locale],
languages: i18n
}))
})
}
}
module.exports = new Output()

View File

@@ -4,8 +4,22 @@
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Find profits in the Warframe market">
<meta name="keywords" content="warframe,market,buy,sell,trade,mod,blueprint,syndicate,relic,order,auction,riven,wts,wtb,sales,worth,platinum,price,checking">
<link rel="canonical" href="https://warframe.center/">
<link rel="alternate" hreflang="en" href="https://warframe.center/">
<link rel="alternate" hreflang="ru" href="https://warframe.center/ru/">
<link rel="alternate" hreflang="uk" href="https://warframe.center/ua/">
<link rel="alternate" hreflang="x-default" href="https://warframe.center/">
<meta property="og:title" content="Warframe Center">
<meta property="og:description" content="Find profits in the Warframe market">
<meta property="og:url" content="https://warframe.center">
<meta property="og:image" content="https://warframe.center/assets/warframe_logo.png">
<meta property="og:site_name" content="Warframe Center">
<meta property="og:locale" content="en">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="style.css">
<title>Warframe Market Gaps</title>
<title>Warframe Center</title>
<!-- Yandex.Metrika counter -->
<script type="text/javascript" >
@@ -28,16 +42,27 @@
<div class="main">
<img class="logo" src="./assets/warframe_logo.png">
<div class="text">
<h1>Market Gaps</h1>
<p>Find a profitable difference between the price of the set and the price of the sum of it's parts.</p>
<h1>{{t.title}}</h1>
<p>{{t.description}}</p>
</div>
</div>
<img id="gunner2" src="./assets/gunner2.png">
<div class="languages">
{{#each languages}}
<a
href="/{{this.link}}"
class="{{#if_eq ../t.link this.link}}selected{{/if_eq}}"
>
<img src="./assets/languages/{{@key}}.png">
</a>
{{/each}}
</div>
</div>
<div class="main-column">
<div class="controls">
<label for="min-difference">Filter by difference:</label>
<label for="min-difference">{{t.filter_by_difference}}</label>
<input type="range" min="1" max="60" value="1" id="min-difference">
<span id="filter-value">1</span>
</div>
@@ -45,10 +70,10 @@
<table>
<thead>
<tr>
<th>Name</th>
<th>Parts price</th>
<th>Set price</th>
<th>Difference</th>
<th>{{t.name}}</th>
<th>{{t.parts_price}}</th>
<th>{{t.set_price}}</th>
<th>{{t.difference}}</th>
</tr>
</thead>
<tbody id="items">
@@ -69,7 +94,7 @@
</div>
<div class="timestamp">
Generated at {{timestamp}}
{{t.generated_at}} {{timestamp}}
</div>
<script src="index.js"></script>