/*
* Jajuk
* Copyright (C) The Jajuk Team
* http://jajuk.info
*
* This program 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 2
* of the License, or any later version.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
*/
package org.jajuk.ui.views;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JToolBar;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.basic.BasicComboBoxRenderer;
import net.miginfocom.swing.MigLayout;
import org.apache.commons.lang.StringUtils;
import org.jajuk.base.Album;
import org.jajuk.base.Artist;
import org.jajuk.base.Directory;
import org.jajuk.base.Track;
import org.jajuk.events.JajukEvent;
import org.jajuk.events.JajukEvents;
import org.jajuk.events.ObservationManager;
import org.jajuk.services.covers.Cover;
import org.jajuk.services.covers.Cover.CoverType;
import org.jajuk.services.players.QueueModel;
import org.jajuk.services.players.StackItem;
import org.jajuk.services.tags.Tag;
import org.jajuk.ui.helpers.JajukMouseAdapter;
import org.jajuk.ui.thumbnails.ThumbnailManager;
import org.jajuk.ui.widgets.InformationJPanel;
import org.jajuk.ui.widgets.JajukButton;
import org.jajuk.ui.widgets.JajukFileChooser;
import org.jajuk.ui.widgets.JajukJToolbar;
import org.jajuk.ui.windows.JajukMainWindow;
import org.jajuk.util.Conf;
import org.jajuk.util.Const;
import org.jajuk.util.DownloadManager;
import org.jajuk.util.IconLoader;
import org.jajuk.util.JajukFileFilter;
import org.jajuk.util.JajukIcons;
import org.jajuk.util.Messages;
import org.jajuk.util.UtilFeatures;
import org.jajuk.util.UtilGUI;
import org.jajuk.util.UtilSystem;
import org.jajuk.util.error.JajukException;
import org.jajuk.util.filters.GIFFilter;
import org.jajuk.util.filters.ImageFilter;
import org.jajuk.util.filters.JPGFilter;
import org.jajuk.util.filters.PNGFilter;
import org.jajuk.util.log.Log;
/**
* Cover view. Displays an image for the current album
*/
public class CoverView extends ViewAdapter implements ActionListener {
/** The Constant PLUS_QUOTE. */
private static final String PLUS_QUOTE = "+\"";
/** The Constant QUOTE_BLANK. */
private static final String QUOTE_BLANK = "\" ";
/** Generated serialVersionUID. */
private static final long serialVersionUID = 1L;
/** No cover cover. */
private static Cover nocover = new Cover(Const.IMAGES_SPLASHSCREEN, CoverType.NO_COVER);
/** Error counter to check connection availability. */
private volatile static int iErrorCounter = 0;
/** Connected one flag : true if jajuk managed once to connect to the web to bring covers. */
private volatile static boolean bOnceConnected = false;
/** Reference File for cover. */
private volatile org.jajuk.base.File fileReference;
/** File directory used as a cache for perfs. */
private volatile Directory dirReference;
/** List of available covers for the current file. */
private final LinkedList<Cover> alCovers = new LinkedList<Cover>();
// control panel
private JPanel jpControl;
private JajukButton jbPrevious;
private JajukButton jbNext;
private JajukButton jbDelete;
private JajukButton jbSave;
private JajukButton jbDefault;
private JLabel jlSize;
private JLabel jlFound;
/** Cover search accuracy combo. */
@SuppressWarnings("rawtypes")
private JComboBox jcbAccuracy;
/** Date last resize (used for adjustment management). */
private volatile long lDateLastResize;
/** URL and size of the image. */
private JLabel jl;
/** Used Cover index. */
private volatile int index = 0;
/** Flag telling that user wants to display a better cover. */
private boolean bGotoBetter = false;
/** Final image to display. */
private volatile ImageIcon ii;
/** Force next track cover reload flag*. */
private volatile boolean bForceCoverReload = true;
private boolean includeControls;
/** Whether the view has not yet been displayed for its first time */
private volatile boolean initEvent = true;
/** Used to lock access to covers collection, we don't synchronize this as this may create thread
* starvations because the EDT requires this lock as well in ViewAdater.stopAllBusyLabels() method. */
private final Lock listLock = new ReentrantLock(true);
/** Parent of this view. */
private JDialog parentJDialog;
/**
* Constructor.
*/
public CoverView() {
super();
}
/**
* Constructor.
*
* @param file Reference file. Used to display cover for a particular file, null if the cover view is used in the "reular" way as a view, not
* as a dialog from catalog view for ie.
*/
public CoverView(final org.jajuk.base.File file) {
super();
fileReference = file;
}
/**
* Constructor.
*
* @param file Reference file. Used to display cover for a particular file, null if the cover view is used in the "reular" way as a view, not
* as a dialog from catalog view for ie.
* @param jd parent dialog (closed after a default cover is selected)
*/
public CoverView(final org.jajuk.base.File file, JDialog jd) {
super();
fileReference = file;
parentJDialog = jd;
}
/*
* (non-Javadoc)
*
* @see org.jajuk.ui.views.IView#initUI()
*/
@Override
public void initUI() {
initUI(true);
}
/**
* Inits the ui.
*
* @param includeControls
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public void initUI(boolean includeControls) {
this.includeControls = includeControls;
// Control panel
jpControl = new JPanel();
if (includeControls) {
jpControl.setBorder(BorderFactory.createEtchedBorder());
}
final JToolBar jtb = new JajukJToolbar();
jbPrevious = new JajukButton(IconLoader.getIcon(JajukIcons.PLAYER_PREVIOUS_SMALL));
jbPrevious.addActionListener(this);
jbPrevious.setToolTipText(Messages.getString("CoverView.4"));
jbNext = new JajukButton(IconLoader.getIcon(JajukIcons.PLAYER_NEXT_SMALL));
jbNext.addActionListener(this);
jbNext.setToolTipText(Messages.getString("CoverView.5"));
jbDelete = new JajukButton(IconLoader.getIcon(JajukIcons.DELETE));
jbDelete.addActionListener(this);
jbDelete.setToolTipText(Messages.getString("CoverView.2"));
jbSave = new JajukButton(IconLoader.getIcon(JajukIcons.SAVE));
jbSave.addActionListener(this);
jbSave.setToolTipText(Messages.getString("CoverView.6"));
jbDefault = new JajukButton(IconLoader.getIcon(JajukIcons.DEFAULT_COVER));
jbDefault.addActionListener(this);
jbDefault.setToolTipText(Messages.getString("CoverView.8"));
jlSize = new JLabel("");
jlFound = new JLabel("");
jcbAccuracy = new JComboBox();
// Add tooltips on combo items
jcbAccuracy.setRenderer(new BasicComboBoxRenderer() {
private static final long serialVersionUID = -6943363556191659895L;
@Override
public Component getListCellRendererComponent(final JList list, final Object value,
final int index, final boolean isSelected, final boolean cellHasFocus) {
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
switch (index) {
case 0:
setToolTipText(Messages.getString("ParameterView.156"));
break;
case 1:
setToolTipText(Messages.getString("ParameterView.157"));
break;
case 2:
setToolTipText(Messages.getString("ParameterView.158"));
break;
case 3:
setToolTipText(Messages.getString("ParameterView.216"));
break;
case 4:
setToolTipText(Messages.getString("ParameterView.217"));
break;
case 5:
setToolTipText(Messages.getString("ParameterView.218"));
break;
}
setBorder(new EmptyBorder(0, 3, 0, 3));
return this;
}
});
jcbAccuracy.setToolTipText(Messages.getString("ParameterView.155"));
jcbAccuracy.addItem(IconLoader.getIcon(JajukIcons.ACCURACY_LOW));
jcbAccuracy.addItem(IconLoader.getIcon(JajukIcons.ACCURACY_MEDIUM));
jcbAccuracy.addItem(IconLoader.getIcon(JajukIcons.ACCURACY_HIGH));
jcbAccuracy.addItem(IconLoader.getIcon(JajukIcons.ARTIST));
jcbAccuracy.addItem(IconLoader.getIcon(JajukIcons.ALBUM));
jcbAccuracy.addItem(IconLoader.getIcon(JajukIcons.TRACK));
int accuracy = getCurrentAccuracy();
jcbAccuracy.setSelectedIndex(accuracy);
jcbAccuracy.addActionListener(this);
jtb.add(jbPrevious);
jtb.add(jbNext);
jtb.addSeparator();
jtb.add(jbDelete);
jtb.add(jbSave);
jtb.add(jbDefault);
if (includeControls) {
jpControl.setLayout(new MigLayout("insets 5 2 5 2", "[][grow][grow][]"));
jpControl.add(jtb);
jpControl.add(jlSize, "center,gapright 5::");
jpControl.add(jlFound, "center,gapright 5::");
jpControl.add(jcbAccuracy, "grow,width 47!,gapright 5");
}
// Cover view used in catalog view should not listen events
if (fileReference == null) {
ObservationManager.register(this);
}
// global layout
MigLayout globalLayout = null;
if (includeControls) {
globalLayout = new MigLayout("ins 0,gapy 10", "[grow]", "[30!][grow]");
} else {
globalLayout = new MigLayout("ins 0,gapy 10", "[grow]", "[grow]");
}
setLayout(globalLayout);
add(jpControl, "grow,wrap");
//Force initial resizing (required after a perspective reset as the component event is not thrown in this case)
componentResized(null);
// Attach the listener for initial cover display and further manual actions against the view when resizing.
addComponentListener(CoverView.this);
}
/*
* (non-Javadoc)
*
* @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
*/
@Override
public void actionPerformed(final ActionEvent e) {
if (e.getSource() == jcbAccuracy) {
handleAccuracy();
} else if (e.getSource() == jbPrevious) { // previous : show a
handlePrevious();
} else if (e.getSource() == jbNext) { // next : show a worse cover
handleNext();
} else if (e.getSource() == jbDelete) { // delete a local cover
handleDelete();
} else if (e.getSource() == jbDefault) {
handleDefault();
} else if ((e.getSource() == jbSave)
&& ((e.getModifiers() & ActionEvent.CTRL_MASK) == ActionEvent.CTRL_MASK)) {
// save a file as... (can be local now)
handleSaveAs();
} else if (e.getSource() == jbSave) {
handleSave();
}
}
/**
* Stores accuracy.
*/
private void handleAccuracy() {
// Note that we have to store/retrieve accuracy using an id. When
// this view is used from a popup, we can't use perspective id
Conf.setProperty(Const.CONF_COVERS_ACCURACY + "_"
+ ((getPerspective() == null) ? "popup" : getPerspective().getID()),
Integer.toString(jcbAccuracy.getSelectedIndex()));
new Thread("Cover Accuracy Thread") {
@Override
public void run() {
// force refresh
if (getPerspective() == null) {
dirReference = null;
}
update(new JajukEvent(JajukEvents.COVER_NEED_REFRESH));
}
}.start();
}
/**
* Called on the previous cover button event.
*/
private void handlePrevious() {
// better cover
bGotoBetter = true;
index++;
if (index > alCovers.size() - 1) {
index = 0;
}
displayCurrentCover();
bGotoBetter = false; // make sure default behavior is to go
// to worse covers
}
/**
* Called on the next cover button event.
*/
private void handleNext() {
bGotoBetter = false;
index--;
if (index < 0) {
index = alCovers.size() - 1;
}
displayCurrentCover();
}
/**
* Called on the delete cover button event.
*/
private void handleDelete() {
// sanity check
if (index >= alCovers.size()) {
Log.warn("Cannot delete cover that is not available.");
return;
}
if (index < 0) {
Log.warn("Cannot delete cover with invalid index.");
return;
}
// get the cover at the specified position
final Cover cover = alCovers.get(index);
// don't delete the splashscreen-jpg!!
if (cover.getType().equals(CoverType.NO_COVER)) {
Log.warn("Cannot delete default Jajuk cover.");
return;
}
// show confirmation message if required
if (Conf.getBoolean(Const.CONF_CONFIRMATIONS_DELETE_COVER)) {
final int iResu = Messages.getChoice(Messages.getString("Confirmation_delete_cover") + " : "
+ cover.getFile(), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE);
if (iResu != JOptionPane.YES_OPTION) {
return;
}
}
// yet there? ok, delete the cover
try {
final File file = cover.getFile();
if (file.isFile() && file.exists()) {
UtilSystem.deleteFile(file);
} else { // not a file, must have a problem
throw new Exception("Encountered file which either is not a file or does not exist: "
+ file);
}
} catch (final Exception ioe) {
Log.error(131, ioe);
Messages.showErrorMessage(131);
return;
}
// If this was the absolute cover, remove the reference in the
// collection
if (cover.getType() == CoverType.SELECTED_COVER) {
dirReference.removeProperty("default_cover");
}
// reorganize covers
try {
listLock.lock();
alCovers.remove(index);
index--;
if (index < 0) {
index = alCovers.size() - 1;
}
ObservationManager.notify(new JajukEvent(JajukEvents.COVER_NEED_REFRESH));
if (fileReference != null) {
update(new JajukEvent(JajukEvents.COVER_NEED_REFRESH));
}
} finally {
listLock.unlock();
}
}
/**
* Called when saving a cover.
*/
private void handleSave() {
// sanity check
if (index >= alCovers.size()) {
Log.warn("Cannot save cover that is not available.");
return;
}
if (index < 0) {
Log.warn("Cannot save cover with invalid index.");
return;
}
// save a file with its original name
new Thread("Cover Save Thread") {
@Override
public void run() {
final Cover cover = alCovers.get(index);
// should not happen, only remote covers here
if (cover.getType() != CoverType.REMOTE_COVER) {
Log.debug("Try to save a local cover");
return;
}
String sFilePath = null;
sFilePath = dirReference.getFio().getPath() + "/"
+ UtilSystem.getOnlyFile(cover.getURL().toString());
sFilePath = convertCoverPath(sFilePath);
try {
// copy file from cache
final File fSource = DownloadManager.downloadToCache(cover.getURL());
final File file = new File(sFilePath);
UtilSystem.copy(fSource, file);
InformationJPanel.getInstance().setMessage(Messages.getString("CoverView.11"),
InformationJPanel.MessageType.INFORMATIVE);
final Cover cover2 = new Cover(file, CoverType.SELECTED_COVER);
if (!alCovers.contains(cover2)) {
alCovers.add(cover2);
setFoundText();
}
// Reset cached cover in associated albums to make sure that new covers
// will be discovered in various views like Catalog View.
resetCachedCover();
// Notify cover change
ObservationManager.notify(new JajukEvent(JajukEvents.COVER_NEED_REFRESH));
// add new cover in others cover views
} catch (final Exception ex) {
Log.error(24, ex);
Messages.showErrorMessage(24);
}
}
}.start();
}
/**
* Reset cached cover in associated albums to make sure that new covers
* will be discovered in various views like Catalog View.
*/
private void resetCachedCover() {
org.jajuk.base.File fCurrent = fileReference;
if (fCurrent == null) {
fCurrent = QueueModel.getPlayingFile();
}
Set<Album> albums = fCurrent.getDirectory().getAlbums();
// If we cached NO_COVER for this album, make sure to reset this value
for (Album album : albums) {
String cachedCoverPath = album.getStringValue(XML_ALBUM_DISCOVERED_COVER);
if (COVER_NONE.equals(cachedCoverPath)) {
album.setProperty(XML_ALBUM_DISCOVERED_COVER, "");
}
ObservationManager.notify(new JajukEvent(JajukEvents.COVER_DEFAULT_CHANGED));
}
}
/**
* Converts a cover path according to options and jajuk conventions.
*
* @param sFilePath current cover path
* @return the converted cover file path
*/
private String convertCoverPath(String sFilePath) {
int pos = sFilePath.lastIndexOf('.');
if (Conf.getBoolean(Const.CONF_COVERS_SAVE_EXPLORER_FRIENDLY)) {
// Covers should be stored as folder.xxx for windows explorer
final String ext;
if (pos == -1) {
ext = "";
} else {
ext = sFilePath.substring(pos, sFilePath.length());
}
String parent = new File(sFilePath).getParent();
return parent + System.getProperty("file.separator") + "Folder" + ext;
} else {
if (pos == -1) {
return sFilePath + Const.FILE_JAJUK_DOWNLOADED_FILES_SUFFIX;
}
// Add a jajuk suffix to know this cover has been downloaded by jajuk
return new StringBuilder(sFilePath).insert(pos, Const.FILE_JAJUK_DOWNLOADED_FILES_SUFFIX)
.toString();
}
}
/**
* Called when saving as a cover.
*/
private void handleSaveAs() {
// sanity check
if (index >= alCovers.size()) {
Log.warn("Cannot save cover that is not available.");
return;
}
if (index < 0) {
Log.warn("Cannot save cover with invalid index.");
return;
}
new Thread("Cover SaveAs Thread") {
@Override
public void run() {
final Cover cover = alCovers.get(index);
final JajukFileChooser jfchooser = new JajukFileChooser(new JajukFileFilter(
GIFFilter.getInstance(), PNGFilter.getInstance(), JPGFilter.getInstance()));
jfchooser.setAcceptDirectories(true);
jfchooser.setCurrentDirectory(dirReference.getFio());
jfchooser.setDialogTitle(Messages.getString("CoverView.10"));
final File finalFile = new File(dirReference.getFio().getPath() + "/"
+ UtilSystem.getOnlyFile(cover.getURL().toString()));
jfchooser.setSelectedFile(finalFile);
final int returnVal = jfchooser.showSaveDialog(JajukMainWindow.getInstance());
File fNew = null;
if (returnVal == JFileChooser.APPROVE_OPTION) {
fNew = jfchooser.getSelectedFile();
// if user try to save as without changing file name
if (fNew.getAbsolutePath().equals(cover.getFile().getAbsolutePath())) {
return;
}
try {
UtilSystem.copy(cover.getFile(), fNew);
InformationJPanel.getInstance().setMessage(Messages.getString("CoverView.11"),
InformationJPanel.MessageType.INFORMATIVE);
// Reset cached cover in associated albums to make sure that new covers
// will be discovered in various views like Catalog View.
resetCachedCover();
// Notify cover change
ObservationManager.notify(new JajukEvent(JajukEvents.COVER_NEED_REFRESH));
} catch (final Exception ex) {
Log.error(24, ex);
Messages.showErrorMessage(24);
}
}
}
}.start();
}
/**
* Called when making a cover default.
*/
private void handleDefault() {
// sanity check
if (index >= alCovers.size()) {
Log.warn("Cannot default cover which is not available.");
return;
}
if (index < 0) {
Log.warn("Cannot default cover with invalid index.");
return;
}
new Thread("Default cover thread") {
@Override
public void run() {
Cover cover = alCovers.get(index);
org.jajuk.base.File fCurrent = fileReference;
// Path of the default cover, it is simply the URL of the current cover for local covers
// but it is another path to a newly created image for tag or remote covers
String destPath = cover.getFile().getAbsolutePath();
if (fCurrent == null) {
fCurrent = QueueModel.getPlayingFile();
}
if (cover.getType() == CoverType.TAG_COVER) {
destPath = dirReference.getFio().getPath() + "/" + cover.getFile().getName();
destPath = convertCoverPath(destPath);
File destFile = new File(destPath);
try {
// Copy cached file to music directory
// Note that the refreshCover() methods automatically
// extract any track cover tag to an image file in the cache
UtilSystem.copy(cover.getFile(), destFile);
Cover cover2 = new Cover(destFile, CoverType.SELECTED_COVER);
alCovers.add(cover2);
} catch (Exception ex) {
Log.error(24, ex);
Messages.showErrorMessage(24);
return;
}
} else if (cover.getType() == CoverType.REMOTE_COVER) {
String sFilename = UtilSystem.getOnlyFile(cover.getURL().toString());
destPath = dirReference.getFio().getPath() + "/" + sFilename;
destPath = convertCoverPath(destPath);
try {
// Download cover and copy file from cache to music directory
File fSource = DownloadManager.downloadToCache(cover.getURL());
File fileDest = new File(destPath);
UtilSystem.copy(fSource, new File(destPath));
Cover cover2 = new Cover(fileDest, CoverType.SELECTED_COVER);
if (!alCovers.contains(cover2)) {
alCovers.add(cover2);
setFoundText();
}
} catch (Exception ex) {
Log.error(24, ex);
Messages.showErrorMessage(24);
return;
}
}
// Remove previous thumbs to avoid using outdated images
// Reset cached cover
ThumbnailManager.cleanThumbs(fCurrent.getTrack().getAlbum());
refreshThumbs(cover);
InformationJPanel.getInstance().setMessage(Messages.getString("Success"),
InformationJPanel.MessageType.INFORMATIVE);
// For every kind of cover types :
ObservationManager.notify(new JajukEvent(JajukEvents.COVER_DEFAULT_CHANGED));
ObservationManager.notify(new JajukEvent(JajukEvents.COVER_NEED_REFRESH));
// then make it the default cover for this album
if (fCurrent != null && fCurrent.getTrack() != null
&& fCurrent.getTrack().getAlbum() != null && cover.getFile() != null) {
Album album = fCurrent.getTrack().getAlbum();
album.setProperty(XML_ALBUM_SELECTED_COVER, destPath);
album.setProperty(XML_ALBUM_DISCOVERED_COVER, destPath);
}
// Close the current dialog
if (parentJDialog!= null) {
parentJDialog.dispose();
}
}
}.start();
}
/*
* (non-Javadoc)
*
* @see java.awt.event.ComponentListener#componentResized(java.awt.event.ComponentEvent )
*/
@Override
public void componentResized(final ComponentEvent e) {
Dimension dim = getSize();
if (dim.getHeight() <= 0 || dim.getWidth() <= 0) {
return;
}
final long lCurrentDate = System.currentTimeMillis(); // adjusting code
if (lCurrentDate - lDateLastResize < 500) { // Do consider only one event every
// 500 ms to avoid race conditions and lead to unexpected states (verified)
return;
}
lDateLastResize = lCurrentDate;
Log.debug("Cover resized, view=" + getID() + " size=" + getSize());
// Run this in another thread to accelerate the component resize events processing and filter by time
new Thread() {
@Override
public void run() {
if (fileReference == null) { // regular cover view
if (QueueModel.isStopped()) {
update(new JajukEvent(JajukEvents.ZERO));
}
// check if a track has already been launched
else if (QueueModel.isPlayingRadio()) {
update(new JajukEvent(JajukEvents.WEBRADIO_LAUNCHED,
ObservationManager.getDetailsLastOccurence(JajukEvents.WEBRADIO_LAUNCHED)));
// If the view is displayed for the first time, a ComponentResized event is launched at its first display but
// we want to perform the full process : update past launches files (FILE_LAUNCHED).
// But if it is no more the initial resize event, we only want to refresh the cover, not the full story.
} else if (!initEvent) {
displayCurrentCover();
} else {
update(new JajukEvent(JajukEvents.FILE_LAUNCHED));
}
} else { // cover view used as dialog
update(new JajukEvent(JajukEvents.COVER_NEED_REFRESH));
}
// It will never more be the first time ...
CoverView.this.initEvent = false;
}
}.start();
}
/**
* Creates the query.
*
* @param file
*
* @return an accurate google search query for a file
*/
public String createQuery(final org.jajuk.base.File file) {
String query = "";
int iAccuracy = getCurrentAccuracy();
final Track track = file.getTrack();
final Artist artist = track.getArtist();
final Album album = track.getAlbum();
switch (iAccuracy) {
case 0: // low, default
if (!artist.seemsUnknown()) {
query += artist.getName() + " ";
} else if (!track.getAlbumArtist().seemsUnknown()) {
query += track.getAlbumArtist().getName() + " ";
}
if (!album.seemsUnknown()) {
query += album.getName() + " ";
}
break;
case 1: // medium
if (!artist.seemsUnknown()) {
query += '\"' + artist.getName() + QUOTE_BLANK;
} else if (!track.getAlbumArtist().seemsUnknown()) {
query += '\"' + track.getAlbumArtist().getName() + QUOTE_BLANK;
}
if (!album.seemsUnknown()) {
query += '\"' + album.getName() + QUOTE_BLANK;
}
break;
case 2: // high
if (!artist.seemsUnknown()) {
query += PLUS_QUOTE + artist.getName() + QUOTE_BLANK;
} else if (!track.getAlbumArtist().seemsUnknown()) {
query += PLUS_QUOTE + track.getAlbumArtist().getName() + QUOTE_BLANK;
}
if (!album.seemsUnknown()) {
query += PLUS_QUOTE + album.getName() + QUOTE_BLANK;
}
break;
case 3: // by artist
if (!artist.seemsUnknown()) {
query += artist.getName() + " ";
} else if (!track.getAlbumArtist().seemsUnknown()) {
query += track.getAlbumArtist().getName() + " ";
}
break;
case 4: // by album
if (!album.seemsUnknown()) {
query += album.getName() + " ";
}
break;
case 5: // by track name
query += track.getName();
break;
default:
break;
}
return query;
}
private int getCurrentAccuracy() {
// Default = medium
int iAccuracy = 1;
try {
iAccuracy = Conf.getInt(Const.CONF_COVERS_ACCURACY + "_"
+ ((getPerspective() == null) ? "popup" : getPerspective().getID()));
} catch (final NumberFormatException e) {
// can append if accuracy never set
Log.debug("Unknown accuracy");
}
return iAccuracy;
}
/**
* Display current cover (at this.index), try all covers in case of error
*/
private void displayCurrentCover() {
UtilGUI.showBusyLabel(CoverView.this); // lookup icon
findRightCover();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
displayCover(index);
}
});
}
private void findRightCover() {
try {
listLock.lock();
// Avoid looping
if (alCovers.size() == 0) {
// should not append
alCovers.add(CoverView.nocover);
// Add at last the default cover if all remote cover has
// been discarded
try {
prepareDisplay(0);
} catch (JajukException e) {
Log.error(e);
}
return;
}
if (alCovers.size() == 1 && (alCovers.get(0)).getType() == CoverType.NO_COVER) {
// only a default cover
try {
prepareDisplay(0);
} catch (JajukException e) {
Log.error(e);
}
return;
}
// else, there is at least one local cover and no
// default cover
while (alCovers.size() > 0) {
try {
prepareDisplay(index);
return; // OK, leave
} catch (Exception e) {
Log.debug("Removed cover: {{" + alCovers.get(index) + "}}");
alCovers.remove(index);
// refresh number of found covers
if (!bGotoBetter) {
// we go to worse covers. If we go to better
// covers, we just
// keep the same index try a worse cover...
if (index - 1 >= 0) {
index--;
} else { // no more worse cover
index = alCovers.size() - 1;
// come back to best cover
}
}
}
}
// if this code is executed, it means than no available
// cover was found, then display default cover
alCovers.add(CoverView.nocover); // Add at last the default cover
// if all remote cover has been discarded
try {
index = 0;
prepareDisplay(index);
} catch (JajukException e) {
Log.error(e);
}
} finally {
listLock.unlock();
}
}
/**
* Gets the cover number.
*
* @return number of real covers (not default) covers found
*/
private int getCoverNumber() {
return alCovers.size();
}
/*
* (non-Javadoc)
*
* @see org.jajuk.ui.IView#getDesc()
*/
@Override
public String getDesc() {
return Messages.getString("CoverView.3");
}
/*
* (non-Javadoc)
*
* @see org.jajuk.events.Observer#getRegistrationKeys()
*/
@Override
public Set<JajukEvents> getRegistrationKeys() {
final Set<JajukEvents> eventSubjectSet = new HashSet<JajukEvents>();
eventSubjectSet.add(JajukEvents.FILE_LAUNCHED);
eventSubjectSet.add(JajukEvents.WEBRADIO_LAUNCHED);
eventSubjectSet.add(JajukEvents.ZERO);
eventSubjectSet.add(JajukEvents.PLAYER_STOP);
eventSubjectSet.add(JajukEvents.COVER_NEED_REFRESH);
return eventSubjectSet;
}
/**
* Long action to compute image to display (download, resizing...)
*
* @param index
*
* @throws JajukException the jajuk exception
*/
private void prepareDisplay(final int index) throws JajukException {
// find next correct cover
Cover cover = null;
ImageIcon icon = null;
try {
cover = alCovers.get(index); // take image at the given index
Log.debug("Display cover: " + cover + " at index :" + index);
Image img = cover.getImage();
// Never mirror our no cover image
if (cover.getType().equals(CoverType.NO_COVER)) {
icon = new ImageIcon(img);
} else {
if (
// should we mirror in our GUI
(includeControls && Conf.getBoolean(Const.CONF_COVERS_MIRROW_COVER))
// should we mirror in fullscreen mode
|| (!includeControls && Conf.getBoolean(Const.CONF_COVERS_MIRROW_COVER_FS_MODE))) {
icon = new ImageIcon(UtilGUI.get3dImage(img));
} else {
icon = new ImageIcon(img);
}
}
if (icon.getIconHeight() == 0 || icon.getIconWidth() == 0) {
throw new JajukException(0, "Wrong picture, size is null");
}
} catch (final FileNotFoundException e) {
// do not display a stacktrace for FileNotfound as we expect this in cases
// where the picture is gone on the net
Log.warn("Cover image not found at URL: "
+ (cover == null ? "<null>" : cover.getURL().toString()));
throw new JajukException(0, e);
} catch (final UnknownHostException e) {
// do not display a stacktrace for HostNotFound as we expect this in cases
// where the whole server is gone on the net
Log.warn("Cover image not found at URL: "
+ (cover == null ? "<null>" : cover.getURL().toString()));
throw new JajukException(0, e);
} catch (final IOException e) { // this cover cannot be loaded
Log.error(e);
throw new JajukException(0, e);
} catch (final InterruptedException e) { // this cover cannot be loaded
Log.error(e);
throw new JajukException(0, e);
}
// We apply a 90% of space availability to avoid image cut-offs (see #1283)
final int iDisplayAreaHeight = (int) (0.9f * CoverView.this.getHeight() - 30);
final int iDisplayAreaWidth = (int) (0.9f * CoverView.this.getWidth() - 10);
// check minimum sizes
if ((iDisplayAreaHeight < 1) || (iDisplayAreaWidth < 1)) {
return;
}
int iNewWidth;
int iNewHeight;
if (iDisplayAreaHeight > iDisplayAreaWidth) {
// Width is smaller than height : try to optimize height
iNewHeight = iDisplayAreaHeight; // take all possible height
// we check now if width will be visible entirely with optimized
// height
final float fHeightRatio = (float) iNewHeight / icon.getIconHeight();
if (icon.getIconWidth() * fHeightRatio <= iDisplayAreaWidth) {
iNewWidth = (int) (icon.getIconWidth() * fHeightRatio);
} else {
// no? so we optimize width
iNewWidth = iDisplayAreaWidth;
iNewHeight = (int) (icon.getIconHeight() * ((float) iNewWidth / icon.getIconWidth()));
}
} else {
// Height is smaller or equal than width : try to optimize width
iNewWidth = iDisplayAreaWidth; // take all possible width
// we check now if height will be visible entirely with
// optimized width
final float fWidthRatio = (float) iNewWidth / icon.getIconWidth();
if (icon.getIconHeight() * fWidthRatio <= iDisplayAreaHeight) {
iNewHeight = (int) (icon.getIconHeight() * fWidthRatio);
} else {
// no? so we optimize width
iNewHeight = iDisplayAreaHeight;
iNewWidth = (int) (icon.getIconWidth() * ((float) iNewHeight / icon.getIconHeight()));
}
}
// Note that at this point, the image is fully loaded (done in the ImageIcon constructor)
ii = UtilGUI.getResizedImage(icon, iNewWidth, iNewHeight);
// Free memory of source image, removing this causes severe memory leaks ! (tested)
icon.getImage().flush();
}
/**
* Display given cover.
*
* @param index index of the cover to display
*/
private void displayCover(final int index) {
if ((alCovers.size() == 0) || (index >= alCovers.size()) || (index < 0)) {
// just a check
alCovers.add(CoverView.nocover); // display nocover by default
displayCover(0);
return;
}
final Cover cover = alCovers.get(index); // take image at the given index
final URL url = cover.getURL();
// enable delete button only for local covers
jbDelete.setEnabled(cover.getType() == CoverType.LOCAL_COVER
|| cover.getType() == CoverType.SELECTED_COVER
|| cover.getType() == CoverType.STANDARD_COVER);
//Disable default command for "none" cover
jbDefault.setEnabled(cover.getType() != CoverType.NO_COVER);
if (url != null) {
jbSave.setEnabled(false);
String sType = " (L)"; // local cover
if (cover.getType() == CoverType.REMOTE_COVER) {
sType = "(@)"; // Web cover
jbSave.setEnabled(true);
} else if (cover.getType() == CoverType.TAG_COVER) {
sType = "(T)"; // Tag cover
}
final String size = cover.getSize();
jl = new JLabel(ii);
jl.setMinimumSize(new Dimension(0, 0)); // required for info
// node resizing
if (cover.getType() == CoverType.TAG_COVER) {
jl.setToolTipText("<html>Tag<br>" + size + "K");
} else {
jl.setToolTipText("<html>" + url.toString() + "<br>" + size + "K");
}
setSizeText(size + "K" + sType);
setFoundText();
}
// set tooltip for previous and next track
try {
int indexPrevious = index + 1;
if (indexPrevious > alCovers.size() - 1) {
indexPrevious = 0;
}
final URL urlPrevious = alCovers.get(indexPrevious).getURL();
if (urlPrevious != null) {
jbPrevious.setToolTipText("<html>" + Messages.getString("CoverView.4") + "<br>"
+ urlPrevious.toString() + "</html>");
}
int indexNext = index - 1;
if (indexNext < 0) {
indexNext = alCovers.size() - 1;
}
final URL urlNext = alCovers.get(indexNext).getURL();
if (urlNext != null) {
jbNext.setToolTipText("<html>" + Messages.getString("CoverView.5") + "<br>"
+ urlNext.toString() + "</html>");
}
} catch (final Exception e) { // the url code can throw out of bounds
// exception for unknown reasons so check it
Log.debug("jl=" + jl + " url={{" + url + "}}");
Log.error(e);
}
if (getComponentCount() > 0) {
removeAll();
}
if (includeControls) {
add(jpControl, "grow,wrap");
}
// Invert the mirrow option when clicking on the cover
jl.addMouseListener(new JajukMouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (!(cover.getType().equals(CoverType.NO_COVER))) {
boolean isMirrowed = includeControls ? Conf.getBoolean(Const.CONF_COVERS_MIRROW_COVER)
: Conf.getBoolean(Const.CONF_COVERS_MIRROW_COVER_FS_MODE);
// Normal cover view
if (includeControls) {
Conf.setProperty(Const.CONF_COVERS_MIRROW_COVER, !isMirrowed + "");
} else {
// Full screen mode
Conf.setProperty(Const.CONF_COVERS_MIRROW_COVER_FS_MODE, !isMirrowed + "");
}
ObservationManager.notify(new JajukEvent(JajukEvents.COVER_NEED_REFRESH));
ObservationManager.notify(new JajukEvent(JajukEvents.PARAMETERS_CHANGE));
}
}
});
add(jl, "center,wrap");
// make sure the image is repainted to avoid overlapping covers
CoverView.this.revalidate();
CoverView.this.repaint();
}
/**
* Refresh default cover thumb (used in catalog view).
*
* @param cover
*/
private void refreshThumbs(final Cover cover) {
if (dirReference == null) {
Log.warn("Cannot refresh thumbnails without reference directory");
return;
}
// refresh thumbs
try {
for (int size = 50; size <= 300; size += 50) {
final Album album = dirReference.getFiles().iterator().next().getTrack().getAlbum();
final File fThumb = ThumbnailManager.getThumbBySize(album, size);
ThumbnailManager.createThumbnail(cover.getFile(), fThumb, size);
}
} catch (final Exception ex) {
Log.error(24, ex);
}
}
/**
* Set the cover Found text.
*/
private void setFoundText() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
// make sure not to display negative indexes
int i = getCoverNumber() - index;
if (i < 0) {
Log.debug("Negative cover index: " + i);
i = 0;
}
jlFound.setText(i + "/" + getCoverNumber());
}
});
}
/**
* Set the cover Found text.
*
* @param sFound specified text
*/
private void setFoundText(final String sFound) {
if (sFound != null) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
jlFound.setText(sFound);
}
});
}
}
/**
* Set the cover size text.
*
* @param sSize
*/
private void setSizeText(final String sSize) {
if (sSize != null) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
jlSize.setText(sSize);
}
});
}
}
/**
* Gets the current image.
*
* @return the current image
*
* @throws IOException Signals that an I/O exception has occurred.
* @throws InterruptedException the interrupted exception
* @throws JajukException the jajuk exception
*/
public Image getCurrentImage() throws IOException, InterruptedException, JajukException {
if (alCovers.size() > 0) {
return alCovers.get(0).getImage();
}
return CoverView.nocover.getImage();
}
/*
* (non-Javadoc)
*
* @see org.jajuk.ui.Observer#update(java.lang.String)
*/
@Override
public void update(final JajukEvent event) {
final JajukEvents subject = event.getSubject();
try {
listLock.lock();
// When receiving this event, check if we should change the cover or
// not (we don't change cover if playing another track of the same album
// except if option shuffle cover is set)
if (JajukEvents.FILE_LAUNCHED.equals(subject)) {
updateFileLaunched(event);
} else if (JajukEvents.ZERO.equals(subject) || JajukEvents.WEBRADIO_LAUNCHED.equals(subject)
|| JajukEvents.PLAYER_STOP.equals(subject)) {
reset();
} else if (JajukEvents.COVER_NEED_REFRESH.equals(subject) && !QueueModel.isPlayingRadio()) {
refreshCovers(true);
displayCurrentCover();
}
} catch (final IOException e) {
Log.error(e);
} finally {
listLock.unlock();
}
}
/**
* Update stop or web radio launched.
*
*/
private void reset() {
// Ignore this event if a reference file has been set
if (fileReference != null) {
return;
}
setFoundText("");
setSizeText("");
alCovers.clear();
alCovers.add(CoverView.nocover); // add the default cover
index = 0;
displayCurrentCover();
dirReference = null;
// Force cover to reload at next track
bForceCoverReload = true;
// disable commands
enableCommands(false);
}
/**
* Update file launched.
*
* @param event
*
* @throws IOException Signals that an I/O exception has occurred.
*/
private void updateFileLaunched(final JajukEvent event) throws IOException {
org.jajuk.base.File last = null;
Properties details = event.getDetails();
if (details != null) {
StackItem item = (StackItem) details.get(Const.DETAIL_OLD);
if (item != null) {
last = item.getFile();
}
}
// Ignore this event if a reference file has been set and if
// this event has already been handled
if ((fileReference != null) && (dirReference != null)) {
return;
}
// if we are always in the same directory, just leave to
// save cpu
boolean dirChanged = last == null ? true : !last.getDirectory().equals(
QueueModel.getPlayingFile().getDirectory());
if (bForceCoverReload) {
dirChanged = true;
}
refreshCovers(dirChanged);
if (Conf.getBoolean(Const.CONF_COVERS_SHUFFLE)) {
// Ignore this event if a reference file has been set
if (fileReference != null) {
return;
}
// choose a random cover
index = (int) (Math.random() * alCovers.size() - 1);
}
displayCurrentCover();
enableCommands(true);
}
/**
* Convenient method to massively enable/disable this view buttons.
*
* @param enable
*/
private void enableCommands(final boolean enable) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
jcbAccuracy.setEnabled(enable);
jbDefault.setEnabled(enable);
jbDelete.setEnabled(enable);
jbNext.setEnabled(enable);
jbPrevious.setEnabled(enable);
jbSave.setEnabled(enable);
jlFound.setVisible(enable);
jlSize.setVisible(enable);
}
});
}
/**
* Covers refreshing effective code
* <p>
* Must be called outside the EDT, contains network access
* </p>.
*
* @param dirChanged
*
* @throws IOException Signals that an I/O exception has occurred.
*/
private void refreshCovers(boolean dirChanged) throws IOException {
// Reset this flag
bForceCoverReload = false;
org.jajuk.base.File fCurrent = fileReference;
// check if a file has been given for this cover view
// if not, take current cover
if (fCurrent == null) {
fCurrent = QueueModel.getPlayingFile();
}
// no current cover and nothing playing
if (fCurrent == null) {
dirReference = null;
} else {
// store this dir
dirReference = fCurrent.getDirectory();
}
if (dirReference == null) {
alCovers.clear();
alCovers.add(CoverView.nocover);
index = 0;
return;
}
if (fCurrent == null) {
throw new IllegalArgumentException("Internal Error: Unexpected value, "
+ "variable fCurrent should not be empty. dirReference: " + dirReference);
}
// We only need to refresh the other covers if the directory changed
// but we still clear tag-based covers even if directory didn't change
// so the song-specific tag is taken into account.
Iterator<Cover> it = alCovers.iterator();
while (it.hasNext()) {
Cover cover = it.next();
if (cover.getType() == CoverType.TAG_COVER) {
it.remove();
}
}
if (dirChanged) {
// remove all existing covers
alCovers.clear();
// Search for local covers in all directories mapping
// the current track to reach other devices covers and
// display them together
final Track trackCurrent = fCurrent.getTrack();
final List<org.jajuk.base.File> alFiles = trackCurrent.getFiles();
// Add any selected default cover
String defaultCoverPath = trackCurrent.getAlbum().getStringValue(XML_ALBUM_SELECTED_COVER);
if (StringUtils.isNotBlank(defaultCoverPath)) {
File coverFile = new File(defaultCoverPath);
if (coverFile.exists()) {
final Cover cover = new Cover(coverFile, CoverType.SELECTED_COVER);
// Avoid dups
if (!alCovers.contains(cover)) {
alCovers.add(cover);
}
}
}
// list of files mapping the track
for (final org.jajuk.base.File file : alFiles) {
final Directory dirScanned = file.getDirectory();
if (!dirScanned.getDevice().isMounted()) {
// if the device is not ready, just ignore it
continue;
}
// Now search for regular or standard local covers
// null if none file found
final java.io.File[] files = dirScanned.getFio().listFiles();
for (int i = 0; (files != null) && (i < files.length); i++) {
// check size to avoid out of memory errors
if (files[i].length() > Const.MAX_COVER_SIZE * 1024) {
continue;
}
final JajukFileFilter filter = ImageFilter.getInstance();
if (filter.accept(files[i])) {
Cover cover = null;
if (UtilFeatures.isStandardCover(files[i])) {
cover = new Cover(files[i], CoverType.STANDARD_COVER);
} else {
cover = new Cover(files[i], CoverType.LOCAL_COVER);
}
if (!alCovers.contains(cover)) {
alCovers.add(cover);
}
}
}
}
// Then we search for web covers online if max
// connection errors number is not reached or if user
// already managed to connect.
// We also drop the query if user required none internet access
if (Conf.getBoolean(Const.CONF_COVERS_AUTO_COVER)
&& !Conf.getBoolean(Const.CONF_NETWORK_NONE_INTERNET_ACCESS)
&& (CoverView.bOnceConnected || (CoverView.iErrorCounter < Const.STOP_TO_SEARCH))) {
try {
final String sQuery = createQuery(fCurrent);
Log.debug("Query={{" + sQuery + "}}");
if (!sQuery.isEmpty()) {
// there is not enough information in tags
// for a web search
List<URL> alUrls;
alUrls = DownloadManager.getRemoteCoversList(sQuery);
CoverView.bOnceConnected = true;
// user managed once to connect to the web
if (alUrls.size() > Const.MAX_REMOTE_COVERS) {
// limit number of remote covers
alUrls = new ArrayList<URL>(alUrls.subList(0, Const.MAX_REMOTE_COVERS));
}
Collections.reverse(alUrls);
// set best results to be displayed first
final Iterator<URL> it2 = alUrls.iterator();
// add found covers
while (it2.hasNext()) {
// load each cover (pre-load or post-load)
// and stop if a signal has been emitted
final URL url = it2.next();
final Cover cover = new Cover(url, CoverType.REMOTE_COVER);
// Create a cover with given url ( image
// will be really downloaded when
// required if no preload)
if (!alCovers.contains(cover)) {
Log.debug("Found Cover: {{" + url.toString() + "}}");
alCovers.add(cover);
}
}
}
} catch (final IOException e) {
Log.warn(e.getMessage());
// can occur in case of timeout or error during
// covers list download
CoverView.iErrorCounter++;
if (CoverView.iErrorCounter == Const.STOP_TO_SEARCH) {
Log.warn("Too many connection fails," + " stop to search for covers online");
InformationJPanel.getInstance().setMessage(Messages.getString("Error.030"),
InformationJPanel.MessageType.WARNING);
}
} catch (final Exception e) {
Log.error(e);
}
}
}
// Check for tag covers
try {
Tag tag = new Tag(fCurrent.getFIO(), false);
List<Cover> tagCovers = tag.getCovers();
// Reverse order of the found tag covers because we want best last
// in alCovers and we want to keep tag order.
Collections.reverse(tagCovers);
for (Cover cover : tagCovers) {
// Avoid dups
if (!alCovers.contains(cover)) {
alCovers.add(cover);
}
}
} catch (JajukException e1) {
Log.error(e1);
}
if (alCovers.size() == 0) {// add the default cover if none
// other cover has been found
alCovers.add(CoverView.nocover);
}
Collections.sort(alCovers);
Log.debug("Local cover list: {{" + alCovers + "}}");
if (Conf.getBoolean(Const.CONF_COVERS_SHUFFLE)) {
// choose a random cover
index = (int) (Math.random() * alCovers.size());
} else {
index = alCovers.size() - 1;
// current index points to the best available cover
}
}
}