View Javadoc

1   /***
2    *     Ambient - A music player for the Android platform
3    Copyright (C) 2007 Martin Vysny
4    
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9    
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14  
15   You should have received a copy of the GNU General Public License
16   along with this program.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package sk.baka.ambient.library;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.net.MalformedURLException;
24  import java.net.URL;
25  import java.util.Set;
26  import java.util.zip.GZIPInputStream;
27  
28  import javax.xml.parsers.FactoryConfigurationError;
29  
30  import org.xml.sax.Attributes;
31  import org.xml.sax.SAXException;
32  import org.xml.sax.helpers.DefaultHandler;
33  
34  import sk.baka.ambient.AmbientApplication;
35  import sk.baka.ambient.NotifyingInputStream;
36  import sk.baka.ambient.R;
37  import sk.baka.ambient.collection.TrackMetadataBean;
38  import sk.baka.ambient.collection.TrackOriginEnum;
39  import sk.baka.ambient.collection.TrackMetadataBean.Builder;
40  import sk.baka.ambient.commons.IOUtils;
41  import sk.baka.ambient.commons.MiscUtils;
42  import android.content.Context;
43  import android.content.SharedPreferences;
44  import android.content.SharedPreferences.Editor;
45  import android.view.Gravity;
46  import android.widget.Toast;
47  
48  /***
49   * Scans the Magnatune store.
50   * 
51   * @author Martin Vysny
52   */
53  public class MagnatuneScanner implements IFileScanner {
54  	private Library library = null;
55  	private GenreCache genreCache;
56  	/***
57  	 * The location of a gzipped xml file containing the Magnatune collection.
58  	 */
59  	private final static URL COLLECTION_URL;
60  	/***
61  	 * Timestamp of last collection update.
62  	 */
63  	private final static URL TIMESTAMP_URL;
64  	static {
65  		try {
66  			COLLECTION_URL = new URL(
67  					"http://magnatune.com/info/song_info2_xml.gz");
68  			TIMESTAMP_URL = new URL(
69  					"http://magnatune.com/info/last_update_timestamp");
70  		} catch (MalformedURLException e) {
71  			throw new RuntimeException(e);
72  		}
73  	}
74  
75  	private long getCurrentTimestamp() throws IOException {
76  		final InputStream in = TIMESTAMP_URL.openStream();
77  		try {
78  			final String str = IOUtils.readLine(in);
79  			return Long.parseLong(str);
80  		} finally {
81  			MiscUtils.closeQuietly(in);
82  		}
83  	}
84  
85  	public void init(Library library, GenreCache genreCache) {
86  		this.library = library;
87  		this.genreCache = genreCache;
88  	}
89  
90  	public void run() {
91  		try {
92  			// check the timestamp
93  			final long timestamp = checkTimestamp();
94  			if (timestamp < 0) {
95  				return;
96  			}
97  			checkInterrupted();
98  			// clean the database
99  			library.backend.clean(getOrigin());
100 			// parse the XML file
101 			parseMagnatuneXML();
102 			// remember new timestamp
103 			if (!Thread.currentThread().isInterrupted()) {
104 				final Editor e = getPrefs().edit();
105 				e.putLong("timestamp", timestamp);
106 				e.commit();
107 			}
108 		} catch (Exception e) {
109 			throw new RuntimeException(e);
110 		}
111 	}
112 
113 	private SharedPreferences getPrefs() {
114 		return AmbientApplication.getInstance().getSharedPreferences(
115 				"magnatune", Context.MODE_PRIVATE);
116 	}
117 
118 	/***
119 	 * Checks current magnatune timestamp.
120 	 * 
121 	 * @return positive long when the rescan is needed (the value of the new
122 	 *         timestamp), negative long if rescan is not needed.
123 	 * @throws IOException
124 	 */
125 	private long checkTimestamp() throws IOException {
126 		final long currentTimestamp = getCurrentTimestamp();
127 		final SharedPreferences prefs = getPrefs();
128 		final long lastTimestamp = prefs.getLong("timestamp", 0);
129 		if (lastTimestamp >= currentTimestamp) {
130 			AmbientApplication.getHandler().post(new Runnable() {
131 				public void run() {
132 					final Toast toast = Toast.makeText(AmbientApplication
133 							.getInstance(),
134 							R.string.magnatuneRescanUnnecessary,
135 							Toast.LENGTH_LONG);
136 					toast.setGravity(Gravity.CENTER, 0, 0);
137 					toast.show();
138 				}
139 			});
140 			userNotified = true;
141 			return -1;
142 		}
143 		return currentTimestamp;
144 	}
145 
146 	private final String caption = AmbientApplication.getInstance().getString(
147 			R.string.magnatune_rescanning);
148 	
149 	private void parseMagnatuneXML() throws FactoryConfigurationError,
150 			SAXException, IOException {
151 		final InputStream in = new GZIPInputStream(NotifyingInputStream
152 				.fromURL(COLLECTION_URL, 50, caption));
153 		IOUtils.parseXML(in, new Handler());
154 	}
155 
156 	private void checkInterrupted() {
157 		if (Thread.currentThread().isInterrupted()) {
158 			throw new RuntimeException("Interrupted");
159 		}
160 	}
161 
162 	/***
163 	 * Handles elements from the magnatune collection xml.
164 	 * 
165 	 * @author Martin Vysny
166 	 */
167 	private final class Handler extends DefaultHandler {
168 		private boolean wasAllSongs = false;
169 
170 		@Override
171 		public void endDocument() {
172 			wasAllSongs = false;
173 		}
174 
175 		@Override
176 		public void startDocument() {
177 			checkInterrupted();
178 			wasAllSongs = false;
179 		}
180 
181 		@Override
182 		public void startElement(String uri, String localName, String name,
183 				Attributes attributes) throws SAXException {
184 			checkInterrupted();
185 			if (localName.equals("AllSongs")) {
186 				wasAllSongs = true;
187 			} else if (localName.equals("Track")) {
188 				if (!wasAllSongs) {
189 					throw new SAXException("AllSongs element not found");
190 				}
191 				handleTrack(attributes);
192 			} else {
193 				throw new SAXException("Unknown element " + localName);
194 			}
195 		}
196 	}
197 
198 	public TrackOriginEnum getOrigin() {
199 		return TrackOriginEnum.Magnatune;
200 	}
201 
202 	/***
203 	 * Stores the tag into the db.
204 	 * 
205 	 * @param tag
206 	 *            the tag to store.
207 	 */
208 	private void storeTag(TrackMetadataBean tag) {
209 		// get genre ids
210 		final Set<Long> genreIds = genreCache.register(tag.getGenre());
211 		// register the track
212 		library.backend.registerNewTrack(tag, genreIds);
213 	}
214 
215 	private void handleTrack(Attributes attributes) {
216 		final Builder track = TrackMetadataBean.newBuilder();
217 		track.artist = MiscUtils.fixArtistAlbumName(attributes
218 				.getValue("",
219 				"artist"));
220 		track.album = MiscUtils
221 				.fixArtistAlbumName(attributes.getValue("album"));
222 		track.title = attributes.getValue("title");
223 		track.trackNumber = attributes.getValue("tracknum");
224 		track.yearReleased = attributes.getValue("year");
225 		track.genre = attributes.getValue("genre");
226 		track.length = Integer.parseInt(attributes.getValue("seconds"));
227 		track.location = attributes.getValue("url");
228 		track.origin = TrackOriginEnum.Magnatune;
229 		// use ogg - we cannot use ogg because of
230 		// http://code.google.com/p/android/issues/detail?id=734
231 		// if (location.endsWith(".mp3")) {
232 		// location = location.substring(0, location.length() - 4) + ".ogg";
233 		// }
234 		track.buyURL = attributes.getValue("buy");
235 		track.artistDesc = attributes.getValue("artistdesc");
236 		track.artistURL = attributes.getValue("home");
237 		track.license = attributes.getValue("license");
238 		final TrackMetadataBean bean = track.build(-1);
239 		storeTag(bean);
240 	}
241 
242 	private boolean userNotified = false;
243 	
244 	public boolean isUserNotified() {
245 		return userNotified;
246 	}
247 }