net.sourceforge.subsonic.domain.MusicFile.java Source code

Java tutorial

Introduction

Here is the source code for net.sourceforge.subsonic.domain.MusicFile.java

Source

/*
 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 <http://www.gnu.org/licenses/>.
    
 Copyright 2009 (C) Sindre Mehus
 */
package net.sourceforge.subsonic.domain;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.filefilter.DirectoryFileFilter;
import org.apache.commons.io.filefilter.FalseFileFilter;
import org.apache.commons.io.filefilter.FileFileFilter;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.springframework.util.StringUtils;

import net.sourceforge.subsonic.Logger;
import net.sourceforge.subsonic.service.MusicFileService;
import net.sourceforge.subsonic.service.ServiceLocator;
import net.sourceforge.subsonic.service.SettingsService;
import net.sourceforge.subsonic.service.metadata.MetaDataParser;
import net.sourceforge.subsonic.util.FileUtil;
import net.sourceforge.subsonic.util.StringUtil;

/**
 * Represents a file or directory containing music. Music files can be put in a {@link Playlist},
 * and may be streamed to remote players.  All music files are located in a configurable root music folder.
 *
 * @author Sindre Mehus
 */
public class MusicFile implements Serializable {

    private static final Logger LOG = Logger.getLogger(MusicFile.class);

    private File file;
    private boolean isFile;
    private boolean isDirectory;
    private boolean isVideo;
    private long lastModified;
    private MetaData metaData;
    private Set<String> excludes;

    /**
     * Do not use this method directly. Instead, use {@link MusicFileService#getMusicFile}.
     *
     * @param file A file on the local file system.
     * @deprecated Use {@link MusicFileService#getMusicFile} instead.
     */
    @Deprecated
    public MusicFile(File file) {
        this.file = file;

        // Cache these values for performance.
        isFile = file.isFile();
        isDirectory = file.isDirectory();
        lastModified = file.lastModified();
        String suffix = FilenameUtils.getExtension(file.getName()).toLowerCase();
        isVideo = isFile && isVideoFile(suffix);
    }

    /**
     * Empty constructor.  Used for testing purposes only.
     */
    protected MusicFile() {
        isFile = true;
    }

    /**
     * Returns the underlying {@link File}.
     *
     * @return The file wrapped by this MusicFile.
     */
    public File getFile() {
        return file;
    }

    /**
     * Returns whether this music file is a normal file (and not a directory).
     *
     * @return Whether this music file is a normal file (and not a directory).
     */
    public boolean isFile() {
        return isFile;
    }

    /**
     * Returns whether this music file is a directory.
     *
     * @return Whether this music file is a directory.
     */
    public boolean isDirectory() {
        return isDirectory;
    }

    /**
     * Returns whether this "music" file is a video.
     *
     * @return Whether this "music" file is a video.
     */
    public boolean isVideo() {
        return isVideo;
    }

    /**
     * Returns whether this music file is an album, i.e., whether it is a directory containing
     * songs.
     *
     * @return Whether this music file is an album
     * @throws IOException If an I/O error occurs.
     */
    public boolean isAlbum() throws IOException {
        return !isFile && getFirstChild() != null;
    }

    /**
     * Returns whether this music file is one of the root music folders.
     *
     * @return Whether this music file is one of the root music folders.
     */
    public boolean isRoot() {
        SettingsService settings = ServiceLocator.getSettingsService();
        List<MusicFolder> folders = settings.getAllMusicFolders();
        for (MusicFolder folder : folders) {
            if (file.equals(folder.getPath())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns the time this music file was last modified.
     *
     * @return The time since this music file was last modified, in milliseconds since the epoch.
     */
    public long lastModified() {
        return lastModified;
    }

    /**
     * Returns the length of the music file.
     * The return value is unspecified if this music file is a directory.
     *
     * @return The length, in bytes, of the music file, or
     *         or <code>0L</code> if the file does not exist
     */
    public long length() {
        return file.length();
    }

    /**
     * Returns whether this music file exists.
     *
     * @return Whether this music file exists.
     */
    public boolean exists() {
        return file.exists();
    }

    /**
     * Returns the name of the music file. This is normally just the last name in
     * the pathname's name sequence.
     *
     * @return The name of the music file.
     */
    public String getName() {
        String name = file.getName();
        try {
            // Remove artist name from album name, if present.
            String parentName = getParent().getName() + " - ";
            if (name.startsWith(parentName)) {
                name = name.substring(parentName.length());
            }
        } catch (Exception x) {
            // Ignored
        }
        return name;
    }

    /**
     * Same as {@link #getName}, but without file suffix (unless this music file
     * represents a directory).
     *
     * @return The name of the file without the suffix
     */
    public String getNameWithoutSuffix() {
        String name = getName();
        if (isDirectory()) {
            return name;
        }
        int i = name.lastIndexOf('.');
        return i == -1 ? name : name.substring(0, i);
    }

    /**
     * Returns the file suffix, e.g., "mp3".
     *
     * @return The file suffix.
     */
    public String getSuffix() {
        return StringUtils.getFilenameExtension(getName());
    }

    /**
     * Returns the full pathname as a string.
     *
     * @return The full pathname as a string.
     */
    public String getPath() {
        return file.getPath();
    }

    /**
     * Returns meta data for this music file.
     *
     * @return Meta data (artist, album, title etc) for this music file.
     */
    public synchronized MetaData getMetaData() {
        if (metaData == null) {
            MetaDataParser parser = ServiceLocator.getMetaDataParserFactory().getParser(this);
            metaData = (parser == null) ? null : parser.getMetaData(this);
        }
        return metaData;
    }

    /**
     * Returns the title of the music file, by attempting to parse relevant meta-data embedded in the file,
     * for instance ID3 tags in MP3 files. <p/>
     * If this music file is a directory, or if no tags are found, this method is equivalent to {@link #getNameWithoutSuffix}.
     *
     * @return The song title of this music file.
     */
    public String getTitle() {
        return getMetaData() == null ? getNameWithoutSuffix() : getMetaData().getTitle();
    }

    /**
     * Returns the parent music file.
     *
     * @return The parent music file, or <code>null</code> if no parent exists.
     * @throws IOException If an I/O error occurs.
     */
    public MusicFile getParent() throws IOException {
        File parent = file.getParentFile();
        return parent == null ? null : createMusicFile(parent);
    }

    /**
     * Returns all music files that are children of this music 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 music files.
     * @throws IOException If an I/O error occurs.
     */
    public List<MusicFile> getChildren(boolean includeFiles, boolean includeDirectories, boolean sort)
            throws IOException {

        FileFilter filter;
        if (includeFiles && includeDirectories) {
            filter = TrueFileFilter.INSTANCE;
        } else if (includeFiles) {
            filter = FileFileFilter.FILE;
        } else if (includeDirectories) {
            filter = DirectoryFileFilter.DIRECTORY;
        } else {
            filter = FalseFileFilter.INSTANCE;
        }

        File[] children = FileUtil.listFiles(file, filter);
        List<MusicFile> result = new ArrayList<MusicFile>(children.length);

        for (File child : children) {
            try {
                if (acceptMedia(child)) {
                    result.add(createMusicFile(child));
                }
            } catch (SecurityException x) {
                LOG.warn("Failed to create MusicFile for " + child, x);
            }
        }

        if (sort) {
            Collections.sort(result, new MusicFileSorter());
        }

        return result;
    }

    /**
     * Returns all music files that are children, grand-children etc of this music file.
     *
     * @param includeDirectories Whether directories should be included in the result.
     * @param sort               Whether to sort files in the same directory.
     * @return All descendant music files.
     * @throws IOException If an I/O error occurs.
     */
    public List<MusicFile> getDescendants(final boolean includeDirectories, final boolean sort) throws IOException {
        final List<MusicFile> result = new ArrayList<MusicFile>();

        Visitor visitor = new Visitor() {
            public void visit(MusicFile musicFile) {
                result.add(musicFile);
            }

            public boolean includeDirectories() {
                return includeDirectories;
            }

            public boolean sorted() {
                return sort;
            }
        };

        accept(visitor);
        return result;
    }

    /**
     * Accepts the given visitor (as in the <em>Visitor</em> pattern). Recursively calls <code>accept()</code> on all
     * descendants of this music file.
     *
     * @param visitor The visitor.
     * @throws IOException If an I/O error occurs.
     */
    public void accept(Visitor visitor) throws IOException {
        if (isFile || visitor.includeDirectories()) {
            visitor.visit(this);
        }

        if (isDirectory()) {
            List<MusicFile> children = getChildren(true, true, visitor.sorted());

            for (MusicFile child : children) {
                child.accept(visitor);
            }
        }
    }

    private MusicFile createMusicFile(File file) {
        return ServiceLocator.getMusicFileService().getMusicFile(file);
    }

    /**
     * Returns the first direct child (excluding directories).
     * This method is an optimization.
     *
     * @return The first child, or <code>null</code> if not found.
     * @throws IOException If an I/O error occurs.
     */
    public MusicFile getFirstChild() throws IOException {
        File[] files = FileUtil.listFiles(file);
        for (File f : files) {
            if (f.isFile() && acceptMedia(f)) {
                try {
                    return createMusicFile(f);
                } catch (SecurityException x) {
                    LOG.warn("Failed to create MusicFile for " + f, x);
                }
            }
        }
        return null;
    }

    private boolean acceptMedia(File file) throws IOException {

        if (isExcluded(file)) {
            return false;
        }

        if (file.isDirectory()) {
            return true;
        }

        String suffix = FilenameUtils.getExtension(file.getName()).toLowerCase();
        return isMusicFile(suffix) || isVideoFile(suffix);
    }

    private static boolean isMusicFile(String suffix) {
        for (String s : ServiceLocator.getSettingsService().getMusicFileTypesAsArray()) {
            if (suffix.equals(s.toLowerCase())) {
                return true;
            }
        }
        return false;
    }

    private static boolean isVideoFile(String suffix) {
        for (String s : ServiceLocator.getSettingsService().getVideoFileTypesAsArray()) {
            if (suffix.equals(s.toLowerCase())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns whether the given file is excluded, i.e., whether it is listed in 'subsonic_exclude.txt' in
     * the current directory.
     *
     * @param file The child file in question.
     * @return Whether the child file is excluded.
     */
    public boolean isExcluded(File file) throws IOException {

        // Exclude all hidden files starting with a "." or "@eaDir" (thumbnail dir created on Synology devices).
        if (file.getName().startsWith(".") || file.getName().startsWith("@eaDir")) {
            return true;
        }

        if (excludes == null) {
            excludes = new HashSet<String>();
            File excludeFile = new File(this.file, "subsonic_exclude.txt");
            if (excludeFile.exists()) {
                String[] lines = StringUtil.readLines(new FileInputStream(excludeFile));
                for (String line : lines) {
                    excludes.add(line.toLowerCase());
                }
            }
        }

        return excludes.contains(file.getName().toLowerCase());
    }

    /**
     * Returns whether this music file is equal to another object.
     *
     * @param o The object to compare to.
     * @return Whether this music file is equal to another object.
     */
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof MusicFile)) {
            return false;
        }

        final MusicFile musicFile = (MusicFile) o;

        if (file != null ? !file.equals(musicFile.file) : musicFile.file != null) {
            return false;
        }

        return true;
    }

    /**
     * Returns the hash code of this music file.
     *
     * @return The hash code of this music file.
     */
    @Override
    public int hashCode() {
        return (file != null ? file.hashCode() : 0);
    }

    /**
     * Equivalent to {@link #getPath}.
     *
     * @return This music file as a string.
     */
    @Override
    public String toString() {
        return getPath();
    }

    /**
     * Contains meta-data (song title, artist, album etc) for a music file.
     */
    public static class MetaData implements Serializable {

        private Integer discNumber;
        private Integer trackNumber;
        private String title;
        private String artist;
        private String album;
        private String genre;
        private String year;
        private Integer bitRate;
        private Boolean variableBitRate;
        private Integer duration;
        private String format;
        private Long fileSize;
        private Integer width;
        private Integer height;

        public Integer getDiscNumber() {
            return discNumber;
        }

        public void setDiscNumber(Integer discNumber) {
            this.discNumber = discNumber;
        }

        public Integer getTrackNumber() {
            return trackNumber;
        }

        public void setTrackNumber(Integer trackNumber) {
            this.trackNumber = trackNumber;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public String getArtist() {
            return artist;
        }

        public void setArtist(String artist) {
            this.artist = artist;
        }

        public String getAlbum() {
            return album;
        }

        public void setAlbum(String album) {
            this.album = album;
        }

        public String getGenre() {
            return genre;
        }

        public void setGenre(String genre) {
            this.genre = genre;
        }

        public String getYear() {
            return year;
        }

        public Integer getYearAsInteger() {
            if (year == null || year.length() < 4) {
                return null;
            }
            try {
                return new Integer(year.substring(0, 4));
            } catch (NumberFormatException x) {
                return null;
            }
        }

        public void setYear(String year) {
            this.year = year;
        }

        public Integer getBitRate() {
            return bitRate;
        }

        public void setBitRate(Integer bitRate) {
            this.bitRate = bitRate;
        }

        public Boolean getVariableBitRate() {
            return variableBitRate;
        }

        public void setVariableBitRate(Boolean variableBitRate) {
            this.variableBitRate = variableBitRate;
        }

        public Integer getDuration() {
            return duration;
        }

        public String getDurationAsString() {
            if (duration == null) {
                return null;
            }

            StringBuffer result = new StringBuffer(8);

            int seconds = duration;

            int hours = seconds / 3600;
            seconds -= hours * 3600;

            int minutes = seconds / 60;
            seconds -= minutes * 60;

            if (hours > 0) {
                result.append(hours).append(':');
                if (minutes < 10) {
                    result.append('0');
                }
            }

            result.append(minutes).append(':');
            if (seconds < 10) {
                result.append('0');
            }
            result.append(seconds);

            return result.toString();
        }

        public void setDuration(Integer duration) {
            this.duration = duration;
        }

        public String getFormat() {
            return format;
        }

        public void setFormat(String format) {
            this.format = format;
        }

        public Long getFileSize() {
            return fileSize;
        }

        public void setFileSize(Long fileSize) {
            this.fileSize = fileSize;
        }

        public Integer getWidth() {
            return width;
        }

        public void setWidth(Integer width) {
            this.width = width;
        }

        public Integer getHeight() {
            return height;
        }

        public void setHeight(Integer height) {
            this.height = height;
        }
    }

    /**
     * Comparator for sorting music files.
     */
    private static class MusicFileSorter implements Comparator<MusicFile> {

        public int compare(MusicFile a, MusicFile b) {
            if (a.isFile() && b.isDirectory()) {
                return 1;
            }

            if (a.isDirectory() && b.isFile()) {
                return -1;
            }

            if (a.isDirectory() && b.isDirectory()) {
                return a.getName().compareToIgnoreCase(b.getName());
            }

            Integer trackA = a.getMetaData() == null ? null : a.getMetaData().getTrackNumber();
            Integer trackB = b.getMetaData() == null ? null : b.getMetaData().getTrackNumber();

            if (trackA == null && trackB != null) {
                return 1;
            }

            if (trackA != null && trackB == null) {
                return -1;
            }

            if (trackA == null && trackB == null) {
                return a.getName().compareToIgnoreCase(b.getName());
            }

            // Compare by disc number, if present.
            Integer discA = a.getMetaData() == null ? null : a.getMetaData().getDiscNumber();
            Integer discB = b.getMetaData() == null ? null : b.getMetaData().getDiscNumber();
            if (discA != null && discB != null) {
                int i = discA.compareTo(discB);
                if (i != 0) {
                    return i;
                }
            }

            return trackA.compareTo(trackB);
        }
    }

    /**
     * Defines a visitor (as in the <em>Visitor</em> pattern), used to traverse a hierarchy of music files.
     */
    public static interface Visitor {

        /**
         * Visits the given music file.
         *
         * @param musicFile The music file to visist.
         */
        void visit(MusicFile musicFile);

        /**
         * Whether this visitor wants to visit directories.
         *
         * @return Whether this visitor wants to visit directories.
         */
        boolean includeDirectories();

        /**
         * Whether this visitor wants to visit files in ascending order (within a given directory).
         *
         * @return Whether this visitor wants to visit files in ascending order.
         */
        boolean sorted();
    }
}