Skip to content

Commit 446ca69

Browse files
feat: new source added (#52)
* feat: Add configuration for comichubfree card classes and update API host * feat: add new comic source, - add a temp. UI to Search between comic sources - refactor comic details fetch function to get new souces comic details page * feat: enhance comic details fetching and add pagination support * feat: add support for new comic source and enhance chapter fetching logic * feat: enhance searchComic function to support multiple comic sources and improve result formatting * feat: update default comic source to comichubfree in Home component * fixup: update history management to trim query from URL and rename comic source class --------- Co-authored-by: Prem Kumar Sharma <premsharma12com@gmail.com>
2 parents f720c25 + df7b508 commit 446ca69

File tree

10 files changed

+618
-236
lines changed

10 files changed

+618
-236
lines changed

src/Redux/Actions/GlobalActions.js

Lines changed: 179 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ import {Alert} from 'react-native';
1616
import {goBack} from '../../Navigation/NavigationService';
1717
import APICaller from '../Controller/Interceptor';
1818
import crashlytics from '@react-native-firebase/crashlytics';
19+
import {ComicHostName} from '../../Utils/APIs';
20+
import {
21+
ComicBookPageClasses,
22+
ComicDetailPageClasses,
23+
} from '../../Screens/Comic/APIs/constance';
1924

2025
/**
2126
* Action creator for handling watched data.
@@ -110,70 +115,65 @@ export const fetchComicDetails =
110115

111116
const response = await APICaller.get(link);
112117
const html = response.data;
113-
const $ = cheerio.load(html);
118+
let $ = cheerio.load(html);
119+
120+
const hostkey = Object.keys(ComicHostName).find(key =>
121+
link.includes(key),
122+
);
123+
const config = ComicDetailPageClasses[hostkey];
124+
if (!config) throw new Error(`No config found for source: ${hostkey}`);
125+
126+
const detailsContainer = $(config.detailsContainer);
127+
const title = $(config.title).text().trim();
114128

115-
// Details Section
116-
const detailsContainer = $('.list-container');
117-
const title = $('img.img-responsive').attr('alt')?.trim();
118129
let imgSrc = detailsContainer
119-
.find('.boxed img.img-responsive')
120-
.attr('src');
130+
.find(config.imgSrc)
131+
.attr(config.getImageAttr);
121132
if (imgSrc && imgSrc.startsWith('//')) {
122133
imgSrc = 'https:' + imgSrc;
123134
}
124135

125-
// Build a details map from the <dl class="dl-horizontal">
126136
const details = {};
127-
detailsContainer.find('dl.dl-horizontal dt').each((i, el) => {
137+
detailsContainer.find(config.detailsDL).each((i, el) => {
128138
const key = $(el).text().trim().replace(':', '');
129139
const dd = $(el).next('dd');
130-
if (key === 'Tags') {
131-
const tags = [];
140+
141+
if (key.toLowerCase() === 'tags' || key.toLowerCase() === 'genres') {
142+
const list = [];
132143
dd.find('a').each((j, a) => {
133-
tags.push($(a).text().trim());
144+
list.push($(a).text().trim());
134145
});
135-
details[key] = tags;
136-
} else if (key === 'Categories') {
137-
details[key] = dd.find('a').first().text().trim();
138-
} else if (key === 'Rating') {
139-
details[key] = dd.text().trim();
146+
details[key] = list;
140147
} else {
141148
details[key] = dd.text().trim();
142149
}
143150
});
144151

145-
// Summary Section
146-
const summary = $('div.manga.well p').text().trim();
147-
148-
// Chapters Section
149-
const chapters = [];
150-
$('ul.chapters li').each((i, el) => {
151-
const chapterTitle = $(el).find('h5.chapter-title-rtl a').text().trim();
152-
const chapterLink = $(el).find('h5.chapter-title-rtl a').attr('href');
153-
const chapterDate = $(el)
154-
.find('div.date-chapter-title-rtl')
155-
.text()
156-
.trim();
157-
chapters.push({
158-
title: chapterTitle,
159-
link: chapterLink,
160-
date: chapterDate,
161-
});
162-
});
152+
const summary = $(config.summary).text().trim();
153+
const chapters = await fetchChaptersWithPagination($, config, link);
154+
const pagination = getChapterPagination($, config);
163155

164-
// Create comic details object using the new API structure
165156
const comicDetails = {
166157
title,
167158
imgSrc,
168159
type: details['Type'] || null,
169160
status: details['Status'] || null,
170-
releaseDate: details['Date of release'] || null,
171-
categories: details['Categories'] || null,
161+
releaseDate:
162+
details['Release'] ||
163+
details['Released'] ||
164+
details['Date of release'] ||
165+
null,
166+
categories: details['Category'] || details['Categories'] || null,
172167
tags: details['Tags'] || [],
168+
genres: details['Genres'] || [],
169+
author: details['Author'] || null,
170+
alternativeName:
171+
details['Alternative'] || details['Alternative name'] || null,
173172
views: details['Views'] || null,
174173
rating: details['Rating'] || null,
175174
summary,
176175
chapters,
176+
pagination,
177177
link,
178178
};
179179

@@ -210,6 +210,54 @@ export const fetchComicDetails =
210210
}
211211
};
212212

213+
const fetchChaptersWithPagination = async ($, config, link) => {
214+
const chapters = [];
215+
const visitedPages = new Set();
216+
let currentLink = link;
217+
218+
while (!visitedPages.has(currentLink)) {
219+
visitedPages.add(currentLink);
220+
221+
$(config.chaptersList).each((i, el) => {
222+
const chapterTitle = $(el).find(config.chapterTitle).text().trim();
223+
const chapterLink = $(el).find(config.chapterLink).attr('href');
224+
const chapterDate = $(el).find(config.chapterDate).text().trim();
225+
226+
if (chapterTitle && chapterLink) {
227+
chapters.push({
228+
title: chapterTitle,
229+
link: chapterLink,
230+
date: chapterDate,
231+
});
232+
}
233+
});
234+
235+
const nextPageLink = $(config.pagination)
236+
.filter((i, el) => $(el).text().trim().toLowerCase() === 'next')
237+
.attr('href');
238+
239+
if (!nextPageLink) break;
240+
241+
const response = await APICaller.get(nextPageLink);
242+
currentLink = nextPageLink;
243+
$ = cheerio.load(response.data);
244+
}
245+
246+
return chapters;
247+
};
248+
249+
const getChapterPagination = ($, config) => {
250+
const pages = [];
251+
$(config.pagination).each((i, el) => {
252+
const text = $(el).text().trim();
253+
const href = $(el).attr('href');
254+
if (text && href) {
255+
pages.push({text, link: href});
256+
}
257+
});
258+
return pages;
259+
};
260+
213261
/**
214262
* Fetches comic book data from a given URL and dispatches appropriate actions based on the result.
215263
*
@@ -220,7 +268,18 @@ export const fetchComicDetails =
220268
export const fetchComicBook =
221269
(comicBook, setPageLink = null, isDownloadComic) =>
222270
async (dispatch, getState) => {
271+
let newcomicBook = comicBook;
272+
// Dynamically get host config
273+
const hostkey = Object.keys(ComicHostName).find(key =>
274+
comicBook.includes(key),
275+
);
276+
277+
if (hostkey == 'comichubfree') {
278+
newcomicBook = `${comicBook}/all`;
279+
}
280+
223281
if (!isDownloadComic) dispatch(fetchDataStart());
282+
224283
try {
225284
const Data = getState().data.dataByUrl[comicBook];
226285
if (Data) {
@@ -232,40 +291,48 @@ export const fetchComicBook =
232291
dispatch(checkDownTime());
233292
return;
234293
}
235-
const response = await APICaller.get(comicBook);
294+
295+
const response = await APICaller.get(newcomicBook);
236296
const html = response.data;
237297
const $ = cheerio.load(html);
238298

239-
// New API: Extract chapter images using data-src attribute
240-
const imageContainer = $('.imagecnt');
299+
const config = ComicBookPageClasses[hostkey];
300+
if (!config) {
301+
throw new Error(`No chapter page config found for source: ${hostkey}`);
302+
}
303+
304+
const {
305+
imageContainer,
306+
imageSelector,
307+
imageAttr,
308+
titleSelector,
309+
titleAttr,
310+
} = config;
311+
312+
const container = $(imageContainer);
241313
const imgSources = [];
242-
imageContainer
243-
.find('img.img-responsive[data-src]')
244-
.each((index, element) => {
245-
const src = $(element).attr('data-src')?.trim();
246-
if (src) {
247-
imgSources.push(src);
248-
}
249-
});
314+
315+
container.find(imageSelector).each((i, el) => {
316+
const src = $(el).attr(imageAttr)?.trim();
317+
if (src) imgSources.push(src);
318+
});
319+
320+
const title =
321+
container.find(titleSelector).first().attr(titleAttr)?.trim() || '';
250322

251323
const data = {
252324
images: imgSources,
253-
// It is assumed the chapter title is embedded in the alt text of the first image.
254-
// Adjust the extraction as needed.
255-
title:
256-
imageContainer
257-
.find('img.img-responsive')
258-
.first()
259-
.attr('alt')
260-
?.trim() || '',
325+
title,
261326
lastReadPage: 0,
262327
BookmarkPages: [],
263-
ComicDetailslink: '',
328+
ComicDetailslink: '', // set externally if needed
264329
};
330+
console.log('imgSources', data);
265331

266332
if (setPageLink) {
267333
setPageLink(data.ComicDetailslink);
268334
}
335+
269336
dispatch(fetchDataSuccess({url: comicBook, data}));
270337
if (isDownloadComic) return {url: comicBook, data};
271338
} catch (error) {
@@ -467,24 +534,58 @@ export const getAdvancedSearchFilters = () => async dispatch => {
467534
* @param {string} queryValue - The value to be appended to the search URL.
468535
* @returns {Function} A thunk function that performs the async operation and returns the result.
469536
*/
470-
export const searchComic = (queryValue) => async dispatch => {
471-
dispatch(fetchDataStart());
472-
const url = `https://readcomicsonline.ru/search?query=${encodeURIComponent(
473-
queryValue,
474-
)}`;
475-
try {
476-
const response = await APICaller.get(url);
477-
dispatch(fetchDataSuccess({url, data: response?.data}));
478-
dispatch(StopLoading());
479-
dispatch(ClearError());
480-
dispatch(checkDownTime());
481-
return response?.data;
482-
} catch (error) {
483-
crashlytics().recordError(error);
484-
console.log('Error details:', error);
485-
console.error('Error fetching search results:', error);
486-
dispatch(fetchDataFailure(error.message));
487-
dispatch(checkDownTime(error));
488-
return null;
489-
}
490-
};
537+
export const searchComic =
538+
(queryValue, source = 'readcomicsonline') =>
539+
async dispatch => {
540+
dispatch(fetchDataStart());
541+
542+
let url;
543+
const host =
544+
source === 'readcomicsonline'
545+
? 'https://readcomicsonline.ru'
546+
: 'https://comichubfree.com';
547+
548+
try {
549+
if (source === 'readcomicsonline') {
550+
url = `${host}/search?query=${encodeURIComponent(queryValue)}`;
551+
const response = await APICaller.get(url);
552+
const suggestions = response?.data?.suggestions || [];
553+
554+
const formatted = suggestions.map(item => ({
555+
title: item.value,
556+
data: item.data,
557+
link: `${host}/comic/${item.data}`,
558+
}));
559+
560+
dispatch(fetchDataSuccess({url, data: formatted}));
561+
dispatch(StopLoading());
562+
dispatch(ClearError());
563+
dispatch(checkDownTime());
564+
return formatted;
565+
} else if (source === 'comichubfree') {
566+
url = `${host}/ajax/search?key=${encodeURIComponent(queryValue)}`;
567+
const response = await APICaller.get(url);
568+
const json = response?.data || [];
569+
570+
const formatted = json.map(item => ({
571+
title: item.title,
572+
data: item.slug,
573+
link: `${host}/comic/${item.slug}`,
574+
}));
575+
576+
dispatch(fetchDataSuccess({url, data: formatted}));
577+
dispatch(StopLoading());
578+
dispatch(ClearError());
579+
dispatch(checkDownTime());
580+
return formatted;
581+
}
582+
583+
throw new Error(`Unsupported source: ${source}`);
584+
} catch (error) {
585+
crashlytics().recordError(error);
586+
console.log('Error details:', error);
587+
dispatch(fetchDataFailure(error.message));
588+
dispatch(checkDownTime(error));
589+
return null;
590+
}
591+
};

src/Redux/Reducers/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,10 @@ const Reducers = createSlice({
142142
},
143143
pushHistory: (state, action) => {
144144
// state.history.push(action.payload);
145-
state.history[action.payload.link] = action.payload;
145+
//trim the query from the url
146+
const link = action.payload.link.split('?')[0];
147+
148+
state.history[link] = action.payload;
146149
},
147150
UpdateSearch: (state, action) => {
148151
//push data to search array on top

0 commit comments

Comments
 (0)