View Javadoc
1   /******************************************************************************
2    * LibraryManagerImpl.java - Synchronize access to the library
3    * 
4    * BuckoVidLib - The BuckoSoft Video Library
5    * Copyright(c) 2014 - Dick Balaska
6    *
7    */
8   package com.buckosoft.BuckoVidLib.business;
9   
10  import java.io.File;
11  import java.io.FileNotFoundException;
12  import java.io.FileOutputStream;
13  import java.io.IOException;
14  import java.io.UnsupportedEncodingException;
15  import java.net.URLEncoder;
16  import java.text.SimpleDateFormat;
17  import java.util.ArrayList;
18  import java.util.Collection;
19  import java.util.Collections;
20  import java.util.Comparator;
21  import java.util.Date;
22  import java.util.List;
23  import java.util.Set;
24  import java.util.Timer;
25  import java.util.TimerTask;
26  import java.util.regex.Pattern;
27  
28  import org.apache.commons.io.FileUtils;
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  import org.springframework.beans.factory.annotation.Autowired;
32  import org.springframework.stereotype.Component;
33  
34  import tv.plex.domain.Directory;
35  import tv.plex.domain.MediaContainer;
36  
37  import com.buckosoft.BuckoVidLib.db.Database;
38  import com.buckosoft.BuckoVidLib.domain.Actor;
39  import com.buckosoft.BuckoVidLib.domain.Director;
40  import com.buckosoft.BuckoVidLib.domain.Genre;
41  import com.buckosoft.BuckoVidLib.domain.Library;
42  import com.buckosoft.BuckoVidLib.domain.LibrarySection;
43  import com.buckosoft.BuckoVidLib.domain.LibrarySection.Type;
44  import com.buckosoft.BuckoVidLib.domain.TVSeason;
45  import com.buckosoft.BuckoVidLib.domain.TVShow;
46  import com.buckosoft.BuckoVidLib.domain.Video;
47  import com.buckosoft.BuckoVidLib.domain.VideoBase;
48  import com.buckosoft.BuckoVidLib.domain.VideoTexts;
49  import com.buckosoft.BuckoVidLib.domain.Writer;
50  import com.buckosoft.BuckoVidLib.domain.rest.admin.RestRefreshStatus;
51  import com.buckosoft.BuckoVidLib.domain.rest.admin.RestStatusString;
52  import com.buckosoft.BuckoVidLib.util.ConfigManager;
53  import com.buckosoft.BuckoVidLib.util.DomainConverter;
54  import com.buckosoft.BuckoVidLib.web.ImageController;
55  import com.buckosoft.BuckoVidLib.web.PosterController;
56  import com.buckosoft.BuckoVidLib.web.plex.client.ClientFactory;
57  import com.buckosoft.BuckoVidLib.web.plex.client.LibraryService;
58  
59  /** All access to the library goes through this module.
60   * This is so we can synchronize access, free up stale memory, 
61   * and check for new library entries.
62   * This should be the only class that uses tv.plex.domain, all others use the buckosoft domain objects
63   * @author dick
64   * @since 2014-10-18
65   */
66  @Component
67  public class LibraryManagerImpl implements LibraryManager {
68  	private final Log log = LogFactory.getLog(getClass());
69  
70  	@Autowired
71  	private	Database		db;
72  	
73  	@Autowired
74  	private	PosterController	posterController;
75  	
76  	@Autowired
77  	private	ImageController		imageController;
78  	
79  	private	Library			library = null;
80  	private	long			lastUpdateTime = 0;
81  	private LibraryService	libraryService;
82  	private List<String>	skipSections;
83  	private	boolean			continueOnError = true;
84  	private	boolean			logExceptions = true;
85  
86  	private	final static String	mtPrimary = "primary";
87  	private	final static String	mtSuccess = "success";
88  	private	final static String	mtInfo = "info";
89  	private	final static String	mtWarning = "warning";
90  	private	final static String	mtDanger = "danger";
91  
92  	public LibraryManagerImpl() {
93  		libraryService = ClientFactory.getLibraryClient();
94  		skipSections = setupSkipSections();
95  		setupRecentTimerCheck();
96  		lastUpdateTime = new Date().getTime();
97  		continueOnError = ConfigManager.getBoolean("BuckoVidLib.continueOnError", true);
98  		logExceptions = ConfigManager.getBoolean("BuckoVidLib.logExceptions", true);
99  
100 		
101 	}
102 
103 
104 	/* (non-Javadoc)
105 	 * @see com.buckosoft.BuckoVidLib.business.LibraryManager#getLastUpdateTime()
106 	 */
107 	@Override
108 	public long getLastUpdateTime() {
109 		return(this.lastUpdateTime);
110 	}
111 
112 
113 	@Override
114 	public List<LibrarySection> getSectionList() {
115 		if (library == null)
116 			loadLibrary();
117 		synchronized (library) {
118 			return(library.getLibrarySections());
119 		}
120 	}
121 
122 	/* (non-Javadoc)
123 	 * @see com.buckosoft.BuckoVidLib.business.LibraryManager#getSectionList(boolean)
124 	 */
125 	@Override
126 	public List<LibrarySection> getSectionList(boolean allowRestricted) {
127 		if (library == null)
128 			loadLibrary();
129 		synchronized (library) {
130 			if (allowRestricted)
131 				return(library.getLibrarySections());
132 			return(library.getUnrestrictedLibrarySections());
133 		}
134 	}
135 
136 	/* (non-Javadoc)
137 	 * @see com.buckosoft.BuckoVidLib.business.LibraryManager#getRandomMovie(boolean)
138 	 */
139 	@Override
140 	public VideoBase getRandomMovie(boolean allowRestricted) {
141 		if (library == null)
142 			loadLibrary();
143 		// calcualte total number to choose from
144 		int	total = 0;
145 		VideoBase choice = null;
146 		synchronized (library) {
147 			List<LibrarySection> list = getSectionList(allowRestricted); 
148 			for (LibrarySection ls : list)
149 				total += ls.getVideos().size();
150 			int target = (int)(double)(Math.random()*(double)total);
151 			log.info("random " + target + " of " + total);
152 			for (LibrarySection ls : list) {
153 				if (ls.getVideos().size() > target) {
154 					// pick here
155 					choice = ls.getVideos().get(target);
156 					break;
157 				}
158 				target -= ls.getVideos().size();
159 			}
160 		}
161 		if (choice == null) {
162 			log.warn("Failed to pick a random movie");
163 			return(null);
164 		}
165 		return(choice);
166 	}
167 
168 
169 	/* (non-Javadoc)
170 	 * @see com.buckosoft.BuckoVidLib.business.LibraryManager#getVideoFromKey(int, boolean)
171 	 */
172 	@Override
173 	public Video getVideoFromKey(int key, boolean allowRestricted) {
174 		if (library == null)
175 			loadLibrary();
176 		log.info("getVideoFromKey: " + DomainConverter.intToKey(key) + " allowRestrict?: " + allowRestricted);
177 		VideoBase vb = library.getVideoBase(key);
178 		if (vb == null)
179 			return(null);
180 		Video v = db.getVideo(vb.getId());
181 		if (v == null)
182 			return(null);
183 //		if (v == null) {
184 //			log.warn("Failed to get video. Let's try a list of them...");
185 //			List<Video> list = db.getVideos(vb.getId());
186 //			log.warn("We got " + list.size() + " videos for key " + vb.getId());
187 //		}
188 		v.setVideoTexts(db.getVideoTexts(v.getId()));
189 		if (v instanceof TVShow) {
190 			TVShow tv = (TVShow)v;
191 			tv.setSeasons(db.getTVSeasons(v.getId()));
192 		}
193 		if (allowRestricted)
194 			return(v);
195 		if (isRestricted(v))
196 			return(null);
197 		return(v);
198 	}
199 
200 	private boolean isRestricted(VideoBase v) {
201 		for (LibrarySection ls : library.getUnrestrictedLibrarySections()) {
202 			if (ls.getVideos().contains(v))
203 				return(false);
204 		}
205 		return(true);
206 	}
207 
208 	/* (non-Javadoc)
209 	 * @see com.buckosoft.BuckoVidLib.business.LibraryManager#getVideoBaseFromKey(int, boolean)
210 	 */
211 	@Override
212 	public VideoBase getVideoBaseFromKey(int key, boolean allowRestricted) {
213 		if (library == null)
214 			loadLibrary();
215 		log.debug("getVideoBaseFromKey: " + DomainConverter.intToKey(key) + " AR?: " + allowRestricted);
216 		VideoBase v = library.getVideoBase(key);
217 		if (allowRestricted)
218 			return(v);
219 		if (isRestricted(v))
220 			return(null);
221 		return(v);
222 	}
223 
224 	@Override
225 	public List<VideoBase>	getNewestVideos(int count, boolean allowRestricted) {
226 		if (library == null)
227 			loadLibrary();
228 		List<VideoBase> vl;
229 		List<VideoBase> vla = new ArrayList<VideoBase>();
230 		synchronized (library) {
231 			vl = library.getNewestVideos();
232 			for (VideoBase v : vl) {
233 				if (allowRestricted || !isRestricted(v)) {
234 					vla.add(v);
235 					if (vla.size() >= count)
236 						return(vla);
237 				}
238 			}
239 		}
240 		return(vla);
241 	}
242 
243 	
244 	/* (non-Javadoc)
245 	 * @see com.buckosoft.BuckoVidLib.business.LibraryManager#findVideos(java.lang.String, boolean, int)
246 	 */
247 	@Override
248 	public List<VideoBase> findVideos(Pattern pattern, boolean allowRestricted, int maxReturned) {
249 		if (library == null)
250 			loadLibrary();
251 		List<VideoBase> vbl = new ArrayList<VideoBase>();
252 		synchronized (library) {
253 			for (VideoBase v : library.getVideoMap().values()) {
254 				if (allowRestricted || !isRestricted(v)) {
255 					//if (!v.getTitle().matches("(?i).*"+key + ".*"))
256 					if (!pattern.matcher(v.getTitle()).matches())
257 						continue;
258 					vbl.add(v);
259 					if (vbl.size() >= maxReturned)
260 						break;
261 				}
262 			}
263 		}
264 		Collections.sort(vbl, videoBaseNameSort);
265 		return(vbl);
266 	}
267 	private class VideoBaseNameSort implements Comparator<VideoBase> {
268 		@Override
269 		public int compare(VideoBase arg0, VideoBase arg1) {
270 			return(arg0.getTitle().compareToIgnoreCase(arg1.getTitle()));
271 		}
272 	}
273 	private	VideoBaseNameSort videoBaseNameSort = new VideoBaseNameSort();
274 
275 
276 	/* (non-Javadoc)
277 	 * @see com.buckosoft.BuckoVidLib.business.LibraryManager#findActors(java.util.regex.Pattern, int)
278 	 */
279 	@Override
280 	public List<Actor> findActors(String key, int maxReturned) {
281 		if (library == null)
282 			loadLibrary();
283 		List<Actor> la = db.findActors(key, maxReturned);
284 		Collections.sort(la, actorNameSort);
285 		return(la);
286 	}
287 	private class ActorNameSort implements Comparator<Actor> {
288 		@Override
289 		public int compare(Actor arg0, Actor arg1) {
290 			return(arg0.getName().compareToIgnoreCase(arg1.getName()));
291 		}
292 	}
293 	private	ActorNameSort actorNameSort = new ActorNameSort();
294 
295 
296 	/* (non-Javadoc)
297 	 * @see com.buckosoft.BuckoVidLib.business.LibraryManager#findDirectors(java.util.regex.Pattern, int)
298 	 */
299 	@Override
300 	public List<Director> findDirectors(String key, int maxReturned) {
301 		List<Director> ld = db.findDirectors(key, maxReturned);
302 		Collections.sort(ld, directorNameSort);
303 		return(ld);
304 	}
305 
306 	private class DirectorNameSort implements Comparator<Director> {
307 		@Override
308 		public int compare(Director arg0, Director arg1) {
309 			return(arg0.getName().compareToIgnoreCase(arg1.getName()));
310 		}
311 	}
312 	private	DirectorNameSort directorNameSort = new DirectorNameSort();
313 
314 
315 	/* (non-Javadoc)
316 	 * @see com.buckosoft.BuckoVidLib.business.LibraryManager#findWriters(java.util.regex.Pattern, int)
317 	 */
318 	@Override
319 	public List<Writer> findWriters(String key, int maxReturned) {
320 		List<Writer> lw = db.findWriters(key, maxReturned);
321 		Collections.sort(lw, writerNameSort);
322 		return(lw);
323 	}
324 	private class WriterNameSort implements Comparator<Writer> {
325 		@Override
326 		public int compare(Writer arg0, Writer arg1) {
327 			return(arg0.getName().compareToIgnoreCase(arg1.getName()));
328 		}
329 	}
330 	private	WriterNameSort writerNameSort = new WriterNameSort();
331 
332 
333 	@Override
334 	public List<VideoBase>	getLibrarySectionVideos(int key) {
335 		if (library == null)
336 			loadLibrary();
337 		List<VideoBase> vl = new ArrayList<VideoBase>();
338 		synchronized (library) {
339 			LibrarySection ls = library.getLibrarySection(key);
340 			for (VideoBase v : ls.getVideos()) {
341 				vl.add(v);
342 			}
343 		}
344 		return(vl);
345 		
346 	}
347 
348 	/* (non-Javadoc)
349 	 * @see com.buckosoft.BuckoVidLib.business.LibraryManager#getDirectorVideos(int)
350 	 */
351 	@Override
352 	public List<VideoBase> getDirectorVideos(int key) {
353 		if (library == null)
354 			loadLibrary();
355 		List<VideoBase> vl = new ArrayList<VideoBase>();
356 		List<Integer>	videoIds = db.getVideoIdsByDirector(key);
357 		synchronized (library) {
358 			for (int videoId : videoIds) {
359 				vl.add(library.getVideoById(videoId));
360 			}
361 		}
362 		return vl;
363 	}
364 
365 	/* (non-Javadoc)
366 	 * @see com.buckosoft.BuckoVidLib.business.LibraryManager#getWriterVideos(int)
367 	 */
368 	@Override
369 	public List<VideoBase> getWriterVideos(int key) {
370 		if (library == null)
371 			loadLibrary();
372 		List<VideoBase> vl = new ArrayList<VideoBase>();
373 		synchronized (library) {
374 			List<Integer>	videoIds = db.getVideoIdsByWriter(key);
375 			for (int videoId : videoIds) {
376 				vl.add(library.getVideoById(videoId));
377 			}
378 		}
379 		return vl;
380 	}
381 
382 	/* (non-Javadoc)
383 	 * @see com.buckosoft.BuckoVidLib.business.LibraryManager#getActorVideos(int)
384 	 */
385 	@Override
386 	public List<VideoBase> getActorVideos(int key) {
387 		if (library == null)
388 			loadLibrary();
389 		List<VideoBase> vl = new ArrayList<VideoBase>();
390 		List<Integer>	videoIds = db.getVideoIdsByActor(key);
391 		synchronized (library) {
392 			for (int videoId : videoIds) {
393 				vl.add(library.getVideoById(videoId));
394 			}
395 		}
396 		return vl;
397 	}
398 
399 	/* (non-Javadoc)
400 	 * @see com.buckosoft.BuckoVidLib.business.LibraryManager#getTVSeasonFromKey(int)
401 	 */
402 	@Override
403 	public TVSeason getTVSeasonFromHashKey(int key) {
404 		return(db.getTVSeasonFromHashKey(key));
405 	}
406 
407 
408 	@Override
409 	public int	getMovieCount(boolean allowRestricted) {
410 		if (library == null)
411 			loadLibrary();
412 		int	count = 0;
413 		synchronized (library) {
414 			for (LibrarySection ls : this.getSectionList(allowRestricted)) {
415 				if (ls.getType() == Type.Movie)
416 					count += ls.getVideoCount();
417 			}
418 		}
419 		return(count);
420 		
421 	}
422 
423 	/* (non-Javadoc)
424 	 * @see com.buckosoft.BuckoVidLib.business.LibraryManager#getTVShowCount()
425 	 */
426 	@Override
427 	public int getTVShowCount(boolean allowRestricted) {
428 		if (library == null)
429 			loadLibrary();
430 		int	count = 0;
431 		synchronized (library) {
432 			for (LibrarySection ls : this.getSectionList(allowRestricted)) {
433 				if (ls.getType() == Type.TVShow)
434 					count += ls.getVideoCount();
435 			}
436 		}
437 		return(count);
438 	}
439 
440 	@Override
441 	/** Note: allowRestricted is ignored
442 	 */
443 	public int	getTVEpisodeCount(boolean allowRestricted) {
444 		if (library == null)
445 			loadLibrary();
446 		int	count = 0;
447 		List<TVSeason> seasons = db.getTVSeasons();
448 		for (TVSeason season : seasons) {
449 			count += season.getEpisodeCount();
450 		}
451 		return(count);
452 	}
453 
454 
455 	///////////////////////////////////////////////////////////////////////////
456 	private void loadLibrary() {
457 		log.info("Loading library");
458 		boolean b = ConfigManager.getBoolean("BuckoVidLib.runFromPlex", true);
459 		if (b)
460 			loadLibraryFromPlex();
461 		else
462 			loadLibraryFromDatabase();
463 		log.info("library loaded");
464 	}
465 	
466 	///////////////////////////////////////////////////////////////////////////
467 	private void loadLibraryFromDatabase() {
468 		log.info("Loading library from Database");
469 		library = new Library();
470 		List<String> restrictedSectionNames = setupRestrictedSectionNames();
471 		library.setRestrictedSectionNames(restrictedSectionNames);
472 		List<VideoBase> videos = db.getVideoBases();
473 		synchronized (library) {
474 			List<LibrarySection> list = db.getLibrarySections();
475 			log.info("Loading " + list.size() + " sections with " + videos.size() + " videos.");
476 			for (LibrarySection ls : list) {
477 				if (restrictedSectionNames.contains(ls.getName()))
478 					ls.setRestricted(true);
479 			}
480 			for (VideoBase v : videos) {
481 				for (LibrarySection ls : list) {
482 					if (v.getSection() == ls.getKey()) {
483 						library.addVideo(v.getHashKey(), v);
484 						ls.addVideo(v);
485 						break;
486 					}
487 				}
488 			}
489 			for (LibrarySection ls : list) {
490 				Collections.sort(ls.getVideos(), videoBaseIndexSort);
491 			}
492 			library.setLibrarySections(list);
493 			if (log.isInfoEnabled()) {
494 				if (library.getNewestVideo() == null) {
495 					log.warn("Empty library!");
496 				} else {
497 					log.info("newest video: " + library.getNewestVideo().getTitle() 
498 						+ " (" + library.getNewestVideo().getYear()
499 						+ ") h:" + DomainConverter.intToKey(library.getNewestVideo().getHashKey()) 
500 						+ " d:" + library.getNewestVideo().getAddedAt());
501 				}
502 			}
503 		}
504 	}
505 
506 	///////////////////////////////////////////////////////////////////////////
507 	private void loadLibraryFromPlex() {
508 		log.info("Loading library from plex");
509 		library = new Library();
510 		List<String> restrictedSectionNames = setupRestrictedSectionNames();
511 		library.setRestrictedSectionNames(restrictedSectionNames);
512 		db.truncateLibrary();
513 		synchronized (library) {
514 			MediaContainer mc = libraryService.sections();
515 			List<LibrarySection> list = new ArrayList<LibrarySection>();
516 			for (Directory d : mc.getDirectories()) {
517 				if (!skipSections.contains(d.getTitle()))
518 					list.add(DomainConverter.toLibrarySection(d));
519 			}
520 			for (LibrarySection ls : list) {
521 				if (restrictedSectionNames.contains(ls.getName()))
522 					ls.setRestricted(true);
523 				db.addLibrarySection(ls);
524 				log.info("Processing section " + ls.getName());
525 				mc = libraryService.videosAll(ls.getKey());
526 				if (ls.getType() == Type.Movie) {
527 					for (tv.plex.domain.Video pv : mc.getVideos()) {
528 						com.buckosoft.BuckoVidLib.domain.Video v = DomainConverter.toVideo(pv);
529 						v.setSection(ls.getKey());
530 						v.setHashKey(v.hashCode());
531 						updateMappings(null, library, v);
532 						try {
533 							db.saveVideo(v);
534 						} catch (Exception e) {
535 							throw new RuntimeException("Failed to add video: " + v.getTitle() + " (" + v.getYear() + ") hash: " + v.getHashKey(), e);
536 						}
537 						library.addVideo(v.getHashKey(), v);
538 						ls.addVideo(v);
539 					}
540 				} else if (ls.getType() == Type.TVShow) {
541 					for (tv.plex.domain.Directory pd : mc.getDirectories()) {
542 						com.buckosoft.BuckoVidLib.domain.TVShow tv = DomainConverter.toTVShow(pd);
543 						tv.setSection(ls.getKey());
544 						tv.setHashKey(tv.hashCode());
545 						updateMappings(null, library, tv);
546 						try {
547 							db.saveVideo(tv);
548 						} catch (Exception e) {
549 							throw new RuntimeException("Failed to add TV Show: " + tv.getTitle() + " (" + tv.getYear() + ") hash: " + tv.getHashKey(), e);
550 						}
551 						loadTVSeasonsFromPlex(tv);
552 						library.addVideo(tv.getHashKey(), tv);	// do this after we get our id from the database
553 						ls.addVideo(tv);
554 					}
555 				}
556 			}
557 			library.setLibrarySections(list);
558 		}
559 		if (log.isInfoEnabled()) {
560 			log.info("newest video: " + library.getNewestVideo().getTitle() 
561 				+ " (" + library.getNewestVideo().getYear()
562 				+ ") h:" + DomainConverter.intToKey(library.getNewestVideo().getHashKey()) 
563 				+ " d:" + library.getNewestVideo().getAddedAt());
564 		}
565 	}
566 
567 	PlexLibraryUpdater	plexLibraryUpdaterTask;
568 	Thread				plexLibraryUpdaterThread;
569 	@Override
570 	public void startRefreshLibraryFromPlex() {
571 		plexLibraryUpdaterTask = new PlexLibraryUpdater();
572 		plexLibraryUpdaterThread = new Thread(plexLibraryUpdaterTask);
573 		plexLibraryUpdaterThread.setName("PlexLibraryRefresh");
574 		plexLibraryUpdaterThread.start();
575 		
576 	}
577 	@Override
578 	public RestRefreshStatus getRefreshLibraryFromPlexStatus() {
579 		if (plexLibraryUpdaterTask == null) {
580 			RestRefreshStatus	rls = new RestRefreshStatus();
581 			rls.addMessage("warning", "Library Updater is null");
582 			return(rls);
583 		}
584 		return(plexLibraryUpdaterTask.getRefreshStatus());
585 	}
586 
587 	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
588 	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
589 	public class PlexLibraryUpdater implements Runnable {
590 		private final Log log = LogFactory.getLog(getClass());
591 		private	RestRefreshStatus	restRefreshStatus = new RestRefreshStatus();
592 
593 		
594 		/** Get a thread safe copy of the refresh status
595 		 * @return a percentage of completion and some strings
596 		 */
597 		public RestRefreshStatus getRefreshStatus() {
598 			RestRefreshStatus	rls = new RestRefreshStatus();
599 			List<RestStatusString>	messages = new ArrayList<RestStatusString>();
600 			synchronized (restRefreshStatus) {
601 				rls.setPercentComplete(restRefreshStatus.getPercentComplete());
602 				if (restRefreshStatus.getMessages() != null)
603 					for (RestStatusString rss : restRefreshStatus.getMessages())
604 						messages.add(new RestStatusString(rss.getType(), rss.getMessage()));
605 			}
606 			rls.setMessages(messages);
607 			if (log.isTraceEnabled()) {
608 				for (RestStatusString rss : rls.getMessages())
609 					log.trace("RestStatusString: " + rss.getType() + " " + rss.getMessage());
610 			}
611 			return(rls);
612 		}
613 		
614 		private void addMessage(String type, String message) {
615 			log.info(message);
616 			synchronized(restRefreshStatus) {
617 				restRefreshStatus.addMessage(type, message);
618 			}
619 		}
620 
621 		private void abort(String message) {
622 			addMessage(mtDanger, "Update aborted: " + message);
623 			throw new RuntimeException(message);
624 		}
625 //		private void abort(String message, Exception e) {
626 //			addMessage(mtDanger, "Update aborted: " + message);
627 //			throw new RuntimeException(message, e);
628 //		}
629 		
630 		private	String statusMessage(String s, VideoBase v, String message) {
631 			StringBuilder sb = new StringBuilder();
632 			if (s == null) {
633 				sb.append(v.getTitle());
634 				sb.append(" (");
635 				sb.append(v.getYear());
636 				sb.append(") : ");
637 			} else {
638 				sb.append(s);
639 				sb.append(" : ");
640 			}
641 			sb.append(message);
642 			return(sb.toString());
643 		}
644 
645 		/** Update our database with the latest info from plex
646 		 */
647 		@Override
648 		public void run() {
649 			//log.info("Updating library from plex");
650 			addMessage(mtPrimary, "Updating library from plex...");
651 			boolean reloadme = false;
652 			
653 			Library	plibrary = new Library();
654 			LibraryService	libraryService;
655 			libraryService = ClientFactory.getLibraryClient();
656 			List<String> restrictedSectionNames = setupRestrictedSectionNames();
657 			List<String> skipSections = setupSkipSections();
658 			List<Integer> existingHashKeys = new ArrayList<Integer>();
659 			for (Integer hk : library.getVideoMap().keySet())
660 				existingHashKeys.add(hk);
661 			MediaContainer mc = libraryService.sections();
662 			List<LibrarySection> list = new ArrayList<LibrarySection>();
663 			for (Directory d : mc.getDirectories()) {
664 				if (!skipSections.contains(d.getTitle()))
665 					list.add(DomainConverter.toLibrarySection(d));
666 			}
667 			addMessage(mtInfo, "Servicing " + list.size() + " sections");
668 			int	currentLibrarySectionIndex = -1;
669 			for (LibrarySection ls : list) {
670 				
671 				currentLibrarySectionIndex++;
672 				int pcBase = currentLibrarySectionIndex * 100 / list.size();
673 				double pcInc = 100 / list.size();
674 				synchronized(restRefreshStatus) {
675 					restRefreshStatus.setPercentComplete(pcBase);
676 				}
677 				boolean sortme = false;
678 				//log.info("pcBase = " + pcBase + " section = " + currentLibrarySectionIndex + " name = " + ls.getName());
679 				if (restrictedSectionNames.contains(ls.getName()))
680 					ls.setRestricted(true);
681 				if (library.getLibrarySection(ls.getKey()) == null) {
682 					LibrarySection nls = new LibrarySection(ls);
683 					addMessage(mtInfo, "Adding new" + (ls.isRestricted() ? " restricted" : "") + " LibrarySection: " + ls.getName());
684 					db.addLibrarySection(ls);
685 					library.addLibrarySection(nls);
686 				}
687 				log.info("Processing section " + ls.getName());
688 				mc = libraryService.videosAll(ls.getKey());
689 				VideoBase ovb;
690 				Video ov;
691 				String s;
692 				if (ls.getType() == Type.Movie) {
693 					int pcIndex = -1;
694 					for (tv.plex.domain.Video pv : mc.getVideos()) {
695 						pcIndex++;
696 						int i = (int)(pcIndex*pcInc/mc.getSize());
697 						synchronized(restRefreshStatus) {
698 							restRefreshStatus.setPercentComplete(pcBase+i);
699 						}
700 						//log.info("pcBase = " + pcBase + " mcSize = " + mc.getSize() + " pcInc = " + pcInc + " i = " + i + " % = " + (pcBase+i));
701 
702 						if (pv.getTitle().equals(("The Boxtrolls"))) {
703 							log.trace("Doing The Boxtrolls...");
704 						}
705 						Video v = DomainConverter.toVideo(pv);
706 						v.setSection(ls.getKey());
707 						v.setHashKey(v.hashCode());
708 						existingHashKeys.remove((Integer)v.getHashKey());
709 						updateMappings(restRefreshStatus, library, v);
710 						synchronized (library) {
711 							ovb = library.getVideoBase(v.getHashKey());
712 						}
713 						if (ovb == null) {
714 							s = statusMessage(null, v, "New Video");
715 							log.info(s);
716 							try {
717 								db.saveVideo(v);
718 								synchronized (library) {
719 									library.addVideo(v.getHashKey(), v);
720 									library.getLibrarySection(ls.getKey()).addVideo(v);
721 								}
722 								plibrary.addVideo(v.getHashKey(), v);
723 								ls.addVideo(v);
724 								sortme = true;
725 								addMessage(mtSuccess, s);
726 							} catch (Exception e) {
727 								s = statusMessage(s, v, "Failed to save video");
728 								addMessage(mtDanger, s);
729 								if (!continueOnError)
730 									throw new RuntimeException(s, e);
731 							}
732 						} else {
733 							ov = db.getVideo(ovb.getId());
734 							if (ov == null) {
735 								addMessage(mtDanger, " : failed to load Video " + ovb.getId() + " from database");
736 							} else {
737 								ProcessStatus ps = processVideo(v, ov);
738 								if (ps.reloadme)
739 									reloadme = true;
740 								if (ps.sortme)
741 									sortme = true;
742 								ls.addVideo(ov);
743 								plibrary.addVideo(v.getHashKey(), ov);		// stash away so we can later look for deleted videos and sort
744 							}
745 						}
746 						v.setVideoTexts(null);					// we don't need these memory hogs anymore
747 					}
748 				} else if (ls.getType() == Type.TVShow) {
749 					int pcIndex = -1;
750 					TVShow otv;
751 					Collection<Directory> dirs = mc.getDirectories();
752 					for (tv.plex.domain.Directory pd : dirs) {
753 						pcIndex++;
754 						int i = (int)(pcIndex*pcInc/mc.getSize());
755 						log.trace("i=" + i);
756 						synchronized(restRefreshStatus) {
757 							restRefreshStatus.setPercentComplete(pcBase+i);
758 						}
759 						TVShow tv = DomainConverter.toTVShow(pd);
760 						tv.setSection(ls.getKey());
761 						tv.setHashKey(tv.hashCode());
762 						existingHashKeys.remove((Integer)tv.getHashKey());
763 						updateMappings(restRefreshStatus, library, tv);
764 						synchronized (library) {
765 							ovb = library.getVideoBase(tv.getHashKey());
766 						}
767 						if (ovb == null) {
768 							s = statusMessage(null, tv, "New TV Show");
769 							log.info(s);
770 							try {
771 								db.saveVideo(tv);
772 								synchronized (library) {
773 									library.addVideo(tv.getHashKey(), tv);
774 									library.getLibrarySection(ls.getKey()).addVideo(tv);
775 								}
776 								plibrary.addVideo(tv.getHashKey(), tv);
777 								ls.addVideo(tv);
778 								sortme = true;
779 								addMessage(mtSuccess, s);
780 							} catch (Exception e) {
781 								s = statusMessage(s, tv, "Failed to save TVShow");
782 								addMessage(mtDanger, s);
783 								if (!continueOnError)
784 									throw new RuntimeException(s, e);
785 								break;
786 							}
787 							otv = null;
788 						} else {
789 							otv = (TVShow)db.getVideo(ovb.getId());
790 							if (otv == null) {
791 								addMessage(mtDanger, " : failed to load TVShow " + ovb.getId() + " from database");
792 							} else {
793 								ProcessStatus ps = processVideo(tv, otv);
794 								tv.setId(otv.getId());
795 								if (ps.reloadme)
796 									reloadme = true;
797 								if (ps.sortme)
798 									sortme = true;
799 								ls.addVideo(otv);
800 								plibrary.addVideo(tv.getHashKey(), tv);		// stash away so we can later look for deleted videos and sort
801 							}
802 						}
803 						tv.setVideoTexts(null);
804 						loadTVSeasonsFromPlex(tv);
805 					}
806 				} else {
807 					abort("Bug: Unknown section type in " + ls.getName());
808 				}
809 				if (sortme) {
810 					reloadme = true;
811 					Collections.sort(ls.getVideos(), videoNameSort);
812 					int i =0;
813 					for (VideoBase vb : ls.getVideos())	{
814 						Video v = (Video)vb;
815 						i++;
816 						v.setSortIndex(i);
817 						db.saveVideo(v);
818 					}
819 				}
820 			}
821 			plibrary.setLibrarySections(list);
822 			// check for deleted videos
823 			if (!existingHashKeys.isEmpty()) {
824 				log.info("Deleting " + existingHashKeys.size() + " videos");
825 				//addMessage(mtWarning, "Deleting " + existingHashKeys.size() + " videos");
826 				reloadme = true;
827 				for (Integer hk : existingHashKeys) {
828 					VideoBase vb;
829 					synchronized (library) { 
830 						vb = library.getVideoBase(hk);
831 					}
832 					if (vb == null) {
833 						abort("Wanted to delete Video for key " + hk + " but it doesn't exist");
834 					}
835 					addMessage(mtWarning, statusMessage(null, vb, "Deleted"));
836 					db.deleteVideo(vb);
837 				}
838 			}
839 			checkImageCache(plibrary);
840 			if (reloadme) {
841 				synchronized(restRefreshStatus) {
842 					restRefreshStatus.setPercentComplete(99);
843 				}
844 				addMessage(mtInfo, "Reloading library");
845 				loadLibraryFromDatabase();
846 				lastUpdateTime = new Date().getTime();
847 			}
848 			synchronized(restRefreshStatus) {
849 				restRefreshStatus.setPercentComplete(100);
850 			}
851 			addMessage(mtSuccess, "Done.");
852 			if (log.isInfoEnabled()) {
853 				log.info("newest video: " + plibrary.getNewestVideo().getTitle() 
854 						+ " (" + plibrary.getNewestVideo().getYear()
855 						+ ") d:" + plibrary.getNewestVideo().getAddedAt());
856 			}
857 		}
858 
859 		private class ProcessStatus {
860 			boolean sortme = false;
861 			boolean	reloadme = false;
862 		}
863 		private ProcessStatus processVideo(Video v, Video ov) {
864 			ProcessStatus ps = new ProcessStatus();
865 			boolean saveme = false;
866 			boolean statusWarning = false;
867 			String s;
868 			s = null;
869 			if (ov.getUpdatedAt() != v.getUpdatedAt()) {
870 				ov.setUpdatedAt(v.getUpdatedAt());
871 				saveme = true;
872 				s = statusMessage(s, v, "timekey updated");
873 			}
874 
875 			VideoTexts vt = db.getVideoTexts(ov.getId());
876 			if (vt == null) {
877 				vt = new VideoTexts();
878 				vt.setVideoId(ov.getId());
879 			}
880 			boolean vtupdate = false;
881 			if (v.getVideoTexts() == null) {
882 				if (vt.getTagline() != null) {
883 					s = statusMessage(s, v, "tagline disappeared");
884 					vt.setTagline(null);
885 					vtupdate = true;
886 				}
887 				if (vt.getSummary() != null) {
888 					s = statusMessage(s, v, "summary disappeared");
889 					vt.setSummary(null);
890 					vtupdate = true;
891 				}
892 			} else {
893 				VideoTexts vvt = v.getVideoTexts();
894 				if (vvt.getSummary() != null && !vvt.getSummary().equals(vt.getSummary())) {
895 					s = statusMessage(s, v, "summary updated");
896 					vt.setSummary(vvt.getSummary());
897 					vtupdate = true;
898 					try {
899 						if (URLEncoder.encode(vt.getSummary(), "utf-8").length() > db.getMaxVideoTextsSummaryLength()) {
900 							statusWarning = true;
901 							s = statusMessage(s, v, "summary too big (" + vt.getSummary().length() + ")");
902 						}
903 					} catch (UnsupportedEncodingException e) {
904 						statusWarning = true;
905 						s = statusMessage(s, v, "summary encoding failure");
906 					}
907 				}
908 				if (vvt.getTagline() != null && !vvt.getTagline().equals(vt.getTagline())) {
909 					s = statusMessage(s, v, "tagline updated");
910 					vt.setTagline(v.getVideoTexts().getTagline());
911 					vtupdate = true;
912 					try {
913 						if (URLEncoder.encode(vt.getTagline(), "utf-8").length() > db.getMaxVideoTextsTaglineLength()) {
914 							statusWarning = true;
915 							s = statusMessage(s, v, "tagline too big (" + vt.getTagline().length() + ")");
916 						}
917 					} catch (UnsupportedEncodingException e) {
918 						statusWarning = true;
919 						s = statusMessage(s, v, "tagline encoding failure");
920 					}
921 				}
922 			}
923 			if (v.getSortTitle() != null && !v.getSortTitle().equals(ov.getSortTitle())) {
924 				ov.setSortTitle(v.getSortTitle());
925 				s = statusMessage(s, v, "sortTitle updated");
926 				saveme = true;
927 				ps.sortme = true;
928 			} else if (ov.getSortTitle() != null && v.getSortTitle() == null) {
929 				ov.setSortTitle(v.getSortTitle());
930 				s = statusMessage(s, v, "sortTitle updated");
931 				saveme = true;
932 				ps.sortme = true;
933 			}
934 			if (v.getStudio() != null && !v.getStudio().equals(ov.getStudio())) {
935 				ov.setStudio(v.getStudio());
936 				s = statusMessage(s, v, "studio updated");
937 				saveme = true;
938 			} else if (ov.getStudio() != null && v.getStudio() == null) {
939 				ov.setStudio(v.getStudio());
940 				s = statusMessage(s, v, "studio updated");
941 				saveme = true;
942 			}
943 			if (saveme || vtupdate) {
944 				if (statusWarning) {
945 					log.warn(s);
946 					addMessage(mtDanger, s);
947 				} else {
948 					log.info(s);
949 					addMessage(mtSuccess, s);
950 				}
951 				if (vtupdate) {
952 					ov.setVideoTexts(vt);
953 					saveme = true;
954 				}
955 				if (saveme) {
956 					db.saveVideo(ov);
957 					ps.reloadme = true;
958 				}
959 			}
960 			ov.setVideoTexts(null);					// we don't need these memory hogs anymore
961 			return(ps);
962 		}
963 	
964 		private	class CacheDirs {
965 			File smPosterDir;
966 			File lgPosterDir;
967 			File rawPosterDir;
968 			File artDir;
969 			int count = 0;
970 		}
971 		///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
972 		private void checkImageCache(Library plibrary) {
973 			CacheDirs cacheDirs = new CacheDirs();
974 			boolean b = ConfigManager.getBoolean("BuckoVidLib.useImageCache", false);
975 			if (!b)
976 				return;
977 			addMessage(mtInfo, "Refreshing images");
978 			String base = ConfigManager.getString("BuckoVidLib.imageCacheBaseDir", null);
979 			if (base == null) {
980 				addMessage(mtDanger, "BuckoVidLib.imageCacheBaseDir is undefined");
981 				return;
982 			}
983 			File dir = new File(base);
984 			if (!dir.isDirectory()) {
985 				addMessage(mtDanger, "BuckoVidLib.imageCacheBaseDir '" + base + "' is not a directory");
986 				return;
987 			}
988 			cacheDirs.smPosterDir = new File(dir, "smposter");
989 			cacheDirs.lgPosterDir = new File(dir, "lgposter");
990 			cacheDirs.rawPosterDir = new File(dir, "rposter");
991 			cacheDirs.artDir = new File(dir, "art");
992 			if (!cacheDirs.smPosterDir.isDirectory()) {
993 				if (!cacheDirs.smPosterDir.mkdir()) {
994 					addMessage(mtDanger, "imageCache: failed to create smPosterDir: '" + cacheDirs.smPosterDir.getAbsolutePath() + "'");
995 					return;
996 				}
997 				addMessage(mtInfo, "Created directory " + cacheDirs.smPosterDir.getAbsolutePath());
998 			}
999 			if (!cacheDirs.lgPosterDir.isDirectory()) {
1000 				if (!cacheDirs.lgPosterDir.mkdir()) {
1001 					addMessage(mtDanger, "imageCache: failed to create lgPosterDir: '" + cacheDirs.lgPosterDir.getAbsolutePath() + "'");
1002 					return;
1003 				}
1004 				addMessage(mtInfo, "Created directory " + cacheDirs.lgPosterDir.getAbsolutePath());
1005 			}
1006 			if (!cacheDirs.rawPosterDir.isDirectory()) {
1007 				if (!cacheDirs.rawPosterDir.mkdir()) {
1008 					addMessage(mtDanger, "imageCache: failed to create smPosterDir: '" + cacheDirs.rawPosterDir.getAbsolutePath() + "'");
1009 					return;
1010 				}
1011 				addMessage(mtInfo, "Created directory " + cacheDirs.rawPosterDir.getAbsolutePath());
1012 			}
1013 			if (!cacheDirs.artDir.isDirectory()) {
1014 				if (!cacheDirs.artDir.mkdir()) {
1015 					addMessage(mtDanger, "imageCache: failed to create smPosterDir: '" + cacheDirs.artDir.getAbsolutePath() + "'");
1016 					return;
1017 				}
1018 				addMessage(mtInfo, "Created directory " + cacheDirs.artDir.getAbsolutePath());
1019 				File in = new File(dir, "defaultBackground.jpg");
1020 				File out = new File(cacheDirs.artDir, "0");
1021 				try {
1022 					FileUtils.copyFile(in, out);
1023 				} catch (IOException e) {
1024 					addMessage(mtWarning, "imageCache: failed to copy defaultBackground: '" + cacheDirs.artDir.getAbsolutePath() + "'");
1025 				}
1026 				in = new File(dir, "greyBackground.png");
1027 				out = new File(cacheDirs.artDir, "1");
1028 				try {
1029 					FileUtils.copyFile(in, out);
1030 				} catch (IOException e) {
1031 					addMessage(mtWarning, "imageCache: failed to copy greyBackground: '" + cacheDirs.artDir.getAbsolutePath() + "'");
1032 				}
1033 			}
1034 			restRefreshStatus.setPercentComplete(0);
1035 			imageController.setUseImageCache(false);						// turn off the cache so we read new images from plex
1036 			Collection<VideoBase> col = plibrary.getVideoMap().values();
1037 			int todo = col.size();
1038 			int i = 0;
1039 			//int count = 0;
1040 			for (VideoBase vb : col) {
1041 				Video v = (Video)vb;
1042 				restRefreshStatus.setPercentComplete(i++ * 100 / todo);
1043 				String key = DomainConverter.intToKey(v.getHashKey());
1044 				if (v.getTitle().equals(("North and South"))) {
1045 					log.trace("Doing North and South...");
1046 				}
1047 
1048 				try {
1049 					processPoster(cacheDirs, v, key);
1050 				} catch (Exception e) {
1051 					String s = "Failed to read poster for " + v.getTitle();
1052 					addMessage(mtDanger, s);
1053 					if (logExceptions)
1054 						log.warn(s, e);
1055 				}
1056 		
1057 				if (!processBackground(cacheDirs, v, key) && !continueOnError)
1058 					break;
1059 				if (v instanceof TVShow) {
1060 					TVShow tv = (TVShow)v;
1061 					log.info("Do TVShow " + tv.getTitle());
1062 					for (TVSeason season : tv.getSeasons()) {
1063 						try {
1064 							processPoster(cacheDirs, v, DomainConverter.intToKey(season.getHashKey()));
1065 						} catch (Exception e) {
1066 							String s = "Failed to read poster for " + v.getTitle() + " : " + season.getTitle();
1067 							addMessage(mtDanger, s);
1068 							if (logExceptions)
1069 								log.warn(s, e);
1070 						}
1071 					}
1072 				}
1073 			}
1074 			addMessage(mtInfo, "Processed " + cacheDirs.count + " video images");
1075 			restRefreshStatus.setPercentComplete(100);
1076 			imageController.setUseImageCache(true);
1077 		}
1078 	
1079 		private void processPoster(CacheDirs cacheDirs, Video v, String key) throws Exception {
1080 			File file = new File(cacheDirs.smPosterDir, key);
1081 			if (file.isFile() && file.lastModified()/1000 > v.getUpdatedAt()) {
1082 				long last = file.lastModified()/1000;
1083 				long vlast = v.getUpdatedAt();
1084 				log.debug("skipping because newer " + v.getTitle() + " (" + (vlast - last) + ")");
1085 				return;
1086 			}
1087 			cacheDirs.count++;
1088 			FileOutputStream fos = null;
1089 			try {
1090 				 fos = new FileOutputStream(file);
1091 			} catch (FileNotFoundException e) {
1092 				String s = "posterCacheSM: Can't open '" + file.getAbsolutePath() + "' for writing";
1093 				addMessage(mtDanger, s);
1094 				throw(new Exception(s, e));
1095 			}
1096 			
1097 			try {
1098 				if (fos != null)
1099 					posterController.handleRequest(key, 150, 225, null, fos);
1100 			} catch (Exception e) {
1101 				String s = "posterCacheSM: Can't write '" + file.getAbsolutePath() + "'";
1102 				addMessage(mtDanger, s);
1103 				try { fos.close(); } catch (IOException e1) {}
1104 				file.delete();
1105 				throw(new Exception(s, e));
1106 			}
1107 			try {
1108 				if (fos != null)
1109 					fos.close();
1110 			} catch (IOException e) {
1111 				String s = "posterCacheSM: Can't write/close '" + file.getAbsolutePath() + "'";
1112 				addMessage(mtDanger, s);
1113 				file.delete();
1114 				throw(new Exception(s, e));
1115 			}
1116 			////////////////////////////////////////////////////////////////////////////////////////////
1117 			file = new File(cacheDirs.lgPosterDir, key);
1118 			fos = null;
1119 			try {
1120 				 fos = new FileOutputStream(file);
1121 			} catch (FileNotFoundException e) {
1122 				String s = "posterCacheLG: Can't open '" + file.getAbsolutePath() + "' for writing";
1123 				addMessage(mtDanger, s);
1124 				throw(new Exception(s, e));
1125 			}
1126 			
1127 			try {
1128 				if (fos != null)
1129 					posterController.handleRequest(key, 250, 375, null, fos);
1130 			} catch (Exception e) {
1131 				String s = "posterCacheLG: Can't write '" + file.getAbsolutePath() + "'";
1132 				addMessage(mtDanger, s);
1133 				try { fos.close(); } catch (IOException e1) {}
1134 				file.delete();
1135 				throw(new Exception(s, e));
1136 			}
1137 			try {
1138 				if (fos != null)
1139 					fos.close();
1140 			} catch (IOException e) {
1141 				String s = "posterCacheLG: Can't write/close '" + file.getAbsolutePath() + "'";
1142 				addMessage(mtDanger, s);
1143 				file.delete();
1144 				throw(new Exception(s, e));
1145 			}
1146 			////////////////////////////////////////////////////////////////////////////////////////////
1147 			file = new File(cacheDirs.rawPosterDir, key);
1148 			fos = null;
1149 			try {
1150 				 fos = new FileOutputStream(file);
1151 			} catch (FileNotFoundException e) {
1152 				String s = "posterCacheRaw: Can't open '" + file.getAbsolutePath() + "' for writing";
1153 				addMessage(mtDanger, s);
1154 				throw(new Exception(s, e));
1155 			}
1156 			
1157 			try {
1158 				if (fos != null)
1159 					posterController.handleRequest(key, 0, 0, null, fos);
1160 			} catch (Exception e) {
1161 				String s = "posterCacheRaw: Failure: " + e.getMessage();
1162 				addMessage(mtDanger, s);
1163 				try { fos.close(); } catch (IOException e1) {}
1164 				file.delete();
1165 				throw(new Exception(s, e));
1166 			}
1167 			try {
1168 				if (fos != null)
1169 					fos.close();
1170 			} catch (IOException e) {
1171 				String s = "posterCacheRaw: Can't write/close '" + file.getAbsolutePath() + "'";
1172 				addMessage(mtDanger, s);
1173 				file.delete();
1174 				throw(new Exception(s, e));
1175 			}
1176 		}
1177 		
1178 		private boolean processBackground(CacheDirs cacheDirs, Video v, String key) {
1179 			////////////////////////////////////////////////////////////////////////////////////////////
1180 			FileOutputStream fos = null;
1181 			File file;
1182 			file = new File(cacheDirs.artDir, key);
1183 			fos = null;
1184 			try {
1185 				 fos = new FileOutputStream(file);
1186 			} catch (FileNotFoundException e) {
1187 				addMessage(mtDanger, "imageCache: Can't open '" + file.getAbsolutePath() + "' for writing");
1188 				if (!continueOnError)
1189 					return(false);
1190 			}
1191 			try {
1192 				if (fos != null)
1193 					imageController.handleRequest(key, fos);
1194 			} catch (Exception e) {
1195 				addMessage(mtDanger, "imageCache: Failure: " + e.getMessage());
1196 				try { fos.close(); } catch (IOException e1) {}
1197 				file.delete();
1198 				if (!continueOnError)
1199 					return(false);
1200 			}
1201 			try {
1202 				if (fos != null)
1203 					fos.close();
1204 			} catch (IOException e) {
1205 				addMessage(mtDanger, "imageCache: Can't write/close '" + file.getAbsolutePath() + "'");
1206 				if (!continueOnError)
1207 					return(false);
1208 			}
1209 			return(true);
1210 		}
1211 	}
1212 	
1213 	private class VideoNameSort implements Comparator<VideoBase> {
1214 		@Override
1215 		public int compare(VideoBase arg0, VideoBase arg1) {
1216 			Video v0 = (Video)arg0;
1217 			Video v1 = (Video)arg1;
1218 			String s0;
1219 			String s1;
1220 			s0 = v0.getSortTitle();
1221 			s1 = v1.getSortTitle();
1222 			if (s0 == null)
1223 				s0 = v0.getTitle();
1224 			if (s1 == null)
1225 				s1 = v1.getTitle();
1226 
1227 			return(s0.compareToIgnoreCase(s1));
1228 		}
1229 	}
1230 	private	VideoNameSort videoNameSort = new VideoNameSort();
1231 	
1232 	private class VideoBaseIndexSort implements Comparator<VideoBase> {
1233 		@Override
1234 		public int compare(VideoBase arg0, VideoBase arg1) {
1235 			if (arg0.getSortIndex() == arg1.getSortIndex())
1236 				return(arg0.getTitle().compareToIgnoreCase(arg1.getTitle()));
1237 			return(Integer.compare(arg0.getSortIndex(), arg1.getSortIndex()));
1238 		}
1239 	}
1240 	private	VideoBaseIndexSort videoBaseIndexSort = new VideoBaseIndexSort();
1241 
1242 	private	SimpleDateFormat _df = new SimpleDateFormat("YYYYMMdd:HHmmss");
1243 	private	String	debugCal(long cal) {
1244 		Date d = new Date(cal*1000);
1245 		return(_df.format(d));
1246 	}
1247 	
1248 	private	void loadTVSeasonsFromPlex(TVShow tv) {
1249 		if (tv.getTitle().equals(("North and South"))) {
1250 			log.trace("Doing North and South...");
1251 		}
1252 		MediaContainer mc = libraryService.tvSeasons(tv.getPlexKey());
1253 		log.info("loadTVSeasonsFromPlex " + tv.getTitle() + " :" + tv.getAddedAt() + " u:" + tv.getUpdatedAt());
1254 		if (log.isDebugEnabled())
1255 			log.debug("zz " + tv.getTitle() + " added:" + debugCal(tv.getAddedAt()) + " upd:" + debugCal(tv.getUpdatedAt()));
1256 		for (tv.plex.domain.Directory pd : mc.getDirectories()) {
1257 			if (pd.getKey().endsWith("/allLeaves"))
1258 				continue;
1259 			TVSeason season = DomainConverter.toTVSeason(pd);
1260 			season.setVideoId(tv.getId());
1261 			if (log.isDebugEnabled())
1262 				log.debug("zz " + season.getTitle() + " added:" + debugCal(season.getAddedAt()) + " upd:" + debugCal(season.getUpdatedAt()));
1263 			MediaContainer ms = libraryService.tvShows(season.getPlexKey());
1264 			for (tv.plex.domain.Video s : ms.getVideos()) {			// if we add a season, we want the main show to bubble to the top of the recent list.
1265 				if (s.getAddedAt() > season.getAddedAt())
1266 					tv.setAddedAt(s.getAddedAt());
1267 			}
1268 			season.setHashKey(season.hashCode());
1269 			TVSeason oseason = db.getTVSeasonFromHashKey(season.getHashKey());
1270 			if (oseason == null) {
1271 				db.saveTVSeason(season);
1272 			} else {
1273 				String s = null;
1274 				if (oseason.getPlexKey() != season.getPlexKey())
1275 					s = "plexKey Collision on " + tv.getTitle() + " : " + oseason.getTitle();
1276 				if (oseason.getVideoId() != season.getVideoId())
1277 					s = "Mismatched videoId on " + tv.getTitle() + " : " + oseason.getTitle();
1278 				if (!oseason.getTitle().equals(season.getTitle()))
1279 					s = "Mismatched title on " + tv.getTitle() + " : " + oseason.getTitle();
1280 				if (s != null) {
1281 					log.error(s);
1282 					if (!continueOnError)
1283 						throw new RuntimeException(s);
1284 					break;
1285 				}
1286 				oseason.setEpisodeCount(season.getEpisodeCount());
1287 				oseason.setUpdatedAt(season.getUpdatedAt());
1288 				db.saveTVSeason(oseason);
1289 			}
1290 			tv.addSeason(season);
1291 		}
1292 	}
1293 
1294 	private	void updateMappings(RestRefreshStatus restRefreshStatus, Library library, Video video) {
1295 		Set<Genre> genres = library.getGenres();
1296 		for (Genre g : video.getGenres()) {
1297 			for (Genre gs : genres) {
1298 				if (g.getTag().equals(gs.getTag())) {
1299 					g.setId(gs.getId());
1300 					break;
1301 				}
1302 			}
1303 			if (g.getId() != 0)
1304 				continue;
1305 			Genre gs = db.getGenre(g.getTag());
1306 			if (gs != null) {
1307 				g.clone(gs);
1308 				genres.add(g);
1309 				continue;
1310 			}
1311 			genres.add(g);
1312 			if (restRefreshStatus != null)
1313 				synchronized (restRefreshStatus) {
1314 					restRefreshStatus.addMessage(mtInfo, "Adding Genre: " + g.getTag());
1315 				}
1316 			db.addGenre(g);
1317 		}
1318 		Set<Actor> actors = library.getActors();
1319 		for (Actor a : video.getActors()) {
1320 			for (Actor as : actors) {
1321 				if (a.getName().equals(as.getName())) {
1322 					a.setId(as.getId());
1323 					break;
1324 				}
1325 			}
1326 			if (a.getId() != 0)
1327 				continue;
1328 			Actor as = db.getActor(a.getName());
1329 			if (as != null) {
1330 				a.clone(as);
1331 				actors.add(a);
1332 				continue;
1333 			}
1334 			actors.add(a);
1335 			if (restRefreshStatus != null)
1336 				synchronized (restRefreshStatus) {
1337 					restRefreshStatus.addMessage(mtInfo, "Adding Actor: " + a.getName());
1338 				}
1339 			db.addActor(a);
1340 		}
1341 		Set<Director> directors = library.getDirectors();
1342 		for (Director d : video.getDirectors()) {
1343 			for (Director ds : directors) {
1344 				if (d.getName().equals(ds.getName())) {
1345 					d.setId(ds.getId());
1346 					break;
1347 				}
1348 			}
1349 			if (d.getId() != 0)
1350 				continue;
1351 			Director ds = db.getDirector(d.getName());
1352 			if (ds != null) {
1353 				d.clone(ds);
1354 				directors.add(d);
1355 				continue;
1356 			}
1357 			directors.add(d);
1358 			if (restRefreshStatus != null)
1359 				synchronized (restRefreshStatus) {
1360 					restRefreshStatus.addMessage(mtInfo, "Adding Director: " + d.getName());
1361 				}
1362 			db.addDirector(d);
1363 		}
1364 		Set<Writer> writers = library.getWriters();
1365 		for (Writer w : video.getWriters()) {
1366 			for (Writer ws : writers) {
1367 				if (w.getName().equals(ws.getName())) {
1368 					w.setId(ws.getId());
1369 					break;
1370 				}
1371 			}
1372 			if (w.getId() != 0)
1373 				continue;
1374 			Writer ws = db.getWriter(w.getName());
1375 			if (ws != null) {
1376 				w.clone(ws);
1377 				writers.add(w);
1378 				continue;
1379 			}
1380 			writers.add(w);
1381 			if (restRefreshStatus != null)
1382 				synchronized (restRefreshStatus) {
1383 					restRefreshStatus.addMessage(mtInfo, "Adding Writer: " + w.getName());
1384 				}
1385 			db.addWriter(w);
1386 		}
1387 	}
1388 
1389 	private	List<String> setupSkipSections() {
1390 		List<String> skipSections = new ArrayList<String>();
1391 		String s = ConfigManager.getString("BuckoVidLib.skipSections", null);
1392 		if (s != null) {
1393 			String[] ss = s.split(",");
1394 			for (String t : ss) {
1395 				skipSections.add(t.trim());
1396 				log.info("skipSections: " + t);
1397 			}
1398 		} else {
1399 			log.info("skipSections: none");
1400 		}
1401 		return(skipSections);
1402 	}
1403 	private	List<String> setupRestrictedSectionNames() {
1404 		List<String> restrictedSections = new ArrayList<String>();
1405 		String s = ConfigManager.getString("BuckoVidLib.restrictedSections", null);
1406 		if (s != null) {
1407 			String[] ss = s.split(",");
1408 			for (String t : ss) {
1409 				restrictedSections.add(t.trim());
1410 				log.debug("restrictedSections: " + t);
1411 			}
1412 		}
1413 		return(restrictedSections);
1414 	}
1415 
1416 	///////////////////////////////////////////////////////////////////////////
1417 	private	Timer	recentCheckTimer = new Timer("RecentCheck");
1418 	private	TimerTask	recentCheckTask = new RecentCheckTask();
1419 	private	void setupRecentTimerCheck() {
1420 		int delay = ConfigManager.getInt("BuckoVidLib.recentUpdateCheck", 60);
1421 		recentCheckTimer.schedule(recentCheckTask, delay*1000, delay*1000);
1422 	}
1423 	private	class RecentCheckTask extends TimerTask {
1424 
1425 		@Override
1426 		public void run() {
1427 			log.debug("Checking recently added:");
1428 			if (library == null) {
1429 				log.debug("library == null");
1430 				loadLibrary();
1431 				return;
1432 			}
1433 			int vc = db.getVideoCount();
1434 			log.debug("video count: " + vc);
1435 			MediaContainer mc = libraryService.recentlyAdded();
1436 			if (mc == null)
1437 				return;
1438 			ArrayList<Video> list = new ArrayList<Video>();
1439 			for (Directory d : mc.getDirectories()) {
1440 				if (!skipSections.contains(d.getTitle())) {
1441 					TVShow tv = DomainConverter.toTVShow(d);
1442 					boolean match = false;
1443 					for (Video v : list){
1444 						if (v.hashCode() == tv.hashCode()) {
1445 							match = true;
1446 							break;
1447 						}
1448 					}
1449 					if (!match)
1450 						list.add(tv);
1451 				}
1452 			}
1453 			for (tv.plex.domain.Video pv : mc.getVideos()) {
1454 				Video bv = DomainConverter.toVideo(pv);
1455 				boolean match = false;
1456 				for (Video v : list){
1457 					if (v.hashCode() == bv.hashCode()) {
1458 						match = true;
1459 						break;
1460 					}
1461 				}
1462 				if (!match)
1463 					list.add(bv);
1464 			}
1465 			Video newestVideo = null;
1466 			for (Video v : list) {
1467 				if (newestVideo == null || 
1468 						(v.getAddedAt() > newestVideo.getAddedAt()))
1469 					newestVideo = v;
1470 				if (v instanceof TVShow) {
1471 					TVShow tv = (TVShow)v;
1472 					for (TVSeason season : tv.getSeasons()) {
1473 						if (season.getAddedAt() > newestVideo.getAddedAt()) {
1474 							newestVideo = tv;
1475 						}
1476 					}
1477 				}
1478 			}
1479 			if (newestVideo != null) {
1480 				log.debug("recentCheck: newest video: " 
1481 						+ newestVideo.getTitle() 
1482 						+ " d:" + newestVideo.getAddedAt());
1483 				if (library.getNewestVideo().getAddedAt() != newestVideo.getAddedAt()) {
1484 					log.info("library updated. Reloading");
1485 					loadLibrary();
1486 					lastUpdateTime = new Date().getTime();
1487 				}
1488 			}
1489 		}	// end run
1490 	}
1491 	
1492 }