mangafox: fix mangafox, but site is too full of javascript and obvious anti-copy...
[fanfix.git] / src / be / nikiroo / fanfix / supported / MangaFox.java
CommitLineData
08fe2e33
NR
1package be.nikiroo.fanfix.supported;
2
3import java.io.IOException;
4import java.io.InputStream;
5import java.net.MalformedURLException;
6import java.net.URL;
cb554033 7import java.util.AbstractMap;
08fe2e33
NR
8import java.util.ArrayList;
9import java.util.Collections;
10import java.util.List;
11import java.util.Map.Entry;
cb554033
NR
12
13import org.jsoup.helper.DataUtil;
f3ce1b69 14import org.jsoup.nodes.Document;
cb554033 15import org.jsoup.nodes.Element;
08fe2e33
NR
16
17import be.nikiroo.fanfix.Instance;
68686a37 18import be.nikiroo.fanfix.data.MetaData;
16a81ef7 19import be.nikiroo.utils.Image;
ed08c171 20import be.nikiroo.utils.Progress;
08fe2e33
NR
21import be.nikiroo.utils.StringUtils;
22
cb554033 23class MangaFox extends BasicSupport {
08fe2e33
NR
24 @Override
25 protected boolean isHtml() {
26 return true;
27 }
28
08fe2e33 29 @Override
cb554033 30 protected MetaData getMeta() throws IOException {
68686a37
NR
31 MetaData meta = new MetaData();
32
cb554033 33 meta.setTitle(getTitle());
f3ce1b69
NR
34 // No date anymore on mangafox
35 // meta.setDate();
36 meta.setAuthor(getAuthor());
37 meta.setTags(getTags());
727108fe 38 meta.setSource(getType().getSourceName());
cb554033 39 meta.setUrl(getSource().toString());
727108fe 40 meta.setPublisher(getType().getSourceName());
cb554033 41 meta.setUuid(getSource().toString());
68686a37 42 meta.setLuid("");
276f95c6 43 meta.setLang("en");
68686a37
NR
44 meta.setSubject("manga");
45 meta.setType(getType().toString());
46 meta.setImageDocument(true);
cb554033 47 meta.setCover(getCover());
68686a37
NR
48
49 return meta;
08fe2e33
NR
50 }
51
cb554033
NR
52 private String getTitle() {
53 Element doc = getSourceNode();
08fe2e33 54
f3ce1b69
NR
55 Element el = doc.getElementsByClass("detail-info-right-title-font").first();
56 if (el != null) {
57 return StringUtils.unhtml(el.text()).trim();
08fe2e33
NR
58 }
59
60 return null;
61 }
62
f3ce1b69
NR
63 private String getAuthor() {
64 StringBuilder builder = new StringBuilder();
65 for (String author : getListA("detail-info-right-say")) {
66 if (builder.length() > 0)
67 builder.append(", ");
68 builder.append(author);
69 }
70
71 return builder.toString();
72 }
73
74 private List<String> getTags() {
75 return getListA("detail-info-right-tag-list");
76 }
77
78 private List<String> getListA(String uniqueClass) {
79 List<String> list = new ArrayList<String>();
80
81 Element doc = getSourceNode();
82 Element el = doc.getElementsByClass(uniqueClass).first();
83 if (el != null) {
84 for (Element valueA : el.getElementsByTag("a")) {
85 list.add(StringUtils.unhtml(valueA.text()).trim());
08fe2e33
NR
86 }
87 }
88
f3ce1b69 89 return list;
08fe2e33
NR
90 }
91
92 @Override
cb554033
NR
93 protected String getDesc() {
94 Element doc = getSourceNode();
f3ce1b69 95 Element title = doc.getElementsByClass("fullcontent").first();
cb554033 96 if (title != null) {
af1f506f 97 return StringUtils.unhtml(title.text()).trim();
08fe2e33
NR
98 }
99
100 return null;
101 }
102
cb554033
NR
103 private Image getCover() {
104 Element doc = getSourceNode();
f3ce1b69 105 Element cover = doc.getElementsByClass("detail-info-cover-img").first();
08fe2e33 106 if (cover != null) {
cb554033
NR
107 String coverUrl = cover.absUrl("src");
108
68686a37 109 InputStream coverIn;
08fe2e33 110 try {
cb554033 111 coverIn = openEx(coverUrl);
68686a37 112 try {
16a81ef7 113 return new Image(coverIn);
68686a37
NR
114 } finally {
115 coverIn.close();
116 }
117 } catch (IOException e) {
cb554033 118 Instance.getTraceHandler().error(e);
08fe2e33
NR
119 }
120 }
121
122 return null;
123 }
124
125 @Override
cb554033 126 protected List<Entry<String, URL>> getChapters(Progress pg) {
08fe2e33
NR
127 List<Entry<String, URL>> urls = new ArrayList<Entry<String, URL>>();
128
f3ce1b69
NR
129 String prefix = getTitle(); // each chapter starts with this prefix, then a
130 // chapter number (including "x.5"), then name
41c3bba7 131
f3ce1b69 132 // normally, only one list...
cb554033 133 Element doc = getSourceNode();
f3ce1b69
NR
134 for (Element list : doc.getElementsByClass("detail-main-list")) {
135 for (Element el : list.getElementsByTag("a")) {
136 String title = el.attr("title");
137 if (title.startsWith(prefix)) {
138 title = title.substring(prefix.length()).trim();
08fe2e33 139 }
08fe2e33 140
f3ce1b69
NR
141 String url = el.absUrl("href");
142
143 try {
144 urls.add(new AbstractMap.SimpleEntry<String, URL>(title, new URL(url)));
145 } catch (Exception e) {
146 Instance.getTraceHandler().error(e);
41c3bba7 147 }
41c3bba7 148 }
41c3bba7 149 }
08fe2e33 150
f3ce1b69
NR
151 // by default, the chapters are in reversed order
152 Collections.reverse(urls);
153
08fe2e33
NR
154 return urls;
155 }
156
157 @Override
f3ce1b69 158 protected String getChapterContent(URL chapUrl, int number, Progress pg) throws IOException {
ed08c171
NR
159 if (pg == null) {
160 pg = new Progress();
ed08c171
NR
161 }
162
08fe2e33 163 StringBuilder builder = new StringBuilder();
08fe2e33 164
f3ce1b69
NR
165 Document chapDoc = DataUtil.load(Instance.getCache().open(chapUrl, this, false), "UTF-8", chapUrl.toString());
166
167 // Example of what we want:
168 // URL: http://fanfox.net/manga/solo_leveling/c110.5/1.html#ipg1
169 // IMAGE, not working:
170 // http://s.fanfox.net/store/manga/29037/110.5/compressed/s034.jpg?token=f630767b0c96f6cc793fc8f1fc177c0ae9342eb1&amp;ttl=1585929600
171 // IMAGE, working:
172 // http://s.fanfox.net/store/manga/29037/000.0/compressed/m2018110o_143554_925.jpg?token=7d74569986335d49651ef1040f7dcb9dbd559b1b&ttl=1585929600
173 // NOTE: (c110.5 -> 110.5, c000 -> 000.0)
174 // NOTE: image key: m2018110o_143554_925 can be found in the script, but not
175 // sorted
176
177 // 0. Get the javascript content
178 StringBuilder javascript = new StringBuilder();
179 for (Element script : chapDoc.getElementsByTag("script")) {
180 javascript.append(script.html());
181 javascript.append("\n");
cb554033 182 }
cb554033 183
f3ce1b69
NR
184 // 1. Get the chapter url part
185 String chap = chapUrl.getPath();
186 chap = chap.split("#")[0];
187 if (chap.endsWith("/1.html")) {
188 chap = chap.substring(0, chap.length() - "/1.html".length());
189 }
190 int pos = chap.lastIndexOf("/");
191 chap = chap.substring(pos + 1);
192 if (!chap.contains(".")) {
193 chap = chap + ".0";
194 }
195 if (chap.startsWith("c")) {
196 chap = chap.substring(1);
197 }
cb554033 198
f3ce1b69
NR
199 // 2. Token:
200 // <meta name="og:image"
201 // content="http://fmcdn.fanfox.net/store/manga/29037/cover.jpg?token=4b2056d83973716c715f2404940822dff942a7b4&ttl=1585998000&v=1584582495"
202 Element el = chapDoc.select("meta[name=\"og:image\"]").first();
203 String token = el.attr("content").split("\\?")[1];
204
205 // 3. Comic ID
206 int comicId = getIntVar(javascript, "comicid");
207
208 // 4. Get images
209 List<String> chapKeys = getImageKeys(javascript);
210 // http://s.fanfox.net/store/manga/29037/000.0/compressed/m2018110o_143554_925.jpg?token=7d74569986335d49651ef1040f7dcb9dbd559b1b&ttl=1585929600
211 String base = "http://s.fanfox.net/store/manga/%s/%s/compressed/%s.jpg?%s";
212 for (String key : chapKeys) {
213 String img = String.format(base, comicId, chap, key, token);
214 builder.append("[");
215 builder.append(img);
216 builder.append("]<br/>");
08fe2e33
NR
217 }
218
08fe2e33
NR
219 return builder.toString();
220 }
221
f3ce1b69
NR
222 private int getIntVar(StringBuilder builder, String var) {
223 var = "var " + var;
224
225 int pos = builder.indexOf(var) + var.length();
226 String value = builder.subSequence(pos, pos + 20).toString();
227 value = value.split("=")[1].trim();
228 value = value.split(";")[0].trim();
229
230 return Integer.parseInt(value);
231 }
232
233 private List<String> getImageKeys(StringBuilder builder) {
234 List<String> chapKeys = new ArrayList<String>();
235
236 String start = "|compressed|";
237 String stop = ">";
238 int pos = builder.indexOf(start) + start.length();
239 int pos2 = builder.indexOf(stop, pos) - stop.length();
240
241 String data = builder.substring(pos, pos2);
242 data = data.replace("|", "'");
243 for (String key : data.split("'")) {
244 if (key.startsWith("m") && !key.equals("manga")) {
245 chapKeys.add(key);
246 }
08fe2e33 247 }
f3ce1b69
NR
248
249 Collections.sort(chapKeys);
250 return chapKeys;
08fe2e33
NR
251 }
252
253 /**
f3ce1b69
NR
254 * Open the URL through the cache, but: retry a second time after 100ms if it
255 * fails, remove the query part of the {@link URL} before saving it to the cache
256 * (so it can be recalled later).
08fe2e33 257 *
f3ce1b69 258 * @param url the {@link URL}
08fe2e33
NR
259 *
260 * @return the resource
261 *
f3ce1b69 262 * @throws IOException in case of I/O error
08fe2e33
NR
263 */
264 private InputStream openEx(String url) throws IOException {
265 try {
f3ce1b69 266 return Instance.getCache().open(new URL(url), withoutQuery(url), this, true);
08fe2e33
NR
267 } catch (Exception e) {
268 // second chance
269 try {
270 Thread.sleep(100);
271 } catch (InterruptedException ee) {
272 }
273
f3ce1b69 274 return Instance.getCache().open(new URL(url), withoutQuery(url), this, true);
08fe2e33
NR
275 }
276 }
277
278 /**
279 * Return the same input {@link URL} but without the query part.
280 *
f3ce1b69 281 * @param url the inpiut {@link URL} as a {@link String}
08fe2e33
NR
282 *
283 * @return the input {@link URL} without query
284 */
285 private URL withoutQuery(String url) {
286 URL o = null;
287 try {
288 // Remove the query from o (originalUrl), so it can be cached
289 // correctly
290 o = new URL(url);
291 o = new URL(o.getProtocol() + "://" + o.getHost() + o.getPath());
292
293 return o;
294 } catch (MalformedURLException e) {
295 return null;
296 }
297 }
cb554033 298
cb554033
NR
299 @Override
300 protected boolean supports(URL url) {
f3ce1b69
NR
301 return "mangafox.me".equals(url.getHost()) || "www.mangafox.me".equals(url.getHost())
302 || "fanfox.net".equals(url.getHost()) || "www.fanfox.net".equals(url.getHost());
cb554033 303 }
08fe2e33 304}