/* This file is part of JFLICKS. JFLICKS is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. JFLICKS is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with JFLICKS. If not, see <http://www.gnu.org/licenses/>. */ package org.jflicks.videomanager.system; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.imageio.ImageIO; import org.jflicks.db.DbWorker; import org.jflicks.job.JobContainer; import org.jflicks.job.JobManager; import org.jflicks.nms.BaseNMS; import org.jflicks.nms.NMS; import org.jflicks.nms.NMSConstants; import org.jflicks.nms.Video; import org.jflicks.util.FileFind; import org.jflicks.util.LogUtil; import org.jflicks.util.Util; import org.jflicks.videomanager.BaseVideoManager; import com.db4o.ObjectContainer; import com.db4o.ObjectSet; import com.db4o.osgi.Db4oService; import com.db4o.query.Predicate; /** * This is our implementation of a VideoManager. * * @author Doug Barnum * @version 1.0 */ public class SystemVideoManager extends BaseVideoManager implements DbWorker { private static final String TV_REGEX = "S\\d\\dE\\d\\d"; private ObjectContainer objectContainer; private Db4oService db4oService; private Pattern pattern; /** * Default empty constructor. */ public SystemVideoManager() { setTitle("SystemVideoManager"); setPattern(Pattern.compile(TV_REGEX)); } private Pattern getPattern() { return (pattern); } private void setPattern(Pattern p) { pattern = p; } /** * We use the Db4oService to persist the configuration data. * * @return A Db4oService instance. */ public Db4oService getDb4oService() { return (db4oService); } /** * We use the Db4oService to persist the configuration data. * * @param s A Db4oService instance. */ public void setDb4oService(Db4oService s) { db4oService = s; } private synchronized ObjectContainer getObjectContainer() { if (objectContainer == null) { Db4oService s = getDb4oService(); if (s != null) { com.db4o.config.Configuration config = s.newConfiguration(); objectContainer = s.openFile(config, "db/video.dat"); } else { LogUtil.log(LogUtil.WARNING, "SystemVideoManager: Db4oService null!"); } } return (objectContainer); } /** * {@inheritDoc} */ public void save(Video v) { addVideo(v); } private void purge(ObjectContainer db, Class c) { if ((db != null) && (c != null)) { ObjectSet result = db.queryByExample(c); while (result.hasNext()) { db.delete(result.next()); } } } /** * Close up all resources. */ public void close() { if (objectContainer != null) { boolean result = objectContainer.close(); LogUtil.log(LogUtil.DEBUG, "SystemVideoManager: closed " + result); objectContainer = null; } else { LogUtil.log(LogUtil.DEBUG, "SystemVideoManager: Tried to close " + "but objectContainer null."); } } /** * {@inheritDoc} */ public Video getVideoById(String id) { Video result = null; ObjectContainer oc = getObjectContainer(); if ((id != null) && (oc != null)) { final String vid = id; List<Video> vids = oc.query(new Predicate<Video>() { public boolean match(Video v) { return (vid.equals(v.getId())); } }); if ((vids != null) && (vids.size() > 0)) { result = vids.get(0); if (result != null) { String h = getHost(); int p = getHttpPort(); if (h != null) { String top = "http://" + h + ":" + p + "/" + NMSConstants.HTTP_IMAGES_NAME + "/"; String sid = result.getId(); if (sid != null) { result.setBannerURL(top + getImageName(sid + "_banner.jpg")); result.setPosterURL(top + getImageName(sid + "_poster.jpg")); result.setFanartURL(top + getImageName(sid + "_fanart.jpg")); } result.setHostPort(h + ":" + p); } } } } return (result); } /** * {@inheritDoc} */ public Video[] getVideos() { Video[] result = null; ObjectContainer oc = getObjectContainer(); if (oc != null) { ObjectSet<Video> os = oc.queryByExample(Video.class); if (os != null) { result = os.toArray(new Video[os.size()]); // We should update the image URLs for the client. Persisting // the URLs is not a good idea because the URL could change. // Either by config the port changes or less likely the IP // changes. Either way we will update them. We don't check // if they actually exist, we will just build them by rule. if (result != null) { String h = getHost(); int p = getPort(); int hport = getHttpPort(); if (h != null) { String hp = h + ":" + p; String top = "http://" + h + ":" + hport + "/" + NMSConstants.HTTP_IMAGES_NAME + "/"; for (int i = 0; i < result.length; i++) { String sid = null; if (result[i].isTV()) { sid = result[i].getSubcategory(); if (sid != null) { sid = sid.replaceAll(" ", "_"); sid = sid.replaceAll("'", "_"); sid = sid.replaceAll(",", "_"); } } else { sid = result[i].getId(); } if (sid != null) { result[i].setBannerURL(top + getImageName(sid + "_banner.jpg")); result[i].setPosterURL(top + getImageName(sid + "_poster.jpg")); result[i].setFanartURL(top + getImageName(sid + "_fanart.jpg")); } result[i].setHostPort(hp); result[i].setStreamURL(computeStreamURL(result[i])); // We also want to check that the "added" // property is set. We added the "added" // property later on in development so this // is a bit of hack code to update // the DB to match the file time. if (result[i].getAdded() == 0L) { File f = new File(result[i].getPath()); if ((f.exists()) && (f.isFile())) { result[i].setAdded(f.lastModified()); addVideo(result[i]); } } } } // Now we want to sort them by the "added" property. Arrays.sort(result, new VideoSortByAdded()); } } } return (result); } /** * {@inheritDoc} */ public void removeVideo(Video v) { if (v != null) { //removeVideoFromDisk(v); removeVideoFromDB(v); videoScan(); } } /** * Convenience method to get a Video instance given just it's filename. * Remember that this is NOT the full path, it's just the name of the * file itself. * * @param s A given file name. * @return A Video instance if it is found. */ public Video getVideoByFilename(String s) { Video result = null; ObjectContainer oc = getObjectContainer(); if ((oc != null) && (s != null)) { final String fname = s; List<Video> vids = oc.query(new Predicate<Video>() { public boolean match(Video v) { return (fname.equals(v.getFilename())); } }); if ((vids != null) && (vids.size() > 0)) { // We should have just one... result = vids.get(0); if (result != null) { result.setHostPort(getHost() + ":" + getPort()); } } } return (result); } /** * Add a Video to our database. * * @param v A Video to add. */ public void addVideo(Video v) { ObjectContainer oc = getObjectContainer(); if ((v != null) && (oc != null)) { removeVideoFromDB(v); oc.store(new Video(v)); oc.commit(); } } /** * Actually delete the file on disk. * * @param v A Video instance to use to get the path of the file to delete. * @return True on success. */ public boolean removeVideoFromDisk(Video v) { boolean result = false; if (v != null) { File file = new File(v.getPath()); result = file.delete(); } return (result); } /** * This is a convenience method to remove a Video from the database, * mostly because it has been deleted from disk. If you want to hide * a video without physically deleting the file, set the "hidden" * property on the and add it again. * * @param v A given Video to remove. */ public void removeVideoFromDB(Video v) { ObjectContainer oc = getObjectContainer(); if ((v != null) && (oc != null)) { final String id = v.getId(); List<Video> vids = oc.query(new Predicate<Video>() { public boolean match(Video v) { return (id.equals(v.getId())); } }); if (vids != null) { // We will delete them all but we should have only found 1. for (int i = 0; i < vids.size(); i++) { oc.delete(vids.get(i)); } } } } /** * {@inheritDoc} */ public void generateArtwork(Video v, int seconds) { NMS n = getNMS(); if ((v != null) && (n != null)) { if (seconds <= 0) { seconds = 60; } try { File tmp = File.createTempFile("generate", ".jpg"); ThumbnailerJob job = new ThumbnailerJob(v.getPath(), tmp.getPath(), seconds); JobContainer jc = JobManager.getJobContainer(job); jc.start(); boolean done = false; int count = 0; while (!done) { if (!jc.isAlive()) { done = true; } else { count += 100; if (count > 9900) { done = true; } else { JobManager.sleep(100); } } } // We should be done at this point... if ((tmp.exists()) && (tmp.isFile())) { BufferedImage bi = ImageIO.read(tmp); if (bi != null) { LogUtil.log(LogUtil.DEBUG, "width: " + bi.getWidth()); LogUtil.log(LogUtil.DEBUG, "height: " + bi.getHeight()); if (bi.getWidth() < 1280) { // We have a source video that is 4x3. We need to // cut off the bottom. bi = Util.scale(bi, 1280); } else if ((bi.getWidth() == 1440) && (bi.getHeight() == 1080)) { // We have a 1440x1080 image that needs to be // resized. bi = Util.resize(bi, 1280, 720); } LogUtil.log(LogUtil.DEBUG, "after width: " + bi.getWidth()); LogUtil.log(LogUtil.DEBUG, "after height: " + bi.getHeight()); int height = bi.getHeight(); String sid = null; if (v.isTV()) { sid = v.getSubcategory(); if (sid != null) { sid = sid.replaceAll(" ", "_"); sid = sid.replaceAll("'", "_"); sid = sid.replaceAll(",", "_"); } } else { sid = v.getId(); } // At this point our image should be 1280x720 which // will be our fanart. Next we make a poster by // doing a center cut. First make it an image // type that works in OpenJDK. Also should work in // Oracle. BufferedImage fanbi = new BufferedImage(bi.getWidth(), bi.getHeight(), BufferedImage.TYPE_3BYTE_BGR); int[] oldData = bi.getRGB(0, 0, bi.getWidth(), bi.getHeight(), null, 0, bi.getWidth()); fanbi.setRGB(0, 0, bi.getWidth(), bi.getHeight(), oldData, 0, bi.getWidth()); File fanart = File.createTempFile("fanart", ".jpg"); ImageIO.write(fanbi, "jpg", fanart); BufferedImage pbi = fanbi.getSubimage(360, 0, 495, height); File poster = File.createTempFile("poster", ".jpg"); ImageIO.write(pbi, "jpg", poster); String uri = fanart.toURI().toString(); n.save(NMSConstants.FANART_IMAGE_TYPE, uri, sid); uri = poster.toURI().toString(); n.save(NMSConstants.POSTER_IMAGE_TYPE, uri, sid); } } } catch (IOException ex) { LogUtil.log(LogUtil.DEBUG, ex.getMessage()); } } } private long computeDuration(Video v) { long result = 0L; if (v != null) { MediainfoJob job = new MediainfoJob(v); JobContainer jc = JobManager.getJobContainer(job); jc.start(); boolean done = false; int count = 0; while (!done) { if (!jc.isAlive()) { done = true; } else { count += 100; if (count > 2900) { done = true; } else { JobManager.sleep(100); } } } result = (long) job.getSeconds(); } return (result); } /** * {@inheritDoc} */ public synchronized void videoScan() { LogUtil.log(LogUtil.INFO, "Time to scan for video files..."); // First we are going to see if the DB video has been // changed to an mp4 extension. This is really hack code // and we should remove later. What we will do is if the // file path doesn't exist, but an mp4 version does, then // we will update the database. Video[] currentVids = getVideos(); if ((currentVids != null) && (currentVids.length > 0)) { for (int i = 0; i < currentVids.length; i++) { Video v = currentVids[i]; String path = v.getPath(); File vfile = new File(path); if (!vfile.exists()) { // It's gone. See if there is an mp4 version. path = path.substring(0, path.lastIndexOf(".")); path = path + ".mp4"; vfile = new File(path); if (vfile.exists()) { v.setPath(path); v.setFilename(vfile.getName()); addVideo(v); } } } } String[] array = getConfiguredVideoDirectories(); if (array != null) { String[] exts = getConfiguredVideoExtensions(); for (int i = 0; i < array.length; i++) { File dir = new File(array[i]); if ((dir.exists()) && (dir.isDirectory())) { FileFind ff = FileFind.getInstance(); File[] files = ff.find(dir, exts); if (files != null) { for (int j = 0; j < files.length; j++) { String name = files[j].getName(); String path = files[j].getPath(); Video v = getVideoByFilename(name); if (v == null) { String title = name; int index = title.lastIndexOf("."); if (index != -1) { title = title.substring(0, index); } v = new Video(); v.setCategory(guessVideoCategory(title)); v.setSeason(guessSeason(title)); v.setEpisode(guessEpisode(title)); v.setFilename(name); v.setTitle(guessVideoTitle(title, v.isTV())); v.setPath(path); v.setHidden(false); if (v.isTV()) { v.setSubcategory(v.getTitle() + " Season " + v.getSeason()); } else { v.setSubcategory( NMSConstants.UNKNOWN_GENRE); } v.setDuration(computeDuration(v)); addVideo(v); } else if (path != null) { if ((!path.equals(v.getPath())) || (v.isHidden())) { v.setPath(path); v.setHidden(false); addVideo(v); } } } } } } } removeMissingPaths(); } private void removeMissingPaths() { Video[] array = getVideos(); if ((array != null) && (array.length > 0)) { for (int i = 0; i < array.length; i++) { String title = array[i].getTitle(); String path = array[i].getPath(); if ((title != null) && (path != null)) { File f = new File(path); if (!f.exists()) { // Check to see if we should set it hidden or // purge it from the DB. File vpurge = new File("vpurge"); if (vpurge.exists()) { removeVideoFromDB(array[i]); } else { LogUtil.log(LogUtil.INFO, "Will hide <" + title + "> with path <" + path + ">"); array[i].setHidden(true); addVideo(array[i]); } } } } } } private String guessVideoTitle(String s, boolean tv) { String result = s; if (result != null) { result = result.replaceAll("_", " "); result = result.replaceAll("-", " "); if (tv) { int index = result.lastIndexOf(" "); if (index != -1) { result = result.substring(0, index); } } result = result.trim(); } return (result); } private String guessVideoCategory(String s) { String result = NMSConstants.VIDEO_MOVIE; Pattern p = getPattern(); if (p != null) { Matcher matcher = pattern.matcher(s); if (matcher.find()) { result = NMSConstants.VIDEO_TV; } } return (result); } private int guessSeason(String s) { int result = 1; Pattern p = getPattern(); if (p != null) { Matcher matcher = pattern.matcher(s); if (matcher.find()) { String tmp = matcher.group(); tmp = tmp.substring(1, 3); result = Util.str2int(tmp, result); } } return (result); } private int guessEpisode(String s) { int result = 1; Pattern p = getPattern(); if (p != null) { Matcher matcher = pattern.matcher(s); if (matcher.find()) { String tmp = matcher.group(); tmp = tmp.substring(4); result = Util.str2int(tmp, result); } } return (result); } private File getImageHome() { File result = null; NMS n = getNMS(); if (n instanceof BaseNMS) { BaseNMS bn = (BaseNMS) n; String path = bn.getConfiguredImageHome(); if (path != null) { result = new File(path); } } return (result); } private String getImageName(String s) { String result = "no_image.jpg"; if (s != null) { File f = getImageHome(); if (f != null) { File iname = new File(f, s); if ((iname.exists()) && (iname.isFile())) { result = s; } } } return (result); } static class VideoSortByAdded implements Comparator<Video>, Serializable { public VideoSortByAdded() { } public int compare(Video v0, Video v1) { Long l0 = Long.valueOf(v0.getAdded()); Long l1 = Long.valueOf(v1.getAdded()); return (l1.compareTo(l0)); } } }