root/trunk/src/main/org/lastpod/DbReader.java

Revision 67, 10.6 kB (checked in by chris, 3 years ago)

removed unnecessary this.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1 /*
2  * LastPod is an application used to publish one's iPod play counts to Last.fm.
3  * Copyright (C) 2007  Chris Tilden
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (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, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18  */
19 package org.lastpod;
20
21 import java.io.BufferedInputStream;
22 import java.io.File;
23 import java.io.FileInputStream;
24 import java.io.IOException;
25
26 import java.math.BigInteger;
27
28 import java.util.ArrayList;
29 import java.util.Calendar;
30 import java.util.Collections;
31
32 /**
33  * Reads the iTunes database directly from the iPod.
34  * @author muti
35  * @author Chris Tilden
36  * @version $Id$
37  */
38 public class DbReader {
39     /**
40      * The location of the iTunes path.
41      */
42     private String iTunesPath;
43
44     /**
45      * The location of the iTunes database file.
46      */
47     private String itunesfile;
48
49     /**
50      * The location of the iPod play counts file.
51      */
52     private String playcountsfile;
53
54     /**
55      * A buffered stream that reads the iTunes database file.
56      */
57     private BufferedInputStream itunesistream;
58
59     /**
60      * A buffered stream that reads the iPod play counts file.
61      */
62     private BufferedInputStream playcountsistream;
63
64     /**
65      * A list of all the tracks from the iTunes databaes file.
66      */
67     private ArrayList tracklist;
68
69     /**
70      * Stores a boolean value that will be passed into <code>TrackItem</code>
71      */
72     boolean parseVariousArtists;
73
74     /**
75      * A list of the recently played tracks from the iPod play counts file.
76      * (Sorted by play time. This is important because otherwise Last.fm will
77      * reject them.)
78      */
79     private ArrayList recentplays;
80
81     /**
82      * Default constructor should not be used.
83      */
84     private DbReader() {
85         /* Default constructor. */
86     }
87
88     /**
89      * Initializes the class with the locations of the iPod DB files.
90      *
91      * @param itunespath  Directory containing the iTunesDB and the corresponding
92      *                         Play Counts, including trailing "\" or "/".
93      */
94     public DbReader(String itunespath, boolean parseVariousArtists) {
95         if (!itunespath.endsWith(File.separator)) {
96             itunespath += File.separator;
97         }
98
99         this.iTunesPath = itunespath;
100         this.itunesfile = itunespath + "iTunesDB";
101         this.playcountsfile = itunespath + "Play Counts";
102         this.tracklist = new ArrayList();
103         this.recentplays = new ArrayList();
104         this.parseVariousArtists = parseVariousArtists;
105     }
106
107     /**
108      * Gets the recent plays.
109      * @return Returns recent plays.
110      */
111     public ArrayList getRecentplays() {
112         return recentplays;
113     }
114
115     /**
116      * Attempts to open and parse the DB & Play Counts files, creating
117      * the appropriate data structures.
118      * @throws IOException  Thrown if errors occur.
119      */
120     public void parse() throws IOException {
121         try {
122             FileInputStream itstream = new FileInputStream(itunesfile);
123             itunesistream = new BufferedInputStream(itstream, 65535);
124         } catch (IOException e) {
125             throw new IOException("Error reading iTunes Database");
126         }
127
128         try {
129             FileInputStream pcstream = new FileInputStream(playcountsfile);
130             playcountsistream = new BufferedInputStream(pcstream, 65535);
131         } catch (IOException e) {
132             String errorMsg =
133                 "Error reading Play Counts Database.\n"
134                 + "Have you listened to any music on your iPod recently?\n"
135                 + "This can also be caused if you are running iTunes and you have it setup "
136                 + "to automatically run iTunes when an iPod is detected.";
137             throw new IOException(errorMsg);
138         }
139
140         parseitunesdb();
141         parseplaycounts();
142
143         itunesistream.close();
144         playcountsistream.close();
145     }
146
147     /**
148      * Parses track information from the iTunesDB.
149      * @throws IOException  Thrown if errors occur.
150      */
151     public void parseitunesdb() throws IOException {
152         byte[] buf = new byte[1];
153
154         //we seek one at a time because the mhit marker won't always be at a multiple of four
155         while (itunesistream.read(buf) != -1) {
156             if (buf[0] == 'm') { //Search for MHIT
157                 itunesistream.mark(1048576);
158                 buf = new byte[3];
159                 itunesistream.read(buf);
160
161                 if (new String(buf).equals("hit")) {
162                     tracklist.add(parsemhit());
163                 } else {
164                     itunesistream.reset();
165                 }
166             }
167
168             buf = new byte[1];
169         }
170     }
171
172     /**
173      * Parses an MHIT object from the iTunes Database.
174      * @return Returns parsed track object.
175      * @throws IOException  Thrown if errors occur.
176      */
177     public TrackItem parsemhit() throws IOException {
178         byte[] dword = new byte[4];
179         TrackItem track = new TrackItem();
180         track.setParseVariousArtists(parseVariousArtists);
181
182         itunesistream.mark(1048576); //mark beginning of MHIT location
183
184         itunesistream.read(dword);
185
186         long headersize = DbReader.littleEndianToBigInt(dword).longValue();
187
188         DbReader.skipFully(itunesistream, 4);
189         itunesistream.read(dword);
190
191         long nummhods = DbReader.littleEndianToBigInt(dword).longValue();
192
193         itunesistream.read(dword);
194         track.setTrackid(DbReader.littleEndianToBigInt(dword).longValue());
195
196         DbReader.skipFully(itunesistream, 20);
197         itunesistream.read(dword);
198         track.setLength(DbReader.littleEndianToBigInt(dword).longValue() / 1000);
199
200         itunesistream.reset();
201         DbReader.skipFully(itunesistream, headersize - 4); //skip to end of MHIT
202
203         for (long i = 0; i < nummhods; i++) {
204             parsemhod(track);
205         }
206
207         return track;
208     }
209
210     /**
211      * Parses an MHOD object and sets proper fields in the track item object.
212      * @param track  Track Item.
213      * @throws IOException  Thrown if errors occur.
214      */
215     public void parsemhod(TrackItem track) throws IOException {
216         byte[] dword = new byte[4];
217
218         itunesistream.mark(1048576); //mark beginning of MHOD location
219
220         DbReader.skipFully(itunesistream, 8);
221
222         itunesistream.read(dword);
223
224         long totalsize = DbReader.littleEndianToBigInt(dword).longValue();
225
226         itunesistream.read(dword);
227
228         int mhodtype = DbReader.littleEndianToBigInt(dword).intValue();
229
230         if ((mhodtype == 1) || (mhodtype == 3) || (mhodtype == 4)) {
231             DbReader.skipFully(itunesistream, 12);
232             itunesistream.read(dword);
233
234             int strlen = DbReader.littleEndianToBigInt(dword).intValue();
235
236             DbReader.skipFully(itunesistream, 8);
237
238             byte[] data = new byte[strlen];
239             itunesistream.read(data);
240
241             String stringdata = new String(data, "UTF-16LE");
242
243             switch (mhodtype) {
244             case 1:
245                 track.setTrack(stringdata);
246
247                 break;
248
249             case 3:
250                 track.setAlbum(stringdata);
251
252                 break;
253
254             case 4:
255                 track.setArtist(stringdata);
256
257                 break;
258             }
259         }
260
261         itunesistream.reset();
262         DbReader.skipFully(itunesistream, totalsize);
263     }
264
265     /**
266      * Parses play counts information from "Play Counts".
267      * @throws IOException  Thrown if errors occur.
268      */
269     public void parseplaycounts() throws IOException {
270         byte[] dword = new byte[4];
271
272         DbReader.skipFully(playcountsistream, 8);
273         playcountsistream.read(dword);
274
275         long entrylen = DbReader.littleEndianToBigInt(dword).longValue();
276
277         playcountsistream.read(dword);
278
279         int numentries = DbReader.littleEndianToBigInt(dword).intValue();
280
281         DbReader.skipFully(playcountsistream, 80); //skip rest of header
282
283         for (int i = 0; i < (numentries - 1); i++) {
284             playcountsistream.mark(1048576); //save beginning of entry location
285
286             playcountsistream.read(dword);
287
288             long playcount = DbReader.littleEndianToBigInt(dword).longValue();
289
290             if (playcount > 0) {
291                 playcountsistream.read(dword);
292
293                 long lastplayed = DbReader.littleEndianToBigInt(dword).longValue();
294                 lastplayed -= 2082844800; //convert to UNIX timestamp
295
296                 Calendar calendar = Calendar.getInstance();
297                 long offset =
298                     calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET);
299                 lastplayed -= (offset / 1000);
300
301                 TrackItem temptrack = (TrackItem) tracklist.get(i);
302                 temptrack.setPlaycount(playcount);
303                 temptrack.setLastplayed(lastplayed - temptrack.getLength());
304
305                 if (History.getInstance(iTunesPath).isInHistory(temptrack.getLastplayed())) {
306                     temptrack.setActive(Boolean.FALSE);
307                 }
308
309                 recentplays.add(tracklist.get(i));
310             }
311
312             playcountsistream.reset();
313             DbReader.skipFully(playcountsistream, entrylen);
314         }
315
316         Collections.sort(recentplays);
317     }
318
319     /**
320      * This converts any size byte array to a BigInteger.
321      * @param num  Little-Endian byte array.
322      * @return A BigInt.
323      */
324     public static BigInteger littleEndianToBigInt(byte[] num) {
325         byte temp;
326
327         int upperBound = num.length - 1;
328         int lowerBound = 0;
329
330         while (lowerBound < upperBound) {
331             temp = num[lowerBound];
332             num[lowerBound] = num[upperBound];
333             num[upperBound] = temp;
334             lowerBound++;
335             upperBound--;
336         }
337
338         return new BigInteger(1, num);
339     }
340
341     /**
342      * Guarantees that the specified number of bytes will be skipped.
343      * @param stream Input Stream.
344      * @param bytes Number of bytes to skip.
345      * @throws IOException  Thrown if errors occur.
346      */
347     public static void skipFully(BufferedInputStream stream, long bytes)
348             throws IOException {
349         for (long i = stream.skip(bytes); i < bytes; i += stream.skip(bytes - i)) {
350             /* The loop itself performs all the logic needed to skip. */
351         }
352     }
353 }
Note: See TracBrowser for help on using the browser.