update from master
[fanfix.git] / library / WebLibraryServer.java
CommitLineData
f433d153
NR
1package be.nikiroo.fanfix.library;
2
3import java.io.ByteArrayInputStream;
f433d153
NR
4import java.io.IOException;
5import java.io.InputStream;
4536c5cf 6import java.net.URL;
f433d153 7import java.util.ArrayList;
4536c5cf 8import java.util.Arrays;
f433d153
NR
9import java.util.HashMap;
10import java.util.LinkedList;
11import java.util.List;
12import java.util.Map;
13
f433d153
NR
14import org.json.JSONArray;
15import org.json.JSONObject;
16
17import be.nikiroo.fanfix.Instance;
18import be.nikiroo.fanfix.bundles.Config;
f433d153
NR
19import be.nikiroo.fanfix.data.Chapter;
20import be.nikiroo.fanfix.data.JsonIO;
21import be.nikiroo.fanfix.data.MetaData;
22import be.nikiroo.fanfix.data.Paragraph;
23import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
24import be.nikiroo.fanfix.data.Story;
f433d153 25import be.nikiroo.utils.Image;
c91f1830 26import be.nikiroo.utils.LoginResult;
f433d153 27import be.nikiroo.utils.NanoHTTPD;
f433d153
NR
28import be.nikiroo.utils.NanoHTTPD.Response;
29import be.nikiroo.utils.NanoHTTPD.Response.Status;
4536c5cf 30import be.nikiroo.utils.Progress;
b063fe0a 31import be.nikiroo.utils.Progress.ProgressListener;
f433d153 32
c91f1830
NR
33public class WebLibraryServer extends WebLibraryServerHtml {
34 class WLoginResult extends LoginResult {
c91f1830
NR
35 public WLoginResult(boolean badLogin, boolean badCookie) {
36 super(badLogin, badCookie);
37 }
38
39 public WLoginResult(String who, String key, String subkey, boolean rw,
40 boolean wl, boolean bl) {
41 super(who, key, subkey, (rw ? "|rw" : "") + (wl ? "|wl" : "")
42 + (bl ? "|bl" : "") + "|");
f433d153
NR
43 }
44
c91f1830 45 public WLoginResult(String cookie, String who, String key,
f433d153 46 List<String> subkeys) {
c91f1830
NR
47 super(cookie, who, key, subkeys,
48 subkeys == null || subkeys.isEmpty());
f433d153
NR
49 }
50
51 public boolean isRw() {
c91f1830 52 return getOption().contains("|rw|");
f433d153
NR
53 }
54
55 public boolean isWl() {
c91f1830 56 return getOption().contains("|wl|");
f433d153
NR
57 }
58
c91f1830
NR
59 public boolean isBl() {
60 return getOption().contains("|bl|");
f433d153
NR
61 }
62 }
63
f433d153
NR
64 private Map<String, Story> storyCache = new HashMap<String, Story>();
65 private LinkedList<String> storyCacheOrder = new LinkedList<String>();
66 private long storyCacheSize = 0;
67 private long maxStoryCacheSize;
c91f1830
NR
68
69 private List<String> whitelist;
70 private List<String> blacklist;
f433d153 71
4536c5cf
NR
72 private Map<String, Progress> imprts = new HashMap<String, Progress>();
73
f433d153 74 public WebLibraryServer(boolean secure) throws IOException {
c91f1830 75 super(secure);
f433d153
NR
76
77 int cacheMb = Instance.getInstance().getConfig()
78 .getInteger(Config.SERVER_MAX_CACHE_MB, 100);
79 maxStoryCacheSize = cacheMb * 1024 * 1024;
80
81 setTraceHandler(Instance.getInstance().getTraceHandler());
82
c91f1830
NR
83 whitelist = Instance.getInstance().getConfig()
84 .getList(Config.SERVER_WHITELIST, new ArrayList<String>());
85 blacklist = Instance.getInstance().getConfig()
86 .getList(Config.SERVER_BLACKLIST, new ArrayList<String>());
f433d153
NR
87 }
88
89 /**
90 * Start the server (listen on the network for new connections).
91 * <p>
92 * Can only be called once.
93 * <p>
94 * This call is asynchronous, and will just start a new {@link Thread} on
95 * itself (see {@link WebLibraryServer#run()}).
96 */
97 public void start() {
98 new Thread(this).start();
99 }
100
c91f1830
NR
101 @Override
102 protected WLoginResult login(boolean badLogin, boolean badCookie) {
103 return new WLoginResult(false, false);
f433d153
NR
104 }
105
c91f1830
NR
106 @Override
107 protected WLoginResult login(String who, String cookie) {
108 List<String> subkeys = Instance.getInstance().getConfig()
109 .getList(Config.SERVER_ALLOWED_SUBKEYS);
f433d153
NR
110 String realKey = Instance.getInstance().getConfig()
111 .getString(Config.SERVER_KEY);
c91f1830
NR
112
113 return new WLoginResult(cookie, who, realKey, subkeys);
f433d153
NR
114 }
115
116 // allow rw/wl
c91f1830
NR
117 @Override
118 protected WLoginResult login(String who, String key, String subkey) {
f433d153 119 String realKey = Instance.getInstance().getConfig()
c91f1830 120 .getString(Config.SERVER_KEY, "");
f433d153
NR
121
122 // I don't like NULLs...
f433d153
NR
123 key = key == null ? "" : key;
124 subkey = subkey == null ? "" : subkey;
125
126 if (!realKey.equals(key)) {
c91f1830 127 return new WLoginResult(true, false);
f433d153
NR
128 }
129
c91f1830 130 // defaults are true (as previous versions without the feature)
f433d153
NR
131 boolean rw = true;
132 boolean wl = true;
c91f1830 133 boolean bl = true;
f433d153
NR
134
135 rw = Instance.getInstance().getConfig().getBoolean(Config.SERVER_RW,
136 rw);
f433d153 137
c91f1830
NR
138 List<String> allowed = Instance.getInstance().getConfig().getList(
139 Config.SERVER_ALLOWED_SUBKEYS, new ArrayList<String>());
f433d153 140
c91f1830
NR
141 if (!allowed.isEmpty()) {
142 if (!allowed.contains(subkey)) {
143 return new WLoginResult(true, false);
144 }
f433d153 145
c91f1830
NR
146 if ((subkey + "|").contains("|rw|")) {
147 rw = true;
148 }
149 if ((subkey + "|").contains("|wl|")) {
150 wl = false; // |wl| = bypass whitelist
151 }
152 if ((subkey + "|").contains("|bl|")) {
153 bl = false; // |bl| = bypass blacklist
154 }
f433d153
NR
155 }
156
c91f1830 157 return new WLoginResult(who, key, subkey, rw, wl, bl);
f433d153
NR
158 }
159
c91f1830
NR
160 @Override
161 protected Response getList(String uri, WLoginResult login)
f433d153 162 throws IOException {
c91f1830 163 if (WebLibraryUrls.LIST_URL_METADATA.equals(uri)) {
f433d153 164 List<JSONObject> jsons = new ArrayList<JSONObject>();
c91f1830 165 for (MetaData meta : metas(login)) {
f433d153
NR
166 jsons.add(JsonIO.toJson(meta));
167 }
168
169 return newInputStreamResponse("application/json",
170 new ByteArrayInputStream(
171 new JSONArray(jsons).toString().getBytes()));
172 }
173
174 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
175 NanoHTTPD.MIME_PLAINTEXT, null);
176 }
177
f433d153
NR
178 // /story/luid/chapter/para <-- text/image
179 // /story/luid/cover <-- image
180 // /story/luid/metadata <-- json
4b3d19dc 181 // /story/luid/json <-- json, whole chapter (no images)
c91f1830
NR
182 @Override
183 protected Response getStoryPart(String uri, WLoginResult login) {
4536c5cf 184 String[] uriParts = uri.split("/");
f433d153
NR
185 int off = 2;
186
4536c5cf 187 if (uriParts.length < off + 2) {
f433d153
NR
188 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
189 NanoHTTPD.MIME_PLAINTEXT, null);
190 }
191
4536c5cf
NR
192 String luid = uriParts[off + 0];
193 String chapterStr = uriParts[off + 1];
194 String imageStr = uriParts.length < off + 3 ? null : uriParts[off + 2];
f433d153
NR
195
196 // 1-based (0 = desc)
197 int chapter = 0;
198 if (chapterStr != null && !"cover".equals(chapterStr)
b459e462
NR
199 && !"metadata".equals(chapterStr)
200 && !"json".equals(chapterStr)) {
f433d153
NR
201 try {
202 chapter = Integer.parseInt(chapterStr);
203 if (chapter < 0) {
204 throw new NumberFormatException();
205 }
206 } catch (NumberFormatException e) {
207 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
208 NanoHTTPD.MIME_PLAINTEXT, "Chapter is not valid");
209 }
210 }
211
212 // 1-based
213 int paragraph = 1;
214 if (imageStr != null) {
215 try {
216 paragraph = Integer.parseInt(imageStr);
217 if (paragraph < 0) {
218 throw new NumberFormatException();
219 }
220 } catch (NumberFormatException e) {
221 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
222 NanoHTTPD.MIME_PLAINTEXT, "Paragraph is not valid");
223 }
224 }
225
226 String mimeType = NanoHTTPD.MIME_PLAINTEXT;
227 InputStream in = null;
228 try {
229 if ("cover".equals(chapterStr)) {
4536c5cf 230 Image img = storyCover(luid, login);
f433d153
NR
231 if (img != null) {
232 in = img.newInputStream();
233 }
b459e462
NR
234 // TODO: get correct image type
235 mimeType = "image/png";
f433d153 236 } else if ("metadata".equals(chapterStr)) {
c91f1830 237 MetaData meta = meta(luid, login);
f433d153
NR
238 JSONObject json = JsonIO.toJson(meta);
239 mimeType = "application/json";
240 in = new ByteArrayInputStream(json.toString().getBytes());
b459e462 241 } else if ("json".equals(chapterStr)) {
c91f1830 242 Story story = story(luid, login);
4b3d19dc
NR
243 JSONObject json = JsonIO.toJson(story);
244 mimeType = "application/json";
245 in = new ByteArrayInputStream(json.toString().getBytes());
f433d153 246 } else {
c91f1830 247 Story story = story(luid, login);
f433d153
NR
248 if (story != null) {
249 if (chapter == 0) {
250 StringBuilder builder = new StringBuilder();
251 for (Paragraph p : story.getMeta().getResume()) {
252 if (builder.length() == 0) {
253 builder.append("\n");
254 }
255 builder.append(p.getContent());
256 }
257
258 in = new ByteArrayInputStream(
259 builder.toString().getBytes("utf-8"));
260 } else {
261 Paragraph para = story.getChapters().get(chapter - 1)
262 .getParagraphs().get(paragraph - 1);
263 Image img = para.getContentImage();
264 if (para.getType() == ParagraphType.IMAGE) {
265 // TODO: get correct image type
266 mimeType = "image/png";
267 in = img.newInputStream();
268 } else {
269 in = new ByteArrayInputStream(
270 para.getContent().getBytes("utf-8"));
271 }
272 }
273 }
274 }
275 } catch (IndexOutOfBoundsException e) {
276 return NanoHTTPD.newFixedLengthResponse(Status.NOT_FOUND,
277 NanoHTTPD.MIME_PLAINTEXT,
278 "Chapter or paragraph does not exist");
279 } catch (IOException e) {
280 Instance.getInstance().getTraceHandler()
281 .error(new IOException("Cannot get image: " + uri, e));
282 return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR,
283 NanoHTTPD.MIME_PLAINTEXT, "Error when processing request");
284 }
285
286 return newInputStreamResponse(mimeType, in);
287 }
288
4536c5cf
NR
289 // /story/luid/source
290 // /story/luid/title
291 // /story/luid/author
292 @Override
293 protected Response setStoryPart(String uri, String value,
294 WLoginResult login) throws IOException {
295 String[] uriParts = uri.split("/");
296 int off = 2; // "" and "story"
297
298 if (uriParts.length < off + 2) {
299 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
300 NanoHTTPD.MIME_PLAINTEXT, "Invalid story part request");
301 }
302
159e970c
NR
303 if (!login.isRw()) {
304 return NanoHTTPD.newFixedLengthResponse(Status.FORBIDDEN,
305 NanoHTTPD.MIME_PLAINTEXT, "SET story part not allowed");
306 }
307
4536c5cf
NR
308 String luid = uriParts[off + 0];
309 String type = uriParts[off + 1];
310
311 if (!Arrays.asList("source", "title", "author").contains(type)) {
312 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
313 NanoHTTPD.MIME_PLAINTEXT,
314 "Invalid SET story part: " + type);
315 }
316
317 if (meta(luid, login) != null) {
318 BasicLibrary lib = Instance.getInstance().getLibrary();
319 if ("source".equals(type)) {
320 lib.changeSource(luid, value, null);
321 } else if ("title".equals(type)) {
322 lib.changeTitle(luid, value, null);
323 } else if ("author".equals(type)) {
324 lib.changeAuthor(luid, value, null);
325 }
326 }
327
328 return newInputStreamResponse(NanoHTTPD.MIME_PLAINTEXT, null);
329 }
330
331 @Override
332 protected Response getCover(String uri, WLoginResult login)
333 throws IOException {
334 String[] uriParts = uri.split("/");
335 int off = 2; // "" and "cover"
336
337 if (uriParts.length < off + 2) {
338 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
339 NanoHTTPD.MIME_PLAINTEXT, "Invalid cover request");
340 }
341
342 String type = uriParts[off + 0];
343 String id = uriParts[off + 1];
344
345 InputStream in = null;
346
347 if ("story".equals(type)) {
348 Image img = storyCover(id, login);
349 if (img != null) {
350 in = img.newInputStream();
351 }
352 } else if ("source".equals(type)) {
353 Image img = sourceCover(id, login);
354 if (img != null) {
355 in = img.newInputStream();
356 }
357 } else if ("author".equals(type)) {
358 Image img = authorCover(id, login);
359 if (img != null) {
360 in = img.newInputStream();
361 }
362 } else {
363 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
364 NanoHTTPD.MIME_PLAINTEXT,
365 "Invalid GET cover type: " + type);
366 }
367
368 // TODO: get correct image type
369 return newInputStreamResponse("image/png", in);
370 }
371
372 @Override
373 protected Response setCover(String uri, String luid, WLoginResult login)
374 throws IOException {
375 String[] uriParts = uri.split("/");
376 int off = 2; // "" and "cover"
377
378 if (uriParts.length < off + 2) {
379 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
380 NanoHTTPD.MIME_PLAINTEXT, "Invalid cover request");
381 }
382
159e970c
NR
383 if (!login.isRw()) {
384 return NanoHTTPD.newFixedLengthResponse(Status.FORBIDDEN,
385 NanoHTTPD.MIME_PLAINTEXT, "Cover request not allowed");
386 }
387
4536c5cf
NR
388 String type = uriParts[off + 0];
389 String id = uriParts[off + 1];
390
391 if ("source".equals(type)) {
392 sourceCover(id, login, luid);
393 } else if ("author".equals(type)) {
394 authorCover(id, login, luid);
395 } else {
396 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
397 NanoHTTPD.MIME_PLAINTEXT,
398 "Invalid SET cover type: " + type);
399 }
400
401 return newInputStreamResponse(NanoHTTPD.MIME_PLAINTEXT, null);
402 }
403
404 @Override
405 protected Response imprt(String uri, String urlStr, WLoginResult login)
406 throws IOException {
407 final BasicLibrary lib = Instance.getInstance().getLibrary();
408
159e970c
NR
409 if (!login.isRw()) {
410 return NanoHTTPD.newFixedLengthResponse(Status.FORBIDDEN,
411 NanoHTTPD.MIME_PLAINTEXT, "Import not allowed");
412 }
413
4536c5cf
NR
414 final URL url = new URL(urlStr);
415 final Progress pg = new Progress();
416 final String luid = lib.getNextId();
417
b063fe0a
NR
418 // Keep the latest name
419 pg.addProgressListener(new ProgressListener() {
420 @Override
421 public void progress(Progress progress, String name) {
422 pg.setName(name);
423 }
424 });
425
4536c5cf
NR
426 synchronized (imprts) {
427 imprts.put(luid, pg);
428 }
429
430 new Thread(new Runnable() {
431 @Override
432 public void run() {
433 try {
cd1aa392 434 lib.imprt(url, luid, pg);
4536c5cf
NR
435 } catch (IOException e) {
436 Instance.getInstance().getTraceHandler().error(e);
437 } finally {
438 synchronized (imprts) {
439 imprts.remove(luid);
440 }
441 }
442 }
443 }, "Import story: " + urlStr).start();
444
4536c5cf
NR
445 return NanoHTTPD.newFixedLengthResponse(Status.OK,
446 NanoHTTPD.MIME_PLAINTEXT, luid);
447 }
448
449 @Override
450 protected Response imprtProgress(String uri, WLoginResult login) {
451 String[] uriParts = uri.split("/");
452 int off = 2; // "" and "import"
453
454 if (uriParts.length < off + 1) {
455 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
456 NanoHTTPD.MIME_PLAINTEXT, "Invalid cover request");
457 }
458
459 String luid = uriParts[off + 0];
460
461 Progress pg = null;
462 synchronized (imprts) {
463 pg = imprts.get(luid);
464 }
465 if (pg != null) {
466 return NanoHTTPD.newFixedLengthResponse(Status.OK,
467 "application/json", JsonIO.toJson(pg).toString());
468 }
469
470 return newInputStreamResponse(NanoHTTPD.MIME_PLAINTEXT, null);
471 }
472
159e970c
NR
473 @Override
474 protected Response delete(String uri, WLoginResult login)
475 throws IOException {
476 String[] uriParts = uri.split("/");
477 int off = 2; // "" and "delete"
478
479 if (uriParts.length < off + 1) {
480 return NanoHTTPD.newFixedLengthResponse(Status.BAD_REQUEST,
481 NanoHTTPD.MIME_PLAINTEXT, "Invalid delete request");
482 }
483
484 if (!login.isRw()) {
485 return NanoHTTPD.newFixedLengthResponse(Status.FORBIDDEN,
486 NanoHTTPD.MIME_PLAINTEXT, "Delete not allowed");
487 }
488
489 String luid = uriParts[off + 0];
490
491 BasicLibrary lib = Instance.getInstance().getLibrary();
492 lib.delete(luid);
493
494 return newInputStreamResponse(NanoHTTPD.MIME_PLAINTEXT, null);
495 }
496
c91f1830
NR
497 @Override
498 protected List<MetaData> metas(WLoginResult login) throws IOException {
f433d153 499 BasicLibrary lib = Instance.getInstance().getLibrary();
c91f1830
NR
500 List<MetaData> metas = new ArrayList<MetaData>();
501 for (MetaData meta : lib.getList().getMetas()) {
502 if (isAllowed(meta, login)) {
503 metas.add(meta);
504 }
f433d153
NR
505 }
506
c91f1830 507 return metas;
f433d153
NR
508 }
509
510 // NULL if not whitelist OK or if not found
c91f1830
NR
511 @Override
512 protected Story story(String luid, WLoginResult login) throws IOException {
f433d153
NR
513 synchronized (storyCache) {
514 if (storyCache.containsKey(luid)) {
515 Story story = storyCache.get(luid);
c91f1830 516 if (!isAllowed(story.getMeta(), login))
f433d153 517 return null;
f433d153
NR
518
519 return story;
520 }
521 }
522
523 Story story = null;
c91f1830 524 MetaData meta = meta(luid, login);
f433d153
NR
525 if (meta != null) {
526 BasicLibrary lib = Instance.getInstance().getLibrary();
527 story = lib.getStory(luid, null);
528 long size = sizeOf(story);
529
530 synchronized (storyCache) {
531 // Could have been added by another request
532 if (!storyCache.containsKey(luid)) {
533 while (!storyCacheOrder.isEmpty()
534 && storyCacheSize + size > maxStoryCacheSize) {
535 String oldestLuid = storyCacheOrder.removeFirst();
536 Story oldestStory = storyCache.remove(oldestLuid);
537 maxStoryCacheSize -= sizeOf(oldestStory);
538 }
539
540 storyCacheOrder.add(luid);
541 storyCache.put(luid, story);
542 }
543 }
544 }
545
546 return story;
547 }
548
c91f1830
NR
549 private MetaData meta(String luid, WLoginResult login) throws IOException {
550 BasicLibrary lib = Instance.getInstance().getLibrary();
551 MetaData meta = lib.getInfo(luid);
552 if (!isAllowed(meta, login))
553 return null;
f433d153 554
c91f1830 555 return meta;
f433d153
NR
556 }
557
4536c5cf
NR
558 private Image storyCover(String luid, WLoginResult login)
559 throws IOException {
c91f1830
NR
560 MetaData meta = meta(luid, login);
561 if (meta != null) {
562 BasicLibrary lib = Instance.getInstance().getLibrary();
563 return lib.getCover(meta.getLuid());
f433d153 564 }
f433d153 565
c91f1830 566 return null;
f433d153
NR
567 }
568
4536c5cf
NR
569 private Image authorCover(String author, WLoginResult login)
570 throws IOException {
571 Image img = null;
572
573 List<MetaData> metas = new MetaResultList(metas(login)).filter(null,
574 author, null);
575 if (metas.size() > 0) {
576 BasicLibrary lib = Instance.getInstance().getLibrary();
577 img = lib.getCustomAuthorCover(author);
578 if (img == null)
579 img = lib.getCover(metas.get(0).getLuid());
580 }
581
582 return img;
583
584 }
585
586 private void authorCover(String author, WLoginResult login, String luid)
587 throws IOException {
588 if (meta(luid, login) != null) {
589 List<MetaData> metas = new MetaResultList(metas(login)).filter(null,
590 author, null);
591 if (metas.size() > 0) {
592 BasicLibrary lib = Instance.getInstance().getLibrary();
593 lib.setAuthorCover(author, luid);
594 }
595 }
596 }
597
598 private Image sourceCover(String source, WLoginResult login)
599 throws IOException {
600 Image img = null;
601
602 List<MetaData> metas = new MetaResultList(metas(login)).filter(source,
603 null, null);
604 if (metas.size() > 0) {
605 BasicLibrary lib = Instance.getInstance().getLibrary();
606 img = lib.getCustomSourceCover(source);
607 if (img == null)
608 img = lib.getCover(metas.get(0).getLuid());
609 }
610
611 return img;
612 }
613
614 private void sourceCover(String source, WLoginResult login, String luid)
615 throws IOException {
616 if (meta(luid, login) != null) {
617 List<MetaData> metas = new MetaResultList(metas(login))
618 .filter(source, null, null);
619 if (metas.size() > 0) {
620 BasicLibrary lib = Instance.getInstance().getLibrary();
621 lib.setSourceCover(source, luid);
622 }
623 }
624 }
625
c91f1830 626 private boolean isAllowed(MetaData meta, WLoginResult login) {
4536c5cf
NR
627 MetaResultList one = new MetaResultList(Arrays.asList(meta));
628 if (login.isWl() && !whitelist.isEmpty()) {
629 if (one.filter(whitelist, null, null).isEmpty()) {
630 return false;
631 }
f433d153 632 }
4536c5cf
NR
633 if (login.isBl() && !blacklist.isEmpty()) {
634 if (!one.filter(blacklist, null, null).isEmpty()) {
635 return false;
636 }
b459e462
NR
637 }
638
c91f1830 639 return true;
b459e462
NR
640 }
641
c91f1830
NR
642 private long sizeOf(Story story) {
643 long size = 0;
644 for (Chapter chap : story) {
645 for (Paragraph para : chap) {
646 if (para.getType() == ParagraphType.IMAGE) {
647 size += para.getContentImage().getSize();
648 } else {
649 size += para.getContent().length();
650 }
651 }
b459e462
NR
652 }
653
c91f1830 654 return size;
b459e462
NR
655 }
656
657 public static void main(String[] args) throws IOException {
658 Instance.init();
659 WebLibraryServer web = new WebLibraryServer(false);
660 web.run();
661 }
f433d153 662}