Changeset 79

Show
Ignore:
Timestamp:
06/16/07 00:37:24 (1 year ago)
Author:
chris
Message:

r7798@flan: chris | 2007-06-16 00:35:49 -0700
Completes trac ticket #16:
Multi-play track submissions


So iSproggler will submit the multi-plays for tracks, and I wondered
how that was possible.


It detects the number of plays for a track, and simply submits it some
n times with a small time offset to not trigger spam protection. Not
ideal, but since the iPod db only stores last play and number of
plays, the best solution currently possible I'd think without
seriously hacking the db, and thus the iPod software.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/src/main/org/lastpod/DbReader.java

    r68 r79  
    1919package org.lastpod; 
    2020 
    21 import org.lastpod.util.IoUtils
     21import org.lastpod.parser.TrackItemParser
    2222 
    23 import java.io.BufferedInputStream; 
    24 import java.io.File; 
    25 import java.io.FileInputStream; 
    2623import java.io.IOException; 
    2724import java.io.InputStream; 
     
    3027 
    3128import java.util.ArrayList; 
    32 import java.util.Calendar; 
    33 import java.util.Collections; 
     29import java.util.List; 
    3430 
    3531/** 
     
    4137public class DbReader { 
    4238    /** 
    43      * The location of the iTunes path
     39     * Parses the itunesDatabase
    4440     */ 
    45     private String iTunesPath
     41    private TrackItemParser itunesDbParser
    4642 
    4743    /** 
    48      * The location of the iTunes database file. 
     44     * Parses the Play Counts file. 
    4945     */ 
    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; 
    6647 
    6748    /** 
     
    7051     * reject them.) 
    7152     */ 
    72     private ArrayList recentplays; 
     53    private List recentplays; 
    7354 
    7455    /** 
     
    8263     * Initializes the class with the locations of the iPod DB files. 
    8364     * 
    84      * @param itunespath  Directory containing the iTunesDB and the corresponding 
    85      *                         Play Counts, including trailing "\" or "/". 
    86      * @param parseVariousArtists  If <code>true</code> then parses "Various 
    87      * Artists" 
    8865     */ 
    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; 
    9868        this.recentplays = new ArrayList(); 
    99         this.parseVariousArtists = parseVariousArtists
     69        this.playCountsParser = playCountsParser
    10070    } 
    10171 
     
    10474     * @return Returns recent plays. 
    10575     */ 
    106     public ArrayList getRecentplays() { 
     76    public List getRecentplays() { 
    10777        return recentplays; 
    10878    } 
     
    11181     * Attempts to open and parse the DB & Play Counts files, creating 
    11282     * the appropriate data structures. 
    113      * @throws IOException  Thrown if errors occur. 
    11483     */ 
    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(); 
    12086 
    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(); 
    32989    } 
    33090 
  • trunk/src/main/org/lastpod/ModelImpl.java

    r78 r79  
    1919package org.lastpod; 
    2020 
     21import org.lastpod.parser.ItunesDbParser; 
     22import org.lastpod.parser.PlayCountsParser; 
     23 
    2124import org.lastpod.util.MiscUtilities; 
    22  
    23 import java.io.IOException; 
    2425 
    2526import java.util.ArrayList; 
     
    8081        String iTunesPath = fPrefs.get("iTunes Path", "default"); 
    8182        String parseVariousArtistsStr = fPrefs.get("parseVariousArtists", "1"); 
     83        String parseMultiPlayTracksStr = fPrefs.get("parseMultiPlayTracks", "1"); 
    8284        boolean parseVariousArtists = parseVariousArtistsStr.equals("1") ? true : false; 
     85        boolean parseMultiPlayTracks = parseMultiPlayTracksStr.equals("1") ? true : false; 
    8386 
    8487        if (iTunesPath.equals("default")) { 
     
    8891        } 
    8992 
    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); 
    9196 
    9297        try { 
     
    9499            recentlyPlayed = reader.getRecentplays(); 
    95100            userInterface.newTrackListAvailable(recentlyPlayed); 
    96         } catch (IOException e) { 
     101        } catch (Exception e) { 
    97102            StackTraceElement[] trace = e.getStackTrace(); 
    98103 
  • trunk/src/main/org/lastpod/PreferencesEditor.java

    r71 r79  
    7575    private JTextField backupUrlField; 
    7676    private JCheckBox parseVariousArtistsCheck; 
     77    private JCheckBox parseMultiPlayTracksCheck; 
    7778    private JTextField iTunesfield; 
    7879    private JCheckBox iTCheck; 
     
    237238        String toolTip = 
    238239            "<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."; 
    247251        p3.setToolTipText(toolTip); 
    248252 
     
    266270        p3.add(parseVariousArtistsCheck); 
    267271 
    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); 
    269287        p.add(p3); 
    270288 
     
    374392            parseVariousArtistsCheck.setSelected(false); 
    375393        } 
     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        } 
    376402    } 
    377403 
     
    390416 
    391417        boolean selected = parseVariousArtistsCheck.isSelected(); 
     418        boolean selected2 = parseMultiPlayTracksCheck.isSelected(); 
    392419        String parseVariousArtists = selected ? "1" : "0"; 
     420        String parseMultiPlayTracks = selected2 ? "1" : "0"; 
    393421 
    394422        fPrefs.put("iTunes Path", newItunesPath); 
     
    397425        fPrefs.put("iTunes Status", this.iTunesStatus.getText()); 
    398426        fPrefs.put("parseVariousArtists", parseVariousArtists); 
     427        fPrefs.put("parseMultiPlayTracks", parseMultiPlayTracks); 
    399428 
    400429        try { 
  • trunk/src/main/org/lastpod/TrackItem.java

    r56 r79  
    3737 
    3838    /** 
    39      * Default constructor 
     39     * Default constructor. 
    4040     */ 
    4141    public TrackItem() { 
     
    5151 
    5252    /** 
     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    /** 
    5370     * If <code>true</code>; sets this track to be submitted, otherwise do not 
    5471     * submit. 
  • trunk/src/main/org/lastpod/util/IoUtils.java

    r53 r79  
    2424import java.io.Reader; 
    2525import java.io.Writer; 
     26 
     27import java.math.BigInteger; 
    2628 
    2729/** 
     
    8890        } 
    8991    } 
     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    } 
    90127} 
  • trunk/src/test/org/lastpod/TrackItemTest.java

    r56 r79  
    3434    public static Test suite() { 
    3535        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()); 
    3669    } 
    3770