mirror of
https://github.com/arcan1s/ffxivbis.git
synced 2026-01-10 19:53:40 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5025e760c1 | |||
| 35c784bf89 | |||
| 757c4cc6df | |||
| ca24ee298e | |||
| af7a92af35 | |||
| 4c05ceefcd | |||
| fa43517b16 | |||
| 77e99439e7 | |||
| c9eb311cfe | |||
| d662e303c8 |
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@ -12,6 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: extract version
|
||||
id: version
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
|
||||
@ -21,13 +22,24 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
filter: 'Release \d+\.\d+\.\d+'
|
||||
|
||||
- name: setup JDK
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: install sbt
|
||||
uses: eclipse-score/apt-install@main
|
||||
with:
|
||||
packages: wget
|
||||
- run: |
|
||||
wget https://scala.jfrog.io/artifactory/debian/sbt-1.11.7.deb
|
||||
sudo dpkg -i sbt-1.11.7.deb
|
||||
|
||||
- name: create dist
|
||||
run: make dist
|
||||
|
||||
- name: release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
|
||||
10
.github/workflows/run-tests.yml
vendored
10
.github/workflows/run-tests.yml
vendored
@ -13,10 +13,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: setup JDK
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 18
|
||||
|
||||
- name: install sbt
|
||||
uses: eclipse-score/apt-install@main
|
||||
with:
|
||||
packages: wget
|
||||
- run: |
|
||||
wget https://scala.jfrog.io/artifactory/debian/sbt-1.11.7.deb
|
||||
sudo dpkg -i sbt-1.11.7.deb
|
||||
|
||||
- name: run tests
|
||||
run: make tests
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
|
||||
<div class="container">
|
||||
<div id="toolbar">
|
||||
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-loot-dialog" hidden>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-loot-dialog" hidden>
|
||||
<i class="bi bi-plus"></i> add
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="reload()">
|
||||
@ -147,7 +147,7 @@
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="suggestLoot()">suggest</button>
|
||||
<button type="submit" class="btn btn-primary">add</button>
|
||||
<button id="add-btn" type="submit" class="btn btn-primary">add</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -4,7 +4,7 @@ me.arcanis.ffxivbis {
|
||||
include "item_data.json"
|
||||
|
||||
# xivapi base url, string, required
|
||||
xivapi-url = "https://xivapi.com"
|
||||
xivapi-url = "https://v2.xivapi.com"
|
||||
# xivapi developer key, string, optional
|
||||
#xivapi-key = "abcdef"
|
||||
}
|
||||
|
||||
@ -12,5 +12,5 @@ trait ValidatorHelper {
|
||||
|
||||
object ValidatorHelper {
|
||||
|
||||
final val VALID_CHARACTERS = HashSet.from("!@#$%^&*()-_=+;:',./? ")
|
||||
final val VALID_CHARACTERS = HashSet.from("!@#$%^&*()-_=+;:',./?| ")
|
||||
}
|
||||
|
||||
@ -113,7 +113,7 @@ object Piece {
|
||||
case "weapon" => Weapon(pieceType, job)
|
||||
case "head" => Head(pieceType, job)
|
||||
case "body" => Body(pieceType, job)
|
||||
case "hands" => Hands(pieceType, job)
|
||||
case "hand" | "hands" => Hands(pieceType, job)
|
||||
case "legs" => Legs(pieceType, job)
|
||||
case "feet" => Feet(pieceType, job)
|
||||
case "ears" => Ears(pieceType, job)
|
||||
|
||||
@ -16,7 +16,7 @@ import com.typesafe.scalalogging.StrictLogging
|
||||
import me.arcanis.ffxivbis.messages.BiSProviderMessage
|
||||
import me.arcanis.ffxivbis.models.{BiS, Job, Piece, PieceType}
|
||||
import me.arcanis.ffxivbis.service.bis.parser.Parser
|
||||
import me.arcanis.ffxivbis.service.bis.parser.impl.{Ariyala, Etro}
|
||||
import me.arcanis.ffxivbis.service.bis.parser.impl.{Ariyala, Etro, XIVGear}
|
||||
import spray.json._
|
||||
|
||||
import java.nio.file.Paths
|
||||
@ -52,7 +52,10 @@ class BisProvider(context: ActorContext[BiSProviderMessage])
|
||||
val url = Uri(link)
|
||||
val id = Paths.get(link).normalize.getFileName.toString
|
||||
|
||||
val parser = if (url.authority.host.address().contains("etro")) Etro else Ariyala
|
||||
val parser =
|
||||
if (url.authority.host.address().contains("etro")) Etro
|
||||
else if (url.authority.host.address().contains("xivgear.app")) XIVGear
|
||||
else Ariyala
|
||||
val uri = parser.uri(url, id)
|
||||
sendRequest(uri, BisProvider.parseBisJsonToPieces(job, parser, getPieceType))
|
||||
} catch {
|
||||
@ -81,12 +84,11 @@ object BisProvider {
|
||||
}
|
||||
}
|
||||
|
||||
def remapKey(key: String): Option[String] = key match {
|
||||
case "mainhand" => Some("weapon")
|
||||
case "chest" => Some("body")
|
||||
case "ringLeft" | "fingerL" => Some("left ring")
|
||||
case "ringRight" | "fingerR" => Some("right ring")
|
||||
case "weapon" | "head" | "body" | "hands" | "legs" | "feet" | "ears" | "neck" | "wrist" | "wrists" => Some(key)
|
||||
case _ => None
|
||||
def remapKey(key: String): Option[String] = Some(key.toLowerCase).collect {
|
||||
case "mainhand" => "weapon"
|
||||
case "chest" => "body"
|
||||
case "ringleft" | "fingerl" => "left ring"
|
||||
case "ringright" | "fingerr" => "right ring"
|
||||
case "weapon" | "head" | "body" | "hand" | "hands" | "legs" | "feet" | "ears" | "neck" | "wrist" | "wrists" => key
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,6 @@ import spray.json._
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.util.Try
|
||||
import scala.util.matching.Regex
|
||||
|
||||
trait XivApi extends RequestExecutor {
|
||||
|
||||
@ -41,106 +40,38 @@ trait XivApi extends RequestExecutor {
|
||||
else remotePieceType(remote).map(_ ++ local)
|
||||
}
|
||||
|
||||
private def remotePieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType]] = {
|
||||
val uriForItems = Uri(xivapiUrl)
|
||||
.withPath(Uri.Path / "item")
|
||||
.withQuery(
|
||||
Uri.Query(
|
||||
Map(
|
||||
"columns" -> Seq("ID", "GameContentLinks").mkString(","),
|
||||
"ids" -> itemIds.mkString(","),
|
||||
"private_key" -> xivapiKey.getOrElse("")
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
sendRequest(uriForItems, XivApi.parseXivapiJsonToShop).flatMap { shops =>
|
||||
val shopIds = shops.values.map(_._2).toSet
|
||||
val columns = shops.values.map(pair => s"ItemCost${pair._1}").toSet
|
||||
val uriForShops = Uri(xivapiUrl)
|
||||
.withPath(Uri.Path / "specialshop")
|
||||
.withQuery(
|
||||
Uri.Query(
|
||||
Map(
|
||||
"columns" -> (columns + "ID").mkString(","),
|
||||
"ids" -> shopIds.mkString(","),
|
||||
"private_key" -> xivapiKey.getOrElse("")
|
||||
private def remotePieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType]] =
|
||||
Future
|
||||
.traverse(itemIds) { id =>
|
||||
val uriForItem = Uri(xivapiUrl)
|
||||
.withPath(Uri.Path / "api" / "sheet" / "Item" / id.toString)
|
||||
.withQuery(
|
||||
Uri.Query(
|
||||
Map(
|
||||
"fields" -> Seq("Lot").mkString(","),
|
||||
"private_key" -> xivapiKey.getOrElse("")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
sendRequest(uriForShops, XivApi.parseXivapiJsonToType(shops))
|
||||
}
|
||||
}
|
||||
sendRequest(uriForItem, XivApi.parseXivapiJsonToLot).map(id -> _)
|
||||
}
|
||||
.map(_.toMap)
|
||||
}
|
||||
|
||||
object XivApi {
|
||||
|
||||
private val defaultShop = JsObject("IsUnique" -> JsNumber(1), "StackSize" -> JsNumber(999))
|
||||
|
||||
private val itemRegexp = new Regex("""Item(Receive|Cost)(\d+)""", "type", "index")
|
||||
|
||||
private def parseXivapiJsonToShop(
|
||||
js: JsObject
|
||||
)(implicit executionContext: ExecutionContext): Future[Map[Long, (String, Long)]] = {
|
||||
def extractTraderId(js: JsObject) =
|
||||
js.fields
|
||||
.get("Recipe")
|
||||
.map(_ => "crafted" -> -1L) // you can craft this item
|
||||
.orElse { // lets try shop items
|
||||
js.fields("SpecialShop").asJsObject.fields.collectFirst {
|
||||
case (shopName, JsArray(array)) if itemRegexp.matches(shopName) =>
|
||||
val shopId = array.head match {
|
||||
case JsNumber(id) => id.toLong
|
||||
case other => throw deserializationError(s"Could not parse $other")
|
||||
}
|
||||
itemRegexp.findFirstMatchIn(shopName).get.group("index") -> shopId
|
||||
}
|
||||
}
|
||||
.getOrElse(throw deserializationError(s"Could not parse $js"))
|
||||
|
||||
private def parseXivapiJsonToLot(js: JsObject)(implicit executionContext: ExecutionContext): Future[PieceType] =
|
||||
Future {
|
||||
js.fields("Results") match {
|
||||
case array: JsArray =>
|
||||
array.elements
|
||||
.map(_.asJsObject.getFields("ID", "GameContentLinks") match {
|
||||
case Seq(JsNumber(id), shop: JsObject) => id.toLong -> extractTraderId(shop.asJsObject)
|
||||
case other => throw deserializationError(s"Could not parse $other")
|
||||
})
|
||||
.toMap
|
||||
case other => throw deserializationError(s"Could not parse $other")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def parseXivapiJsonToType(
|
||||
shops: Map[Long, (String, Long)]
|
||||
)(js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[Long, PieceType]] =
|
||||
Future {
|
||||
val shopMap = js.fields("Results") match {
|
||||
case array: JsArray =>
|
||||
array.elements.collect { case shop: JsObject =>
|
||||
shop.fields("ID") match {
|
||||
case JsNumber(id) => id.toLong -> shop
|
||||
case other => throw deserializationError(s"Could not parse $other")
|
||||
js.fields("fields") match {
|
||||
case JsObject(fields) =>
|
||||
fields
|
||||
.get("Lot")
|
||||
.collect {
|
||||
case JsBoolean(true) => PieceType.Savage
|
||||
case JsBoolean(false) => PieceType.Tome
|
||||
}
|
||||
}.toMap
|
||||
case other => throw deserializationError(s"Could not parse $other")
|
||||
}
|
||||
|
||||
shops.map { case (itemId, (index, shopId)) =>
|
||||
val pieceType =
|
||||
if (index == "crafted" && shopId == -1L) PieceType.Crafted
|
||||
else
|
||||
Try(shopMap(shopId).fields(s"ItemCost$index").asJsObject)
|
||||
.getOrElse(defaultShop)
|
||||
.getFields("IsUnique", "StackSize") match {
|
||||
case Seq(JsNumber(isUnique), JsNumber(stackSize)) =>
|
||||
if (isUnique == 1 || stackSize.toLong != 999) PieceType.Tome // either upgraded gear or tomes found
|
||||
else PieceType.Savage
|
||||
case other => throw deserializationError(s"Could not parse $other")
|
||||
}
|
||||
itemId -> pieceType
|
||||
.getOrElse(throw deserializationError(s"Could not find lot field in $fields"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 Evgeniy Alekseev.
|
||||
*
|
||||
* This file is part of ffxivbis
|
||||
* (see https://github.com/arcan1s/ffxivbis).
|
||||
*
|
||||
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||
*/
|
||||
package me.arcanis.ffxivbis.service.bis.parser.impl
|
||||
|
||||
import akka.http.scaladsl.model.Uri
|
||||
import me.arcanis.ffxivbis.models.Job
|
||||
import me.arcanis.ffxivbis.service.bis.BisProvider
|
||||
import me.arcanis.ffxivbis.service.bis.parser.Parser
|
||||
import spray.json.{deserializationError, JsNumber, JsObject}
|
||||
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
object XIVGear extends Parser {
|
||||
|
||||
override def parse(job: Job, js: JsObject)(implicit executionContext: ExecutionContext): Future[Map[String, Long]] =
|
||||
Future {
|
||||
val set = js.fields.get("items") match {
|
||||
case Some(JsObject(items)) => items
|
||||
case other => throw deserializationError(s"Invalid job name $other")
|
||||
}
|
||||
set.foldLeft(Map.empty[String, Long]) {
|
||||
case (acc, (key, JsObject(properties))) =>
|
||||
val pieceId = properties.get("id").collect { case JsNumber(id) =>
|
||||
id.toLong
|
||||
}
|
||||
(for (
|
||||
piece <- BisProvider.remapKey(key);
|
||||
id <- pieceId
|
||||
) yield (piece, id)).map(acc + _).getOrElse(acc)
|
||||
case (acc, _) => acc
|
||||
}
|
||||
}
|
||||
|
||||
override def uri(root: Uri, id: String): Uri = {
|
||||
val gearSet = Uri(id).query().get("page").map(_.replace("sl|", "")).getOrElse(id)
|
||||
root.withHost(s"api.${root.authority.host.address()}").withPath(Uri.Path / "shortlink" / gearSet)
|
||||
}
|
||||
}
|
||||
@ -52,12 +52,28 @@ object Fixtures {
|
||||
Piece.Ring(pieceType = PieceType.Tome, Job.SGE, "right ring")
|
||||
)
|
||||
)
|
||||
lazy val bis4: BiS = BiS(
|
||||
Seq(
|
||||
Piece.Weapon(pieceType = PieceType.Savage ,Job.VPR),
|
||||
Piece.Head(pieceType = PieceType.Savage, Job.VPR),
|
||||
Piece.Body(pieceType = PieceType.Savage, Job.VPR),
|
||||
Piece.Hands(pieceType = PieceType.Tome, Job.VPR),
|
||||
Piece.Legs(pieceType = PieceType.Tome, Job.VPR),
|
||||
Piece.Feet(pieceType = PieceType.Savage, Job.VPR),
|
||||
Piece.Ears(pieceType = PieceType.Tome, Job.VPR),
|
||||
Piece.Neck(pieceType = PieceType.Savage, Job.VPR),
|
||||
Piece.Wrist(pieceType = PieceType.Tome, Job.VPR),
|
||||
Piece.Ring(pieceType = PieceType.Savage, Job.VPR, "left ring"),
|
||||
Piece.Ring(pieceType = PieceType.Tome, Job.VPR, "right ring")
|
||||
)
|
||||
)
|
||||
|
||||
lazy val link: String = "https://ffxiv.ariyala.com/19V5R"
|
||||
lazy val link2: String = "https://ffxiv.ariyala.com/1A0WM"
|
||||
lazy val link3: String = "https://etro.gg/gearset/26a67536-b4ce-4adc-a46a-f70e348bb138"
|
||||
lazy val link4: String = "https://etro.gg/gearset/865fc886-994f-4c28-8fc1-4379f160a916"
|
||||
lazy val link5: String = "https://ffxiv.ariyala.com/1FGU0"
|
||||
lazy val link6: String = "https://xivgear.app/?page=sl%7Cd65b4776-01e1-4269-af74-0bc6e01ca2ec"
|
||||
|
||||
lazy val lootWeapon: Piece = Piece.Weapon(pieceType = PieceType.Tome, Job.AnyJob)
|
||||
lazy val lootBody: Piece = Piece.Body(pieceType = PieceType.Savage, Job.AnyJob)
|
||||
|
||||
@ -35,11 +35,17 @@ class BisProviderTest extends ScalaTestWithActorTestKit(Settings.withRandomDatab
|
||||
probe.expectMessage(askTimeout, Fixtures.bis)
|
||||
}
|
||||
|
||||
"get best in slot set (etro 2)" in {
|
||||
"get best in slot set (etro 2)" ignore {
|
||||
val probe = testKit.createTestProbe[BiS]()
|
||||
provider ! DownloadBiS(Fixtures.link4, Job.DNC, probe.ref)
|
||||
probe.expectMessage(askTimeout, Fixtures.bis2)
|
||||
}
|
||||
|
||||
"get best in slot set (xivgear)" in {
|
||||
val probe = testKit.createTestProbe[BiS]()
|
||||
provider ! DownloadBiS(Fixtures.link6, Job.VPR, probe.ref)
|
||||
probe.expectMessage(askTimeout, Fixtures.bis4)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
version := "0.15.1"
|
||||
version := "0.15.6"
|
||||
|
||||
Reference in New Issue
Block a user