Index: /trunk/src/test/org/lastpod/parser/ItunesStatsParserTest.java
===================================================================
--- /trunk/src/test/org/lastpod/parser/ItunesStatsParserTest.java (revision 89)
+++ /trunk/src/test/org/lastpod/parser/ItunesStatsParserTest.java (revision 89)
@@ -0,0 +1,118 @@
+/*
+ * LastPod is an application used to publish one's iPod play counts to Last.fm.
+ * Copyright (C) 2007 Chris Tilden
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.lastpod.parser;
+
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+import org.lastpod.TrackItem;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests the ItunesStatsParser, which is responsible for parsing the "iTunesStats"
+ * file. This file is used by the 2nd generation iPod shuffle to store the play
+ * count information. This parser is dependent on an iTunesDB parser to supply
+ * the track information.
+ * @author Chris Tilden
+ */
+public class ItunesStatsParserTest extends TestCase {
+ /**
+ * Returns a JUnit TestSuite for this test case.
+ * @return A JUnit TestSuite for this test case.
+ */
+ public static Test suite() {
+ return new TestSuite(ItunesStatsParserTest.class);
+ }
+
+ /**
+ * Performs the necessary tests.
+ */
+ public void testItunesStatsParser() {
+ TrackItemParser itunesDbParser = new ItunesDbParserTester();
+ ItunesStatsParser itunesStatsParser = new ItunesStatsParser("../src/test", false);
+ itunesStatsParser.setTrackList(itunesDbParser.parse());
+
+ List recentPlays = itunesStatsParser.parse();
+
+ assertTrue(recentPlays.size() == 1);
+
+ TrackItem track = (TrackItem) recentPlays.get(0);
+ assertEquals(track.getLength(), 233);
+ assertEquals(track.getArtist(), "Korn");
+ assertEquals(track.getAlbum(), "Issues");
+ assertEquals(track.getTrack(), "Beg for Me");
+ assertEquals(track.getPlaycount(), 1);
+ assertTrue(track.getLastplayed() != 0);
+ }
+
+ /**
+ * Mocks parsing the iTunes DB file from the iPod and creates a List of
+ * TrackItems. Note: This mock parser is used only for the test in the
+ * parent class.
+ * @author Chris Tilden
+ */
+ private class ItunesDbParserTester implements TrackItemParser {
+ /**
+ * Stores a boolean value that will be passed into TrackItem.
+ */
+ boolean parseVariousArtists;
+
+ /**
+ * Default constructor.
+ */
+ public ItunesDbParserTester() {
+ /* Default constructor. */
+ }
+
+ /**
+ * Performs parsing.
+ * @return A List containing TrackItems. It
+ * contains all tracks from the iTunes database.
+ */
+ public List parse() {
+ List trackList = new ArrayList();
+ TrackItem trackItem = null;
+
+ for (int i = 0; i < 131; i++) {
+ trackItem = new TrackItem();
+ trackList.add(trackItem);
+ }
+
+ trackItem = (TrackItem) trackList.get(0);
+ trackItem.setTrackid(65900);
+ trackItem.setLength(233);
+ trackItem.setArtist("Korn");
+ trackItem.setAlbum("Issues");
+ trackItem.setTrack("Beg for Me");
+
+ return trackList;
+ }
+
+ /**
+ * Does nothing for this implementation.
+ * @param trackList Does nothing for this implementation.
+ */
+ public void setTrackList(List trackList) {
+ /* Do nothing. */
+ }
+ }
+}
Index: /trunk/src/main/org/lastpod/parser/ItunesStatsParser.java
===================================================================
--- /trunk/src/main/org/lastpod/parser/ItunesStatsParser.java (revision 89)
+++ /trunk/src/main/org/lastpod/parser/ItunesStatsParser.java (revision 89)
@@ -0,0 +1,199 @@
+/*
+ * LastPod is an application used to publish one's iPod play counts to Last.fm.
+ * Copyright (C) 2007 Chris Tilden
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.lastpod.parser;
+
+import org.lastpod.TrackItem;
+
+import org.lastpod.util.IoUtils;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Parses the iTunesStats file from the iPod shuffle and creates a
+ * List of TrackItems. Note: the TrackItems returned
+ * contain play count information and things like track title, artist name,
+ * album name, etc.
+ * @author Chris Tilden
+ */
+public class ItunesStatsParser implements TrackItemParser {
+ /**
+ * The location of the iTunes path.
+ */
+ private String iTunesPath;
+
+ /**
+ * The location of the iPod play counts file.
+ */
+ private String iTunesStatsFile;
+
+ /**
+ * Stores a boolean value that will be passed into TrackItem.
+ */
+ boolean parseMultiPlayTracks;
+
+ /**
+ * Contains all tracks from the iTunes database.
+ */
+ private List trackList;
+
+ /**
+ * Default constructor should not be used.
+ */
+ private ItunesStatsParser() {
+ /* Default constructor. */
+ }
+
+ /**
+ * Initializes the class with the locations of the iPod DB files.
+ *
+ * @param iTunesPath Directory containing the iTunesDB and the corresponding
+ * iTunesStats, including trailing "\" or "/".
+ * @param parseMultiPlayTracks If true parses the play count
+ * values. Manufactures additional TrackItems in the recently played list,
+ * in order to properly count tracks that were played more than once.
+ */
+ public ItunesStatsParser(String iTunesPath, boolean parseMultiPlayTracks) {
+ if (!iTunesPath.endsWith(File.separator)) {
+ iTunesPath += File.separator;
+ }
+
+ this.iTunesPath = iTunesPath;
+ this.iTunesStatsFile = iTunesPath + "iTunesStats";
+ this.parseMultiPlayTracks = parseMultiPlayTracks;
+ }
+
+ /**
+ * Sets the List of tracks.
+ * @param trackList The List of tracks.
+ */
+ public void setTrackList(List trackList) {
+ this.trackList = trackList;
+ }
+
+ /**
+ * Performs parsing.
+ * @return A List of complete TrackItems that
+ * were in the play counts file.
+ */
+ public List parse() {
+ if ((trackList == null) || (trackList.size() == 0)) {
+ throw new RuntimeException("Programming error, setTrackList() was never performed!");
+ }
+
+ InputStream playCountsFileIn = null;
+ InputStream playCountsBufferedIn = null;
+
+ try {
+ playCountsFileIn = new FileInputStream(iTunesStatsFile);
+ playCountsBufferedIn = new BufferedInputStream(playCountsFileIn, 65535);
+
+ return parseitunesStats(playCountsBufferedIn);
+ } catch (IOException e) {
+ String errorMsg =
+ "Error reading iTunesStats Database.\n"
+ + "Have you listened to any music on your iPod recently?\n"
+ + "This can also be caused if you are running iTunes and you have it setup "
+ + "to automatically run iTunes when an iPod is detected.";
+ throw new RuntimeException(errorMsg);
+ } finally {
+ IoUtils.cleanup(playCountsFileIn, null);
+ IoUtils.cleanup(playCountsBufferedIn, null);
+ }
+ }
+
+ /**
+ * Parses play counts information from "Play Counts".
+ * @param itunesStatsistream A stream that reads the iPod play counts file.
+ * @return A List of complete TrackItems that
+ * were in the play counts file.
+ * @throws IOException Thrown if errors occur.
+ */
+ private List parseitunesStats(InputStream itunesStatsistream)
+ throws IOException {
+ byte[] threeBytes = new byte[3];
+ List recentPlays = new ArrayList();
+
+ itunesStatsistream.read(threeBytes);
+
+ int numentries = IoUtils.littleEndianToBigInt(threeBytes).intValue();
+
+ IoUtils.skipFully(itunesStatsistream, 3); //skip rest of header
+
+ Calendar calendar = Calendar.getInstance();
+
+ for (int i = 0; i < (numentries - 1); i++) {
+ /* Skip unused data. */
+ IoUtils.skipFully(itunesStatsistream, 12);
+
+ itunesStatsistream.read(threeBytes);
+
+ /* Skip 'skippedcount'. */
+ IoUtils.skipFully(itunesStatsistream, 3);
+
+ long playcount = IoUtils.littleEndianToBigInt(threeBytes).longValue();
+
+ if (playcount > 0) {
+ TrackItem temptrack = (TrackItem) trackList.get(i);
+ temptrack.setPlaycount(playcount);
+ calendar.add(Calendar.SECOND, -(int) temptrack.getLength());
+ temptrack.setLastplayed(calendar.getTimeInMillis() / 1000);
+
+ recentPlays.add(trackList.get(i));
+
+ if (parseMultiPlayTracks && (playcount > 1)) {
+ long numberToManufacture = playcount - 1;
+
+ for (long j = 0; j < numberToManufacture; j++) {
+ temptrack = manufactureTrack(temptrack, calendar);
+ recentPlays.add(temptrack);
+ }
+ }
+ }
+ }
+
+ Collections.sort(recentPlays);
+
+ return recentPlays;
+ }
+
+ /**
+ * Manufactures a Track based on the given Track.
+ * @param temptrack The track to manufacture (if needed).
+ * @return A manufactured TrackItem.
+ */
+ private TrackItem manufactureTrack(TrackItem temptrack, Calendar calendar) {
+ TrackItem manufacturedTrack = new TrackItem(temptrack);
+ calendar.add(Calendar.SECOND, -(int) manufacturedTrack.getLength());
+ manufacturedTrack.setLastplayed(calendar.getTimeInMillis() / 1000);
+
+ manufacturedTrack.setPlaycount(1);
+ temptrack.setPlaycount(1);
+
+ return manufacturedTrack;
+ }
+}
Index: /trunk/src/main/org/lastpod/UI.java
===================================================================
--- /trunk/src/main/org/lastpod/UI.java (revision 83)
+++ /trunk/src/main/org/lastpod/UI.java (revision 89)
@@ -113,5 +113,5 @@
*/
public UI(Model model) {
- frame = new JFrame("LastPod (v0.8)");
+ frame = new JFrame("LastPod (v0.9)");
submitStatus = new JLabel();
Index: /trunk/src/main/org/lastpod/ModelImpl.java
===================================================================
--- /trunk/src/main/org/lastpod/ModelImpl.java (revision 85)
+++ /trunk/src/main/org/lastpod/ModelImpl.java (revision 89)
@@ -20,7 +20,12 @@
import org.lastpod.parser.ItunesDbParser;
+import org.lastpod.parser.ItunesStatsParser;
import org.lastpod.parser.PlayCountsParser;
-
+import org.lastpod.parser.TrackItemParser;
+
+import org.lastpod.util.ItunesStatsFilter;
import org.lastpod.util.MiscUtilities;
+
+import java.io.File;
import java.util.ArrayList;
@@ -95,5 +100,17 @@
ItunesDbParser itunesDbParser =
new ItunesDbParser(iTunesPath, parseVariousArtists, splitVariousArtistStrings);
- PlayCountsParser playCountsParser = new PlayCountsParser(iTunesPath, parseMultiPlayTracks);
+
+ /* Defaults to the parser for non-shuffle iPods. */
+ TrackItemParser playCountsParser = new PlayCountsParser(iTunesPath, parseMultiPlayTracks);
+
+ /* Checks for the "iTunesStats" file. If it exists, switch to the iPod
+ * shuffle parser. */
+ File file = new File(iTunesPath);
+ File[] itunesStatsFiles = file.listFiles(new ItunesStatsFilter());
+
+ if ((itunesStatsFiles != null) && (itunesStatsFiles.length != 0)) {
+ playCountsParser = new ItunesStatsParser(iTunesPath, parseMultiPlayTracks);
+ }
+
DbReader reader = new DbReader(itunesDbParser, playCountsParser);
Index: /trunk/src/main/org/lastpod/action/DeletePlayCounts.java
===================================================================
--- /trunk/src/main/org/lastpod/action/DeletePlayCounts.java (revision 71)
+++ /trunk/src/main/org/lastpod/action/DeletePlayCounts.java (revision 89)
@@ -21,4 +21,6 @@
import org.lastpod.Model;
import org.lastpod.UI;
+
+import org.lastpod.util.ItunesStatsFilter;
import java.awt.event.ActionEvent;
@@ -54,4 +56,11 @@
/**
+ * Stores the file location of the "Play Counts" or "iTunesStats" file.
+ * (For non-shuffle iPods there is a "Play Counts" file, for shuffle
+ * iPods the "iTunesStats" file is used.)
+ */
+ private File playCountsFile;
+
+ /**
* Constructs this action.
* @param userInterface The application's user interface.
@@ -69,4 +78,26 @@
putValue(SHORT_DESCRIPTION, desc);
putValue(MNEMONIC_KEY, new Integer(mnemonic));
+
+ /* Setup Playcounts file; based on either iPod shuffle or non-shuffle.
+ */
+ Preferences fPrefs = Preferences.userRoot().node("ws/afterglo/audioPod");
+ String iTunesPath = fPrefs.get("iTunes Path", "default");
+
+ if (!iTunesPath.endsWith(File.separator)) {
+ iTunesPath += File.separator;
+ }
+
+ /* Defaults the file for non-shuffle iPods. */
+ playCountsFile = new File(iTunesPath + "Play Counts");
+
+ /* Checks for the "iTunesStats" file. If it exists, switch to the iPod
+ * shuffle file. */
+ File file = new File(iTunesPath);
+ File[] itunesStatsFiles = file.listFiles(new ItunesStatsFilter());
+
+ if ((itunesStatsFiles != null) && (itunesStatsFiles.length != 0)) {
+ playCountsFile = new File(iTunesPath + "iTunesStats");
+ putValue(SHORT_DESCRIPTION, "Removes the iTunesStats file from the iPod shuffle.");
+ }
}
@@ -98,5 +129,5 @@
}
- File playCountsFile = new File(iTunesPath + "Play Counts");
+ playCountsFile = new File(iTunesPath + "Play Counts");
boolean succuss = false;
Index: /trunk/src/main/org/lastpod/util/ItunesStatsFilter.java
===================================================================
--- /trunk/src/main/org/lastpod/util/ItunesStatsFilter.java (revision 89)
+++ /trunk/src/main/org/lastpod/util/ItunesStatsFilter.java (revision 89)
@@ -0,0 +1,38 @@
+/*
+ * LastPod is an application used to publish one's iPod play counts to Last.fm.
+ * Copyright (C) 2007 Chris Tilden
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.lastpod.util;
+
+import java.io.File;
+import java.io.FilenameFilter;
+
+/**
+ * Checks for the existence of the file "iTunesStats".
+ * @author Chris Tilden
+ */
+public class ItunesStatsFilter implements FilenameFilter {
+ /**
+ * Returns true if the file "iTunesStats" is present.
+ * @param dir The directory to check.
+ * @param name The name of the file.
+ * @return true if the file is present.
+ */
+ public boolean accept(File dir, String name) {
+ return (name.equals("iTunesStats"));
+ }
+}