View Javadoc

1   package org.musicontroller.streaming;
2   
3   import java.io.BufferedOutputStream;
4   import java.io.File;
5   import java.io.FileInputStream;
6   import java.io.IOException;
7   import java.io.InputStream;
8   import java.io.OutputStream;
9   import java.io.PrintStream;
10  import java.util.Date;
11  import java.util.HashMap;
12  import java.util.LinkedList;
13  import java.util.List;
14  import java.util.Map;
15  import java.util.zip.Adler32;
16  import java.util.zip.CheckedOutputStream;
17  import java.util.zip.ZipEntry;
18  import java.util.zip.ZipOutputStream;
19  
20  import org.apache.log4j.Logger;
21  import org.apache.tapestry.IRequestCycle;
22  import org.apache.tapestry.engine.ExternalService;
23  import org.apache.tapestry.engine.IEngineService;
24  import org.apache.tapestry.engine.ILink;
25  import org.apache.tapestry.services.LinkFactory;
26  import org.apache.tapestry.services.ServiceConstants;
27  import org.apache.tapestry.util.ContentType;
28  import org.apache.tapestry.web.WebResponse;
29  import org.musicontroller.core.Contract_PS;
30  import org.musicontroller.core.Event;
31  import org.musicontroller.core.Playlist;
32  import org.musicontroller.core.Song;
33  import org.musicontroller.dao.Dao;
34  import org.musicontroller.security.IUser;
35  import org.varienaja.util.FileOperations;
36  
37  public class Downloader extends ExternalService implements IEngineService {
38  	private static final Logger log = Logger.getLogger(Downloader.class);
39  
40  	public static final String SERVICE_NAME = "download";
41      private LinkFactory _linkFactory;
42      private WebResponse _response;
43  	private Dao _dao;
44  	
45  	/*
46  	 * A DownloadLink must be constructed with 4 parameters:
47  	 * Kind (S Song or P Playlist)
48  	 * Id The id of the Song or Playlist
49  	 * Userid, the id of the user requesting to download this Song or Playlist
50  	 * Passhash, the hash of the pass of the User.
51  	 * When passhash and userid match, the Song or Playlist is transferred. 
52  	 * (non-Javadoc)
53  	 * @see org.apache.tapestry.engine.ExternalService#getLink(boolean, java.lang.Object)
54  	 */
55  	public ILink getLink(boolean post, Object parameter) {
56  		//The parameter is an array
57  		Object[] params = (Object[]) parameter;
58  		
59  		String kind = params[0]==null ? "P" : params[0].toString();
60  		Long id = params[1]==null ? 0 : (Long) params[1];
61  		Long userid = params[2]==null ? 0 : (Long) params[2];
62  		String passhash = params[3].toString();
63  		
64          Map<String,String> parameters = new HashMap<String,String>();
65          parameters.put(ServiceConstants.SERVICE, getName());
66          parameters.put("userid",userid.toString());
67          parameters.put("passhash", passhash);
68          parameters.put("id", id.toString());
69          parameters.put("kind", kind);
70          
71          return _linkFactory.constructLink(this, false, parameters, true);
72  	}
73  	
74  	/*
75  	 * (non-Javadoc)
76  	 * @see org.apache.tapestry.engine.ExternalService#service(org.apache.tapestry.IRequestCycle)
77  	 */
78  	public void service(IRequestCycle cycle) {
79  		String kind = cycle.getParameter("kind");
80  		long userid = Long.parseLong(cycle.getParameter("userid"));
81  		String passhash = cycle.getParameter("passhash");
82  		long id = Long.parseLong(cycle.getParameter("id"));
83  		if ("P".equals(kind)) {
84  			servePlaylist(userid, passhash, id);
85  		} else {
86  			serveSong(userid, passhash, id);
87  		}
88  	}
89  
90  	/**
91  	 * Copies the musical contents of a Song to the response, if userid and
92  	 * passhash match. After the complete transfer of the song, a
93  	 * Downloaded-Event is added to the Song.
94  	 * @param userid The user
95  	 * @param passhash The passhash of the user.
96  	 * @param songid The id of the Song to download.
97  	 */
98  	private void serveSong(long userid, String passhash, long songid) {
99  		log.debug("Transferring song "+songid+" to client.");
100 		
101 		IUser user = _dao.getUserById(userid);
102 		if (passhash!=null && passhash.equals(user.getPassword())) { //Check MD5-sum
103 			Song song = _dao.getSongById(songid);
104 			File songfile = song.getLink().getFile();
105 			String songlink = songfile.getAbsolutePath();
106 			String namePart = songlink.substring(songlink.lastIndexOf(File.separator)+1);
107         	_response.setHeader("Content-Disposition","attachment; filename="+namePart.replaceAll(" ","_"));
108 			
109         	OutputStream out = null;
110 			try {
111 				out = _response.getOutputStream(new ContentType("audio/mpeg"));
112 				InputStream in = null;
113 				try {
114 					in = new FileInputStream(songfile);
115 					FileOperations.copyStream(in,out);
116 					StreamMaster.getDJByUser(userid).getMusiController().
117 					saveSongEvent(songid,Event.downloaded,userid,new Date());
118 				} catch (IOException e) {
119 					log.error("Error copying file to downloadstream. "+e);
120 				} finally {
121 					try {
122 						if (in!=null) in.close();
123 					} catch(IOException e) {
124 						log.error("Error closing inputstream. "+e);
125 					}
126 				}
127 			} catch (Exception e) {
128 				log.fatal("Error while transferring song to client.");
129 			} finally {
130 				try {
131 					if (out!=null) out.close();
132 				} catch (IOException e) {
133 					log.error("Error closing outputstream "+e);
134 				}
135 			}
136 		} else {
137 			log.debug("False credentials provided, ignoring download-request.");
138 		}
139 	}
140 
141 	/**
142 	 * Copies the musical contents of a Playlist to the response, if userid and
143 	 * passhash match AND the user is logged on. After the complete transfer of
144 	 * the Playlist, a Downloaded-Event is added to each of its Songs.
145 	 * @param userid The user
146 	 * @param passhash The passhash of the user.
147 	 * @param playlistid The id of the Playlist to download.
148 	 */
149 	private void servePlaylist(long userid, String passhash, long playlistid) {
150 		log.debug("Transferring playlist "+playlistid+" to client.");
151 		
152 		if (StreamMaster.getDJByUser(userid)!=null) { //User is logged on
153 			IUser user = _dao.getUserById(userid);
154 			if (passhash!=null && passhash.equals(user.getPassword())) { //Check MD5-sum
155 	        	//Copy playlist contents to a songlist, so we use the Playlist-object
156 	        	//as short as possible. This way, we prevent concurrentmodificationerrors
157 	        	//and such when downloading and editting the same playlist at the same time.
158 	        	Playlist playlist = _dao.getPlaylistById(playlistid, user);
159 	        	Map<Long,Date> songidset = new HashMap<Long,Date>();
160 	        	List<Long> songidlist = new LinkedList<Long>();
161 	        	for (Contract_PS psc : playlist.getSongs()) {
162 	        		songidset.put(psc.getSong().getId(),null);
163 	        		songidlist.add(psc.getSong().getId());
164 	        	}
165 	        	
166 	        	_response.setHeader("Content-Disposition","attachment; filename="+playlist.getName().replaceAll(" ","_")+".zip");
167 	        	OutputStream out = null;
168 				try {
169 					out = _response.getOutputStream(new ContentType("application/zip"));
170 					
171 					outputPlaylist(playlist.getName(), out,songidlist,songidset);
172 					//Save the songevents all at once (if the download was aborted (exception thrown!), no download-events are written)
173 					
174 					for (Map.Entry<Long,Date> es : songidset.entrySet()) {
175 						StreamMaster.getDJByUser(userid).getMusiController().
176 							saveSongEvent(es.getKey(),Event.downloaded,userid,es.getValue());
177 					}
178 				} catch (Exception e) {
179 					log.fatal("Error while transferring playlist to client.");
180 				} finally {
181 					try {
182 						if (out!=null) out.close();
183 					} catch (IOException e) {
184 						log.error("Error closing outputstream "+e);
185 					}
186 				}
187 			} else {
188 				log.debug("False credentials provided, ignoring download-request.");
189 			}
190 		} else {
191 			log.debug("This user has not logged on, ignoring download-request.");
192 		}
193 	}
194 	
195 	public String getName() {
196 		return SERVICE_NAME;
197 	}
198 
199     public void setLinkFactory(LinkFactory linkFactory) {
200         _linkFactory = linkFactory;
201     }
202 
203     public void setResponse(WebResponse response) {
204         _response = response;
205     }
206     
207     public void setDao(Dao dao) {
208         _dao = dao;
209     }
210     
211     /**
212      * Outputs all songs in the songidlist as one zip-archive to the OutputStream.
213      * A m3u-file, containing the Playlist is added as well. Songs that appear
214      * more than once in the playlist are listed likewise in the m3u-file, but
215      * the corresponding mp3-file is only contained once in the .zip-file.
216      * This method modifies the songidlist such, that afterwards all Entries have
217      * a Date, which is the moment that the streaming finished.
218      * @param playlistname The name of the playlist
219      * @param out The stream to write the songs to
220      * @param songidlist All id's of the songs in the playlist, in order of appearence
221      * @param songidset The map of (songid,downloaddate) pairs
222      */
223     private void outputPlaylist(String playlistname, OutputStream out, List<Long> songidlist, Map<Long,Date> songidset) {
224     	//TODO output the albumcover as well
225     	List<String> m3uContents = new LinkedList<String>();
226     	m3uContents.add("#EXTM3U");
227 		ZipOutputStream zout = null;
228 		
229 		try {
230 			CheckedOutputStream checksum = new CheckedOutputStream(out, new Adler32());
231 			zout = new ZipOutputStream(new BufferedOutputStream(checksum));
232 
233 			for (Long songId : songidlist) {
234 				Song song = _dao.getSongById(songId);
235 				File songfile = song.getLink().getFile();
236 				String songlink = songfile.getAbsolutePath();
237 				String ext = songlink.substring(songlink.lastIndexOf("."));
238 				String name = FileOperations.translateIllegalFileChars(song.getBand().getName() +" - " +song.getName());
239 				String filename = name+ext;
240 				
241 				m3uContents.add("#EXTINF:" + song.getLength()/1000 + "," + name);
242 				m3uContents.add(filename);
243 				
244 				if (songidset.get(songId)==null) { // Output the song itself only once even if it is contained multiple times within the playlist.
245 					ZipEntry ze = new ZipEntry(filename);
246 					zout.putNextEntry(ze);
247 					InputStream in = null;
248 					try {
249 						in = new FileInputStream(songfile);
250 						FileOperations.copyStream(in,zout);
251 						songidset.put(songId ,new Date());
252 					} catch (IOException e) {
253 						log.error("Error copying file to downloadstream. "+e);
254 					} finally {
255 						try {
256 							if (in!=null) in.close();
257 						} catch(IOException e) {
258 							log.error("Error closing inputstream. "+e);
259 						}
260 					}
261 				}
262 				_dao.evict(song);
263 			}
264 			ZipEntry ze = new ZipEntry(playlistname+".m3u");
265 			zout.putNextEntry(ze);
266 			PrintStream ps = new PrintStream(zout);
267 			for (String m3uLine : m3uContents) {
268 				ps.println(m3uLine);
269 			}
270 			ps.flush();
271 			
272 			zout.close();
273 		} catch (IOException e) {
274 			log.error("Error streaming playlist: "+e);
275 			songidset.clear(); //Do not save download-events.
276 		}
277     }
278     
279 }