code cleanup / jdoc
[nikiroo-utils.git] / src / be / nikiroo / fanfix / supported / E621.java
1 package be.nikiroo.fanfix.supported;
2
3 import java.io.IOException;
4 import java.io.UnsupportedEncodingException;
5 import java.net.MalformedURLException;
6 import java.net.URL;
7 import java.net.URLDecoder;
8 import java.util.AbstractMap;
9 import java.util.ArrayList;
10 import java.util.Date;
11 import java.util.LinkedList;
12 import java.util.List;
13 import java.util.Map.Entry;
14
15 import org.json.JSONArray;
16 import org.json.JSONException;
17 import org.json.JSONObject;
18 import org.jsoup.helper.DataUtil;
19 import org.jsoup.nodes.Document;
20 import org.jsoup.nodes.Element;
21
22 import be.nikiroo.fanfix.Instance;
23 import be.nikiroo.fanfix.bundles.Config;
24 import be.nikiroo.fanfix.data.MetaData;
25 import be.nikiroo.utils.Image;
26 import be.nikiroo.utils.Progress;
27 import be.nikiroo.utils.StringUtils;
28 import be.nikiroo.utils.Version;
29
30 /**
31 * Support class for <a href="http://e621.net/">e621.net</a> and
32 * <a href="http://e926.net/">e926.net</a>, a Furry website supporting comics,
33 * including some of MLP.
34 * <p>
35 * <a href="http://e926.net/">e926.net</a> only shows the "clean" images and
36 * comics, but it can be difficult to browse.
37 *
38 * @author niki
39 */
40 class E621 extends BasicSupport {
41 @Override
42 protected boolean supports(URL url) {
43 String host = url.getHost();
44 if (host.startsWith("www.")) {
45 host = host.substring("www.".length());
46 }
47
48 return ("e621.net".equals(host) || "e926.net".equals(host))
49 && (isPool(url) || isSearchOrSet(url));
50 }
51
52 @Override
53 protected boolean isHtml() {
54 return true;
55 }
56
57 @Override
58 protected MetaData getMeta() throws IOException {
59 MetaData meta = new MetaData();
60
61 meta.setTitle(getTitle());
62 meta.setAuthor(getAuthor());
63 meta.setDate(bsHelper.formatDate(getDate()));
64 meta.setTags(getTags());
65 meta.setUrl(getSource().toString());
66 meta.setUuid(getSource().toString());
67 meta.setLuid("");
68 meta.setLang("en");
69 meta.setSubject("Furry");
70 meta.setImageDocument(true);
71 meta.setCover(getCover());
72 meta.setFakeCover(true);
73
74 return meta;
75 }
76
77 @Override
78 protected String getDesc() throws IOException {
79 if (isSearchOrSet(getSource())) {
80 StringBuilder builder = new StringBuilder();
81 builder.append("<div>");
82 builder.append("A collection of images from ")
83 .append(getSource().getHost()) //
84 .append("<br/>\n") //
85 .append("&nbsp;&nbsp;&nbsp;&nbsp;Time of creation: "
86 + StringUtils.fromTime(new Date().getTime()))
87 .append("<br/>\n") //
88 .append("&nbsp;&nbsp;&nbsp;&nbsp;tTags: ");//
89 for (String tag : getTags()) {
90 builder.append(
91 "\n<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;")
92 .append(tag);
93 }
94 builder.append("\n</div>");
95
96 return builder.toString();
97 }
98
99 if (isPool(getSource())) {
100 Element el = getSourceNode().getElementById("description");
101 if (el != null) {
102 return el.html();
103 }
104 }
105
106 return null;
107 }
108
109 @Override
110 protected List<Entry<String, URL>> getChapters(Progress pg)
111 throws IOException {
112 int i = 1;
113 String jsonUrl = getJsonUrl();
114 if (jsonUrl != null) {
115 for (i = 1; true; i++) {
116 if (i > 1) {
117 try {
118 // The API does not accept more than 2 request per sec,
119 // and asks us to limit at one per sec when possible
120 Thread.sleep(1000);
121 } catch (InterruptedException e) {
122 }
123 }
124
125 try {
126 JSONObject json = getJson(jsonUrl + "&page=" + i, false);
127 if (!json.has("posts"))
128 break;
129 JSONArray posts = json.getJSONArray("posts");
130 if (posts.isEmpty())
131 break;
132 } catch (Exception e) {
133 e.printStackTrace();
134 }
135 }
136
137 // The last page was empty:
138 i--;
139 }
140
141 // The pages and images are in reverse order on /posts/
142 List<Entry<String, URL>> chapters = new LinkedList<Entry<String, URL>>();
143 for (int page = i; page > 0; page--) {
144 chapters.add(new AbstractMap.SimpleEntry<String, URL>(
145 "Page " + Integer.toString(i - page + 1),
146 new URL(jsonUrl + "&page=" + page)));
147 }
148
149 return chapters;
150 }
151
152 @Override
153 protected String getChapterContent(URL chapUrl, int number, Progress pg)
154 throws IOException {
155 StringBuilder builder = new StringBuilder();
156
157 JSONObject json = getJson(chapUrl, false);
158 JSONArray postsArr = json.getJSONArray("posts");
159
160 // The pages and images are in reverse order on /posts/
161 List<JSONObject> posts = new ArrayList<JSONObject>(postsArr.length());
162 for (int i = postsArr.length() - 1; i >= 0; i--) {
163 Object o = postsArr.get(i);
164 if (o instanceof JSONObject)
165 posts.add((JSONObject) o);
166 }
167
168 for (JSONObject post : posts) {
169 if (!post.has("file"))
170 continue;
171 JSONObject file = post.getJSONObject("file");
172 if (!file.has("url"))
173 continue;
174
175 try {
176 String url = file.getString("url");
177 builder.append("[");
178 builder.append(url);
179 builder.append("]<br/>");
180 } catch (JSONException e) {
181 // Can be NULL if filtered
182 // When the value is NULL, we get an exception
183 // but the "has" method still returns true
184 Instance.getInstance().getTraceHandler()
185 .error("Cannot get image for chapter " + number + " of "
186 + getSource());
187 }
188 }
189
190 return builder.toString();
191 }
192
193 @Override
194 protected URL getCanonicalUrl(URL source) {
195 // Convert search-pools into proper pools
196 if (source.getPath().equals("/posts") && source.getQuery() != null
197 && source.getQuery().startsWith("tags=pool%3A")) {
198 String poolNumber = source.getQuery()
199 .substring("tags=pool%3A".length());
200 try {
201 Integer.parseInt(poolNumber);
202 String base = source.getProtocol() + "://" + source.getHost();
203 if (source.getPort() != -1) {
204 base = base + ":" + source.getPort();
205 }
206 source = new URL(base + "/pools/" + poolNumber);
207 } catch (NumberFormatException e) {
208 // Not a simple pool, skip
209 } catch (MalformedURLException e) {
210 // Cannot happen
211 }
212 }
213
214 if (isSetOriginalUrl(source)) {
215 try {
216 Document doc = DataUtil.load(Instance.getInstance().getCache()
217 .open(source, this, false), "UTF-8", source.toString());
218 for (Element shortname : doc
219 .getElementsByClass("set-shortname")) {
220 for (Element el : shortname.getElementsByTag("a")) {
221 if (!el.attr("href").isEmpty())
222 return new URL(el.absUrl("href"));
223 }
224 }
225 } catch (IOException e) {
226 Instance.getInstance().getTraceHandler().error(e);
227 }
228 }
229
230 if (isPool(source)) {
231 try {
232 return new URL(
233 source.toString().replace("/pool/show/", "/pools/"));
234 } catch (MalformedURLException e) {
235 }
236 }
237
238 return super.getCanonicalUrl(source);
239 }
240
241 private String getTitle() {
242 String title = "";
243
244 Element el = getSourceNode().getElementsByTag("title").first();
245 if (el != null) {
246 title = el.text().trim();
247 }
248
249 for (String s : new String[] { "e621", "-", "e621", "Pool", "-" }) {
250 if (title.startsWith(s)) {
251 title = title.substring(s.length()).trim();
252 }
253 if (title.endsWith(s)) {
254 title = title.substring(0, title.length() - s.length()).trim();
255 }
256 }
257
258 if (isSearchOrSet(getSource())) {
259 title = title.isEmpty() ? "e621" : "[e621] " + title;
260 }
261
262 return title;
263 }
264
265 private String getAuthor() {
266 List<String> list = new ArrayList<String>();
267 String jsonUrl = getJsonUrl();
268 if (jsonUrl != null) {
269 try {
270 JSONObject json = getJson(jsonUrl, false);
271 JSONArray posts = json.getJSONArray("posts");
272 for (Object obj : posts) {
273 if (!(obj instanceof JSONObject))
274 continue;
275
276 JSONObject post = (JSONObject) obj;
277 if (!post.has("tags"))
278 continue;
279
280 JSONObject tags = post.getJSONObject("tags");
281 if (!tags.has("artist"))
282 continue;
283
284 JSONArray artists = tags.getJSONArray("artist");
285 for (Object artist : artists) {
286 if (list.contains(artist.toString()))
287 continue;
288
289 list.add(artist.toString());
290 }
291 }
292 } catch (Exception e) {
293 e.printStackTrace();
294 }
295 }
296
297 StringBuilder builder = new StringBuilder();
298 for (String artist : list) {
299 if (builder.length() > 0) {
300 builder.append(", ");
301 }
302 builder.append(artist);
303 }
304
305 return builder.toString();
306 }
307
308 private String getDate() {
309 String jsonUrl = getJsonUrl();
310 if (jsonUrl != null) {
311 try {
312 JSONObject json = getJson(jsonUrl, false);
313 JSONArray posts = json.getJSONArray("posts");
314 for (Object obj : posts) {
315 if (!(obj instanceof JSONObject))
316 continue;
317
318 JSONObject post = (JSONObject) obj;
319 if (!post.has("created_at"))
320 continue;
321
322 return post.getString("created_at");
323 }
324 } catch (Exception e) {
325 e.printStackTrace();
326 }
327 }
328
329 return "";
330 }
331
332 // no tags for pools
333 private List<String> getTags() {
334 List<String> tags = new ArrayList<String>();
335 if (isSearchOrSet(getSource())) {
336 String str = getTagsFromUrl(getSource());
337 for (String tag : str.split("\\+")) {
338 try {
339 tags.add(URLDecoder.decode(tag.trim(), "UTF-8").trim());
340 } catch (UnsupportedEncodingException e) {
341 }
342 }
343 }
344
345 return tags;
346 }
347
348 // returns "xxx+ddd+ggg" if "tags=xxx+ddd+ggg" was present in the query
349 private String getTagsFromUrl(URL url) {
350 String tags = url == null ? "" : url.getQuery();
351 int pos = tags.indexOf("tags=");
352
353 if (pos >= 0) {
354 tags = tags.substring(pos).substring("tags=".length());
355 } else {
356 return "";
357 }
358
359 pos = tags.indexOf('&');
360 if (pos > 0) {
361 tags = tags.substring(0, pos);
362 }
363 pos = tags.indexOf('/');
364 if (pos > 0) {
365 tags = tags.substring(0, pos);
366 }
367
368 return tags;
369 }
370
371 private Image getCover() throws IOException {
372 Image image = null;
373 List<Entry<String, URL>> chapters = getChapters(null);
374 if (!chapters.isEmpty()) {
375 URL chap1Url = chapters.get(0).getValue();
376 String imgsChap1 = getChapterContent(chap1Url, 1, null);
377 if (!imgsChap1.isEmpty()) {
378 imgsChap1 = imgsChap1.split("]")[0].substring(1).trim();
379 image = bsImages.getImage(this, new URL(imgsChap1));
380 }
381 }
382
383 return image;
384 }
385
386 // always /posts.json/ url
387 private String getJsonUrl() {
388 String url = null;
389 if (isSearchOrSet(getSource())) {
390 url = getSource().toString().replace("/posts", "/posts.json");
391 }
392
393 if (isPool(getSource())) {
394 String poolNumber = getSource().getPath()
395 .substring("/pools/".length());
396 url = "https://e621.net/posts.json" + "?tags=pool%3A" + poolNumber;
397 }
398
399 if (url != null) {
400 // Note: one way to override the blacklist
401 String login = Instance.getInstance().getConfig()
402 .getString(Config.LOGIN_E621_LOGIN);
403 String apk = Instance.getInstance().getConfig()
404 .getString(Config.LOGIN_E621_APIKEY);
405
406 if (login != null && !login.isEmpty() && apk != null
407 && !apk.isEmpty()) {
408 url = String.format("%s&login=%s&api_key=%s&_client=%s", url,
409 login, apk, "fanfix-" + Version.getCurrentVersion());
410 }
411 }
412
413 return url;
414 }
415
416 // note: will be removed at getCanonicalUrl()
417 private boolean isSetOriginalUrl(URL originalUrl) {
418 return originalUrl.getPath().startsWith("/post_sets/");
419 }
420
421 private boolean isPool(URL url) {
422 return url.getPath().startsWith("/pools/")
423 || url.getPath().startsWith("/pool/show/");
424 }
425
426 // set will be renamed into search by canonical url
427 private boolean isSearchOrSet(URL url) {
428 return
429 // search:
430 (url.getPath().equals("/posts") && url.getQuery().contains("tags="))
431 // or set:
432 || isSetOriginalUrl(url);
433 }
434 }