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 */ 026 027package de.umass.lastfm.scrobble; 028 029import java.io.BufferedReader; 030import java.io.BufferedWriter; 031import java.io.IOException; 032import java.io.InputStream; 033import java.io.InputStreamReader; 034import java.io.OutputStream; 035import java.io.OutputStreamWriter; 036import java.net.HttpURLConnection; 037import java.net.Proxy; 038import java.util.Collection; 039import java.util.Collections; 040 041import de.umass.lastfm.Caller; 042import de.umass.lastfm.Session; 043 044import static de.umass.util.StringUtilities.encode; 045import static de.umass.util.StringUtilities.md5; 046 047/** 048 * This class manages communication with the server for scrobbling songs. You can retrieve an instance of this class by calling 049 * {@link #newScrobbler(String, String, String) newScrobbler}.<br/> 050 * It contains methods to perform the handshake, notify Last.fm about a now playing song and submitting songs to a musical profile, aka 051 * scrobbling songs.<br/> 052 * See <a href="https://www.last.fm/api/submissions">https://www.last.fm/api/submissions</a> for a deeper explanation of the protocol and 053 * various guidelines on how to use the scrobbling service, since this class does not cover error handling or caching.<br/> 054 * All methods in this class, which are communicating with the server, return an instance of {@link ResponseStatus} which contains 055 * information if the operation was successful or not.<br/> 056 * This class respects the <code>proxy</code> property in the {@link Caller} class in all its HTTP calls. If you need the 057 * <code>Scrobbler</code> to use a Proxy server, set it with {@link Caller#setProxy(java.net.Proxy)}. 058 * 059 * @author Janni Kovacs 060 * @see de.umass.lastfm.Track#scrobble(ScrobbleData, de.umass.lastfm.Session) 061 * @see de.umass.lastfm.Track#scrobble(String, String, int, de.umass.lastfm.Session) 062 * @see de.umass.lastfm.Track#scrobble(java.util.List, de.umass.lastfm.Session) 063 * @deprecated The 1.2.x scrobble protocol has now been deprecated in favour of the 2.0 protocol which is part of the Last.fm web services 064 * API. 065 */ 066@Deprecated 067public class Scrobbler { 068 069 private static final String DEFAULT_HANDSHAKE_URL = "http://post.audioscrobbler.com/"; 070 private String handshakeUrl = DEFAULT_HANDSHAKE_URL; 071 072 private final String clientId, clientVersion; 073 private final String user; 074 075 private String sessionId; 076 private String nowPlayingUrl; 077 private String submissionUrl; 078 079 private Scrobbler(String clientId, String clientVersion, String user) { 080 this.clientId = clientId; 081 this.clientVersion = clientVersion; 082 this.user = user; 083 } 084 085 /** 086 * Sets the URL to use to perform a handshake. Use this method to redirect your scrobbles to another service, like Libre.fm. 087 * 088 * @param handshakeUrl The new handshake url. 089 */ 090 public void setHandshakeURL(String handshakeUrl) { 091 this.handshakeUrl = handshakeUrl; 092 } 093 094 /** 095 * Creates a new <code>Scrobbler</code> instance bound to the specified <code>user</code>. 096 * 097 * @param clientId The client id (or "tst") 098 * @param clientVersion The client version (or "1.0") 099 * @param user The last.fm user 100 * @return a new <code>Scrobbler</code> instance 101 */ 102 public static Scrobbler newScrobbler(String clientId, String clientVersion, String user) { 103 return new Scrobbler(clientId, clientVersion, user); 104 } 105 106 /** 107 * Performs a standard handshake with the user's password. 108 * 109 * @param password The user's password 110 * @return the status of the operation 111 * @throws IOException on I/O errors 112 */ 113 public ResponseStatus handshake(String password) throws IOException { 114 long time = System.currentTimeMillis() / 1000; 115 String auth = md5(md5(password) + time); 116 String url = String.format("%s?hs=true&p=1.2.1&c=%s&v=%s&u=%s&t=%s&a=%s", handshakeUrl, clientId, 117 clientVersion, user, time, auth); 118 return performHandshake(url); 119 } 120 121 /** 122 * Performs a web-service handshake. 123 * 124 * @param session An authenticated Session. 125 * @return the status of the operation 126 * @throws IOException on I/O errors 127 * @see de.umass.lastfm.Authenticator 128 */ 129 public ResponseStatus handshake(Session session) throws IOException { 130 long time = System.currentTimeMillis() / 1000; 131 String auth = md5(session.getSecret() + time); 132 String url = String 133 .format("%s?hs=true&p=1.2.1&c=%s&v=%s&u=%s&t=%s&a=%s&api_key=%s&sk=%s", handshakeUrl, clientId, 134 clientVersion, user, time, auth, session.getApiKey(), session.getKey()); 135 return performHandshake(url); 136 } 137 138 /** 139 * Internally performs the handshake operation by calling the given <code>url</code> and examining the response. 140 * 141 * @param url The URL to call 142 * @return the status of the operation 143 * @throws IOException on I/O errors 144 */ 145 private ResponseStatus performHandshake(String url) throws IOException { 146 HttpURLConnection connection = Caller.getInstance().openConnection(url); 147 InputStream is = connection.getInputStream(); 148 BufferedReader r = new BufferedReader(new InputStreamReader(is)); 149 String status = r.readLine(); 150 int statusCode = ResponseStatus.codeForStatus(status); 151 ResponseStatus responseStatus; 152 if (statusCode == ResponseStatus.OK) { 153 this.sessionId = r.readLine(); 154 this.nowPlayingUrl = r.readLine(); 155 this.submissionUrl = r.readLine(); 156 responseStatus = new ResponseStatus(statusCode); 157 } else if (statusCode == ResponseStatus.FAILED) { 158 responseStatus = new ResponseStatus(statusCode, status.substring(status.indexOf(' ') + 1)); 159 } else { 160 return new ResponseStatus(statusCode); 161 } 162 r.close(); 163 return responseStatus; 164 } 165 166 /** 167 * Submits 'now playing' information. This does not affect the musical profile of the user. 168 * 169 * @param artist The artist's name 170 * @param track The track's title 171 * @return the status of the operation 172 * @throws IOException on I/O errors 173 */ 174 public ResponseStatus nowPlaying(String artist, String track) throws IOException { 175 return nowPlaying(artist, track, null, -1, -1); 176 } 177 178 /** 179 * Submits 'now playing' information. This does not affect the musical profile of the user. 180 * 181 * @param artist The artist's name 182 * @param track The track's title 183 * @param album The album or <code>null</code> 184 * @param length The length of the track in seconds 185 * @param tracknumber The position of the track in the album or -1 186 * @return the status of the operation 187 * @throws IOException on I/O errors 188 */ 189 public ResponseStatus nowPlaying(String artist, String track, String album, int length, int tracknumber) throws 190 IOException { 191 if (sessionId == null) 192 throw new IllegalStateException("Perform successful handshake first."); 193 String b = album != null ? encode(album) : ""; 194 String l = length == -1 ? "" : String.valueOf(length); 195 String n = tracknumber == -1 ? "" : String.valueOf(tracknumber); 196 String body = String 197 .format("s=%s&a=%s&t=%s&b=%s&l=%s&n=%s&m=", sessionId, encode(artist), encode(track), b, l, n); 198 if (Caller.getInstance().isDebugMode()) 199 System.out.println("now playing: " + body); 200 HttpURLConnection urlConnection = Caller.getInstance().openConnection(nowPlayingUrl); 201 urlConnection.setRequestMethod("POST"); 202 urlConnection.setDoOutput(true); 203 OutputStream outputStream = urlConnection.getOutputStream(); 204 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream)); 205 writer.write(body); 206 writer.close(); 207 InputStream is = urlConnection.getInputStream(); 208 BufferedReader r = new BufferedReader(new InputStreamReader(is)); 209 String status = r.readLine(); 210 r.close(); 211 return new ResponseStatus(ResponseStatus.codeForStatus(status)); 212 } 213 214 /** 215 * Scrobbles a song. 216 * 217 * @param artist The artist's name 218 * @param track The track's title 219 * @param album The album or <code>null</code> 220 * @param length The length of the track in seconds 221 * @param tracknumber The position of the track in the album or -1 222 * @param source The source of the track 223 * @param startTime The time the track started playing in UNIX timestamp format and UTC time zone 224 * @return the status of the operation 225 * @throws IOException on I/O errors 226 */ 227 public ResponseStatus submit(String artist, String track, String album, int length, int tracknumber, Source source, 228 long startTime) throws IOException { 229 return submit(new SubmissionData(artist, track, album, length, tracknumber, source, startTime)); 230 } 231 232 /** 233 * Scrobbles a song. 234 * 235 * @param data Contains song information 236 * @return the status of the operation 237 * @throws IOException on I/O errors 238 */ 239 public ResponseStatus submit(SubmissionData data) throws IOException { 240 return submit(Collections.singletonList(data)); 241 } 242 243 /** 244 * Scrobbles up to 50 songs at once. Song info is contained in the <code>Collection</code> passed. Songs must be in 245 * chronological order of their play, that means the track first in the list has been played before the track second 246 * in the list and so on. 247 * 248 * @param data A list of song infos 249 * @return the status of the operation 250 * @throws IOException on I/O errors 251 * @throws IllegalArgumentException if data contains more than 50 entries 252 */ 253 public ResponseStatus submit(Collection<SubmissionData> data) throws IOException { 254 if (sessionId == null) 255 throw new IllegalStateException("Perform successful handshake first."); 256 if (data.size() > 50) 257 throw new IllegalArgumentException("Max 50 submissions at once"); 258 StringBuilder builder = new StringBuilder(data.size() * 100); 259 int index = 0; 260 for (SubmissionData submissionData : data) { 261 builder.append(submissionData.toString(sessionId, index)); 262 builder.append('\n'); 263 index++; 264 } 265 String body = builder.toString(); 266 if (Caller.getInstance().isDebugMode()) 267 System.out.println("submit: " + body); 268 HttpURLConnection urlConnection = Caller.getInstance().openConnection(submissionUrl); 269 urlConnection.setRequestMethod("POST"); 270 urlConnection.setDoOutput(true); 271 OutputStream outputStream = urlConnection.getOutputStream(); 272 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream)); 273 writer.write(body); 274 writer.close(); 275 InputStream is = urlConnection.getInputStream(); 276 BufferedReader r = new BufferedReader(new InputStreamReader(is)); 277 String status = r.readLine(); 278 r.close(); 279 int statusCode = ResponseStatus.codeForStatus(status); 280 if (statusCode == ResponseStatus.FAILED) { 281 return new ResponseStatus(statusCode, status.substring(status.indexOf(' ') + 1)); 282 } 283 return new ResponseStatus(statusCode); 284 } 285}