1 Commits

Author SHA1 Message Date
8a37c66be8 build: install sbt explicitly 2026-01-08 12:37:31 +02:00
7 changed files with 99 additions and 30 deletions

View File

@@ -35,7 +35,7 @@ jobs:
packages: wget packages: wget
- run: | - run: |
wget https://scala.jfrog.io/artifactory/debian/sbt-1.11.7.deb wget https://scala.jfrog.io/artifactory/debian/sbt-1.11.7.deb
sudo dpkg -i sbt-1.11.7.deb dpkg -i sbt-1.11.7.deb
- name: create dist - name: create dist
run: make dist run: make dist

View File

@@ -26,7 +26,7 @@ jobs:
packages: wget packages: wget
- run: | - run: |
wget https://scala.jfrog.io/artifactory/debian/sbt-1.11.7.deb wget https://scala.jfrog.io/artifactory/debian/sbt-1.11.7.deb
sudo dpkg -i sbt-1.11.7.deb dpkg -i sbt-1.11.7.deb
- name: run tests - name: run tests
run: make tests run: make tests

View File

@@ -41,7 +41,7 @@
<div class="container"> <div class="container">
<div id="toolbar"> <div id="toolbar">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-loot-dialog" hidden> <button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-loot-dialog" hidden>
<i class="bi bi-plus"></i> add <i class="bi bi-plus"></i> add
</button> </button>
<button class="btn btn-secondary" onclick="reload()"> <button class="btn btn-secondary" onclick="reload()">
@@ -147,7 +147,7 @@
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button> <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="button" class="btn btn-secondary" onclick="suggestLoot()">suggest</button>
<button id="add-btn" type="submit" class="btn btn-primary">add</button> <button type="submit" class="btn btn-primary">add</button>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -4,7 +4,7 @@ me.arcanis.ffxivbis {
include "item_data.json" include "item_data.json"
# xivapi base url, string, required # xivapi base url, string, required
xivapi-url = "https://v2.xivapi.com" xivapi-url = "https://xivapi.com"
# xivapi developer key, string, optional # xivapi developer key, string, optional
#xivapi-key = "abcdef" #xivapi-key = "abcdef"
} }

View File

@@ -15,6 +15,7 @@ import spray.json._
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import scala.jdk.CollectionConverters._ import scala.jdk.CollectionConverters._
import scala.util.Try import scala.util.Try
import scala.util.matching.Regex
trait XivApi extends RequestExecutor { trait XivApi extends RequestExecutor {
@@ -40,38 +41,106 @@ trait XivApi extends RequestExecutor {
else remotePieceType(remote).map(_ ++ local) else remotePieceType(remote).map(_ ++ local)
} }
private def remotePieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType]] = private def remotePieceType(itemIds: Seq[Long]): Future[Map[Long, PieceType]] = {
Future val uriForItems = Uri(xivapiUrl)
.traverse(itemIds) { id => .withPath(Uri.Path / "item")
val uriForItem = Uri(xivapiUrl)
.withPath(Uri.Path / "api" / "sheet" / "Item" / id.toString)
.withQuery( .withQuery(
Uri.Query( Uri.Query(
Map( Map(
"fields" -> Seq("Lot").mkString(","), "columns" -> Seq("ID", "GameContentLinks").mkString(","),
"ids" -> itemIds.mkString(","),
"private_key" -> xivapiKey.getOrElse("") "private_key" -> xivapiKey.getOrElse("")
) )
) )
) )
sendRequest(uriForItem, XivApi.parseXivapiJsonToLot).map(id -> _) 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("")
)
)
)
sendRequest(uriForShops, XivApi.parseXivapiJsonToType(shops))
}
} }
.map(_.toMap)
} }
object XivApi { object XivApi {
private def parseXivapiJsonToLot(js: JsObject)(implicit executionContext: ExecutionContext): Future[PieceType] = private val defaultShop = JsObject("IsUnique" -> JsNumber(1), "StackSize" -> JsNumber(999))
Future {
js.fields("fields") match { private val itemRegexp = new Regex("""Item(Receive|Cost)(\d+)""", "type", "index")
case JsObject(fields) =>
fields private def parseXivapiJsonToShop(
.get("Lot") js: JsObject
.collect { )(implicit executionContext: ExecutionContext): Future[Map[Long, (String, Long)]] = {
case JsBoolean(true) => PieceType.Savage def extractTraderId(js: JsObject) =
case JsBoolean(false) => PieceType.Tome 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")
} }
.getOrElse(throw deserializationError(s"Could not find lot field in $fields")) itemRegexp.findFirstMatchIn(shopName).get.group("index") -> shopId
}
}
.getOrElse(throw deserializationError(s"Could not parse $js"))
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")
}
}.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
} }
} }
} }

View File

@@ -35,7 +35,7 @@ class BisProviderTest extends ScalaTestWithActorTestKit(Settings.withRandomDatab
probe.expectMessage(askTimeout, Fixtures.bis) probe.expectMessage(askTimeout, Fixtures.bis)
} }
"get best in slot set (etro 2)" ignore { "get best in slot set (etro 2)" in {
val probe = testKit.createTestProbe[BiS]() val probe = testKit.createTestProbe[BiS]()
provider ! DownloadBiS(Fixtures.link4, Job.DNC, probe.ref) provider ! DownloadBiS(Fixtures.link4, Job.DNC, probe.ref)
probe.expectMessage(askTimeout, Fixtures.bis2) probe.expectMessage(askTimeout, Fixtures.bis2)

View File

@@ -1 +1 @@
version := "0.15.6" version := "0.15.5"