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}