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")); + } +}