View Javadoc

1   package org.musicontroller.gui.importer;
2   
3   import java.util.Collection;
4   import java.util.Date;
5   import java.util.HashSet;
6   import java.util.List;
7   import java.util.Set;
8   
9   import org.apache.hivemind.util.PropertyUtils;
10  import org.apache.tapestry.IExternalPage;
11  import org.apache.tapestry.IPage;
12  import org.apache.tapestry.IRequestCycle;
13  import org.apache.tapestry.annotations.EventListener;
14  import org.apache.tapestry.annotations.InjectObject;
15  import org.apache.tapestry.engine.ExternalServiceParameter;
16  import org.apache.tapestry.engine.IEngineService;
17  import org.apache.tapestry.engine.ILink;
18  import org.apache.tapestry.event.BrowserEvent;
19  import org.apache.tapestry.event.PageBeginRenderListener;
20  import org.apache.tapestry.event.PageEvent;
21  import org.apache.tapestry.html.BasePage;
22  import org.apache.tapestry.services.ServiceMap;
23  import org.musicontroller.core.Playlist;
24  import org.musicontroller.importer.Importer;
25  import org.musicontroller.importer.ImporterException;
26  import org.musicontroller.importer.MusicArchiveBean;
27  import org.musicontroller.importer.MusicArchiveEntryBean;
28  import org.musicontroller.importer.PlaylistImportProperties;
29  import org.musicontroller.service.McService;
30  import org.varienaja.util.coverart.CoverArtSearchResult;
31  
32  /**
33   * Screen for showing music archive contents, editing properties and 
34   * keywords and importing the archive contents into the MusiController database.
35   * <p>The functions of this screen:
36   * <ul>
37   * <li> Set the playlist name, artist, keywords and release date on any song in the music archive.
38   * <li> Select or deselect entries in the music archive for import.
39   * <li> Propose a cover art image; Let the user select a different cover art image.
40   * <li> Import the selected entries of the music archive into the music controller.
41   * </ul>
42   * <p>Page properties:
43   * <ul>
44   * <li>The music archive contains information about the MP3 files in an imported ZIP. It is passed to
45   *    this page as the <code>archive</code> property.
46   * <li>The <code>importer</code> property holds an object capable of importing music into the musicontroller.
47   *     This object implements the <code>importer</code> interface.
48   * <li>The <code>feedback</code> property provides a mechanism for giving messages to the user.
49   * <li>
50   * </ul>
51   * 
52   * <p>The page attempts to find a suitable cover image:
53   * <ol>
54   * <li> If the music archive contains a cover art URL, that URL is used.
55   * <li> If a persistent playlist with the same name and artist exist, the cover art of that playlist is used.
56   * <li> A cover art search is performed, using search criteria derived from the information in the music archive.
57   *      The first result of that search is suggested as the cover art.
58   * </ol>
59   * 
60   * @see MusicArchiveBean
61   * 
62   * @author Hans Drexler
63   * @revision $Id: MusicArchive.java,v 1.1 2010/03/16 18:55:43 varienaja Exp $
64   */
65  public abstract class MusicArchive extends BasePage implements IExternalPage, PageBeginRenderListener {
66  
67  	@InjectObject("engine-service:page") public abstract IEngineService getPageService(); 
68  	@InjectObject("engine-service:external") public abstract IEngineService getExternalService();
69  	
70  	public abstract ServiceMap getServiceMap();
71  
72  	public abstract McService getMcService();
73  	
74  	/**
75  	 * This property holds the bean with information about the
76  	 * songs in the archive.
77  	 */
78  	public abstract MusicArchiveBean getArchive();
79  
80  	/**
81  	 * Sets a message for the user. The message will be shown in bold face.
82  	 * @param message The message to show.
83  	 */
84  	public abstract void setFeedback(String message);
85  	
86  	/**
87  	 * The importer object knows how to import music into the
88  	 * database.
89  	 * @return The importer object.
90  	 */
91  	public abstract Importer getImporter();
92  
93  	/**
94  	 * Getter for the property that iterates over the playlist entries in the archive.
95  	 * @return The value of the <tt>playlistEntry</tt> property.
96  	 */
97  	public abstract PlaylistImportProperties getPlaylistEntry();
98  	
99  	/**
100 	 * Interpret parameters from other pages. This pages uses the <code>CoverArtEdit</code> page to select a cover
101 	 * art image. That page returns the selected cover art by calling back to this page with three
102 	 * parameters. The first is the playlist id (which is null in this case). The second is the
103 	 * selected CoverArtSearchResult. The third is the playlist name that was passed to the <code>CoverArtEdit</code> page.
104 	 *  Test if this second parameter is non-null, and set the selected cover art if it is.
105 	 */
106 	public void activateExternalPage(Object[] args, IRequestCycle cycle) {
107 		CoverArtSearchResult cover = null;
108 		if(args.length>1 && args[1]!=null) {
109 			cover = (CoverArtSearchResult) args[1];
110 			String playlistName = (String) args[2];
111 			PlaylistImportProperties props = getArchive().getPlaylistProperties(playlistName);
112 			if(props!=null) {
113 				props.setCoverArt(cover.getURI().toString());
114 			}
115 		}
116 	}
117 
118 	/**
119 	 * Prepare rendering the page. Detect persistent playlists with the same name.
120 	 * Repeat the cover art search only once per hour.
121 	 */
122 	public void pageBeginRender(PageEvent event) {
123 		for(String playlistName : getArchive().getPlaylistNames()) {
124 			PlaylistImportProperties props = getArchive().getPlaylistProperties(playlistName);
125 			long MillisInHour = 1000L * 3600;
126 			if(!props.isCoverArtSearchDone() && 
127 					(props.getTimeOfLastCoverArtSearch()==null || new Date().getTime() - props.getTimeOfLastCoverArtSearch().getTime()>MillisInHour) ) {
128 				String url = guessUrl(playlistName);
129 				if(url!=null && url.length()>0) {
130 					props.setCoverArt(url);
131 					props.setCoverArtSearchDone(true);
132 				}
133 				props.setTimeOfLastCoverArtSearch(new Date());
134 			}
135 		}
136 	}
137 
138 
139 	/**
140 	 * Guess a cover art URL, if possible. Else, return "".
141 	 * @return The cover art url, if possible.
142 	 */
143 	private String guessUrl(String playlistName) {
144 		// Step 1: Use URL of persistent playlist that might be the same.
145 		Set<Playlist> candidates = getMcService().guessPlaylistsInArchive(getArchive());
146 		if(candidates.size()>0) {
147 			Playlist takeThis = candidates.iterator().next();
148 			ILink link = getServiceMap().getService("coverart").getLink(false,new Object[]{takeThis.getId(),200});
149 			return "http:"+link.getURL();
150 		}
151 		// Step 2: Try a cover art search.
152 		String guessedBandName = getMcService().guessBandNameOfArchive(getArchive());
153 		String guessedPlaylistName = playlistName;
154 		if(guessedPlaylistName==null) {
155 			Set<String> playlistNames = new HashSet<String>();
156 			for(MusicArchiveEntryBean entry: getArchive().getEntrySet()) {
157 				if(entry.getPlaylistName()!=null && entry.getPlaylistName().trim().length()>0) {
158 					playlistNames.add(entry.getPlaylistName());
159 					break;
160 				}
161 			}
162 			if(playlistNames.size()>0) {
163 				guessedPlaylistName = playlistNames.iterator().next();
164 			}
165 		}
166 		Collection<CoverArtSearchResult> coverArtList = getMcService().getCoverArtList(guessedBandName, guessedPlaylistName);
167 		if(coverArtList.size()>0) {
168 			return coverArtList.iterator().next().getURI().toString();
169 		}
170 		// Nothing found...
171 		return "";
172 	}
173 
174 	/**
175 	 * Goto the EditCoverArt page. The playlist is not persistent, so we must specify the band name and 
176 	 * playlist name using the 3rd and 4th argument. Guess the band name and playlist name. It is possible
177 	 * that this is a partial import of an already persistent playlist. Try to retrieve a persistent
178 	 * playlist with the same playlist name as the name in the first song entry.
179 	 * If this yields no result, guess the band name and playlist name from the first song entry. 
180 	 * The first argument to the CoverArtEdit page must be null to indicate a non persistent playlist.
181 	 * The second argument is the name of the page the CoverArtEdit page should return to ("MusicArchive").
182 	 */
183 	public ILink editCoverArt(IRequestCycle cycle) {
184 		IEngineService service = getExternalService();
185 		// Guess the band and playlist name. Both must be set in order to use the Covert art selection page.
186 		String bandName = getMcService().guessBandNameOfArchive(getArchive());
187 		String playlistName = null;
188 		// Is there a persistent playlist with the same name?
189 		List<Playlist> candidates = getMcService().findImportedPlaylist(getArchive());
190 		if(candidates.size()>0) {
191 			Playlist candidate = candidates.iterator().next();
192 			playlistName = candidate.getName();
193 		}
194 		MusicArchiveBean bean = getArchive();
195 		for (MusicArchiveEntryBean entry : bean.getEntrySet()) {
196 			if(playlistName==null&&(entry.getPlaylistName()!=null && entry.getPlaylistName().length()>0)) {
197 				playlistName = entry.getPlaylistName();
198 			}
199 		}
200 		if(bandName!=null && playlistName!=null) {
201 			ExternalServiceParameter parameter = new ExternalServiceParameter("CoverArtEdit", new Object[]{null,"MusicArchive",bandName,playlistName});
202 			ILink link = service.getLink(false, parameter);
203 			return link;
204 		} else {
205 			IPage page = cycle.getPage("MusicArchive");
206 			PropertyUtils.write(page,"archive",bean);
207 			PropertyUtils.write(page,"feedback","Please set band name and playlist name before selecting cover art.");
208 			ILink pageLink = getPageService().getLink(false, page); 
209 			return pageLink;
210 		}
211 	}
212 
213 	/**
214 	 * Returns true if a persistent playlist with the same name as <tt>playlistEntry</tt> exists or false otherwise. 
215 	 * @return True if a persistent playlist with the same name exists or false otherwise.
216 	 */
217 	public boolean isExistingPlaylist() {
218 		PlaylistImportProperties entry = getPlaylistEntry();
219 		List<Playlist> hits = getMcService().searchPlaylistByName(entry.getPlaylistName());
220 		return hits.size()>0;
221 	}
222 	
223 	/**
224 	 * Imports the MusicArchiveBean into the database. Errors while importing are reported.
225 	 * Upon successful import, the User is redirected to the ImportProgress-page, where he
226 	 * can select another MusicArchiveBean to import.
227 	 * @param cycle The Tapestry MVC cycle
228 	 * @param bean The MusicArchiveBean to import into the database
229 	 */
230 	public void importArchive(IRequestCycle cycle, MusicArchiveBean bean) {
231 		try {
232 			getImporter().importMusic(bean);
233 			/* This time-consuming method is blocking. Although the importMusic-procedure
234 			 * is not as slow as is hat been, it still takes a while. I (Varienaja) choose
235 			 * to keep this method blocking, as the User gets immediate feedback. 
236 			 */
237 			IPage page = cycle.getPage("ImportProgress");
238 			PropertyUtils.write(page,"feedback","Archive "+bean.getArchiveName()+" imported without errors.");
239 			cycle.activate(page);
240 		} catch (ImporterException e) {
241 			IPage page = cycle.getPage("MusicArchive");
242 			PropertyUtils.write(page,"archive",bean);
243 			PropertyUtils.write(page,"feedback","Error while importing: "+e.getLocalizedMessage());
244 			cycle.activate(page);
245 		}
246 	}
247 	
248 	/**
249 	 * Fill the song entries without a song index with a reasonable
250 	 * song index value.
251 	 * 
252 	 * @param cycle The tapestry MVC cycle.
253 	 * @param bean The MusicArchive bean that holds the information
254 	 *             about the songs in the archive.
255 	 */
256 	public void fillSongIndex(IRequestCycle cycle, MusicArchiveBean bean) {
257 		int index = 1;
258 		for (MusicArchiveEntryBean entry : bean.getEntrySet()) {
259 			if (entry.getSongIndex()<1) {
260 				entry.setSongIndex(index);
261 				index = index + 1;
262 			} else {
263 				index = entry.getSongIndex() + 1;
264 			}
265 		}
266 	}
267 	
268 	/**
269 	 * Fill the song entries without a playlist name with the name
270 	 * inserted into the first row. Updates the list of playlists
271 	 * shown in the screen.
272 	 * 
273 	 * @param cycle The tapestry MVC cycle.
274 	 * @param bean The MusicArchive bean that holds the information
275 	 *             about the songs in the archive.
276 	 */
277 	public void fillPlaylistName(IRequestCycle cycle, MusicArchiveBean bean) {
278 		String fillValue = null;
279 		for (MusicArchiveEntryBean entry : bean.getEntrySet()) {
280 			if (fillValue==null) {
281 				// use the value in this entry as the fillValue, if it is non-null. Trim the value.
282 				fillValue = entry.getPlaylistName()==null ? null : entry.getPlaylistName().trim();
283 			} else {
284 				entry.setPlaylistName(fillValue);
285 			}
286 		}
287 		getArchive().analyseMusicArchive();
288 		cycle.getResponseBuilder().updateComponent("archiveForm");
289 	}
290 	
291 	/**
292 	 * Fill the song entries with a band name with the name entered into the first row.
293 	 * 
294 	 * @param cycle The tapestry MVC cycle.
295 	 * @param bean The MusicArchive bean that holds the information
296 	 *             about the songs in the archive.
297 	 */
298 	public void fillBandName(IRequestCycle cycle, MusicArchiveBean bean) {
299 		String fillValue = null;
300 		for (MusicArchiveEntryBean entry : bean.getEntrySet()) {
301 			if (fillValue==null) {
302 				// use the value in this entry as the fillValue, if it is non-null. Trim the value.
303 				fillValue = entry.getBandName()==null ? null : entry.getBandName().trim();
304 			} else {
305 				entry.setBandName(fillValue);
306 			}
307 		}
308 	}
309 
310 	/**
311 	 * Fill the song entries with keywords the same as entered into the first row.
312 	 * 
313 	 * @param cycle The tapestry MVC cycle.
314 	 * @param bean The MusicArchive bean that holds the information
315 	 *             about the songs in the archive.
316 	 */
317 	public void fillKeywords(IRequestCycle cycle, MusicArchiveBean bean) {
318 		String fillValue = null;
319 		for (MusicArchiveEntryBean entry : bean.getEntrySet()) {
320 			if (fillValue==null) {
321 				// use the value in this entry as the fillValue, if it is non-null.
322 				fillValue = entry.getKeywordString()==null ? null : entry.getKeywordString().trim();
323 			} else {
324 				entry.setKeywordString(fillValue);
325 			}
326 		}
327 	}
328 
329 	@EventListener(targets="playlistNameEditBox", events="onchange", autoSubmit=true)
330 	/**
331 	 * Re-ananlyse the contents of the music archive if the user changed a playlist name.
332 	 * @param event The Tapestry Dojo event that fired.
333 	 */
334 	public void playlistNameChangedListener(BrowserEvent event) {
335 		getArchive().analyseMusicArchive();
336 		getRequestCycle().getResponseBuilder().updateComponent("archiveForm");
337 	}
338 	
339 	@EventListener(targets="addToExistingPlaylist", events="onchange", autoSubmit=true)
340 	/**
341 	 * When the "add to existing playlist" check box is on, disable the release date edit box
342 	 * and rerender the form. 
343 	 */
344 	public void addToExistingPlaylistChangedListener(BrowserEvent event) {
345 		getRequestCycle().getResponseBuilder().updateComponent("archiveForm");
346 	}
347 }