Skip to content

Commit

Permalink
feat: safebooru client
Browse files Browse the repository at this point in the history
  • Loading branch information
magonxesp committed Feb 12, 2024
1 parent 87b80b3 commit 1eab96b
Show file tree
Hide file tree
Showing 14 changed files with 480 additions and 6 deletions.
3 changes: 3 additions & 0 deletions src/main/kotlin/com/magonxesp/booruclient/ClientException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ sealed class ClientException(override val message: String? = null) : Exception(m
class InvalidTagFormat(override val message: String? = null) : ClientException(message)
class UnknownError(override val message: String? = null) : ClientException(message)
class RequestFailed(override val message: String? = null) : ClientException(message)
class InvalidLimit(override val message: String? = null) : ClientException(message)
class InvalidPage(override val message: String? = null) : ClientException(message)
class ParseError(override val message: String? = null) : ClientException(message)
}
7 changes: 7 additions & 0 deletions src/main/kotlin/com/magonxesp/booruclient/StringUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.magonxesp.booruclient

fun String?.toIntOrDefault() =
if (!isNullOrBlank() && toIntOrNull() != null) toInt() else 0

fun String?.toLongOrDefault() =
if (!isNullOrBlank() && toLongOrNull() != null) toLong() else 0
2 changes: 1 addition & 1 deletion src/main/kotlin/com/magonxesp/booruclient/Tag.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Tag(val value: String) {
}

private fun validate() {
if (!Regex("[a-zA-Z*]+(?:_[a-zA-Z*]+)*").matches(value)) {
if (!Regex("^[a-zA-Z\\*\\-0-9]+(?:_[a-zA-Z\\*\\-0-9]+)*\$").matches(value)) {
throw ClientException.InvalidTagFormat("The format of the tag is invalid, the valid format is for example: suzumiya_haruhi")
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/main/kotlin/com/magonxesp/booruclient/safebooru/Order.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.magonxesp.booruclient.safebooru

enum class Order(val value: String) {
DESC("desc"),
ASC("asc")
}
10 changes: 10 additions & 0 deletions src/main/kotlin/com/magonxesp/booruclient/safebooru/Rating.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.magonxesp.booruclient.safebooru

enum class Rating(val value: String) {
SAFE("safe"),
/**
* Safebooru only has safe content, the explicit rating don't work
*/
EXPLICIT("explicit"),
QUESTIONABLE("questionable")
}
288 changes: 288 additions & 0 deletions src/main/kotlin/com/magonxesp/booruclient/safebooru/SafebooruClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
package com.magonxesp.booruclient.safebooru

import com.magonxesp.booruclient.Client
import com.magonxesp.booruclient.ClientException
import com.magonxesp.booruclient.Tag
import org.w3c.dom.Element
import org.w3c.dom.Node
import org.xml.sax.InputSource
import java.io.StringReader
import java.net.URLEncoder
import javax.xml.parsers.DocumentBuilderFactory

class SafebooruClient : Client() {
override val baseUrl: String = "https://safebooru.org"

class Builder {
private val queryParameters: MutableMap<String, String> = mutableMapOf(
"page" to "dapi",
"s" to "post",
"q" to "index",
)

private val tags = mutableListOf<String>()

/**
* Search posts by tag, adding other tags with [tag] method will search
* by posts that contains all tags added to search
*/
fun tag(tag: String) {
tags.add(Tag(tag).value)
}

/**
* Search posts that don't have this tag
*/
fun notHaveTag(tag: String) {
tags.add("-${Tag(tag).value}")
}

/**
* Search posts by tags that [startsWith] and [endsWith]
*/
fun startsAndEndWith(startsWith: String, endsWith: String) {
tags.add("${Tag(startsWith).value}*$${Tag(endsWith).value}")
}

/**
* Search by user
*/
fun user(user: String) {
tags.add("user:${Tag(user).value}")
}

/**
* Search for posts with the MD5 hash.
*/
fun md5(md5: String) {
tags.add("md5:$md5")
}

/**
* Search for posts whose MD5 starts with the MD5 hash foo.
*/
fun startsWithMd5(md5: String) {
tags.add("md5:$md5*")
}

/**
* Search posts by [rating]
*/
fun rating(rating: Rating) {
tags.add("rating:${rating.value}")
}

/**
* Search posts that not have [rating]
*/
fun notHaveRating(rating: Rating) {
tags.add("-rating:${rating.value}")
}

/**
* Search posts that have [parent]
*/
fun parent(parent: String) {
tags.add("parent:$parent")
}

/**
* Search posts by width greater than [width]
*/
fun widthGreaterThan(width: Int) {
tags.add("width:>$width")
}

/**
* Search posts by width greater or equal than [width]
*/
fun widthGreaterOrEqualThan(width: Int) {
tags.add("width:>=$width")
}

/**
* Search posts by width equal than [width]
*/
fun widthEqualThan(width: Int) {
tags.add("width:=$width")
}

/**
* Search posts by width less than [width]
*/
fun widthLessThan(width: Int) {
tags.add("width:<$width")
}

/**
* Search posts by width less or equal than [width]
*/
fun widthLessOrEqualThan(width: Int) {
tags.add("width:<=$width")
}

/**
* Search posts by height greater than [height]
*/
fun heightGreaterThan(height: Int) {
tags.add("height:>$height")
}

/**
* Search posts by height greater or equal than [height]
*/
fun heightGreaterOrEqualThan(height: Int) {
tags.add("height:>=$height")
}

/**
* Search posts by height equal than [height]
*/
fun heightEqualThan(height: Int) {
tags.add("height:=$height")
}

/**
* Search posts by height less than [height]
*/
fun heightLessThan(height: Int) {
tags.add("height:<$height")
}

/**
* Search posts by height less or equal than [height]
*/
fun heightLessOrEqualThan(height: Int) {
tags.add("height:<=$height")
}

/**
* Search posts by score greater than [score]
*/
fun scoreGreaterThan(score: Int) {
tags.add("score:>$score")
}

/**
* Search posts by score greater or equal than [score]
*/
fun scoreGreaterOrEqualThan(score: Int) {
tags.add("score:>=$score")
}

/**
* Search posts by score equal than [score]
*/
fun scoreEqualThan(score: Int) {
tags.add("score:=$score")
}

/**
* Search posts by score less than [score]
*/
fun scoreLessThan(score: Int) {
tags.add("score:<$score")
}

/**
* Search posts by score less or equal than [score]
*/
fun scoreLessOrEqualThan(score: Int) {
tags.add("score:<=$score")
}

/**
* Sort posts by [type]
*/
fun sortBy(type: SortType, order: Order = Order.ASC) {
tags.add("sort:${type.value}:${order.value}")
}

/**
* Set the posts limit, it should be greater than 0 or will throw [ClientException.InvalidLimit]
*
* @throws ClientException.InvalidLimit
*/
fun limit(limit: Int) {
if (limit < 1) {
throw ClientException.InvalidLimit("The limit should be greater than 0")
}

queryParameters["limit"] = limit.toString()
}

/**
* Set the page number for the pager, It should be grater than 0 or will throw [ClientException.InvalidPage]
*
* @throws ClientException.InvalidPage
*/
fun page(page: Int) {
if (page < 1) {
throw ClientException.InvalidPage("The page should be greater than 0")
}

queryParameters["pid"] = page.toString()
}

/**
* Change ID of the post. This is in Unix time so there are likely others with the same value if updated at the same time.
*/
fun cid(cid: String) {
queryParameters["cid"] = cid
}

/**
* The post id
*/
fun id(id: String) {
queryParameters["cid"] = id
}

/**
* Show the deleted posts
*/
fun showDeleted() {
queryParameters["deleted"] = "show"
}

/**
* Build the query string for perform the search
*/
fun build(): Map<String, String> {
if (tags.isNotEmpty()) {
val tagsEncoded = URLEncoder.encode(tags.joinToString(" "), "utf-8")
queryParameters["tags"] = tagsEncoded
}

return queryParameters
}
}

private fun parseXmlPosts(xml: String): SafebooruPostCollection {
val factory = DocumentBuilderFactory.newInstance()
val builder = factory.newDocumentBuilder()
val reader = InputSource(StringReader(xml))
val document = builder.parse(reader)

val root = document.documentElement

if (root.tagName == "response") {
val response = root.parseSafebooruResponse()
throw ClientException.RequestFailed("Safebooru request failed: ${response.reason}")
}

if (root.tagName != "posts") {
throw ClientException.ParseError("Error parsing safebooru xml: posts element not found")
}

return root.parseSafebooruPostCollection()
}

suspend fun search(setup: Builder.() -> Unit): SafebooruPostCollection {
val builder = Builder()
builder.setup()

val rawXml = get("/index.php", builder.build())
return parseXmlPosts(rawXml)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.magonxesp.booruclient.safebooru

data class SafebooruPost(
val height: Int,
val score: Int,
val fileUrl: String,
val parentId: String,
val sampleUrl: String,
val sampleWidth: Int,
val sampleHeight: Int,
val previewUrl: String,
val rating: String,
val tags: String,
val id: Int,
val width: Int,
val change: Long,
val md5: String,
val creatorId: Int,
val hasChildren: Boolean,
val createdAt: String,
val status: String,
val source: String,
val hasNotes: Boolean,
val hasComments: Boolean,
val previewWidth: Int,
val previewHeight: Int
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.magonxesp.booruclient.safebooru

data class SafebooruPostCollection(
val count: Int,
val offset: Int,
val posts: List<SafebooruPost>
)
Loading

0 comments on commit 1eab96b

Please sign in to comment.