1 package be
.nikiroo
.fanfix
.supported
;
3 import java
.io
.IOException
;
4 import java
.io
.UnsupportedEncodingException
;
5 import java
.net
.MalformedURLException
;
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
;
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
;
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
;
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.
35 * <a href="http://e926.net/">e926.net</a> only shows the "clean" images and
36 * comics, but it can be difficult to browse.
40 class E621
extends BasicSupport
{
42 protected boolean supports(URL url
) {
43 String host
= url
.getHost();
44 if (host
.startsWith("www.")) {
45 host
= host
.substring("www.".length());
48 return ("e621.net".equals(host
) || "e926.net".equals(host
))
49 && (isPool(url
) || isSearchOrSet(url
));
53 protected boolean isHtml() {
58 protected MetaData
getMeta() throws IOException
{
59 MetaData meta
= new MetaData();
61 meta
.setTitle(getTitle());
62 meta
.setAuthor(getAuthor());
63 meta
.setDate(bsHelper
.formatDate(getDate()));
64 meta
.setTags(getTags());
65 meta
.setSource(getType().getSourceName());
66 meta
.setUrl(getSource().toString());
67 meta
.setPublisher(getType().getSourceName());
68 meta
.setUuid(getSource().toString());
71 meta
.setSubject("Furry");
72 meta
.setType(getType().toString());
73 meta
.setImageDocument(true);
74 meta
.setCover(getCover());
75 meta
.setFakeCover(true);
81 protected String
getDesc() throws IOException
{
82 if (isSearchOrSet(getSource())) {
83 StringBuilder builder
= new StringBuilder();
84 builder
.append("A collection of images from ")
85 .append(getSource().getHost()).append("\n") //
86 .append("\tTime of creation: "
87 + StringUtils
.fromTime(new Date().getTime()))
89 .append("\tTags: ");//
90 for (String tag
: getTags()) {
91 builder
.append("\t\t").append(tag
);
94 return builder
.toString();
97 if (isPool(getSource())) {
98 Element el
= getSourceNode().getElementById("description");
108 protected List
<Entry
<String
, URL
>> getChapters(Progress pg
)
111 String jsonUrl
= getJsonUrl();
112 if (jsonUrl
!= null) {
113 for (i
= 1; true; i
++) {
116 // The API does not accept more than 2 request per sec,
117 // and asks us to limit at one per sec when possible
119 } catch (InterruptedException e
) {
124 JSONObject json
= getJson(jsonUrl
+ "&page=" + i
, false);
125 if (!json
.has("posts"))
127 JSONArray posts
= json
.getJSONArray("posts");
130 } catch (Exception e
) {
135 // The last page was empty:
139 // The pages and images are in reverse order on /posts/
140 List
<Entry
<String
, URL
>> chapters
= new LinkedList
<Entry
<String
, URL
>>();
141 for (int page
= i
; page
> 0; page
--) {
142 chapters
.add(new AbstractMap
.SimpleEntry
<String
, URL
>(
143 "Page " + Integer
.toString(i
- page
+ 1),
144 new URL(jsonUrl
+ "&page=" + page
)));
151 protected String
getChapterContent(URL chapUrl
, int number
, Progress pg
)
153 StringBuilder builder
= new StringBuilder();
155 JSONObject json
= getJson(chapUrl
, false);
156 JSONArray postsArr
= json
.getJSONArray("posts");
158 // The pages and images are in reverse order on /posts/
159 List
<JSONObject
> posts
= new ArrayList
<JSONObject
>(postsArr
.length());
160 for (int i
= postsArr
.length() - 1; i
>= 0; i
--) {
161 Object o
= postsArr
.get(i
);
162 if (o
instanceof JSONObject
)
163 posts
.add((JSONObject
) o
);
166 for (JSONObject post
: posts
) {
167 if (!post
.has("file"))
169 JSONObject file
= post
.getJSONObject("file");
170 if (!file
.has("url"))
174 String url
= file
.getString("url");
177 builder
.append("]<br/>");
178 } catch (JSONException e
) {
179 // Can be NULL if filtered
180 // When the value is NULL, we get an exception
181 // but the "has" method still returns true
182 Instance
.getInstance().getTraceHandler()
183 .error("Cannot get image for chapter " + number
+ " of "
188 return builder
.toString();
192 protected URL
getCanonicalUrl(URL source
) {
193 // Convert search-pools into proper pools
194 if (source
.getPath().equals("/posts") && source
.getQuery() != null
195 && source
.getQuery().startsWith("tags=pool%3A")) {
196 String poolNumber
= source
.getQuery()
197 .substring("tags=pool%3A".length());
199 Integer
.parseInt(poolNumber
);
200 String base
= source
.getProtocol() + "://" + source
.getHost();
201 if (source
.getPort() != -1) {
202 base
= base
+ ":" + source
.getPort();
204 source
= new URL(base
+ "/pools/" + poolNumber
);
205 } catch (NumberFormatException e
) {
206 // Not a simple pool, skip
207 } catch (MalformedURLException e
) {
212 if (isSetOriginalUrl(source
)) {
214 Document doc
= DataUtil
.load(Instance
.getInstance().getCache()
215 .open(source
, this, false), "UTF-8", source
.toString());
216 for (Element shortname
: doc
217 .getElementsByClass("set-shortname")) {
218 for (Element el
: shortname
.getElementsByTag("a")) {
219 if (!el
.attr("href").isEmpty())
220 return new URL(el
.absUrl("href"));
223 } catch (IOException e
) {
224 Instance
.getInstance().getTraceHandler().error(e
);
228 if (isPool(source
)) {
231 source
.toString().replace("/pool/show/", "/pools/"));
232 } catch (MalformedURLException e
) {
236 return super.getCanonicalUrl(source
);
239 private String
getTitle() {
242 Element el
= getSourceNode().getElementsByTag("title").first();
244 title
= el
.text().trim();
247 for (String s
: new String
[] { "e621", "-", "e621", "Pool", "-" }) {
248 if (title
.startsWith(s
)) {
249 title
= title
.substring(s
.length()).trim();
251 if (title
.endsWith(s
)) {
252 title
= title
.substring(0, title
.length() - s
.length()).trim();
256 if (isSearchOrSet(getSource())) {
257 title
= title
.isEmpty() ?
"e621" : "[e621] " + title
;
263 private String
getAuthor() {
264 List
<String
> list
= new ArrayList
<String
>();
265 String jsonUrl
= getJsonUrl();
266 if (jsonUrl
!= null) {
268 JSONObject json
= getJson(jsonUrl
, false);
269 JSONArray posts
= json
.getJSONArray("posts");
270 for (Object obj
: posts
) {
271 if (!(obj
instanceof JSONObject
))
274 JSONObject post
= (JSONObject
) obj
;
275 if (!post
.has("tags"))
278 JSONObject tags
= post
.getJSONObject("tags");
279 if (!tags
.has("artist"))
282 JSONArray artists
= tags
.getJSONArray("artist");
283 for (Object artist
: artists
) {
284 if (list
.contains(artist
.toString()))
287 list
.add(artist
.toString());
290 } catch (Exception e
) {
295 StringBuilder builder
= new StringBuilder();
296 for (String artist
: list
) {
297 if (builder
.length() > 0) {
298 builder
.append(", ");
300 builder
.append(artist
);
303 return builder
.toString();
306 private String
getDate() {
307 String jsonUrl
= getJsonUrl();
308 if (jsonUrl
!= null) {
310 JSONObject json
= getJson(jsonUrl
, false);
311 JSONArray posts
= json
.getJSONArray("posts");
312 for (Object obj
: posts
) {
313 if (!(obj
instanceof JSONObject
))
316 JSONObject post
= (JSONObject
) obj
;
317 if (!post
.has("created_at"))
320 return post
.getString("created_at");
322 } catch (Exception e
) {
331 private List
<String
> getTags() {
332 List
<String
> tags
= new ArrayList
<String
>();
333 if (isSearchOrSet(getSource())) {
334 String str
= getTagsFromUrl(getSource());
335 for (String tag
: str
.split("\\+")) {
337 tags
.add(URLDecoder
.decode(tag
.trim(), "UTF-8").trim());
338 } catch (UnsupportedEncodingException e
) {
346 // returns "xxx+ddd+ggg" if "tags=xxx+ddd+ggg" was present in the query
347 private String
getTagsFromUrl(URL url
) {
348 String tags
= url
== null ?
"" : url
.getQuery();
349 int pos
= tags
.indexOf("tags=");
352 tags
= tags
.substring(pos
).substring("tags=".length());
357 pos
= tags
.indexOf('&');
359 tags
= tags
.substring(0, pos
);
361 pos
= tags
.indexOf('/');
363 tags
= tags
.substring(0, pos
);
369 private Image
getCover() throws IOException
{
371 List
<Entry
<String
, URL
>> chapters
= getChapters(null);
372 if (!chapters
.isEmpty()) {
373 URL chap1Url
= chapters
.get(0).getValue();
374 String imgsChap1
= getChapterContent(chap1Url
, 1, null);
375 if (!imgsChap1
.isEmpty()) {
376 imgsChap1
= imgsChap1
.split("]")[0].substring(1).trim();
377 image
= bsImages
.getImage(this, new URL(imgsChap1
));
384 // always /posts.json/ url
385 private String
getJsonUrl() {
387 if (isSearchOrSet(getSource())) {
388 url
= getSource().toString().replace("/posts", "/posts.json");
391 if (isPool(getSource())) {
392 String poolNumber
= getSource().getPath()
393 .substring("/pools/".length());
394 url
= "https://e621.net/posts.json" + "?tags=pool%3A" + poolNumber
;
398 // Note: one way to override the blacklist
399 String login
= Instance
.getInstance().getConfig()
400 .getString(Config
.LOGIN_E621_LOGIN
);
401 String apk
= Instance
.getInstance().getConfig()
402 .getString(Config
.LOGIN_E621_APIKEY
);
404 if (login
!= null && !login
.isEmpty() && apk
!= null
406 url
= String
.format("%s&login=%s&api_key=%s&_client=%s", url
,
407 login
, apk
, "fanfix-" + Version
.getCurrentVersion());
414 // note: will be removed at getCanonicalUrl()
415 private boolean isSetOriginalUrl(URL originalUrl
) {
416 return originalUrl
.getPath().startsWith("/post_sets/");
419 private boolean isPool(URL url
) {
420 return url
.getPath().startsWith("/pools/")
421 || url
.getPath().startsWith("/pool/show/");
424 // set will be renamed into search by canonical url
425 private boolean isSearchOrSet(URL url
) {
428 (url
.getPath().equals("/posts") && url
.getQuery().contains("tags="))
430 || isSetOriginalUrl(url
);