From a1a18f77a50804e0127dfa4b0f5240c49c541184 Mon Sep 17 00:00:00 2001 From: Scott Jackson Date: Mon, 2 Jul 2012 21:24:02 -0700 Subject: Initial Commit --- .../subsonic/service/MediaFileService.java | 614 +++++++++++++++++++++ 1 file changed, 614 insertions(+) create mode 100644 subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaFileService.java (limited to 'subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaFileService.java') diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaFileService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaFileService.java new file mode 100644 index 00000000..bc575714 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaFileService.java @@ -0,0 +1,614 @@ +/* + This file is part of Subsonic. + + Subsonic 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. + + Subsonic 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 Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +import net.sf.ehcache.Ehcache; +import net.sf.ehcache.Element; +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.AlbumDao; +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.domain.Album; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MediaFileComparator; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.service.metadata.JaudiotaggerParser; +import net.sourceforge.subsonic.service.metadata.MetaData; +import net.sourceforge.subsonic.service.metadata.MetaDataParser; +import net.sourceforge.subsonic.service.metadata.MetaDataParserFactory; +import net.sourceforge.subsonic.util.FileUtil; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import static net.sourceforge.subsonic.domain.MediaFile.MediaType.*; + +/** + * Provides services for instantiating and caching media files and cover art. + * + * @author Sindre Mehus + */ +public class MediaFileService { + + private static final Logger LOG = Logger.getLogger(MediaFileService.class); + + private Ehcache mediaFileMemoryCache; + + private SecurityService securityService; + private SettingsService settingsService; + private MediaFileDao mediaFileDao; + private AlbumDao albumDao; + private MetaDataParserFactory metaDataParserFactory; + + /** + * Returns a media file instance for the given file. If possible, a cached value is returned. + * + * @param file A file on the local file system. + * @return A media file instance, or null if not found. + * @throws SecurityException If access is denied to the given file. + */ + public MediaFile getMediaFile(File file) { + return getMediaFile(file, settingsService.isFastCacheEnabled()); + } + + /** + * Returns a media file instance for the given file. If possible, a cached value is returned. + * + * @param file A file on the local file system. + * @return A media file instance, or null if not found. + * @throws SecurityException If access is denied to the given file. + */ + public MediaFile getMediaFile(File file, boolean useFastCache) { + + // Look in fast memory cache first. + Element element = mediaFileMemoryCache.get(file); + MediaFile result = element == null ? null : (MediaFile) element.getObjectValue(); + if (result != null) { + return result; + } + + if (!securityService.isReadAllowed(file)) { + throw new SecurityException("Access denied to file " + file); + } + + // Secondly, look in database. + result = mediaFileDao.getMediaFile(file.getPath()); + if (result != null) { + result = checkLastModified(result, useFastCache); + mediaFileMemoryCache.put(new Element(file, result)); + return result; + } + + if (!FileUtil.exists(file)) { + return null; + } + // Not found in database, must read from disk. + result = createMediaFile(file); + + // Put in cache and database. + mediaFileMemoryCache.put(new Element(file, result)); + mediaFileDao.createOrUpdateMediaFile(result); + + return result; + } + + private MediaFile checkLastModified(MediaFile mediaFile, boolean useFastCache) { + if (useFastCache || mediaFile.getChanged().getTime() >= FileUtil.lastModified(mediaFile.getFile())) { + return mediaFile; + } + mediaFile = createMediaFile(mediaFile.getFile()); + mediaFileDao.createOrUpdateMediaFile(mediaFile); + return mediaFile; + } + + /** + * Returns a media file instance for the given path name. If possible, a cached value is returned. + * + * @param pathName A path name for a file on the local file system. + * @return A media file instance. + * @throws SecurityException If access is denied to the given file. + */ + public MediaFile getMediaFile(String pathName) { + return getMediaFile(new File(pathName)); + } + + // TODO: Optimize with memory caching. + public MediaFile getMediaFile(int id) { + MediaFile mediaFile = mediaFileDao.getMediaFile(id); + if (mediaFile == null) { + return null; + } + + if (!securityService.isReadAllowed(mediaFile.getFile())) { + throw new SecurityException("Access denied to file " + mediaFile); + } + + return checkLastModified(mediaFile, settingsService.isFastCacheEnabled()); + } + + public MediaFile getParentOf(MediaFile mediaFile) { + if (mediaFile.getParentPath() == null) { + return null; + } + return getMediaFile(mediaFile.getParentPath()); + } + + public List getChildrenOf(String parentPath, boolean includeFiles, boolean includeDirectories, boolean sort) { + return getChildrenOf(new File(parentPath), includeFiles, includeDirectories, sort); + } + + public List getChildrenOf(File parent, boolean includeFiles, boolean includeDirectories, boolean sort) { + return getChildrenOf(getMediaFile(parent), includeFiles, includeDirectories, sort); + } + + /** + * Returns all media files that are children of a given media file. + * + * @param includeFiles Whether files should be included in the result. + * @param includeDirectories Whether directories should be included in the result. + * @param sort Whether to sort files in the same directory. + * @return All children media files. + */ + public List getChildrenOf(MediaFile parent, boolean includeFiles, boolean includeDirectories, boolean sort) { + return getChildrenOf(parent, includeFiles, includeDirectories, sort, settingsService.isFastCacheEnabled()); + } + + /** + * Returns all media files that are children of a given media file. + * + * @param includeFiles Whether files should be included in the result. + * @param includeDirectories Whether directories should be included in the result. + * @param sort Whether to sort files in the same directory. + * @return All children media files. + */ + public List getChildrenOf(MediaFile parent, boolean includeFiles, boolean includeDirectories, boolean sort, boolean useFastCache) { + + if (!parent.isDirectory()) { + return Collections.emptyList(); + } + + // Make sure children are stored and up-to-date in the database. + if (!useFastCache) { + updateChildren(parent); + } + + List result = new ArrayList(); + for (MediaFile child : mediaFileDao.getChildrenOf(parent.getPath())) { + child = checkLastModified(child, useFastCache); + if (child.isDirectory() && includeDirectories) { + result.add(child); + } + if (child.isFile() && includeFiles) { + result.add(child); + } + } + + if (sort) { + Comparator comparator = new MediaFileComparator(settingsService.isSortAlbumsByYear()); + // Note: Intentionally not using Collections.sort() since it can be problematic on Java 7. + // http://www.oracle.com/technetwork/java/javase/compatibility-417013.html#jdk7 + Set set = new TreeSet(comparator); + set.addAll(result); + result = new ArrayList(set); + } + + return result; + } + + /** + * Returns whether the given file is the root of a media folder. + * + * @see MusicFolder + */ + public boolean isRoot(MediaFile mediaFile) { + for (MusicFolder musicFolder : settingsService.getAllMusicFolders(false, true)) { + if (mediaFile.getPath().equals(musicFolder.getPath().getPath())) { + return true; + } + } + return false; + } + + /** + * Returns all genres in the music collection. + * + * @return Sorted list of genres. + */ + public List getGenres() { + return mediaFileDao.getGenres(); + } + + /** + * Returns the most frequently played albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @return The most frequently played albums. + */ + public List getMostFrequentlyPlayedAlbums(int offset, int count) { + return mediaFileDao.getMostFrequentlyPlayedAlbums(offset, count); + } + + /** + * Returns the most recently played albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @return The most recently played albums. + */ + public List getMostRecentlyPlayedAlbums(int offset, int count) { + return mediaFileDao.getMostRecentlyPlayedAlbums(offset, count); + } + + /** + * Returns the most recently added albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @return The most recently added albums. + */ + public List getNewestAlbums(int offset, int count) { + return mediaFileDao.getNewestAlbums(offset, count); + } + + /** + * Returns the most recently starred albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param username Returns albums starred by this user. + * @return The most recently starred albums for this user. + */ + public List getStarredAlbums(int offset, int count, String username) { + return mediaFileDao.getStarredAlbums(offset, count, username); + } + + /** + * Returns albums in alphabetial order. + * + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param byArtist Whether to sort by artist name + * @return Albums in alphabetical order. + */ + public List getAlphabetialAlbums(int offset, int count, boolean byArtist) { + return mediaFileDao.getAlphabetialAlbums(offset, count, byArtist); + } + + public Date getMediaFileStarredDate(int id, String username) { + return mediaFileDao.getMediaFileStarredDate(id, username); + } + + public void populateStarredDate(List mediaFiles, String username) { + for (MediaFile mediaFile : mediaFiles) { + populateStarredDate(mediaFile, username); + } + } + + public void populateStarredDate(MediaFile mediaFile, String username) { + Date starredDate = mediaFileDao.getMediaFileStarredDate(mediaFile.getId(), username); + mediaFile.setStarredDate(starredDate); + } + + private void updateChildren(MediaFile parent) { + + // Check timestamps. + if (parent.getChildrenLastUpdated().getTime() >= parent.getChanged().getTime()) { + return; + } + + List storedChildren = mediaFileDao.getChildrenOf(parent.getPath()); + Map storedChildrenMap = new HashMap(); + for (MediaFile child : storedChildren) { + storedChildrenMap.put(child.getPath(), child); + } + + List children = filterMediaFiles(FileUtil.listFiles(parent.getFile())); + for (File child : children) { + if (storedChildrenMap.remove(child.getPath()) == null) { + // Add children that are not already stored. + mediaFileDao.createOrUpdateMediaFile(createMediaFile(child)); + } + } + + // Delete children that no longer exist on disk. + for (String path : storedChildrenMap.keySet()) { + mediaFileDao.deleteMediaFile(path); + } + + // Update timestamp in parent. + parent.setChildrenLastUpdated(parent.getChanged()); + parent.setPresent(true); + mediaFileDao.createOrUpdateMediaFile(parent); + } + + private List filterMediaFiles(File[] candidates) { + List result = new ArrayList(); + for (File candidate : candidates) { + String suffix = FilenameUtils.getExtension(candidate.getName()).toLowerCase(); + if (!isExcluded(candidate) && (FileUtil.isDirectory(candidate) || isAudioFile(suffix) || isVideoFile(suffix))) { + result.add(candidate); + } + } + return result; + } + + private boolean isAudioFile(String suffix) { + for (String s : settingsService.getMusicFileTypesAsArray()) { + if (suffix.equals(s.toLowerCase())) { + return true; + } + } + return false; + } + + private boolean isVideoFile(String suffix) { + for (String s : settingsService.getVideoFileTypesAsArray()) { + if (suffix.equals(s.toLowerCase())) { + return true; + } + } + return false; + } + + /** + * Returns whether the given file is excluded. + * + * @param file The child file in question. + * @return Whether the child file is excluded. + */ + private boolean isExcluded(File file) { + + // Exclude all hidden files starting with a "." or "@eaDir" (thumbnail dir created on Synology devices). + String name = file.getName(); + return name.startsWith(".") || name.startsWith("@eaDir") || name.equals("Thumbs.db"); + } + + private MediaFile createMediaFile(File file) { + MediaFile mediaFile = new MediaFile(); + Date lastModified = new Date(FileUtil.lastModified(file)); + mediaFile.setPath(file.getPath()); + mediaFile.setFolder(securityService.getRootFolderForFile(file)); + mediaFile.setParentPath(file.getParent()); + mediaFile.setChanged(lastModified); + mediaFile.setLastScanned(new Date()); + mediaFile.setPlayCount(0); + mediaFile.setChildrenLastUpdated(new Date(0)); + mediaFile.setCreated(lastModified); + mediaFile.setMediaType(DIRECTORY); + mediaFile.setPresent(true); + + if (file.isFile()) { + + MetaDataParser parser = metaDataParserFactory.getParser(file); + if (parser != null) { + MetaData metaData = parser.getMetaData(file); + mediaFile.setArtist(metaData.getArtist()); + mediaFile.setAlbumArtist(metaData.getArtist()); + mediaFile.setAlbumName(metaData.getAlbumName()); + mediaFile.setTitle(metaData.getTitle()); + mediaFile.setDiscNumber(metaData.getDiscNumber()); + mediaFile.setTrackNumber(metaData.getTrackNumber()); + mediaFile.setGenre(metaData.getGenre()); + mediaFile.setYear(metaData.getYear()); + mediaFile.setDurationSeconds(metaData.getDurationSeconds()); + mediaFile.setBitRate(metaData.getBitRate()); + mediaFile.setVariableBitRate(metaData.getVariableBitRate()); + mediaFile.setHeight(metaData.getHeight()); + mediaFile.setWidth(metaData.getWidth()); + } + String format = StringUtils.trimToNull(StringUtils.lowerCase(FilenameUtils.getExtension(mediaFile.getPath()))); + mediaFile.setFormat(format); + mediaFile.setFileSize(FileUtil.length(file)); + mediaFile.setMediaType(getMediaType(mediaFile)); + + } else { + + // Is this an album? + if (!isRoot(mediaFile)) { + File[] children = FileUtil.listFiles(file); + File firstChild = null; + for (File child : filterMediaFiles(children)) { + if (FileUtil.isFile(child)) { + firstChild = child; + break; + } + } + + if (firstChild != null) { + mediaFile.setMediaType(ALBUM); + + // Guess artist/album name and year. + MetaDataParser parser = metaDataParserFactory.getParser(firstChild); + if (parser != null) { + MetaData metaData = parser.getMetaData(firstChild); + mediaFile.setArtist(metaData.getArtist()); + mediaFile.setAlbumName(metaData.getAlbumName()); + mediaFile.setYear(metaData.getYear()); + } + + // Look for cover art. + try { + File coverArt = findCoverArt(children); + if (coverArt != null) { + mediaFile.setCoverArtPath(coverArt.getPath()); + } + } catch (IOException x) { + LOG.error("Failed to find cover art.", x); + } + + } else { + mediaFile.setArtist(file.getName()); + } + } + } + + return mediaFile; + } + + private MediaFile.MediaType getMediaType(MediaFile mediaFile) { + if (isVideoFile(mediaFile.getFormat())) { + return VIDEO; + } + String path = mediaFile.getPath().toLowerCase(); + String genre = StringUtils.trimToEmpty(mediaFile.getGenre()).toLowerCase(); + if (path.contains("podcast") || genre.contains("podcast")) { + return PODCAST; + } + if (path.contains("audiobook") || genre.contains("audiobook") || path.contains("audio book") || genre.contains("audio book")) { + return AUDIOBOOK; + } + return MUSIC; + } + + public void refreshMediaFile(MediaFile mediaFile) { + mediaFile = createMediaFile(mediaFile.getFile()); + mediaFileDao.createOrUpdateMediaFile(mediaFile); + mediaFileMemoryCache.remove(mediaFile.getFile()); + } + + /** + * Returns a cover art image for the given media file. + */ + public File getCoverArt(MediaFile mediaFile) { + if (mediaFile.getCoverArtFile() != null) { + return mediaFile.getCoverArtFile(); + } + MediaFile parent = getParentOf(mediaFile); + return parent == null ? null : parent.getCoverArtFile(); + } + + /** + * Finds a cover art image for the given directory, by looking for it on the disk. + */ + private File findCoverArt(File[] candidates) throws IOException { + for (String mask : settingsService.getCoverArtFileTypesAsArray()) { + for (File candidate : candidates) { + if (candidate.isFile() && candidate.getName().toUpperCase().endsWith(mask.toUpperCase()) && !candidate.getName().startsWith(".")) { + return candidate; + } + } + } + + // Look for embedded images in audiofiles. (Only check first audio file encountered). + JaudiotaggerParser parser = new JaudiotaggerParser(); + for (File candidate : candidates) { + if (parser.isApplicable(candidate)) { + if (parser.isImageAvailable(getMediaFile(candidate))) { + return candidate; + } else { + return null; + } + } + } + return null; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setMediaFileMemoryCache(Ehcache mediaFileMemoryCache) { + this.mediaFileMemoryCache = mediaFileMemoryCache; + } + + public void setMediaFileDao(MediaFileDao mediaFileDao) { + this.mediaFileDao = mediaFileDao; + } + + /** + * Returns all media files that are children, grand-children etc of a given media file. + * Directories are not included in the result. + * + * @param sort Whether to sort files in the same directory. + * @return All descendant music files. + */ + public List getDescendantsOf(MediaFile ancestor, boolean sort) { + + if (ancestor.isFile()) { + return Arrays.asList(ancestor); + } + + List result = new ArrayList(); + + for (MediaFile child : getChildrenOf(ancestor, true, true, sort)) { + if (child.isDirectory()) { + result.addAll(getDescendantsOf(child, sort)); + } else { + result.add(child); + } + } + return result; + } + + public void setMetaDataParserFactory(MetaDataParserFactory metaDataParserFactory) { + this.metaDataParserFactory = metaDataParserFactory; + } + + public void updateMediaFile(MediaFile mediaFile) { + mediaFileDao.createOrUpdateMediaFile(mediaFile); + } + + /** + * Increments the play count and last played date for the given media file and its + * directory and album. + */ + public void incrementPlayCount(MediaFile file) { + Date now = new Date(); + file.setLastPlayed(now); + file.setPlayCount(file.getPlayCount() + 1); + updateMediaFile(file); + + MediaFile parent = getParentOf(file); + if (!isRoot(parent)) { + parent.setLastPlayed(now); + parent.setPlayCount(parent.getPlayCount() + 1); + updateMediaFile(parent); + } + + Album album = albumDao.getAlbum(file.getAlbumArtist(), file.getAlbumName()); + if (album != null) { + album.setLastPlayed(now); + album.setPlayCount(album.getPlayCount() + 1); + albumDao.createOrUpdateAlbum(album); + } + } + + public void setAlbumDao(AlbumDao albumDao) { + this.albumDao = albumDao; + } + +} -- cgit v1.2.3