├── .gitignore ├── README.md ├── htdocs ├── .htaccess ├── api.js ├── config.php.sample ├── favicon.ico ├── footer.php ├── header.php ├── include.php ├── index.php ├── indicator.gif ├── showlists.php ├── simple-proxy.php ├── spotify-desktop.png ├── spotify-mobile.png ├── stalkify-gs.png ├── stalkify-gs2.png ├── stalkify-icon-fw.png ├── stalkify-icon.png ├── stalkify-me.php ├── stalkify.png ├── stillwaiting.php ├── style.css ├── void.gif └── welcome.php ├── service ├── add-new-feed-type.txt ├── config.py.sample ├── config.rb.sample ├── import.py ├── log │ └── run ├── match.py ├── pylast.py ├── reset.sql ├── run ├── service ├── service-import ├── service-match ├── service-sync ├── stalkify.sql ├── sync.rb └── tail └── todo.md /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | service/spotify_appkey.key 3 | dependencies/ 4 | service/clear.rb 5 | *.pyc 6 | service/config.py 7 | service/config.rb 8 | service/bk/ 9 | service/tmp/ 10 | service/log/supervise 11 | service/supervise 12 | htdocs/config.php 13 | service/config.rb 14 | service/runit/ 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | See [Stalkify.com](http://www.stalkify.com) for a sense of what this does. -------------------------------------------------------------------------------- /htdocs/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine On 3 | RewriteBase / 4 | RewriteCond %{REQUEST_FILENAME} !-f 5 | RewriteCond %{REQUEST_FILENAME} !-d 6 | RewriteRule . /index.php [L] 7 | 8 | -------------------------------------------------------------------------------- /htdocs/api.js: -------------------------------------------------------------------------------- 1 | var stalkify = { 2 | proxyJSON: function(url, callback){$.getJSON('/simple-proxy?url='+encodeURIComponent(url), callback);}, 3 | spotify: { 4 | search:function(type, objname, search, callback){stalkify.proxyJSON('http://ws.spotify.com/search/1/'+type+'.json?q='+encodeURIComponent(search), function(data) {callback(data.contents[objname]);});}, 5 | searchArtist:function(artist, callback){stalkify.spotify.search('artist', 'artists', artist, callback);}, 6 | searchTrack:function(track, callback){stalkify.spotify.search('track', 'tracks', track, callback);}, 7 | searchAlbum:function(album, callback){stalkify.spotify.search('album', 'albums', album, callback);} 8 | }, 9 | lastfm: { 10 | get:function(request, callback){request['api_key']='0f9d58ba56bfa4bd4b24ba62b9568615'; request['format']='json'; stalkify.proxyJSON('http://ws.audioscrobbler.com/2.0/?'+$.param(request), function(data) {callback(data.contents);});}, 11 | getNeighbours:function(username, callback){stalkify.lastfm.get({method:'user.getneighbours', user:username}, function(o){callback(o.neighbours.user);});}, 12 | getTopArtists:function(username, callback){stalkify.lastfm.get({method:'user.gettopartists', user:username}, function(o){callback(o.topartists.artist);});}, 13 | getWeeklyArtistChart:function(username, callback){stalkify.lastfm.get({method:'user.getweeklyartistchart', user:username}, function(o){callback(o.weeklyartistchart.artist);});}, 14 | squareImage:function(url){return(url.replace(/\/([0-9]{2,3})\//img, "/$1s/"));} 15 | } 16 | } -------------------------------------------------------------------------------- /htdocs/config.php.sample: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /htdocs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steffentchr/legacy-stalkify/6d7d49c349ae313440046fa9b9453a9a23a61fdd/htdocs/favicon.ico -------------------------------------------------------------------------------- /htdocs/footer.php: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /htdocs/header.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | Stalkify / Last.fm and Spotify bundled into goodness 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

14 |
Last.fm + Spotify bundled into goodness
15 | 16 | 22 | 23 | -------------------------------------------------------------------------------- /htdocs/include.php: -------------------------------------------------------------------------------- 1 | 2, 14 | 'portability' => DB_PORTABILITY_ALL, 15 | ); 16 | 17 | $db = DB::connect($dsn, $options); 18 | $db->setFetchMode(DB_FETCHMODE_ASSOC); 19 | if (PEAR::isError($db)) { 20 | error_log('DB ERROR: ' . $res->getMessage()); 21 | die($db->getMessage()); 22 | } 23 | } 24 | function db_close() { 25 | global $db; 26 | $db->disconnect(); 27 | } 28 | function db_query($sql) { 29 | global $db; 30 | $res = $db->query($sql); 31 | 32 | // Always check that result is not an error 33 | if (PEAR::isError($res)) { 34 | error_log('DB ERROR: ' . $res->getMessage()); 35 | die($res->getMessage()); 36 | } 37 | return($res); 38 | } 39 | function db_insert($table, $array) { 40 | global $db; 41 | $res = $db->autoExecute($table, $array, DB_AUTOQUERY_INSERT); 42 | 43 | // Always check that result is not an error 44 | if (PEAR::isError($res)) { 45 | error_log('DB ERROR: ' . $res->getMessage()); 46 | die($res->getMessage()); 47 | } 48 | return($res); 49 | } 50 | function db_update($table, $array, $where) { 51 | global $db; 52 | $res = $db->autoExecute($table, $array, DB_AUTOQUERY_UPDATE, $where); 53 | 54 | // Always check that result is not an error 55 | if (PEAR::isError($res)) { 56 | error_log('DB ERROR: ' . $res->getMessage()); 57 | die($res->getMessage()); 58 | } 59 | return($res); 60 | } 61 | function db_quote($string) { 62 | global $db; 63 | return($db->quoteSmart($string)); 64 | } 65 | function db_nextval($seq) { 66 | global $db; 67 | return($db->nextId($seq)); 68 | } 69 | function random_string($length) { 70 | $pattern = "1234567890abcdefghijklmnopqrstuvwxyz"; 71 | for($i=0;$i<$length;$i++) { 72 | if(isset($key)) 73 | $key .= $pattern{rand(0,35)}; 74 | else 75 | $key = $pattern{rand(0,35)}; 76 | } 77 | return $key; 78 | } 79 | ?> -------------------------------------------------------------------------------- /htdocs/index.php: -------------------------------------------------------------------------------- 1 | 2) { 16 | // Asked for a specific playlist 17 | $playlist = $stubs[2]; 18 | 19 | $res = db_query("select * from stalkify_playlists where lastfm_username = ".db_quote($username)." and feed_type = ".db_quote($playlist)." and spotify_uri is not null"); 20 | if ($res->numRows()==0) { 21 | Header('Location: /'.$username); 22 | } else { 23 | $res->fetchInto($row); 24 | $playlist_id = $row['playlist_id']; 25 | db_query("update stalkify_playlists set num_opens=num_opens+1 where playlist_id = " . db_quote($playlist_id)); 26 | $url = $row['spotify_uri']; 27 | //preg_match("/spotify:user:([^:]+):([^:]+):([^:]+)/", $row['spotify_uri'], $matches); 28 | //$url = "http://open.spotify.com/user/".$matches[1]."/".$matches[2]."/".$matches[3]; 29 | Header('Location: '.$url); 30 | } 31 | } 32 | 33 | $res = db_query("select * from stalkify_playlists where lastfm_username = ".db_quote($username)); 34 | if ($res->numRows()==0) { 35 | // Check that the user exists and then create 36 | $url = "http://www.last.fm/user/" . $username; 37 | $ch = curl_init($url); 38 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 39 | $_x = curl_exec($ch); 40 | if (curl_getinfo($ch, CURLINFO_HTTP_CODE) == 200) { 41 | db_query("insert into stalkify_playlists (lastfm_username, playlist_name, feed_type, update_interval) values (".db_quote($username).", '@'||".db_quote($username)."||' / live', 'recent', '10 minutes'::interval)"); 42 | db_query("insert into stalkify_playlists (lastfm_username, playlist_name, feed_type, last_updated) values (".db_quote($username).", '@'||".db_quote($username)."||' / all-time', 'toptracks-overall', now()-'30 days'::interval+'2 minutes'::interval)"); 43 | db_query("insert into stalkify_playlists (lastfm_username, playlist_name, feed_type, last_updated) values (".db_quote($username).", '@'||".db_quote($username)."||' / this week', 'toptracks-7day', now()-'2 days'::interval+'5 minutes'::interval)"); 44 | db_query("insert into stalkify_playlists (lastfm_username, playlist_name, feed_type, last_updated) values (".db_quote($username).", '@'||".db_quote($username)."||' / 3 months', 'toptracks-3month', now()-'15 days'::interval+'8 minutes'::interval)"); 45 | db_query("insert into stalkify_playlists (lastfm_username, playlist_name, feed_type, last_updated) values (".db_quote($username).", '@'||".db_quote($username)."||' / 6 months', 'toptracks-6month', now()-'30 days'::interval+'12 minutes'::interval)"); 46 | db_query("insert into stalkify_playlists (lastfm_username, playlist_name, feed_type, last_updated) values (".db_quote($username).", '@'||".db_quote($username)."||' / this year', 'toptracks-12month', now()-'30 days'::interval+'15 minutes'::interval)"); 47 | db_query("insert into stalkify_playlists (lastfm_username, playlist_name, feed_type, last_updated) values (".db_quote($username).", '@'||".db_quote($username)."||' / loved tracks', 'lovedtracks', now()-'1 days'::interval+'15 minutes'::interval)"); 48 | Header('Location: /'.$username); 49 | } else { 50 | Header("Location: /welcome?msg=Wow.+That+is+not+a+real+Last.fm+user"); 51 | } 52 | } else { 53 | // Show lists! 54 | include('header.php'); 55 | include('showlists.php'); 56 | include('footer.php'); 57 | } 58 | 59 | ?> 60 | 61 | 62 | -------------------------------------------------------------------------------- /htdocs/indicator.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steffentchr/legacy-stalkify/6d7d49c349ae313440046fa9b9453a9a23a61fdd/htdocs/indicator.gif -------------------------------------------------------------------------------- /htdocs/showlists.php: -------------------------------------------------------------------------------- 1 | 17 | 18 |

@

19 | 20 | 21 | fetchInto($row)) { 24 | $t = $row['feed_type']; 25 | $s = $row['status']; 26 | $n = $prettyname[$t]; 27 | $d = $prettydesc[$t]; 28 | ?> 29 | class="discrete"> 30 | 43 | 44 | 45 | 46 | 49 | 50 |
31 |

32 | 33 | 34 | * 35 | 36 | 37 | 38 | 39 | * 40 | 41 |

42 |
51 | 52 | 53 | 54 | 58 | 70 | 71 | 72 | 73 | 77 | 104 | -------------------------------------------------------------------------------- /htdocs/simple-proxy.php: -------------------------------------------------------------------------------- 1 | and 42 | // are disabled by default, see for more information. 43 | // callback - If specified, the response JSON will be wrapped in this named 44 | // function call. This parameter and are disabled by 45 | // default, see for more information. 46 | // user_agent - This value will be sent to the remote URL request as the 47 | // `User-Agent:` HTTP request header. If omitted, the browser user agent 48 | // will be passed through. 49 | // send_cookies - If send_cookies=1, all cookies will be forwarded through to 50 | // the remote URL request. 51 | // send_session - If send_session=1 and send_cookies=1, the SID cookie will be 52 | // forwarded through to the remote URL request. 53 | // full_headers - If a JSON request and full_headers=1, the JSON response will 54 | // contain detailed header information. 55 | // full_status - If a JSON request and full_status=1, the JSON response will 56 | // contain detailed cURL status information, otherwise it will just contain 57 | // the `http_code` property. 58 | // 59 | // Topic: POST Parameters 60 | // 61 | // All POST parameters are automatically passed through to the remote URL 62 | // request. 63 | // 64 | // Topic: JSON requests 65 | // 66 | // This request will return the contents of the specified url in JSON format. 67 | // 68 | // Request: 69 | // 70 | // > ba-simple-proxy.php?url=http://example.com/ 71 | // 72 | // Response: 73 | // 74 | // > { "contents": "...", "headers": {...}, "status": {...} } 75 | // 76 | // JSON object properties: 77 | // 78 | // contents - (String) The contents of the remote URL resource. 79 | // headers - (Object) A hash of HTTP headers returned by the remote URL 80 | // resource. 81 | // status - (Object) A hash of status codes returned by cURL. 82 | // 83 | // Topic: JSONP requests 84 | // 85 | // This request will return the contents of the specified url in JSONP format 86 | // (but only if $enable_jsonp is enabled in the PHP script). 87 | // 88 | // Request: 89 | // 90 | // > ba-simple-proxy.php?url=http://example.com/&callback=foo 91 | // 92 | // Response: 93 | // 94 | // > foo({ "contents": "...", "headers": {...}, "status": {...} }) 95 | // 96 | // JSON object properties: 97 | // 98 | // contents - (String) The contents of the remote URL resource. 99 | // headers - (Object) A hash of HTTP headers returned by the remote URL 100 | // resource. 101 | // status - (Object) A hash of status codes returned by cURL. 102 | // 103 | // Topic: Native requests 104 | // 105 | // This request will return the contents of the specified url in the format it 106 | // was received in, including the same content-type and other headers (but only 107 | // if $enable_native is enabled in the PHP script). 108 | // 109 | // Request: 110 | // 111 | // > ba-simple-proxy.php?url=http://example.com/&mode=native 112 | // 113 | // Response: 114 | // 115 | // > ... 116 | // 117 | // Topic: Notes 118 | // 119 | // * Assumes magic_quotes_gpc = Off in php.ini 120 | // 121 | // Topic: Configuration Options 122 | // 123 | // These variables can be manually edited in the PHP file if necessary. 124 | // 125 | // $enable_jsonp - Only enable if you really need to. If you 126 | // install this script on the same server as the page you're calling it 127 | // from, plain JSON will work. Defaults to false. 128 | // $enable_native - You can enable , but you should only do 129 | // this if you also whitelist specific URLs using $valid_url_regex, to avoid 130 | // possible XSS vulnerabilities. Defaults to false. 131 | // $valid_url_regex - This regex is matched against the url parameter to 132 | // ensure that it is valid. This setting only needs to be used if either 133 | // $enable_jsonp or $enable_native are enabled. Defaults to '/.*/' which 134 | // validates all URLs. 135 | // 136 | // ############################################################################ 137 | 138 | // Change these configuration options if needed, see above descriptions for info. 139 | $enable_jsonp = true; 140 | $enable_native = true; 141 | $valid_url_regex = '/http\:\/\/ws\.(spotify|audioscrobbler)\.com\/.+/'; 142 | 143 | // ############################################################################ 144 | 145 | $url = $_GET['url']; 146 | 147 | if ( !$url ) { 148 | 149 | // Passed url not specified. 150 | $contents = 'ERROR: url not specified'; 151 | $status = array( 'http_code' => 'ERROR' ); 152 | 153 | } else if ( !preg_match( $valid_url_regex, $url ) ) { 154 | 155 | // Passed url doesn't match $valid_url_regex. 156 | $contents = 'ERROR: invalid url'; 157 | $status = array( 'http_code' => 'ERROR' ); 158 | 159 | } else { 160 | $ch = curl_init( $url ); 161 | 162 | if ( strtolower($_SERVER['REQUEST_METHOD']) == 'post' ) { 163 | curl_setopt( $ch, CURLOPT_POST, true ); 164 | curl_setopt( $ch, CURLOPT_POSTFIELDS, $_POST ); 165 | } 166 | 167 | if ( $_GET['send_cookies'] ) { 168 | $cookie = array(); 169 | foreach ( $_COOKIE as $key => $value ) { 170 | $cookie[] = $key . '=' . $value; 171 | } 172 | if ( $_GET['send_session'] ) { 173 | $cookie[] = SID; 174 | } 175 | $cookie = implode( '; ', $cookie ); 176 | 177 | curl_setopt( $ch, CURLOPT_COOKIE, $cookie ); 178 | } 179 | 180 | curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true ); 181 | curl_setopt( $ch, CURLOPT_HEADER, true ); 182 | curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); 183 | 184 | curl_setopt( $ch, CURLOPT_USERAGENT, $_GET['user_agent'] ? $_GET['user_agent'] : $_SERVER['HTTP_USER_AGENT'] ); 185 | 186 | list( $header, $contents ) = preg_split( '/([\r\n][\r\n])\\1/', curl_exec( $ch ), 2 ); 187 | 188 | $status = curl_getinfo( $ch ); 189 | 190 | curl_close( $ch ); 191 | } 192 | 193 | // Split header text into an array. 194 | $header_text = preg_split( '/[\r\n]+/', $header ); 195 | 196 | if ( $_GET['mode'] == 'native' ) { 197 | if ( !$enable_native ) { 198 | $contents = 'ERROR: invalid mode'; 199 | $status = array( 'http_code' => 'ERROR' ); 200 | } 201 | 202 | // Propagate headers to response. 203 | foreach ( $header_text as $header ) { 204 | if ( preg_match( '/^(?:Content-Type|Content-Language|Set-Cookie):/i', $header ) ) { 205 | header( $header ); 206 | } 207 | } 208 | 209 | print $contents; 210 | 211 | } else { 212 | 213 | // $data will be serialized into JSON data. 214 | $data = array(); 215 | 216 | // Propagate all HTTP headers into the JSON data object. 217 | if ( $_GET['full_headers'] ) { 218 | $data['headers'] = array(); 219 | 220 | foreach ( $header_text as $header ) { 221 | preg_match( '/^(.+?):\s+(.*)$/', $header, $matches ); 222 | if ( $matches ) { 223 | $data['headers'][ $matches[1] ] = $matches[2]; 224 | } 225 | } 226 | } 227 | 228 | // Propagate all cURL request / response info to the JSON data object. 229 | if ( $_GET['full_status'] ) { 230 | $data['status'] = $status; 231 | } else { 232 | $data['status'] = array(); 233 | $data['status']['http_code'] = $status['http_code']; 234 | } 235 | 236 | // Set the JSON data object contents, decoding it from JSON if possible. 237 | $decoded_json = json_decode( $contents ); 238 | $data['contents'] = $decoded_json ? $decoded_json : $contents; 239 | 240 | // Generate appropriate content-type header. 241 | $is_xhr = strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'; 242 | header( 'Content-type: application/' . ( $is_xhr ? 'json' : 'x-javascript' ) ); 243 | 244 | // Get JSONP callback. 245 | $jsonp_callback = $enable_jsonp && isset($_GET['callback']) ? $_GET['callback'] : null; 246 | 247 | // Generate JSON/JSONP string 248 | $json = json_encode( $data ); 249 | 250 | print $jsonp_callback ? "$jsonp_callback($json)" : $json; 251 | 252 | } 253 | 254 | ?> 255 | -------------------------------------------------------------------------------- /htdocs/spotify-desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steffentchr/legacy-stalkify/6d7d49c349ae313440046fa9b9453a9a23a61fdd/htdocs/spotify-desktop.png -------------------------------------------------------------------------------- /htdocs/spotify-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steffentchr/legacy-stalkify/6d7d49c349ae313440046fa9b9453a9a23a61fdd/htdocs/spotify-mobile.png -------------------------------------------------------------------------------- /htdocs/stalkify-gs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steffentchr/legacy-stalkify/6d7d49c349ae313440046fa9b9453a9a23a61fdd/htdocs/stalkify-gs.png -------------------------------------------------------------------------------- /htdocs/stalkify-gs2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steffentchr/legacy-stalkify/6d7d49c349ae313440046fa9b9453a9a23a61fdd/htdocs/stalkify-gs2.png -------------------------------------------------------------------------------- /htdocs/stalkify-icon-fw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steffentchr/legacy-stalkify/6d7d49c349ae313440046fa9b9453a9a23a61fdd/htdocs/stalkify-icon-fw.png -------------------------------------------------------------------------------- /htdocs/stalkify-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steffentchr/legacy-stalkify/6d7d49c349ae313440046fa9b9453a9a23a61fdd/htdocs/stalkify-icon.png -------------------------------------------------------------------------------- /htdocs/stalkify-me.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

How do I get stalked?

4 |

Stalkify works with everything that speak Last.fm − so if you want to be stalked, you simply need to set your Winamp, your iTunes, your Spotify or your whatever up to tell Last.fm about the tracks you're playing.

5 | 6 | 7 |

Spotify Desktop?

8 |

Spotify can be made to lastfmify your tracks under Preferences:

9 |

10 | 11 | 12 |

Spotify Mobile?

13 |

So can Spotify Mobile under More or Settings:

14 |

15 | 16 | 17 |

What else?

18 |

It's almost a certainty that your favorite music player is stalkable. Head over to Last.fm to learn how. 19 | 20 | 21 | -------------------------------------------------------------------------------- /htdocs/stalkify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steffentchr/legacy-stalkify/6d7d49c349ae313440046fa9b9453a9a23a61fdd/htdocs/stalkify.png -------------------------------------------------------------------------------- /htdocs/stillwaiting.php: -------------------------------------------------------------------------------- 1 | 2 |

3 | Please hang on while we stalkify 4 |
5 | This might take a few long minutes 6 |
7 | 10 | 11 | -------------------------------------------------------------------------------- /htdocs/style.css: -------------------------------------------------------------------------------- 1 | /* RESET */ 2 | body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}ol,ul {list-style:none;}caption,th {text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym {border:0;} 3 | 4 | /* BODY */ 5 | body {background:white; font-family:Helvetica, Arial, sans-serif; } 6 | a {color:#000; text-decoration: none;} 7 | a:hover {border-bottom: 2px solid #000;} 8 | 9 | h1 a:hover {border-bottom:0;} 10 | 11 | 12 | #frame {width:380px; margin:40px auto;} 13 | h1 {text-align:center; text-shadow: 2px 2px 2px #999; font-size:80px; font-weight:bold; text-transform:uppercase; color:#222;} 14 | #tagline {text-align:center; margin-bottom:80px; font-size:20px;} 15 | h2 {text-shadow: 1px 1px 1px #B5C6A8; font-size:35px; font-weight:bold; color:#222; margin-bottom:20px; text-align:center;} 16 | h3 {color: #000; font-size: 18px; font-weight: bold; text-shadow: 1px 1px 1px #ccc; line-height: 1em; margin:0 0 5px 0;} 17 | h4 {color: #000; font-size: 16px; font-weight: bold; text-shadow: 1px 1px 1px #ccc; line-height: 1em; margin:25px 0 5px 0; text-align:center;} 18 | p { margin: 0 0 20px 0; color:#333;} 19 | p a { color:#000;} 20 | 21 | table tr td {font-size:13px;} 22 | 23 | 24 | form {text-align:center;} 25 | .smallmeta {text-align:center; color:#999; font-size:12px; line-height:20px; margin:5px 80px;} 26 | .smallmeta a {color:#888; text-decoration:underline;} 27 | .smallmeta a:hover {border-bottom-width:0;} 28 | 29 | .bigmessage {font-size:17px; text-align:center; padding:40px 0; font-style:italic; background-color:#eee; margin-bottom:20px; } 30 | 31 | .playlist-list td {vertical-align:top;} 32 | .playlist-list-name {text-align:right; padding-right:25px; white-space:nowrap;} 33 | .playlist-list-desc {padding-bottom:20px; line-height:18px;} 34 | tr.discrete td, tr.discrete td h3 {color:#aaa !important;} 35 | 36 | /*.listen-in {padding-top:50px;}*/ 37 | .listen-in {position:absolute; right:15px; top:40px; background-color:#eee; padding:20px; font-size:15px;} 38 | .listen-in a {display:block; font-size:12px;} 39 | 40 | #footer {color:#888; font-size:13px; line-height:20px; margin:100px 0 20px 0; padding-top:15px; border-top:1px solid #eee;} 41 | #footer p {color:#888;} 42 | #footer a {color:#888; text-decoration:underline;} 43 | #footer a:hover {border-bottom-width:0;} 44 | 45 | #artistsContainer {overflow:auto; width:396;} 46 | .lastfmartist {width:126px; height:126px; border:1px solid #333; position:relative; background:url('http://stalkify.com/indicator.gif') center center no-repeat; cursor:pointer;} 47 | .lastfmartist-meta div {position:absolute; left:5px; background-color:#333; color:white; font-size:11px; padding:1px; font-weight:bold;} 48 | .lastfmartist-meta .lastfmartist-name {bottom:20px;} 49 | .lastfmartist-meta .lastfmartist-playcount {bottom:5px; font-size:9px;} 50 | #artistsContainer li {width:126px; height:126px; overflow:hidden; float:left; font-size:10px; text-align:left; line-height:12px;} 51 | #artistsContainer li img {background-color:#BDBD1D;} 52 | -------------------------------------------------------------------------------- /htdocs/void.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steffentchr/legacy-stalkify/6d7d49c349ae313440046fa9b9453a9a23a61fdd/htdocs/void.gif -------------------------------------------------------------------------------- /htdocs/welcome.php: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 |
11 | 12 |
13 | 16 | 17 |
18 | 19 | 20 |
21 |
22 | Tell us a Last.fm username. We'll give you back Spotify playlists with live updated recent tracks, user favorites, similar profiles and direct links to artists. 23 |
24 | 25 | 26 |
Listen in: 27 | now()-'10 minutes'::interval group by pl.lastfm_username order by random() limit 20"); 29 | while ($res->fetchInto($row)) { 30 | ?> 31 | @ 32 | 35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /service/add-new-feed-type.txt: -------------------------------------------------------------------------------- 1 | begin transaction; 2 | insert into stalkify_playlists 3 | (lastfm_username, playlist_name, feed_type, update_interval) 4 | select lastfm_username, 'Stalkify: '||lastfm_username||' / loved tracks' as playlist_name, 'lovedtracks' as feed_type, '12 hours'::interval as update_interval from stalkify_playlists where feed_type = 'recent'; 5 | -------------------------------------------------------------------------------- /service/config.py.sample: -------------------------------------------------------------------------------- 1 | DB_CONN_STRING = "dbname=stalkifydb user=stalkifydb" 2 | 3 | LASTFM_API_KEY = "lastfmapikey" 4 | LASTFM_API_SECRET = "lastfmapisecret" 5 | 6 | SPOTIFY_USERNAME = "spotuser" 7 | SPOTIFY_PASSWORD = "spotpass" 8 | -------------------------------------------------------------------------------- /service/config.rb.sample: -------------------------------------------------------------------------------- 1 | SPOTIFY_APPKEY = [0x01, 0x7B, 0x70, 0x1C, 0xB2, 0x7B, 0x7E, 0xBE, 0x4E, 0xEE, 0x73, 0xD1, 0x1C, 0xFE, 0xCF, 0x87, 0x1B, 0xED, 0xB9, 0x48, 0x4D, 0x39, 0xF3, 0xD5, 0x4C, 0x96, 0x45, 0xBB, 0xD0, 0x7B, 0xD1, 0x86, 0x9E, 0xEB, 0xC7, 0xD3, 0xEC, 0x66, 0x5A, 0x76, 0xF3, 0x70, 0x8B, 0xFE, 0xA8, 0xFF, 0x29, 0xD4, 0xE0, 0x25, 0xC7, 0xB4, 0x64, 0x7C, 0xBF, 0x41, 0x5D, 0xB5, 0xFF, 0x6E, 0x94, 0xA0, 0x76, 0x65, 0x40, 0xEF, 0xD7, 0x43, 0x90, 0xF7, 0x9B, 0x6D, 0x86, 0x49, 0x02, 0x5C, 0xEA, 0x9C, 0xAB, 0xF8, 0x1F, 0x54, 0x21, 0xE6, 0xF1, 0x4F, 0xE5, 0xD3, 0xFF, 0x5C, 0xA2, 0x88, 0xFD, 0x51, 0xEE, 0x57, 0xDF, 0xBF, 0x5E, 0x16, 0x16, 0xB9, 0x20, 0xFD, 0x35, 0x1F, 0xE9, 0xB0, 0xFF, 0x38, 0x85, 0xF4, 0x3B, 0x2E, 0xF2, 0x90, 0x37, 0x93, 0xBD, 0x33, 0x9A, 0xCB, 0xA2, 0x73, 0xE5, 0xEB, 0x33, 0xA0, 0xE3, 0x57, 0xF4, 0xD2, 0xE7, 0x18, 0x64, 0x9A, 0xE8, 0xE8, 0x07, 0x78, 0x8B, 0x0E, 0x57, 0x8C, 0x0A, 0xA5, 0xE1, 0xDD, 0x6F, 0x37, 0xC9, 0xE2, 0x11, 0xDB, 0x55, 0xF5, 0xED, 0x81, 0x2F, 0x54, 0xDF, 0x4F, 0x24, 0xED, 0x7A, 0xB6, 0x86, 0x45, 0xF0, 0xE8, 0x64, 0x49, 0xB9, 0x06, 0xC2, 0x16, 0xDF, 0x67, 0xAD, 0x44, 0xB7, 0x14, 0xE2, 0x99, 0x66, 0x53, 0x60, 0x4E, 0xD3, 0x58, 0xB0, 0x1D, 0x4C, 0x38, 0x15, 0xB4, 0x28, 0xEA, 0x08, 0x7B, 0xBC, 0x28, 0xA2, 0x5D, 0xCA, 0x98, 0x02, 0xC7, 0x51, 0x5E, 0x65, 0x74, 0x47, 0x6B, 0x3D, 0x6F, 0x45, 0xF4, 0x40, 0x0E, 0xE5, 0x94, 0xAB, 0x19, 0x29, 0x67, 0xE0, 0x6B, 0xD0, 0x25, 0xB8, 0x65, 0x3D, 0x76, 0xCF, 0x5F, 0xDB, 0x31, 0xC6, 0xEB, 0x2D, 0xAA, 0x48, 0x44, 0x24, 0x32, 0xA5, 0xBC, 0x92, 0x9B, 0xA4, 0xFC, 0xE9, 0x60, 0x8B, 0xE2, 0x27, 0x03, 0xBB, 0x94, 0xDD, 0x86, 0x0C, 0xCD, 0x4A, 0x3C, 0x77, 0xD2, 0x39, 0x45, 0x68, 0xC4, 0xFF, 0xB9, 0x0D, 0xF9, 0xFD, 0x9D, 0xEB, 0x61, 0xF7, 0xA0, 0x90, 0x67, 0xD9, 0x14, 0x0E, 0xE6, 0x75, 0xD9, 0x70, 0xC1, 0xFF, 0xF3, 0x49, 0xE3, 0x57, 0xDB, 0x8E, 0xB5, 0x87, 0xD8, 0x43, 0xCE, 0x67, 0x0E, 0x91, 0xB6, 0xE8, 0x66, 0xFC, 0x35, 0x31, 0x84, 0x20, 0x25, 0x31, 0x41, 0x94, 0xB9, 0xCE].pack('C*') 2 | SPOTIFY_USERNAME = 'USERNAME' 3 | SPOTIFY_PASSWORD = 'PASSWORD' 4 | 5 | 6 | DB_NAME = 'stalkifydb' 7 | DB_USERNAME = 'stalkifyuser' 8 | DB_PASSWORD = 'stalkifypass' 9 | -------------------------------------------------------------------------------- /service/import.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import traceback 4 | import time 5 | 6 | import pylast 7 | import spotimeta 8 | import psycopg2 9 | import config 10 | 11 | #print ("Start A") 12 | #sys.stdout.flush() 13 | 14 | last_network = pylast.get_lastfm_network(api_key = config.LASTFM_API_KEY) 15 | #print ("Start B") 16 | #sys.stdout.flush() 17 | last_network.disable_caching() 18 | #print ("Start C") 19 | #sys.stdout.flush() 20 | 21 | db_conn = psycopg2.connect(config.DB_CONN_STRING) 22 | 23 | #print ("Start D") 24 | #sys.stdout.flush() 25 | 26 | ### 1. IMPORT TRACK FROM LAST.FM ### 27 | cur = db_conn.cursor() 28 | cur.execute("select playlist_id, lastfm_username, feed_type, last_updated from stalkify_playlists where (last_updated is null or last_updated+update_interval0: 54 | r = cur.fetchone() 55 | recent_name = r[0] 56 | recent_artist = r[1] 57 | else: 58 | recent_name = "" 59 | recent_artist = "" 60 | 61 | #print ("Most recent for %s is %s - %s" % (lastfm_username, recent_name, recent_artist)) 62 | 63 | # Get data from last.fm 64 | tracks = last_user.get_recent_tracks(20) 65 | addtracks = [] 66 | for played_track in tracks: 67 | t = played_track['track'] 68 | name = t.get_title().encode('utf-8') 69 | artist = t.get_artist().get_name().encode('utf-8') 70 | 71 | # Already added? 72 | if artist == recent_artist and name == recent_name: 73 | #print "Matched recent track, breaking" 74 | break 75 | 76 | addtracks.append(played_track) 77 | 78 | addtracks.reverse() 79 | for played_track in addtracks: 80 | t = played_track['track'] 81 | name = t.get_title().encode('utf-8') 82 | artist = t.get_artist().get_name().encode('utf-8') 83 | #print ("Recent track for %s: %s - %s, time = %s" % (lastfm_username, name, artist, played_track['timestamp'])) 84 | 85 | # Add the row 86 | cur.execute("insert into stalkify_tracks (playlist_id, name, artist) values (%s, %s, %s)", (playlist_id, name, artist)) 87 | numtracks = numtracks + 1 88 | db_conn.commit() 89 | 90 | 91 | else: 92 | # Clear previous lists 93 | print ("Queued clearing %s/%s" % (lastfm_username, feed_type)) 94 | cur.execute("update stalkify_playlists set clear_p = true where playlist_id = %s", (playlist_id, )) 95 | cur.execute("delete from stalkify_tracks where playlist_id = %s", (playlist_id, )) 96 | 97 | if feed_type == "lovedtracks": 98 | print("Getting lovedtraks") 99 | sys.stdout.flush() 100 | tracks = last_user.get_loved_tracks(limit=500) 101 | print("Done retrieving lovedtraks") 102 | sys.stdout.flush() 103 | else: 104 | # Get data from last.fm 105 | if feed_type == "toptracks-7day": 106 | period = pylast.PERIOD_7DAYS 107 | elif feed_type == "toptracks-3month": 108 | period = pylast.PERIOD_3MONTHS 109 | elif feed_type == "toptracks-6month": 110 | period = pylast.PERIOD_6MONTHS 111 | elif feed_type == "toptracks-12month": 112 | period = pylast.PERIOD_12MONTHS 113 | else: 114 | period = pylast.PERIOD_OVERALL 115 | 116 | tracks = last_user.get_top_tracks(period) 117 | 118 | 119 | for track in tracks: 120 | if feed_type == "lovedtracks": 121 | t = track['track'] 122 | else: 123 | t = track['item'] 124 | 125 | name = t.get_title().encode('utf-8') 126 | artist = t.get_artist().get_name().encode('utf-8') 127 | 128 | # Add the row 129 | print("insert into stalkify_tracks (playlist_id, name, artist) values (%s, %s, %s)", (playlist_id, name, artist)) 130 | sys.stdout.flush() 131 | cur.execute("insert into stalkify_tracks (playlist_id, name, artist) values (%s, %s, %s)", (playlist_id, name, artist)) 132 | numtracks = numtracks + 1 133 | db_conn.commit() 134 | 135 | 136 | except: 137 | do = "nothing" 138 | 139 | 140 | cur.close() 141 | db_conn.commit() 142 | 143 | print ("Retrieved %s tracks from last.fm" % (numtracks, )) 144 | sys.stdout.flush() 145 | 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /service/log/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec svlogd -tt /var/log/stalkify 3 | -------------------------------------------------------------------------------- /service/match.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import traceback 4 | import time 5 | 6 | import pylast 7 | import spotimeta 8 | import psycopg2 9 | import config 10 | 11 | last_network = pylast.get_lastfm_network(api_key = config.LASTFM_API_KEY) 12 | last_network.disable_caching() 13 | 14 | db_conn = psycopg2.connect(config.DB_CONN_STRING) 15 | db_conn.set_isolation_level(0) 16 | print "Begun matching" 17 | 18 | ### 2. MATCH TRACKS FROM DATA ALREADY IN DATABASE ### 19 | cur = db_conn.cursor() 20 | print ("Running match from database -- looing for songs on spotify") 21 | sys.stdout.flush() 22 | cur.execute("select tr.track_id, trmatch.spotify_uri from stalkify_tracks tr, stalkify_tracks trmatch where tr.spotify_uri is null and tr.processed_p is false and trmatch.spotify_uri is not null and tr.artist=trmatch.artist and tr.name=trmatch.name group by tr.track_id, trmatch.spotify_uri") 23 | totaltracks = 0 24 | for row in cur.fetchall(): 25 | totaltracks = totaltracks + 1 26 | track_id = row[0] 27 | spotify_uri = row[1] 28 | cur.execute("update stalkify_tracks set spotify_uri=%s where track_id = %s", (spotify_uri, track_id)) 29 | print ("%s: Queued %s, %s from database" % (totaltracks, spotify_uri, track_id)) 30 | sys.stdout.flush() 31 | 32 | print ("Matched %s tracks from db" % (totaltracks, )) 33 | sys.stdout.flush() 34 | db_conn.commit() 35 | cur.close() 36 | 37 | ### 2a. MATCH NOT-FOUND TRACKS FROM DATA ALREADY IN DATABASE ### 38 | cur = db_conn.cursor() 39 | print ("Running match from database -- looking for songs not found on spotify") 40 | sys.stdout.flush() 41 | cur.execute("select tr.track_id from stalkify_tracks tr, stalkify_tracks trmatch where tr.spotify_uri is null and tr.processed_p is false and trmatch.spotify_uri is null and trmatch.processed_p is true and tr.artist=trmatch.artist and tr.name=trmatch.name group by tr.track_id") 42 | totaltracks = 0 43 | for row in cur.fetchall(): 44 | totaltracks = totaltracks + 1 45 | track_id = row[0] 46 | cur.execute("update stalkify_tracks set processed_p=true, processed_date=now() where track_id = %s", (track_id, )) 47 | print ("%s: Queued %s not on spotify according to database" % (totaltracks, track_id)) 48 | sys.stdout.flush() 49 | 50 | print ("Matched %s tracks not on spotify from db" % (totaltracks, )) 51 | sys.stdout.flush() 52 | db_conn.commit() 53 | cur.close() 54 | 55 | 56 | ### 3. MATCH REMAINING TRACKS ON SPOTIFY AND UPDATE WITH SPOTIFY_URI ### 57 | cur = db_conn.cursor() 58 | cur.execute("select track_id, name, artist from stalkify_tracks where spotify_uri is null and processed_p is false order by track_id limit 1000") 59 | totaltracks = 0 60 | hittracks = 0 61 | missedtracks = 0 62 | errortracks = 0 63 | for row in cur.fetchall(): 64 | totaltracks = totaltracks + 1 65 | try: 66 | track_id = row[0] 67 | name = row[1].decode('utf-8') 68 | artist = row[2].decode('utf-8') 69 | 70 | search = spotimeta.search_track("%s %s" % (name, artist)) 71 | if search["total_results"]>0: 72 | # Save the spotify_uri; then the sync script will carry on the good word 73 | spotify_uri = search["result"][0]["href"] 74 | cur.execute("update stalkify_tracks set spotify_uri=%s where track_id = %s", (spotify_uri, track_id)) 75 | spotimeta_artist = search["result"][0]["artist"]["name"] 76 | spotimeta_name = search["result"][0]["name"] 77 | print ("%s: Queued %s - %s (%s): %s - %s" % (totaltracks, artist, name, spotify_uri, spotimeta_artist, spotimeta_name)) 78 | sys.stdout.flush() 79 | hittracks = hittracks + 1 80 | else: 81 | # We won't be able to do more for these tracks 82 | cur.execute("update stalkify_tracks set processed_p=true, processed_date=now() where track_id = %s", (track_id, )) 83 | missedtracks = missedtracks + 1 84 | except: 85 | cur.execute("update stalkify_tracks set processed_p=true, processed_date=now() where track_id = %s", (track_id, )) 86 | errortracks = errortracks + 1 87 | 88 | 89 | db_conn.commit() 90 | time.sleep(0.2) 91 | 92 | print ("Hit %s, missed %s and errored %s" % (hittracks, missedtracks, errortracks)) 93 | sys.stdout.flush() 94 | 95 | 96 | cur.close() 97 | 98 | db_conn.close() 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /service/pylast.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pylast - A Python interface to Last.fm (and other API compatible social networks) 4 | # Copyright (C) 2008-2009 Amr Hassan 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 19 | # USA 20 | # 21 | # http://code.google.com/p/pylast/ 22 | 23 | __version__ = '0.4' 24 | __author__ = 'Amr Hassan' 25 | __copyright__ = "Copyright (C) 2008-2009 Amr Hassan" 26 | __license__ = "gpl" 27 | __email__ = 'amr.hassan@gmail.com' 28 | 29 | import hashlib 30 | import httplib 31 | import urllib 32 | import threading 33 | from xml.dom import minidom 34 | import xml.dom 35 | import time 36 | import shelve 37 | import tempfile 38 | import sys 39 | import htmlentitydefs 40 | 41 | try: 42 | import collections 43 | except ImportError: 44 | pass 45 | 46 | STATUS_INVALID_SERVICE = 2 47 | STATUS_INVALID_METHOD = 3 48 | STATUS_AUTH_FAILED = 4 49 | STATUS_INVALID_FORMAT = 5 50 | STATUS_INVALID_PARAMS = 6 51 | STATUS_INVALID_RESOURCE = 7 52 | STATUS_TOKEN_ERROR = 8 53 | STATUS_INVALID_SK = 9 54 | STATUS_INVALID_API_KEY = 10 55 | STATUS_OFFLINE = 11 56 | STATUS_SUBSCRIBERS_ONLY = 12 57 | STATUS_INVALID_SIGNATURE = 13 58 | STATUS_TOKEN_UNAUTHORIZED = 14 59 | STATUS_TOKEN_EXPIRED = 15 60 | 61 | EVENT_ATTENDING = '0' 62 | EVENT_MAYBE_ATTENDING = '1' 63 | EVENT_NOT_ATTENDING = '2' 64 | 65 | PERIOD_OVERALL = 'overall' 66 | PERIOD_7DAYS = "7day" 67 | PERIOD_3MONTHS = '3month' 68 | PERIOD_6MONTHS = '6month' 69 | PERIOD_12MONTHS = '12month' 70 | 71 | DOMAIN_ENGLISH = 0 72 | DOMAIN_GERMAN = 1 73 | DOMAIN_SPANISH = 2 74 | DOMAIN_FRENCH = 3 75 | DOMAIN_ITALIAN = 4 76 | DOMAIN_POLISH = 5 77 | DOMAIN_PORTUGUESE = 6 78 | DOMAIN_SWEDISH = 7 79 | DOMAIN_TURKISH = 8 80 | DOMAIN_RUSSIAN = 9 81 | DOMAIN_JAPANESE = 10 82 | DOMAIN_CHINESE = 11 83 | 84 | COVER_SMALL = 0 85 | COVER_MEDIUM = 1 86 | COVER_LARGE = 2 87 | COVER_EXTRA_LARGE = 3 88 | COVER_MEGA = 4 89 | 90 | IMAGES_ORDER_POPULARITY = "popularity" 91 | IMAGES_ORDER_DATE = "dateadded" 92 | 93 | 94 | USER_MALE = 'Male' 95 | USER_FEMALE = 'Female' 96 | 97 | SCROBBLE_SOURCE_USER = "P" 98 | SCROBBLE_SOURCE_NON_PERSONALIZED_BROADCAST = "R" 99 | SCROBBLE_SOURCE_PERSONALIZED_BROADCAST = "E" 100 | SCROBBLE_SOURCE_LASTFM = "L" 101 | SCROBBLE_SOURCE_UNKNOWN = "U" 102 | 103 | SCROBBLE_MODE_PLAYED = "" 104 | SCROBBLE_MODE_LOVED = "L" 105 | SCROBBLE_MODE_BANNED = "B" 106 | SCROBBLE_MODE_SKIPPED = "S" 107 | 108 | """ 109 | A list of the implemented webservices (from http://www.last.fm/api/intro) 110 | ===================================== 111 | # Album 112 | 113 | * album.addTags DONE 114 | * album.getInfo DONE 115 | * album.getTags DONE 116 | * album.removeTag DONE 117 | * album.search DONE 118 | 119 | # Artist 120 | 121 | * artist.addTags DONE 122 | * artist.getEvents DONE 123 | * artist.getImages DONE 124 | * artist.getInfo DONE 125 | * artist.getPodcast TODO 126 | * artist.getShouts DONE 127 | * artist.getSimilar DONE 128 | * artist.getTags DONE 129 | * artist.getTopAlbums DONE 130 | * artist.getTopFans DONE 131 | * artist.getTopTags DONE 132 | * artist.getTopTracks DONE 133 | * artist.removeTag DONE 134 | * artist.search DONE 135 | * artist.share DONE 136 | * artist.shout DONE 137 | 138 | # Auth 139 | 140 | * auth.getMobileSession DONE 141 | * auth.getSession DONE 142 | * auth.getToken DONE 143 | 144 | # Event 145 | 146 | * event.attend DONE 147 | * event.getAttendees DONE 148 | * event.getInfo DONE 149 | * event.getShouts DONE 150 | * event.share DONE 151 | * event.shout DONE 152 | 153 | # Geo 154 | 155 | * geo.getEvents 156 | * geo.getTopArtists 157 | * geo.getTopTracks 158 | 159 | # Group 160 | 161 | * group.getMembers DONE 162 | * group.getWeeklyAlbumChart DONE 163 | * group.getWeeklyArtistChart DONE 164 | * group.getWeeklyChartList DONE 165 | * group.getWeeklyTrackChart DONE 166 | 167 | # Library 168 | 169 | * library.addAlbum DONE 170 | * library.addArtist DONE 171 | * library.addTrack DONE 172 | * library.getAlbums DONE 173 | * library.getArtists DONE 174 | * library.getTracks DONE 175 | 176 | # Playlist 177 | 178 | * playlist.addTrack DONE 179 | * playlist.create DONE 180 | * playlist.fetch DONE 181 | 182 | # Radio 183 | 184 | * radio.getPlaylist 185 | * radio.tune 186 | 187 | # Tag 188 | 189 | * tag.getSimilar DONE 190 | * tag.getTopAlbums DONE 191 | * tag.getTopArtists DONE 192 | * tag.getTopTags DONE 193 | * tag.getTopTracks DONE 194 | * tag.getWeeklyArtistChart DONE 195 | * tag.getWeeklyChartList DONE 196 | * tag.search DONE 197 | 198 | # Tasteometer 199 | 200 | * tasteometer.compare DONE 201 | 202 | # Track 203 | 204 | * track.addTags DONE 205 | * track.ban DONE 206 | * track.getInfo DONE 207 | * track.getSimilar DONE 208 | * track.getTags DONE 209 | * track.getTopFans DONE 210 | * track.getTopTags DONE 211 | * track.love DONE 212 | * track.removeTag DONE 213 | * track.search DONE 214 | * track.share DONE 215 | 216 | # User 217 | 218 | * user.getEvents DONE 219 | * user.getFriends DONE 220 | * user.getInfo DONE 221 | * user.getLovedTracks DONE 222 | * user.getNeighbours DONE 223 | * user.getPastEvents DONE 224 | * user.getPlaylists DONE 225 | * user.getRecentStations TODO 226 | * user.getRecentTracks DONE 227 | * user.getRecommendedArtists DONE 228 | * user.getRecommendedEvents DONE 229 | * user.getShouts DONE 230 | * user.getTopAlbums DONE 231 | * user.getTopArtists DONE 232 | * user.getTopTags DONE 233 | * user.getTopTracks DONE 234 | * user.getWeeklyAlbumChart DONE 235 | * user.getWeeklyArtistChart DONE 236 | * user.getWeeklyChartList DONE 237 | * user.getWeeklyTrackChart DONE 238 | * user.shout DONE 239 | 240 | # Venue 241 | 242 | * venue.getEvents DONE 243 | * venue.getPastEvents DONE 244 | * venue.search DONE 245 | """ 246 | 247 | class Network(object): 248 | """ 249 | A music social network website that is Last.fm or one exposing a Last.fm compatible API 250 | """ 251 | 252 | def __init__(self, name, homepage, ws_server, api_key, api_secret, session_key, submission_server, username, password_hash, 253 | domain_names, urls): 254 | """ 255 | name: the name of the network 256 | homepage: the homepage url 257 | ws_server: the url of the webservices server 258 | api_key: a provided API_KEY 259 | api_secret: a provided API_SECRET 260 | session_key: a generated session_key or None 261 | submission_server: the url of the server to which tracks are submitted (scrobbled) 262 | username: a username of a valid user 263 | password_hash: the output of pylast.md5(password) where password is the user's password thingy 264 | domain_names: a dict mapping each DOMAIN_* value to a string domain name 265 | urls: a dict mapping types to urls 266 | 267 | if username and password_hash were provided and not session_key, session_key will be 268 | generated automatically when needed. 269 | 270 | Either a valid session_key or a combination of username and password_hash must be present for scrobbling. 271 | 272 | You should use a preconfigured network object through a get_*_network(...) method instead of creating an object 273 | of this class, unless you know what you're doing. 274 | """ 275 | 276 | self.ws_server = ws_server 277 | self.submission_server = submission_server 278 | self.name = name 279 | self.homepage = homepage 280 | self.api_key = api_key 281 | self.api_secret = api_secret 282 | self.session_key = session_key 283 | self.username = username 284 | self.password_hash = password_hash 285 | self.domain_names = domain_names 286 | self.urls = urls 287 | 288 | self.cache_backend = None 289 | self.proxy_enabled = False 290 | self.proxy = None 291 | self.last_call_time = 0 292 | 293 | #generate a session_key if necessary 294 | if (self.api_key and self.api_secret) and not self.session_key and (self.username and self.password_hash): 295 | sk_gen = SessionKeyGenerator(self) 296 | self.session_key = sk_gen.get_session_key(self.username, self.password_hash) 297 | 298 | def get_artist(self, artist_name): 299 | """ 300 | Return an Artist object 301 | """ 302 | 303 | return Artist(artist_name, self) 304 | 305 | def get_track(self, artist, title): 306 | """ 307 | Return a Track object 308 | """ 309 | 310 | return Track(artist, title, self) 311 | 312 | def get_album(self, artist, title): 313 | """ 314 | Return an Album object 315 | """ 316 | 317 | return Album(artist, title, self) 318 | 319 | def get_authenticated_user(self): 320 | """ 321 | Returns the authenticated user 322 | """ 323 | 324 | return AuthenticatedUser(self) 325 | 326 | def get_country(self, country_name): 327 | """ 328 | Returns a country object 329 | """ 330 | 331 | return Country(country_name, self) 332 | 333 | def get_group(self, name): 334 | """ 335 | Returns a Group object 336 | """ 337 | 338 | return Group(name, self) 339 | 340 | def get_user(self, username): 341 | """ 342 | Returns a user object 343 | """ 344 | 345 | return User(username, self) 346 | 347 | def get_tag(self, name): 348 | """ 349 | Returns a tag object 350 | """ 351 | 352 | return Tag(name, self) 353 | 354 | def get_scrobbler(self, client_id, client_version): 355 | """ 356 | Returns a Scrobbler object used for submitting tracks to the server 357 | 358 | Quote from http://www.last.fm/api/submissions: 359 | ======== 360 | Client identifiers are used to provide a centrally managed database of 361 | the client versions, allowing clients to be banned if they are found to 362 | be behaving undesirably. The client ID is associated with a version 363 | number on the server, however these are only incremented if a client is 364 | banned and do not have to reflect the version of the actual client application. 365 | 366 | During development, clients which have not been allocated an identifier should 367 | use the identifier tst, with a version number of 1.0. Do not distribute code or 368 | client implementations which use this test identifier. Do not use the identifiers 369 | used by other clients. 370 | ========= 371 | 372 | To obtain a new client identifier please contact: 373 | * Last.fm: submissions@last.fm 374 | * # TODO: list others 375 | 376 | ...and provide us with the name of your client and its homepage address. 377 | """ 378 | 379 | return Scrobbler(self, client_id, client_version) 380 | 381 | def _get_language_domain(self, domain_language): 382 | """ 383 | Returns the mapped domain name of the network to a DOMAIN_* value 384 | """ 385 | 386 | if domain_language in self.domain_names: 387 | return self.domain_names[domain_language] 388 | 389 | def _get_url(self, domain, type): 390 | return "http://%s/%s" %(self._get_language_domain(domain), self.urls[type]) 391 | 392 | def _get_ws_auth(self): 393 | """ 394 | Returns a (API_KEY, API_SECRET, SESSION_KEY) tuple. 395 | """ 396 | return (self.api_key, self.api_secret, self.session_key) 397 | 398 | def _delay_call(self): 399 | """ 400 | Makes sure that web service calls are at least a second apart 401 | """ 402 | 403 | # delay time in seconds 404 | DELAY_TIME = 1.0 405 | now = time.time() 406 | 407 | if (now - self.last_call_time) < DELAY_TIME: 408 | time.sleep(1) 409 | 410 | self.last_call_time = now 411 | 412 | def create_new_playlist(self, title, description): 413 | """ 414 | Creates a playlist for the authenticated user and returns it 415 | title: The title of the new playlist. 416 | description: The description of the new playlist. 417 | """ 418 | 419 | params = {} 420 | params['title'] = _unicode(title) 421 | params['description'] = _unicode(description) 422 | 423 | doc = _Request(self, 'playlist.create', params).execute(False) 424 | 425 | e_id = doc.getElementsByTagName("id")[0].firstChild.data 426 | user = doc.getElementsByTagName('playlists')[0].getAttribute('user') 427 | 428 | return Playlist(user, e_id, self) 429 | 430 | def get_top_tags(self, limit=None): 431 | """Returns a sequence of the most used tags as a sequence of TopItem objects.""" 432 | 433 | doc = _Request(self, "tag.getTopTags").execute(True) 434 | seq = [] 435 | for node in doc.getElementsByTagName("tag"): 436 | tag = Tag(_extract(node, "name"), self) 437 | weight = _number(_extract(node, "count")) 438 | 439 | if len(seq) < limit: 440 | seq.append(TopItem(tag, weight)) 441 | 442 | return seq 443 | 444 | def enable_proxy(self, host, port): 445 | """Enable a default web proxy""" 446 | 447 | self.proxy = [host, _number(port)] 448 | self.proxy_enabled = True 449 | 450 | def disable_proxy(self): 451 | """Disable using the web proxy""" 452 | 453 | self.proxy_enabled = False 454 | 455 | def is_proxy_enabled(self): 456 | """Returns True if a web proxy is enabled.""" 457 | 458 | return self.proxy_enabled 459 | 460 | def _get_proxy(self): 461 | """Returns proxy details.""" 462 | 463 | return self.proxy 464 | 465 | def enable_caching(self, file_path = None): 466 | """Enables caching request-wide for all cachable calls. 467 | In choosing the backend used for caching, it will try _SqliteCacheBackend first if 468 | the module sqlite3 is present. If not, it will fallback to _ShelfCacheBackend which uses shelve.Shelf objects. 469 | 470 | * file_path: A file path for the backend storage file. If 471 | None set, a temp file would probably be created, according the backend. 472 | """ 473 | 474 | if not file_path: 475 | file_path = tempfile.mktemp(prefix="pylast_tmp_") 476 | 477 | self.cache_backend = _ShelfCacheBackend(file_path) 478 | 479 | def disable_caching(self): 480 | """Disables all caching features.""" 481 | 482 | self.cache_backend = None 483 | 484 | def is_caching_enabled(self): 485 | """Returns True if caching is enabled.""" 486 | 487 | return not (self.cache_backend == None) 488 | 489 | def _get_cache_backend(self): 490 | 491 | return self.cache_backend 492 | 493 | def search_for_album(self, album_name): 494 | """Searches for an album by its name. Returns a AlbumSearch object. 495 | Use get_next_page() to retreive sequences of results.""" 496 | 497 | return AlbumSearch(album_name, self) 498 | 499 | def search_for_artist(self, artist_name): 500 | """Searches of an artist by its name. Returns a ArtistSearch object. 501 | Use get_next_page() to retreive sequences of results.""" 502 | 503 | return ArtistSearch(artist_name, self) 504 | 505 | def search_for_tag(self, tag_name): 506 | """Searches of a tag by its name. Returns a TagSearch object. 507 | Use get_next_page() to retreive sequences of results.""" 508 | 509 | return TagSearch(tag_name, self) 510 | 511 | def search_for_track(self, artist_name, track_name): 512 | """Searches of a track by its name and its artist. Set artist to an empty string if not available. 513 | Returns a TrackSearch object. 514 | Use get_next_page() to retreive sequences of results.""" 515 | 516 | return TrackSearch(artist_name, track_name, self) 517 | 518 | def search_for_venue(self, venue_name, country_name): 519 | """Searches of a venue by its name and its country. Set country_name to an empty string if not available. 520 | Returns a VenueSearch object. 521 | Use get_next_page() to retreive sequences of results.""" 522 | 523 | return VenueSearch(venue_name, country_name, self) 524 | 525 | def get_track_by_mbid(self, mbid): 526 | """Looks up a track by its MusicBrainz ID""" 527 | 528 | params = {"mbid": _unicode(mbid)} 529 | 530 | doc = _Request(self, "track.getInfo", params).execute(True) 531 | 532 | return Track(_extract(doc, "name", 1), _extract(doc, "name"), self) 533 | 534 | def get_artist_by_mbid(self, mbid): 535 | """Loooks up an artist by its MusicBrainz ID""" 536 | 537 | params = {"mbid": _unicode(mbid)} 538 | 539 | doc = _Request(self, "artist.getInfo", params).execute(True) 540 | 541 | return Artist(_extract(doc, "name"), self) 542 | 543 | def get_album_by_mbid(self, mbid): 544 | """Looks up an album by its MusicBrainz ID""" 545 | 546 | params = {"mbid": _unicode(mbid)} 547 | 548 | doc = _Request(self, "album.getInfo", params).execute(True) 549 | 550 | return Album(_extract(doc, "artist"), _extract(doc, "name"), self) 551 | 552 | def get_lastfm_network(api_key="", api_secret="", session_key = "", username = "", password_hash = ""): 553 | """ 554 | Returns a preconfigured Network object for Last.fm 555 | 556 | api_key: a provided API_KEY 557 | api_secret: a provided API_SECRET 558 | session_key: a generated session_key or None 559 | username: a username of a valid user 560 | password_hash: the output of pylast.md5(password) where password is the user's password 561 | 562 | if username and password_hash were provided and not session_key, session_key will be 563 | generated automatically when needed. 564 | 565 | Either a valid session_key or a combination of username and password_hash must be present for scrobbling. 566 | 567 | Most read-only webservices only require an api_key and an api_secret, see about obtaining them from: 568 | http://www.last.fm/api/account 569 | """ 570 | 571 | return Network ( 572 | name = "Last.fm", 573 | homepage = "http://last.fm", 574 | ws_server = ("ws.audioscrobbler.com", "/2.0/"), 575 | api_key = api_key, 576 | api_secret = api_secret, 577 | session_key = session_key, 578 | submission_server = "http://post.audioscrobbler.com:80/", 579 | username = username, 580 | password_hash = password_hash, 581 | domain_names = { 582 | DOMAIN_ENGLISH: 'www.last.fm', 583 | DOMAIN_GERMAN: 'www.lastfm.de', 584 | DOMAIN_SPANISH: 'www.lastfm.es', 585 | DOMAIN_FRENCH: 'www.lastfm.fr', 586 | DOMAIN_ITALIAN: 'www.lastfm.it', 587 | DOMAIN_POLISH: 'www.lastfm.pl', 588 | DOMAIN_PORTUGUESE: 'www.lastfm.com.br', 589 | DOMAIN_SWEDISH: 'www.lastfm.se', 590 | DOMAIN_TURKISH: 'www.lastfm.com.tr', 591 | DOMAIN_RUSSIAN: 'www.lastfm.ru', 592 | DOMAIN_JAPANESE: 'www.lastfm.jp', 593 | DOMAIN_CHINESE: 'cn.last.fm', 594 | }, 595 | urls = { 596 | "album": "music/%(artist)s/%(album)s", 597 | "artist": "music/%(artist)s", 598 | "event": "event/%(id)s", 599 | "country": "place/%(country_name)s", 600 | "playlist": "user/%(user)s/library/playlists/%(appendix)s", 601 | "tag": "tag/%(name)s", 602 | "track": "music/%(artist)s/_/%(title)s", 603 | "group": "group/%(name)s", 604 | "user": "user/%(name)s", 605 | } 606 | ) 607 | 608 | def get_librefm_network(api_key="", api_secret="", session_key = "", username = "", password_hash = ""): 609 | """ 610 | Returns a preconfigured Network object for Libre.fm 611 | 612 | api_key: a provided API_KEY 613 | api_secret: a provided API_SECRET 614 | session_key: a generated session_key or None 615 | username: a username of a valid user 616 | password_hash: the output of pylast.md5(password) where password is the user's password 617 | 618 | if username and password_hash were provided and not session_key, session_key will be 619 | generated automatically when needed. 620 | """ 621 | 622 | return Network ( 623 | name = "Libre.fm", 624 | homepage = "http://alpha.dev.libre.fm", 625 | ws_server = ("alpha.dev.libre.fm", "/2.0/"), 626 | api_key = api_key, 627 | api_secret = api_secret, 628 | session_key = session_key, 629 | submission_server = "http://turtle.libre.fm:80/", 630 | username = username, 631 | password_hash = password_hash, 632 | domain_names = { 633 | DOMAIN_ENGLISH: "alpha.dev.libre.fm", 634 | DOMAIN_GERMAN: "alpha.dev.libre.fm", 635 | DOMAIN_SPANISH: "alpha.dev.libre.fm", 636 | DOMAIN_FRENCH: "alpha.dev.libre.fm", 637 | DOMAIN_ITALIAN: "alpha.dev.libre.fm", 638 | DOMAIN_POLISH: "alpha.dev.libre.fm", 639 | DOMAIN_PORTUGUESE: "alpha.dev.libre.fm", 640 | DOMAIN_SWEDISH: "alpha.dev.libre.fm", 641 | DOMAIN_TURKISH: "alpha.dev.libre.fm", 642 | DOMAIN_RUSSIAN: "alpha.dev.libre.fm", 643 | DOMAIN_JAPANESE: "alpha.dev.libre.fm", 644 | DOMAIN_CHINESE: "alpha.dev.libre.fm", 645 | }, 646 | urls = { 647 | "album": "artist/%(artist)s/album/%(album)s", 648 | "artist": "artist/%(artist)s", 649 | "event": "event/%(id)s", 650 | "country": "place/%(country_name)s", 651 | "playlist": "user/%(user)s/library/playlists/%(appendix)s", 652 | "tag": "tag/%(name)s", 653 | "track": "music/%(artist)s/_/%(title)s", 654 | "group": "group/%(name)s", 655 | "user": "user/%(name)s", 656 | } 657 | ) 658 | 659 | class _ShelfCacheBackend(object): 660 | """Used as a backend for caching cacheable requests.""" 661 | def __init__(self, file_path = None): 662 | self.shelf = shelve.open(file_path) 663 | 664 | def get_xml(self, key): 665 | return self.shelf[key] 666 | 667 | def set_xml(self, key, xml_string): 668 | self.shelf[key] = xml_string 669 | 670 | def has_key(self, key): 671 | return key in self.shelf.keys() 672 | 673 | class _ThreadedCall(threading.Thread): 674 | """Facilitates calling a function on another thread.""" 675 | 676 | def __init__(self, sender, funct, funct_args, callback, callback_args): 677 | 678 | threading.Thread.__init__(self) 679 | 680 | self.funct = funct 681 | self.funct_args = funct_args 682 | self.callback = callback 683 | self.callback_args = callback_args 684 | 685 | self.sender = sender 686 | 687 | def run(self): 688 | 689 | output = [] 690 | 691 | if self.funct: 692 | if self.funct_args: 693 | output = self.funct(*self.funct_args) 694 | else: 695 | output = self.funct() 696 | 697 | if self.callback: 698 | if self.callback_args: 699 | self.callback(self.sender, output, *self.callback_args) 700 | else: 701 | self.callback(self.sender, output) 702 | 703 | class _Request(object): 704 | """Representing an abstract web service operation.""" 705 | 706 | def __init__(self, network, method_name, params = {}): 707 | 708 | self.params = params 709 | self.network = network 710 | 711 | (self.api_key, self.api_secret, self.session_key) = network._get_ws_auth() 712 | 713 | self.params["api_key"] = self.api_key 714 | self.params["method"] = method_name 715 | 716 | if network.is_caching_enabled(): 717 | self.cache = network._get_cache_backend() 718 | 719 | if self.session_key: 720 | self.params["sk"] = self.session_key 721 | self.sign_it() 722 | 723 | def sign_it(self): 724 | """Sign this request.""" 725 | 726 | if not "api_sig" in self.params.keys(): 727 | self.params['api_sig'] = self._get_signature() 728 | 729 | def _get_signature(self): 730 | """Returns a 32-character hexadecimal md5 hash of the signature string.""" 731 | 732 | keys = self.params.keys()[:] 733 | 734 | keys.sort() 735 | 736 | string = "" 737 | 738 | for name in keys: 739 | string += name 740 | string += self.params[name] 741 | 742 | string += self.api_secret 743 | 744 | return md5(string) 745 | 746 | def _get_cache_key(self): 747 | """The cache key is a string of concatenated sorted names and values.""" 748 | 749 | keys = self.params.keys() 750 | keys.sort() 751 | 752 | cache_key = str() 753 | 754 | for key in keys: 755 | if key != "api_sig" and key != "api_key" and key != "sk": 756 | cache_key += key + _string(self.params[key]) 757 | 758 | return hashlib.sha1(cache_key).hexdigest() 759 | 760 | def _get_cached_response(self): 761 | """Returns a file object of the cached response.""" 762 | 763 | if not self._is_cached(): 764 | response = self._download_response() 765 | self.cache.set_xml(self._get_cache_key(), response) 766 | 767 | return self.cache.get_xml(self._get_cache_key()) 768 | 769 | def _is_cached(self): 770 | """Returns True if the request is already in cache.""" 771 | 772 | return self.cache.has_key(self._get_cache_key()) 773 | 774 | def _download_response(self): 775 | """Returns a response body string from the server.""" 776 | 777 | # Delay the call if necessary 778 | #self.network._delay_call() # enable it if you want. 779 | 780 | data = [] 781 | for name in self.params.keys(): 782 | data.append('='.join((name, urllib.quote_plus(_string(self.params[name]))))) 783 | data = '&'.join(data) 784 | 785 | headers = { 786 | "Content-type": "application/x-www-form-urlencoded", 787 | 'Accept-Charset': 'utf-8', 788 | 'User-Agent': "pylast" + '/' + __version__ 789 | } 790 | 791 | (HOST_NAME, HOST_SUBDIR) = self.network.ws_server 792 | 793 | if self.network.is_proxy_enabled(): 794 | conn = httplib.HTTPConnection(host = self._get_proxy()[0], port = self._get_proxy()[1]) 795 | conn.request(method='POST', url="http://" + HOST_NAME + HOST_SUBDIR, 796 | body=data, headers=headers) 797 | else: 798 | conn = httplib.HTTPConnection(host=HOST_NAME) 799 | conn.request(method='POST', url=HOST_SUBDIR, body=data, headers=headers) 800 | 801 | response = conn.getresponse() 802 | response_text = _unicode(response.read()) 803 | self._check_response_for_errors(response_text) 804 | return response_text 805 | 806 | def execute(self, cacheable = False): 807 | """Returns the XML DOM response of the POST Request from the server""" 808 | 809 | if self.network.is_caching_enabled() and cacheable: 810 | response = self._get_cached_response() 811 | else: 812 | response = self._download_response() 813 | 814 | return minidom.parseString(_string(response)) 815 | 816 | def _check_response_for_errors(self, response): 817 | """Checks the response for errors and raises one if any exists.""" 818 | 819 | doc = minidom.parseString(_string(response)) 820 | e = doc.getElementsByTagName('lfm')[0] 821 | 822 | if e.getAttribute('status') != "ok": 823 | e = doc.getElementsByTagName('error')[0] 824 | status = e.getAttribute('code') 825 | details = e.firstChild.data.strip() 826 | raise WSError(self.network, status, details) 827 | 828 | class SessionKeyGenerator(object): 829 | """Methods of generating a session key: 830 | 1) Web Authentication: 831 | a. network = get_*_network(API_KEY, API_SECRET) 832 | b. sg = SessionKeyGenerator(network) 833 | c. url = sg.get_web_auth_url() 834 | d. Ask the user to open the url and authorize you, and wait for it. 835 | e. session_key = sg.get_web_auth_session_key(url) 836 | 2) Username and Password Authentication: 837 | a. network = get_*_network(API_KEY, API_SECRET) 838 | b. username = raw_input("Please enter your username: ") 839 | c. password_hash = pylast.md5(raw_input("Please enter your password: ") 840 | d. session_key = SessionKeyGenerator(network).get_session_key(username, password_hash) 841 | 842 | A session key's lifetime is infinie, unless the user provokes the rights of the given API Key. 843 | 844 | If you create a Network object with just a API_KEY and API_SECRET and a username and a password_hash, a 845 | SESSION_KEY will be automatically generated for that network and stored in it so you don't have to do this 846 | manually, unless you want to. 847 | """ 848 | 849 | def __init__(self, network): 850 | self.network = network 851 | self.web_auth_tokens = {} 852 | 853 | def _get_web_auth_token(self): 854 | """Retrieves a token from the network for web authentication. 855 | The token then has to be authorized from getAuthURL before creating session. 856 | """ 857 | 858 | request = _Request(self.network, 'auth.getToken') 859 | 860 | # default action is that a request is signed only when 861 | # a session key is provided. 862 | request.sign_it() 863 | 864 | doc = request.execute() 865 | 866 | e = doc.getElementsByTagName('token')[0] 867 | return e.firstChild.data 868 | 869 | def get_web_auth_url(self): 870 | """The user must open this page, and you first, then call get_web_auth_session_key(url) after that.""" 871 | 872 | token = self._get_web_auth_token() 873 | 874 | url = '%(homepage)s/api/auth/?api_key=%(api)s&token=%(token)s' % \ 875 | {"homepage": self.network.homepage, "api": self.network.api_key, "token": token} 876 | 877 | self.web_auth_tokens[url] = token 878 | 879 | return url 880 | 881 | def get_web_auth_session_key(self, url): 882 | """Retrieves the session key of a web authorization process by its url.""" 883 | 884 | if url in self.web_auth_tokens.keys(): 885 | token = self.web_auth_tokens[url] 886 | else: 887 | token = "" #that's gonna raise a WSError of an unauthorized token when the request is executed. 888 | 889 | request = _Request(self.network, 'auth.getSession', {'token': token}) 890 | 891 | # default action is that a request is signed only when 892 | # a session key is provided. 893 | request.sign_it() 894 | 895 | doc = request.execute() 896 | 897 | return doc.getElementsByTagName('key')[0].firstChild.data 898 | 899 | def get_session_key(self, username, password_hash): 900 | """Retrieve a session key with a username and a md5 hash of the user's password.""" 901 | 902 | params = {"username": username, "authToken": md5(username + password_hash)} 903 | request = _Request(self.network, "auth.getMobileSession", params) 904 | 905 | # default action is that a request is signed only when 906 | # a session key is provided. 907 | request.sign_it() 908 | 909 | doc = request.execute() 910 | 911 | return _extract(doc, "key") 912 | 913 | def _namedtuple(name, children): 914 | """ 915 | collections.namedtuple is available in (python >= 2.6) 916 | """ 917 | 918 | v = sys.version_info 919 | if v[1] >= 6 and v[0] < 3: 920 | return collections.namedtuple(name, children) 921 | else: 922 | def fancydict(*args): 923 | d = {} 924 | i = 0 925 | for child in children: 926 | d[child.strip()] = args[i] 927 | i += 1 928 | return d 929 | 930 | return fancydict 931 | 932 | TopItem = _namedtuple("TopItem", ["item", "weight"]) 933 | SimilarItem = _namedtuple("SimilarItem", ["item", "match"]) 934 | LibraryItem = _namedtuple("LibraryItem", ["item", "playcount", "tagcount"]) 935 | PlayedTrack = _namedtuple("PlayedTrack", ["track", "playback_date", "timestamp"]) 936 | LovedTrack = _namedtuple("LovedTrack", ["track", "date", "timestamp"]) 937 | ImageSizes = _namedtuple("ImageSizes", ["original", "large", "largesquare", "medium", "small", "extralarge"]) 938 | Image = _namedtuple("Image", ["title", "url", "dateadded", "format", "owner", "sizes", "votes"]) 939 | Shout = _namedtuple("Shout", ["body", "author", "date"]) 940 | 941 | def _string_output(funct): 942 | def r(*args): 943 | return _string(funct(*args)) 944 | 945 | return r 946 | 947 | def _pad_list(given_list, desired_length, padding = None): 948 | """ 949 | Pads a list to be of the desired_length. 950 | """ 951 | 952 | while len(given_list) < desired_length: 953 | given_list.append(padding) 954 | 955 | return given_list 956 | 957 | class _BaseObject(object): 958 | """An abstract webservices object.""" 959 | 960 | network = None 961 | 962 | def __init__(self, network): 963 | self.network = network 964 | 965 | def _request(self, method_name, cacheable = False, params = None): 966 | if not params: 967 | params = self._get_params() 968 | 969 | return _Request(self.network, method_name, params).execute(cacheable) 970 | 971 | def _get_params(self): 972 | """Returns the most common set of parameters between all objects.""" 973 | 974 | return {} 975 | 976 | def __hash__(self): 977 | return hash(self.network) + \ 978 | hash(str(type(self)) + "".join(self._get_params().keys() + self._get_params().values()).lower()) 979 | 980 | class _Taggable(object): 981 | """Common functions for classes with tags.""" 982 | 983 | def __init__(self, ws_prefix): 984 | self.ws_prefix = ws_prefix 985 | 986 | def add_tags(self, *tags): 987 | """Adds one or several tags. 988 | * *tags: Any number of tag names or Tag objects. 989 | """ 990 | 991 | for tag in tags: 992 | self._add_tag(tag) 993 | 994 | def _add_tag(self, tag): 995 | """Adds one or several tags. 996 | * tag: one tag name or a Tag object. 997 | """ 998 | 999 | if isinstance(tag, Tag): 1000 | tag = tag.get_name() 1001 | 1002 | params = self._get_params() 1003 | params['tags'] = _unicode(tag) 1004 | 1005 | self._request(self.ws_prefix + '.addTags', False, params) 1006 | 1007 | def _remove_tag(self, single_tag): 1008 | """Remove a user's tag from this object.""" 1009 | 1010 | if isinstance(single_tag, Tag): 1011 | single_tag = single_tag.get_name() 1012 | 1013 | params = self._get_params() 1014 | params['tag'] = _unicode(single_tag) 1015 | 1016 | self._request(self.ws_prefix + '.removeTag', False, params) 1017 | 1018 | def get_tags(self): 1019 | """Returns a list of the tags set by the user to this object.""" 1020 | 1021 | # Uncacheable because it can be dynamically changed by the user. 1022 | params = self._get_params() 1023 | 1024 | doc = self._request(self.ws_prefix + '.getTags', False, params) 1025 | tag_names = _extract_all(doc, 'name') 1026 | tags = [] 1027 | for tag in tag_names: 1028 | tags.append(Tag(tag, self.network)) 1029 | 1030 | return tags 1031 | 1032 | def remove_tags(self, *tags): 1033 | """Removes one or several tags from this object. 1034 | * *tags: Any number of tag names or Tag objects. 1035 | """ 1036 | 1037 | for tag in tags: 1038 | self._remove_tag(tag) 1039 | 1040 | def clear_tags(self): 1041 | """Clears all the user-set tags. """ 1042 | 1043 | self.remove_tags(*(self.get_tags())) 1044 | 1045 | def set_tags(self, *tags): 1046 | """Sets this object's tags to only those tags. 1047 | * *tags: any number of tag names. 1048 | """ 1049 | 1050 | c_old_tags = [] 1051 | old_tags = [] 1052 | c_new_tags = [] 1053 | new_tags = [] 1054 | 1055 | to_remove = [] 1056 | to_add = [] 1057 | 1058 | tags_on_server = self.get_tags() 1059 | 1060 | for tag in tags_on_server: 1061 | c_old_tags.append(tag.get_name().lower()) 1062 | old_tags.append(tag.get_name()) 1063 | 1064 | for tag in tags: 1065 | c_new_tags.append(tag.lower()) 1066 | new_tags.append(tag) 1067 | 1068 | for i in range(0, len(old_tags)): 1069 | if not c_old_tags[i] in c_new_tags: 1070 | to_remove.append(old_tags[i]) 1071 | 1072 | for i in range(0, len(new_tags)): 1073 | if not c_new_tags[i] in c_old_tags: 1074 | to_add.append(new_tags[i]) 1075 | 1076 | self.remove_tags(*to_remove) 1077 | self.add_tags(*to_add) 1078 | 1079 | def get_top_tags(self, limit = None): 1080 | """Returns a list of the most frequently used Tags on this object.""" 1081 | 1082 | doc = self._request(self.ws_prefix + '.getTopTags', True) 1083 | 1084 | elements = doc.getElementsByTagName('tag') 1085 | seq = [] 1086 | 1087 | for element in elements: 1088 | if limit and len(seq) >= limit: 1089 | break 1090 | tag_name = _extract(element, 'name') 1091 | tagcount = _extract(element, 'count') 1092 | 1093 | seq.append(TopItem(Tag(tag_name, self.network), tagcount)) 1094 | 1095 | return seq 1096 | 1097 | class WSError(Exception): 1098 | """Exception related to the Network web service""" 1099 | 1100 | def __init__(self, network, status, details): 1101 | self.status = status 1102 | self.details = details 1103 | self.network = network 1104 | 1105 | @_string_output 1106 | def __str__(self): 1107 | return self.details 1108 | 1109 | def get_id(self): 1110 | """Returns the exception ID, from one of the following: 1111 | STATUS_INVALID_SERVICE = 2 1112 | STATUS_INVALID_METHOD = 3 1113 | STATUS_AUTH_FAILED = 4 1114 | STATUS_INVALID_FORMAT = 5 1115 | STATUS_INVALID_PARAMS = 6 1116 | STATUS_INVALID_RESOURCE = 7 1117 | STATUS_TOKEN_ERROR = 8 1118 | STATUS_INVALID_SK = 9 1119 | STATUS_INVALID_API_KEY = 10 1120 | STATUS_OFFLINE = 11 1121 | STATUS_SUBSCRIBERS_ONLY = 12 1122 | STATUS_TOKEN_UNAUTHORIZED = 14 1123 | STATUS_TOKEN_EXPIRED = 15 1124 | """ 1125 | 1126 | return self.status 1127 | 1128 | class Album(_BaseObject, _Taggable): 1129 | """An album.""" 1130 | 1131 | title = None 1132 | artist = None 1133 | 1134 | def __init__(self, artist, title, network): 1135 | """ 1136 | Create an album instance. 1137 | # Parameters: 1138 | * artist: An artist name or an Artist object. 1139 | * title: The album title. 1140 | """ 1141 | 1142 | _BaseObject.__init__(self, network) 1143 | _Taggable.__init__(self, 'album') 1144 | 1145 | if isinstance(artist, Artist): 1146 | self.artist = artist 1147 | else: 1148 | self.artist = Artist(artist, self.network) 1149 | 1150 | self.title = title 1151 | 1152 | @_string_output 1153 | def __repr__(self): 1154 | return u"%s - %s" %(self.get_artist().get_name(), self.get_title()) 1155 | 1156 | def __eq__(self, other): 1157 | return (self.get_title().lower() == other.get_title().lower()) and (self.get_artist().get_name().lower() == other.get_artist().get_name().lower()) 1158 | 1159 | def __ne__(self, other): 1160 | return (self.get_title().lower() != other.get_title().lower()) or (self.get_artist().get_name().lower() != other.get_artist().get_name().lower()) 1161 | 1162 | def _get_params(self): 1163 | return {'artist': self.get_artist().get_name(), 'album': self.get_title(), } 1164 | 1165 | def get_artist(self): 1166 | """Returns the associated Artist object.""" 1167 | 1168 | return self.artist 1169 | 1170 | def get_title(self): 1171 | """Returns the album title.""" 1172 | 1173 | return self.title 1174 | 1175 | def get_name(self): 1176 | """Returns the album title (alias to Album.get_title).""" 1177 | 1178 | return self.get_title() 1179 | 1180 | def get_release_date(self): 1181 | """Retruns the release date of the album.""" 1182 | 1183 | return _extract(self._request("album.getInfo", cacheable = True), "releasedate") 1184 | 1185 | def get_cover_image(self, size = COVER_EXTRA_LARGE): 1186 | """ 1187 | Returns a uri to the cover image 1188 | size can be one of: 1189 | COVER_MEGA 1190 | COVER_EXTRA_LARGE 1191 | COVER_LARGE 1192 | COVER_MEDIUM 1193 | COVER_SMALL 1194 | """ 1195 | 1196 | return _extract_all(self._request("album.getInfo", cacheable = True), 'image')[size] 1197 | 1198 | def get_id(self): 1199 | """Returns the ID""" 1200 | 1201 | return _extract(self._request("album.getInfo", cacheable = True), "id") 1202 | 1203 | def get_playcount(self): 1204 | """Returns the number of plays on the network""" 1205 | 1206 | return _number(_extract(self._request("album.getInfo", cacheable = True), "playcount")) 1207 | 1208 | def get_listener_count(self): 1209 | """Returns the number of liteners on the network""" 1210 | 1211 | return _number(_extract(self._request("album.getInfo", cacheable = True), "listeners")) 1212 | 1213 | def get_top_tags(self, limit=None): 1214 | """Returns a list of the most-applied tags to this album.""" 1215 | 1216 | doc = self._request("album.getInfo", True) 1217 | e = doc.getElementsByTagName("toptags")[0] 1218 | 1219 | seq = [] 1220 | for name in _extract_all(e, "name"): 1221 | if len(seq) < limit: 1222 | seq.append(Tag(name, self.network)) 1223 | 1224 | return seq 1225 | 1226 | def get_tracks(self): 1227 | """Returns the list of Tracks on this album.""" 1228 | 1229 | uri = 'lastfm://playlist/album/%s' %self.get_id() 1230 | 1231 | return XSPF(uri, self.network).get_tracks() 1232 | 1233 | def get_mbid(self): 1234 | """Returns the MusicBrainz id of the album.""" 1235 | 1236 | return _extract(self._request("album.getInfo", cacheable = True), "mbid") 1237 | 1238 | def get_url(self, domain_name = DOMAIN_ENGLISH): 1239 | """Returns the url of the album page on the network. 1240 | # Parameters: 1241 | * domain_name str: The network's language domain. Possible values: 1242 | o DOMAIN_ENGLISH 1243 | o DOMAIN_GERMAN 1244 | o DOMAIN_SPANISH 1245 | o DOMAIN_FRENCH 1246 | o DOMAIN_ITALIAN 1247 | o DOMAIN_POLISH 1248 | o DOMAIN_PORTUGUESE 1249 | o DOMAIN_SWEDISH 1250 | o DOMAIN_TURKISH 1251 | o DOMAIN_RUSSIAN 1252 | o DOMAIN_JAPANESE 1253 | o DOMAIN_CHINESE 1254 | """ 1255 | 1256 | artist = _url_safe(self.get_artist().get_name()) 1257 | album = _url_safe(self.get_title()) 1258 | 1259 | return self.network._get_url(domain_name, "album") %{'artist': artist, 'album': album} 1260 | 1261 | def get_wiki_published_date(self): 1262 | """Returns the date of publishing this version of the wiki.""" 1263 | 1264 | doc = self._request("album.getInfo", True) 1265 | 1266 | if len(doc.getElementsByTagName("wiki")) == 0: 1267 | return 1268 | 1269 | node = doc.getElementsByTagName("wiki")[0] 1270 | 1271 | return _extract(node, "published") 1272 | 1273 | def get_wiki_summary(self): 1274 | """Returns the summary of the wiki.""" 1275 | 1276 | doc = self._request("album.getInfo", True) 1277 | 1278 | if len(doc.getElementsByTagName("wiki")) == 0: 1279 | return 1280 | 1281 | node = doc.getElementsByTagName("wiki")[0] 1282 | 1283 | return _extract(node, "summary") 1284 | 1285 | def get_wiki_content(self): 1286 | """Returns the content of the wiki.""" 1287 | 1288 | doc = self._request("album.getInfo", True) 1289 | 1290 | if len(doc.getElementsByTagName("wiki")) == 0: 1291 | return 1292 | 1293 | node = doc.getElementsByTagName("wiki")[0] 1294 | 1295 | return _extract(node, "content") 1296 | 1297 | class Artist(_BaseObject, _Taggable): 1298 | """An artist.""" 1299 | 1300 | name = None 1301 | 1302 | def __init__(self, name, network): 1303 | """Create an artist object. 1304 | # Parameters: 1305 | * name str: The artist's name. 1306 | """ 1307 | 1308 | _BaseObject.__init__(self, network) 1309 | _Taggable.__init__(self, 'artist') 1310 | 1311 | self.name = name 1312 | 1313 | @_string_output 1314 | def __repr__(self): 1315 | return self.get_name() 1316 | 1317 | def __eq__(self, other): 1318 | return self.get_name().lower() == other.get_name().lower() 1319 | 1320 | def __ne__(self, other): 1321 | return self.get_name().lower() != other.get_name().lower() 1322 | 1323 | def _get_params(self): 1324 | return {'artist': self.get_name()} 1325 | 1326 | def get_name(self): 1327 | """Returns the name of the artist.""" 1328 | 1329 | return self.name 1330 | 1331 | def get_cover_image(self, size = COVER_LARGE): 1332 | """ 1333 | Returns a uri to the cover image 1334 | size can be one of: 1335 | COVER_MEGA 1336 | COVER_EXTRA_LARGE 1337 | COVER_LARGE 1338 | COVER_MEDIUM 1339 | COVER_SMALL 1340 | """ 1341 | 1342 | return _extract_all(self._request("artist.getInfo", True), "image")[size] 1343 | 1344 | def get_playcount(self): 1345 | """Returns the number of plays on the network.""" 1346 | 1347 | return _number(_extract(self._request("artist.getInfo", True), "playcount")) 1348 | 1349 | def get_mbid(self): 1350 | """Returns the MusicBrainz ID of this artist.""" 1351 | 1352 | doc = self._request("artist.getInfo", True) 1353 | 1354 | return _extract(doc, "mbid") 1355 | 1356 | def get_listener_count(self): 1357 | """Returns the number of liteners on the network.""" 1358 | 1359 | return _number(_extract(self._request("artist.getInfo", True), "listeners")) 1360 | 1361 | def is_streamable(self): 1362 | """Returns True if the artist is streamable.""" 1363 | 1364 | return bool(_number(_extract(self._request("artist.getInfo", True), "streamable"))) 1365 | 1366 | def get_bio_published_date(self): 1367 | """Returns the date on which the artist's biography was published.""" 1368 | 1369 | return _extract(self._request("artist.getInfo", True), "published") 1370 | 1371 | def get_bio_summary(self): 1372 | """Returns the summary of the artist's biography.""" 1373 | 1374 | return _extract(self._request("artist.getInfo", True), "summary") 1375 | 1376 | def get_bio_content(self): 1377 | """Returns the content of the artist's biography.""" 1378 | 1379 | return _extract(self._request("artist.getInfo", True), "content") 1380 | 1381 | def get_upcoming_events(self): 1382 | """Returns a list of the upcoming Events for this artist.""" 1383 | 1384 | doc = self._request('artist.getEvents', True) 1385 | 1386 | ids = _extract_all(doc, 'id') 1387 | 1388 | events = [] 1389 | for e_id in ids: 1390 | events.append(Event(e_id, self.network)) 1391 | 1392 | return events 1393 | 1394 | def get_similar(self, limit = None): 1395 | """Returns the similar artists on the network.""" 1396 | 1397 | params = self._get_params() 1398 | if limit: 1399 | params['limit'] = _unicode(limit) 1400 | 1401 | doc = self._request('artist.getSimilar', True, params) 1402 | 1403 | names = _extract_all(doc, "name") 1404 | matches = _extract_all(doc, "match") 1405 | 1406 | artists = [] 1407 | for i in range(0, len(names)): 1408 | artists.append(SimilarItem(Artist(names[i], self.network), _number(matches[i]))) 1409 | 1410 | return artists 1411 | 1412 | def get_top_albums(self): 1413 | """Retuns a list of the top albums.""" 1414 | 1415 | doc = self._request('artist.getTopAlbums', True) 1416 | 1417 | seq = [] 1418 | 1419 | for node in doc.getElementsByTagName("album"): 1420 | name = _extract(node, "name") 1421 | artist = _extract(node, "name", 1) 1422 | playcount = _extract(node, "playcount") 1423 | 1424 | seq.append(TopItem(Album(artist, name, self.network), playcount)) 1425 | 1426 | return seq 1427 | 1428 | def get_top_tracks(self): 1429 | """Returns a list of the most played Tracks by this artist.""" 1430 | 1431 | doc = self._request("artist.getTopTracks", True) 1432 | 1433 | seq = [] 1434 | for track in doc.getElementsByTagName('track'): 1435 | 1436 | title = _extract(track, "name") 1437 | artist = _extract(track, "name", 1) 1438 | playcount = _number(_extract(track, "playcount")) 1439 | 1440 | seq.append( TopItem(Track(artist, title, self.network), playcount) ) 1441 | 1442 | return seq 1443 | 1444 | def get_top_fans(self, limit = None): 1445 | """Returns a list of the Users who played this artist the most. 1446 | # Parameters: 1447 | * limit int: Max elements. 1448 | """ 1449 | 1450 | doc = self._request('artist.getTopFans', True) 1451 | 1452 | seq = [] 1453 | 1454 | elements = doc.getElementsByTagName('user') 1455 | 1456 | for element in elements: 1457 | if limit and len(seq) >= limit: 1458 | break 1459 | 1460 | name = _extract(element, 'name') 1461 | weight = _number(_extract(element, 'weight')) 1462 | 1463 | seq.append(TopItem(User(name, self.network), weight)) 1464 | 1465 | return seq 1466 | 1467 | def share(self, users, message = None): 1468 | """Shares this artist (sends out recommendations). 1469 | # Parameters: 1470 | * users [User|str,]: A list that can contain usernames, emails, User objects, or all of them. 1471 | * message str: A message to include in the recommendation message. 1472 | """ 1473 | 1474 | #last.fm currently accepts a max of 10 recipient at a time 1475 | while(len(users) > 10): 1476 | section = users[0:9] 1477 | users = users[9:] 1478 | self.share(section, message) 1479 | 1480 | nusers = [] 1481 | for user in users: 1482 | if isinstance(user, User): 1483 | nusers.append(user.get_name()) 1484 | else: 1485 | nusers.append(user) 1486 | 1487 | params = self._get_params() 1488 | recipients = ','.join(nusers) 1489 | params['recipient'] = recipients 1490 | if message: params['message'] = _unicode(message) 1491 | 1492 | self._request('artist.share', False, params) 1493 | 1494 | def get_url(self, domain_name = DOMAIN_ENGLISH): 1495 | """Returns the url of the artist page on the network. 1496 | # Parameters: 1497 | * domain_name: The network's language domain. Possible values: 1498 | o DOMAIN_ENGLISH 1499 | o DOMAIN_GERMAN 1500 | o DOMAIN_SPANISH 1501 | o DOMAIN_FRENCH 1502 | o DOMAIN_ITALIAN 1503 | o DOMAIN_POLISH 1504 | o DOMAIN_PORTUGUESE 1505 | o DOMAIN_SWEDISH 1506 | o DOMAIN_TURKISH 1507 | o DOMAIN_RUSSIAN 1508 | o DOMAIN_JAPANESE 1509 | o DOMAIN_CHINESE 1510 | """ 1511 | 1512 | artist = _url_safe(self.get_name()) 1513 | 1514 | return self.network._get_url(domain_name, "artist") %{'artist': artist} 1515 | 1516 | def get_images(self, order=IMAGES_ORDER_POPULARITY, limit=None): 1517 | """ 1518 | Returns a sequence of Image objects 1519 | if limit is None it will return all 1520 | order can be IMAGES_ORDER_POPULARITY or IMAGES_ORDER_DATE 1521 | """ 1522 | 1523 | images = [] 1524 | 1525 | params = self._get_params() 1526 | params["order"] = order 1527 | nodes = _collect_nodes(limit, self, "artist.getImages", True, params) 1528 | for e in nodes: 1529 | if _extract(e, "name"): 1530 | user = User(_extract(e, "name"), self.network) 1531 | else: 1532 | user = None 1533 | 1534 | images.append(Image( 1535 | _extract(e, "title"), 1536 | _extract(e, "url"), 1537 | _extract(e, "dateadded"), 1538 | _extract(e, "format"), 1539 | user, 1540 | ImageSizes(*_extract_all(e, "size")), 1541 | (_extract(e, "thumbsup"), _extract(e, "thumbsdown")) 1542 | ) 1543 | ) 1544 | return images 1545 | 1546 | def get_shouts(self, limit=50): 1547 | """ 1548 | Returns a sequqence of Shout objects 1549 | """ 1550 | 1551 | shouts = [] 1552 | for node in _collect_nodes(limit, self, "artist.getShouts", False): 1553 | shouts.append(Shout( 1554 | _extract(node, "body"), 1555 | User(_extract(node, "author"), self.network), 1556 | _extract(node, "date") 1557 | ) 1558 | ) 1559 | return shouts 1560 | 1561 | def shout(self, message): 1562 | """ 1563 | Post a shout 1564 | """ 1565 | 1566 | params = self._get_params() 1567 | params["message"] = message 1568 | 1569 | self._request("artist.Shout", False, params) 1570 | 1571 | 1572 | class Event(_BaseObject): 1573 | """An event.""" 1574 | 1575 | id = None 1576 | 1577 | def __init__(self, event_id, network): 1578 | _BaseObject.__init__(self, network) 1579 | 1580 | self.id = _unicode(event_id) 1581 | 1582 | @_string_output 1583 | def __repr__(self): 1584 | return "Event #" + self.get_id() 1585 | 1586 | def __eq__(self, other): 1587 | return self.get_id() == other.get_id() 1588 | 1589 | def __ne__(self, other): 1590 | return self.get_id() != other.get_id() 1591 | 1592 | def _get_params(self): 1593 | return {'event': self.get_id()} 1594 | 1595 | def attend(self, attending_status): 1596 | """Sets the attending status. 1597 | * attending_status: The attending status. Possible values: 1598 | o EVENT_ATTENDING 1599 | o EVENT_MAYBE_ATTENDING 1600 | o EVENT_NOT_ATTENDING 1601 | """ 1602 | 1603 | params = self._get_params() 1604 | params['status'] = _unicode(attending_status) 1605 | 1606 | self._request('event.attend', False, params) 1607 | 1608 | def get_attendees(self): 1609 | """ 1610 | Get a list of attendees for an event 1611 | """ 1612 | 1613 | doc = self._request("event.getAttendees", False) 1614 | 1615 | users = [] 1616 | for name in _extract_all(doc, "name"): 1617 | users.append(User(name, self.network)) 1618 | 1619 | return users 1620 | 1621 | def get_id(self): 1622 | """Returns the id of the event on the network. """ 1623 | 1624 | return self.id 1625 | 1626 | def get_title(self): 1627 | """Returns the title of the event. """ 1628 | 1629 | doc = self._request("event.getInfo", True) 1630 | 1631 | return _extract(doc, "title") 1632 | 1633 | def get_headliner(self): 1634 | """Returns the headliner of the event. """ 1635 | 1636 | doc = self._request("event.getInfo", True) 1637 | 1638 | return Artist(_extract(doc, "headliner"), self.network) 1639 | 1640 | def get_artists(self): 1641 | """Returns a list of the participating Artists. """ 1642 | 1643 | doc = self._request("event.getInfo", True) 1644 | names = _extract_all(doc, "artist") 1645 | 1646 | artists = [] 1647 | for name in names: 1648 | artists.append(Artist(name, self.network)) 1649 | 1650 | return artists 1651 | 1652 | def get_venue(self): 1653 | """Returns the venue where the event is held.""" 1654 | 1655 | doc = self._request("event.getInfo", True) 1656 | 1657 | v = doc.getElementsByTagName("venue")[0] 1658 | venue_id = _number(_extract(v, "id")) 1659 | 1660 | return Venue(venue_id, self.network) 1661 | 1662 | def get_start_date(self): 1663 | """Returns the date when the event starts.""" 1664 | 1665 | doc = self._request("event.getInfo", True) 1666 | 1667 | return _extract(doc, "startDate") 1668 | 1669 | def get_description(self): 1670 | """Returns the description of the event. """ 1671 | 1672 | doc = self._request("event.getInfo", True) 1673 | 1674 | return _extract(doc, "description") 1675 | 1676 | def get_cover_image(self, size = COVER_LARGE): 1677 | """ 1678 | Returns a uri to the cover image 1679 | size can be one of: 1680 | COVER_MEGA 1681 | COVER_EXTRA_LARGE 1682 | COVER_LARGE 1683 | COVER_MEDIUM 1684 | COVER_SMALL 1685 | """ 1686 | 1687 | doc = self._request("event.getInfo", True) 1688 | 1689 | return _extract_all(doc, "image")[size] 1690 | 1691 | def get_attendance_count(self): 1692 | """Returns the number of attending people. """ 1693 | 1694 | doc = self._request("event.getInfo", True) 1695 | 1696 | return _number(_extract(doc, "attendance")) 1697 | 1698 | def get_review_count(self): 1699 | """Returns the number of available reviews for this event. """ 1700 | 1701 | doc = self._request("event.getInfo", True) 1702 | 1703 | return _number(_extract(doc, "reviews")) 1704 | 1705 | def get_url(self, domain_name = DOMAIN_ENGLISH): 1706 | """Returns the url of the event page on the network. 1707 | * domain_name: The network's language domain. Possible values: 1708 | o DOMAIN_ENGLISH 1709 | o DOMAIN_GERMAN 1710 | o DOMAIN_SPANISH 1711 | o DOMAIN_FRENCH 1712 | o DOMAIN_ITALIAN 1713 | o DOMAIN_POLISH 1714 | o DOMAIN_PORTUGUESE 1715 | o DOMAIN_SWEDISH 1716 | o DOMAIN_TURKISH 1717 | o DOMAIN_RUSSIAN 1718 | o DOMAIN_JAPANESE 1719 | o DOMAIN_CHINESE 1720 | """ 1721 | 1722 | return self.network._get_url(domain_name, "event") %{'id': self.get_id()} 1723 | 1724 | def share(self, users, message = None): 1725 | """Shares this event (sends out recommendations). 1726 | * users: A list that can contain usernames, emails, User objects, or all of them. 1727 | * message: A message to include in the recommendation message. 1728 | """ 1729 | 1730 | #last.fm currently accepts a max of 10 recipient at a time 1731 | while(len(users) > 10): 1732 | section = users[0:9] 1733 | users = users[9:] 1734 | self.share(section, message) 1735 | 1736 | nusers = [] 1737 | for user in users: 1738 | if isinstance(user, User): 1739 | nusers.append(user.get_name()) 1740 | else: 1741 | nusers.append(user) 1742 | 1743 | params = self._get_params() 1744 | recipients = ','.join(nusers) 1745 | params['recipient'] = recipients 1746 | if message: params['message'] = _unicode(message) 1747 | 1748 | self._request('event.share', False, params) 1749 | 1750 | def get_shouts(self, limit=50): 1751 | """ 1752 | Returns a sequqence of Shout objects 1753 | """ 1754 | 1755 | shouts = [] 1756 | for node in _collect_nodes(limit, self, "event.getShouts", False): 1757 | shouts.append(Shout( 1758 | _extract(node, "body"), 1759 | User(_extract(node, "author"), self.network), 1760 | _extract(node, "date") 1761 | ) 1762 | ) 1763 | return shouts 1764 | 1765 | def shout(self, message): 1766 | """ 1767 | Post a shout 1768 | """ 1769 | 1770 | params = self._get_params() 1771 | params["message"] = message 1772 | 1773 | self._request("event.Shout", False, params) 1774 | 1775 | class Country(_BaseObject): 1776 | """A country at Last.fm.""" 1777 | 1778 | name = None 1779 | 1780 | def __init__(self, name, network): 1781 | _BaseObject.__init__(self, network) 1782 | 1783 | self.name = name 1784 | 1785 | @_string_output 1786 | def __repr__(self): 1787 | return self.get_name() 1788 | 1789 | def __eq__(self, other): 1790 | return self.get_name().lower() == other.get_name().lower() 1791 | 1792 | def __ne__(self, other): 1793 | return self.get_name() != other.get_name() 1794 | 1795 | def _get_params(self): 1796 | return {'country': self.get_name()} 1797 | 1798 | def _get_name_from_code(self, alpha2code): 1799 | # TODO: Have this function lookup the alpha-2 code and return the country name. 1800 | 1801 | return alpha2code 1802 | 1803 | def get_name(self): 1804 | """Returns the country name. """ 1805 | 1806 | return self.name 1807 | 1808 | def get_top_artists(self): 1809 | """Returns a sequence of the most played artists.""" 1810 | 1811 | doc = self._request('geo.getTopArtists', True) 1812 | 1813 | seq = [] 1814 | for node in doc.getElementsByTagName("artist"): 1815 | name = _extract(node, 'name') 1816 | playcount = _extract(node, "playcount") 1817 | 1818 | seq.append(TopItem(Artist(name, self.network), playcount)) 1819 | 1820 | return seq 1821 | 1822 | def get_top_tracks(self): 1823 | """Returns a sequence of the most played tracks""" 1824 | 1825 | doc = self._request("geo.getTopTracks", True) 1826 | 1827 | seq = [] 1828 | 1829 | for n in doc.getElementsByTagName('track'): 1830 | 1831 | title = _extract(n, 'name') 1832 | artist = _extract(n, 'name', 1) 1833 | playcount = _number(_extract(n, "playcount")) 1834 | 1835 | seq.append( TopItem(Track(artist, title, self.network), playcount)) 1836 | 1837 | return seq 1838 | 1839 | def get_url(self, domain_name = DOMAIN_ENGLISH): 1840 | """Returns the url of the event page on the network. 1841 | * domain_name: The network's language domain. Possible values: 1842 | o DOMAIN_ENGLISH 1843 | o DOMAIN_GERMAN 1844 | o DOMAIN_SPANISH 1845 | o DOMAIN_FRENCH 1846 | o DOMAIN_ITALIAN 1847 | o DOMAIN_POLISH 1848 | o DOMAIN_PORTUGUESE 1849 | o DOMAIN_SWEDISH 1850 | o DOMAIN_TURKISH 1851 | o DOMAIN_RUSSIAN 1852 | o DOMAIN_JAPANESE 1853 | o DOMAIN_CHINESE 1854 | """ 1855 | 1856 | country_name = _url_safe(self.get_name()) 1857 | 1858 | return self.network._get_url(domain_name, "country") %{'country_name': country_name} 1859 | 1860 | 1861 | class Library(_BaseObject): 1862 | """A user's Last.fm library.""" 1863 | 1864 | user = None 1865 | 1866 | def __init__(self, user, network): 1867 | _BaseObject.__init__(self, network) 1868 | 1869 | if isinstance(user, User): 1870 | self.user = user 1871 | else: 1872 | self.user = User(user, self.network) 1873 | 1874 | self._albums_index = 0 1875 | self._artists_index = 0 1876 | self._tracks_index = 0 1877 | 1878 | @_string_output 1879 | def __repr__(self): 1880 | return repr(self.get_user()) + "'s Library" 1881 | 1882 | def _get_params(self): 1883 | return {'user': self.user.get_name()} 1884 | 1885 | def get_user(self): 1886 | """Returns the user who owns this library.""" 1887 | 1888 | return self.user 1889 | 1890 | def add_album(self, album): 1891 | """Add an album to this library.""" 1892 | 1893 | params = self._get_params() 1894 | params["artist"] = album.get_artist.get_name() 1895 | params["album"] = album.get_name() 1896 | 1897 | self._request("library.addAlbum", False, params) 1898 | 1899 | def add_artist(self, artist): 1900 | """Add an artist to this library.""" 1901 | 1902 | params = self._get_params() 1903 | params["artist"] = artist.get_name() 1904 | 1905 | self._request("library.addArtist", False, params) 1906 | 1907 | def add_track(self, track): 1908 | """Add a track to this library.""" 1909 | 1910 | params = self._get_params() 1911 | params["track"] = track.get_title() 1912 | 1913 | self._request("library.addTrack", False, params) 1914 | 1915 | def get_albums(self, limit=50): 1916 | """ 1917 | Returns a sequence of Album objects 1918 | if limit==None it will return all (may take a while) 1919 | """ 1920 | 1921 | seq = [] 1922 | for node in _collect_nodes(limit, self, "library.getAlbums", True): 1923 | name = _extract(node, "name") 1924 | artist = _extract(node, "name", 1) 1925 | playcount = _number(_extract(node, "playcount")) 1926 | tagcount = _number(_extract(node, "tagcount")) 1927 | 1928 | seq.append(LibraryItem(Album(artist, name, self.network), playcount, tagcount)) 1929 | 1930 | return seq 1931 | 1932 | def get_artists(self, limit=50): 1933 | """ 1934 | Returns a sequence of Album objects 1935 | if limit==None it will return all (may take a while) 1936 | """ 1937 | 1938 | seq = [] 1939 | for node in _collect_nodes(limit, self, "library.getArtists", True): 1940 | name = _extract(node, "name") 1941 | 1942 | playcount = _number(_extract(node, "playcount")) 1943 | tagcount = _number(_extract(node, "tagcount")) 1944 | 1945 | seq.append(LibraryItem(Artist(name, self.network), playcount, tagcount)) 1946 | 1947 | return seq 1948 | 1949 | def get_tracks(self, limit=50): 1950 | """ 1951 | Returns a sequence of Album objects 1952 | if limit==None it will return all (may take a while) 1953 | """ 1954 | 1955 | seq = [] 1956 | for node in _collect_nodes(limit, self, "library.getTracks", True): 1957 | name = _extract(node, "name") 1958 | artist = _extract(node, "name", 1) 1959 | playcount = _number(_extract(node, "playcount")) 1960 | tagcount = _number(_extract(node, "tagcount")) 1961 | 1962 | seq.append(LibraryItem(Track(artist, name, self.network), playcount, tagcount)) 1963 | 1964 | return seq 1965 | 1966 | 1967 | class Playlist(_BaseObject): 1968 | """A Last.fm user playlist.""" 1969 | 1970 | id = None 1971 | user = None 1972 | 1973 | def __init__(self, user, id, network): 1974 | _BaseObject.__init__(self, network) 1975 | 1976 | if isinstance(user, User): 1977 | self.user = user 1978 | else: 1979 | self.user = User(user, self.network) 1980 | 1981 | self.id = _unicode(id) 1982 | 1983 | @_string_output 1984 | def __repr__(self): 1985 | return repr(self.user) + "'s playlist # " + repr(self.id) 1986 | 1987 | def _get_info_node(self): 1988 | """Returns the node from user.getPlaylists where this playlist's info is.""" 1989 | 1990 | doc = self._request("user.getPlaylists", True) 1991 | 1992 | for node in doc.getElementsByTagName("playlist"): 1993 | if _extract(node, "id") == str(self.get_id()): 1994 | return node 1995 | 1996 | def _get_params(self): 1997 | return {'user': self.user.get_name(), 'playlistID': self.get_id()} 1998 | 1999 | def get_id(self): 2000 | """Returns the playlist id.""" 2001 | 2002 | return self.id 2003 | 2004 | def get_user(self): 2005 | """Returns the owner user of this playlist.""" 2006 | 2007 | return self.user 2008 | 2009 | def get_tracks(self): 2010 | """Returns a list of the tracks on this user playlist.""" 2011 | 2012 | uri = u'lastfm://playlist/%s' %self.get_id() 2013 | 2014 | return XSPF(uri, self.network).get_tracks() 2015 | 2016 | def add_track(self, track): 2017 | """Adds a Track to this Playlist.""" 2018 | 2019 | params = self._get_params() 2020 | params['artist'] = track.get_artist().get_name() 2021 | params['track'] = track.get_title() 2022 | 2023 | self._request('playlist.addTrack', False, params) 2024 | 2025 | def get_title(self): 2026 | """Returns the title of this playlist.""" 2027 | 2028 | return _extract(self._get_info_node(), "title") 2029 | 2030 | def get_creation_date(self): 2031 | """Returns the creation date of this playlist.""" 2032 | 2033 | return _extract(self._get_info_node(), "date") 2034 | 2035 | def get_size(self): 2036 | """Returns the number of tracks in this playlist.""" 2037 | 2038 | return _number(_extract(self._get_info_node(), "size")) 2039 | 2040 | def get_description(self): 2041 | """Returns the description of this playlist.""" 2042 | 2043 | return _extract(self._get_info_node(), "description") 2044 | 2045 | def get_duration(self): 2046 | """Returns the duration of this playlist in milliseconds.""" 2047 | 2048 | return _number(_extract(self._get_info_node(), "duration")) 2049 | 2050 | def is_streamable(self): 2051 | """Returns True if the playlist is streamable. 2052 | For a playlist to be streamable, it needs at least 45 tracks by 15 different artists.""" 2053 | 2054 | if _extract(self._get_info_node(), "streamable") == '1': 2055 | return True 2056 | else: 2057 | return False 2058 | 2059 | def has_track(self, track): 2060 | """Checks to see if track is already in the playlist. 2061 | * track: Any Track object. 2062 | """ 2063 | 2064 | return track in self.get_tracks() 2065 | 2066 | def get_cover_image(self, size = COVER_LARGE): 2067 | """ 2068 | Returns a uri to the cover image 2069 | size can be one of: 2070 | COVER_MEGA 2071 | COVER_EXTRA_LARGE 2072 | COVER_LARGE 2073 | COVER_MEDIUM 2074 | COVER_SMALL 2075 | """ 2076 | 2077 | return _extract(self._get_info_node(), "image")[size] 2078 | 2079 | def get_url(self, domain_name = DOMAIN_ENGLISH): 2080 | """Returns the url of the playlist on the network. 2081 | * domain_name: The network's language domain. Possible values: 2082 | o DOMAIN_ENGLISH 2083 | o DOMAIN_GERMAN 2084 | o DOMAIN_SPANISH 2085 | o DOMAIN_FRENCH 2086 | o DOMAIN_ITALIAN 2087 | o DOMAIN_POLISH 2088 | o DOMAIN_PORTUGUESE 2089 | o DOMAIN_SWEDISH 2090 | o DOMAIN_TURKISH 2091 | o DOMAIN_RUSSIAN 2092 | o DOMAIN_JAPANESE 2093 | o DOMAIN_CHINESE 2094 | """ 2095 | 2096 | english_url = _extract(self._get_info_node(), "url") 2097 | appendix = english_url[english_url.rfind("/") + 1:] 2098 | 2099 | return self.network._get_url(domain_name, "playlist") %{'appendix': appendix, "user": self.get_user().get_name()} 2100 | 2101 | 2102 | class Tag(_BaseObject): 2103 | """A Last.fm object tag.""" 2104 | 2105 | # TODO: getWeeklyArtistChart (too lazy, i'll wait for when someone requests it) 2106 | 2107 | name = None 2108 | 2109 | def __init__(self, name, network): 2110 | _BaseObject.__init__(self, network) 2111 | 2112 | self.name = name 2113 | 2114 | def _get_params(self): 2115 | return {'tag': self.get_name()} 2116 | 2117 | @_string_output 2118 | def __repr__(self): 2119 | return self.get_name() 2120 | 2121 | def __eq__(self, other): 2122 | return self.get_name().lower() == other.get_name().lower() 2123 | 2124 | def __ne__(self, other): 2125 | return self.get_name().lower() != other.get_name().lower() 2126 | 2127 | def get_name(self): 2128 | """Returns the name of the tag. """ 2129 | 2130 | return self.name 2131 | 2132 | def get_similar(self): 2133 | """Returns the tags similar to this one, ordered by similarity. """ 2134 | 2135 | doc = self._request('tag.getSimilar', True) 2136 | 2137 | seq = [] 2138 | names = _extract_all(doc, 'name') 2139 | for name in names: 2140 | seq.append(Tag(name, self.network)) 2141 | 2142 | return seq 2143 | 2144 | def get_top_albums(self): 2145 | """Retuns a list of the top albums.""" 2146 | 2147 | doc = self._request('tag.getTopAlbums', True) 2148 | 2149 | seq = [] 2150 | 2151 | for node in doc.getElementsByTagName("album"): 2152 | name = _extract(node, "name") 2153 | artist = _extract(node, "name", 1) 2154 | playcount = _extract(node, "playcount") 2155 | 2156 | seq.append(TopItem(Album(artist, name, self.network), playcount)) 2157 | 2158 | return seq 2159 | 2160 | def get_top_tracks(self): 2161 | """Returns a list of the most played Tracks by this artist.""" 2162 | 2163 | doc = self._request("tag.getTopTracks", True) 2164 | 2165 | seq = [] 2166 | for track in doc.getElementsByTagName('track'): 2167 | 2168 | title = _extract(track, "name") 2169 | artist = _extract(track, "name", 1) 2170 | playcount = _number(_extract(track, "playcount")) 2171 | 2172 | seq.append( TopItem(Track(artist, title, self.network), playcount) ) 2173 | 2174 | return seq 2175 | 2176 | def get_top_artists(self): 2177 | """Returns a sequence of the most played artists.""" 2178 | 2179 | doc = self._request('tag.getTopArtists', True) 2180 | 2181 | seq = [] 2182 | for node in doc.getElementsByTagName("artist"): 2183 | name = _extract(node, 'name') 2184 | playcount = _extract(node, "playcount") 2185 | 2186 | seq.append(TopItem(Artist(name, self.network), playcount)) 2187 | 2188 | return seq 2189 | 2190 | def get_weekly_chart_dates(self): 2191 | """Returns a list of From and To tuples for the available charts.""" 2192 | 2193 | doc = self._request("tag.getWeeklyChartList", True) 2194 | 2195 | seq = [] 2196 | for node in doc.getElementsByTagName("chart"): 2197 | seq.append( (node.getAttribute("from"), node.getAttribute("to")) ) 2198 | 2199 | return seq 2200 | 2201 | def get_weekly_artist_charts(self, from_date = None, to_date = None): 2202 | """Returns the weekly artist charts for the week starting from the from_date value to the to_date value.""" 2203 | 2204 | params = self._get_params() 2205 | if from_date and to_date: 2206 | params["from"] = from_date 2207 | params["to"] = to_date 2208 | 2209 | doc = self._request("tag.getWeeklyArtistChart", True, params) 2210 | 2211 | seq = [] 2212 | for node in doc.getElementsByTagName("artist"): 2213 | item = Artist(_extract(node, "name"), self.network) 2214 | weight = _number(_extract(node, "weight")) 2215 | seq.append(TopItem(item, weight)) 2216 | 2217 | return seq 2218 | 2219 | def get_url(self, domain_name = DOMAIN_ENGLISH): 2220 | """Returns the url of the tag page on the network. 2221 | * domain_name: The network's language domain. Possible values: 2222 | o DOMAIN_ENGLISH 2223 | o DOMAIN_GERMAN 2224 | o DOMAIN_SPANISH 2225 | o DOMAIN_FRENCH 2226 | o DOMAIN_ITALIAN 2227 | o DOMAIN_POLISH 2228 | o DOMAIN_PORTUGUESE 2229 | o DOMAIN_SWEDISH 2230 | o DOMAIN_TURKISH 2231 | o DOMAIN_RUSSIAN 2232 | o DOMAIN_JAPANESE 2233 | o DOMAIN_CHINESE 2234 | """ 2235 | 2236 | name = _url_safe(self.get_name()) 2237 | 2238 | return self.network._get_url(domain_name, "tag") %{'name': name} 2239 | 2240 | class Track(_BaseObject, _Taggable): 2241 | """A Last.fm track.""" 2242 | 2243 | artist = None 2244 | title = None 2245 | 2246 | def __init__(self, artist, title, network): 2247 | _BaseObject.__init__(self, network) 2248 | _Taggable.__init__(self, 'track') 2249 | 2250 | if isinstance(artist, Artist): 2251 | self.artist = artist 2252 | else: 2253 | self.artist = Artist(artist, self.network) 2254 | 2255 | self.title = title 2256 | 2257 | @_string_output 2258 | def __repr__(self): 2259 | return self.get_artist().get_name() + ' - ' + self.get_title() 2260 | 2261 | def __eq__(self, other): 2262 | return (self.get_title().lower() == other.get_title().lower()) and (self.get_artist().get_name().lower() == other.get_artist().get_name().lower()) 2263 | 2264 | def __ne__(self, other): 2265 | return (self.get_title().lower() != other.get_title().lower()) or (self.get_artist().get_name().lower() != other.get_artist().get_name().lower()) 2266 | 2267 | def _get_params(self): 2268 | return {'artist': self.get_artist().get_name(), 'track': self.get_title()} 2269 | 2270 | def get_artist(self): 2271 | """Returns the associated Artist object.""" 2272 | 2273 | return self.artist 2274 | 2275 | def get_title(self): 2276 | """Returns the track title.""" 2277 | 2278 | return self.title 2279 | 2280 | def get_name(self): 2281 | """Returns the track title (alias to Track.get_title).""" 2282 | 2283 | return self.get_title() 2284 | 2285 | def get_id(self): 2286 | """Returns the track id on the network.""" 2287 | 2288 | doc = self._request("track.getInfo", True) 2289 | 2290 | return _extract(doc, "id") 2291 | 2292 | def get_duration(self): 2293 | """Returns the track duration.""" 2294 | 2295 | doc = self._request("track.getInfo", True) 2296 | 2297 | return _number(_extract(doc, "duration")) 2298 | 2299 | def get_mbid(self): 2300 | """Returns the MusicBrainz ID of this track.""" 2301 | 2302 | doc = self._request("track.getInfo", True) 2303 | 2304 | return _extract(doc, "mbid") 2305 | 2306 | def get_listener_count(self): 2307 | """Returns the listener count.""" 2308 | 2309 | doc = self._request("track.getInfo", True) 2310 | 2311 | return _number(_extract(doc, "listeners")) 2312 | 2313 | def get_playcount(self): 2314 | """Returns the play count.""" 2315 | 2316 | doc = self._request("track.getInfo", True) 2317 | return _number(_extract(doc, "playcount")) 2318 | 2319 | def is_streamable(self): 2320 | """Returns True if the track is available at Last.fm.""" 2321 | 2322 | doc = self._request("track.getInfo", True) 2323 | return _extract(doc, "streamable") == "1" 2324 | 2325 | def is_fulltrack_available(self): 2326 | """Returns True if the fulltrack is available for streaming.""" 2327 | 2328 | doc = self._request("track.getInfo", True) 2329 | return doc.getElementsByTagName("streamable")[0].getAttribute("fulltrack") == "1" 2330 | 2331 | def get_album(self): 2332 | """Returns the album object of this track.""" 2333 | 2334 | doc = self._request("track.getInfo", True) 2335 | 2336 | albums = doc.getElementsByTagName("album") 2337 | 2338 | if len(albums) == 0: 2339 | return 2340 | 2341 | node = doc.getElementsByTagName("album")[0] 2342 | return Album(_extract(node, "artist"), _extract(node, "title"), self.network) 2343 | 2344 | def get_wiki_published_date(self): 2345 | """Returns the date of publishing this version of the wiki.""" 2346 | 2347 | doc = self._request("track.getInfo", True) 2348 | 2349 | if len(doc.getElementsByTagName("wiki")) == 0: 2350 | return 2351 | 2352 | node = doc.getElementsByTagName("wiki")[0] 2353 | 2354 | return _extract(node, "published") 2355 | 2356 | def get_wiki_summary(self): 2357 | """Returns the summary of the wiki.""" 2358 | 2359 | doc = self._request("track.getInfo", True) 2360 | 2361 | if len(doc.getElementsByTagName("wiki")) == 0: 2362 | return 2363 | 2364 | node = doc.getElementsByTagName("wiki")[0] 2365 | 2366 | return _extract(node, "summary") 2367 | 2368 | def get_wiki_content(self): 2369 | """Returns the content of the wiki.""" 2370 | 2371 | doc = self._request("track.getInfo", True) 2372 | 2373 | if len(doc.getElementsByTagName("wiki")) == 0: 2374 | return 2375 | 2376 | node = doc.getElementsByTagName("wiki")[0] 2377 | 2378 | return _extract(node, "content") 2379 | 2380 | def love(self): 2381 | """Adds the track to the user's loved tracks. """ 2382 | 2383 | self._request('track.love') 2384 | 2385 | def ban(self): 2386 | """Ban this track from ever playing on the radio. """ 2387 | 2388 | self._request('track.ban') 2389 | 2390 | def get_similar(self): 2391 | """Returns similar tracks for this track on the network, based on listening data. """ 2392 | 2393 | doc = self._request('track.getSimilar', True) 2394 | 2395 | seq = [] 2396 | for node in doc.getElementsByTagName("track"): 2397 | title = _extract(node, 'name') 2398 | artist = _extract(node, 'name', 1) 2399 | match = _number(_extract(node, "match")) 2400 | 2401 | seq.append(SimilarItem(Track(artist, title, self.network), match)) 2402 | 2403 | return seq 2404 | 2405 | def get_top_fans(self, limit = None): 2406 | """Returns a list of the Users who played this track.""" 2407 | 2408 | doc = self._request('track.getTopFans', True) 2409 | 2410 | seq = [] 2411 | 2412 | elements = doc.getElementsByTagName('user') 2413 | 2414 | for element in elements: 2415 | if limit and len(seq) >= limit: 2416 | break 2417 | 2418 | name = _extract(element, 'name') 2419 | weight = _number(_extract(element, 'weight')) 2420 | 2421 | seq.append(TopItem(User(name, self.network), weight)) 2422 | 2423 | return seq 2424 | 2425 | def share(self, users, message = None): 2426 | """Shares this track (sends out recommendations). 2427 | * users: A list that can contain usernames, emails, User objects, or all of them. 2428 | * message: A message to include in the recommendation message. 2429 | """ 2430 | 2431 | #last.fm currently accepts a max of 10 recipient at a time 2432 | while(len(users) > 10): 2433 | section = users[0:9] 2434 | users = users[9:] 2435 | self.share(section, message) 2436 | 2437 | nusers = [] 2438 | for user in users: 2439 | if isinstance(user, User): 2440 | nusers.append(user.get_name()) 2441 | else: 2442 | nusers.append(user) 2443 | 2444 | params = self._get_params() 2445 | recipients = ','.join(nusers) 2446 | params['recipient'] = recipients 2447 | if message: params['message'] = _unicode(message) 2448 | 2449 | self._request('track.share', False, params) 2450 | 2451 | def get_url(self, domain_name = DOMAIN_ENGLISH): 2452 | """Returns the url of the track page on the network. 2453 | * domain_name: The network's language domain. Possible values: 2454 | o DOMAIN_ENGLISH 2455 | o DOMAIN_GERMAN 2456 | o DOMAIN_SPANISH 2457 | o DOMAIN_FRENCH 2458 | o DOMAIN_ITALIAN 2459 | o DOMAIN_POLISH 2460 | o DOMAIN_PORTUGUESE 2461 | o DOMAIN_SWEDISH 2462 | o DOMAIN_TURKISH 2463 | o DOMAIN_RUSSIAN 2464 | o DOMAIN_JAPANESE 2465 | o DOMAIN_CHINESE 2466 | """ 2467 | 2468 | artist = _url_safe(self.get_artist().get_name()) 2469 | title = _url_safe(self.get_title()) 2470 | 2471 | return self.network._get_url(domain_name, "track") %{'domain': self.network._get_language_domain(domain_name), 'artist': artist, 'title': title} 2472 | 2473 | def get_shouts(self, limit=50): 2474 | """ 2475 | Returns a sequqence of Shout objects 2476 | """ 2477 | 2478 | shouts = [] 2479 | for node in _collect_nodes(limit, self, "track.getShouts", False): 2480 | shouts.append(Shout( 2481 | _extract(node, "body"), 2482 | User(_extract(node, "author"), self.network), 2483 | _extract(node, "date") 2484 | ) 2485 | ) 2486 | return shouts 2487 | 2488 | def shout(self, message): 2489 | """ 2490 | Post a shout 2491 | """ 2492 | 2493 | params = self._get_params() 2494 | params["message"] = message 2495 | 2496 | self._request("track.Shout", False, params) 2497 | 2498 | class Group(_BaseObject): 2499 | """A Last.fm group.""" 2500 | 2501 | name = None 2502 | 2503 | def __init__(self, group_name, network): 2504 | _BaseObject.__init__(self, network) 2505 | 2506 | self.name = group_name 2507 | 2508 | @_string_output 2509 | def __repr__(self): 2510 | return self.get_name() 2511 | 2512 | def __eq__(self, other): 2513 | return self.get_name().lower() == other.get_name().lower() 2514 | 2515 | def __ne__(self, other): 2516 | return self.get_name() != other.get_name() 2517 | 2518 | def _get_params(self): 2519 | return {'group': self.get_name()} 2520 | 2521 | def get_name(self): 2522 | """Returns the group name. """ 2523 | return self.name 2524 | 2525 | def get_weekly_chart_dates(self): 2526 | """Returns a list of From and To tuples for the available charts.""" 2527 | 2528 | doc = self._request("group.getWeeklyChartList", True) 2529 | 2530 | seq = [] 2531 | for node in doc.getElementsByTagName("chart"): 2532 | seq.append( (node.getAttribute("from"), node.getAttribute("to")) ) 2533 | 2534 | return seq 2535 | 2536 | def get_weekly_artist_charts(self, from_date = None, to_date = None): 2537 | """Returns the weekly artist charts for the week starting from the from_date value to the to_date value.""" 2538 | 2539 | params = self._get_params() 2540 | if from_date and to_date: 2541 | params["from"] = from_date 2542 | params["to"] = to_date 2543 | 2544 | doc = self._request("group.getWeeklyArtistChart", True, params) 2545 | 2546 | seq = [] 2547 | for node in doc.getElementsByTagName("artist"): 2548 | item = Artist(_extract(node, "name"), self.network) 2549 | weight = _number(_extract(node, "playcount")) 2550 | seq.append(TopItem(item, weight)) 2551 | 2552 | return seq 2553 | 2554 | def get_weekly_album_charts(self, from_date = None, to_date = None): 2555 | """Returns the weekly album charts for the week starting from the from_date value to the to_date value.""" 2556 | 2557 | params = self._get_params() 2558 | if from_date and to_date: 2559 | params["from"] = from_date 2560 | params["to"] = to_date 2561 | 2562 | doc = self._request("group.getWeeklyAlbumChart", True, params) 2563 | 2564 | seq = [] 2565 | for node in doc.getElementsByTagName("album"): 2566 | item = Album(_extract(node, "artist"), _extract(node, "name"), self.network) 2567 | weight = _number(_extract(node, "playcount")) 2568 | seq.append(TopItem(item, weight)) 2569 | 2570 | return seq 2571 | 2572 | def get_weekly_track_charts(self, from_date = None, to_date = None): 2573 | """Returns the weekly track charts for the week starting from the from_date value to the to_date value.""" 2574 | 2575 | params = self._get_params() 2576 | if from_date and to_date: 2577 | params["from"] = from_date 2578 | params["to"] = to_date 2579 | 2580 | doc = self._request("group.getWeeklyTrackChart", True, params) 2581 | 2582 | seq = [] 2583 | for node in doc.getElementsByTagName("track"): 2584 | item = Track(_extract(node, "artist"), _extract(node, "name"), self.network) 2585 | weight = _number(_extract(node, "playcount")) 2586 | seq.append(TopItem(item, weight)) 2587 | 2588 | return seq 2589 | 2590 | def get_url(self, domain_name = DOMAIN_ENGLISH): 2591 | """Returns the url of the group page on the network. 2592 | * domain_name: The network's language domain. Possible values: 2593 | o DOMAIN_ENGLISH 2594 | o DOMAIN_GERMAN 2595 | o DOMAIN_SPANISH 2596 | o DOMAIN_FRENCH 2597 | o DOMAIN_ITALIAN 2598 | o DOMAIN_POLISH 2599 | o DOMAIN_PORTUGUESE 2600 | o DOMAIN_SWEDISH 2601 | o DOMAIN_TURKISH 2602 | o DOMAIN_RUSSIAN 2603 | o DOMAIN_JAPANESE 2604 | o DOMAIN_CHINESE 2605 | """ 2606 | 2607 | name = _url_safe(self.get_name()) 2608 | 2609 | return self.network._get_url(domain_name, "group") %{'name': name} 2610 | 2611 | def get_members(self, limit=50): 2612 | """ 2613 | Returns a sequence of User objects 2614 | if limit==None it will return all 2615 | """ 2616 | 2617 | nodes = _collect_nodes(limit, self, "group.getMembers", False) 2618 | 2619 | users = [] 2620 | 2621 | for node in nodes: 2622 | users.append(User(_extract(node, "name"), self.network)) 2623 | 2624 | return users 2625 | 2626 | class XSPF(_BaseObject): 2627 | "A Last.fm XSPF playlist.""" 2628 | 2629 | uri = None 2630 | 2631 | def __init__(self, uri, network): 2632 | _BaseObject.__init__(self, network) 2633 | 2634 | self.uri = uri 2635 | 2636 | def _get_params(self): 2637 | return {'playlistURL': self.get_uri()} 2638 | 2639 | @_string_output 2640 | def __repr__(self): 2641 | return self.get_uri() 2642 | 2643 | def __eq__(self, other): 2644 | return self.get_uri() == other.get_uri() 2645 | 2646 | def __ne__(self, other): 2647 | return self.get_uri() != other.get_uri() 2648 | 2649 | def get_uri(self): 2650 | """Returns the Last.fm playlist URI. """ 2651 | 2652 | return self.uri 2653 | 2654 | def get_tracks(self): 2655 | """Returns the tracks on this playlist.""" 2656 | 2657 | doc = self._request('playlist.fetch', True) 2658 | 2659 | seq = [] 2660 | for n in doc.getElementsByTagName('track'): 2661 | title = _extract(n, 'title') 2662 | artist = _extract(n, 'creator') 2663 | 2664 | seq.append(Track(artist, title, self.network)) 2665 | 2666 | return seq 2667 | 2668 | class User(_BaseObject): 2669 | """A Last.fm user.""" 2670 | 2671 | name = None 2672 | 2673 | def __init__(self, user_name, network): 2674 | _BaseObject.__init__(self, network) 2675 | 2676 | self.name = user_name 2677 | 2678 | self._past_events_index = 0 2679 | self._recommended_events_index = 0 2680 | self._recommended_artists_index = 0 2681 | 2682 | @_string_output 2683 | def __repr__(self): 2684 | return self.get_name() 2685 | 2686 | def __eq__(self, another): 2687 | return self.get_name() == another.get_name() 2688 | 2689 | def __ne__(self, another): 2690 | return self.get_name() != another.get_name() 2691 | 2692 | def _get_params(self): 2693 | return {"user": self.get_name()} 2694 | 2695 | def get_name(self): 2696 | """Returns the nuser name.""" 2697 | 2698 | return self.name 2699 | 2700 | def get_upcoming_events(self): 2701 | """Returns all the upcoming events for this user. """ 2702 | 2703 | doc = self._request('user.getEvents', True) 2704 | 2705 | ids = _extract_all(doc, 'id') 2706 | events = [] 2707 | 2708 | for e_id in ids: 2709 | events.append(Event(e_id, self.network)) 2710 | 2711 | return events 2712 | 2713 | def get_friends(self, limit = 50): 2714 | """Returns a list of the user's friends. """ 2715 | 2716 | seq = [] 2717 | for node in _collect_nodes(limit, self, "user.getFriends", False): 2718 | seq.append(User(_extract(node, "name"), self.network)) 2719 | 2720 | return seq 2721 | 2722 | def get_loved_tracks(self, limit=50): 2723 | """Returns this user's loved track as a sequence of LovedTrack objects 2724 | in reverse order of their timestamp, all the way back to the first track. 2725 | 2726 | If limit==None, it will try to pull all the available data. 2727 | 2728 | This method uses caching. Enable caching only if you're pulling a 2729 | large amount of data. 2730 | 2731 | Use extract_items() with the return of this function to 2732 | get only a sequence of Track objects with no playback dates. """ 2733 | 2734 | params = self._get_params() 2735 | if limit: 2736 | params['limit'] = _unicode(limit) 2737 | 2738 | seq = [] 2739 | for track in _collect_nodes(limit, self, "user.getLovedTracks", True, params): 2740 | 2741 | title = _extract(track, "name") 2742 | artist = _extract(track, "name", 1) 2743 | date = _extract(track, "date") 2744 | timestamp = track.getElementsByTagName("date")[0].getAttribute("uts") 2745 | 2746 | seq.append(LovedTrack(Track(artist, title, self.network), date, timestamp)) 2747 | 2748 | return seq 2749 | 2750 | def get_neighbours(self, limit = 50): 2751 | """Returns a list of the user's friends.""" 2752 | 2753 | params = self._get_params() 2754 | if limit: 2755 | params['limit'] = _unicode(limit) 2756 | 2757 | doc = self._request('user.getNeighbours', True, params) 2758 | 2759 | seq = [] 2760 | names = _extract_all(doc, 'name') 2761 | 2762 | for name in names: 2763 | seq.append(User(name, self.network)) 2764 | 2765 | return seq 2766 | 2767 | def get_past_events(self, limit=50): 2768 | """ 2769 | Returns a sequence of Event objects 2770 | if limit==None it will return all 2771 | """ 2772 | 2773 | seq = [] 2774 | for n in _collect_nodes(limit, self, "user.getPastEvents", False): 2775 | seq.append(Event(_extract(n, "id"), self.network)) 2776 | 2777 | return seq 2778 | 2779 | def get_playlists(self): 2780 | """Returns a list of Playlists that this user owns.""" 2781 | 2782 | doc = self._request("user.getPlaylists", True) 2783 | 2784 | playlists = [] 2785 | for playlist_id in _extract_all(doc, "id"): 2786 | playlists.append(Playlist(self.get_name(), playlist_id, self.network)) 2787 | 2788 | return playlists 2789 | 2790 | def get_now_playing(self): 2791 | """Returns the currently playing track, or None if nothing is playing. """ 2792 | 2793 | params = self._get_params() 2794 | params['limit'] = '1' 2795 | 2796 | doc = self._request('user.getRecentTracks', False, params) 2797 | 2798 | e = doc.getElementsByTagName('track')[0] 2799 | 2800 | artist = _extract(e, 'artist') 2801 | title = _extract(e, 'name') 2802 | 2803 | return Track(artist, title, self.network) 2804 | 2805 | 2806 | def get_recent_tracks(self, limit = 10): 2807 | """Returns this user's played track as a sequence of PlayedTrack objects 2808 | in reverse order of their playtime, all the way back to the first track. 2809 | 2810 | If limit==None, it will try to pull all the available data. 2811 | 2812 | This method uses caching. Enable caching only if you're pulling a 2813 | large amount of data. 2814 | 2815 | Use extract_items() with the return of this function to 2816 | get only a sequence of Track objects with no playback dates. """ 2817 | 2818 | params = self._get_params() 2819 | if limit: 2820 | params['limit'] = _unicode(limit) 2821 | 2822 | seq = [] 2823 | for track in _collect_nodes(limit, self, "user.getRecentTracks", True, params): 2824 | 2825 | if track.hasAttribute('nowplaying'): 2826 | continue 2827 | 2828 | title = _extract(track, "name") 2829 | artist = _extract(track, "artist") 2830 | date = _extract(track, "date") 2831 | timestamp = track.getElementsByTagName("date")[0].getAttribute("uts") 2832 | 2833 | seq.append(PlayedTrack(Track(artist, title, self.network), date, timestamp)) 2834 | 2835 | return seq 2836 | 2837 | def get_id(self): 2838 | """Returns the user id.""" 2839 | 2840 | doc = self._request("user.getInfo", True) 2841 | 2842 | return _extract(doc, "id") 2843 | 2844 | def get_language(self): 2845 | """Returns the language code of the language used by the user.""" 2846 | 2847 | doc = self._request("user.getInfo", True) 2848 | 2849 | return _extract(doc, "lang") 2850 | 2851 | def get_country(self): 2852 | """Returns the name of the country of the user.""" 2853 | 2854 | doc = self._request("user.getInfo", True) 2855 | 2856 | return Country(_extract(doc, "country"), self.network) 2857 | 2858 | def get_age(self): 2859 | """Returns the user's age.""" 2860 | 2861 | doc = self._request("user.getInfo", True) 2862 | 2863 | return _number(_extract(doc, "age")) 2864 | 2865 | def get_gender(self): 2866 | """Returns the user's gender. Either USER_MALE or USER_FEMALE.""" 2867 | 2868 | doc = self._request("user.getInfo", True) 2869 | 2870 | value = _extract(doc, "gender") 2871 | 2872 | if value == 'm': 2873 | return USER_MALE 2874 | elif value == 'f': 2875 | return USER_FEMALE 2876 | 2877 | return None 2878 | 2879 | def is_subscriber(self): 2880 | """Returns whether the user is a subscriber or not. True or False.""" 2881 | 2882 | doc = self._request("user.getInfo", True) 2883 | 2884 | return _extract(doc, "subscriber") == "1" 2885 | 2886 | def get_playcount(self): 2887 | """Returns the user's playcount so far.""" 2888 | 2889 | doc = self._request("user.getInfo", True) 2890 | 2891 | return _number(_extract(doc, "playcount")) 2892 | 2893 | def get_top_albums(self, period = PERIOD_OVERALL): 2894 | """Returns the top albums played by a user. 2895 | * period: The period of time. Possible values: 2896 | o PERIOD_OVERALL 2897 | o PERIOD_7DAYS 2898 | o PERIOD_3MONTHS 2899 | o PERIOD_6MONTHS 2900 | o PERIOD_12MONTHS 2901 | """ 2902 | 2903 | params = self._get_params() 2904 | params['period'] = period 2905 | 2906 | doc = self._request('user.getTopAlbums', True, params) 2907 | 2908 | seq = [] 2909 | for album in doc.getElementsByTagName('album'): 2910 | name = _extract(album, 'name') 2911 | artist = _extract(album, 'name', 1) 2912 | playcount = _extract(album, "playcount") 2913 | 2914 | seq.append(TopItem(Album(artist, name, self.network), playcount)) 2915 | 2916 | return seq 2917 | 2918 | def get_top_artists(self, period = PERIOD_OVERALL): 2919 | """Returns the top artists played by a user. 2920 | * period: The period of time. Possible values: 2921 | o PERIOD_OVERALL 2922 | o PERIOD_7DAYS 2923 | o PERIOD_3MONTHS 2924 | o PERIOD_6MONTHS 2925 | o PERIOD_12MONTHS 2926 | """ 2927 | 2928 | params = self._get_params() 2929 | params['period'] = period 2930 | 2931 | doc = self._request('user.getTopArtists', True, params) 2932 | 2933 | seq = [] 2934 | for node in doc.getElementsByTagName('artist'): 2935 | name = _extract(node, 'name') 2936 | playcount = _extract(node, "playcount") 2937 | 2938 | seq.append(TopItem(Artist(name, self.network), playcount)) 2939 | 2940 | return seq 2941 | 2942 | def get_top_tags(self, limit = None): 2943 | """Returns a sequence of the top tags used by this user with their counts as (Tag, tagcount). 2944 | * limit: The limit of how many tags to return. 2945 | """ 2946 | 2947 | doc = self._request("user.getTopTags", True) 2948 | 2949 | seq = [] 2950 | for node in doc.getElementsByTagName("tag"): 2951 | if len(seq) < limit: 2952 | seq.append(TopItem(Tag(_extract(node, "name"), self.network), _extract(node, "count"))) 2953 | 2954 | return seq 2955 | 2956 | def get_top_tracks(self, period = PERIOD_OVERALL): 2957 | """Returns the top tracks played by a user. 2958 | * period: The period of time. Possible values: 2959 | o PERIOD_OVERALL 2960 | o PERIOD_7DAYS 2961 | o PERIOD_3MONTHS 2962 | o PERIOD_6MONTHS 2963 | o PERIOD_12MONTHS 2964 | """ 2965 | 2966 | params = self._get_params() 2967 | params['period'] = period 2968 | 2969 | doc = self._request('user.getTopTracks', True, params) 2970 | 2971 | seq = [] 2972 | for track in doc.getElementsByTagName('track'): 2973 | name = _extract(track, 'name') 2974 | artist = _extract(track, 'name', 1) 2975 | playcount = _extract(track, "playcount") 2976 | 2977 | seq.append(TopItem(Track(artist, name, self.network), playcount)) 2978 | 2979 | return seq 2980 | 2981 | def get_weekly_chart_dates(self): 2982 | """Returns a list of From and To tuples for the available charts.""" 2983 | 2984 | doc = self._request("user.getWeeklyChartList", True) 2985 | 2986 | seq = [] 2987 | for node in doc.getElementsByTagName("chart"): 2988 | seq.append( (node.getAttribute("from"), node.getAttribute("to")) ) 2989 | 2990 | return seq 2991 | 2992 | def get_weekly_artist_charts(self, from_date = None, to_date = None): 2993 | """Returns the weekly artist charts for the week starting from the from_date value to the to_date value.""" 2994 | 2995 | params = self._get_params() 2996 | if from_date and to_date: 2997 | params["from"] = from_date 2998 | params["to"] = to_date 2999 | 3000 | doc = self._request("user.getWeeklyArtistChart", True, params) 3001 | 3002 | seq = [] 3003 | for node in doc.getElementsByTagName("artist"): 3004 | item = Artist(_extract(node, "name"), self.network) 3005 | weight = _number(_extract(node, "playcount")) 3006 | seq.append(TopItem(item, weight)) 3007 | 3008 | return seq 3009 | 3010 | def get_weekly_album_charts(self, from_date = None, to_date = None): 3011 | """Returns the weekly album charts for the week starting from the from_date value to the to_date value.""" 3012 | 3013 | params = self._get_params() 3014 | if from_date and to_date: 3015 | params["from"] = from_date 3016 | params["to"] = to_date 3017 | 3018 | doc = self._request("user.getWeeklyAlbumChart", True, params) 3019 | 3020 | seq = [] 3021 | for node in doc.getElementsByTagName("album"): 3022 | item = Album(_extract(node, "artist"), _extract(node, "name"), self.network) 3023 | weight = _number(_extract(node, "playcount")) 3024 | seq.append(TopItem(item, weight)) 3025 | 3026 | return seq 3027 | 3028 | def get_weekly_track_charts(self, from_date = None, to_date = None): 3029 | """Returns the weekly track charts for the week starting from the from_date value to the to_date value.""" 3030 | 3031 | params = self._get_params() 3032 | if from_date and to_date: 3033 | params["from"] = from_date 3034 | params["to"] = to_date 3035 | 3036 | doc = self._request("user.getWeeklyTrackChart", True, params) 3037 | 3038 | seq = [] 3039 | for node in doc.getElementsByTagName("track"): 3040 | item = Track(_extract(node, "artist"), _extract(node, "name"), self.network) 3041 | weight = _number(_extract(node, "playcount")) 3042 | seq.append(TopItem(item, weight)) 3043 | 3044 | return seq 3045 | 3046 | def compare_with_user(self, user, shared_artists_limit = None): 3047 | """Compare this user with another Last.fm user. 3048 | Returns a sequence (tasteometer_score, (shared_artist1, shared_artist2, ...)) 3049 | user: A User object or a username string/unicode object. 3050 | """ 3051 | 3052 | if isinstance(user, User): 3053 | user = user.get_name() 3054 | 3055 | params = self._get_params() 3056 | if shared_artists_limit: 3057 | params['limit'] = _unicode(shared_artists_limit) 3058 | params['type1'] = 'user' 3059 | params['type2'] = 'user' 3060 | params['value1'] = self.get_name() 3061 | params['value2'] = user 3062 | 3063 | doc = self._request('tasteometer.compare', False, params) 3064 | 3065 | score = _extract(doc, 'score') 3066 | 3067 | artists = doc.getElementsByTagName('artists')[0] 3068 | shared_artists_names = _extract_all(artists, 'name') 3069 | 3070 | shared_artists_seq = [] 3071 | 3072 | for name in shared_artists_names: 3073 | shared_artists_seq.append(Artist(name, self.network)) 3074 | 3075 | return (score, shared_artists_seq) 3076 | 3077 | def get_image(self): 3078 | """Returns the user's avatar.""" 3079 | 3080 | doc = self._request("user.getInfo", True) 3081 | 3082 | return _extract(doc, "image") 3083 | 3084 | def get_url(self, domain_name = DOMAIN_ENGLISH): 3085 | """Returns the url of the user page on the network. 3086 | * domain_name: The network's language domain. Possible values: 3087 | o DOMAIN_ENGLISH 3088 | o DOMAIN_GERMAN 3089 | o DOMAIN_SPANISH 3090 | o DOMAIN_FRENCH 3091 | o DOMAIN_ITALIAN 3092 | o DOMAIN_POLISH 3093 | o DOMAIN_PORTUGUESE 3094 | o DOMAIN_SWEDISH 3095 | o DOMAIN_TURKISH 3096 | o DOMAIN_RUSSIAN 3097 | o DOMAIN_JAPANESE 3098 | o DOMAIN_CHINESE 3099 | """ 3100 | 3101 | name = _url_safe(self.get_name()) 3102 | 3103 | return self.network._get_url(domain_name, "user") %{'name': name} 3104 | 3105 | def get_library(self): 3106 | """Returns the associated Library object. """ 3107 | 3108 | return Library(self, self.network) 3109 | 3110 | def get_shouts(self, limit=50): 3111 | """ 3112 | Returns a sequqence of Shout objects 3113 | """ 3114 | 3115 | shouts = [] 3116 | for node in _collect_nodes(limit, self, "user.getShouts", False): 3117 | shouts.append(Shout( 3118 | _extract(node, "body"), 3119 | User(_extract(node, "author"), self.network), 3120 | _extract(node, "date") 3121 | ) 3122 | ) 3123 | return shouts 3124 | 3125 | def shout(self, message): 3126 | """ 3127 | Post a shout 3128 | """ 3129 | 3130 | params = self._get_params() 3131 | params["message"] = message 3132 | 3133 | self._request("user.Shout", False, params) 3134 | 3135 | class AuthenticatedUser(User): 3136 | def __init__(self, network): 3137 | User.__init__(self, "", network); 3138 | 3139 | def _get_params(self): 3140 | return {"user": self.get_name()} 3141 | 3142 | def get_name(self): 3143 | """Returns the name of the authenticated user.""" 3144 | 3145 | doc = self._request("user.getInfo", True, {"user": ""}) # hack 3146 | 3147 | self.name = _extract(doc, "name") 3148 | return self.name 3149 | 3150 | def get_recommended_events(self, limit=50): 3151 | """ 3152 | Returns a sequence of Event objects 3153 | if limit==None it will return all 3154 | """ 3155 | 3156 | seq = [] 3157 | for node in _collect_nodes(limit, self, "user.getRecommendedEvents", False): 3158 | seq.append(Event(_extract(node, "id"), self.network)) 3159 | 3160 | return seq 3161 | 3162 | def get_recommended_artists(self, limit=50): 3163 | """ 3164 | Returns a sequence of Event objects 3165 | if limit==None it will return all 3166 | """ 3167 | 3168 | seq = [] 3169 | for node in _collect_nodes(limit, self, "user.getRecommendedArtists", False): 3170 | seq.append(Artist(_extract(node, "name"), self.network)) 3171 | 3172 | return seq 3173 | 3174 | class _Search(_BaseObject): 3175 | """An abstract class. Use one of its derivatives.""" 3176 | 3177 | def __init__(self, ws_prefix, search_terms, network): 3178 | _BaseObject.__init__(self, network) 3179 | 3180 | self._ws_prefix = ws_prefix 3181 | self.search_terms = search_terms 3182 | 3183 | self._last_page_index = 0 3184 | 3185 | def _get_params(self): 3186 | params = {} 3187 | 3188 | for key in self.search_terms.keys(): 3189 | params[key] = self.search_terms[key] 3190 | 3191 | return params 3192 | 3193 | def get_total_result_count(self): 3194 | """Returns the total count of all the results.""" 3195 | 3196 | doc = self._request(self._ws_prefix + ".search", True) 3197 | 3198 | return _extract(doc, "opensearch:totalResults") 3199 | 3200 | def _retreive_page(self, page_index): 3201 | """Returns the node of matches to be processed""" 3202 | 3203 | params = self._get_params() 3204 | params["page"] = str(page_index) 3205 | doc = self._request(self._ws_prefix + ".search", True, params) 3206 | 3207 | return doc.getElementsByTagName(self._ws_prefix + "matches")[0] 3208 | 3209 | def _retrieve_next_page(self): 3210 | self._last_page_index += 1 3211 | return self._retreive_page(self._last_page_index) 3212 | 3213 | class AlbumSearch(_Search): 3214 | """Search for an album by name.""" 3215 | 3216 | def __init__(self, album_name, network): 3217 | 3218 | _Search.__init__(self, "album", {"album": album_name}, network) 3219 | 3220 | def get_next_page(self): 3221 | """Returns the next page of results as a sequence of Album objects.""" 3222 | 3223 | master_node = self._retrieve_next_page() 3224 | 3225 | seq = [] 3226 | for node in master_node.getElementsByTagName("album"): 3227 | seq.append(Album(_extract(node, "artist"), _extract(node, "name"), self.network)) 3228 | 3229 | return seq 3230 | 3231 | class ArtistSearch(_Search): 3232 | """Search for an artist by artist name.""" 3233 | 3234 | def __init__(self, artist_name, network): 3235 | _Search.__init__(self, "artist", {"artist": artist_name}, network) 3236 | 3237 | def get_next_page(self): 3238 | """Returns the next page of results as a sequence of Artist objects.""" 3239 | 3240 | master_node = self._retrieve_next_page() 3241 | 3242 | seq = [] 3243 | for node in master_node.getElementsByTagName("artist"): 3244 | seq.append(Artist(_extract(node, "name"), self.network)) 3245 | 3246 | return seq 3247 | 3248 | class TagSearch(_Search): 3249 | """Search for a tag by tag name.""" 3250 | 3251 | def __init__(self, tag_name, network): 3252 | 3253 | _Search.__init__(self, "tag", {"tag": tag_name}, network) 3254 | 3255 | def get_next_page(self): 3256 | """Returns the next page of results as a sequence of Tag objects.""" 3257 | 3258 | master_node = self._retrieve_next_page() 3259 | 3260 | seq = [] 3261 | for node in master_node.getElementsByTagName("tag"): 3262 | seq.append(Tag(_extract(node, "name"), self.network)) 3263 | 3264 | return seq 3265 | 3266 | class TrackSearch(_Search): 3267 | """Search for a track by track title. If you don't wanna narrow the results down 3268 | by specifying the artist name, set it to empty string.""" 3269 | 3270 | def __init__(self, artist_name, track_title, network): 3271 | 3272 | _Search.__init__(self, "track", {"track": track_title, "artist": artist_name}, network) 3273 | 3274 | def get_next_page(self): 3275 | """Returns the next page of results as a sequence of Track objects.""" 3276 | 3277 | master_node = self._retrieve_next_page() 3278 | 3279 | seq = [] 3280 | for node in master_node.getElementsByTagName("track"): 3281 | seq.append(Track(_extract(node, "artist"), _extract(node, "name"), self.network)) 3282 | 3283 | return seq 3284 | 3285 | class VenueSearch(_Search): 3286 | """Search for a venue by its name. If you don't wanna narrow the results down 3287 | by specifying a country, set it to empty string.""" 3288 | 3289 | def __init__(self, venue_name, country_name, network): 3290 | 3291 | _Search.__init__(self, "venue", {"venue": venue_name, "country": country_name}, network) 3292 | 3293 | def get_next_page(self): 3294 | """Returns the next page of results as a sequence of Track objects.""" 3295 | 3296 | master_node = self._retrieve_next_page() 3297 | 3298 | seq = [] 3299 | for node in master_node.getElementsByTagName("venue"): 3300 | seq.append(Venue(_extract(node, "id"), self.network)) 3301 | 3302 | return seq 3303 | 3304 | class Venue(_BaseObject): 3305 | """A venue where events are held.""" 3306 | 3307 | # TODO: waiting for a venue.getInfo web service to use. 3308 | 3309 | id = None 3310 | 3311 | def __init__(self, id, network): 3312 | _BaseObject.__init__(self, network) 3313 | 3314 | self.id = _number(id) 3315 | 3316 | @_string_output 3317 | def __repr__(self): 3318 | return "Venue #" + str(self.id) 3319 | 3320 | def __eq__(self, other): 3321 | return self.get_id() == other.get_id() 3322 | 3323 | def _get_params(self): 3324 | return {"venue": self.get_id()} 3325 | 3326 | def get_id(self): 3327 | """Returns the id of the venue.""" 3328 | 3329 | return self.id 3330 | 3331 | def get_upcoming_events(self): 3332 | """Returns the upcoming events in this venue.""" 3333 | 3334 | doc = self._request("venue.getEvents", True) 3335 | 3336 | seq = [] 3337 | for node in doc.getElementsByTagName("event"): 3338 | seq.append(Event(_extract(node, "id"), self.network)) 3339 | 3340 | return seq 3341 | 3342 | def get_past_events(self): 3343 | """Returns the past events held in this venue.""" 3344 | 3345 | doc = self._request("venue.getEvents", True) 3346 | 3347 | seq = [] 3348 | for node in doc.getElementsByTagName("event"): 3349 | seq.append(Event(_extract(node, "id"), self.network)) 3350 | 3351 | return seq 3352 | 3353 | def md5(text): 3354 | """Returns the md5 hash of a string.""" 3355 | 3356 | h = hashlib.md5() 3357 | h.update(_string(text)) 3358 | 3359 | return h.hexdigest() 3360 | 3361 | def async_call(sender, call, callback = None, call_args = None, callback_args = None): 3362 | """This is the function for setting up an asynchronous operation. 3363 | * call: The function to call asynchronously. 3364 | * callback: The function to call after the operation is complete, Its prototype has to be like: 3365 | callback(sender, output[, param1, param3, ... ]) 3366 | * call_args: A sequence of args to be passed to call. 3367 | * callback_args: A sequence of args to be passed to callback. 3368 | """ 3369 | 3370 | thread = _ThreadedCall(sender, call, call_args, callback, callback_args) 3371 | thread.start() 3372 | 3373 | def _unicode(text): 3374 | if type(text) == unicode: 3375 | return text 3376 | 3377 | if type(text) == int: 3378 | return unicode(text) 3379 | 3380 | return unicode(text, "utf-8") 3381 | 3382 | def _string(text): 3383 | if type(text) == str: 3384 | return text 3385 | 3386 | if type(text) == int: 3387 | return str(text) 3388 | 3389 | return text.encode("utf-8") 3390 | 3391 | def _collect_nodes(limit, sender, method_name, cacheable, params=None): 3392 | """ 3393 | Returns a sequqnce of dom.Node objects about as close to 3394 | limit as possible 3395 | """ 3396 | 3397 | if not limit: limit = sys.maxint 3398 | if not params: params = sender._get_params() 3399 | 3400 | nodes = [] 3401 | page = 1 3402 | end_of_pages = False 3403 | 3404 | while len(nodes) < limit and not end_of_pages: 3405 | params["page"] = str(page) 3406 | doc = sender._request(method_name, cacheable, params) 3407 | 3408 | main = doc.documentElement.childNodes[1] 3409 | 3410 | if main.hasAttribute("totalPages"): 3411 | total_pages = _number(main.getAttribute("totalPages")) 3412 | elif main.hasAttribute("totalpages"): 3413 | total_pages = _number(main.getAttribute("totalpages")) 3414 | else: 3415 | raise Exception("No total pages attribute") 3416 | 3417 | for node in main.childNodes: 3418 | if not node.nodeType == xml.dom.Node.TEXT_NODE and len(nodes) < limit: 3419 | nodes.append(node) 3420 | 3421 | if page >= total_pages: 3422 | end_of_pages = True 3423 | 3424 | page += 1 3425 | 3426 | return nodes 3427 | 3428 | def _extract(node, name, index = 0): 3429 | """Extracts a value from the xml string""" 3430 | 3431 | nodes = node.getElementsByTagName(name) 3432 | 3433 | if len(nodes): 3434 | if nodes[index].firstChild: 3435 | return _unescape_htmlentity(nodes[index].firstChild.data.strip()) 3436 | else: 3437 | return None 3438 | 3439 | def _extract_all(node, name, limit_count = None): 3440 | """Extracts all the values from the xml string. returning a list.""" 3441 | 3442 | seq = [] 3443 | 3444 | for i in range(0, len(node.getElementsByTagName(name))): 3445 | if len(seq) == limit_count: 3446 | break 3447 | 3448 | seq.append(_extract(node, name, i)) 3449 | 3450 | return seq 3451 | 3452 | def _url_safe(text): 3453 | """Does all kinds of tricks on a text to make it safe to use in a url.""" 3454 | 3455 | if type(text) == unicode: 3456 | text = text.encode('utf-8') 3457 | 3458 | return urllib.quote_plus(urllib.quote_plus(text)).lower() 3459 | 3460 | def _number(string): 3461 | """ 3462 | Extracts an int from a string. Returns a 0 if None or an empty string was passed 3463 | """ 3464 | 3465 | if not string: 3466 | return 0 3467 | elif string == "": 3468 | return 0 3469 | else: 3470 | try: 3471 | return int(string) 3472 | except ValueError: 3473 | return float(string) 3474 | 3475 | def _unescape_htmlentity(string): 3476 | 3477 | string = _unicode(string) 3478 | 3479 | mapping = htmlentitydefs.name2codepoint 3480 | for key in mapping: 3481 | string = string.replace("&%s;" %key, unichr(mapping[key])) 3482 | 3483 | return string 3484 | 3485 | def extract_items(topitems_or_libraryitems): 3486 | """Extracts a sequence of items from a sequence of TopItem or LibraryItem objects.""" 3487 | 3488 | seq = [] 3489 | for i in topitems_or_libraryitems: 3490 | seq.append(i.item) 3491 | 3492 | return seq 3493 | 3494 | class ScrobblingError(Exception): 3495 | def __init__(self, message): 3496 | Exception.__init__(self) 3497 | self.message = message 3498 | 3499 | @_string_output 3500 | def __str__(self): 3501 | return self.message 3502 | 3503 | class BannedClientError(ScrobblingError): 3504 | def __init__(self): 3505 | ScrobblingError.__init__(self, "This version of the client has been banned") 3506 | 3507 | class BadAuthenticationError(ScrobblingError): 3508 | def __init__(self): 3509 | ScrobblingError.__init__(self, "Bad authentication token") 3510 | 3511 | class BadTimeError(ScrobblingError): 3512 | def __init__(self): 3513 | ScrobblingError.__init__(self, "Time provided is not close enough to current time") 3514 | 3515 | class BadSessionError(ScrobblingError): 3516 | def __init__(self): 3517 | ScrobblingError.__init__(self, "Bad session id, consider re-handshaking") 3518 | 3519 | class _ScrobblerRequest(object): 3520 | 3521 | def __init__(self, url, params, network, type="POST"): 3522 | self.params = params 3523 | self.type = type 3524 | (self.hostname, self.subdir) = urllib.splithost(url[len("http:"):]) 3525 | self.network = network 3526 | 3527 | def execute(self): 3528 | """Returns a string response of this request.""" 3529 | 3530 | connection = httplib.HTTPConnection(self.hostname) 3531 | 3532 | data = [] 3533 | for name in self.params.keys(): 3534 | value = urllib.quote_plus(self.params[name]) 3535 | data.append('='.join((name, value))) 3536 | data = "&".join(data) 3537 | 3538 | headers = { 3539 | "Content-type": "application/x-www-form-urlencoded", 3540 | "Accept-Charset": "utf-8", 3541 | "User-Agent": "pylast" + "/" + __version__, 3542 | "HOST": self.hostname 3543 | } 3544 | 3545 | if self.type == "GET": 3546 | connection.request("GET", self.subdir + "?" + data, headers = headers) 3547 | else: 3548 | connection.request("POST", self.subdir, data, headers) 3549 | response = connection.getresponse().read() 3550 | 3551 | self._check_response_for_errors(response) 3552 | 3553 | return response 3554 | 3555 | def _check_response_for_errors(self, response): 3556 | """When passed a string response it checks for erros, raising 3557 | any exceptions as necessary.""" 3558 | 3559 | lines = response.split("\n") 3560 | status_line = lines[0] 3561 | 3562 | if status_line == "OK": 3563 | return 3564 | elif status_line == "BANNED": 3565 | raise BannedClientError() 3566 | elif status_line == "BADAUTH": 3567 | raise BadAuthenticationError() 3568 | elif status_line == "BadTimeError": 3569 | raise BadTimeError() 3570 | elif status_line == "BadSessionError": 3571 | raise BadSessionError() 3572 | elif status_line.startswith("FAILED "): 3573 | reason = status_line[status_line.find("FAILED ")+len("FAILED "):] 3574 | raise ScrobblingError(reason) 3575 | 3576 | class Scrobbler(object): 3577 | """A class for scrobbling tracks to Last.fm""" 3578 | 3579 | session_id = None 3580 | nowplaying_url = None 3581 | submissions_url = None 3582 | 3583 | def __init__(self, network, client_id, client_version): 3584 | self.client_id = client_id 3585 | self.client_version = client_version 3586 | self.username = network.username 3587 | self.password = network.password_hash 3588 | self.network = network 3589 | 3590 | def _do_handshake(self): 3591 | """Handshakes with the server""" 3592 | 3593 | timestamp = str(int(time.time())) 3594 | 3595 | if self.password and self.username: 3596 | token = md5(self.password + timestamp) 3597 | elif self.network.api_key and self.network.api_secret and self.network.session_key: 3598 | if not self.username: 3599 | self.username = self.network.get_authenticated_user().get_name() 3600 | token = md5(self.network.api_secret + timestamp) 3601 | 3602 | params = {"hs": "true", "p": "1.2.1", "c": self.client_id, 3603 | "v": self.client_version, "u": self.username, "t": timestamp, 3604 | "a": token} 3605 | 3606 | if self.network.session_key and self.network.api_key: 3607 | params["sk"] = self.network.session_key 3608 | params["api_key"] = self.network.api_key 3609 | 3610 | server = self.network.submission_server 3611 | response = _ScrobblerRequest(server, params, self.network, "GET").execute().split("\n") 3612 | 3613 | self.session_id = response[1] 3614 | self.nowplaying_url = response[2] 3615 | self.submissions_url = response[3] 3616 | 3617 | def _get_session_id(self, new = False): 3618 | """Returns a handshake. If new is true, then it will be requested from the server 3619 | even if one was cached.""" 3620 | 3621 | if not self.session_id or new: 3622 | self._do_handshake() 3623 | 3624 | return self.session_id 3625 | 3626 | def report_now_playing(self, artist, title, album = "", duration = "", track_number = "", mbid = ""): 3627 | 3628 | params = {"s": self._get_session_id(), "a": artist, "t": title, 3629 | "b": album, "l": duration, "n": track_number, "m": mbid} 3630 | 3631 | try: 3632 | _ScrobblerRequest(self.nowplaying_url, params, self.network).execute() 3633 | except BadSessionError: 3634 | self._do_handshake() 3635 | self.report_now_playing(artist, title, album, duration, track_number, mbid) 3636 | 3637 | def scrobble(self, artist, title, time_started, source, mode, duration, album="", track_number="", mbid=""): 3638 | """Scrobble a track. parameters: 3639 | artist: Artist name. 3640 | title: Track title. 3641 | time_started: UTC timestamp of when the track started playing. 3642 | source: The source of the track 3643 | SCROBBLE_SOURCE_USER: Chosen by the user (the most common value, unless you have a reason for choosing otherwise, use this). 3644 | SCROBBLE_SOURCE_NON_PERSONALIZED_BROADCAST: Non-personalised broadcast (e.g. Shoutcast, BBC Radio 1). 3645 | SCROBBLE_SOURCE_PERSONALIZED_BROADCAST: Personalised recommendation except Last.fm (e.g. Pandora, Launchcast). 3646 | SCROBBLE_SOURCE_LASTFM: ast.fm (any mode). In this case, the 5-digit recommendation_key value must be set. 3647 | SCROBBLE_SOURCE_UNKNOWN: Source unknown. 3648 | mode: The submission mode 3649 | SCROBBLE_MODE_PLAYED: The track was played. 3650 | SCROBBLE_MODE_LOVED: The user manually loved the track (implies a listen) 3651 | SCROBBLE_MODE_SKIPPED: The track was skipped (Only if source was Last.fm) 3652 | SCROBBLE_MODE_BANNED: The track was banned (Only if source was Last.fm) 3653 | duration: Track duration in seconds. 3654 | album: The album name. 3655 | track_number: The track number on the album. 3656 | mbid: MusicBrainz ID. 3657 | """ 3658 | 3659 | params = {"s": self._get_session_id(), "a[0]": _string(artist), "t[0]": _string(title), 3660 | "i[0]": str(time_started), "o[0]": source, "r[0]": mode, "l[0]": str(duration), 3661 | "b[0]": _string(album), "n[0]": track_number, "m[0]": mbid} 3662 | 3663 | _ScrobblerRequest(self.submissions_url, params, self.network).execute() 3664 | 3665 | def scrobble_many(self, tracks): 3666 | """ 3667 | Scrobble several tracks at once. 3668 | 3669 | tracks: A sequence of a sequence of parameters for each trach. The order of parameters 3670 | is the same as if passed to the scrobble() method. 3671 | """ 3672 | 3673 | remainder = [] 3674 | 3675 | if len(tracks) > 50: 3676 | remainder = tracks[50:] 3677 | tracks = tracks[:50] 3678 | 3679 | params = {"s": self._get_session_id()} 3680 | 3681 | i = 0 3682 | for t in tracks: 3683 | _pad_list(t, 9, "") 3684 | params["a[%s]" % str(i)] = _string(t[0]) 3685 | params["t[%s]" % str(i)] = _string(t[1]) 3686 | params["i[%s]" % str(i)] = str(t[2]) 3687 | params["o[%s]" % str(i)] = t[3] 3688 | params["r[%s]" % str(i)] = t[4] 3689 | params["l[%s]" % str(i)] = str(t[5]) 3690 | params["b[%s]" % str(i)] = _string(t[6]) 3691 | params["n[%s]" % str(i)] = t[7] 3692 | params["m[%s]" % str(i)] = t[8] 3693 | 3694 | i += 1 3695 | 3696 | _ScrobblerRequest(self.submissions_url, params, self.network).execute() 3697 | 3698 | if remainder: 3699 | self.scrobble_many(remainder) 3700 | -------------------------------------------------------------------------------- /service/reset.sql: -------------------------------------------------------------------------------- 1 | delete from stalkify_tracks; 2 | delete from stalkify_playlists; 3 | 4 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type, update_interval) values ('steffentchr', 'Stalkify: steffentchr / live', 'recent', '1 minute'::interval); 5 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('steffentchr', 'Stalkify: steffentchr / all-time', 'toptracks-overall'); 6 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('steffentchr', 'Stalkify: steffentchr / this week', 'toptracks-7day'); 7 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('steffentchr', 'Stalkify: steffentchr / 3 months', 'toptracks-3month'); 8 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('steffentchr', 'Stalkify: steffentchr / 6 months', 'toptracks-6month'); 9 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('steffentchr', 'Stalkify: steffentchr / this year', 'toptracks-12month'); 10 | 11 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type, update_interval) values ('pollethewonder', 'Stalkify: pollethewonder / live', 'recent', '1 minute'::interval); 12 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('pollethewonder', 'Stalkify: pollethewonder / all-time', 'toptracks-overall'); 13 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('pollethewonder', 'Stalkify: pollethewonder / this week', 'toptracks-7day'); 14 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('pollethewonder', 'Stalkify: pollethewonder / 3 months', 'toptracks-3month'); 15 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('pollethewonder', 'Stalkify: pollethewonder / 6 months', 'toptracks-6month'); 16 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('pollethewonder', 'Stalkify: pollethewonder / this year', 'toptracks-12month'); 17 | 18 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type, update_interval) values ('jacobms', 'Stalkify: jacobms / live', 'recent', '1 minute'::interval); 19 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('jacobms', 'Stalkify: jacobms / all-time', 'toptracks-overall'); 20 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('jacobms', 'Stalkify: jacobms / this week', 'toptracks-7day'); 21 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('jacobms', 'Stalkify: jacobms / 3 months', 'toptracks-3month'); 22 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('jacobms', 'Stalkify: jacobms / 6 months top tracks', 'toptracks-6month'); 23 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('jacobms', 'Stalkify: jacobms / this year', 'toptracks-12month'); 24 | 25 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type, update_interval) values ('mortenjust', 'Stalkify: mortenjust / live', 'recent', '1 minute'::interval); 26 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('mortenjust', 'Stalkify: mortenjust / all-time', 'toptracks-overall'); 27 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('mortenjust', 'Stalkify: mortenjust / this week', 'toptracks-7day'); 28 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('mortenjust', 'Stalkify: mortenjust / 3 months', 'toptracks-3month'); 29 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('mortenjust', 'Stalkify: mortenjust / 6 months', 'toptracks-6month'); 30 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('mortenjust', 'Stalkify: mortenjust / this year', 'toptracks-12month'); 31 | -------------------------------------------------------------------------------- /service/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec 2>&1 3 | exec chpst -usteffentchr sh service 4 | -------------------------------------------------------------------------------- /service/service: -------------------------------------------------------------------------------- 1 | cd /web/stalkify/service 2 | 3 | while true; do 4 | echo IMPORT 5 | ./import.py 6 | echo SYNC 7 | ./sync.rb 8 | sleep 2 9 | done -------------------------------------------------------------------------------- /service/service-import: -------------------------------------------------------------------------------- 1 | cd /web/stalkify/service 2 | 3 | while true; do 4 | echo IMPORT 5 | nice -n 19 ./import.py 6 | sleep 20 7 | done -------------------------------------------------------------------------------- /service/service-match: -------------------------------------------------------------------------------- 1 | cd /web/stalkify/service 2 | 3 | while true; do 4 | echo MATCH 5 | nice -n 19 ./match.py 6 | sleep 20 7 | done -------------------------------------------------------------------------------- /service/service-sync: -------------------------------------------------------------------------------- 1 | cd /web/stalkify/service 2 | 3 | while true; do 4 | echo SYNC 5 | nice -n 19 ./sync.rb 6 | sleep 120 7 | done -------------------------------------------------------------------------------- /service/stalkify.sql: -------------------------------------------------------------------------------- 1 | create sequence seq_stalkify; 2 | create table stalkify_playlists ( 3 | playlist_id integer 4 | not null 5 | primary key 6 | default nextval('seq_stalkify'), 7 | lastfm_username text 8 | not null, 9 | playlist_name text 10 | not null, 11 | feed_type text 12 | not null, 13 | spotify_uri text, 14 | creation_date timestamptz 15 | not null 16 | default now(), 17 | update_interval interval 18 | not null 19 | default '1 day'::interval, 20 | num_opens integer 21 | not null 22 | default 0, 23 | last_updated timestamptz, 24 | clear_p boolean 25 | default false 26 | ); 27 | 28 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type, update_interval) values ('steffentchr', 'Stalkify: steffentchr''s recent', 'recent', '1 minute'::interval); 29 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('steffentchr', 'Stalkify: steffentchr''s all-time top tracks', 'toptracks-overall'); 30 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('steffentchr', 'Stalkify: steffentchr''s top tracks this week', 'toptracks-7day'); 31 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('steffentchr', 'Stalkify: steffentchr''s 3-months top tracks', 'toptracks-3month'); 32 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('steffentchr', 'Stalkify: steffentchr''s 6-months top tracks', 'toptracks-6month'); 33 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('steffentchr', 'Stalkify: steffentchr''s top tracks this year', 'toptracks-12month'); 34 | 35 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type, update_interval) values ('pollethewonder', 'Stalkify: pollethewonder''s recent', 'recent', '1 minute'::interval); 36 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('pollethewonder', 'Stalkify: pollethewonder''s all-time top tracks', 'toptracks-overall'); 37 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('pollethewonder', 'Stalkify: pollethewonder''s top tracks this week', 'toptracks-7day'); 38 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('pollethewonder', 'Stalkify: pollethewonder''s 3-months top tracks', 'toptracks-3month'); 39 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('pollethewonder', 'Stalkify: pollethewonder''s 6-months top tracks', 'toptracks-6month'); 40 | insert into stalkify_playlists (lastfm_username, playlist_name, feed_type) values ('pollethewonder', 'Stalkify: pollethewonder''s top tracks this year', 'toptracks-12month'); 41 | 42 | create table stalkify_tracks ( 43 | track_id integer 44 | not null 45 | primary key 46 | default nextval('seq_stalkify'), 47 | playlist_id integer 48 | not null 49 | references stalkify_playlists, 50 | name text 51 | not null, 52 | artist text 53 | not null, 54 | spotify_uri text, 55 | creation_date timestamptz 56 | not null 57 | default now(), 58 | processed_p boolean 59 | not null 60 | default false, 61 | processed_date timestamptz 62 | ); 63 | 64 | create index idx_track_playlist_id on stalkify_tracks(playlist_id); 65 | create index idx_playlist_spotify_uri on stalkify_playlists(spotify_uri); 66 | create index idx_track_name on stalkify_tracks(name); 67 | create index idx_track_artist on stalkify_tracks(artist); 68 | create index idx_track_name_artist on stalkify_tracks(name, artist); 69 | create index idx_track_processed_p on stalkify_tracks(processed_p); 70 | 71 | -------------------------------------------------------------------------------- /service/sync.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require 'postgres' 5 | require 'greenstripes' 6 | require 'config' 7 | 8 | printf "Logging in...\n" 9 | 10 | 11 | # Connect to Spotify 12 | session = GreenStripes::Session.new(SPOTIFY_APPKEY, 'GreenStripes', 'tmp', 'tmp') 13 | session.login(SPOTIFY_USERNAME, SPOTIFY_PASSWORD) 14 | printf "Waiting for Spotify to confirm login\n" 15 | session.process_events until session.connection_state == GreenStripes::ConnectionState::LOGGED_IN 16 | printf "Yes, we're logged in!\n" 17 | 18 | printf "Now, get the playlist container via the API.\n" 19 | 20 | pc = session.playlist_container() 21 | printf "Done with playlist container.\n" 22 | 23 | # Connect to pgsql 24 | printf "Connect to database.\n" 25 | conn = PGconn.connect("localhost",5432,"","",DB_NAME,DB_USERNAME,DB_PASSWORD) 26 | printf "Done connecting to DB.\n" 27 | 28 | # Sync playlists 29 | printf "Syncing playlists\n" 30 | results = conn.query("select playlist_id, playlist_name from stalkify_playlists pl where spotify_uri is null and exists (select 1 from stalkify_tracks tr where tr.playlist_id = pl.playlist_id)") 31 | results.each do |row| 32 | playlist_id = row[0] 33 | playlist_name = row[1] 34 | 35 | p = pc.add_new_playlist(playlist_name) 36 | uri = GreenStripes::Link.new(p).to_s() 37 | conn.query("update stalkify_playlists set spotify_uri = '" + uri + "' where playlist_id = " + playlist_id) 38 | printf "Created playlist '%s' with uri '%s'\n", p.name(), uri 39 | end 40 | 41 | # Index existing playlists 42 | playlists = {} 43 | index = 0 44 | printf "Indexing playlists\n" 45 | while index0 64 | # this is a method hacked into greenstripes 65 | p.remove_first_track() 66 | end 67 | end 68 | conn.query("update stalkify_playlists set clear_p = false where playlist_id = " + playlist_id) 69 | end 70 | 71 | 72 | 73 | 74 | 75 | # Sync tracks 76 | printf "Sync tracks\n" 77 | results = conn.query("select tr.track_id, pl.spotify_uri, tr.spotify_uri from stalkify_playlists pl, stalkify_tracks tr where tr.playlist_id=pl.playlist_id and tr.spotify_uri is not null and tr.processed_p is false and pl.clear_p is false and pl.spotify_uri is not null order by track_id asc") 78 | results.each do |row| 79 | track_id = row[0] 80 | playlist_uri = row[1] 81 | track_uri = row[2] 82 | 83 | p = playlists[playlist_uri] 84 | if p then 85 | t = GreenStripes::Link.new(track_uri).to_track() 86 | printf "Added track %s (%s) to %s (%s)\n", t.name(), track_uri, p.name(), playlist_uri 87 | tr = [t]; 88 | p.add_tracks(tr, 1, p.num_tracks()) 89 | conn.query("update stalkify_tracks set processed_p=true, processed_date=now() where track_id = " + track_id) 90 | else 91 | printf "Orphaned playlist ID, fix for %s\n", playlist_uri 92 | conn.query("update stalkify_playlists set spotify_uri = NULL where spotify_uri = '" + playlist_uri + "'") 93 | end 94 | end 95 | 96 | printf "Closing down\n" 97 | conn.close() 98 | 99 | session.logout 100 | session.process_events until session.connection_state == GreenStripes::ConnectionState::LOGGED_OUT 101 | 102 | printf "Done\n" 103 | exit() 104 | -------------------------------------------------------------------------------- /service/tail: -------------------------------------------------------------------------------- 1 | tail -F /var/log/stalkify-import/current /var/log/stalkify-match/current /var/log/stalkify-sync/current -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | ### Quick! 2 | 3 | * Remove tracks in correct manner 4 | * Fewer sql injections 5 | * Optimize status indicator 6 | 7 | ### Social 8 | 9 | * New feature on stalkify.com: Tweet it 10 | * New feature on stalkify.com: Facebook like 11 | 12 | ### Future 13 | 14 | * Include/handle now playing track 15 | * New lists: Top albums 16 | * New list: Recommended artists 17 | * New feature on stalkify.com: Neighbouring users 18 | --------------------------------------------------------------------------------