initial commit

This commit is contained in:
RY4N
2026-03-27 00:33:48 +06:00
commit 3a63c75b3a
63 changed files with 6301 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
const allGenres: string[] = [
'action',
'adventure',
'cars',
'comedy',
'dementia',
'demons',
'drama',
'ecchi',
'fantasy',
'game',
'harem',
'historical',
'horror',
'isekai',
'josei',
'kids',
'magic',
'martial arts',
'mecha',
'military',
'music',
'mystery',
'parody',
'police',
'psychological',
'romance',
'samurai',
'school',
'sci-fi',
'seinen',
'shoujo',
'shoujo ai',
'shounen',
'shounen ai',
'slice of life',
'space',
'sports',
'super power',
'supernatural',
'thriller',
'vampire',
];
const allGenresController = (): string[] => {
return allGenres;
};
export default allGenresController;

View File

@@ -0,0 +1,21 @@
import { Context } from 'hono';
import { extractCharacterDetail, CharacterDetail } from '../extractor/extractCharacterDetail';
import { axiosInstance } from '../services/axiosInstance';
import { validationError } from '../utils/errors';
const characterDetailConroller = async (c: Context): Promise<CharacterDetail> => {
const id = c.req.param('id');
if (!id) throw new validationError('id is required');
const result = await axiosInstance(`/${id.replace(':', '/')}`);
if (!result.success || !result.data) {
throw new validationError(result.message || 'make sure given endpoint is correct');
}
const response = extractCharacterDetail(result.data);
return response;
};
export default characterDetailConroller;

View File

@@ -0,0 +1,39 @@
import { Context } from 'hono';
import config from '../config/config';
import { validationError } from '../utils/errors';
import { extractCharacters, CharactersResponse } from '../extractor/extractCharacters';
import { axiosInstance } from '../services/axiosInstance';
const charactersController = async (c: Context): Promise<CharactersResponse> => {
try {
const id = c.req.param('id');
const page = c.req.query('page') || '1';
if (!id) throw new validationError('id is required');
const idNum = id.split('-').pop();
const endpoint = `/ajax/character/list/${idNum}?page=${page}`;
const result = await axiosInstance(endpoint, {
headers: { Referer: `${config.baseurl}/home` },
});
if (!result.success || !result.data) {
throw new validationError(result.message || 'characters not found');
}
const response = extractCharacters(result.data);
return response;
} catch (err: unknown) {
if (err instanceof Error) {
console.log(err.message);
} else {
console.log(err);
}
throw new validationError('characters not found');
}
};
export default charactersController;

View File

@@ -0,0 +1,20 @@
import { Context } from 'hono';
import { extractDetailpage } from '../extractor/extractDetailpage';
import { axiosInstance } from '../services/axiosInstance';
import { validationError } from '../utils/errors';
import { DetailAnime } from '../types/anime';
const detailpageController = async (c: Context): Promise<DetailAnime> => {
const id = c.req.param('id');
const result = await axiosInstance(`/${id}`);
if (!result.success || !result.data) {
throw new validationError(
result.message || 'Failed to fetch detail page',
'maybe id is incorrect : ' + id
);
}
return extractDetailpage(result.data);
};
export default detailpageController;

View File

@@ -0,0 +1,29 @@
import { Context } from 'hono';
import config from '../config/config';
import { validationError } from '../utils/errors';
import { extractEpisodes, Episode } from '../extractor/extractEpisodes';
import { axiosInstance } from '../services/axiosInstance';
const episodesController = async (c: Context): Promise<Episode[]> => {
const id = c.req.param('id');
if (!id) throw new validationError('id is required');
const idNum = id.split('-').at(-1);
const ajaxUrl = `/ajax/v2/episode/list/${idNum}`;
const result = await axiosInstance(ajaxUrl, {
headers: { Referer: `${config.baseurl}/watch/${id}` },
});
if (!result.success || !result.data) {
throw new validationError(result.message || 'make sure the id is correct', {
validIdEX: 'one-piece-100',
});
}
const response = extractEpisodes(result.data);
return response;
};
export default episodesController;

View File

@@ -0,0 +1,106 @@
import { Context } from 'hono';
import filterOptions from '../utils/filter';
import { axiosInstance } from '../services/axiosInstance';
import { validationError } from '../utils/errors';
import { extractListPage, ListPageResponse } from '../extractor/extractListpage';
const filterController = async (c: Context): Promise<ListPageResponse> => {
const {
// will receive string and send as a string
keyword = null,
sort = null,
// will recieve an array as string will "," saparated and send as "," saparated string
genres = null,
// will recieve as string and send as index of that string "see filterOptions"
type = null,
status = null,
rated = null,
score = null,
season = null,
language = null,
page = '1',
} = c.req.query();
const pageNum = Number(page);
const queryArr = [
{ title: 'keyword', val: keyword },
{ title: 'sort', val: sort },
{ title: 'type', val: type },
{ title: 'status', val: status },
{ title: 'rated', val: rated },
{ title: 'score', val: score },
{ title: 'season', val: season },
{ title: 'language', val: language },
{ title: 'genres', val: genres },
];
const params = new URLSearchParams();
queryArr.forEach(v => {
if (v.val) {
switch (v.title) {
case 'keyword':
params.set('keyword', formatKeyword(v.val));
break;
case 'genres':
params.set('genres', formatGenres(v.val));
break;
case 'sort': {
const formattedSort = formatSort(v.val);
if (formattedSort) params.set('sort', formattedSort);
break;
}
default: {
const formattedOption = formatOption(v.title, v.val);
if (formattedOption) params.set(v.title, formattedOption);
}
}
}
});
if (pageNum > 1) params.set('page', String(pageNum));
const endpoint = keyword ? '/search' : '/filter';
const queryString = params.toString();
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
const result = await axiosInstance(url);
console.log(result.message);
if (!result.success || !result.data)
throw new validationError(result.message || 'something went wrong will queries');
const response = extractListPage(result.data);
return response;
};
const formatKeyword = (v: string) => v.toLowerCase();
const formatSort = (v: string) => {
const index = filterOptions.sort.indexOf(v.toLowerCase().replace(' ', '_'));
if (index === -1) return null;
return filterOptions.sort[index];
};
const formatGenres = (v: string) => {
let indexes = v
.split(',')
.map(genre =>
(filterOptions as Record<string, string[]>).genres.indexOf(
genre.toLowerCase().replaceAll(' ', '_')
)
)
.filter(i => i !== -1)
.map(i => i + 1);
return indexes.length > 0 ? indexes.join(',') : '';
};
const formatOption = (k: string, v: string) => {
const index = (filterOptions as Record<string, string[]>)[k].indexOf(v);
if (index === -1) return null;
return index.toString();
};
export default filterController;

View File

@@ -0,0 +1,18 @@
import { axiosInstance } from '../services/axiosInstance';
import { validationError } from '../utils/errors';
import { extractHomepage } from '../extractor/extractHomepage';
import { HomePage } from '../types/anime';
const homepageController = async (): Promise<HomePage> => {
console.log('Fetching homepage data from external API...');
const result = await axiosInstance('/home');
if (!result.success || !result.data) {
console.error('Homepage fetch failed:', result.message);
throw new validationError(result.message || 'Failed to fetch homepage');
}
return extractHomepage(result.data);
};
export default homepageController;

View File

@@ -0,0 +1,60 @@
import { Context } from 'hono';
import { extractListPage, ListPageResponse } from '../extractor/extractListpage';
import { axiosInstance } from '../services/axiosInstance';
import { NotFoundError, validationError } from '../utils/errors';
const listpageController = async (c: Context): Promise<ListPageResponse> => {
const validateQueries = [
'top-airing',
'most-popular',
'most-favorite',
'completed',
'recently-added',
'recently-updated',
'top-upcoming',
'genre',
'producer',
'az-list',
'subbed-anime',
'dubbed-anime',
'movie',
'tv',
'ova',
'ona',
'special',
'events',
];
const query = c.req.param('query')?.toLowerCase() || '';
if (!validateQueries.includes(query))
throw new validationError('invalid query', { validateQueries });
let category = c.req.param('category') || null;
const page = c.req.query('page') || '1';
if ((query === 'genre' || query === 'producer') && !category) {
throw new validationError(`category is require for query ${query}`);
}
if (query !== 'genre' && query !== 'producer' && query !== 'az-list' && category) {
category = null;
}
let nromalizeCategory = category && category.replaceAll(' ', '-').toLowerCase();
if (nromalizeCategory === 'martial-arts') nromalizeCategory = 'marial-arts';
const endpoint = category
? `/${query}/${nromalizeCategory}?page=${page}`
: `/${query}?page=${page}`;
const result = await axiosInstance(endpoint);
if (!result.success || !result.data) {
throw new validationError(result.message || 'make sure given endpoint is correct');
}
const response = extractListPage(result.data);
if (response.response.length < 1) throw new NotFoundError();
return response;
};
export default listpageController;

View File

@@ -0,0 +1,21 @@
import { Context } from 'hono';
import { axiosInstance } from '../services/axiosInstance';
import { validationError } from '../utils/errors';
import { extractNews, NewsResponse } from '../extractor/extractNews';
const newsController = async (c: Context): Promise<NewsResponse> => {
const page = c.req.query('page') || '1';
console.log(`Fetching news page ${page} from external API...`);
const endpoint = page === '1' ? '/news' : `/news?page=${page}`;
const result = await axiosInstance(endpoint);
if (!result.success || !result.data) {
console.error('News fetch failed:', result.message);
throw new validationError(result.message || 'Failed to fetch news');
}
return extractNews(result.data);
};
export default newsController;

View File

@@ -0,0 +1,21 @@
import { Context } from 'hono';
import { extractNextEpisodeSchedule } from '../extractor/extractNextEpisodeSchedule';
import { axiosInstance } from '../services/axiosInstance';
import { validationError } from '../utils/errors';
const nextEpisodeSchaduleController = async (c: Context): Promise<unknown> => {
const id = c.req.param('id');
if (!id) throw new validationError('id is required');
const data = await axiosInstance('/watch/' + id);
if (!data.success || !data.data)
throw new validationError(data.message || 'make sure id is correct');
const response = extractNextEpisodeSchedule(data.data);
return response;
};
export default nextEpisodeSchaduleController;

View File

@@ -0,0 +1,33 @@
import { Context } from 'hono';
import { axiosInstance } from '../services/axiosInstance';
import { validationError } from '../utils/errors';
import * as cheerio from 'cheerio';
const randomController = async (_c: Context): Promise<{ id: string }> => {
console.log('Fetching random anime...');
const result = await axiosInstance('/home');
if (!result.success || !result.data) {
console.error('Random anime fetch failed:', result.message);
throw new validationError(result.message || 'Failed to fetch homepage for random selection');
}
const $ = cheerio.load(result.data);
const animes: string[] = [];
$('.flw-item').each((i, el) => {
const link = $(el).find('.film-name .dynamic-name').attr('href');
const id = link?.split('/').pop();
if (id) animes.push(id);
});
if (animes.length === 0) {
throw new validationError('No anime found');
}
const randomId = animes[Math.floor(Math.random() * animes.length)];
return { id: randomId };
};
export default randomController;

View File

@@ -0,0 +1,84 @@
import { Context } from 'hono';
import config from '../config/config';
import { validationError } from '../utils/errors';
import { extractSchedule, ScheduledAnime } from '../extractor/extractSchedule';
import { axiosInstance } from '../services/axiosInstance';
export interface ScheduleResponse {
success: boolean;
data: {
[date: string]: ScheduledAnime[];
};
}
async function schedulesController(c: Context): Promise<ScheduleResponse> {
const today = new Date();
const dateParam = c.req.query('date');
let startDate = today;
if (dateParam) {
const [year, month, day] = dateParam.split('-').map(Number);
startDate = new Date(year, month - 1, day);
if (isNaN(startDate.getTime())) {
throw new validationError('Invalid date format. Use YYYY-MM-DD');
}
}
const dates: string[] = [];
for (let i = 0; i < 7; i++) {
const d = new Date(startDate);
d.setDate(startDate.getDate() + i);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
dates.push(`${year}-${month}-${day}`);
}
try {
const promises = dates.map(async date => {
const ajaxUrl = `/ajax/schedule/list?tzOffset=-330&date=${date}`;
try {
const result = await axiosInstance(ajaxUrl, {
headers: { Referer: `${config.baseurl}/home` },
});
if (!result.success || !result.data) {
throw new Error(result.message || 'Failed to fetch');
}
const jsonData = JSON.parse(result.data);
return {
date,
shows: extractSchedule(jsonData.html),
};
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
console.error(`Failed to fetch schedule for ${date}: ${errorMessage}`);
return {
date,
shows: [] as ScheduledAnime[],
error: 'Failed to fetch',
};
}
});
const results = await Promise.all(promises);
// Format response to map dates to shows
const response: { [date: string]: ScheduledAnime[] } = {};
results.forEach(result => {
response[result.date] = result.shows;
});
return {
success: true,
data: response,
};
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.error(errorMessage);
throw new validationError('Failed to fetch schedules');
}
}
export default schedulesController;

View File

@@ -0,0 +1,30 @@
import { Context } from 'hono';
import { extractListPage, ListPageResponse } from '../extractor/extractListpage';
import { axiosInstance } from '../services/axiosInstance';
import { NotFoundError, validationError } from '../utils/errors';
const searchController = async (c: Context): Promise<ListPageResponse> => {
const keyword = c.req.query('keyword') || null;
const page = c.req.query('page') || '1';
if (!keyword) throw new validationError('query is required');
const noSpaceKeyword = keyword.trim().toLowerCase().replace(/\s+/g, '+');
const endpoint = `/search?keyword=${noSpaceKeyword}&page=${page}`;
const result = await axiosInstance(endpoint);
if (!result.success || !result.data) {
throw new validationError(result.message || 'make sure given endpoint is correct');
}
const response = extractListPage(result.data);
if (response.response.length < 1) {
throw new NotFoundError('page not found');
}
return response;
};
export default searchController;

View File

@@ -0,0 +1,32 @@
import { Context } from 'hono';
import config from '../config/config';
import { validationError } from '../utils/errors';
import { extractSuggestions, Suggestion } from '../extractor/extractSuggestions';
import { axiosInstance } from '../services/axiosInstance';
const suggestionController = async (c: Context): Promise<Suggestion[]> => {
const keyword = c.req.query('keyword') || null;
if (!keyword) throw new validationError('query is required');
const noSpaceKeyword = keyword.trim().toLowerCase().replace(/\s+/g, '+');
const endpoint = `/ajax/search/suggest?keyword=${noSpaceKeyword}`;
const result = await axiosInstance(endpoint, {
headers: { Referer: `${config.baseurl}/home` },
});
if (!result.success || !result.data) {
throw new validationError(result.message || 'suggestion not found');
}
// Parse HTML from JSON if necessary, or just use result.data if it's already HTML
// In hianime API, suggestion ajax usually returns JSON with { status, html }
// but my axiosInstance returns response.text() which is the raw JSON string.
const jsonData = JSON.parse(result.data);
const response = extractSuggestions(jsonData.html);
return response;
};
export default suggestionController;

View File

@@ -0,0 +1,18 @@
import { Context } from 'hono';
import { axiosInstance } from '../services/axiosInstance';
import { validationError } from '../utils/errors';
import { extractTopSearch, TopSearchAnime } from '../extractor/extractTopSearch';
const topSearchController = async (_c: Context): Promise<TopSearchAnime[]> => {
console.log('Fetching top search data from external API...');
const result = await axiosInstance('/');
if (!result.success || !result.data) {
console.error('Top search fetch failed:', result.message);
throw new validationError(result.message || 'Failed to fetch top search');
}
return extractTopSearch(result.data);
};
export default topSearchController;