mirror of https://github.com/aruppi/aruppi-api.git
Initial preview API for v5 with 2 enpoints
parent
2308dd49f5
commit
ff7b55ce40
@ -0,0 +1,4 @@
|
|||||||
|
build
|
||||||
|
.gradle
|
||||||
|
.git
|
||||||
|
*.md
|
@ -1,2 +1,50 @@
|
|||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
# System Files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Environment Variables
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker-compose.override.yml
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
bin/
|
||||||
|
!**/src/main/**/bin/
|
||||||
|
!**/src/test/**/bin/
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
!**/src/main/**/out/
|
||||||
|
!**/src/test/**/out/
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
gradle/
|
||||||
|
|
||||||
|
gradlew
|
||||||
|
|
||||||
|
gradlew.bat
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
# Stage 1: Cache Gradle dependencies
|
||||||
|
FROM gradle:8-jdk21-corretto AS cache
|
||||||
|
RUN mkdir -p /home/gradle/cache_home
|
||||||
|
ENV GRADLE_USER_HOME /home/gradle/cache_home
|
||||||
|
COPY build.gradle.kts /home/gradle/src/build.gradle.kts
|
||||||
|
COPY settings.gradle.kts /home/gradle/src/settings.gradle.kts
|
||||||
|
COPY gradle/libs.versions.toml /home/gradle/src/gradle/libs.versions.toml
|
||||||
|
WORKDIR /home/gradle/src
|
||||||
|
RUN gradle clean build -i --stacktrace
|
||||||
|
|
||||||
|
# Stage 2: Build Application
|
||||||
|
FROM gradle:8-jdk21-corretto AS build
|
||||||
|
COPY --from=cache /home/gradle/cache_home /home/gradle/.gradle
|
||||||
|
COPY . /usr/src/app/
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
COPY --chown=gradle:gradle . /home/gradle/src
|
||||||
|
WORKDIR /home/gradle/src
|
||||||
|
# Build the fat JAR, Gradle also supports shadow
|
||||||
|
# and boot JAR by default.
|
||||||
|
RUN gradle buildFatJar --no-daemon
|
||||||
|
|
||||||
|
# Stage 3: Create the Runtime Image
|
||||||
|
FROM amazoncorretto:21 AS runtime
|
||||||
|
EXPOSE 8080/tcp
|
||||||
|
RUN mkdir /app
|
||||||
|
COPY --from=build /home/gradle/src/build/libs/*.jar /app/app.jar
|
||||||
|
COPY entrypoint.sh /app/entrypoint.sh
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
@ -0,0 +1,33 @@
|
|||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.jvm)
|
||||||
|
alias(libs.plugins.ktor)
|
||||||
|
alias(libs.plugins.kotlin.plugin.serialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "com.jeluchu"
|
||||||
|
version = "5.0.0"
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass.set("io.ktor.server.netty.EngineMain")
|
||||||
|
|
||||||
|
val isDevelopment: Boolean = project.ext.has("development")
|
||||||
|
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.bson)
|
||||||
|
implementation(libs.logback.classic)
|
||||||
|
implementation(libs.ktor.server.core)
|
||||||
|
implementation(libs.ktor.server.netty)
|
||||||
|
implementation(libs.ktor.server.swagger)
|
||||||
|
implementation(libs.mongodb.driver.core)
|
||||||
|
implementation(libs.mongodb.driver.sync)
|
||||||
|
implementation(libs.ktor.server.config.yaml)
|
||||||
|
implementation(libs.ktor.server.status.pages)
|
||||||
|
implementation(libs.ktor.serialization.kotlinx.json)
|
||||||
|
implementation(libs.ktor.server.content.negotiation)
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: ktor_app
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
driver: bridge
|
@ -0,0 +1,2 @@
|
|||||||
|
MONGO_CONNECTION_STRING=mongodb://user:password@host:port
|
||||||
|
MONGO_DATABASE_NAME=aruppi
|
@ -0,0 +1,3 @@
|
|||||||
|
kotlin.code.style=official
|
||||||
|
org.gradle.caching=true
|
||||||
|
org.gradle.daemon=false
|
@ -0,0 +1 @@
|
|||||||
|
rootProject.name = "aruppi-api"
|
@ -0,0 +1,15 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package com.jeluchu
|
||||||
|
|
||||||
|
import com.jeluchu.core.configuration.initInstallers
|
||||||
|
import com.jeluchu.core.configuration.initRoutes
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.netty.*
|
||||||
|
|
||||||
|
fun main(args: Array<String>) = EngineMain.main(args)
|
||||||
|
|
||||||
|
fun Application.module() {
|
||||||
|
initInstallers()
|
||||||
|
initRoutes()
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package com.jeluchu.core.configuration
|
||||||
|
|
||||||
|
import com.jeluchu.core.messages.ErrorMessages
|
||||||
|
import com.jeluchu.core.models.ErrorResponse
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.plugins.contentnegotiation.*
|
||||||
|
import io.ktor.server.plugins.statuspages.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
|
||||||
|
fun Application.initInstallers() {
|
||||||
|
install(StatusPages) {
|
||||||
|
status(HttpStatusCode.NotFound) { call, _ ->
|
||||||
|
call.respond(HttpStatusCode.NotFound,
|
||||||
|
ErrorResponse(ErrorMessages.NotFound.message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package com.jeluchu.core.configuration
|
||||||
|
|
||||||
|
import com.mongodb.client.MongoClients
|
||||||
|
import com.mongodb.client.MongoDatabase
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.config.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Establishes connection with a MongoDB database.
|
||||||
|
*
|
||||||
|
* The following configuration properties (in application.yaml/application.conf) can be specified:
|
||||||
|
* * `db.mongo.connectionStrings` connectionStrings for your MongoDB
|
||||||
|
* * `db.mongo.database.name` name of the database
|
||||||
|
*
|
||||||
|
* IMPORTANT NOTE: in order to make MongoDB connection working, you have to start a MongoDB server first.
|
||||||
|
* See the instructions here: https://www.mongodb.com/docs/manual/administration/install-community/
|
||||||
|
* all the paramaters above
|
||||||
|
*
|
||||||
|
* @returns [MongoDatabase] instance
|
||||||
|
* */
|
||||||
|
fun Application.connectToMongoDB(): MongoDatabase {
|
||||||
|
val connectionStrings = environment.config.tryGetString("db.mongo.connectionStrings") ?: "mongodb://"
|
||||||
|
val databaseName = environment.config.tryGetString("db.mongo.database.name") ?: "aruppi"
|
||||||
|
|
||||||
|
val mongoClient = MongoClients.create(connectionStrings)
|
||||||
|
val database = mongoClient.getDatabase(databaseName)
|
||||||
|
|
||||||
|
monitor.subscribe(ApplicationStopped) {
|
||||||
|
mongoClient.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return database
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package com.jeluchu.core.configuration
|
||||||
|
|
||||||
|
import com.jeluchu.features.anime.routes.animeEndpoints
|
||||||
|
import com.mongodb.client.MongoDatabase
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
|
||||||
|
fun Application.initRoutes(
|
||||||
|
mongoDatabase: MongoDatabase = connectToMongoDB()
|
||||||
|
) = routing {
|
||||||
|
route("api/v5") {
|
||||||
|
initDocumentation()
|
||||||
|
animeEndpoints(mongoDatabase)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.jeluchu.core.configuration
|
||||||
|
|
||||||
|
import io.ktor.server.plugins.swagger.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
|
||||||
|
fun Route.initDocumentation() {
|
||||||
|
swaggerUI(
|
||||||
|
path = "/",
|
||||||
|
swaggerFile = "openapi/documentation.yaml"
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package com.jeluchu.core.messages
|
||||||
|
|
||||||
|
sealed class ErrorMessages(val message: String) {
|
||||||
|
data class Custom(val error: String) : ErrorMessages(error)
|
||||||
|
data object NotFound : ErrorMessages("Nyaaaaaaaan! This request has not been found by our alpaca-neko")
|
||||||
|
data object AnimeNotFound : ErrorMessages("This malId is not in our database")
|
||||||
|
data object InvalidMalId : ErrorMessages("The provided id of malId is invalid")
|
||||||
|
data object InvalidInput : ErrorMessages("Invalid input provided")
|
||||||
|
data object UnauthorizedMongo : ErrorMessages("Check the MongoDb Connection String to be able to correctly access this request.")
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package com.jeluchu.core.messages
|
||||||
|
|
||||||
|
sealed class GeneralMessages(val message: String) {
|
||||||
|
data class Custom(val info: String) : GeneralMessages(info)
|
||||||
|
data object DocumentationReference : GeneralMessages("More information on the Aruppi API can be found in the official documentation at: http://0.0.0.0:8080/swagger")
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.jeluchu.core.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ErrorResponse(
|
||||||
|
val error: String
|
||||||
|
)
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.jeluchu.core.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MessageResponse(
|
||||||
|
val message: String
|
||||||
|
)
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.jeluchu.core.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SuccessResponse(
|
||||||
|
val success: String
|
||||||
|
)
|
@ -0,0 +1,195 @@
|
|||||||
|
package com.jeluchu.features.anime.mappers
|
||||||
|
|
||||||
|
import com.example.models.*
|
||||||
|
import org.bson.Document
|
||||||
|
|
||||||
|
fun documentToMoreInfoEntity(doc: Document): MoreInfoEntity {
|
||||||
|
return MoreInfoEntity(
|
||||||
|
id = doc.getLong("id") ?: 0,
|
||||||
|
malId = doc.getInteger("malId", 0) ?: 0,
|
||||||
|
title = doc.getString("title") ?: "",
|
||||||
|
poster = doc.getString("poster") ?: "",
|
||||||
|
cover = doc.getString("cover") ?: "",
|
||||||
|
genres = doc.getList("genres", String::class.java) ?: emptyList(),
|
||||||
|
synopsis = doc.getString("synopsis") ?: "",
|
||||||
|
episodes = doc.getList("episodes", Document::class.java)?.map { documentToMergedEpisode(it) } ?: emptyList(),
|
||||||
|
episodesCount = doc.getInteger("episodesCount", 0) ?: 0,
|
||||||
|
score = doc.getString("score") ?: "",
|
||||||
|
staff = doc.getList("staff", Document::class.java)?.map { documentToStaff(it) } ?: emptyList(),
|
||||||
|
characters = doc.getList("characters", Document::class.java)?.map { documentToCharacter(it) } ?: emptyList(),
|
||||||
|
status = doc.getString("status") ?: "",
|
||||||
|
type = doc.getString("type") ?: "",
|
||||||
|
url = doc.getString("url") ?: "",
|
||||||
|
promo = doc.get("promo", Document::class.java)?.let { documentToVideoPromo(it) } ?: VideoPromo(),
|
||||||
|
source = doc.getString("source") ?: "",
|
||||||
|
duration = doc.getString("duration") ?: "",
|
||||||
|
rank = doc.getInteger("rank", 0) ?: 0,
|
||||||
|
titles = doc.getList("titles", Document::class.java)?.map { documentToAlternativeTitles(it) } ?: emptyList(),
|
||||||
|
airing = doc.getBoolean("airing", false) ?: false,
|
||||||
|
aired = doc.get("aired", Document::class.java)?.let { documentToAiringTime(it) } ?: AiringTime(),
|
||||||
|
broadcast = doc.get("broadcast", Document::class.java)?.let { documentToAnimeBroadcast(it) } ?: AnimeBroadcast(),
|
||||||
|
season = doc.getString("season") ?: "",
|
||||||
|
year = doc.getInteger("year", 0),
|
||||||
|
external = doc.getList("external", Document::class.java)?.map { documentToExternalLinks(it) } ?: emptyList(),
|
||||||
|
streaming = doc.getList("streaming", Document::class.java)?.map { documentToExternalLinks(it) } ?: emptyList(),
|
||||||
|
studios = doc.getList("studios", Document::class.java)?.map { documentToCompanies(it) } ?: emptyList(),
|
||||||
|
licensors = doc.getList("licensors", Document::class.java)?.map { documentToCompanies(it) } ?: emptyList(),
|
||||||
|
producers = doc.getList("producers", Document::class.java)?.map { documentToCompanies(it) } ?: emptyList(),
|
||||||
|
theme = doc.get("theme", Document::class.java)?.let { documentToThemes(it) } ?: Themes(),
|
||||||
|
relations = doc.getList("relations", Document::class.java)?.map { documentToRelated(it) } ?: emptyList(),
|
||||||
|
stats = doc.get("stats", Document::class.java)?.let { documentToStatistics(it) } ?: Statistics(),
|
||||||
|
gallery = doc.getList("gallery", Document::class.java)?.map { documentToImageMediaEntity(it) } ?: emptyList(),
|
||||||
|
episodeSource = doc.getString("episodeSource") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun documentToActor(doc: Document): Actor {
|
||||||
|
return Actor(
|
||||||
|
person = doc.get("person", Document::class.java)?.let { documentToIndividual(it) } ?: Individual(),
|
||||||
|
language = doc.getString("language") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun documentToAiringTime(doc: Document): AiringTime {
|
||||||
|
return AiringTime(
|
||||||
|
from = doc.getString("from") ?: "",
|
||||||
|
to = doc.getString("to") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun documentToAlternativeTitles(doc: Document): AlternativeTitles {
|
||||||
|
return AlternativeTitles(
|
||||||
|
title = doc.getString("title") ?: "",
|
||||||
|
type = doc.getString("type") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun documentToAnimeBroadcast(doc: Document): AnimeBroadcast {
|
||||||
|
return AnimeBroadcast(
|
||||||
|
day = doc.getString("day") ?: "",
|
||||||
|
time = doc.getString("time") ?: "",
|
||||||
|
timezone = doc.getString("timezone") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun documentToAnimeSource(doc: Document): AnimeSource {
|
||||||
|
return AnimeSource(
|
||||||
|
id = doc.getString("id") ?: "",
|
||||||
|
source = doc.getString("source") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun documentToCharacter(doc: Document): Character {
|
||||||
|
return Character(
|
||||||
|
character = doc.get("character", Document::class.java)?.let { documentToIndividual(it) } ?: Individual(),
|
||||||
|
role = doc.getString("role") ?: "",
|
||||||
|
voiceActor = doc.getList("voiceActor", Document::class.java)?.map { documentToActor(it) } ?: emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun documentToCompanies(doc: Document): Companies {
|
||||||
|
return Companies(
|
||||||
|
malId = doc.getInteger("malId", 0),
|
||||||
|
name = doc.getString("name") ?: "",
|
||||||
|
type = doc.getString("type") ?: "",
|
||||||
|
url = doc.getString("url") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun documentToExternalLinks(doc: Document): ExternalLinks {
|
||||||
|
return ExternalLinks(
|
||||||
|
url = doc.getString("url") ?: "",
|
||||||
|
name = doc.getString("name") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun documentToImageMediaEntity(doc: Document): ImageMediaEntity {
|
||||||
|
return ImageMediaEntity(
|
||||||
|
media = doc.getString("media") ?: "",
|
||||||
|
thumbnail = doc.getString("thumbnail") ?: "",
|
||||||
|
width = doc.getInteger("width", 0),
|
||||||
|
height = doc.getInteger("height", 0),
|
||||||
|
url = doc.getString("url") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun documentToImages(doc: Document): Images {
|
||||||
|
return Images(
|
||||||
|
generic = doc.getString("generic") ?: "",
|
||||||
|
small = doc.getString("small") ?: "",
|
||||||
|
medium = doc.getString("medium") ?: "",
|
||||||
|
large = doc.getString("large") ?: "",
|
||||||
|
maximum = doc.getString("maximum") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun documentToIndividual(doc: Document): Individual {
|
||||||
|
return Individual(
|
||||||
|
malId = doc.getInteger("malId", 0),
|
||||||
|
url = doc.getString("url") ?: "",
|
||||||
|
name = doc.getString("name") ?: "",
|
||||||
|
images = doc.getString("images") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun documentToMergedEpisode(doc: Document): MergedEpisode {
|
||||||
|
return MergedEpisode(
|
||||||
|
number = doc.getInteger("number", 0),
|
||||||
|
ids = doc.getList("ids", Document::class.java)?.map { documentToAnimeSource(it) }?.toMutableList() ?: mutableListOf(),
|
||||||
|
nextEpisodeDate = doc.getString("nextEpisodeDate") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun documentToRelated(doc: Document): Related {
|
||||||
|
return Related(
|
||||||
|
entry = doc.getList("entry", Document::class.java)?.map { documentToCompanies(it) } ?: emptyList(),
|
||||||
|
relation = doc.getString("relation") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun documentToScore(doc: Document): Score {
|
||||||
|
return Score(
|
||||||
|
percentage = when (val value = doc["percentage"]) {
|
||||||
|
is Double -> value
|
||||||
|
is Int -> value.toDouble()
|
||||||
|
else -> 0.0
|
||||||
|
},
|
||||||
|
score = doc.getInteger("score", 0),
|
||||||
|
votes = doc.getInteger("votes", 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun documentToStaff(doc: Document): Staff {
|
||||||
|
return Staff(
|
||||||
|
person = doc.get("person", Document::class.java)?.let { documentToIndividual(it) } ?: Individual(),
|
||||||
|
positions = doc.getList("positions", String::class.java) ?: emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun documentToStatistics(doc: Document): Statistics {
|
||||||
|
return Statistics(
|
||||||
|
completed = doc.getInteger("completed"),
|
||||||
|
dropped = doc.getInteger("dropped"),
|
||||||
|
onHold = doc.getInteger("onHold"),
|
||||||
|
planToWatch = doc.getInteger("planToWatch"),
|
||||||
|
scores = doc.getList("scores", Document::class.java)?.map { documentToScore(it) } ?: emptyList(),
|
||||||
|
total = doc.getInteger("total"),
|
||||||
|
watching = doc.getInteger("watching")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun documentToThemes(doc: Document): Themes {
|
||||||
|
return Themes(
|
||||||
|
endings = doc.getList("endings", String::class.java) ?: emptyList(),
|
||||||
|
openings = doc.getList("openings", String::class.java) ?: emptyList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun documentToVideoPromo(doc: Document): VideoPromo {
|
||||||
|
return VideoPromo(
|
||||||
|
embedUrl = doc.getString("embedUrl") ?: "",
|
||||||
|
url = doc.getString("url") ?: "",
|
||||||
|
youtubeId = doc.getString("youtubeId") ?: "",
|
||||||
|
images = doc.get("images", Document::class.java)?.let { documentToImages(it) } ?: Images()
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Actor(
|
||||||
|
/**
|
||||||
|
* Person generic info.
|
||||||
|
* @see Individual
|
||||||
|
*/
|
||||||
|
val person: Individual = Individual(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Language of the person.
|
||||||
|
*/
|
||||||
|
val language: String = ""
|
||||||
|
)
|
@ -0,0 +1,16 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AiringTime(
|
||||||
|
/**
|
||||||
|
* Start date airing.
|
||||||
|
*/
|
||||||
|
val from: String = "",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End date airing.
|
||||||
|
*/
|
||||||
|
val to: String = ""
|
||||||
|
)
|
@ -0,0 +1,16 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AlternativeTitles(
|
||||||
|
/**
|
||||||
|
* Title for anime.
|
||||||
|
*/
|
||||||
|
val title: String = "",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Title type for anime.
|
||||||
|
*/
|
||||||
|
val type: String = ""
|
||||||
|
)
|
@ -0,0 +1,21 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AnimeBroadcast(
|
||||||
|
/**
|
||||||
|
* Day in broadcast.
|
||||||
|
*/
|
||||||
|
val day: String = "",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time date in broadcast.
|
||||||
|
*/
|
||||||
|
val time: String = "",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timezone in broadcast.
|
||||||
|
*/
|
||||||
|
val timezone: String = ""
|
||||||
|
)
|
@ -0,0 +1,10 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AnimeSource(
|
||||||
|
val id: String,
|
||||||
|
val source: String
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,10 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Character(
|
||||||
|
var character: Individual = Individual(),
|
||||||
|
var role: String = "",
|
||||||
|
var voiceActor: List<Actor> = emptyList()
|
||||||
|
)
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Companies(
|
||||||
|
/**
|
||||||
|
* ID associated with MyAnimeList.
|
||||||
|
*/
|
||||||
|
val malId: Int = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name for company.
|
||||||
|
*/
|
||||||
|
val name: String = "",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type for company.
|
||||||
|
*/
|
||||||
|
val type: String = "",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Url for company.
|
||||||
|
*/
|
||||||
|
val url: String = ""
|
||||||
|
)
|
@ -0,0 +1,16 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ExternalLinks(
|
||||||
|
/**
|
||||||
|
* Url for trailer.
|
||||||
|
*/
|
||||||
|
val url: String = "",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of external info.
|
||||||
|
*/
|
||||||
|
val name: String = ""
|
||||||
|
)
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ImageMediaEntity(
|
||||||
|
val media: String,
|
||||||
|
val thumbnail: String,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
val url: String
|
||||||
|
)
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Images(
|
||||||
|
val generic: String = "",
|
||||||
|
val small: String = "",
|
||||||
|
val medium: String = "",
|
||||||
|
val large: String = "",
|
||||||
|
val maximum: String = ""
|
||||||
|
)
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Individual(
|
||||||
|
val malId: Int = 0,
|
||||||
|
val url: String = "",
|
||||||
|
val name: String = "",
|
||||||
|
val images: String = ""
|
||||||
|
)
|
@ -0,0 +1,10 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MergedEpisode(
|
||||||
|
var number: Int,
|
||||||
|
var ids: MutableList<AnimeSource> = mutableListOf(),
|
||||||
|
var nextEpisodeDate: String = ""
|
||||||
|
)
|
@ -0,0 +1,53 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.bson.Document
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MoreInfoEntity(
|
||||||
|
val id: Long? = null,
|
||||||
|
var malId: Int = 0,
|
||||||
|
var title: String = "",
|
||||||
|
var poster: String = "",
|
||||||
|
var cover: String = "",
|
||||||
|
var genres: List<String> = emptyList(),
|
||||||
|
var synopsis: String = "",
|
||||||
|
var episodes: List<MergedEpisode> = emptyList(),
|
||||||
|
var episodesCount: Int = 0,
|
||||||
|
var score: String = "",
|
||||||
|
var staff: List<Staff> = emptyList(),
|
||||||
|
var characters: List<Character> = emptyList(),
|
||||||
|
var status: String = "",
|
||||||
|
var type: String = "",
|
||||||
|
val url: String = "",
|
||||||
|
val promo: VideoPromo = VideoPromo(),
|
||||||
|
val source: String = "",
|
||||||
|
val duration: String = "",
|
||||||
|
val rank: Int = 0,
|
||||||
|
val titles: List<AlternativeTitles> = emptyList(),
|
||||||
|
val airing: Boolean = false,
|
||||||
|
val aired: AiringTime = AiringTime(),
|
||||||
|
val broadcast: AnimeBroadcast = AnimeBroadcast(),
|
||||||
|
val season: String = "",
|
||||||
|
val year: Int = 0,
|
||||||
|
val external: List<ExternalLinks> = emptyList(),
|
||||||
|
val streaming: List<ExternalLinks> = emptyList(),
|
||||||
|
val studios: List<Companies> = emptyList(),
|
||||||
|
val licensors: List<Companies> = emptyList(),
|
||||||
|
val producers: List<Companies> = emptyList(),
|
||||||
|
val theme: Themes = Themes(),
|
||||||
|
val relations: List<Related> = emptyList(),
|
||||||
|
val stats: Statistics = Statistics(),
|
||||||
|
val gallery: List<ImageMediaEntity> = emptyList(),
|
||||||
|
val episodeSource: String = ""
|
||||||
|
) {
|
||||||
|
fun toDocument(): Document = Document.parse(Json.encodeToString(this))
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
fun fromDocument(document: Document): MoreInfoEntity = json.decodeFromString(document.toJson())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Related(
|
||||||
|
/**
|
||||||
|
* List of entries for relation in anime.
|
||||||
|
* @see Companies
|
||||||
|
*/
|
||||||
|
val entry: List<Companies>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relation for anime.
|
||||||
|
*/
|
||||||
|
val relation: String
|
||||||
|
)
|
@ -0,0 +1,10 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Score(
|
||||||
|
val percentage: Double,
|
||||||
|
val score: Int,
|
||||||
|
val votes: Int
|
||||||
|
)
|
@ -0,0 +1,9 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Staff(
|
||||||
|
var person: Individual = Individual(),
|
||||||
|
var positions: List<String> = emptyList()
|
||||||
|
)
|
@ -0,0 +1,14 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Statistics(
|
||||||
|
val completed: Int? = null,
|
||||||
|
val dropped: Int? = null,
|
||||||
|
val onHold: Int? = null,
|
||||||
|
val planToWatch: Int? = null,
|
||||||
|
val scores: List<Score>? = emptyList(),
|
||||||
|
val total: Int? = null,
|
||||||
|
val watching: Int? = null
|
||||||
|
)
|
@ -0,0 +1,16 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
open class Themes(
|
||||||
|
/**
|
||||||
|
* List of endings.
|
||||||
|
*/
|
||||||
|
val endings: List<String> = emptyList(),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of openings.
|
||||||
|
*/
|
||||||
|
val openings: List<String> = emptyList()
|
||||||
|
)
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.example.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
open class VideoPromo(
|
||||||
|
/**
|
||||||
|
* Embed url for trailer.
|
||||||
|
*/
|
||||||
|
val embedUrl: String = "",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Url for trailer.
|
||||||
|
*/
|
||||||
|
val url: String = "",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Youtube id for trailer.
|
||||||
|
*/
|
||||||
|
val youtubeId: String = "",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Images for trailer.
|
||||||
|
*/
|
||||||
|
val images: Images = Images()
|
||||||
|
)
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.jeluchu.features.anime.routes
|
||||||
|
|
||||||
|
import com.jeluchu.features.anime.services.AnimeService
|
||||||
|
import com.mongodb.client.MongoDatabase
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
|
||||||
|
fun Route.animeEndpoints(
|
||||||
|
mongoDatabase: MongoDatabase,
|
||||||
|
service: AnimeService = AnimeService(mongoDatabase)
|
||||||
|
) {
|
||||||
|
get("/directory") { service.getDirectory(call) }
|
||||||
|
get("/anime/{id}") { service.getAnimeByMalId(call) }
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
package com.jeluchu.features.anime.services
|
||||||
|
|
||||||
|
import com.jeluchu.core.messages.ErrorMessages
|
||||||
|
import com.jeluchu.core.models.ErrorResponse
|
||||||
|
import com.jeluchu.features.anime.mappers.documentToMoreInfoEntity
|
||||||
|
import com.mongodb.client.MongoDatabase
|
||||||
|
import com.mongodb.client.model.Filters
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
class AnimeService(
|
||||||
|
database: MongoDatabase
|
||||||
|
) {
|
||||||
|
private val directoryCollection = database.getCollection("animedetails")
|
||||||
|
|
||||||
|
suspend fun getDirectory(
|
||||||
|
call: RoutingCall
|
||||||
|
) = withContext(Dispatchers.IO) {
|
||||||
|
call.response.headers.append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
|
||||||
|
try {
|
||||||
|
val elements = directoryCollection.find().toList()
|
||||||
|
val directory = elements.map { documentToMoreInfoEntity(it) }
|
||||||
|
val json = Json.encodeToString(directory)
|
||||||
|
call.respond(HttpStatusCode.OK, json)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
call.respond(HttpStatusCode.Unauthorized, ErrorResponse(ErrorMessages.UnauthorizedMongo.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getAnimeByMalId(
|
||||||
|
call: RoutingCall
|
||||||
|
) = withContext(Dispatchers.IO) {
|
||||||
|
call.response.headers.append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
|
||||||
|
try {
|
||||||
|
val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException(ErrorMessages.InvalidMalId.message)
|
||||||
|
directoryCollection.find(Filters.eq("malId", id)).firstOrNull()?.let { anime ->
|
||||||
|
val info = documentToMoreInfoEntity(anime)
|
||||||
|
call.respond(HttpStatusCode.OK, Json.encodeToString(info))
|
||||||
|
} ?: call.respond(HttpStatusCode.NotFound, ErrorResponse(ErrorMessages.AnimeNotFound.message))
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
call.respond(HttpStatusCode.NotFound, ErrorResponse(ErrorMessages.InvalidInput.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
|||||||
|
ktor:
|
||||||
|
application:
|
||||||
|
modules:
|
||||||
|
- com.jeluchu.ApplicationKt.module
|
||||||
|
deployment:
|
||||||
|
port: 8080
|
||||||
|
host: "0.0.0.0"
|
||||||
|
|
||||||
|
db:
|
||||||
|
mongo:
|
||||||
|
connectionStrings: ${?MONGO_CONNECTION_STRING}
|
||||||
|
database:
|
||||||
|
name: ${?MONGO_DATABASE_NAME}
|
@ -0,0 +1,36 @@
|
|||||||
|
openapi: "3.0.3"
|
||||||
|
info:
|
||||||
|
title: "Aruppi API"
|
||||||
|
description: "Application API"
|
||||||
|
version: "5.0.0-preview"
|
||||||
|
servers:
|
||||||
|
- url: "http://0.0.0.0:8080/api/v5"
|
||||||
|
paths:
|
||||||
|
/directory:
|
||||||
|
get:
|
||||||
|
description: "Hello World!"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: "OK"
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: "string"
|
||||||
|
examples:
|
||||||
|
Example#1:
|
||||||
|
value: "Hello World!"
|
||||||
|
/anime/{id}:
|
||||||
|
get:
|
||||||
|
description: "Hello World!"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: "OK"
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: "string"
|
||||||
|
examples:
|
||||||
|
Example#1:
|
||||||
|
value: "Hello World!"
|
||||||
|
components:
|
||||||
|
schemas: {}
|
Loading…
Reference in New Issue