001/*
002 * Copyright (c) 2012, the Last.fm Java Project and Committers
003 * All rights reserved.
004 *
005 * Redistribution and use of this software in source and binary forms, with or without modification, are
006 * permitted provided that the following conditions are met:
007 *
008 * - Redistributions of source code must retain the above
009 *   copyright notice, this list of conditions and the
010 *   following disclaimer.
011 *
012 * - Redistributions in binary form must reproduce the above
013 *   copyright notice, this list of conditions and the
014 *   following disclaimer in the documentation and/or other
015 *   materials provided with the distribution.
016 *
017 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
018 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
019 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
020 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
021 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
022 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
023 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
024 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
025 */
026package de.umass.lastfm;
027
028import java.util.ArrayList;
029import java.util.Collection;
030import java.util.Collections;
031import java.util.Date;
032import java.util.HashMap;
033import java.util.List;
034import java.util.Locale;
035import java.util.Map;
036
037import de.umass.lastfm.scrobble.IgnoredMessageCode;
038import de.umass.lastfm.scrobble.ScrobbleData;
039import de.umass.lastfm.scrobble.ScrobbleResult;
040import de.umass.util.MapUtilities;
041import de.umass.util.StringUtilities;
042import de.umass.xml.DomElement;
043
044/**
045 * Bean that contains information related to <code>Track</code>s and provides bindings to methods
046 * in the <code>track.</code> namespace.
047 *
048 * @author Janni Kovacs
049 */
050public class Track extends MusicEntry {
051
052  private enum ScrobbleResultType {
053    NOW_PLAYING,
054    SINGLE_SCROBBLE,
055    MULTIPLE_SCROBBLES
056  }
057
058  static final ItemFactory<Track> FACTORY = new TrackFactory();
059
060  public static final String ARTIST_PAGE = "artistpage";
061  public static final String ALBUM_PAGE = "albumpage";
062  public static final String TRACK_PAGE = "trackpage";
063
064  private String artist;
065  private String artistMbid;
066
067  protected String album;               // protected for use in Playlist.playlistFromElement
068  private String albumMbid;
069  private int position = -1;
070
071  private boolean fullTrackAvailable;
072  private boolean nowPlaying;
073
074  private Date playedWhen;
075  protected int duration;               // protected for use in Playlist.playlistFromElement
076  protected String location;            // protected for use in Playlist.playlistFromElement
077  //protected int playcount = 0;
078
079  protected Map<String, String> lastFmExtensionInfos = new HashMap<String, String>();           // protected for use in Playlist.playlistFromElement
080
081
082  protected Track(String name, String url, String artist) {
083    super(name, url);
084    this.artist = artist;
085  }
086
087  protected Track(String name, String url, String mbid, int playcount, int listeners, boolean streamable,
088          String artist, String artistMbid, boolean fullTrackAvailable, boolean nowPlaying) {
089    super(name, url, mbid, playcount, listeners, streamable);
090    this.artist = artist;
091    this.artistMbid = artistMbid;
092    this.fullTrackAvailable = fullTrackAvailable;
093    this.nowPlaying = nowPlaying;
094  }
095
096  /**
097   * Returns the duration of the song, if available, in seconds. The duration attribute is only available
098   * for tracks retrieved by {@link Playlist#fetch(String, String) Playlist.fetch} and
099   * {@link Track#getInfo(String, String, String) Track.getInfo}.
100   *
101   * @return duration in seconds
102   */
103  public int getDuration() {
104    return duration;
105  }
106
107  public String getArtist() {
108    return artist;
109  }
110
111  public String getArtistMbid() {
112    return artistMbid;
113  }
114
115  public String getAlbum() {
116    return album;
117  }
118
119  public String getAlbumMbid() {
120    return albumMbid;
121  }
122
123  public boolean isFullTrackAvailable() {
124    return fullTrackAvailable;
125  }
126
127  public boolean isNowPlaying() {
128    return nowPlaying;
129  }
130
131  /**
132    * Set Method for class field 'playcount'.
133    *
134    * @param playcount is the value to set this class field to.
135    *
136    **/
137/*  public  void setPlaycount(int playcount)
138  {
139    this.playcount = playcount;
140  } */ // setPlaycount Method
141
142
143  /**
144    * Get Method for class field 'playcount'.
145    *
146    * @return int - The value the class field 'playcount'.
147    *
148    **/
149/*  public int getPlaycount()
150  {
151    return playcount;
152  } */  // getPlaycount Method
153
154
155  /**
156   * Returns the location (URL) of this Track. This information is only available with the {@link Radio} services.
157   *
158   * @return the location
159   */
160  public String getLocation() {
161    return location;
162  }
163
164  /**
165   * Returns last.fm specific information about this Track. Only available in Tracks fetched from
166   * radio playlists. <tt>key</tt> can be one of the following:
167   * <ul>
168   * <li>artistpage</li>
169   * <li>albumpage</li>
170   * <li>trackpage</li>
171   * <li>buyTrackURL</li>
172   * <li>buyAlbumURL</li>
173   * <li>freeTrackURL</li>
174   * </ul>
175   * Or use the available constants in this class.<br/>
176   * Note that the key string is case sensitive.
177   *
178   * @param key A key
179   * @return associated value
180   * @see #ARTIST_PAGE
181   * @see #ALBUM_PAGE
182   * @see #TRACK_PAGE
183   */
184  public String getLastFmInfo(String key) {
185    return lastFmExtensionInfos.get(key);
186  }
187
188  /**
189   * Returns the time when the track was played, if this data is available (e.g. for recent tracks) or <code>null</code>,
190   * if this data is not available.<br/>
191   *
192   * @return the date when the track was played or <code>null</code>
193   */
194  public Date getPlayedWhen() {
195    return playedWhen;
196  }
197
198  /**
199   * Returns the position of this track in its associated album, or -1 if not available.
200   *
201   * @return the album position
202   */
203  public int getPosition() {
204    return position;
205  }
206
207  /**
208   * Searches for a track with the given name and returns a list of possible matches.
209   *
210   * @param track Track name
211   * @param apiKey The API key
212   * @return a list of possible matches
213   * @see #search(String, String, int, String)
214   */
215  public static Collection<Track> search(String track, String apiKey) {
216    return search(null, track, 30, apiKey);
217  }
218
219  /**
220   * Searches for a track with the given name and returns a list of possible matches.
221   * Specify an artist name or a limit to narrow down search results.
222   * Pass <code>null</code> for the artist parameter if you want to specify a limit but don't want
223   * to define an artist.
224   *
225   * @param artist Artist's name or <code>null</code>
226   * @param track Track name
227   * @param limit Number of maximum results
228   * @param apiKey The API key
229   * @return a list of possible matches
230   */
231  public static Collection<Track> search(String artist, String track, int limit, String apiKey) {
232    Map<String, String> params = new HashMap<String, String>();
233    params.put("track", track);
234    params.put("limit", String.valueOf(limit));
235    MapUtilities.nullSafePut(params, "artist", artist);
236    Result result = Caller.getInstance().call("track.search", apiKey, params);
237    if(!result.isSuccessful())
238      return Collections.emptyList();
239    DomElement element = result.getContentElement();
240    DomElement matches = element.getChild("trackmatches");
241    return ResponseBuilder.buildCollection(matches, Track.class);
242  }
243
244  /**
245   * Retrieves the top tags for the given track. You either have to specify a track and artist name or
246   * a mbid. If you specify an mbid you may pass <code>null</code> for the first parameter.
247   *
248   * @param artist Artist name or <code>null</code> if an MBID is specified
249   * @param trackOrMbid Track name or MBID
250   * @param apiKey The API key
251   * @return list of tags
252   */
253  public static Collection<Tag> getTopTags(String artist, String trackOrMbid, String apiKey) {
254    Map<String, String> params = new HashMap<String, String>();
255    if (StringUtilities.isMbid(trackOrMbid)) {
256      params.put("mbid", trackOrMbid);
257    } else {
258      params.put("artist", artist);
259      params.put("track", trackOrMbid);
260    }
261    Result result = Caller.getInstance().call("track.getTopTags", apiKey, params);
262    return ResponseBuilder.buildCollection(result, Tag.class);
263  }
264
265  /**
266   * Retrieves the top fans for the given track. You either have to specify a track and artist name or
267   * a mbid. If you specify an mbid you may pass <code>null</code> for the first parameter.
268   *
269   * @param artist Artist name or <code>null</code> if an MBID is specified
270   * @param trackOrMbid Track name or MBID
271   * @param apiKey The API key
272   * @return list of fans
273   */
274  public static Collection<User> getTopFans(String artist, String trackOrMbid, String apiKey) {
275    Map<String, String> params = new HashMap<String, String>();
276    if (StringUtilities.isMbid(trackOrMbid)) {
277      params.put("mbid", trackOrMbid);
278    } else {
279      params.put("artist", artist);
280      params.put("track", trackOrMbid);
281    }
282    Result result = Caller.getInstance().call("track.getTopFans", apiKey, params);
283    return ResponseBuilder.buildCollection(result, User.class);
284  }
285
286  /**
287   * Tag an album using a list of user supplied tags.
288   *
289   * @param artist The artist name in question
290   * @param track The track name in question
291   * @param tags A comma delimited list of user supplied tags to apply to this track. Accepts a maximum of 10 tags.
292   * @param session A Session instance.
293   * @return the Result of the operation
294   */
295  public static Result addTags(String artist, String track, String tags, Session session) {
296    return Caller.getInstance().call("track.addTags", session, "artist", artist, "track", track, "tags", tags);
297  }
298
299  /**
300   * Remove a user's tag from a track.
301   *
302   * @param artist The artist name in question
303   * @param track The track name in question
304   * @param tag A single user tag to remove from this track.
305   * @param session A Session instance.
306   * @return the Result of the operation
307   */
308  public static Result removeTag(String artist, String track, String tag, Session session) {
309    return Caller.getInstance().call("track.removeTag", session, "artist", artist, "track", track, "tag", tag);
310  }
311
312  /**
313   * Share a track twith one or more Last.fm users or other friends.
314   *
315   * @param artist An artist name.
316   * @param track A track name.
317   * @param message A message to send with the recommendation or <code>null</code>. If not supplied a default message will be used.
318   * @param recipient A comma delimited list of email addresses or Last.fm usernames. Maximum is 10.
319   * @param session A Session instance
320   * @return the Result of the operation
321   */
322  public static Result share(String artist, String track, String message, String recipient, Session session) {
323    Map<String, String> params = StringUtilities.map("artist", artist, "track", track, "recipient", recipient);
324    MapUtilities.nullSafePut(params, "message", message);
325    return Caller.getInstance().call("track.share", session, params);
326  }
327
328
329  /**
330   * Love a track for a user profile.
331   *
332   * @param artist An artist name
333   * @param track A track name
334   * @param session A Session instance
335   * @return the Result of the operation
336   */
337  public static Result love(String artist, String track, Session session) {
338    return Caller.getInstance().call("track.love", session, "artist", artist, "track", track);
339  }
340
341
342  /**
343   * UnLove a track for a user profile.
344   *
345   * @param artist An artist name
346   * @param track A track name
347   * @param session A Session instance
348   * @return the Result of the operation
349   */
350  public static Result unlove(String artist, String track, Session session) {
351    return Caller.getInstance().call("track.unlove", session, "artist", artist, "track", track);
352  }
353
354
355  /**
356   * Ban a track for a given user profile.
357   *
358   * @param artist An artist name
359   * @param track A track name
360   * @param session A Session instance
361   * @return the Result of the operation
362   */
363  public static Result ban(String artist, String track, Session session) {
364    return Caller.getInstance().call("track.ban", session, "artist", artist, "track", track);
365  }
366
367  /**
368   * UnBan a track for a given user profile.
369   *
370   * @param artist An artist name
371   * @param track A track name
372   * @param session A Session instance
373   * @return the Result of the operation
374   */
375  public static Result unban(String artist, String track, Session session) {
376    return Caller.getInstance().call("track.unban", session, "artist", artist, "track", track);
377  }
378
379  /**
380   * Get the similar tracks for this track on Last.fm, based on listening data.<br/>
381   * You have to provide either an artist and a track name <i>or</i> an mbid. Pass <code>null</code>
382   * for parameters you don't need.
383   *
384   * @param artist The artist name in question
385   * @param trackOrMbid The track name in question or the track's MBID
386   * @param apiKey A Last.fm API key.
387   * @return a list of similar <code>Track</code>s
388   */
389  public static Collection<Track> getSimilar(String artist, String trackOrMbid, String apiKey) {
390    return getSimilar(artist, trackOrMbid, apiKey, 100);
391  }
392
393
394  /**
395   * Get the similar tracks for this track on Last.fm, based on listening data.<br/>
396   * You have to provide either an artist and a track name <i>or</i> an mbid. Pass <code>null</code>
397   * for parameters you don't need.
398   *
399   * @param artist The artist name in question
400   * @param trackOrMbid The track name in question or the track's MBID
401   * @param apiKey A Last.fm API key.
402   * @param limit number of results to return
403   * @return a list of similar <code>Track</code>s
404   */
405  public static Collection<Track> getSimilar(String artist, String trackOrMbid, String apiKey, int limit) {
406    Map<String, String> params = new HashMap<String, String>();
407    if (StringUtilities.isMbid(trackOrMbid)) {
408      params.put("mbid", trackOrMbid);
409    } else {
410      params.put("artist", artist);
411      params.put("track", trackOrMbid);
412    }
413
414    if (limit > 0){
415      params.put("limit", "" + limit);
416    }
417    Result result = Caller.getInstance().call("track.getSimilar", apiKey, params);
418    return ResponseBuilder.buildCollection(result, Track.class);
419  }
420
421  /**
422   * Get the tags applied by an individual user to an track on Last.fm.
423   *
424   * @param artist The artist name in question
425   * @param track The track name in question
426   * @param session A Session instance
427   * @return a list of tags
428   */
429  public static Collection<String> getTags(String artist, String track, Session session) {
430    Result result = Caller.getInstance().call("track.getTags", session, "artist", artist, "track", track);
431    DomElement element = result.getContentElement();
432    Collection<String> tags = new ArrayList<String>();
433    for (DomElement domElement : element.getChildren("tag")) {
434      tags.add(domElement.getChildText("name"));
435    }
436    return tags;
437  }
438
439  /**
440   * Get the metadata for a track on Last.fm using the artist/track name or a musicbrainz id.
441   *
442   * @param artist The artist name in question or <code>null</code> if an mbid is specified
443   * @param trackOrMbid The track name in question or the musicbrainz id for the track
444   * @param apiKey A Last.fm API key.
445   * @return Track information
446   */
447  public static Track getInfo(String artist, String trackOrMbid, String apiKey) {
448    return getInfo(artist, trackOrMbid, null, null, apiKey);
449  }
450
451  /**
452   * Get the metadata for a track on Last.fm using the artist/track name or a musicbrainz id.
453   *
454   * @param artist The artist name in question or <code>null</code> if an mbid is specified
455   * @param trackOrMbid The track name in question or the musicbrainz id for the track
456   * @param locale The language to fetch info in, or <code>null</code>
457   * @param username The username for the context of the request, or <code>null</code>. If supplied, the user's playcount for this track and whether they have loved the track is included in the response
458   * @param apiKey A Last.fm API key.
459   * @return Track information
460   */
461  public static Track getInfo(String artist, String trackOrMbid, Locale locale, String username, String apiKey) {
462    Map<String, String> params = new HashMap<String, String>();
463    if (StringUtilities.isMbid(trackOrMbid)) {
464      params.put("mbid", trackOrMbid);
465    } else {
466      params.put("artist", artist);
467      params.put("track", trackOrMbid);
468    }
469    if (locale != null && locale.getLanguage().length() != 0) {
470      params.put("lang", locale.getLanguage());
471    }
472    MapUtilities.nullSafePut(params, "username", username);
473    Result result = Caller.getInstance().call("track.getInfo", apiKey, params);
474    if (!result.isSuccessful())
475      return null;
476    DomElement content = result.getContentElement();
477    DomElement album = content.getChild("album");
478    Track track = FACTORY.createItemFromElement(content);
479    if (album != null) {
480      String pos = album.getAttribute("position");
481      if ((pos != null) && pos.length() != 0) {
482        track.position = Integer.parseInt(pos);
483      }
484      track.album = album.getChildText("title");
485      track.albumMbid = album.getChildText("mbid");
486      ImageHolder.loadImages(track, album);
487    }
488    return track;
489  }
490
491  /**
492   * Get a list of Buy Links for a particular Track. It is required that you supply either the artist and track params or the mbid param.
493   *
494   * @param artist The artist name in question
495   * @param albumOrMbid Track name or MBID
496   * @param country A country name, as defined by the ISO 3166-1 country names standard
497   * @param apiKey A Last.fm API key
498   * @return a Collection of {@link BuyLink}s
499   */
500  public static Collection<BuyLink> getBuylinks(String artist, String albumOrMbid, String country, String apiKey) {
501    Map<String, String> params = new HashMap<String, String>();
502    if (StringUtilities.isMbid(albumOrMbid)) {
503      params.put("mbid", albumOrMbid);
504    } else {
505      params.put("artist", artist);
506      params.put("album", albumOrMbid);
507    }
508    params.put("country", country);
509    Result result = Caller.getInstance().call("track.getBuylinks", apiKey, params);
510    if (!result.isSuccessful())
511      return Collections.emptyList();
512    DomElement element = result.getContentElement();
513    DomElement physicals = element.getChild("physicals");
514    DomElement downloads = element.getChild("downloads");
515    Collection<BuyLink> links = new ArrayList<BuyLink>();
516    for (DomElement e : physicals.getChildren("affiliation")) {
517      links.add(BuyLink.linkFromElement(BuyLink.StoreType.PHYSICAl, e));
518    }
519    for (DomElement e : downloads.getChildren("affiliation")) {
520      links.add(BuyLink.linkFromElement(BuyLink.StoreType.DIGITAL, e));
521    }
522    return links;
523  }
524
525  /**
526   * Use the last.fm corrections data to check whether the supplied track has a correction to a canonical track. This method returns a new
527   * {@link Track} object containing the corrected data, or <code>null</code> if the supplied Artist/Track combination was not found.
528   *
529   * @param artist The artist name to correct
530   * @param track The track name to correct
531   * @param apiKey A Last.fm API key
532   * @return a new {@link Track}, or <code>null</code>
533   */
534  public static Track getCorrection(String artist, String track, String apiKey) {
535    Result result = Caller.getInstance().call("track.getCorrection", apiKey, "artist", artist, "track", track);
536    if (!result.isSuccessful())
537      return null;
538    DomElement correctionElement = result.getContentElement().getChild("correction");
539    if (correctionElement == null)
540      return new Track(track, null, artist);
541    DomElement trackElem = correctionElement.getChild("track");
542    return FACTORY.createItemFromElement(trackElem);
543  }
544
545  /**
546   * Get shouts for a track.
547   *
548   * @param artist The artist name
549   * @param trackOrMbid The track name or a mausicbrainz id
550   * @param apiKey A Last.fm API key.
551   * @return a page of <code>Shout</code>s
552   */
553  public static PaginatedResult<Shout> getShouts(String artist, String trackOrMbid, String apiKey) {
554    return getShouts(artist, trackOrMbid, -1, -1, apiKey);
555  }
556
557  /**
558   * Get shouts for a track.
559   *
560   * @param artist The artist name
561   * @param trackOrMbid The track name or a mausicbrainz id
562   * @param page The page number to fetch
563   * @param apiKey A Last.fm API key.
564   * @return a page of <code>Shout</code>s
565   */
566  public static PaginatedResult<Shout> getShouts(String artist, String trackOrMbid, int page, String apiKey) {
567    return getShouts(artist, trackOrMbid, page, -1, apiKey);
568  }
569
570  /**
571   * Get shouts for a track.
572   *
573   * @param artist The artist name
574   * @param trackOrMbid The track name or a mausicbrainz id
575   * @param page The page number to fetch
576   * @param limit An integer used to limit the number of shouts returned per page or -1 for default
577   * @param apiKey A Last.fm API key.
578   * @return a page of <code>Shout</code>s
579   */
580  public static PaginatedResult<Shout> getShouts(String artist, String trackOrMbid, int page, int limit, String apiKey) {
581    Map<String, String> params = new HashMap<String, String>();
582    if (StringUtilities.isMbid(trackOrMbid)) {
583      params.put("mbid", trackOrMbid);
584    } else {
585      params.put("artist", artist);
586      params.put("track", trackOrMbid);
587    }
588    MapUtilities.nullSafePut(params, "limit", limit);
589    MapUtilities.nullSafePut(params, "page", page);
590    Result result = Caller.getInstance().call("track.getShouts", apiKey, params);
591    return ResponseBuilder.buildPaginatedResult(result, Shout.class);
592  }
593
594  /**
595   * Converts a generic web services Result object into a more specific ScrobbleResult.
596   *
597   * @param result Web services Result.
598   * @param scrobbleResultType The type of scrobble result contained within the Result.
599   * @return A ScrobbleResult containing the original Result information plus extra fields specific to scrobble and now playing results.
600   */
601  private static List<ScrobbleResult> convertToScrobbleResults(Result result, ScrobbleResultType scrobbleResultType) {
602    List<ScrobbleResult> scrobbleResults = new ArrayList<ScrobbleResult>();
603    if (!result.isSuccessful()) {
604      // if result failed then we have no extra information
605      ScrobbleResult scrobbleResult = new ScrobbleResult(result);
606      scrobbleResults.add(scrobbleResult);
607    } else {
608      DomElement element = result.getContentElement();
609      if (scrobbleResultType == ScrobbleResultType.NOW_PLAYING) {
610        ScrobbleResult scrobbleResult = new ScrobbleResult(result);
611        parseIntoScrobbleResult(element, scrobbleResult);
612        scrobbleResults.add(scrobbleResult);
613      } else if (scrobbleResultType == ScrobbleResultType.SINGLE_SCROBBLE) {
614        ScrobbleResult scrobbleResult = new ScrobbleResult(result);
615        parseIntoScrobbleResult(element.getChild("scrobble"), scrobbleResult);
616        scrobbleResults.add(scrobbleResult);
617      } else if (scrobbleResultType == ScrobbleResultType.MULTIPLE_SCROBBLES) {
618        for (DomElement scrobbleElement : element.getChildren("scrobble")) {
619          ScrobbleResult scrobbleResult = new ScrobbleResult(result);
620          parseIntoScrobbleResult(scrobbleElement, scrobbleResult);
621          scrobbleResults.add(scrobbleResult);
622        }
623      }
624    }
625    return scrobbleResults;
626  }
627
628  /**
629   * Parses a DomElement containing scrobble or now playing response data into the passed ScrobbleResult.
630   *
631   * @param scrobbleElement DomElement containing scrobble or now playing response data.
632   * @param scrobbleResult ScrobbleResult to add the response data to.
633   */
634  private static void parseIntoScrobbleResult(DomElement scrobbleElement, ScrobbleResult scrobbleResult) {
635    DomElement trackElement = scrobbleElement.getChild("track");
636    scrobbleResult.setTrack(trackElement.getText());
637    scrobbleResult.setArtistCorrected(StringUtilities.convertToBoolean(trackElement.getAttribute("corrected")));
638
639    DomElement artistElement = scrobbleElement.getChild("artist");
640    scrobbleResult.setArtist(artistElement.getText());
641    scrobbleResult.setArtistCorrected(StringUtilities.convertToBoolean(artistElement.getAttribute("corrected")));
642
643    DomElement albumElement = scrobbleElement.getChild("album");
644    scrobbleResult.setAlbum(albumElement.getText());
645    scrobbleResult.setAlbumCorrected(StringUtilities.convertToBoolean(albumElement.getAttribute("corrected")));
646
647    DomElement albumArtistElement = scrobbleElement.getChild("albumArtist");
648    scrobbleResult.setAlbumArtist(albumArtistElement.getText());
649    scrobbleResult.setAlbumArtistCorrected(StringUtilities.convertToBoolean(albumArtistElement.getAttribute("corrected")));
650
651    String timeString = scrobbleElement.getChildText("timestamp");
652    if (timeString != null) {
653      // will be non-null for scrobble results only
654      scrobbleResult.setTimestamp(Integer.parseInt(timeString));
655    }
656
657    DomElement ignoredMessageElement = scrobbleElement.getChild("ignoredMessage");
658    int ignoredMessageCode = Integer.parseInt(ignoredMessageElement.getAttribute("code"));
659    if (ignoredMessageCode > 0) {
660      scrobbleResult.setIgnored(true);
661      scrobbleResult.setIgnoredMessageCode(IgnoredMessageCode.valueOfCode(ignoredMessageCode));
662      scrobbleResult.setIgnoredMessage(ignoredMessageElement.getText());
663    }
664  }
665
666  public static ScrobbleResult scrobble(ScrobbleData scrobbleData, Session session) {
667    Map<String, String> params = new HashMap<String, String>();
668    // required params
669    params.put("artist", scrobbleData.getArtist());
670    params.put("track", scrobbleData.getTrack());
671    params.put("timestamp", String.valueOf(scrobbleData.getTimestamp()));
672    // optional params
673    MapUtilities.nullSafePut(params, "album", scrobbleData.getAlbum());
674    MapUtilities.nullSafePut(params, "albumArtist", scrobbleData.getAlbumArtist());
675    MapUtilities.nullSafePut(params, "duration", scrobbleData.getDuration());
676    MapUtilities.nullSafePut(params, "mbid", scrobbleData.getMusicBrainzId());
677    MapUtilities.nullSafePut(params, "trackNumber", scrobbleData.getTrackNumber());
678    MapUtilities.nullSafePut(params, "streamId", scrobbleData.getStreamId());
679    params.put("chosenByUser", StringUtilities.convertFromBoolean(scrobbleData.isChosenByUser()));
680
681    Result result = Caller.getInstance().call("track.scrobble", session, params);
682    return convertToScrobbleResults(result, ScrobbleResultType.SINGLE_SCROBBLE).get(0);
683  }
684
685  public static ScrobbleResult scrobble(String artistName, String trackName, int timestamp, Session session) {
686    ScrobbleData scrobbleData = new ScrobbleData(artistName, trackName, timestamp);
687    return scrobble(scrobbleData, session);
688  }
689
690  public static ScrobbleResult scrobble(String artistName, String albumName, String trackName, int timestamp, Session session) {
691    ScrobbleData scrobbleData = new ScrobbleData(artistName, albumName, trackName, timestamp);
692    return scrobble(scrobbleData, session);
693  }
694
695  public static List<ScrobbleResult> scrobble(List<ScrobbleData> scrobbleData, Session session) {
696    Map<String, String> params = new HashMap<String, String>();
697    for (int i = 0; i < scrobbleData.size(); i++) {
698      ScrobbleData scrobble = scrobbleData.get(i);
699      // required params
700      params.put("artist[" + i + "]", scrobble.getArtist());
701      params.put("track[" + i + "]", scrobble.getTrack());
702      params.put("timestamp[" + i + "]", String.valueOf(scrobble.getTimestamp()));
703      // optional params
704      MapUtilities.nullSafePut(params, "album[" + i + "]", scrobble.getAlbum());
705      MapUtilities.nullSafePut(params, "albumArtist[" + i + "]", scrobble.getAlbumArtist());
706      MapUtilities.nullSafePut(params, "duration[" + i + "]", scrobble.getDuration());
707      MapUtilities.nullSafePut(params, "mbid[" + i + "]", scrobble.getMusicBrainzId());
708      MapUtilities.nullSafePut(params, "trackNumber[" + i + "]", scrobble.getTrackNumber());
709      MapUtilities.nullSafePut(params, "streamId[" + i + "]", scrobble.getStreamId());
710      params.put("chosenByUser[" + i + "]", StringUtilities.convertFromBoolean(scrobble.isChosenByUser()));
711    }
712
713    Result result = Caller.getInstance().call("track.scrobble", session, params);
714    return convertToScrobbleResults(result, ScrobbleResultType.MULTIPLE_SCROBBLES);
715  }
716
717  public static ScrobbleResult updateNowPlaying(ScrobbleData scrobbleData, Session session) {
718    Map<String, String> params = new HashMap<String, String>();
719    // required params
720    params.put("artist", scrobbleData.getArtist());
721    params.put("track", scrobbleData.getTrack());
722    // optional params
723    MapUtilities.nullSafePut(params, "album", scrobbleData.getAlbum());
724    MapUtilities.nullSafePut(params, "albumArtist", scrobbleData.getAlbumArtist());
725    MapUtilities.nullSafePut(params, "duration", scrobbleData.getDuration());
726    MapUtilities.nullSafePut(params, "mbid", scrobbleData.getMusicBrainzId());
727    MapUtilities.nullSafePut(params, "trackNumber", scrobbleData.getTrackNumber());
728    MapUtilities.nullSafePut(params, "streamId", scrobbleData.getStreamId());
729    Result result = Caller.getInstance().call("track.updateNowPlaying", session, params);
730    return convertToScrobbleResults(result, ScrobbleResultType.NOW_PLAYING).get(0);
731  }
732
733  public static ScrobbleResult updateNowPlaying(String artistName, String trackName, Session session) {
734    ScrobbleData scrobbleData = new ScrobbleData();
735    scrobbleData.setArtist(artistName);
736    scrobbleData.setTrack(trackName);
737    return updateNowPlaying(scrobbleData, session);
738  }
739
740  @Override
741  public String toString() {
742    return "Track[name=" + name + ",artist=" + artist + ", album=" + album + ", position=" + position + ", duration=" + duration
743        + ", location=" + location + ", nowPlaying=" + nowPlaying + ", playcount=" + playcount + ", fullTrackAvailable=" + fullTrackAvailable + ", playedWhen="
744        + playedWhen + ", artistMbId=" + artistMbid + ", albumMbId" + albumMbid + "]";
745  }
746
747  private static class TrackFactory implements ItemFactory<Track> {
748    public Track createItemFromElement(DomElement element) {
749      Track track = new Track(null, null, null);
750      MusicEntry.loadStandardInfo(track, element);
751      final String nowPlayingAttr = element.getAttribute("nowplaying");
752      if (nowPlayingAttr != null)
753        track.nowPlaying = Boolean.valueOf(nowPlayingAttr);
754      if (element.hasChild("duration")) {
755        String duration = element.getChildText("duration");
756        if(duration.length() != 0) {
757          int durationLength = Integer.parseInt(duration);
758          // So it seems last.fm couldn't decide which format to send the duration in.
759          // It's supplied in milliseconds for Playlist.fetch and Track.getInfo but Artist.getTopTracks returns (much saner) seconds
760          // so we're doing a little sanity check for the duration to be over or under 10'000 and decide what to do
761          track.duration = durationLength > 10000 ? durationLength / 1000 : durationLength;
762        }
763      }
764      /*  TBG: addition */
765      /*
766      if (element.hasChild("playcount")) {
767        String playcountStr = element.getChildText("playcount");
768        if(playcountStr.length() != 0) {
769          track.playcount = Integer.parseInt(playcountStr);
770        }
771      } */
772      /*  TBG: addition */
773      DomElement album = element.getChild("album");
774      if (album != null) {
775        track.album = album.getText();
776        track.albumMbid = album.getAttribute("mbid");
777      }
778      DomElement artist = element.getChild("artist");
779      if (artist.getChild("name") != null) {
780        track.artist = artist.getChildText("name");
781        track.artistMbid = artist.getChildText("mbid");
782      } else {
783        track.artist = artist.getText();
784        track.artistMbid = artist.getAttribute("mbid");
785      }
786      DomElement date = element.getChild("date");
787      if (date != null) {
788        String uts = date.getAttribute("uts");
789        long utsTime = Long.parseLong(uts);
790        track.playedWhen = new Date(utsTime * 1000);
791      }
792      DomElement stream = element.getChild("streamable");
793      if (stream != null) {
794        String s = stream.getAttribute("fulltrack");
795        track.fullTrackAvailable = s != null && Integer.parseInt(s) == 1;
796      }
797      return track;
798    }
799  }
800}