Changeset 79
- Timestamp:
- 06/16/07 00:37:24 (1 year ago)
- Files:
-
- trunk/src/main/org/lastpod/DbReader.java (modified) (7 diffs)
- trunk/src/main/org/lastpod/ModelImpl.java (modified) (4 diffs)
- trunk/src/main/org/lastpod/PreferencesEditor.java (modified) (6 diffs)
- trunk/src/main/org/lastpod/TrackItem.java (modified) (2 diffs)
- trunk/src/main/org/lastpod/parser (added)
- trunk/src/main/org/lastpod/parser/ItunesDbParser.java (added)
- trunk/src/main/org/lastpod/parser/PlayCountsParser.java (added)
- trunk/src/main/org/lastpod/parser/TrackItemParser.java (added)
- trunk/src/main/org/lastpod/util/IoUtils.java (modified) (2 diffs)
- trunk/src/test/Play Counts (added)
- trunk/src/test/org/lastpod/DbReaderTest.java (added)
- trunk/src/test/org/lastpod/TrackItemTest.java (modified) (1 diff)
- trunk/src/test/org/lastpod/parser (added)
- trunk/src/test/org/lastpod/parser/MockItunesDbParser.java (added)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/src/main/org/lastpod/DbReader.java
r68 r79 19 19 package org.lastpod; 20 20 21 import org.lastpod. util.IoUtils;21 import org.lastpod.parser.TrackItemParser; 22 22 23 import java.io.BufferedInputStream;24 import java.io.File;25 import java.io.FileInputStream;26 23 import java.io.IOException; 27 24 import java.io.InputStream; … … 30 27 31 28 import java.util.ArrayList; 32 import java.util.Calendar; 33 import java.util.Collections; 29 import java.util.List; 34 30 35 31 /** … … 41 37 public class DbReader { 42 38 /** 43 * The location of the iTunes path.39 * Parses the itunesDatabase. 44 40 */ 45 private String iTunesPath;41 private TrackItemParser itunesDbParser; 46 42 47 43 /** 48 * The location of the iTunes databasefile.44 * Parses the Play Counts file. 49 45 */ 50 private String itunesfile; 51 52 /** 53 * The location of the iPod play counts file. 54 */ 55 private String playcountsfile; 56 57 /** 58 * A list of all the tracks from the iTunes databaes file. 59 */ 60 private ArrayList tracklist; 61 62 /** 63 * Stores a boolean value that will be passed into <code>TrackItem</code>. 64 */ 65 boolean parseVariousArtists; 46 private TrackItemParser playCountsParser; 66 47 67 48 /** … … 70 51 * reject them.) 71 52 */ 72 private ArrayList recentplays;53 private List recentplays; 73 54 74 55 /** … … 82 63 * Initializes the class with the locations of the iPod DB files. 83 64 * 84 * @param itunespath Directory containing the iTunesDB and the corresponding85 * Play Counts, including trailing "\" or "/".86 * @param parseVariousArtists If <code>true</code> then parses "Various87 * Artists"88 65 */ 89 public DbReader(String itunespath, boolean parseVariousArtists) { 90 if (!itunespath.endsWith(File.separator)) { 91 itunespath += File.separator; 92 } 93 94 this.iTunesPath = itunespath; 95 this.itunesfile = itunespath + "iTunesDB"; 96 this.playcountsfile = itunespath + "Play Counts"; 97 this.tracklist = new ArrayList(); 66 public DbReader(TrackItemParser itunesDbParser, TrackItemParser playCountsParser) { 67 this.itunesDbParser = itunesDbParser; 98 68 this.recentplays = new ArrayList(); 99 this.p arseVariousArtists = parseVariousArtists;69 this.playCountsParser = playCountsParser; 100 70 } 101 71 … … 104 74 * @return Returns recent plays. 105 75 */ 106 public ArrayList getRecentplays() {76 public List getRecentplays() { 107 77 return recentplays; 108 78 } … … 111 81 * Attempts to open and parse the DB & Play Counts files, creating 112 82 * the appropriate data structures. 113 * @throws IOException Thrown if errors occur.114 83 */ 115 public void parse() throws IOException { 116 InputStream itunesFileIn = null; 117 InputStream itunesBufferedIn = null; 118 InputStream playCountsFileIn = null; 119 InputStream playCountsBufferedIn = null; 84 public void parse() { 85 List trackList = itunesDbParser.parse(); 120 86 121 try { 122 itunesFileIn = new FileInputStream(itunesfile); 123 itunesBufferedIn = new BufferedInputStream(itunesFileIn, 65535); 124 125 parseitunesdb(itunesBufferedIn); 126 } catch (IOException e) { 127 throw new IOException("Error reading iTunes Database"); 128 } finally { 129 IoUtils.cleanup(itunesFileIn, null); 130 IoUtils.cleanup(itunesBufferedIn, null); 131 } 132 133 try { 134 playCountsFileIn = new FileInputStream(playcountsfile); 135 playCountsBufferedIn = new BufferedInputStream(playCountsFileIn, 65535); 136 137 parseplaycounts(playCountsBufferedIn); 138 } catch (IOException e) { 139 String errorMsg = 140 "Error reading Play Counts Database.\n" 141 + "Have you listened to any music on your iPod recently?\n" 142 + "This can also be caused if you are running iTunes and you have it setup " 143 + "to automatically run iTunes when an iPod is detected."; 144 throw new IOException(errorMsg); 145 } finally { 146 IoUtils.cleanup(playCountsFileIn, null); 147 IoUtils.cleanup(playCountsBufferedIn, null); 148 } 149 } 150 151 /** 152 * Parses track information from the iTunesDB. 153 * @param itunesistream A stream that reads the iTunes database file. 154 * @throws IOException Thrown if errors occur. 155 */ 156 public void parseitunesdb(InputStream itunesistream) 157 throws IOException { 158 byte[] buf = new byte[1]; 159 160 //we seek one at a time because the mhit marker won't always be at a multiple of four 161 while (itunesistream.read(buf) != -1) { 162 if (buf[0] == 'm') { //Search for MHIT 163 itunesistream.mark(1048576); 164 buf = new byte[3]; 165 itunesistream.read(buf); 166 167 if (new String(buf).equals("hit")) { 168 tracklist.add(parsemhit(itunesistream)); 169 } else { 170 itunesistream.reset(); 171 } 172 } 173 174 buf = new byte[1]; 175 } 176 } 177 178 /** 179 * Parses an MHIT object from the iTunes Database. 180 * @param itunesistream A stream that reads the iTunes database file. 181 * @return Returns parsed track object. 182 * @throws IOException Thrown if errors occur. 183 */ 184 public TrackItem parsemhit(InputStream itunesistream) 185 throws IOException { 186 byte[] dword = new byte[4]; 187 TrackItem track = new TrackItem(); 188 track.setParseVariousArtists(parseVariousArtists); 189 190 itunesistream.mark(1048576); //mark beginning of MHIT location 191 192 itunesistream.read(dword); 193 194 long headersize = DbReader.littleEndianToBigInt(dword).longValue(); 195 196 DbReader.skipFully(itunesistream, 4); 197 itunesistream.read(dword); 198 199 long nummhods = DbReader.littleEndianToBigInt(dword).longValue(); 200 201 itunesistream.read(dword); 202 track.setTrackid(DbReader.littleEndianToBigInt(dword).longValue()); 203 204 DbReader.skipFully(itunesistream, 20); 205 itunesistream.read(dword); 206 track.setLength(DbReader.littleEndianToBigInt(dword).longValue() / 1000); 207 208 itunesistream.reset(); 209 DbReader.skipFully(itunesistream, headersize - 4); //skip to end of MHIT 210 211 for (long i = 0; i < nummhods; i++) { 212 parsemhod(track, itunesistream); 213 } 214 215 return track; 216 } 217 218 /** 219 * Parses an MHOD object and sets proper fields in the track item object. 220 * @param track Track Item. 221 * @param itunesistream A stream that reads the iTunes database file. 222 * @throws IOException Thrown if errors occur. 223 */ 224 public void parsemhod(TrackItem track, InputStream itunesistream) 225 throws IOException { 226 byte[] dword = new byte[4]; 227 228 itunesistream.mark(1048576); //mark beginning of MHOD location 229 230 DbReader.skipFully(itunesistream, 8); 231 232 itunesistream.read(dword); 233 234 long totalsize = DbReader.littleEndianToBigInt(dword).longValue(); 235 236 itunesistream.read(dword); 237 238 int mhodtype = DbReader.littleEndianToBigInt(dword).intValue(); 239 240 if ((mhodtype == 1) || (mhodtype == 3) || (mhodtype == 4)) { 241 DbReader.skipFully(itunesistream, 12); 242 itunesistream.read(dword); 243 244 int strlen = DbReader.littleEndianToBigInt(dword).intValue(); 245 246 DbReader.skipFully(itunesistream, 8); 247 248 byte[] data = new byte[strlen]; 249 itunesistream.read(data); 250 251 String stringdata = new String(data, "UTF-16LE"); 252 253 switch (mhodtype) { 254 case 1: 255 track.setTrack(stringdata); 256 257 break; 258 259 case 3: 260 track.setAlbum(stringdata); 261 262 break; 263 264 case 4: 265 track.setArtist(stringdata); 266 267 break; 268 } 269 } 270 271 itunesistream.reset(); 272 DbReader.skipFully(itunesistream, totalsize); 273 } 274 275 /** 276 * Parses play counts information from "Play Counts". 277 * @param playcountsistream A stream that reads the iPod play counts file. 278 * @throws IOException Thrown if errors occur. 279 */ 280 public void parseplaycounts(InputStream playcountsistream) 281 throws IOException { 282 byte[] dword = new byte[4]; 283 284 DbReader.skipFully(playcountsistream, 8); 285 playcountsistream.read(dword); 286 287 long entrylen = DbReader.littleEndianToBigInt(dword).longValue(); 288 289 playcountsistream.read(dword); 290 291 int numentries = DbReader.littleEndianToBigInt(dword).intValue(); 292 293 DbReader.skipFully(playcountsistream, 80); //skip rest of header 294 295 for (int i = 0; i < (numentries - 1); i++) { 296 playcountsistream.mark(1048576); //save beginning of entry location 297 298 playcountsistream.read(dword); 299 300 long playcount = DbReader.littleEndianToBigInt(dword).longValue(); 301 302 if (playcount > 0) { 303 playcountsistream.read(dword); 304 305 long lastplayed = DbReader.littleEndianToBigInt(dword).longValue(); 306 lastplayed -= 2082844800; //convert to UNIX timestamp 307 308 Calendar calendar = Calendar.getInstance(); 309 long offset = 310 calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET); 311 lastplayed -= (offset / 1000); 312 313 TrackItem temptrack = (TrackItem) tracklist.get(i); 314 temptrack.setPlaycount(playcount); 315 temptrack.setLastplayed(lastplayed - temptrack.getLength()); 316 317 if (History.getInstance(iTunesPath).isInHistory(temptrack.getLastplayed())) { 318 temptrack.setActive(Boolean.FALSE); 319 } 320 321 recentplays.add(tracklist.get(i)); 322 } 323 324 playcountsistream.reset(); 325 DbReader.skipFully(playcountsistream, entrylen); 326 } 327 328 Collections.sort(recentplays); 87 playCountsParser.setTrackList(trackList); 88 recentplays = playCountsParser.parse(); 329 89 } 330 90 trunk/src/main/org/lastpod/ModelImpl.java
r78 r79 19 19 package org.lastpod; 20 20 21 import org.lastpod.parser.ItunesDbParser; 22 import org.lastpod.parser.PlayCountsParser; 23 21 24 import org.lastpod.util.MiscUtilities; 22 23 import java.io.IOException;24 25 25 26 import java.util.ArrayList; … … 80 81 String iTunesPath = fPrefs.get("iTunes Path", "default"); 81 82 String parseVariousArtistsStr = fPrefs.get("parseVariousArtists", "1"); 83 String parseMultiPlayTracksStr = fPrefs.get("parseMultiPlayTracks", "1"); 82 84 boolean parseVariousArtists = parseVariousArtistsStr.equals("1") ? true : false; 85 boolean parseMultiPlayTracks = parseMultiPlayTracksStr.equals("1") ? true : false; 83 86 84 87 if (iTunesPath.equals("default")) { … … 88 91 } 89 92 90 DbReader reader = new DbReader(iTunesPath, parseVariousArtists); 93 ItunesDbParser itunesDbParser = new ItunesDbParser(iTunesPath, parseVariousArtists); 94 PlayCountsParser playCountsParser = new PlayCountsParser(iTunesPath, parseMultiPlayTracks); 95 DbReader reader = new DbReader(itunesDbParser, playCountsParser); 91 96 92 97 try { … … 94 99 recentlyPlayed = reader.getRecentplays(); 95 100 userInterface.newTrackListAvailable(recentlyPlayed); 96 } catch ( IOException e) {101 } catch (Exception e) { 97 102 StackTraceElement[] trace = e.getStackTrace(); 98 103 trunk/src/main/org/lastpod/PreferencesEditor.java
r71 r79 75 75 private JTextField backupUrlField; 76 76 private JCheckBox parseVariousArtistsCheck; 77 private JCheckBox parseMultiPlayTracksCheck; 77 78 private JTextField iTunesfield; 78 79 private JCheckBox iTCheck; … … 237 238 String toolTip = 238 239 "<html>If a URL is entered the play information will be<br>" 239 + " submitted to both Last.fm and the given URL. This allows one<br>" 240 + " to perform a backup of the Last.fm data.<br><br>" 241 + " If the parse option is checked then LastPod will parse the track<br>" 242 + " information when the artist is \"Various Artists\". The<br>" 243 + " parsing consists of spliting the artist and track from the<br>" 244 + "orgininal track String. (For example, \"Bing Crosby - <br>" 245 + "I'll Be Home for Christmas\" becomes artist=Bing Crosby<br>" 246 + "and track name=I'll Be Home for Christmas."; 240 + "submitted to both Last.fm and the given URL. This allows one<br>" 241 + "to perform a backup of the Last.fm data.<br><br>" 242 + "If the parse various artists option is enabled, LastPod will<br>" 243 + "parse the track information when the artist is \"Various<br>" 244 + "Artists\". The parsing consists of spliting the artist and track<br>" 245 + "from the original track String. (For example, \"Bing<br>" 246 + "Crosby - I'll Be Home for Christmas\" becomes artist=Bing<br>" 247 + "Crosby and track name=I'll Be Home for Christmas.<br><br>" 248 + "If parse multi-play tracks is enabled, LastPod will generate<br>" 249 + "a new track as needed. This provides more accurate statistics,<br>" 250 + "but invalid play times."; 247 251 p3.setToolTipText(toolTip); 248 252 … … 266 270 p3.add(parseVariousArtistsCheck); 267 271 268 SpringUtilities.makeCompactGrid(p3, 2, 2, 5, 2, 3, 4); 272 JLabel parseMultiPlayTracksLabel = new JLabel("Parse Multi-Play Tracks: "); 273 p3.add(parseMultiPlayTracksLabel); 274 parseMultiPlayTracksCheck = new JCheckBox(); 275 parseMultiPlayTracksCheck.addActionListener(new ActionListener() { 276 public void actionPerformed(ActionEvent e) { 277 if (parseMultiPlayTracksCheck.isSelected()) { 278 parseMultiPlayTracksCheck.setText("Enabled"); 279 } else { 280 parseMultiPlayTracksCheck.setText("Disabled"); 281 } 282 } 283 }); 284 p3.add(parseMultiPlayTracksCheck); 285 286 SpringUtilities.makeCompactGrid(p3, 3, 2, 5, 2, 3, 4); 269 287 p.add(p3); 270 288 … … 374 392 parseVariousArtistsCheck.setSelected(false); 375 393 } 394 395 if (fPrefs.get("parseMultiPlayTracks", "1").equals("1")) { 396 parseMultiPlayTracksCheck.setText("Enabled"); 397 parseMultiPlayTracksCheck.setSelected(true); 398 } else { 399 parseMultiPlayTracksCheck.setText("Disabled"); 400 parseMultiPlayTracksCheck.setSelected(false); 401 } 376 402 } 377 403 … … 390 416 391 417 boolean selected = parseVariousArtistsCheck.isSelected(); 418 boolean selected2 = parseMultiPlayTracksCheck.isSelected(); 392 419 String parseVariousArtists = selected ? "1" : "0"; 420 String parseMultiPlayTracks = selected2 ? "1" : "0"; 393 421 394 422 fPrefs.put("iTunes Path", newItunesPath); … … 397 425 fPrefs.put("iTunes Status", this.iTunesStatus.getText()); 398 426 fPrefs.put("parseVariousArtists", parseVariousArtists); 427 fPrefs.put("parseMultiPlayTracks", parseMultiPlayTracks); 399 428 400 429 try { trunk/src/main/org/lastpod/TrackItem.java
r56 r79 37 37 38 38 /** 39 * Default constructor 39 * Default constructor. 40 40 */ 41 41 public TrackItem() { … … 51 51 52 52 /** 53 * Constructs a new TrackItem based off the given one. 54 * @param trackItem The TrackItem to effectively clone. 55 */ 56 public TrackItem(TrackItem trackItem) { 57 super(); 58 this.setTrackid(trackItem.getTrackid()); 59 this.setActive(trackItem.isActive()); 60 this.setLength(trackItem.getLength()); 61 this.setArtist(trackItem.getArtist()); 62 this.setAlbum(trackItem.getAlbum()); 63 this.setTrack(trackItem.getTrack()); 64 this.setPlaycount(trackItem.getPlaycount()); 65 this.setLastplayed(trackItem.getLastplayed()); 66 this.setParseVariousArtists(trackItem.isParseVariousArtists()); 67 } 68 69 /** 53 70 * If <code>true</code>; sets this track to be submitted, otherwise do not 54 71 * submit. trunk/src/main/org/lastpod/util/IoUtils.java
r53 r79 24 24 import java.io.Reader; 25 25 import java.io.Writer; 26 27 import java.math.BigInteger; 26 28 27 29 /** … … 88 90 } 89 91 } 92 93 /** 94 * This converts any size byte array to a BigInteger. 95 * @param num Little-Endian byte array. 96 * @return A BigInt. 97 */ 98 public static BigInteger littleEndianToBigInt(byte[] num) { 99 byte temp; 100 101 int upperBound = num.length - 1; 102 int lowerBound = 0; 103 104 while (lowerBound < upperBound) { 105 temp = num[lowerBound]; 106 num[lowerBound] = num[upperBound]; 107 num[upperBound] = temp; 108 lowerBound++; 109 upperBound--; 110 } 111 112 return new BigInteger(1, num); 113 } 114 115 /** 116 * Guarantees that the specified number of bytes will be skipped. 117 * @param stream Input Stream. 118 * @param bytes Number of bytes to skip. 119 * @throws IOException Thrown if errors occur. 120 */ 121 public static void skipFully(InputStream stream, long bytes) 122 throws IOException { 123 for (long i = stream.skip(bytes); i < bytes; i += stream.skip(bytes - i)) { 124 /* The loop itself performs all the logic needed to skip. */ 125 } 126 } 90 127 } trunk/src/test/org/lastpod/TrackItemTest.java
r56 r79 34 34 public static Test suite() { 35 35 return new TestSuite(TrackItemTest.class); 36 } 37 38 /** 39 * Tests that the copy constructor works properly. 40 */ 41 public void testCopyConstructor() { 42 TrackItem trackItem2 = new TrackItem(); 43 trackItem2.setTrackid(1); 44 trackItem2.setActive(Boolean.TRUE); 45 trackItem2.setLength(60); 46 trackItem2.setArtist("My Chemical Romance"); 47 trackItem2.setAlbum("The Black Parade"); 48 trackItem2.setTrack("Welcome To The Black Parade"); 49 trackItem2.setPlaycount(1); 50 trackItem2.setLastplayed(1); 51 trackItem2.setParseVariousArtists(true); 52 53 TrackItem trackItem = new TrackItem(trackItem2); 54 55 assertEquals(1, trackItem.getTrackid()); 56 assertEquals(Boolean.TRUE, trackItem.isActive()); 57 assertEquals(60, trackItem.getLength()); 58 assertEquals("My Chemical Romance", trackItem.getArtist()); 59 assertEquals("The Black Parade", trackItem.getAlbum()); 60 assertEquals("Welcome To The Black Parade", trackItem.getTrack()); 61 assertEquals(1, trackItem.getPlaycount()); 62 assertEquals(1, trackItem.getLastplayed()); 63 assertEquals(true, trackItem.isParseVariousArtists()); 64 65 /* Make sure the copy constructor worked properly. */ 66 trackItem2.setArtist("Chris Tilden"); 67 assertEquals("Chris Tilden", trackItem2.getArtist()); 68 assertEquals("My Chemical Romance", trackItem.getArtist()); 36 69 } 37 70
