Compare commits

...

No commits in common. 'v5.0.1' and 'v4' have entirely different histories.
v5.0.1 ... v4

@ -1,4 +0,0 @@
build
.gradle
.git
*.md

@ -0,0 +1,6 @@
PORT_LISTEN=5000
DATABASE_HOST=localhost
DATABASE_PORT=27017
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=aruppiTime

@ -0,0 +1,32 @@
module.exports = {
env: {
browser: true,
commonjs: true,
node: true,
},
extends: ['prettier', 'eslint:recommended'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
jsx: false,
},
},
plugins: ['@typescript-eslint/eslint-plugin'],
rules: {
'no-underscore-dangle': 'off',
'class-methods-use-this': 'off',
camelcase: 'off',
'no-unused-vars': 'warn',
'no-undef': 'warn',
},
settings: {
'import/resolver': {
node: {
extensions: ['.ts'],
typescript: {},
},
},
},
};

@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

@ -1,109 +0,0 @@
# This wrorkflow checkouts the latest src code, builds a docker image and
# push it to DockerHun and GitHub Container Registry
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Publish Docker image
on:
push:
#branches: [ v5 ]
tags:
- '[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+'
#paths:
# - 'src/*'
# - '!src/main/resources/*'
workflow_dispatch:
jobs:
build-and-push:
name: Build Docker image and push to multiple registries
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
attestations: write
id-token: write
steps:
# Step 1: Source code checkout
- name: Source code checkout
uses: actions/checkout@v4
#- name: Gradle cache
# uses: actions/setup-java@v4
# with:
# distribution: 'corretto'
# java-version: '21'
# cache: 'gradle'
#
#- name: Setup Gradle
# uses: gradle/actions/setup-gradle@v4
#
#- name: Gradle build
# run: ./gradlew build --no-daemon
# Step 2: Configure Docker Buildx
- name: Configure Docker Buildx
uses: docker/setup-buildx-action@v3
# Step 2.5: Setup QEMU
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# Step 3: Log in to Docker Hub
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Step 4: GitHub Container Registry login
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_TOKEN }}
# Step 5: Configure image tags
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ secrets.DOCKERHUB_USERNAME }}/aruppi-api
ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
type=sha
# Step 6: Build and publish to Docker Hub and GitHub Container Registry
- name: Build and publish
id: push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
#platforms: linux/amd64,linux/arm64
platforms: linux/amd64
cache-from: type=gha
cache-to: type=gha,mode=max
#github-token: ${{ secrets.GHCR_TOKEN }}
## Step 7: Generate artifact attestation
#- name: Generate artifact attestation
# uses: actions/attest-build-provenance@v1
# with:
# subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
# subject-digest: ${{ steps.push.outputs.digest }}
# push-to-registry: true

@ -0,0 +1,24 @@
name: Node.js CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [8.x, 10.x, 12.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run build --if-present
- run: npm test
env:
CI: true

222
.gitignore vendored

@ -1,49 +1,193 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# System Files
.DS_Store
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Environment Variables
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# 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
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
*.iml
*.ipr
# IntelliJ
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
### VS Code ###
.vscode/
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
gradlew
# exclude apidocs
animeflv-docs/
gradlew.bat
.idea/

@ -0,0 +1,5 @@
module.exports = {
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
};

@ -1,29 +0,0 @@
# 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/aruppi-api-all.jar /app/app.jar
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Jéluchu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -1,121 +1,104 @@
# **Aruppi API** (v5.0.0) (Preview release)
# **Aruppi API** (v4.2.2)
> This API has everything about Japan, from anime, music, radio, images, videos ... to japanese culture
>
> These are the services used for the Aruppi App (only available in Spanish language)
![node version](https://img.shields.io/badge/node->=12.17.x-brightgreen.svg)
![npm version](https://img.shields.io/badge/npm->=6.14.x-brightgreen.svg)
![yarn version](https://img.shields.io/badge/yarn->=1.22.x-brightgreen.svg)
![type code](https://img.shields.io/badge/aruppi-API-brightgreen.svg)
![maintenance](https://img.shields.io/badge/maintained-Yes-brightgreen.svg)
![now](https://badgen.net/badge/icon/now?icon=now&label)
![gitrepo](https://img.shields.io/github/stars/aruppi/aruppi-api?style=social)
![Aruppi API Banner](https://raw.githubusercontent.com/aruppi/aruppi-api/v5/assets/cover.png)
Aruppi API has been developed to bring together all the information about Japanese culture, from anime and manga to the most secret places in Japan, its gastronomy and even its festivities.
We are in continuous development to implement more features and improvements in the functioning of it, to later implement it in the mobile application.
# Developer usage
#
## Prerequisites
<img src="./assets/img/cover.png" width="100%" alt="">
- **JDK 21** or higher installed.
- **Gradle** installed (although Ktor can use the Gradle wrapper).
- **Git** for cloning the repository.
- **Docker Compose** installed on your local machine.
- For an easy installation, consider using **Docker Desktop**, which includes Docker Compose. Download it from [Docker Desktop](https://www.docker.com/products/docker-desktop/).
- A **server** or cloud service to deploy the application (e.g., Heroku, AWS, DigitalOcean).
&nbsp;
&nbsp;
&nbsp;
## Install IntelliJ IDEA Community Edition
&nbsp;
&nbsp;
&nbsp;
Download and install **IntelliJ IDEA Community Edition** from the official JetBrains website: [IntelliJ IDEA Community Edition](https://www.jetbrains.com/idea/download)
Aruppi API has been developed to bring together all the information about Japanese culture, from anime and manga to the most secret places in Japan, its gastronomy and even its festivities.
**Instructions:**
We are in continuous development to implement more features and improvements in the functioning of it, to later implement it in the mobile application.
1. Visit the [IntelliJ IDEA download page](https://www.jetbrains.com/idea/download).
2. Click on the **Download** button under the **Community** edition.
3. Once downloaded, open the installer and follow the on-screen instructions to complete the installation.
## **:wrench: Developer usage**
### **Set up project**
#### Clone project
Before cloning the repo **be sure** you have installed:
```bash
git clone https://github.com/aruppi/aruppi-api.git
cd aruppi-api
```
- [**NODE**](https://www.google.com/search?q=how+to+install+node) (version >= 12.17.x)
#### Configure the Environment
In your package manager you can use either **yarn** or **npm**,
you need to have installed these versions:
Create a `.env` file or set the necessary environment variables for your application, such as:
- [**NPM**](https://www.google.com/search?q=how+to+install+npm) (version >= 6.14.x)
- [**YARN**](https://www.google.com/search?q=how+to+install+yarn) (version >= 1.22.x)
```
MONGO_CONNECTION_STRING=mongodb://user:password@host:port
MONGO_DATABASE_NAME=aruppi
```
Then:
- Choose a folder project in your system and switch in `cd [folder path]`
- Clone the repo in your folder path `git clone https://github.com/aruppi/aruppi-api`.
You will be able to find an `example.env` file in the project, simply rename the file by removing example and edit the variables
Here are the steps to get started with the project on both platforms, use the corresponding commands if **npm** or **yarn**.
---
### Build the Project
### **Installation**
#### Build the Services
In order to install the project and all dependencies, enter in the project folder and run `npm install` or you can do the same with yarn with `yarn` in the project
Run the following command in the root directory of your project to build the Docker images:
---
### Start the project
```bash
docker-compose build
npm start
```
#### Start the Services
Start the application and the database using Docker Compose:
```bash
docker-compose up -d
yarn start
```
Explanation:
- `up`: Creates and starts the containers.
- `-d`: Runs the containers in detached mode.
#### Verify the Services are Running
To check if the services are running correctly, use:
### Build the Project
```bash
docker-compose ps
npm build
```
You should see output similar to:
| NAME | IMAGE | COMMAND | SERVICE | CREATED | STATUS | PORTS |
|----------|----------------|----------------------|---------|----------------|--------------|------------------------|
| ktor_app | aruppi-api-app | "/app/entrypoint.sh" | app | 19 seconds ago | Up 2 seconds | 0.0.0.0:8080->8080/tcp |
```bash
yarn build
```
Additionally, you can view the logs to ensure the application has started without errors:
### Test the project
```bash
docker-compose logs -f app
npm test
```
## API Documentation
The API documentation is included within the API. If you want to:
```bash
yarn test
```
- **Consult the endpoints**
- **Perform tests**
- **See the types of responses**
## 📖 API Documentation
Simply navigate to [http://0.0.0.0:8080/api/v5/](http://0.0.0.0:8080/api/v5/) to view the complete documentation.
**Documentation coming soon** at the following link: [**Aruppi Wiki**](https://github.com/aruppi/aruppi-api/wiki)
Where we will show more information about the calls and queries together with the response obtained or the different types of variables in some of the queries.
## Countdown to deprecation of v3 API
Aruppi API version 4.x.x has been deprecated, that's why all of you who are using it should migrate as soon as possible to version 5.x.x which we have already released.
Aruppi API version 3.x.x has been deprecated, that's why all of you who are using it should migrate as soon as possible to version 4.x.x which we have already released.
Otherwise, if you want to use older versions you can host them yourself on your servers, and download the code in the corresponding branches of [**v2.x.x**](https://github.com/aruppi/aruppi-api/tree/v2) and [**v3.x.x**](https://github.com/aruppi/aruppi-api/tree/v3). In case you want to use an even lower version of the API we recommend you to have a look at this other version [**v1.x.x**](https://github.com/aruppi/aruppi-api-v1)
Currently the Aruppi app on Android is already using the v5.x.x API services from version v3.0.0, which you can download from our website: [**Download Aruppi App**](https://aruppi.jeluchu.com/download)
Currently the Aruppi app on Android is already using the v4.x.x API services from version v2.0.8, which you can download from our website: [**Download Aruppi App**](https://aruppi.jeluchu.com/download)
### **📚 Projects that use the API**
@ -151,6 +134,8 @@ Here are the **main contributors to the API**, along with Aruppi's creator
<table>
<tr>
<td align="center"><a href="https://github.com/Jeluchu"><img src="https://avatars.githubusercontent.com/u/32357592?v=4" width="100px;" alt=""/><br /><sub><b>Jéluchu</b></sub></a><br /><a href="https://www.instagram.com/jeluchu/" title="Instagram">📸</a> <a href="https://about.jeluchu.com/" title="About Jelu">🌍</a> <a href="https://twitter.com/Jeluchu" title="Twitter">📢</a><a href="https://www.linkedin.com/in/jesusmariacalderon/" title="LinkedIn">🔍</a></td>
<td align="center"><a href="https://github.com/capitanwesler"><img src="https://avatars.githubusercontent.com/u/61250854?v=4" width="100px;" alt=""/><br /><sub><b>Guillermo</b></sub></a><br/><a href="https://www.facebook.com/profile.php?id=100009163736196" title="Facebook">👀</a> <a href="mailto:guillermo.campanudo@hotmail.com" title="E-mail">📧</a> <a href="https://www.linkedin.com/in/guillermo-campanudo/" title="LinkedIn">🔍</a></td>
<td align="center"><a href="https://github.com/Darkangeel-hd"><img src="https://i.pinimg.com/564x/24/73/c0/2473c02e2ac93f617a28b2b5058bb41d.jpg" width="100px;" alt=""/><br /><sub><b>Darkangeel-hd</b></sub></a><br /><a href="https://i.pinimg.com/originals/19/41/22/1941222eaee4de7d08dc21cc3993e791.jpg" title="Let's All Love lain!">👀</a></td>
</tr>
</table>
@ -160,8 +145,6 @@ There are also **people who have made contributions** and therefore it is also i
<tr>
<td align="center"><a href="https://github.com/Fmaldonado6"><img src="https://avatars.githubusercontent.com/u/28517542?v=4" width="100px;" alt=""/><br /><sub><b>Fmaldonado6</b></sub></a><br/></td>
<td align="center"><a href="https://github.com/HernanSsj"><img src="https://avatars.githubusercontent.com/u/41026227?v=4" width="100px;" alt=""/><br /><sub><b>HernanSsj</b></sub></a><br/></td>
<td align="center"><a href="https://github.com/capitanwesler"><img src="https://avatars.githubusercontent.com/u/61250854?v=4" width="100px;" alt=""/><br /><sub><b>Guillermo</b></sub></a><br/></td>
<td align="center"><a href="https://github.com/Darkangeel-hd"><img src="https://i.pinimg.com/564x/24/73/c0/2473c02e2ac93f617a28b2b5058bb41d.jpg" width="100px;" alt=""/><br /><sub><b>Darkangeel-hd</b></sub></a><br /></td>
</tr>
</table>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

@ -1,33 +0,0 @@
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)
}

@ -1,18 +0,0 @@
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

@ -1,36 +0,0 @@
#!/bin/sh
# Check if MONGO_CONNECTION_STRING is defined
if [ -z "$MONGO_CONNECTION_STRING" ]; then
>&2 echo -e "ERROR: The environment variable MONGO_CONNECTION_STRING is not defined.\n\tYou must provide a valid Connection String"
exit 1
fi
# Verificar si MONGO_DATABASE_NAME está definido
if [ -z "$MONGO_DATABASE_NAME" ]; then
>&2 echo -e "WARNING: The environment variable for the database, MONGO_DATABASE_NAME, is not defined.\n\tUsing aruppi as default value."
MONGO_DATABASE_NAME="aruppi"
fi
# Generate the application.yaml file
cat <<EOF > /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
cd /app/
# Run the application with the specified configuration
exec java -Dconfig.file=/app/application.yaml -jar /app/app.jar

@ -1,2 +0,0 @@
MONGO_CONNECTION_STRING=mongodb://user:password@host:port
MONGO_DATABASE_NAME=aruppi

@ -0,0 +1,4 @@
#! /bin/bash
git log --oneline --all --graph --decorate $(git reflog | awk '{print $1}')

@ -1,3 +0,0 @@
kotlin.code.style=official
org.gradle.caching=true
org.gradle.daemon=false

@ -1,26 +0,0 @@
[versions]
kotlin-version = "2.0.21"
ktor-version = "3.0.1"
logback-version = "1.4.14"
mongo-version = "4.10.2"
[libraries]
ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor-version" }
ktor-server-swagger = { module = "io.ktor:ktor-server-swagger-jvm", version.ref = "ktor-version" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json-jvm", version.ref = "ktor-version" }
ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation-jvm", version.ref = "ktor-version" }
mongodb-driver-core = { module = "org.mongodb:mongodb-driver-core", version.ref = "mongo-version" }
mongodb-driver-sync = { module = "org.mongodb:mongodb-driver-sync", version.ref = "mongo-version" }
bson = { module = "org.mongodb:bson", version.ref = "mongo-version" }
ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor-version" }
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback-version" }
ktor-server-config-yaml = { module = "io.ktor:ktor-server-config-yaml-jvm", version.ref = "ktor-version" }
ktor-server-test-host = { module = "io.ktor:ktor-server-test-host-jvm", version.ref = "ktor-version" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin-version" }
ktor-server-status-pages = { module = "io.ktor:ktor-server-status-pages", version.ref = "kotlin-version" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-version" }
ktor = { id = "io.ktor.plugin", version.ref = "ktor-version" }
kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "2.0.21" }

Binary file not shown.

@ -1,5 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

@ -0,0 +1,13 @@
{
"name": "aruppi",
"version": 2,
"builds": [
{
"src": "./src/index.js",
"use": "@now/node"
}
],
"routes": [
{ "src": "/(.*)", "dest": "/src/index.js" }
]
}

@ -0,0 +1,84 @@
{
"name": "aruppi",
"version": "4.2.2",
"description": "Aruppi is a custom API to obtain data from the Japanese culture for the mobile app",
"main": "./src/api/api.ts",
"scripts": {
"start": "node ./dist/server.js",
"dev": "tsnd --transpile-only --ignore-watch node_modules --respawn src/server.ts",
"build": "tsc -p ."
},
"keywords": [
"aruppi",
"aruppi api",
"aruppi app",
"aruppi android"
],
"author": {
"name": "Jéluchu",
"email": "infoaruppi@gmail.com",
"reason": "Android Developer and Others",
"url": "https://jeluchu.github.io/",
"social": {
"github": "https://github.com/Jeluchu",
"twitter": "https://twitter.com/Jeluchu"
}
},
"contributors": [
{
"name": "Darkangeel",
"url": "https://github.com/Darkangeel-hd",
"reason": "System administration authority (SYSADM)"
},
{
"name": "Capitanwesler",
"url": "https://github.com/capitanwesler",
"reason": "Backend and Frontend developer"
}
],
"engines": {
"node": ">= 12.x",
"npm": ">= 6.14.x"
},
"bugs": {
"url": "https://github.com/aruppi/aruppi-api/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/aruppi/aruppi-api.git"
},
"license": "MIT",
"dependencies": {
"body-parser": "^1.19.0",
"cheerio": "^1.0.0-rc.6",
"compose-middleware": "^5.0.1",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.3",
"got": "^11.8.5",
"helmet": "^4.5.0",
"mongodb": "^3.6.6",
"mongoose": "^5.13.15",
"redis": "^3.1.2",
"rss-parser": "^3.12.0",
"tough-cookie": "^4.0.0",
"ts-node-dev": "^1.1.1"
},
"devDependencies": {
"@types/cheerio": "^0.22.28",
"@types/cors": "^2.8.10",
"@types/express": "^4.17.11",
"@types/node": "^15.0.2",
"@types/redis": "^2.8.28",
"@types/tough-cookie": "^4.0.0",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"eslint": "^7.24.0",
"eslint-config-prettier": "^8.2.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-prettier": "^3.4.0",
"prettier": "^2.2.1",
"ts-node": "^9.1.1",
"typescript": "^4.2.4"
}
}

@ -1 +0,0 @@
rootProject.name = "aruppi-api"

@ -0,0 +1,782 @@
import { NextFunction, Request, Response } from 'express';
import { requestGot } from '../utils/requestCall';
import {
imageUrlToBase64,
jkanimeInfo,
monoschinosInfo,
tioanimeInfo,
videoServersJK,
videoServersMonosChinos,
videoServersTioAnime,
} from '../utils/util';
import { transformUrlServer } from '../utils/transformerUrl';
import AnimeModel, { Anime as ModelA } from '../database/models/anime.model';
import util from 'util';
import { hashStringMd5 } from '../utils/util';
import {
animeExtraInfo,
getAnimeVideoPromo,
getAnimeCharacters,
getRelatedAnimesMAL,
} from '../utils/util';
import urls from '../utils/urls';
import { redisClient } from '../database/connection';
// @ts-ignore
redisClient.get = util.promisify(redisClient.get);
/*
AnimeController - a class to manage the schedule,
top, all the animes, return the last episodes
with async to return promises.
*/
interface Schedule {
title: string;
mal_id: number;
image_url: any;
}
interface Anime {
index: string;
animeId: string;
title: string;
id: string;
type: string;
}
interface Top {
rank: string;
title: string;
url: string;
image_url: string;
type: string;
subtype: string;
page: number;
score: string;
}
interface Episode {
id: string;
title: string;
image: string;
episode: number;
servers: { id: string; url: string; direct: boolean };
}
interface Movie {
id: string;
title: string;
type: string;
page: string;
banner: string;
image: string;
synopsis: string;
status: string;
rate: string;
genres: string[];
episodes: object[];
}
export default class AnimeController {
async schedule(req: Request, res: Response, next: NextFunction) {
const { day } = req.params;
let info: any;
try {
if (redisClient.connected) {
const resultQueryRedis: any = await redisClient.get(
`schedule_${hashStringMd5(day)}`,
);
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
info = await requestGot(`${urls.BASE_JIKAN}schedules?filter=${day}`, {
parse: true,
scrapy: false,
});
} catch (err) {
return next(err);
}
const animeList: Schedule[] = info.data.map((item: any) => ({
title: item.titles.find((x: { type: string; }) => x.type === "Default").title,
malid: item.mal_id,
image: item.images.jpg.image_url,
}));
if (animeList.length > 0) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
redisClient.set(
`schedule_${hashStringMd5(day)}`,
JSON.stringify({ day: animeList }),
);
/* After 6hrs expire the key. */
redisClient.expire(
`schedule_${hashStringMd5(day)}`,
+ 21600,
);
}
res.status(200).json({
day: animeList,
});
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async top(req: Request, res: Response, next: NextFunction) {
const { type, subtype, page } = req.params;
let info: any;
try {
if (redisClient.connected) {
let resultQueryRedis: any;
if (subtype) {
resultQueryRedis = await redisClient.get(
`top_${hashStringMd5(`${type}:${subtype}:${page}`)}`,
);
} else {
resultQueryRedis = await redisClient.get(
`top_${hashStringMd5(`${type}:${page}`)}`,
);
}
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
if (subtype !== undefined) {
info = await requestGot(
`${urls.BASE_JIKAN}top/${type}?filter=${subtype}&page=${page}`,
{ parse: true, scrapy: false },
);
} else {
info = await requestGot(`${urls.BASE_JIKAN}top/${type}?page=${page}`, {
parse: true,
scrapy: false,
});
}
} catch (err) {
return next(err);
}
const top: Top[] = info.data.map((item: any, index: number) => ({
// A little hacky way to fix null ranks
rank: item.rank || index + 1 + (info.pagination.current_page-1)*info.pagination.items.per_page,
title: item.titles.find((x: { type: string; }) => x.type === "Default").title,
url: item.url,
image_url: item.images.jpg.image_url,
type: type,
subtype: subtype,
page: page,
score: item.score,
}));
if (top.length > 0) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
if (subtype) {
redisClient.set(
`top_${hashStringMd5(`${type}:${subtype}:${page}`)}`,
JSON.stringify({ top }),
);
} else {
redisClient.set(
`top_${hashStringMd5(`${type}:${page}`)}`,
JSON.stringify({ top }),
);
}
/* After 24hrs expire the key. */
if (subtype) {
redisClient.expireat(
`top_${hashStringMd5(`${type}:${subtype}:${page}`)}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
} else {
redisClient.expireat(
`top_${hashStringMd5(`${type}:${page}`)}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
}
}
return res.status(200).json({ top });
} else {
return res.status(400).json({ message: 'Aruppi lost in the shell' });
}
}
async getAllAnimes(req: Request, res: Response, next: NextFunction) {
let data: any;
try {
data = await requestGot(`${urls.BASE_ANIMEFLV}api/animes/list`, {
parse: true,
scrapy: false,
spoof: true,
});
} catch (err) {
return next(err);
}
const animes: Anime[] = data.map((item: any) => ({
index: item[0],
animeId: item[3],
title: item[1],
id: item[2],
type: item[4],
}));
if (animes.length > 0) {
res.status(200).send({ animes });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getLastEpisodes(req: Request, res: Response, next: NextFunction) {
let lastEpisodes;
let episodes: Episode[] = [];
let animeList: any[] = [];
try {
if (redisClient.connected) {
const resultQueryRedis: any = await redisClient.get(
`lastEpisodes_${hashStringMd5('lastEpisodes')}`,
);
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
lastEpisodes = await requestGot(`${urls.BASE_ARUPPI_MONOSCHINOS}lastest`, {
scrapy: false,
parse: true,
});
} catch (err) {
return next(err);
}
for (const anime of lastEpisodes) {
animeList.push({
id: `ver/${anime.id}`,
title: anime.title,
image: anime.image,
episode: anime.no,
});
}
for (const anime of animeList) {
episodes.push({
id: anime.id,
title: anime.title,
image: await imageUrlToBase64(anime.image),
episode: anime.episode,
servers: await videoServersMonosChinos(anime.id),
});
}
if (episodes.length > 0) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
redisClient.set(
`lastEpisodes_${hashStringMd5('lastEpisodes')}`,
JSON.stringify({ episodes }),
);
/* After 24hrs expire the key. */
redisClient.expireat(
`lastEpisodes_${hashStringMd5('lastEpisodes')}`,
parseInt(`${+new Date() / 1000}`, 10) + 1800,
);
}
res.status(200).json({
episodes,
});
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getContentTv(req: Request, res: Response, next: NextFunction) {
const { type, page } = req.params;
const url = 'tv';
let data: any;
try {
if (redisClient.connected) {
const resultQueryRedis: any = await redisClient.get(
`contentTv_${hashStringMd5(`${type}:${page}`)}`,
);
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
data = await requestGot(
`${urls.BASE_ANIMEFLV_JELU}${
url.charAt(0).toUpperCase() + url.slice(1)
}/${type}/${page}`,
{
parse: true,
scrapy: false,
},
);
} catch (err) {
return next(err);
}
const animes: Movie[] = data[url].map((item: any) => {
return {
id: item.id,
title: item.title,
type: url,
page: page,
banner: item.banner,
image: item.poster,
synopsis: item.synopsis,
status: item.debut,
rate: item.rating,
genres: item.genres.map((genre: any) => genre),
episodes: item.episodes.map((episode: any) => episode),
};
});
if (animes.length > 0) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
redisClient.set(
`contentTv_${hashStringMd5(`${type}:${page}`)}`,
JSON.stringify({ animes }),
);
/* After 24hrs expire the key. */
redisClient.expireat(
`contentTv_${hashStringMd5(`${type}:${page}`)}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
}
res.status(200).json({
animes,
});
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getContentSpecial(req: Request, res: Response, next: NextFunction) {
const { type, page } = req.params;
const url = 'special';
let data: any;
try {
if (redisClient.connected) {
const resultQueryRedis: any = await redisClient.get(
`contentSpecial_${hashStringMd5(`${type}:${page}`)}`,
);
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
data = await requestGot(
`${urls.BASE_ANIMEFLV_JELU}${
url.charAt(0).toUpperCase() + url.slice(1)
}/${type}/${page}`,
{
parse: true,
scrapy: false,
},
);
} catch (err) {
return next(err);
}
const animes: Movie[] = data[url].map((item: any) => {
return {
id: item.id,
title: item.title,
type: url,
page: page,
banner: item.banner,
image: item.poster,
synopsis: item.synopsis,
status: item.debut,
rate: item.rating,
genres: item.genres.map((genre: any) => genre),
episodes: item.episodes.map((episode: any) => episode),
};
});
if (animes.length > 0) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
redisClient.set(
`contentSpecial_${hashStringMd5(`${type}:${page}`)}`,
JSON.stringify({ animes }),
);
/* After 24hrs expire the key. */
redisClient.expireat(
`contentSpecial_${hashStringMd5(`${type}:${page}`)}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
}
res.status(200).json({
animes,
});
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getContentOva(req: Request, res: Response, next: NextFunction) {
const { type, page } = req.params;
const url = 'ova';
let data: any;
try {
if (redisClient.connected) {
const resultQueryRedis: any = await redisClient.get(
`contentOva_${hashStringMd5(`${type}:${page}`)}`,
);
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
data = await requestGot(
`${urls.BASE_ANIMEFLV_JELU}${
url.charAt(0).toUpperCase() + url.slice(1)
}/${type}/${page}`,
{
parse: true,
scrapy: false,
},
);
} catch (err) {
return next(err);
}
const animes: Movie[] = data[url].map((item: any) => {
return {
id: item.id,
title: item.title,
type: url,
page: page,
banner: item.banner,
image: item.poster,
synopsis: item.synopsis,
status: item.debut,
rate: item.rating,
genres: item.genres.map((genre: any) => genre),
episodes: item.episodes.map((episode: any) => episode),
};
});
if (animes.length > 0) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
redisClient.set(
`contentOva_${hashStringMd5(`${type}:${page}`)}`,
JSON.stringify({ animes }),
);
/* After 24hrs expire the key. */
redisClient.expireat(
`contentOva_${hashStringMd5(`${type}:${page}`)}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
}
res.status(200).json({
animes,
});
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getContentMovie(req: Request, res: Response, next: NextFunction) {
const { type, page } = req.params;
const url = 'movies';
let data: any;
try {
if (redisClient.connected) {
const resultQueryRedis: any = await redisClient.get(
`contentMovie_${hashStringMd5(`${type}:${page}`)}`,
);
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
data = await requestGot(
`${urls.BASE_ANIMEFLV_JELU}${
url.charAt(0).toUpperCase() + url.slice(1)
}/${type}/${page}`,
{
parse: true,
scrapy: false,
},
);
} catch (err) {
return next(err);
}
const animes: Movie[] = data[url].map((item: any) => {
return {
id: item.id,
title: item.title,
type: url,
page: page,
banner: item.banner,
image: item.poster,
synopsis: item.synopsis,
status: item.debut,
rate: item.rating,
genres: item.genres.map((genre: any) => genre),
episodes: item.episodes.map((episode: any) => episode),
};
});
if (animes.length > 0) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
redisClient.set(
`contentMovie_${hashStringMd5(`${type}:${page}`)}`,
JSON.stringify({ animes }),
);
/* After 24hrs expire the key. */
redisClient.expireat(
`contentMovie_${hashStringMd5(`${type}:${page}`)}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
}
res.status(200).json({
animes,
});
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getEpisodes(req: Request, res: Response, next: NextFunction) {
const { title } = req.params;
let searchAnime: ModelA | null;
let episodes: any;
try {
if (redisClient.connected) {
const resultQueryRedis: any = await redisClient.get(
`episodes_${hashStringMd5(title)}`,
);
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
searchAnime = await AnimeModel.findOne({
$or: [{ title: { $eq: title } }, { title: { $eq: `${title} (TV)` } }],
});
} catch (err) {
return next(err);
}
switch (searchAnime?.source) {
case 'jkanime':
episodes = await jkanimeInfo(searchAnime?.id, searchAnime?.mal_id);
break;
case 'monoschinos':
episodes = await monoschinosInfo(searchAnime?.id, searchAnime?.mal_id);
break;
case 'tioanime':
episodes = await tioanimeInfo(searchAnime?.id, searchAnime?.mal_id);
break;
default:
episodes = undefined;
break;
}
if (episodes) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
redisClient.set(
`episodes_${hashStringMd5(title)}`,
JSON.stringify({ episodes }),
);
/* After 24hrs expire the key. */
redisClient.expireat(
`episodes_${hashStringMd5(title)}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
}
res.status(200).json({ episodes });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getServers(req: Request, res: Response, next: NextFunction) {
const { id } = req.params;
let data: any;
try {
if (redisClient.connected) {
const resultQueryRedis: any = await redisClient.get(
`servers_${hashStringMd5(id)}`,
);
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
let indicator = false;
if (id.split('/')[0] === 'ver' && !indicator) {
data = await videoServersTioAnime(id);
if (!data.name) {
indicator = true;
}
}
if (id.split('/')[0] === 'ver' && !indicator) {
data = await videoServersMonosChinos(id);
if (!data.name) {
console.log(data.name);
indicator = true;
}
}
if (!indicator) {
data = undefined;
indicator = true;
/*
This part is just for handling the error
if the two above doesn't complete the operation
does not make sense to have the getServers from
JKAnime.
*/
}
if (data) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
redisClient.set(
`servers_${hashStringMd5(id)}`,
JSON.stringify({ servers: data }),
);
/* After 24hrs expire the key. */
redisClient.expireat(
`servers_${hashStringMd5(id)}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
}
res.status(200).json({ servers: data });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
} catch (err) {
return next(err);
}
}
async getRandomAnime(req: Request, res: Response, next: NextFunction) {
let animeQuery: ModelA[] | null;
let animeResult: any;
try {
animeQuery = await AnimeModel.aggregate([{ $sample: { size: 1 } }]);
} catch (err) {
return next(err);
}
animeResult = {
title: animeQuery[0].title || null,
poster: animeQuery[0].poster || null,
synopsis: animeQuery[0].description || null,
type: animeQuery[0].type || null,
rating: animeQuery[0].score || null,
genres: animeQuery[0].genres || null,
moreInfo: [await animeExtraInfo(animeQuery[0].mal_id)],
promo: await getAnimeVideoPromo(animeQuery[0].mal_id),
characters: await getAnimeCharacters(animeQuery[0].mal_id),
related: await getRelatedAnimesMAL(animeQuery[0].mal_id),
};
if (animeResult) {
res.set('Cache-Control', 'no-store');
res.status(200).json(animeResult);
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
}

@ -0,0 +1,478 @@
import { NextFunction, Request, Response } from 'express';
import { requestGot } from '../utils/requestCall';
import AnimeModel, { Anime } from '../database/models/anime.model';
import util from 'util';
import { hashStringMd5 } from '../utils/util';
import {
animeExtraInfo,
getAnimeVideoPromo,
getAnimeCharacters,
getRelatedAnimesMAL,
} from '../utils/util';
import urls from '../utils/urls';
import { redisClient } from '../database/connection';
// @ts-ignore
redisClient.get = util.promisify(redisClient.get);
/*
DirectoryController - async functions controlling the directory
in the database of MongoDB, functions like getAllDirectory from the DB
other functions with realation to the directory, like the season and stuff.
*/
interface TypeAnime {
title: string;
image: string;
genres: string[];
}
interface Season {
title: string;
image: string;
malink: string;
}
interface Archive {
year: string;
seasons: string[];
}
export default class DirectoryController {
async getAllDirectory(req: Request, res: Response, next: NextFunction) {
const { genres } = req.params;
try {
if (genres === 'sfw') {
await AnimeModel.find(
{
genres: { $nin: ['ecchi', 'Ecchi'] },
},
(err: any, docs: Anime[]) => {
let directory: any[] = [];
for (const item of docs) {
directory.push({
id: item.id,
title: item.title,
mal_id: item.mal_id,
poster: item.poster,
type: item.type,
genres: item.genres,
score: item.score,
source: item.source,
description: item.description,
});
}
if (directory.length > 0) {
res.status(200).json({ directory });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
},
);
} else {
await AnimeModel.find((err: any, docs: Anime[]) => {
let directory: any[] = [];
for (const item of docs) {
directory.push({
id: item.id,
title: item.title,
mal_id: item.mal_id,
poster: item.poster,
type: item.type,
genres: item.genres,
score: item.score,
source: item.source,
description: item.description,
});
}
if (directory.length > 0) {
res.status(200).json({ directory });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
});
}
} catch (err) {
return next(err);
}
}
async getSeason(req: Request, res: Response, next: NextFunction) {
const { year, type } = req.params;
let info: any;
try {
if (redisClient.connected) {
const resultQueryRedis: any = await redisClient.get(
`season_${hashStringMd5(`${year}:${type}`)}`,
);
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
info = await requestGot(`${urls.BASE_JIKAN}seasons/${year}/${type}`, {
scrapy: false,
parse: true,
});
} catch (err) {
return next(err);
}
const season: TypeAnime[] = info.data.map((item: any) => {
return {
title: item.titles.find((x: { type: string; }) => x.type === "Default").title,
image: item.images.jpg.image_url,
genres: item.genres.map((genre: any) => genre.name),
};
});
if (season.length > 0) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
redisClient.set(
`season_${hashStringMd5(`${year}:${type}`)}`,
JSON.stringify({ season }),
);
/* After 24hrs expire the key. */
redisClient.expireat(
`season_${hashStringMd5(`${year}:${type}`)}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
}
res.status(200).json({
season,
});
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async allSeasons(req: Request, res: Response, next: NextFunction) {
let info: any;
try {
if (redisClient.connected) {
const resultQueryRedis: any = await redisClient.get(
`allSeasons_${hashStringMd5('allSeasons')}`,
);
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
info = await requestGot(`${urls.BASE_JIKAN}seasons`, {
parse: true,
scrapy: false,
});
} catch (err) {
return next(err);
}
const archive: Archive[] = info.data.map((item: any) => {
return {
year: item.year,
seasons: item.seasons,
};
});
if (archive.length > 0) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
redisClient.set(
`allSeasons_${hashStringMd5('allSeasons')}`,
JSON.stringify({ archive }),
);
/* After 24hrs expire the key. */
redisClient.expireat(
`allSeasons_${hashStringMd5('allSeasons')}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
}
res.status(200).json({ archive });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async laterSeasons(req: Request, res: Response, next: NextFunction) {
let info: any;
try {
if (redisClient.connected) {
const resultQueryRedis: any = await redisClient.get(
`laterSeasons_${hashStringMd5('laterSeasons')}`,
);
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
info = await requestGot(`${urls.BASE_JIKAN}seasons/upcoming`, {
parse: true,
scrapy: false,
});
} catch (err) {
return next(err);
}
const future: Season[] = info.data.map((item: any) => {
return {
title: item.titles.find((x: { type: string; }) => x.type === "Default").title,
image: item.images.jpg.image_url,
malink: item.url,
};
});
if (future.length > 0) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
redisClient.set(
`laterSeasons_${hashStringMd5('laterSeasons')}`,
JSON.stringify({ future }),
);
/* After 24hrs expire the key. */
redisClient.expireat(
`laterSeasons_${hashStringMd5('laterSeasons')}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
}
res.status(200).json({ future });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getMoreInfo(req: Request, res: Response, next: NextFunction) {
const { title } = req.params;
let resultQuery: Anime | null;
let resultAnime: any;
try {
if (redisClient.connected) {
const resultQueryRedis: any = await redisClient.get(
`moreInfo_${hashStringMd5(title)}`,
);
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
resultQuery = await AnimeModel.findOne({
$or: [{ title: { $eq: title } }, { title: { $eq: `${title} (TV)` } }],
});
const extraInfo: any = await animeExtraInfo(resultQuery!.mal_id);
resultAnime = {
//aruppi_key: hashStringMd5(title),
title: resultQuery?.title,
poster: resultQuery?.poster,
synopsis: resultQuery?.description,
status: !extraInfo.aired.to ? 'En emisión' : 'Finalizado',
type: resultQuery?.type,
rating: resultQuery?.score,
genres: resultQuery?.genres,
moreInfo: [extraInfo],
promo: await getAnimeVideoPromo(resultQuery!.mal_id),
characters: await getAnimeCharacters(resultQuery!.mal_id),
related: await getRelatedAnimesMAL(resultQuery!.mal_id),
};
} catch (err) {
return next(err);
}
if (resultAnime) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
redisClient.set(
`moreInfo_${hashStringMd5(title)}`,
JSON.stringify(resultAnime),
);
/* After 24hrs expire the key. */
redisClient.expireat(
`moreInfo_${hashStringMd5(title)}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
}
res.status(200).json(resultAnime);
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async search(req: Request, res: Response, next: NextFunction) {
const { title } = req.params;
let results: Anime[] | null;
try {
results = await AnimeModel.find({
title: { $regex: new RegExp(title, 'i') },
});
} catch (err) {
return next(err);
}
const resultAnimes: any[] = results.map((item: any) => {
return {
id: item.id,
title: item.title,
type: item.type,
image: item.poster,
};
});
if (resultAnimes.length > 0) {
res.status(200).json({ search: resultAnimes });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getAnimeGenres(req: Request, res: Response, next: NextFunction) {
const { genre, order, page } = req.params;
let result: any;
const genres: any = {
accion: 'Acción',
'artes-marciales': 'Artes Marciales',
aventura: 'Aventuras',
carreras: 'Carreras',
'ciencia-ficcion': 'Ciencia Ficción',
comedia: 'Comedia',
demencia: 'Demencia',
demonios: 'Demonios',
deportes: 'Deportes',
drama: 'Drama',
ecchi: 'Ecchi',
escolares: 'Escolares',
espacial: 'Espacial',
fantasia: 'Fantasía',
harem: 'Harem',
historico: 'Historico',
infantil: 'Infantil',
josei: 'Josei',
juegos: 'Juegos',
magia: 'Magia',
mecha: 'Mecha',
militar: 'Militar',
misterio: 'Misterio',
musica: 'Música',
parodia: 'Parodia',
policia: 'Policía',
psicologico: 'Psicológico',
'recuentos-de-la-vida': 'Recuentos de la vida',
romance: 'Romance',
samurai: 'Samurai',
seinen: 'Seinen',
shoujo: 'Shoujo',
shounen: 'Shounen',
sobrenatural: 'Sobrenatural',
superpoderes: 'Superpoderes',
suspenso: 'Suspenso',
terror: 'Terror',
vampiros: 'Vampiros',
yaoi: 'Yaoi',
yuri: 'Yuri',
};
try {
if (genre === undefined && order === undefined && page === undefined) {
result = await AnimeModel.aggregate([{ $sample: { size: 25 } }]);
} else {
// eslint-disable-next-line no-prototype-builtins
if (genres.hasOwnProperty(genre)) {
if (page !== undefined && parseInt(page) > 1) {
if (order === 'asc') {
result = await AnimeModel.find({ genres: genres[genre] })
.limit(25)
.skip(25 * parseInt(page))
.sort({ title: 'ascending' });
} else if (order === 'desc') {
result = await AnimeModel.find({ genres: genres[genre] })
.limit(25)
.skip(25 * parseInt(page))
.sort({ title: 'descending' });
} else {
result = await AnimeModel.find({ genres: genres[genre] })
.limit(25)
.skip(25 * parseInt(page));
}
} else {
if (order === 'asc') {
result = await AnimeModel.find({ genres: genres[genre] })
.limit(25)
.sort({ title: 'ascending' });
} else if (order === 'desc') {
result = await AnimeModel.find({ genres: genres[genre] })
.limit(25)
.sort({ title: 'descending' });
} else {
result = await AnimeModel.find({ genres: genres[genre] }).limit(
25,
);
}
}
} else {
return res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
} catch (err) {
return next(err);
}
const animes: any[] = result.map((item: any) => {
return {
id: item.id,
title: item.title.trim(),
mention: genre,
page: page,
poster: item.poster,
banner: item.banner,
synopsis: item.synopsis,
type: item.type,
rating: item.rating,
genre: item.genre,
};
});
if (animes.length > 0) {
res.status(200).json({ animes });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
}

@ -0,0 +1,818 @@
import { NextFunction, Request, Response } from 'express';
import Parser from 'rss-parser';
import urls from '../utils/urls';
import { obtainPreviewNews } from '../utils/obtainPreviews';
import { requestGot } from '../utils/requestCall';
import RadioStationModel, {
RadioStation,
} from '../database/models/radiostation.model';
import ThemeModel, { Theme } from '../database/models/theme.model';
import ThemeParser from '../utils/animeTheme';
import { structureThemes } from '../utils/util';
import { getThemes } from '../utils/util';
import WaifuModel, { Waifu } from '../database/models/waifu.model';
import util from 'util';
import { hashStringMd5 } from '../utils/util';
import { redisClient } from '../database/connection';
// @ts-ignore
redisClient.get = util.promisify(redisClient.get);
/*
UtilsController - controller to parse the
feed and get news, all with scraping and
parsing RSS.
*/
const themeParser = new ThemeParser();
type CustomFeed = {
foo: string;
};
type CustomItem = {
bar: number;
itunes: { duration: string; image: string };
'content:encoded': string;
'content:encodedSnippet': string;
};
const parser: Parser<CustomFeed, CustomItem> = new Parser({
customFields: {
feed: ['foo'],
item: ['bar'],
},
});
interface News {
title?: string;
url?: string;
author?: string;
thumbnail?: string;
content?: string;
}
interface Podcast {
title?: string;
duration?: string;
created?: Date | string;
mp3?: string;
}
interface rssPage {
url: string;
author: string;
content: string;
}
export default class UtilsController {
async getAnitakume(req: Request, res: Response, next: NextFunction) {
let feed: CustomFeed & Parser.Output<CustomItem>;
try {
if (redisClient.connected) {
const resultQueryRedis: any = await redisClient.get(
`anitakume_${hashStringMd5('anitakume')}`,
);
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
feed = await parser.parseURL(urls.BASE_IVOOX);
} catch (err) {
return next(err);
}
const podcast: Podcast[] = [];
const monthNames = [
'Enero',
'Febrero',
'Marzo',
'Abril',
'Mayo',
'Junio',
'Julio',
'Agosto',
'Septiembre',
'Octubre',
'Noviembre',
'Diciembre',
];
feed.items.forEach((item: any) => {
const date: Date = new Date(item.pubDate!);
const formattedObject: Podcast = {
title: item.title,
duration: item.itunes.duration,
created: `${date.getDate()} de ${
monthNames[date.getMonth()]
} de ${date.getFullYear()}`,
mp3: item.enclosure?.url,
};
podcast.push(formattedObject);
});
if (podcast.length > 0) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
redisClient.set(
`anitakume_${hashStringMd5('anitakume')}`,
JSON.stringify({ podcast }),
);
/* After 24hrs expire the key. */
redisClient.expireat(
`anitakume_${hashStringMd5('anitakume')}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
}
res.status(200).json({ podcast });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getNews(req: Request, res: Response, next: NextFunction) {
const news: News[] = [];
const pagesRss: rssPage[] = [
{ url: urls.BASE_KUDASAI, author: 'Kudasai', content: 'content_encoded' },
{
url: urls.BASE_RAMENPARADOS,
author: 'Ramen para dos',
content: 'content',
},
{
url: urls.BASE_CRUNCHYROLL,
author: 'Crunchyroll',
content: 'content_encoded',
},
];
try {
if (redisClient.connected) {
const resultQueryRedis: any = await redisClient.get(
`news_${hashStringMd5('news')}`,
);
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
for (const rssPage of pagesRss) {
const feed = await parser.parseURL(rssPage.url);
feed.items.forEach((item: any) => {
const formattedObject: News = {
title: item.title,
url: item.link,
author: feed.title?.includes('Crunchyroll')
? 'Crunchyroll'
: feed.title,
thumbnail: obtainPreviewNews(item['content:encoded']),
content: item['content:encoded'],
};
news.push(formattedObject);
});
}
} catch (err) {
return next(err);
}
if (news.length > 0) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
redisClient.set(
`news_${hashStringMd5('news')}`,
JSON.stringify({ news }),
);
/* After 24hrs expire the key. */
redisClient.expireat(
`news_${hashStringMd5('news')}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
}
res.status(200).json({ news });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getImages(req: Request, res: Response, next: NextFunction) {
const { title } = req.params;
let data: any;
try {
if (redisClient.connected) {
const resultQueryRedis: any = await redisClient.get(
`images_${hashStringMd5(title)}`,
);
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
data = await requestGot(
`${urls.BASE_QWANT}t=images&q=${encodeURIComponent(
title,
)}&count=51&locale=es_ES&safesearch=1`,
{ scrapy: false, parse: true, spoof: true, },
);
} catch (err) {
return next(err);
}
const results: any[] = data.data.result.items.map((item: any) => {
return {
type: item.thumb_type,
thumbnail: `${item.thumbnail}`,
fullsize: `${item.media_fullsize}`,
};
});
if (results.length > 0) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
redisClient.set(
`images_${hashStringMd5(title)}`,
JSON.stringify({ images: results }),
);
/* After 24hrs expire the key. */
redisClient.expireat(
`images_${hashStringMd5(title)}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
}
res.set('Cache-Control', 'max-age=604800');
res.status(200).json({ images: results });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getVideos(req: Request, res: Response, next: NextFunction) {
const { channelId } = req.params;
let data: any;
try {
if (redisClient.connected) {
const resultQueryRedis: any = await redisClient.get(
`videos_${hashStringMd5(channelId)}`,
);
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
data = await requestGot(
`${urls.BASE_YOUTUBE}${channelId}&part=snippet,id&order=date&maxResults=50`,
{ scrapy: false, parse: true },
);
} catch (err) {
return next(err);
}
const results: any[] = data.items.map((item: any) => {
return {
title: item.snippet.title,
videoId: item.id.videoId,
thumbDefault: item.snippet.thumbnails.default.url,
thumbMedium: item.snippet.thumbnails.medium.url,
thumbHigh: item.snippet.thumbnails.high.url,
};
});
if (results.length > 0) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
redisClient.set(
`videos_${hashStringMd5(channelId)}`,
JSON.stringify({ videos: results }),
);
/* After 24hrs expire the key. */
redisClient.expireat(
`videos_${hashStringMd5(channelId)}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
}
res.status(200).json({ videos: results });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getPlaylists(req: Request, res: Response, next: NextFunction) {
const { playlistId } = req.params;
let data: any;
try {
if (redisClient.connected) {
const resultQueryRedis: any = redisClient.get(
`videos_${hashStringMd5(playlistId)}`,
);
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
data = await requestGot(
`${urls.BASE_YOUTUBE_PLAYLIST}${playlistId}`,
{ scrapy: false, parse: true },
);
} catch (err) {
return next(err);
}
const results: any[] = data.items.map((item: any) => {
return {
title: item.snippet.title,
videoId: item.id.videoId,
thumbDefault: item.snippet.thumbnails.default.url,
thumbMedium: item.snippet.thumbnails.medium.url,
thumbHigh: item.snippet.thumbnails.high.url,
};
});
if (results.length > 0) {
if (redisClient.connected) {
/!* Set the key in the redis cache. *!/
redisClient.set(
`videos_${hashStringMd5(playlistId)}`,
JSON.stringify({ videos: results }),
);
/!* After 24hrs expire the key. *!/
redisClient.expireat(
`videos_${hashStringMd5(playlistId)}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
}
res.status(200).json({ videos: results });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getSectionVideos(req: Request, res: Response, next: NextFunction) {
const { type } = req.params;
let y1: any, y2: any, y3: any;
let data: any;
try {
if (type === 'learn') {
data = await requestGot(
`${urls.BASE_YOUTUBE}UCCyQwSS6m2mVB0-H2FOFJtw&part=snippet,id&order=date&maxResults=50`,
{ parse: true, scrapy: false },
);
} else if (type === 'amv') {
y1 = await requestGot(
`${urls.BASE_YOUTUBE}UCkTFkshjAsLMKwhAe1uPC1A&part=snippet,id&order=date&maxResults=25`,
{ parse: true, scrapy: false },
);
y2 = await requestGot(
`${urls.BASE_YOUTUBE}UC2cpvlLeowpqnR6bQofwNew&part=snippet,id&order=date&maxResults=25`,
{ parse: true, scrapy: false },
);
} else if (type === 'produccer') {
y1 = await requestGot(
`${urls.BASE_YOUTUBE}UC-5MT-BUxTzkPTWMediyV0w&part=snippet,id&order=date&maxResults=25`,
{ parse: true, scrapy: false },
);
y2 = await requestGot(
`${urls.BASE_YOUTUBE}UCwUeTOXP3DD9DIvHttowuSA&part=snippet,id&order=date&maxResults=25`,
{ parse: true, scrapy: false },
);
y3 = await requestGot(
`${urls.BASE_YOUTUBE}UCA8Vj7nN8bzT3rsukD2ypUg&part=snippet,id&order=date&maxResults=25`,
{ parse: true, scrapy: false },
);
}
} catch (err) {
return next(err);
}
if (data && !y1 && !y2 && !y3) {
const results: any[] = data.items.map((item: any) => ({
title: item.snippet.title,
videoId: item.id.videoId,
thumbDefault: item.snippet.thumbnails.default.url,
thumbMedium: item.snippet.thumbnails.medium.url,
thumbHigh: item.snippet.thumbnails.high.url,
}));
res.status(200).json({ videos: results });
} else if (!data && y1 && y2 && !y3) {
const results: any[] = y1.items.concat(y2.items).map((item: any) => ({
title: item.snippet.title,
videoId: item.id.videoId,
thumbDefault: item.snippet.thumbnails.default.url,
thumbMedium: item.snippet.thumbnails.medium.url,
thumbHigh: item.snippet.thumbnails.high.url,
}));
res.status(200).json({ videos: results });
} else if (!data && y1 && y2 && y3) {
const results: any[] = y1.items
.concat(y2.items.concat(y3.items))
.map((item: any) => ({
title: item.snippet.title,
videoId: item.id.videoId,
thumbDefault: item.snippet.thumbnails.default.url,
thumbMedium: item.snippet.thumbnails.medium.url,
thumbHigh: item.snippet.thumbnails.high.url,
}));
res.status(200).json({ videos: results });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getRadioStations(req: Request, res: Response, next: NextFunction) {
let data: RadioStation[];
try {
data = await RadioStationModel.find();
} catch (err) {
return next(err);
}
const results: any[] = data.map((item: RadioStation) => {
return {
name: item.name,
url: item.url,
};
});
if (results.length > 0) {
res.status(200).json({ stations: results });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getAllThemes(req: Request, res: Response, next: NextFunction) {
let data: Theme[];
try {
data = await ThemeModel.find();
} catch (err) {
return next(err);
}
const results: any[] = data.map((item: Theme) => {
return {
id: item.id,
title: item.title,
year: item.year,
themes: item.themes,
};
});
if (results.length > 0) {
res.status(200).json({ themes: results });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getOpAndEd(req: Request, res: Response, next: NextFunction) {
const { title } = req.params;
let themes: any;
try {
if (redisClient.connected) {
const resultQueryRedis: any = await redisClient.get(
`oped_${hashStringMd5(title)}`,
);
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
themes = await structureThemes(await themeParser.serie(title), true);
} catch (err) {
return next(err);
}
if (themes) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
redisClient.set(
`oped_${hashStringMd5(title)}`,
JSON.stringify({ themes }),
);
/* After 24hrs expire the key. */
redisClient.expireat(
`oped_${hashStringMd5(title)}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
}
res.status(200).json({ themes });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getThemesYear(req: Request, res: Response, next: NextFunction) {
const { year } = req.params;
let themes: any;
let resultQueryRedis: any;
try {
if (redisClient.connected) {
if (year) {
resultQueryRedis = await redisClient.get(
`themesyear_${hashStringMd5(year)}`,
);
} else {
resultQueryRedis = await redisClient.get(
`themesyear_${hashStringMd5('allYear')}`,
);
}
if (resultQueryRedis) {
const resultRedis: any = JSON.parse(resultQueryRedis);
return res.status(200).json(resultRedis);
}
}
if (year === undefined) {
themes = await themeParser.allYears();
} else {
themes = await structureThemes(await themeParser.year(year), false);
}
} catch (err) {
return next(err);
}
if (themes.length > 0) {
if (redisClient.connected) {
/* Set the key in the redis cache. */
if (year) {
redisClient.set(
`themesyear_${hashStringMd5(year)}`,
JSON.stringify({ themes }),
);
} else {
redisClient.set(
`themesyear_${hashStringMd5('allYear')}`,
JSON.stringify({ themes }),
);
}
/* After 24hrs expire the key. */
if (year) {
redisClient.expireat(
`themesyear_${hashStringMd5(year)}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
} else {
redisClient.expireat(
`themesyear_${hashStringMd5('allYear')}`,
parseInt(`${+new Date() / 1000}`, 10) + 7200,
);
}
}
res.status(200).json({ themes });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async randomTheme(req: Request, res: Response, next: NextFunction) {
let data: any;
try {
data = await requestGot(`${urls.BASE_THEMEMOE}roulette`, {
parse: true,
scrapy: false,
spoof: true,
});
} catch (err) {
return next(err);
}
const random: any[] = getThemes(data.themes);
if (random.length > 0) {
res.set('Cache-Control', 'no-store');
res.status(200).json({ random });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getArtist(req: Request, res: Response, next: NextFunction) {
const { id } = req.params;
let artists: any;
try {
if (id === undefined) {
artists = await themeParser.artists();
} else {
artists = await structureThemes(await themeParser.artist(id), false);
}
} catch (err) {
return next(err);
}
if (artists.length > 0) {
res.status(200).json({ artists });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getDestAnimePlatforms(req: Request, res: Response, next: NextFunction) {
let data: any;
try {
data = await requestGot(
`${urls.BASE_ARUPPI}res/documents/animelegal/top.json`,
{ parse: true, scrapy: false },
);
} catch (err) {
return next(err);
}
const destPlatforms: any[] = data.map((item: any) => {
return {
id: item.id,
name: item.name,
logo: item.logo,
link: item.link,
};
});
if (destPlatforms.length > 0) {
res.status(200).json({ destPlatforms });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getPlatforms(req: Request, res: Response, next: NextFunction) {
const { id } = req.params;
let data: any;
try {
if (id === undefined) {
data = await requestGot(
`${urls.BASE_ARUPPI}res/documents/animelegal/typeplatforms.json`,
{ parse: true, scrapy: false },
);
} else if (
id === 'producers' ||
id === 'apps' ||
id === 'publishers' ||
'events'
) {
data = await requestGot(
`${urls.BASE_ARUPPI}res/documents/animelegal/type/${id}.json`,
{ parse: true, scrapy: false },
);
} else {
data = await requestGot(
`${urls.BASE_ARUPPI}res/documents/animelegal/type/${id}.json`,
{ parse: true, scrapy: false },
);
}
} catch (err) {
return next(err);
}
const platforms: any[] = data.map((item: any) => {
if (id === undefined) {
return {
id: item.id,
name: item.name,
comming: item.comming || false,
cover: item.cover,
};
} else if (
id === 'producers' ||
id === 'apps' ||
id === 'publishers' ||
'events'
) {
return {
id: item.id,
name: item.name,
logo: item.logo,
cover: item.cover,
description: item.description,
type: item.type,
moreInfo: item.moreInfo,
facebook: item.facebook,
twitter: item.twitter,
instagram: item.instagram,
webInfo: item.webInfo,
webpage: item.webpage,
};
} else {
return {
id: item.id,
name: item.name,
type: item.type,
logo: item.logo,
cover: item.cover,
webpage: item.webpage,
};
}
});
if (platforms.length > 0) {
res.status(200).json({ platforms });
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
async getWaifuRandom(req: Request, res: Response, next: NextFunction) {
let waifuQuery: Waifu[] | null;
let waifuResult: any;
try {
waifuQuery = await WaifuModel.aggregate([{ $sample: { size: 1 } }]);
} catch (err) {
return next(err);
}
if (waifuQuery.length > 0) {
waifuResult = {
id: waifuQuery[0].id,
name: waifuQuery[0].name,
weight: waifuQuery[0].weight,
series: waifuQuery[0].series,
height: waifuQuery[0].height,
birthday: waifuQuery[0].birthday,
likes: waifuQuery[0].likes,
trash: waifuQuery[0].trash,
blood_type: waifuQuery[0].blood_type,
hip: waifuQuery[0].hip,
bust: waifuQuery[0].bust,
description: waifuQuery[0].description,
display_picture: waifuQuery[0].display_picture,
waist: waifuQuery[0].waist,
};
}
if (waifuResult) {
res.set('Cache-Control', 'no-store');
res.status(200).json(waifuResult);
} else {
res.status(500).json({ message: 'Aruppi lost in the shell' });
}
}
}

@ -0,0 +1,69 @@
import mongoose from 'mongoose';
import redis, { RedisClient } from 'redis';
import dotenv from 'dotenv';
// Configuring dotenv to read the variable from .env file
dotenv.config();
/*
Create the connection to the database
of mongodb.
*/
export const createConnectionMongo = (databaseObj: {
port: string | undefined;
host: string | undefined;
}) => {
mongoose.connect(
`mongodb://${databaseObj.host}:${databaseObj.port}/anime-directory`,
{
useNewUrlParser: true,
useUnifiedTopology: true,
},
);
mongoose.connection.on('error', err => {
console.log('err', err);
});
mongoose.connection.on('connected', (err, res) => {
console.log('Database connected: mongoose.');
});
};
/*
Create the connection to the cache of
redis, and exporting the redis client
with the call of this file.
*/
export const redisClient: RedisClient = redis.createClient({
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT!),
password: process.env.REDIS_PASSWORD,
retry_strategy: function (options) {
if (options.error && options.error.code === 'ECONNREFUSED') {
// End reconnecting on a specific error and flush all commands with
// a individual error
return new Error('The server refused the connection');
}
if (options.total_retry_time > 1000 * 60 * 60) {
// End reconnecting after a specific timeout and flush all commands
// with a individual error
return new Error('Retry time exhausted');
}
if (options.attempt > 10) {
// End reconnecting with built in error
return undefined;
}
// reconnect after
return Math.min(options.attempt * 100, 3000);
},
});
redisClient.on('connect', () => {
console.log('Redis connected: redis.');
});
redisClient.on('error', function (err) {
console.log('Redis error: ' + err);
});

@ -0,0 +1,33 @@
import { Document, model, Types, Schema } from 'mongoose';
/*
This is the model for each anime
of the directory, the anime model.
*/
export interface Anime extends Document {
id: string;
title: string;
mal_id: number;
poster: string;
type: string;
genres: Types.Array<string>;
score: string;
source: string;
description: string;
}
// Schema for the anime
const AnimeSchema: Schema = new Schema({
id: { type: String },
title: { type: String },
mal_id: { type: Number },
poster: { type: String },
type: { type: String },
genres: [{ type: String }],
score: { type: String },
source: { type: String },
description: { type: String },
});
export default model<Anime>('Anime', AnimeSchema);

@ -0,0 +1,19 @@
import { Document, model, Types, Schema } from 'mongoose';
/*
This is the model for each genre
of the directory, the genre model.
*/
export interface Genre extends Document {
name: string;
value: string;
}
// Schema for the theme
const GenreSchema: Schema = new Schema({
name: { type: String },
value: { type: String },
});
export default model<Genre>('Genre', GenreSchema);

@ -0,0 +1,19 @@
import { Document, model, Schema } from 'mongoose';
/*
This is the model for each radiostation
of the directory, the radiostation model.
*/
export interface RadioStation extends Document {
name: string;
url: string;
}
// Schema for the theme
const RadioStationSchema: Schema = new Schema({
name: { type: String },
url: { type: String },
});
export default model<RadioStation>('RadioStation', RadioStationSchema);

@ -0,0 +1,29 @@
import { Document, model, Types, Schema } from 'mongoose';
/*
This is the model for each theme
of the directory, the theme model.
*/
interface TInterface {
title: string;
video: string;
type: string;
}
export interface Theme extends Document {
id: string;
title: string;
year: string;
themes: Types.Array<TInterface>;
}
// Schema for the theme
const ThemeSchema: Schema = new Schema({
id: { type: String },
title: { type: String },
year: { type: String },
themes: [{ type: Object }],
});
export default model<Theme>('Theme', ThemeSchema);

@ -0,0 +1,43 @@
import { Document, model, Schema } from 'mongoose';
/*
This is the model for each anime
of the directory, the anime model.
*/
export interface Waifu extends Document {
id: string;
name: string;
weight: string;
series: object;
height: string;
birthday: string;
likes: number;
trash: number;
blood_type: string;
hip: string;
bust: string;
description: string;
display_picture: string;
waist: string;
}
// Schema for the Waifu
const WaifuSchema: Schema = new Schema({
id: { type: String },
name: { type: String },
weight: { type: String },
series: { type: Object },
height: { type: String },
birthday: { type: String },
likes: { type: Number },
trash: { type: Number },
blood_type: { type: String },
hip: { type: String },
bust: { type: String },
description: { type: String },
display_picture: { type: String },
waist: { type: String },
});
export default model<Waifu>('Waifu', WaifuSchema);

@ -1,15 +0,0 @@
@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()
}

@ -1,24 +0,0 @@
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()
}
}

@ -1,33 +0,0 @@
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
}

@ -1,15 +0,0 @@
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)
}
}

@ -1,11 +0,0 @@
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"
)
}

@ -1,115 +0,0 @@
package com.jeluchu.core.extensions
import org.bson.Document
import java.text.SimpleDateFormat
import java.util.*
fun Document.getStringSafe(key: String, defaultValue: String = ""): String {
return try {
when (val value = this[key]) {
is String -> value
is Number, is Boolean -> value.toString()
else -> defaultValue
}
} catch (e: Exception) {
defaultValue
}
}
fun Document.getIntSafe(key: String, defaultValue: Int = 0): Int {
return try {
when (val value = this[key]) {
is Int -> value
is Number -> value.toInt()
is String -> value.toIntOrNull() ?: defaultValue
else -> defaultValue
}
} catch (e: Exception) {
defaultValue
}
}
fun Document.getDoubleSafe(key: String, defaultValue: Double = 0.0): Double {
return try {
when (val value = this[key]) {
is Double -> value
is Number -> value.toDouble()
is String -> value.toDoubleOrNull() ?: defaultValue
else -> defaultValue
}
} catch (e: Exception) {
defaultValue
}
}
fun Document.getFloatSafe(key: String, defaultValue: Float = 0.0f): Float {
return try {
when (val value = this[key]) {
is Float -> value
is Number -> value.toFloat()
is String -> value.toFloatOrNull() ?: defaultValue
else -> defaultValue
}
} catch (e: Exception) {
defaultValue
}
}
fun Document.getLongSafe(key: String, defaultValue: Long = 0L): Long {
return try {
val value = this.get(key)
when (value) {
is Long -> value
is Number -> value.toLong()
is String -> value.toLongOrNull() ?: defaultValue
else -> defaultValue
}
} catch (e: Exception) {
defaultValue
}
}
fun Document.getBooleanSafe(key: String, defaultValue: Boolean = false): Boolean {
return try {
when (val value = this[key]) {
is Boolean -> value
is String -> value.toBoolean()
is Number -> value.toInt() != 0
else -> defaultValue
}
} catch (e: Exception) {
defaultValue
}
}
fun Document.getDateSafe(key: String, defaultValue: Date = Date(0)): Date {
return try {
when (val value = this[key]) {
is Date -> value
is String -> SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").parse(value) ?: defaultValue
else -> defaultValue
}
} catch (e: Exception) {
defaultValue
}
}
inline fun <reified T> Document.getListSafe(key: String, defaultValue: List<T> = emptyList()): List<T> {
return try {
when (val value = this[key]) {
is List<*> -> value.filterIsInstance<T>()
else -> defaultValue
}
} catch (e: Exception) {
defaultValue
}
}
fun Document.getDocumentSafe(key: String): Document? {
return try {
val value = this[key]
if (value is Document) value else null
} catch (e: Exception) {
null
}
}

@ -1,14 +0,0 @@
package com.jeluchu.core.extensions
import io.ktor.http.*
import io.ktor.server.routing.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
fun Route.getToJson(
path: String,
request: suspend RoutingContext.() -> Unit
): Route = get(path) {
call.response.headers.append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
withContext(Dispatchers.IO) { request() }
}

@ -1,10 +0,0 @@
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.")
}

@ -1,6 +0,0 @@
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")
}

@ -1,8 +0,0 @@
package com.jeluchu.core.models
import kotlinx.serialization.Serializable
@Serializable
data class ErrorResponse(
val error: String
)

@ -1,8 +0,0 @@
package com.jeluchu.core.models
import kotlinx.serialization.Serializable
@Serializable
data class MessageResponse(
val message: String
)

@ -1,8 +0,0 @@
package com.jeluchu.core.models
import kotlinx.serialization.Serializable
@Serializable
data class SuccessResponse(
val success: String
)

@ -1,6 +0,0 @@
package com.jeluchu.core.utils
object Routes {
const val DIRECTORY = "/directory"
const val ANIME_DETAILS = "/anime/{id}"
}

@ -1,213 +0,0 @@
package com.jeluchu.features.anime.mappers
import com.example.models.*
import com.jeluchu.core.extensions.*
import com.jeluchu.features.anime.models.directory.AnimeDirectoryEntity
import org.bson.Document
fun documentToAnimeDirectoryEntity(doc: Document) = AnimeDirectoryEntity(
rank = doc.getIntSafe("rank"),
year = doc.getIntSafe("year"),
url = doc.getStringSafe("url"),
malId = doc.getIntSafe("malId"),
type = doc.getStringSafe("type"),
score = doc.getStringSafe("score"),
title = doc.getStringSafe("title"),
status = doc.getStringSafe("status"),
season = doc.getStringSafe("season"),
poster = doc.getStringSafe("poster"),
airing = doc.getBooleanSafe("airing"),
genres = doc.getListSafe<String>("genres"),
episodesCount = doc.getIntSafe("episodesCount")
)
fun documentToMoreInfoEntity(doc: Document): MoreInfoEntity {
return MoreInfoEntity(
id = doc.getLongSafe("id"),
malId = doc.getIntSafe("malId"),
title = doc.getStringSafe("title"),
poster = doc.getStringSafe("poster"),
cover = doc.getStringSafe("cover"),
genres = doc.getListSafe<String>("genres"),
synopsis = doc.getStringSafe("synopsis"),
episodes = doc.getListSafe<Document>("episodes").map { documentToMergedEpisode(it) },
episodesCount = doc.getIntSafe("episodesCount", 0),
score = doc.getStringSafe("score"),
staff = doc.getListSafe<Document>("staff").map { documentToStaff(it) },
characters = doc.getListSafe<Document>("characters").map { documentToCharacter(it) },
status = doc.getStringSafe("status"),
type = doc.getStringSafe("type"),
url = doc.getStringSafe("url"),
promo = doc.getDocumentSafe("promo")?.let { documentToVideoPromo(it) } ?: VideoPromo(),
source = doc.getStringSafe("source"),
duration = doc.getStringSafe("duration"),
rank = doc.getIntSafe("rank", 0),
titles = doc.getListSafe<Document>("titles").map { documentToAlternativeTitles(it) },
airing = doc.getBooleanSafe("airing"),
aired = doc.getDocumentSafe("aired")?.let { documentToAiringTime(it) } ?: AiringTime(),
broadcast = doc.getDocumentSafe("broadcast")?.let { documentToAnimeBroadcast(it) } ?: AnimeBroadcast(),
season = doc.getStringSafe("season"),
year = doc.getIntSafe("year", 0),
external = doc.getListSafe<Document>("external").map { documentToExternalLinks(it) },
streaming = doc.getListSafe<Document>("streaming").map { documentToExternalLinks(it) },
studios = doc.getListSafe<Document>("studios").map { documentToCompanies(it) },
licensors = doc.getListSafe<Document>("licensors").map { documentToCompanies(it) },
producers = doc.getListSafe<Document>("producers").map { documentToCompanies(it) },
theme = doc.getDocumentSafe("theme")?.let { documentToThemes(it) } ?: Themes(),
relations = doc.getListSafe<Document>("relations").map { documentToRelated(it) },
stats = doc.getDocumentSafe("stats")?.let { documentToStatistics(it) } ?: Statistics(),
gallery = doc.getListSafe<Document>("gallery").map { documentToImageMediaEntity(it) },
episodeSource = doc.getStringSafe("episodeSource")
)
}
fun documentToActor(doc: Document): Actor {
return Actor(
person = doc.getDocumentSafe("person")?.let { documentToIndividual(it) } ?: Individual(),
language = doc.getStringSafe("language")
)
}
fun documentToAiringTime(doc: Document): AiringTime {
return AiringTime(
from = doc.getStringSafe("from"),
to = doc.getStringSafe("to")
)
}
fun documentToAlternativeTitles(doc: Document): AlternativeTitles {
return AlternativeTitles(
title = doc.getStringSafe("title"),
type = doc.getStringSafe("type")
)
}
fun documentToAnimeBroadcast(doc: Document): AnimeBroadcast {
return AnimeBroadcast(
day = doc.getStringSafe("day"),
time = doc.getStringSafe("time"),
timezone = doc.getStringSafe("timezone")
)
}
fun documentToAnimeSource(doc: Document): AnimeSource {
return AnimeSource(
id = doc.getStringSafe("id"),
source = doc.getStringSafe("source")
)
}
fun documentToCharacter(doc: Document): Character {
return Character(
character = doc.getDocumentSafe("character")?.let { documentToIndividual(it) } ?: Individual(),
role = doc.getStringSafe("role"),
voiceActor = doc.getListSafe<Document>("voiceActor").map { documentToActor(it) }
)
}
fun documentToCompanies(doc: Document): Companies {
return Companies(
malId = doc.getIntSafe("malId", 0),
name = doc.getStringSafe("name"),
type = doc.getStringSafe("type"),
url = doc.getStringSafe("url")
)
}
fun documentToExternalLinks(doc: Document): ExternalLinks {
return ExternalLinks(
url = doc.getStringSafe("url"),
name = doc.getStringSafe("name")
)
}
fun documentToImageMediaEntity(doc: Document): ImageMediaEntity {
return ImageMediaEntity(
media = doc.getStringSafe("media"),
thumbnail = doc.getStringSafe("thumbnail"),
width = doc.getIntSafe("width", 0),
height = doc.getIntSafe("height", 0),
url = doc.getStringSafe("url")
)
}
fun documentToImages(doc: Document): Images {
return Images(
generic = doc.getStringSafe("generic"),
small = doc.getStringSafe("small"),
medium = doc.getStringSafe("medium"),
large = doc.getStringSafe("large"),
maximum = doc.getStringSafe("maximum")
)
}
fun documentToIndividual(doc: Document): Individual {
return Individual(
malId = doc.getIntSafe("malId", 0),
url = doc.getStringSafe("url"),
name = doc.getStringSafe("name"),
images = doc.getStringSafe("images")
)
}
fun documentToMergedEpisode(doc: Document): MergedEpisode {
return MergedEpisode(
number = doc.getIntSafe("number", 0),
ids = doc.getListSafe<Document>("ids").map { documentToAnimeSource(it) }.toMutableList(),
nextEpisodeDate = doc.getStringSafe("nextEpisodeDate")
)
}
fun documentToRelated(doc: Document): Related {
return Related(
entry = doc.getListSafe<Document>("entry").map { documentToCompanies(it) },
relation = doc.getStringSafe("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.getIntSafe("score", 0),
votes = doc.getIntSafe("votes", 0)
)
}
fun documentToStaff(doc: Document): Staff {
return Staff(
person = doc.get("person", Document::class.java)?.let { documentToIndividual(it) } ?: Individual(),
positions = doc.getListSafe<String>("positions")
)
}
fun documentToStatistics(doc: Document): Statistics {
return Statistics(
completed = doc.getIntSafe("completed"),
dropped = doc.getIntSafe("dropped"),
onHold = doc.getIntSafe("onHold"),
planToWatch = doc.getIntSafe("planToWatch"),
scores = doc.getListSafe<Document>("scores").map { documentToScore(it) },
total = doc.getIntSafe("total"),
watching = doc.getIntSafe("watching")
)
}
fun documentToThemes(doc: Document): Themes {
return Themes(
endings = doc.getListSafe<String>("endings"),
openings = doc.getListSafe<String>("openings")
)
}
fun documentToVideoPromo(doc: Document): VideoPromo {
return VideoPromo(
embedUrl = doc.getStringSafe("embedUrl"),
url = doc.getStringSafe("url"),
youtubeId = doc.getStringSafe("youtubeId"),
images = doc.get("images", Document::class.java)?.let { documentToImages(it) } ?: Images()
)
}

@ -1,17 +0,0 @@
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 = ""
)

@ -1,16 +0,0 @@
package com.example.models
import kotlinx.serialization.Serializable
@Serializable
data class AiringTime(
/**
* Start date airing.
*/
val from: String = "",
/**
* End date airing.
*/
val to: String = ""
)

@ -1,16 +0,0 @@
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 = ""
)

@ -1,21 +0,0 @@
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 = ""
)

@ -1,10 +0,0 @@
package com.example.models
import kotlinx.serialization.Serializable
@Serializable
data class AnimeSource(
val id: String,
val source: String
)

@ -1,10 +0,0 @@
package com.example.models
import kotlinx.serialization.Serializable
@Serializable
data class Character(
var character: Individual = Individual(),
var role: String = "",
var voiceActor: List<Actor> = emptyList()
)

@ -1,26 +0,0 @@
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 = ""
)

@ -1,16 +0,0 @@
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 = ""
)

@ -1,12 +0,0 @@
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
)

@ -1,12 +0,0 @@
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 = ""
)

@ -1,11 +0,0 @@
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 = ""
)

@ -1,10 +0,0 @@
package com.example.models
import kotlinx.serialization.Serializable
@Serializable
data class MergedEpisode(
var number: Int,
var ids: MutableList<AnimeSource> = mutableListOf(),
var nextEpisodeDate: String = ""
)

@ -1,53 +0,0 @@
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())
}
}

@ -1,17 +0,0 @@
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
)

@ -1,10 +0,0 @@
package com.example.models
import kotlinx.serialization.Serializable
@Serializable
data class Score(
val percentage: Double,
val score: Int,
val votes: Int
)

@ -1,9 +0,0 @@
package com.example.models
import kotlinx.serialization.Serializable
@Serializable
data class Staff(
var person: Individual = Individual(),
var positions: List<String> = emptyList()
)

@ -1,14 +0,0 @@
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
)

@ -1,16 +0,0 @@
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()
)

@ -1,26 +0,0 @@
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()
)

@ -1,20 +0,0 @@
package com.jeluchu.features.anime.models.directory
import kotlinx.serialization.Serializable
@Serializable
data class AnimeDirectoryEntity(
val rank: Int = 0,
val year: Int = 0,
var malId: Int = 0,
val url: String = "",
var type: String = "",
var score: String = "",
var title: String = "",
var status: String = "",
val season: String = "",
var poster: String = "",
var episodesCount: Int = 0,
val airing: Boolean = false,
var genres: List<String> = emptyList()
)

@ -1,15 +0,0 @@
package com.jeluchu.features.anime.routes
import com.jeluchu.core.extensions.getToJson
import com.jeluchu.core.utils.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)
) {
getToJson(Routes.DIRECTORY) { service.getDirectory(call) }
getToJson(Routes.ANIME_DETAILS) { service.getAnimeByMalId(call) }
}

@ -1,39 +0,0 @@
package com.jeluchu.features.anime.services
import com.jeluchu.core.messages.ErrorMessages
import com.jeluchu.core.models.ErrorResponse
import com.jeluchu.features.anime.mappers.documentToAnimeDirectoryEntity
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.serialization.encodeToString
import kotlinx.serialization.json.Json
class AnimeService(
database: MongoDatabase
) {
private val directoryCollection = database.getCollection("animedetails")
suspend fun getDirectory(call: RoutingCall) = try {
val elements = directoryCollection.find().toList()
val directory = elements.map { documentToAnimeDirectoryEntity(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) = 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))
}
}

@ -1,13 +0,0 @@
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}

@ -1,11 +0,0 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date %-5level [%thread] %logger{0}: %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>

@ -1,36 +0,0 @@
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: {}

@ -0,0 +1,48 @@
import {
Request,
Response,
NextFunction,
ErrorRequestHandler,
RequestHandler,
} from 'express';
/*
Error handler and notFound handler
for all the API like a middleware with
the function next of express.
*/
export const errorHandler: ErrorRequestHandler = (
err: any,
req: Request,
res: Response,
next: NextFunction,
) => {
const statusCode = res.statusCode !== 200 ? res.statusCode : 500;
res.status(statusCode).json({
message: err.message,
stack: process.env.NODE_ENV === 'production' ? '🥞' : err.stack,
});
};
export const notFound: any = (
req: Request,
res: Response,
next: NextFunction,
) => {
res.status(404);
const error = new Error(`🔍 - Not Found - ${req.originalUrl}`);
next(error);
};
// export const requestLoggerMiddleWare: RequestHandler = (req, res, next) => {
// console.log(`${req.method} ${req.originalUrl}`);
// const start: number = new Date().getTime();
// res.on('finish', () => {
// const elapsed: number = new Date().getTime() - start;
// console.info(
// `${req.method} ${req.originalUrl} ${req.statusCode} ${elapsed}ms`,
// );
// });
// next();
// };

@ -0,0 +1,144 @@
import { Router, Request, Response, NextFunction } from 'express';
import AnimeController from './controllers/AnimeController';
import DirectoryController from './controllers/DirectoryController';
import UtilsController from './controllers/UtilsController';
const routes = Router();
const animeController = new AnimeController();
const directoryController = new DirectoryController();
const utilsController = new UtilsController();
routes.get('/', (req: Request, res: Response) => {
// We dont want to enforce the redirect storing, so just check
res.set('Cache-Control', 'no-cache,proxy-revalidate');
res.redirect('/api/v4/');
});
/*
Routes - JSON
Message with the JSON of all the routes in the
/api, parameters and some examples, how to call the
endpoints of the /api.
*/
routes.get('/api/v4/', (req: Request, res: Response) => {
res.set('Cache-Control', 'no-store');
res.json({
message: 'Aruppi /api - 🎏',
author: 'Jéluchu',
version: '4.2.2',
credits: 'The bitch loves /apis that offers data to Aruppi App',
entries: [
{
Schedule: '/api/v4/schedule/:day',
Top: '/api/v4/top/:type/:page/:subtype',
AllAnimes: '/api/v4/allAnimes',
RandomAnime: '/api/v4/randomAnime',
Anitakume: '/api/v4/anitakume',
News: '/api/v4/news',
Season: '/api/v4/season/:year/:type',
'All Seasons': '/api/v4/allSeasons',
'All Directory': '/api/v4/allDirectory/:type',
Genres: '/api/v4/getByGenres/:genre?/:order?/:page?',
'Futures Seasons': '/api/v4/laterSeasons',
LastEpisodes: '/api/v4/lastEpisodes',
Movies: '/api/v4/movies/:type/:page',
Ovas: '/api/v4/ova/:type/:page',
Specials: '/api/v4/special/:type/:page',
Tv: '/api/v4/tv/:type/:page',
MoreInfo: '/api/v4/moreInfo/:title',
GetEpisodes: '/api/v4/getEpisodes/:title',
GetAnimeServers: '/api/v4/getAnimeServers/:id',
Search: '/api/v4/search/:title',
Images: '/api/v4/images/:query',
Videos: '/api/v4/videos/:channelId',
Playlist: '/api/v4/playlistVideos/:playlistId',
'Type Videos': '/api/v4/sectionedVideos/:type',
Radios: '/api/v4/radio',
'All Themes': '/api/v4/allThemes',
Themes: '/api/v4/themes/:title',
'Year Themes': '/api/v4/themesYear/:year?',
'Random Theme': '/api/v4/randomTheme',
'Artists Theme': '/api/v4/artists/:id?',
'Famous Platforms': '/api/v4/destAnimePlatforms',
'Legal Platforms': '/api/v4/platforms/:id?',
},
],
});
});
/* Routes of the app below */
/* Anime Controller */
routes.get('/api/v4/schedule/:day', animeController.schedule);
routes.get('/api/v4/top/:type/:subtype?/:page', animeController.top);
routes.get('/api/v4/allAnimes', animeController.getAllAnimes);
routes.get('/api/v4/lastEpisodes', animeController.getLastEpisodes);
routes.get('/api/v4/movies/:type/:page', animeController.getContentMovie);
routes.get('/api/v4/ova/:type/:page', animeController.getContentOva);
routes.get('/api/v4/special/:type/:page', animeController.getContentSpecial);
routes.get('/api/v4/tv/:type/:page', animeController.getContentTv);
routes.get('/api/v4/getEpisodes/:title', animeController.getEpisodes);
routes.get(
'/api/v4/getAnimeServers/:id([^/]+/[^/]+)',
animeController.getServers,
);
routes.get('/api/v4/randomAnime', animeController.getRandomAnime);
/* Directory Controller */
routes.get(
'/api/v4/allDirectory/:genres?',
directoryController.getAllDirectory,
);
routes.get('/api/v4/season/:year/:type', directoryController.getSeason);
routes.get('/api/v4/allSeasons', directoryController.allSeasons);
routes.get('/api/v4/laterSeasons', directoryController.laterSeasons);
routes.get('/api/v4/moreInfo/:title', directoryController.getMoreInfo);
routes.get('/api/v4/search/:title', directoryController.search);
routes.get(
'/api/v4/getByGenres/:genre?/:order?/:page?',
directoryController.getAnimeGenres,
);
/* Utils Controller */
routes.get('/api/v4/anitakume', utilsController.getAnitakume);
routes.get('/api/v4/news', utilsController.getNews);
routes.get('/api/v4/images/:title', utilsController.getImages);
routes.get('/api/v4/videos/:channelId', utilsController.getVideos);
routes.get('/api/v4/playlistVideos/:playlistId', utilsController.getPlaylists);
routes.get('/api/v4/sectionedVideos/:type', utilsController.getSectionVideos);
routes.get('/api/v4/radio', utilsController.getRadioStations);
routes.get('/api/v4/allThemes', utilsController.getAllThemes);
routes.get('/api/v4/themes/:title', utilsController.getOpAndEd);
routes.get('/api/v4/themesYear/:year?', utilsController.getThemesYear);
routes.get('/api/v4/randomTheme', utilsController.randomTheme);
routes.get('/api/v4/artists/:id?', utilsController.getArtist);
routes.get('/api/v4/destAnimePlatforms', utilsController.getDestAnimePlatforms);
routes.get('/api/v4/platforms/:id?', utilsController.getPlatforms);
routes.get('/api/v4/generateWaifu/', utilsController.getWaifuRandom);
/* Routes to handling the v3 deprecated */
routes.get('/api/v3/*', (req: Request, res: Response, next: NextFunction) => {
res.status(302).redirect('/api/v3');
});
routes.get('/api/v3', (req: Request, res: Response, next: NextFunction) => {
res.status(200).json({
message:
'Sorry, version v3 is not avaiable, if you want to see content go to v4',
});
});
/* Routes to handling the v2 deprecated */
routes.get('/api/v2/*', (req: Request, res: Response, next: NextFunction) => {
res.status(302).redirect('/api/v2');
});
routes.get('/api/v2', (req: Request, res: Response, next: NextFunction) => {
res.status(200).json({
message:
'Sorry, version v2 is not avaiable, if you want to see content go to v4',
});
});
export default routes;

@ -0,0 +1,41 @@
import express, { Application } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import dotenv from 'dotenv';
import { errorHandler, notFound } from './middlewares/middleware';
import {
createConnectionMongo,
} from './database/connection';
import routes from './routes';
const app: Application = express();
dotenv.config();
createConnectionMongo({
host: process.env.DATABASE_HOST,
port: process.env.DATABASE_PORT,
});
app.use(cors());
app.use(helmet());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(routes);
app.use(notFound);
app.use(errorHandler);
/*
Starting the server on the .env process
you can define the PORT where the server
is going to listen in the server.
ex: PORT=3000.
*/
const server = app.listen(process.env.PORT_LISTEN || 3000);
function shutdown(): void {
server.close();
process.exit();
}
process.on('SIGINT', shutdown);
process.on('SIGQUIT', shutdown);
process.on('SIGTERM', shutdown);

@ -0,0 +1,302 @@
import cheerio from 'cheerio';
import { requestGot } from './requestCall';
import urls from './urls';
export default class ThemeParser {
animes: any[] = [];
$: any = '';
async all() {
try {
this.animes = [];
this.$ = await redditocall('year_index');
return await this.parseLinks();
} catch (err) {
console.log(err);
}
}
async allYears() {
try {
this.animes = [];
this.$ = await redditocall('year_index');
return await this.parseYears();
} catch (err) {
console.log(err);
}
}
async serie(title: string) {
try {
this.animes = [];
this.$ = await redditocall('anime_index');
return await this.parseSerie(title);
} catch (err) {
console.log(err);
}
}
async artists() {
try {
this.animes = [];
this.$ = await redditocall('artist');
return await this.parseArtists();
} catch (err) {
console.log(err);
}
}
async artist(id: string) {
try {
this.animes = [];
this.$ = await redditocall(`artist/${id}`);
return await this.parseArtist();
} catch (err) {
console.log(err);
}
}
async random() {
try {
this.animes = [];
this.$ = await redditocall('anime_index');
return await this.parseRandom();
} catch (err) {
console.log(err);
}
}
async year(date: string) {
let animes: any = [];
this.$ = await redditocall(date);
this.$('h3').each((index: number, element: cheerio.Element) => {
let parsed = this.parseAnime(this.$(element));
parsed.year = date;
animes.push(parsed);
});
return animes;
}
parseRandom() {
return new Promise(async resolve => {
let data = this.$('p a');
const origin: any = '1';
let randomize = Math.round(
Math.random() * (data.length - 1 - origin) + parseInt(origin),
);
this.$ = await redditocall(
this.$('p a')
[randomize].attribs.href.split('/r/AnimeThemes/wiki/')[1]
.split('#wiki')[0],
);
let rand = Math.round(Math.random() * this.$('h3').length - 1);
let parsed = this.parseAnime(this.$('h3')[rand]);
resolve(parsed);
});
}
/* -ParseYears
Get the data from the year
get the name and the id to do the respective
scrapping.
*/
parseYears() {
return new Promise(async resolve => {
let years: any[] = [];
this.$('h3 a').each((index: number, element: cheerio.Element) => {
years.push({
id: this.$(element).attr('href').split('/')[4],
name: this.$(element).text(),
});
});
resolve(years);
});
}
parseArtists() {
return new Promise(async resolve => {
let promises = [];
let data = this.$('p a').filter((x: any) => x > 0);
for (let i = 0; i < data.length; i++) {
promises.push({
id: data[i].children[0].parent.attribs.href.split('/')[5],
name: data[i].children[0].data,
});
if (i === data.length - 1) {
resolve(promises);
}
}
});
}
parseArtist() {
return new Promise(async resolve => {
let promises = [];
let data = this.$('h3');
for (let i = 0; i < data.length; i++) {
let parsed = await this.parseAnime(data[i]);
promises.push(parsed);
if (i === data.length - 1) {
resolve(promises);
}
}
});
}
/* - ParseSerie
Parse the HTML from the redditocall
and search for the h3 tag to be the
same of the title and resolve a object.
*/
parseSerie(title: string) {
return new Promise(async resolve => {
let data = this.$('p a');
for (let i = 0; i < data.length; i++) {
let serieElement = data[i].children[0].data;
if (serieElement.split(' (')[0] === title) {
let year = this.$('p a')
[i].attribs.href.split('/r/AnimeThemes/wiki/')[1]
.split('#wiki')[0];
this.$ = await redditocall(
this.$('p a')
[i].attribs.href.split('/r/AnimeThemes/wiki/')[1]
.split('#wiki')[0],
);
for (let i = 0; i < this.$('h3').length; i++) {
if (this.$('h3')[i].children[0].children[0].data === title) {
let parsed = this.parseAnime(this.$('h3')[i]);
parsed.year = year;
resolve(parsed);
}
}
}
}
});
}
parseLinks() {
return new Promise(async resolve => {
let years = this.$('h3 a');
for (let i = 0; i < years.length; i++) {
let yearElement = years[i];
await this.year(this.$(yearElement).attr('href').split('/')[4]).then(
async animes => {
this.animes = this.animes.concat(animes);
if (i === years.length - 1) {
resolve(this.animes);
}
},
);
}
});
}
/* - ParseAnime
Parse the h3 tag and get the table
for the next function to parse the table
and get the information about the ending and
openings.
*/
parseAnime(element: cheerio.Element) {
let el = this.$(element).find('a');
let title = this.$(el).text();
let mal_id = this.$(el).attr('href').split('/')[4];
let next = this.$(element).next();
let theme: any = {
id: mal_id,
title,
};
if (this.$(next).prop('tagName') === 'TABLE') {
theme.themes = this.parseTable(this.$(next));
} else if (this.$(next).prop('tagName') === 'P') {
theme.themes = this.parseTable(this.$(next).next());
}
return theme;
}
/* - ParseTable
Parse the table tag from the HTML
and returns a object with all the
information.
*/
parseTable(element: cheerio.Element): any {
if (this.$(element).prop('tagName') !== 'TABLE') {
return this.parseTable(this.$(element).next());
}
let themes: any = [];
this.$(element)
.find('tbody')
.find('tr')
.each((index: number, element: cheerio.Element) => {
let name = replaceAll(
this.$(element).find('td').eq(0).text(),
'&quot;',
'"',
);
let link = this.$(element).find('td').eq(1).find('a').attr('href');
let linkDesc = this.$(element).find('td').eq(1).find('a').text();
let episodes =
this.$(element).find('td').eq(2).text().length > 0
? this.$(element).find('td').eq(2).text()
: '';
let notes =
this.$(element).find('td').eq(3).text().length > 0
? this.$(element).find('td').eq(3).text()
: '';
themes.push({
name,
link,
desc: linkDesc,
type: name.startsWith('OP')
? `OP${name[2]}`
: name.startsWith('ED')
? `ED${name[2]}`
: 'OP/ED',
episodes,
notes,
});
});
return themes;
}
}
async function redditocall(href: string) {
const resp = await requestGot(urls.REDDIT_ANIMETHEMES + href + '.json', {
parse: true,
scrapy: false,
spoof: true,
});
return cheerio.load(getHTML(resp.data.content_html));
}
function getHTML(str: string) {
let html = replaceAll(str, '&lt;', '<');
html = replaceAll(html, '&gt;', '>');
return html;
}
function replaceAll(str: string, find: string, replace: string) {
return str.replace(new RegExp(find, 'g'), replace);
}

@ -0,0 +1,54 @@
import urls from './urls';
export const obtainPreviewNews = (encoded: string) => {
let image: string;
try {
if (encoded.includes('src="https://img1.ak.crunchyroll.com/')) {
if (
encoded.split('https://img1.ak.crunchyroll.com/')[1].includes('.jpg')
) {
image = `https://img1.ak.crunchyroll.com/${
encoded.split('https://img1.ak.crunchyroll.com/')[1].split('.jpg')[0]
}.jpg`;
} else {
image = `https://img1.ak.crunchyroll.com/${
encoded.split('https://img1.ak.crunchyroll.com/')[1].split('.png')[0]
}.png`;
}
} else if (encoded.includes('<img title=')) {
image = encoded
.substring(encoded.indexOf('<img title="'), encoded.indexOf('" alt'))
.split('src="')[1];
} else if (encoded.includes('<img src=')) {
image = encoded
.substring(encoded.indexOf('<img src="'), encoded.indexOf('" alt'))
.substring(10)
.replace('http', 'https')
.replace('httpss', 'https');
} else if (encoded.includes('<img')) {
image = encoded
.split('src=')[1]
.split(' class=')[0]
.replace('"', '')
.replace('"', '');
} else if (encoded.includes('https://www.youtube.com/embed/')) {
let getSecondThumb = encoded
.split('https://www.youtube.com/embed/')[1]
.split('?feature')[0];
image = `https://img.youtube.com/vi/${getSecondThumb}/0.jpg`;
} else if (encoded.includes('https://www.dailymotion.com/')) {
let getDailymotionThumb = encoded
.substring(encoded.indexOf('" src="'), encoded.indexOf('" a'))
.substring(47);
image = `https://www.dailymotion.com/thumbnail/video/${getDailymotionThumb}`;
} else {
let number = Math.floor(Math.random() * 30);
image = `${urls.BASE_ARUPPI}news/${number}.png`;
}
return image;
} catch (err) {
console.log(err);
}
};

@ -0,0 +1,52 @@
import got from 'got';
import cheerio from 'cheerio';
import { CookieJar } from 'tough-cookie';
// @ts-ignore
import * as got_pjson from 'got/package.json'
const pjson = require('../../package.json');
const cookieJar = new CookieJar();
const aruppi_options: any = {
cookieJar,
'headers': {
'user-agent': `Aruppi-API/${pjson.version} ${got_pjson.name}/${got_pjson.version}`,
'x-client': 'aruppi-api'
},
};
interface Options {
scrapy?: boolean,
parse?: boolean,
spoof?: boolean
}
export const requestGot = async (
url: string,
options?: Options,
): Promise<any> => {
const got_options: any = {...got.defaults.options, ...aruppi_options}
if (options) {
if (options.spoof != null) {
got_options.headers["user-agent"] = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:71.0) Gecko/20100101 Firefox/69.0";
delete got_options.headers['x-client'];
if (!options.spoof)
got_options.headers['user-agent'] = got.defaults.options.headers['user-agent'];
} else if (process.env.ALPI_KEY && (new URL(url)).hostname.match(/\.jeluchu\.xyz$/)) {
got_options.headers['x-aruppi-key'] = process.env.ALPI_KEY;
}
if (options.scrapy) {
const response = await got(url, got_options);
return cheerio.load(response.body);
}
if (options.parse) {
got_options.responseType = 'json';
const response = await got(url, got_options);
return response.body;
}
}
const response = await got.get(url, got_options);
return response;
};

@ -0,0 +1,22 @@
import { requestGot } from './requestCall';
export const transformUrlServer = async (urlReal: any) => {
for (const data of urlReal) {
if (data.server === 'amus' || data.server === 'natsuki') {
let res = await requestGot(data.code.replace('embed', 'check'), {
parse: true,
scrapy: false,
});
data.code = res.file || null;
data.direct = true;
}
}
return urlReal.map((item: any) => {
return {
id: item.title.toLowerCase(),
url: item.code,
direct: item.direct || false,
};
});
};

@ -0,0 +1,23 @@
export default {
BASE_ARUPPI: 'https://aruppi.jeluchu.xyz/',
BASE_ANIMEFLV: 'https://www3.animeflv.net/',
BASE_MONOSCHINOS: 'https://monoschinos2.com/',
BASE_TIOANIME: 'https://tioanime.com/',
BASE_JKANIME: 'https://jkanime.net/',
BASE_ANIMEFLV_JELU: 'https://aruppi.jeluchu.xyz/apis/animeflv/v1/',
BASE_YOUTUBE: 'https://aruppi.jeluchu.xyz/api/Youtube/?channelId=',
BASE_YOUTUBE_PLAYLIST: 'https://aruppi.jeluchu.xyz/api/Youtube/playlist/?playlistId=',
BASE_JIKAN: 'https://aruppi.jeluchu.xyz/apis/jikan/v4/',
BASE_IVOOX: 'https://www.ivoox.com/podcast-anitakume_fg_f1660716_filtro_1.xml',
BASE_KUDASAI: 'https://somoskudasai.com/feed/',
BASE_RAMENPARADOS: 'https://ramenparados.com/category/noticias/anime/feed/',
BASE_CRUNCHYROLL: 'https://www.crunchyroll.com/newsrss?lang=esES',
JKANIME_SEARCH: 'https://jkanime.net/buscar/',
ANIMEFLV_SEARCH: 'https://animeflv.net/browse?',
SEARCH_DIRECTORY: 'https://animeflv.net/browse?order=title&page=',
BASE_EPISODE_IMG_URL: 'https://cdn.animeflv.net/screenshots/',
BASE_QWANT: 'https://api.qwant.com/v3/search/images?',
REDDIT_ANIMETHEMES: 'https://reddit.com/r/AnimeThemes/wiki/',
BASE_THEMEMOE: 'https://themes.moe/api/',
BASE_ARUPPI_MONOSCHINOS: 'https://aruppi.jeluchu.xyz/apis/monoschinos/',
};

File diff suppressed because it is too large Load Diff

@ -0,0 +1,70 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "ES2017" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./" /* Concatenate and emit output to single file. */,
"outDir": "./dist" /* Redirect output structure to the directory. */,
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
/* Module Resolution Options */
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save