diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6fb5710 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +build +.gradle +.git +*.md \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9bea433..772d7b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,50 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ +# System Files .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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..00a6c26 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..f3da4e7 --- /dev/null +++ b/build.gradle.kts @@ -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) +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d795d43 --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..5f8cbff --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +# Check if MONGO_CONNECTION_STRING is defined +if [ -z "$MONGO_CONNECTION_STRING" ]; then + echo "ERROR: The environment variable for the database, MONGO_CONNECTION_STRING is not defined." + exit 1 +fi + +# Verificar si MONGO_DATABASE_NAME está definido +if [ -z "$MONGO_DATABASE_NAME" ]; then + echo "WARNING: The environment variable for the database, MONGO_DATABASE_NAME, is not defined. It is using ‘mongodb’ as the default value." + MONGO_DATABASE_NAME="mongodb" +fi + + +# Generate the application.yaml file +cat < /app/application.yaml +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:-mongodb}" +EOF + +# Run the application with the specified configuration +exec java -Dconfig.file=/app/application.yaml -jar /app/app.jar \ No newline at end of file diff --git a/example.env b/example.env new file mode 100644 index 0000000..a11d9d6 --- /dev/null +++ b/example.env @@ -0,0 +1,2 @@ +MONGO_CONNECTION_STRING=mongodb://user:password@host:port +MONGO_DATABASE_NAME=aruppi \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..1875807 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +kotlin.code.style=official +org.gradle.caching=true +org.gradle.daemon=false \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..b3c9a4a --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "aruppi-api" diff --git a/src/main/kotlin/com/jeluchu/Application.kt b/src/main/kotlin/com/jeluchu/Application.kt new file mode 100644 index 0000000..2c20f74 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/Application.kt @@ -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) = EngineMain.main(args) + +fun Application.module() { + initInstallers() + initRoutes() +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/configuration/Installers.kt b/src/main/kotlin/com/jeluchu/core/configuration/Installers.kt new file mode 100644 index 0000000..99c88d5 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/configuration/Installers.kt @@ -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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/configuration/MongoDb.kt b/src/main/kotlin/com/jeluchu/core/configuration/MongoDb.kt new file mode 100644 index 0000000..641fe2d --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/configuration/MongoDb.kt @@ -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 +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/configuration/Routes.kt b/src/main/kotlin/com/jeluchu/core/configuration/Routes.kt new file mode 100644 index 0000000..aa4e381 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/configuration/Routes.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/configuration/Swagger.kt b/src/main/kotlin/com/jeluchu/core/configuration/Swagger.kt new file mode 100644 index 0000000..0ea8d2b --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/configuration/Swagger.kt @@ -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" + ) +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/messages/ErrorMessages.kt b/src/main/kotlin/com/jeluchu/core/messages/ErrorMessages.kt new file mode 100644 index 0000000..5f8d123 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/messages/ErrorMessages.kt @@ -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.") +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/messages/GeneralMessages.kt b/src/main/kotlin/com/jeluchu/core/messages/GeneralMessages.kt new file mode 100644 index 0000000..1203345 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/messages/GeneralMessages.kt @@ -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") +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/ErrorResponse.kt b/src/main/kotlin/com/jeluchu/core/models/ErrorResponse.kt new file mode 100644 index 0000000..4adeb4b --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/ErrorResponse.kt @@ -0,0 +1,8 @@ +package com.jeluchu.core.models + +import kotlinx.serialization.Serializable + +@Serializable +data class ErrorResponse( + val error: String +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/MessageResponse.kt b/src/main/kotlin/com/jeluchu/core/models/MessageResponse.kt new file mode 100644 index 0000000..f9dcb87 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/MessageResponse.kt @@ -0,0 +1,8 @@ +package com.jeluchu.core.models + +import kotlinx.serialization.Serializable + +@Serializable +data class MessageResponse( + val message: String +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/core/models/SuccessResponse.kt b/src/main/kotlin/com/jeluchu/core/models/SuccessResponse.kt new file mode 100644 index 0000000..3ef6658 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/core/models/SuccessResponse.kt @@ -0,0 +1,8 @@ +package com.jeluchu.core.models + +import kotlinx.serialization.Serializable + +@Serializable +data class SuccessResponse( + val success: String +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/mappers/AnimeMappers.kt b/src/main/kotlin/com/jeluchu/features/anime/mappers/AnimeMappers.kt new file mode 100644 index 0000000..fa658dd --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/mappers/AnimeMappers.kt @@ -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() + ) +} diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/Actor.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Actor.kt new file mode 100644 index 0000000..7ce6dea --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Actor.kt @@ -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 = "" +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/AiringTime.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/AiringTime.kt new file mode 100644 index 0000000..257395d --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/AiringTime.kt @@ -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 = "" +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/AlternativeTitles.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/AlternativeTitles.kt new file mode 100644 index 0000000..524be45 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/AlternativeTitles.kt @@ -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 = "" +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/AnimeBroadcast.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/AnimeBroadcast.kt new file mode 100644 index 0000000..1e34a8c --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/AnimeBroadcast.kt @@ -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 = "" +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/AnimeSource.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/AnimeSource.kt new file mode 100644 index 0000000..47c7ed5 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/AnimeSource.kt @@ -0,0 +1,10 @@ +package com.example.models + +import kotlinx.serialization.Serializable + +@Serializable +data class AnimeSource( + val id: String, + val source: String +) + diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/Character.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Character.kt new file mode 100644 index 0000000..9741f91 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Character.kt @@ -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 = emptyList() +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/Companies.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Companies.kt new file mode 100644 index 0000000..f6ea791 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Companies.kt @@ -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 = "" +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/ExternalLinks.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/ExternalLinks.kt new file mode 100644 index 0000000..5f1fed8 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/ExternalLinks.kt @@ -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 = "" +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/ImageMediaEntity.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/ImageMediaEntity.kt new file mode 100644 index 0000000..e845b62 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/ImageMediaEntity.kt @@ -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 + ) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/Images.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Images.kt new file mode 100644 index 0000000..4e99f1d --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Images.kt @@ -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 = "" +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/Individual.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Individual.kt new file mode 100644 index 0000000..22e3132 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Individual.kt @@ -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 = "" +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/MergedEpisode.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/MergedEpisode.kt new file mode 100644 index 0000000..8e672ad --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/MergedEpisode.kt @@ -0,0 +1,10 @@ +package com.example.models + +import kotlinx.serialization.Serializable + +@Serializable +data class MergedEpisode( + var number: Int, + var ids: MutableList = mutableListOf(), + var nextEpisodeDate: String = "" +) diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/MoreInfoEntity.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/MoreInfoEntity.kt new file mode 100644 index 0000000..606f501 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/MoreInfoEntity.kt @@ -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 = emptyList(), + var synopsis: String = "", + var episodes: List = emptyList(), + var episodesCount: Int = 0, + var score: String = "", + var staff: List = emptyList(), + var characters: List = 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 = emptyList(), + val airing: Boolean = false, + val aired: AiringTime = AiringTime(), + val broadcast: AnimeBroadcast = AnimeBroadcast(), + val season: String = "", + val year: Int = 0, + val external: List = emptyList(), + val streaming: List = emptyList(), + val studios: List = emptyList(), + val licensors: List = emptyList(), + val producers: List = emptyList(), + val theme: Themes = Themes(), + val relations: List = emptyList(), + val stats: Statistics = Statistics(), + val gallery: List = 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()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/Related.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Related.kt new file mode 100644 index 0000000..d3ced56 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Related.kt @@ -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, + + /** + * Relation for anime. + */ + val relation: String +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/Score.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Score.kt new file mode 100644 index 0000000..03209bf --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Score.kt @@ -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 + ) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/Staff.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Staff.kt new file mode 100644 index 0000000..eddcf84 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Staff.kt @@ -0,0 +1,9 @@ +package com.example.models + +import kotlinx.serialization.Serializable + +@Serializable +data class Staff( + var person: Individual = Individual(), + var positions: List = emptyList() +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/Statistics.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Statistics.kt new file mode 100644 index 0000000..e026a62 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Statistics.kt @@ -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? = emptyList(), + val total: Int? = null, + val watching: Int? = null +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/Themes.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Themes.kt new file mode 100644 index 0000000..c29df06 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/Themes.kt @@ -0,0 +1,16 @@ +package com.example.models + +import kotlinx.serialization.Serializable + +@Serializable +open class Themes( + /** + * List of endings. + */ + val endings: List = emptyList(), + + /** + * List of openings. + */ + val openings: List = emptyList() +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/models/anime/VideoPromo.kt b/src/main/kotlin/com/jeluchu/features/anime/models/anime/VideoPromo.kt new file mode 100644 index 0000000..0715092 --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/models/anime/VideoPromo.kt @@ -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() +) \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/routes/AnimeRoutes.kt b/src/main/kotlin/com/jeluchu/features/anime/routes/AnimeRoutes.kt new file mode 100644 index 0000000..dcc4f5d --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/routes/AnimeRoutes.kt @@ -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) } +} \ No newline at end of file diff --git a/src/main/kotlin/com/jeluchu/features/anime/services/AnimeService.kt b/src/main/kotlin/com/jeluchu/features/anime/services/AnimeService.kt new file mode 100644 index 0000000..281729f --- /dev/null +++ b/src/main/kotlin/com/jeluchu/features/anime/services/AnimeService.kt @@ -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)) + } + } +} + diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..baf055d --- /dev/null +++ b/src/main/resources/application.yaml @@ -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} \ No newline at end of file diff --git a/src/main/resources/openapi/documentation.yaml b/src/main/resources/openapi/documentation.yaml new file mode 100644 index 0000000..15a01f1 --- /dev/null +++ b/src/main/resources/openapi/documentation.yaml @@ -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: {} \ No newline at end of file