├── .gitignore ├── LICENSE ├── README.md ├── changelog.md ├── data ├── icons │ ├── pithos-large.ico │ ├── pithos-small.ico │ └── scalable │ │ └── apps │ │ ├── pithos-mono.svg │ │ └── pithos.svg ├── media │ ├── album_default.png │ ├── album_default.svg │ ├── icon.png │ ├── pithos-mono.png │ ├── rate_bg.png │ └── rate_bg.svg └── ui │ ├── AboutPithosDialog.ui │ ├── PithosWindow.ui │ ├── PreferencesPithosDialog.ui │ ├── SearchDialog.ui │ ├── StationsDialog.ui │ ├── about_pithos_dialog.xml │ ├── pithos_window.xml │ ├── preferences_pithos_dialog.xml │ ├── search_dialog.xml │ └── stations_dialog.xml ├── pithos.bat ├── pithos.pyw ├── pithos ├── AboutPithosDialog.py ├── PreferencesPithosDialog.py ├── SearchDialog.py ├── StationsDialog.py ├── __init__.py ├── gobject_worker.py ├── pandora │ ├── __init__.py │ ├── blowfish.py │ ├── fake.py │ └── pandora.py ├── pithosconfig.py ├── plugin.py ├── plugins │ ├── __init__.py │ ├── growl.py │ ├── mediakeys.py │ ├── notification_icon.py │ └── scrobble.py └── pylast.py └── windows ├── MoveFileFolder.nsh ├── dependencies.nsi ├── detect.nsi ├── download.nsi ├── install.nsi └── redist.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.msi 3 | *.exe 4 | /pithos/pithos.ini -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pithos for Windows 2 | ============= 3 | 4 | Pithos is a native Pandora Radio client for ~~Linux~~ Windows. It's much more lightweight than the Pandora.com web client. 5 | 6 | **NOTE** This project is no longer maintained. Upstream [Pithos](https://github.com/pithos/pithos) is actively developed however does not directly support Windows. If you would like to use Pithos on Windows your best chance is probably using the Linux version on Windows via [WSL](https://docs.microsoft.com/en-us/windows/wsl/about). 7 | 8 | 9 | Installation 10 | ----------- 11 | Newest release can be found [here](https://github.com/TingPing/pithos-for-windows/releases) and read notes below. 12 | 13 | 64bit users can just download this [zip](https://github.com/TingPing/pithos-for-windows/archive/master.zip) and extract it, though 64bit isn't recommended and you will have to manage the dependencies yourself. 14 | 15 | See [redist.txt](https://github.com/TingPing/pithos-for-windows/blob/master/windows/redist.txt) for optional plugin requirements. 16 | 17 | Notes 18 | ----- 19 | 20 | You must remove pygtk if installed with an older version. 21 | 22 | If Python is installed to a custom directory(not C:\python27) it must be in the PATH environment variable to work (google to find out how) 23 | 24 | If previous versions crashed please try out the newest version as this has been resolved. 25 | 26 | ------------------ 27 | 28 | ![Pithos for Windows Screenshot](http://i.imgur.com/PcAMD.png) 29 | 30 | ------------------ 31 | 32 | Pithos is not affiliated with or endorsed by Pandora Media, Inc. 33 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | $$title: Changelog 2 | 3 | ### 0.3.17 4 | 2012-05-03 5 | 6 | * Switch to JSON API to work around XMLRPC API update. Thanks to 7 | Christopher Eby for the patch. 8 | [(lp:988395)](https://bugs.launchpad.net/pithos/+bug/988395) 9 | 10 | ### 0.3.16 11 | 2012-04-19 12 | 13 | * Numerous bugfixes 14 | * [(lp:905204)](https://bugs.launchpad.net/pithos/+bug/905204) 15 | * [(lp:876984)](https://bugs.launchpad.net/pithos/+bug/876984) 16 | * [(lp:976327)](https://bugs.launchpad.net/pithos/+bug/976327) 17 | * [(lp:883610)](https://bugs.launchpad.net/pithos/+bug/883610) 18 | * [(lp:891225)](https://bugs.launchpad.net/pithos/+bug/891225) 19 | * [(lp:880142)](https://bugs.launchpad.net/pithos/+bug/880142) 20 | * [(lp:836393)](https://bugs.launchpad.net/pithos/+bug/836393) 21 | * [(lp:907278)](https://bugs.launchpad.net/pithos/+bug/907278) 22 | * [(lp:812626)](https://bugs.launchpad.net/pithos/+bug/812626) 23 | 24 | 25 | * Minor User Interface improvements 26 | * [(lp:904589)](https://bugs.launchpad.net/pithos/+bug/904589) 27 | * [(lp:909198)](https://bugs.launchpad.net/pithos/+bug/909198) 28 | 29 | Thanks to Adam Porter, Derek Ditch, Martin Langhoff, and Malcolm Lewis, who contributed to this release. 30 | 31 | ### 0.3.14 32 | 2011-12-14 33 | 34 | * Use misc.sync method to get Pandora time offset. Fixes "You have no chance 35 | to survive make your time" [(lp:743198)](https://bugs.launchpad.net/pithos/+bug/743198) 36 | (thanks Matt Harrison, Adam Haile) 37 | * Improve error message and make it clear which messages come from Pandora 38 | * Fix media key support on Ubuntu 11.10 [(lp:902322)](https://bugs.launchpad.net/pithos/+bug/902322) (thanks RJP Computing) 39 | 40 | ### 0.3.13 41 | 2011-11-09 42 | 43 | * Use SSL for login - fixes AUTH_WEB_LOGIN_NOT_ALLOWED error from Pandora 44 | change 2 minutes after 0.3.12 release 45 | 46 | ### 0.3.12 47 | 2011-11-09 48 | 49 | * Pandora protocol v33 50 | * Less confusing error dialog for the next Pandora update 51 | * Notify plugin: escape song info so album names with special characters 52 | display properly on non-Ubuntu Gnome. 53 | 54 | ### 0.3.11 55 | 2011-09-22 56 | 57 | * Pandora protocol v32 58 | * Setting for audio format (by Stefan Nelson-Lindall) 59 | 60 | ### 0.3.10 61 | 2011-07-09 62 | 63 | * Pandora protocol v31; No key change, minor change to createStation 64 | 65 | ### 0.3.9 66 | 2011-04-27 67 | 68 | * New Pandora encryption keys [(lp:771804)](https://bugs.launchpad.net/pithos/+bug/771804) 69 | * Enable gstreamer progressive download to improve stream quality and work around [gstreamer bug 648786](https://bugzilla.gnome.org/show_bug.cgi?id=648786) on Ubuntu 11.04. [(lp:705271)](https://bugs.launchpad.net/pithos/+bug/705271) [(lp:759699)](https://bugs.launchpad.net/pithos/+bug/759699) 70 | 71 | ### 0.3.8 72 | 2011-04-12 73 | 74 | * CVE-2011-1500: Fix password leak to local users through file permissions (by Luke Faraone) [(lp:733307)](https://bugs.launchpad.net/pithos/+bug/733307) 75 | * Correctly handle hour-long songs (by Luke Faraone) [(lp:734962)](https://bugs.launchpad.net/pithos/+bug/734962) 76 | * Fix "TypeError: could not convert argument to correct param type" (by Rick Spencer) [(lp:706681)](https://bugs.launchpad.net/pithos/+bug/706681) 77 | 78 | 79 | ### 0.3.7 80 | 2011-01-09 81 | 82 | * Allow feedback to be removed from songs (by Christopher Eby) [(lp:659581)](https://bugs.launchpad.net/pithos/+bug/659581) 83 | * Don't save Pandora feedback to last.fm [(lp:636600)](https://bugs.launchpad.net/pithos/+bug/636600) 84 | * Minor bugfixes [(lp:670131)](https://bugs.launchpad.net/pithos/+bug/670131) [(lp:658230)](https://bugs.launchpad.net/pithos/+bug/658230) 85 | 86 | ### 0.3.6 87 | 2010-11-06 88 | 89 | * New Pandora encryption keys [(lp:671265)](https://bugs.launchpad.net/pithos/+bug/671265) 90 | 91 | ### 0.3.5 92 | 2010-10-30 93 | 94 | * Minor bugfixes [(lp:625095)](https://bugs.launchpad.net/pithos/+bug/625095) [(lp:628923)](https://bugs.launchpad.net/pithos/+bug/628923) [(lp:626980)](https://bugs.launchpad.net/pithos/+bug/626980) 95 | * Run properly on multi-user systems [(lp:667896)](https://bugs.launchpad.net/pithos/+bug/667896) 96 | * Screensaver pause plugin (by Matthew Gregg) 97 | * Integrate volume control with PulseAudio [(lp:650515)](https://bugs.launchpad.net/pithos/+bug/650515) 98 | * New Pandora encryption keys [(lp:655507)](https://bugs.launchpad.net/pithos/+bug/625095) 99 | 100 | ### 0.3.1 101 | 2010-08-26 102 | 103 | * Send song ratings to Last.fm [(lp:621360)](https://bugs.launchpad.net/pithos/+bug/621360) 104 | * Fixes to keybinder media key support 105 | * Better error messages 106 | * Dump debug log and XML to /tmp/pithos.debug.log for better debugging and bug reports 107 | * Don't crash if QuickMix is empty [(lp:622864)](https://bugs.launchpad.net/pithos/+bug/622864) 108 | 109 | ### 0.3 110 | 2010-08-19 111 | 112 | * Last.fm Scrobbling support [(lp:609246)](https://bugs.launchpad.net/pithos/+bug/609246) 113 | * Rewrite Libpiano in Python to avoid C dependency 114 | * Raise existing window if pithos command is run when already open [(lp:610574)](https://bugs.launchpad.net/pithos/+bug/610574) 115 | * Support bookmarking of songs and artists 116 | * Volume control (by Stephen Ostrow) [(lp:617597)](https://bugs.launchpad.net/pithos/+bug/617597) 117 | * Show song info in window title (by Stephen Ostrow) [(lp:614851)](https://bugs.launchpad.net/pithos/+bug/614851) 118 | * Double click a future song to play it (by Stephen Ostrow) [(lp:614852)](https://bugs.launchpad.net/pithos/+bug/614852) 119 | * Make stations dialog sortable (by Stephen Ostrow) [(lp:615415)](https://bugs.launchpad.net/pithos/+bug/615415) 120 | * Notification shown when play/pause media keys are pressed [(lp:614821)](https://bugs.launchpad.net/pithos/+bug/614821) 121 | * Other bugfixes [(lp:615160)](https://bugs.launchpad.net/pithos/+bug/615160) [(lp:614850)](https://bugs.launchpad.net/pithos/+bug/614850) 122 | 123 | ### 0.2.1 124 | 2010-08-02 125 | 126 | * Ellipsize long song titles / artist names [(lp:610859)](https://bugs.launchpad.net/pithos/+bug/610859) 127 | * Request Email instead of Username [(lp:609265)](https://bugs.launchpad.net/pithos/+bug/609265) 128 | 129 | ### 0.2 130 | 2010-07-24 131 | 132 | * Notification icon (by Vince Spicer and Vinod Khare) 133 | * User interface improvements 134 | * DBUS API 135 | 136 | ### 0.1 137 | January 2010 138 | 139 | * Play / Pause / Next Song 140 | * Switching stations 141 | * Remembering user name and password 142 | * Cover Art 143 | * Thumbs Up / Thumbs Down / Tired of this song 144 | * Notification popup with song info 145 | * Launching pandora.com song info page and station page 146 | * Reconnecting when pandora session times out 147 | * Editing QuickMix 148 | * Creating stations 149 | * Media Key support 150 | * Proxy support 151 | -------------------------------------------------------------------------------- /data/icons/pithos-large.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TingPing/pithos-for-windows/02a93e76ff6b616e14f10dfc04d1532bd7d98940/data/icons/pithos-large.ico -------------------------------------------------------------------------------- /data/icons/pithos-small.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TingPing/pithos-for-windows/02a93e76ff6b616e14f10dfc04d1532bd7d98940/data/icons/pithos-small.ico -------------------------------------------------------------------------------- /data/icons/scalable/apps/pithos-mono.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 25 | 27 | image/svg+xml 28 | 30 | 31 | 32 | 33 | 34 | 54 | 56 | 63 | 65 | 69 | 73 | 74 | 76 | 80 | 84 | 85 | 92 | 95 | 96 | 103 | 106 | 107 | 116 | 125 | 134 | 136 | 140 | 144 | 145 | 155 | 164 | 171 | 174 | 175 | 176 | 179 | 182 | 187 | 192 | 197 | 201 | 205 | 210 | 214 | 218 | 223 | 228 | 232 | 237 | 238 | 241 | 244 | 248 | 252 | 256 | 260 | 264 | 265 | 266 | 267 | 268 | -------------------------------------------------------------------------------- /data/icons/scalable/apps/pithos.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 16 | 20 | 24 | 25 | 27 | 31 | 35 | 36 | 43 | 46 | 47 | 54 | 57 | 58 | 67 | 76 | 85 | 87 | 91 | 95 | 96 | 106 | 115 | 122 | 125 | 126 | 127 | 130 | 133 | 138 | 143 | 148 | 152 | 156 | 161 | 165 | 169 | 174 | 179 | 183 | 188 | 189 | 192 | 195 | 199 | 203 | 207 | 211 | 215 | 216 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /data/media/album_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TingPing/pithos-for-windows/02a93e76ff6b616e14f10dfc04d1532bd7d98940/data/media/album_default.png -------------------------------------------------------------------------------- /data/media/album_default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 26 | 30 | 34 | 35 | 42 | 51 | 58 | 62 | 63 | 64 | 82 | 84 | 85 | 87 | image/svg+xml 88 | 90 | 91 | 92 | 93 | 94 | 99 | 106 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /data/media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TingPing/pithos-for-windows/02a93e76ff6b616e14f10dfc04d1532bd7d98940/data/media/icon.png -------------------------------------------------------------------------------- /data/media/pithos-mono.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TingPing/pithos-for-windows/02a93e76ff6b616e14f10dfc04d1532bd7d98940/data/media/pithos-mono.png -------------------------------------------------------------------------------- /data/media/rate_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TingPing/pithos-for-windows/02a93e76ff6b616e14f10dfc04d1532bd7d98940/data/media/rate_bg.png -------------------------------------------------------------------------------- /data/media/rate_bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 27 | 31 | 35 | 36 | 43 | 54 | 55 | 73 | 75 | 76 | 78 | image/svg+xml 79 | 81 | 82 | 83 | 84 | 85 | 90 | 97 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /data/ui/AboutPithosDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 5 8 | ../media/icon.png 9 | normal 10 | False 11 | Pithos 12 | 0.3 13 | Copyright © 2012 Kevin Mehall 14 | A Pandora Radio client for the GNOME Desktop 15 | http://kevinmehall.net/p/pithos 16 | ../media/icon.png 17 | 18 | 19 | True 20 | 2 21 | 22 | 23 | 24 | 25 | 26 | True 27 | end 28 | 29 | 30 | False 31 | end 32 | 0 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /data/ui/PreferencesPithosDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 333 7 | False 8 | 5 9 | Preferences 10 | center 11 | pithos 12 | normal 13 | 14 | 15 | True 16 | False 17 | 2 18 | 19 | 20 | True 21 | False 22 | end 23 | 24 | 25 | gtk-cancel 26 | False 27 | True 28 | True 29 | True 30 | False 31 | True 32 | 33 | 34 | 35 | False 36 | False 37 | 0 38 | 39 | 40 | 41 | 42 | gtk-ok 43 | False 44 | True 45 | True 46 | True 47 | False 48 | True 49 | 50 | 51 | 52 | False 53 | False 54 | 1 55 | 56 | 57 | 58 | 59 | False 60 | True 61 | end 62 | 0 63 | 64 | 65 | 66 | 67 | True 68 | False 69 | 15 70 | 3 71 | 5 72 | 5 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | True 115 | False 116 | 0 117 | Email 118 | 119 | 120 | 1 121 | 2 122 | 1 123 | 2 124 | 125 | 126 | 127 | 128 | True 129 | True 130 | 131 | 132 | 133 | 2 134 | 3 135 | 1 136 | 2 137 | 138 | 139 | 140 | 141 | True 142 | False 143 | 0 144 | Password 145 | 146 | 147 | 1 148 | 2 149 | 2 150 | 3 151 | 152 | 153 | 154 | 155 | True 156 | True 157 | False 158 | 159 | 160 | 161 | 2 162 | 3 163 | 2 164 | 3 165 | 166 | 167 | 168 | 169 | Pandora One Subscriber 170 | False 171 | True 172 | True 173 | False 174 | False 175 | 0.5 176 | True 177 | 178 | 179 | 2 180 | 3 181 | 3 182 | 4 183 | 184 | 185 | 186 | 187 | True 188 | False 189 | 0 190 | Pandora.com Account 191 | 192 | 193 | 194 | 195 | 196 | 3 197 | 5 198 | 199 | 200 | 201 | 202 | True 203 | False 204 | 0 205 | Options 206 | 207 | 208 | 209 | 210 | 211 | 3 212 | 8 213 | 9 214 | 5 215 | 216 | 217 | 218 | 219 | Show Growl notifications 220 | False 221 | True 222 | True 223 | False 224 | False 225 | 0.5 226 | True 227 | 228 | 229 | 1 230 | 3 231 | 10 232 | 11 233 | 234 | 235 | 236 | 237 | True 238 | False 239 | <small><a href='http://pandora.com'>Create an account at pandora.com</a></small> 240 | True 241 | 242 | 243 | 1 244 | 3 245 | 4 246 | 5 247 | 248 | 249 | 250 | 251 | True 252 | False 253 | 0 254 | Audio Quality 255 | 256 | 257 | 1 258 | 2 259 | 6 260 | 7 261 | 262 | 263 | 264 | 265 | True 266 | False 267 | 268 | 269 | 2 270 | 3 271 | 6 272 | 7 273 | 274 | 275 | 276 | 277 | True 278 | False 279 | 0 280 | Proxy URL 281 | 282 | 283 | 1 284 | 2 285 | 7 286 | 8 287 | 288 | 289 | 290 | 291 | True 292 | True 293 | 294 | 295 | 296 | 2 297 | 3 298 | 7 299 | 8 300 | 301 | 302 | 303 | 304 | Show notification area icon 305 | False 306 | True 307 | True 308 | False 309 | False 310 | 0.5 311 | True 312 | 313 | 314 | 1 315 | 3 316 | 11 317 | 12 318 | 319 | 320 | 321 | 322 | True 323 | False 324 | 0 325 | Last.fm Scrobbling 326 | 327 | 328 | 329 | 330 | 331 | 3 332 | 13 333 | 14 334 | 5 335 | 336 | 337 | 338 | 339 | Authenticate 340 | False 341 | True 342 | True 343 | True 344 | False 345 | 346 | 347 | 1 348 | 3 349 | 14 350 | 15 351 | 352 | 353 | 354 | 355 | True 356 | False 357 | 0 358 | Advanced Settings 359 | 360 | 361 | 362 | 363 | 364 | 3 365 | 5 366 | 6 367 | 5 368 | 369 | 370 | 371 | 372 | Use Mediakeys 373 | False 374 | True 375 | True 376 | False 377 | False 378 | 0.5 379 | True 380 | 381 | 382 | 1 383 | 3 384 | 9 385 | 10 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | False 394 | True 395 | 5 396 | 1 397 | 398 | 399 | 400 | 401 | 402 | button2 403 | button1 404 | 405 | 406 | 407 | -------------------------------------------------------------------------------- /data/ui/SearchDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 380 8 | 350 9 | 5 10 | Search 11 | pithos 12 | normal 13 | False 14 | 15 | 16 | True 17 | 2 18 | 19 | 20 | True 21 | 4 22 | 23 | 24 | True 25 | 26 | 27 | True 28 | True 29 | 30 | 31 | 32 | 33 | 0 34 | 35 | 36 | 37 | 38 | Search 39 | True 40 | True 41 | True 42 | image1 43 | 44 | 45 | 46 | False 47 | False 48 | 1 49 | 50 | 51 | 52 | 53 | False 54 | 0 55 | 56 | 57 | 58 | 59 | True 60 | True 61 | automatic 62 | automatic 63 | 64 | 65 | True 66 | True 67 | False 68 | 69 | 70 | 71 | column 72 | 73 | 74 | 75 | 1 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 1 85 | 86 | 87 | 88 | 89 | 1 90 | 91 | 92 | 93 | 94 | True 95 | end 96 | 97 | 98 | gtk-cancel 99 | True 100 | True 101 | True 102 | True 103 | 104 | 105 | False 106 | False 107 | 0 108 | 109 | 110 | 111 | 112 | gtk-ok 113 | True 114 | False 115 | True 116 | True 117 | True 118 | 119 | 120 | 121 | False 122 | False 123 | 1 124 | 125 | 126 | 127 | 128 | False 129 | end 130 | 0 131 | 132 | 133 | 134 | 135 | 136 | button2 137 | okbtn 138 | 139 | 140 | 141 | True 142 | gtk-find 143 | 144 | 145 | -------------------------------------------------------------------------------- /data/ui/StationsDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 480 8 | 420 9 | 1 10 | Manage Stations 11 | pithos 12 | normal 13 | 14 | 15 | 16 | True 17 | vertical 18 | 2 19 | 20 | 21 | True 22 | True 23 | automatic 24 | automatic 25 | 26 | 27 | True 28 | True 29 | 30 | 31 | 32 | 33 | 1 34 | 35 | 36 | 37 | 38 | True 39 | end 40 | 41 | 42 | Add Station 43 | True 44 | True 45 | True 46 | 47 | 48 | 49 | False 50 | False 51 | 0 52 | True 53 | 54 | 55 | 56 | 57 | Genre Stations 58 | False 59 | True 60 | True 61 | True 62 | 63 | 64 | 65 | False 66 | False 67 | 1 68 | True 69 | 70 | 71 | 72 | 73 | Refresh Stations 74 | True 75 | True 76 | True 77 | 78 | 79 | 80 | False 81 | False 82 | 2 83 | True 84 | 85 | 86 | 87 | 88 | gtk-close 89 | True 90 | True 91 | True 92 | True 93 | 94 | 95 | 96 | False 97 | False 98 | 3 99 | 100 | 101 | 102 | 103 | False 104 | end 105 | 0 106 | 107 | 108 | 109 | 110 | 111 | add_station 112 | add_genre_station 113 | refresh_stations 114 | close 115 | 116 | 117 | 118 | True 119 | 120 | 121 | Listen Now 122 | True 123 | True 124 | True 125 | 126 | 127 | 128 | 129 | 130 | gtk-info 131 | True 132 | True 133 | True 134 | 135 | 136 | 137 | 138 | 139 | Rename 140 | True 141 | image1 142 | False 143 | 144 | 145 | 146 | 147 | 148 | gtk-delete 149 | True 150 | True 151 | True 152 | 153 | 154 | 155 | 156 | 157 | True 158 | gtk-edit 159 | 160 | 161 | 5 162 | normal 163 | True 164 | warning 165 | 166 | 167 | True 168 | vertical 169 | 2 170 | 171 | 172 | True 173 | end 174 | 175 | 176 | gtk-cancel 177 | True 178 | True 179 | True 180 | True 181 | 182 | 183 | False 184 | False 185 | 0 186 | 187 | 188 | 189 | 190 | gtk-delete 191 | True 192 | True 193 | True 194 | True 195 | 196 | 197 | False 198 | False 199 | 1 200 | 201 | 202 | 203 | 204 | False 205 | end 206 | 0 207 | 208 | 209 | 210 | 211 | 212 | button2 213 | button1 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /data/ui/about_pithos_dialog.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /data/ui/pithos_window.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /data/ui/preferences_pithos_dialog.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /data/ui/search_dialog.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /data/ui/stations_dialog.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /pithos.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM 3 | REM Win32 launch script for Pithos based on Exailes 4 | REM 5 | REM Since GStreamer SDK and OSSBuild are a bit difficult to work with, we 6 | REM go through and set things up for the user so they don't need to worry 7 | REM too much about PATH variables being set properly and other madness. 8 | REM 9 | REM Additionally, this script tries to be a bit more verbose and let the 10 | REM user know more about the errors that they are seeing, instead of just a 11 | REM stack trace. 12 | REM 13 | 14 | setlocal 15 | 16 | set PYTHON_EXE=pythonw.exe 17 | 18 | echo Detecting Pithos requirements (this may take a minute): 19 | 20 | REM Detect Python in the path 21 | for %%X in (%PYTHON_EXE%) do (set PYTHON_BIN=%%~$PATH:X) 22 | if defined PYTHON_BIN set PYTHON_VIA=environment 23 | if defined PYTHON_BIN goto python_found 24 | 25 | REM No python in path, see if its in a default location. Prefer 26 | REM Python 2.7, since our installer ships with that as default 27 | set PYTHON_BIN=C:\Python27\%PYTHON_EXE% 28 | set PYTHON_VIA=hardcoded 29 | if exist python goto python_found 30 | 31 | :python_found 32 | echo Python : %PYTHON_BIN% (via %PYTHON_VIA%) 33 | if %PYTHON_VIA%==environment set PYTHON_BIN=%PYTHON_EXE% 34 | if %PYTHON_VIA%==hardcoded echo "If this is incorrect add the correct one to the PATH environment variable (google is your friend)" 35 | 36 | REM Detect GStreamer SDK 37 | set GST_VIA=environment 38 | set GST_SDK=N 39 | if defined GSTREAMER_SDK_ROOT_X86 set GST_SDK=%GSTREAMER_SDK_ROOT_X86% 40 | if defined GSTREAMER_SDK_ROOT_X86_64 set GST_SDK=%GSTREAMER_SDK_ROOT_X86_64% 41 | 42 | if not "%GST_SDK%" == "N" goto pygst_env_found 43 | 44 | REM For some reason the GStreamer SDK doesn't define the environment 45 | REM variables globally, so we just have to cheat if we can't do it 46 | REM the 'correct' way 47 | if exist C:\gstreamer-sdk\0.10\x86\bin goto found_pygst_x86_hardcoded 48 | if exist C:\gstreamer-sdk\0.10\x86_64\bin goto found_pygst_x64_hardcoded 49 | goto nogst 50 | 51 | :found_pygst_x86_hardcoded 52 | set GSTREAMER_SDK_ROOT_X86=C:\gstreamer-sdk\0.10\x86 53 | set GST_SDK=%GSTREAMER_SDK_ROOT_X86% 54 | set GST_VIA=hardcoded path 55 | goto pygst_env_found 56 | 57 | :found_pygst_x64_hardcoded 58 | set GSTREAMER_SDK_ROOT_X64=C:\gstreamer-sdk\0.10\x86_64 59 | set GST_SDK=%GSTREAMER_SDK_ROOT_X64% 60 | set GST_VIA=hardcoded path 61 | goto pygst_env_found 62 | 63 | :pygst_env_found 64 | echo GStreamer SDK Runtime : %GST_SDK% (via %GST_VIA%) 65 | 66 | REM 67 | REM Then try to setup the environment properly for GStreamer SDK 68 | REM -> Note that we put the GST path first, so that any needed DLLs 69 | REM are searched for there first, hopefully avoiding DLL hell 70 | REM 71 | 72 | set PATH=%GST_SDK%\bin;%PATH% 73 | set PYGST_BINDINGS=%GST_SDK%\lib\python2.7\site-packages 74 | if defined PYTHONPATH set PYTHONPATH=%PYGST_BINDINGS%;%PYTHONPATH% 75 | if not defined PYTHONPATH set PYTHONPATH=%PYGST_BINDINGS% 76 | 77 | :: Do this in case user has installed Python33 and selected for it to be added to PATH. Pithos does not work with Python33, it gives an exit code ^> 0. 78 | if exist "C:\Python27\%PYTHON_BIN%" set "PYTHON_BIN=C:\Python27\%PYTHON_BIN%" 79 | 80 | %PYTHON_BIN% -c "import pygst;pygst.require('0.10');import gst" 81 | if not %ERRORLEVEL% == 0 goto badgst 82 | 83 | :pygst_found 84 | echo GStreamer Python Bindings : %PYGST_BINDINGS% 85 | 86 | REM Detect PyGTK. We do detection here since it may be in the GStreamer SDK 87 | %PYTHON_BIN% -c "import pygtk;pygtk.require('2.0');import gtk" 88 | if not %ERRORLEVEL% == 0 goto badgtk 89 | 90 | echo PyGTK : OK 91 | 92 | echo Dependencies good, starting Pithos. 93 | echo. 94 | 95 | goto start_pithos 96 | 97 | REM Various errors 98 | 99 | :nopython 100 | echo Python 2.7 was not detected. Please include the python directory in your 101 | echo PATH, or install it. You can download it at http://www.python.com/ 102 | echo. 103 | pause && goto end 104 | 105 | :nogst 106 | echo GStreamer SDK Runtime : not found 107 | echo. 108 | echo GStreamer SDK Runtime was not found. 109 | echo. 110 | echo You can download the 32bit GST SDK runtime at http://www.gstreamer.com/ 111 | echo. 112 | pause && goto end 113 | 114 | :badgst 115 | echo GStreamer Python Bindings : not found 116 | echo. 117 | echo The python bindings for GStreamer could not be imported. Please re-run the 118 | echo installer and ensure that the python bindings are selected for 119 | echo installation (they should be selected by default). 120 | echo. 121 | echo You can download the 32bit GST SDK runtime at http://www.gstreamer.com/ 122 | echo. 123 | pause && goto end 124 | 125 | :badgtk 126 | echo. 127 | echo PyGTK 2.x could not be imported. It is installed by default with the 128 | echo GStreamer SDK (select GTK Python Bindings), or you can use the 129 | echo PyGTK all-in-one installer from http://www.pygtk.org/ 130 | echo. 131 | echo Note that the PyGTK all-in-one installer is NOT compatible with 132 | echo the GStreamer SDK. Please Uninstall it. 133 | echo. 134 | echo You can download the 32bit GST SDK runtime at http://www.gstreamer.com/ 135 | echo. 136 | pause && goto end 137 | 138 | 139 | :start_pithos 140 | pushd %~dp0 141 | start %PYTHON_BIN% pithos.pyw 142 | popd 143 | goto end 144 | 145 | :end 146 | endlocal 147 | -------------------------------------------------------------------------------- /pithos/AboutPithosDialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*- 2 | ### BEGIN LICENSE 3 | # Copyright (C) 2010-2012 Kevin Mehall 4 | #This program is free software: you can redistribute it and/or modify it 5 | #under the terms of the GNU General Public License version 3, as published 6 | #by the Free Software Foundation. 7 | # 8 | #This program is distributed in the hope that it will be useful, but 9 | #WITHOUT ANY WARRANTY; without even the implied warranties of 10 | #MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 11 | #PURPOSE. See the GNU General Public License for more details. 12 | # 13 | #You should have received a copy of the GNU General Public License along 14 | #with this program. If not, see . 15 | ### END LICENSE 16 | 17 | import sys 18 | import os 19 | import gtk 20 | 21 | from pithos.pithosconfig import getdatapath 22 | 23 | class AboutPithosDialog(gtk.AboutDialog): 24 | __gtype_name__ = "AboutPithosDialog" 25 | 26 | def __init__(self): 27 | """__init__ - This function is typically not called directly. 28 | Creation of a AboutPithosDialog requires redeading the associated ui 29 | file and parsing the ui definition extrenally, 30 | and then calling AboutPithosDialog.finish_initializing(). 31 | 32 | Use the convenience function NewAboutPithosDialog to create 33 | NewAboutPithosDialog objects. 34 | 35 | """ 36 | pass 37 | 38 | def finish_initializing(self, builder): 39 | """finish_initalizing should be called after parsing the ui definition 40 | and creating a AboutPithosDialog object with it in order to finish 41 | initializing the start of the new AboutPithosDialog instance. 42 | 43 | """ 44 | #get a reference to the builder and set up the signals 45 | self.builder = builder 46 | self.builder.connect_signals(self) 47 | 48 | #code for other initialization actions should be added here 49 | 50 | def NewAboutPithosDialog(): 51 | """NewAboutPithosDialog - returns a fully instantiated 52 | AboutPithosDialog object. Use this function rather than 53 | creating a AboutPithosDialog instance directly. 54 | 55 | """ 56 | 57 | #look for the ui file that describes the ui 58 | ui_filename = os.path.join(getdatapath(), 'ui', 'AboutPithosDialog.ui') 59 | if not os.path.exists(ui_filename): 60 | ui_filename = None 61 | 62 | builder = gtk.Builder() 63 | builder.add_from_file(ui_filename) 64 | dialog = builder.get_object("about_pithos_dialog") 65 | dialog.finish_initializing(builder) 66 | return dialog 67 | 68 | if __name__ == "__main__": 69 | dialog = NewAboutPithosDialog() 70 | dialog.show() 71 | gtk.main() 72 | 73 | -------------------------------------------------------------------------------- /pithos/PreferencesPithosDialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*- 2 | ### BEGIN LICENSE 3 | # Copyright (C) 2010 Kevin Mehall 4 | #This program is free software: you can redistribute it and/or modify it 5 | #under the terms of the GNU General Public License version 3, as published 6 | #by the Free Software Foundation. 7 | # 8 | #This program is distributed in the hope that it will be useful, but 9 | #WITHOUT ANY WARRANTY; without even the implied warranties of 10 | #MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 11 | #PURPOSE. See the GNU General Public License for more details. 12 | # 13 | #You should have received a copy of the GNU General Public License along 14 | #with this program. If not, see . 15 | ### END LICENSE 16 | 17 | import sys 18 | import os 19 | import stat 20 | import logging 21 | 22 | import gtk 23 | import gobject 24 | 25 | from pithos.pithosconfig import getdatapath, valid_audio_formats 26 | from pithos.plugins.scrobble import LastFmAuth 27 | 28 | configfilename = os.path.join(os.environ['appdata'], 'Pithos\\pithos.ini') 29 | 30 | 31 | class PreferencesPithosDialog(gtk.Dialog): 32 | __gtype_name__ = "PreferencesPithosDialog" 33 | prefernces = {} 34 | 35 | def __init__(self): 36 | """__init__ - This function is typically not called directly. 37 | Creation of a PreferencesPithosDialog requires redeading the associated ui 38 | file and parsing the ui definition extrenally, 39 | and then calling PreferencesPithosDialog.finish_initializing(). 40 | 41 | Use the convenience function NewPreferencesPithosDialog to create 42 | NewAboutPithosDialog objects. 43 | """ 44 | 45 | pass 46 | 47 | def finish_initializing(self, builder): 48 | """finish_initalizing should be called after parsing the ui definition 49 | and creating a AboutPithosDialog object with it in order to finish 50 | initializing the start of the new AboutPithosDialog instance. 51 | """ 52 | 53 | # get a reference to the builder and set up the signals 54 | self.builder = builder 55 | self.builder.connect_signals(self) 56 | 57 | # initialize the "Audio format" combobox backing list 58 | audio_quality_combo = self.builder.get_object('prefs_audio_quality') 59 | fmt_store = gtk.ListStore(gobject.TYPE_STRING) 60 | for audio_format, quality in valid_audio_formats: 61 | fmt_store.append((quality,)) 62 | audio_quality_combo.set_model(fmt_store) 63 | render_text = gtk.CellRendererText() 64 | audio_quality_combo.pack_start(render_text, expand=True) 65 | audio_quality_combo.add_attribute(render_text, "text", 0) 66 | 67 | self.__load_preferences() 68 | 69 | 70 | def get_preferences(self): 71 | """get_preferences - returns a dictionary object that contains 72 | preferences for pithos. 73 | """ 74 | return self.__preferences 75 | 76 | def __load_preferences(self): 77 | #default preferences that will be overwritten if some are saved 78 | self.__preferences = { 79 | "username":'', 80 | "password":'', 81 | "pandora_one":False, 82 | "growl":False, 83 | "last_station_id":None, 84 | "proxy":'', 85 | "show_icon": False, 86 | "lastfm_key": False, 87 | "mediakeys": False, 88 | "volume": 0.5, 89 | "audio_quality": valid_audio_formats[0][0], 90 | } 91 | 92 | try: 93 | f = open(configfilename) 94 | except IOError: 95 | f = [] 96 | 97 | for line in f: 98 | sep = line.find('=') 99 | key = line[:sep] 100 | if key in self.__preferences: 101 | val = line[sep+1:].strip() 102 | if val == 'None': val=None 103 | elif val == 'False': val=False 104 | elif val == 'True': val=True 105 | self.__preferences[key]=val 106 | self.setup_fields() 107 | 108 | def save(self): 109 | existed = os.path.exists(configfilename) 110 | try: 111 | f = open(configfilename, 'w') 112 | except IOError: 113 | pass 114 | 115 | if not existed: 116 | os.makedirs(configfilename[:-10]) 117 | f = open(configfilename, 'w') 118 | 119 | for key in self.__preferences: 120 | f.write('%s=%s\n'%(key, self.__preferences[key])) 121 | f.close() 122 | 123 | def setup_fields(self): 124 | self.builder.get_object('prefs_username').set_text(self.__preferences["username"]) 125 | self.builder.get_object('prefs_password').set_text(self.__preferences["password"]) 126 | self.builder.get_object('checkbutton_pandora_one').set_active(self.__preferences["pandora_one"]) 127 | self.builder.get_object('prefs_proxy').set_text(self.__preferences["proxy"]) 128 | 129 | audio_quality_combo = self.builder.get_object('prefs_audio_quality') 130 | try: 131 | audio_pref_idx = list(audio_format for audio_format, quality in valid_audio_formats).index(self.__preferences["audio_quality"]) 132 | except ValueError: 133 | audio_pref_idx = 0 134 | audio_quality_combo.set_active(audio_pref_idx) 135 | 136 | self.builder.get_object('checkbutton_icon').set_active(self.__preferences["show_icon"]) 137 | self.builder.get_object('checkbutton_growl').set_active(self.__preferences["growl"]) 138 | self.builder.get_object('checkbutton_mediakeys').set_active(self.__preferences["mediakeys"]) 139 | 140 | self.lastfm_auth = LastFmAuth(self.__preferences, "lastfm_key", self.builder.get_object('lastfm_btn')) 141 | 142 | def ok(self, widget, data=None): 143 | """ok - The user has elected to save the changes. 144 | Called before the dialog returns gtk.RESONSE_OK from run(). 145 | """ 146 | 147 | self.__preferences["username"] = self.builder.get_object('prefs_username').get_text() 148 | self.__preferences["password"] = self.builder.get_object('prefs_password').get_text() 149 | self.__preferences["pandora_one"] = self.builder.get_object('checkbutton_pandora_one').get_active() 150 | self.__preferences["growl"] = self.builder.get_object('checkbutton_growl').get_active() 151 | self.__preferences["mediakeys"] = self.builder.get_object('checkbutton_mediakeys').get_active() 152 | self.__preferences["proxy"] = self.builder.get_object('prefs_proxy').get_text() 153 | self.__preferences["audio_quality"] = valid_audio_formats[self.builder.get_object('prefs_audio_quality').get_active()][0] 154 | self.__preferences["show_icon"] = self.builder.get_object('checkbutton_icon').get_active() 155 | 156 | self.save() 157 | 158 | def cancel(self, widget, data=None): 159 | """cancel - The user has elected cancel changes. 160 | Called before the dialog returns gtk.RESPONSE_CANCEL for run() 161 | """ 162 | 163 | self.setup_fields() # restore fields to previous values 164 | pass 165 | 166 | 167 | def NewPreferencesPithosDialog(): 168 | """NewPreferencesPithosDialog - returns a fully instantiated 169 | PreferencesPithosDialog object. Use this function rather than 170 | creating a PreferencesPithosDialog instance directly. 171 | """ 172 | 173 | #look for the ui file that describes the ui 174 | ui_filename = os.path.join(getdatapath(), 'ui', 'PreferencesPithosDialog.ui') 175 | if not os.path.exists(ui_filename): 176 | ui_filename = None 177 | 178 | builder = gtk.Builder() 179 | builder.add_from_file(ui_filename) 180 | dialog = builder.get_object("preferences_pithos_dialog") 181 | dialog.finish_initializing(builder) 182 | return dialog 183 | 184 | if __name__ == "__main__": 185 | dialog = NewPreferencesPithosDialog() 186 | dialog.show() 187 | gtk.main() 188 | 189 | -------------------------------------------------------------------------------- /pithos/SearchDialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*- 2 | ### BEGIN LICENSE 3 | # Copyright (C) 2010-2012 Kevin Mehall 4 | #This program is free software: you can redistribute it and/or modify it 5 | #under the terms of the GNU General Public License version 3, as published 6 | #by the Free Software Foundation. 7 | # 8 | #This program is distributed in the hope that it will be useful, but 9 | #WITHOUT ANY WARRANTY; without even the implied warranties of 10 | #MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 11 | #PURPOSE. See the GNU General Public License for more details. 12 | # 13 | #You should have received a copy of the GNU General Public License along 14 | #with this program. If not, see . 15 | ### END LICENSE 16 | 17 | import sys 18 | import os 19 | import gtk, gobject 20 | import cgi 21 | 22 | from pithos.pithosconfig import getdatapath 23 | 24 | class SearchDialog(gtk.Dialog): 25 | __gtype_name__ = "SearchDialog" 26 | 27 | def __init__(self): 28 | """__init__ - This function is typically not called directly. 29 | Creation of a SearchDialog requires redeading the associated ui 30 | file and parsing the ui definition extrenally, 31 | and then calling SearchDialog.finish_initializing(). 32 | 33 | Use the convenience function NewSearchDialog to create 34 | a SearchDialog object. 35 | 36 | """ 37 | pass 38 | 39 | def finish_initializing(self, builder, worker_run): 40 | """finish_initalizing should be called after parsing the ui definition 41 | and creating a SearchDialog object with it in order to finish 42 | initializing the start of the new SearchDialog instance. 43 | 44 | """ 45 | #get a reference to the builder and set up the signals 46 | self.builder = builder 47 | self.builder.connect_signals(self) 48 | 49 | self.entry = self.builder.get_object('entry') 50 | self.treeview = self.builder.get_object('treeview') 51 | self.okbtn = self.builder.get_object('okbtn') 52 | self.searchbtn = self.builder.get_object('searchbtn') 53 | self.model = gtk.ListStore(gobject.TYPE_PYOBJECT, str) 54 | self.treeview.set_model(self.model) 55 | 56 | self.worker_run = worker_run 57 | 58 | self.result = None 59 | 60 | 61 | def ok(self, widget, data=None): 62 | """ok - The user has elected to save the changes. 63 | Called before the dialog returns gtk.RESONSE_OK from run(). 64 | 65 | """ 66 | 67 | 68 | def cancel(self, widget, data=None): 69 | """cancel - The user has elected cancel changes. 70 | Called before the dialog returns gtk.RESPONSE_CANCEL for run() 71 | 72 | """ 73 | pass 74 | 75 | def search_clicked(self, widget): 76 | self.search(self.entry.get_text()) 77 | 78 | def search(self, query): 79 | if not query: return 80 | def callback(results): 81 | self.model.clear() 82 | for i in results: 83 | if i.resultType is 'song': 84 | mk = "%s by %s"%(cgi.escape(i.title), cgi.escape(i.artist)) 85 | elif i.resultType is 'artist': 86 | mk = "%s (artist)"%(cgi.escape(i.name)) 87 | self.model.append((i, mk)) 88 | self.treeview.show() 89 | self.searchbtn.set_sensitive(True) 90 | self.searchbtn.set_label("Search") 91 | self.worker_run('search', (query,), callback, "Searching...") 92 | self.searchbtn.set_sensitive(False) 93 | self.searchbtn.set_label("Searching...") 94 | 95 | def get_selected(self): 96 | sel = self.treeview.get_selection().get_selected() 97 | if sel: 98 | return self.treeview.get_model().get_value(sel[1], 0) 99 | 100 | def cursor_changed(self, *ignore): 101 | self.result = self.get_selected() 102 | self.okbtn.set_sensitive(not not self.result) 103 | 104 | 105 | def NewSearchDialog(worker_run): 106 | """NewSearchDialog - returns a fully instantiated 107 | dialog-camel_case_nameDialog object. Use this function rather than 108 | creating SearchDialog instance directly. 109 | 110 | """ 111 | 112 | #look for the ui file that describes the ui 113 | ui_filename = os.path.join(getdatapath(), 'ui', 'SearchDialog.ui') 114 | if not os.path.exists(ui_filename): 115 | ui_filename = None 116 | 117 | builder = gtk.Builder() 118 | builder.add_from_file(ui_filename) 119 | dialog = builder.get_object("search_dialog") 120 | dialog.finish_initializing(builder, worker_run) 121 | return dialog 122 | 123 | if __name__ == "__main__": 124 | dialog = NewSearchDialog() 125 | dialog.show() 126 | gtk.main() 127 | 128 | -------------------------------------------------------------------------------- /pithos/StationsDialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*- 2 | ### BEGIN LICENSE 3 | # Copyright (C) 2010-2012 Kevin Mehall 4 | #This program is free software: you can redistribute it and/or modify it 5 | #under the terms of the GNU General Public License version 3, as published 6 | #by the Free Software Foundation. 7 | # 8 | #This program is distributed in the hope that it will be useful, but 9 | #WITHOUT ANY WARRANTY; without even the implied warranties of 10 | #MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 11 | #PURPOSE. See the GNU General Public License for more details. 12 | # 13 | #You should have received a copy of the GNU General Public License along 14 | #with this program. If not, see . 15 | ### END LICENSE 16 | 17 | import sys 18 | import os 19 | import gtk 20 | import logging 21 | import webbrowser 22 | 23 | from pithos.pithosconfig import getdatapath 24 | from pithos import SearchDialog 25 | 26 | class StationsDialog(gtk.Dialog): 27 | __gtype_name__ = "StationsDialog" 28 | 29 | def __init__(self): 30 | """__init__ - This function is typically not called directly. 31 | Creation of a StationsDialog requires redeading the associated ui 32 | file and parsing the ui definition extrenally, 33 | and then calling StationsDialog.finish_initializing(). 34 | 35 | Use the convenience function NewStationsDialog to create 36 | a StationsDialog object. 37 | 38 | """ 39 | pass 40 | 41 | def finish_initializing(self, builder, pithos): 42 | """finish_initalizing should be called after parsing the ui definition 43 | and creating a StationsDialog object with it in order to finish 44 | initializing the start of the new StationsDialog instance. 45 | 46 | """ 47 | #get a reference to the builder and set up the signals 48 | self.builder = builder 49 | self.builder.connect_signals(self) 50 | 51 | self.pithos = pithos 52 | self.model = pithos.stations_model 53 | self.worker_run = pithos.worker_run 54 | self.quickmix_changed = False 55 | self.searchDialog = None 56 | 57 | self.modelfilter = self.model.filter_new() 58 | self.modelfilter.set_visible_func(lambda m, i: m.get_value(i, 0) and not m.get_value(i, 0).isQuickMix) 59 | 60 | self.modelsortable = gtk.TreeModelSort(self.modelfilter) 61 | """ 62 | @todo Leaving it as sorting by date added by default. 63 | Probably should make a radio select in the window or an option in program options for user preference 64 | """ 65 | # self.modelsortable.set_sort_column_id(1, gtk.SORT_ASCENDING) 66 | 67 | self.treeview = self.builder.get_object("treeview") 68 | self.treeview.set_model(self.modelsortable) 69 | self.treeview.connect('button_press_event', self.on_treeview_button_press_event) 70 | 71 | name_col = gtk.TreeViewColumn() 72 | name_col.set_title("Name") 73 | render_text = gtk.CellRendererText() 74 | render_text.set_property('editable', True) 75 | render_text.connect("edited", self.station_renamed) 76 | name_col.pack_start(render_text, expand=True) 77 | name_col.add_attribute(render_text, "text", 1) 78 | name_col.set_expand(True) 79 | name_col.set_sort_column_id(1) 80 | self.treeview.append_column(name_col) 81 | 82 | qm_col = gtk.TreeViewColumn() 83 | qm_col.set_title("In QuickMix") 84 | render_toggle = gtk.CellRendererToggle() 85 | qm_col.pack_start(render_toggle, expand=True) 86 | def qm_datafunc(column, cell, model, iter): 87 | if model.get_value(iter,0).useQuickMix: 88 | cell.set_active(True) 89 | else: 90 | cell.set_active(False) 91 | qm_col.set_cell_data_func(render_toggle, qm_datafunc) 92 | render_toggle.connect("toggled", self.qm_toggled) 93 | self.treeview.append_column(qm_col) 94 | 95 | self.station_menu = builder.get_object("station_menu") 96 | 97 | def qm_toggled(self, renderer, path): 98 | station = self.modelfilter[path][0] 99 | station.useQuickMix = not station.useQuickMix 100 | self.quickmix_changed = True 101 | 102 | def station_renamed(self, cellrenderertext, path, new_text): 103 | station = self.modelfilter[path][0] 104 | self.worker_run(station.rename, (new_text,), context='net', message="Renaming Station...") 105 | self.model[self.modelfilter.convert_path_to_child_path(path)][1] = new_text 106 | 107 | def selected_station(self): 108 | sel = self.treeview.get_selection().get_selected() 109 | if sel: 110 | return self.treeview.get_model().get_value(sel[1], 0) 111 | 112 | def on_treeview_button_press_event(self, treeview, event): 113 | if event.button == 3: 114 | x = int(event.x) 115 | y = int(event.y) 116 | time = event.time 117 | pthinfo = treeview.get_path_at_pos(x, y) 118 | if pthinfo is not None: 119 | path, col, cellx, celly = pthinfo 120 | treeview.grab_focus() 121 | treeview.set_cursor( path, col, 0) 122 | self.station_menu.popup( None, None, None, event.button, time) 123 | return True 124 | 125 | def on_menuitem_listen(self, widget): 126 | station = self.selected_station() 127 | self.pithos.station_changed(station) 128 | self.hide() 129 | 130 | def on_menuitem_info(self, widget): 131 | webbrowser.open(self.selected_station().info_url) 132 | 133 | def on_menuitem_rename(self, widget): 134 | sel = self.treeview.get_selection().get_selected() 135 | path = self.treeview.get_model().get_path(sel[1]) 136 | self.treeview.set_cursor(path, self.treeview.get_column(0) ,True) 137 | 138 | def on_menuitem_delete(self, widget): 139 | station = self.selected_station() 140 | 141 | dialog = self.builder.get_object("delete_confirm_dialog") 142 | dialog.set_property("text", "Are you sure you want to delete the station \"%s\"?"%(station.name)) 143 | response = dialog.run() 144 | dialog.hide() 145 | 146 | if response: 147 | self.worker_run(station.delete, context='net', message="Deleting Station...") 148 | del self.pithos.stations_model[self.pithos.station_index(station)] 149 | if self.pithos.current_station is station: 150 | self.pithos.station_changed(self.model[0][0]) 151 | 152 | def add_station(self, widget): 153 | if self.searchDialog: 154 | self.searchDialog.present() 155 | else: 156 | self.searchDialog = SearchDialog.NewSearchDialog(self.worker_run) 157 | self.searchDialog.show_all() 158 | self.searchDialog.connect("response", self.add_station_cb) 159 | 160 | def refresh_stations(self, widget): 161 | self.pithos.refresh_stations(self.pithos) 162 | 163 | def add_station_cb(self, dialog, response): 164 | print "in add_station_cb", dialog.result, response 165 | if response == 1: 166 | self.worker_run("add_station_by_music_id", (dialog.result.musicId,), self.station_added, "Creating station...") 167 | dialog.hide() 168 | dialog.destroy() 169 | self.searchDialog = None 170 | 171 | def station_added(self, station): 172 | logging.debug("1 "+ repr(station)) 173 | it = self.model.insert_after(self.model.get_iter(1), (station, station.name)) 174 | logging.debug("2 "+ repr(it)) 175 | self.pithos.station_changed(station) 176 | logging.debug("3 ") 177 | self.modelfilter.refilter() 178 | logging.debug("4") 179 | self.treeview.set_cursor(0) 180 | logging.debug("5 ") 181 | 182 | def add_genre_station(self, widget): 183 | """ 184 | This is just a stub for the non-completed buttn 185 | """ 186 | 187 | def on_close(self, widget, data=None): 188 | self.hide() 189 | 190 | if self.quickmix_changed: 191 | self.worker_run("save_quick_mix", message="Saving QuickMix...") 192 | self.quickmix_changed = False 193 | 194 | logging.info("closed dialog") 195 | return True 196 | 197 | def NewStationsDialog(pithos): 198 | """NewStationsDialog - returns a fully instantiated 199 | Dialog object. Use this function rather than 200 | creating StationsDialog instance directly. 201 | 202 | """ 203 | 204 | #look for the ui file that describes the ui 205 | ui_filename = os.path.join(getdatapath(), 'ui', 'StationsDialog.ui') 206 | if not os.path.exists(ui_filename): 207 | ui_filename = None 208 | 209 | builder = gtk.Builder() 210 | builder.add_from_file(ui_filename) 211 | dialog = builder.get_object("stations_dialog") 212 | dialog.finish_initializing(builder, pithos) 213 | return dialog 214 | 215 | if __name__ == "__main__": 216 | dialog = NewStationsDialog() 217 | dialog.show() 218 | gtk.main() 219 | 220 | -------------------------------------------------------------------------------- /pithos/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TingPing/pithos-for-windows/02a93e76ff6b616e14f10dfc04d1532bd7d98940/pithos/__init__.py -------------------------------------------------------------------------------- /pithos/gobject_worker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*- 2 | ### BEGIN LICENSE 3 | # Copyright (C) 2010-2012 Kevin Mehall 4 | #This program is free software: you can redistribute it and/or modify it 5 | #under the terms of the GNU General Public License version 3, as published 6 | #by the Free Software Foundation. 7 | # 8 | #This program is distributed in the hope that it will be useful, but 9 | #WITHOUT ANY WARRANTY; without even the implied warranties of 10 | #MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 11 | #PURPOSE. See the GNU General Public License for more details. 12 | # 13 | #You should have received a copy of the GNU General Public License along 14 | #with this program. If not, see . 15 | ### END LICENSE 16 | 17 | import threading 18 | import Queue 19 | import gobject 20 | import traceback 21 | gobject.threads_init() 22 | 23 | class GObjectWorker(): 24 | def __init__(self): 25 | self.thread = threading.Thread(target=self._run) 26 | self.thread.daemon = True 27 | self.queue = Queue.Queue() 28 | self.thread.start() 29 | 30 | def _run(self): 31 | while True: 32 | command, args, callback, errorback = self.queue.get() 33 | try: 34 | result = command(*args) 35 | if callback: 36 | gobject.idle_add(callback, result) 37 | except Exception, e: 38 | e.traceback = traceback.format_exc() 39 | if errorback: 40 | gobject.idle_add(errorback, e) 41 | 42 | def send(self, command, args=(), callback=None, errorback=None): 43 | if errorback is None: errorback = self._default_errorback 44 | self.queue.put((command, args, callback, errorback)) 45 | 46 | def _default_errorback(self, error): 47 | print "Unhandled exception in worker thread:\n", error.traceback 48 | 49 | if __name__ == '__main__': 50 | worker = GObjectWorker() 51 | import time, gtk 52 | 53 | def test_cmd(a, b): 54 | print "running..." 55 | time.sleep(5) 56 | print "done" 57 | return a*b 58 | 59 | def test_cb(result): 60 | print "got result", result 61 | 62 | print "sending" 63 | worker.send(test_cmd, (3,4), test_cb) 64 | worker.send(test_cmd, ((), ()), test_cb) #trigger exception in worker to test error handling 65 | 66 | gtk.main() 67 | 68 | 69 | -------------------------------------------------------------------------------- /pithos/pandora/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*- 2 | ### BEGIN LICENSE 3 | # Copyright (C) 2010 Kevin Mehall 4 | #This program is free software: you can redistribute it and/or modify it 5 | #under the terms of the GNU General Public License version 3, as published 6 | #by the Free Software Foundation. 7 | # 8 | #This program is distributed in the hope that it will be useful, but 9 | #WITHOUT ANY WARRANTY; without even the implied warranties of 10 | #MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 11 | #PURPOSE. See the GNU General Public License for more details. 12 | # 13 | #You should have received a copy of the GNU General Public License along 14 | #with this program. If not, see . 15 | ### END LICENSE 16 | 17 | from pithos.pandora.pandora import * 18 | 19 | def make_pandora(testing=False): 20 | if testing: 21 | from pithos.pandora.fake import FakePandora 22 | return FakePandora() 23 | else: 24 | return Pandora() 25 | -------------------------------------------------------------------------------- /pithos/pandora/blowfish.py: -------------------------------------------------------------------------------- 1 | # 2 | # blowfish.py 3 | # Copyright (C) 2002 Michael Gilfix 4 | # 5 | # This module is open source; you can redistribute it and/or 6 | # modify it under the terms of the GPL or Artistic License. 7 | # These licenses are available at http://www.opensource.org 8 | # 9 | # This software must be used and distributed in accordance 10 | # with the law. The author claims no liability for its 11 | # misuse. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 16 | # 17 | 18 | """ 19 | Blowfish Encryption 20 | 21 | This module is a pure python implementation of Bruce Schneier's 22 | encryption scheme 'Blowfish'. Blowish is a 16-round Feistel Network 23 | cipher and offers substantial speed gains over DES. 24 | 25 | The key is a string of length anywhere between 64 and 448 bits, or 26 | equivalently 8 and 56 bytes. The encryption and decryption functions operate 27 | on 64-bit blocks, or 8 byte strings. 28 | 29 | Send questions, comments, bugs my way: 30 | Michael Gilfix 31 | """ 32 | 33 | __author__ = "Michael Gilfix " 34 | 35 | class Blowfish: 36 | 37 | """Blowfish encryption Scheme 38 | 39 | This class implements the encryption and decryption 40 | functionality of the Blowfish cipher. 41 | 42 | Public functions: 43 | 44 | def __init__ (self, key) 45 | Creates an instance of blowfish using 'key' 46 | as the encryption key. Key is a string of 47 | length ranging from 8 to 56 bytes (64 to 448 48 | bits). Once the instance of the object is 49 | created, the key is no longer necessary. 50 | 51 | def encrypt (self, data): 52 | Encrypt an 8 byte (64-bit) block of text 53 | where 'data' is an 8 byte string. Returns an 54 | 8-byte encrypted string. 55 | 56 | def decrypt (self, data): 57 | Decrypt an 8 byte (64-bit) encrypted block 58 | of text, where 'data' is the 8 byte encrypted 59 | string. Returns an 8-byte string of plaintext. 60 | 61 | def cipher (self, xl, xr, direction): 62 | Encrypts a 64-bit block of data where xl is 63 | the upper 32-bits and xr is the lower 32-bits. 64 | 'direction' is the direction to apply the 65 | cipher, either ENCRYPT or DECRYPT constants. 66 | returns a tuple of either encrypted or decrypted 67 | data of the left half and right half of the 68 | 64-bit block. 69 | 70 | Private members: 71 | 72 | def __round_func (self, xl) 73 | Performs an obscuring function on the 32-bit 74 | block of data 'xl', which is the left half of 75 | the 64-bit block of data. Returns the 32-bit 76 | result as a long integer. 77 | 78 | """ 79 | 80 | # Cipher directions 81 | ENCRYPT = 0 82 | DECRYPT = 1 83 | 84 | # For the __round_func 85 | modulus = long (2) ** 32 86 | 87 | def __init__ (self, key): 88 | 89 | if not key or len (key) < 8 or len (key) > 56: 90 | raise RuntimeError, "Attempted to initialize Blowfish cipher with key of invalid length: %s" %len (key) 91 | 92 | self.p_boxes = [ 93 | 0x243F6A88, 0x85A308D3, 0x13198A2E, 0x03707344, 94 | 0xA4093822, 0x299F31D0, 0x082EFA98, 0xEC4E6C89, 95 | 0x452821E6, 0x38D01377, 0xBE5466CF, 0x34E90C6C, 96 | 0xC0AC29B7, 0xC97C50DD, 0x3F84D5B5, 0xB5470917, 97 | 0x9216D5D9, 0x8979FB1B 98 | ] 99 | 100 | self.s_boxes = [ 101 | [ 102 | 0xD1310BA6, 0x98DFB5AC, 0x2FFD72DB, 0xD01ADFB7, 103 | 0xB8E1AFED, 0x6A267E96, 0xBA7C9045, 0xF12C7F99, 104 | 0x24A19947, 0xB3916CF7, 0x0801F2E2, 0x858EFC16, 105 | 0x636920D8, 0x71574E69, 0xA458FEA3, 0xF4933D7E, 106 | 0x0D95748F, 0x728EB658, 0x718BCD58, 0x82154AEE, 107 | 0x7B54A41D, 0xC25A59B5, 0x9C30D539, 0x2AF26013, 108 | 0xC5D1B023, 0x286085F0, 0xCA417918, 0xB8DB38EF, 109 | 0x8E79DCB0, 0x603A180E, 0x6C9E0E8B, 0xB01E8A3E, 110 | 0xD71577C1, 0xBD314B27, 0x78AF2FDA, 0x55605C60, 111 | 0xE65525F3, 0xAA55AB94, 0x57489862, 0x63E81440, 112 | 0x55CA396A, 0x2AAB10B6, 0xB4CC5C34, 0x1141E8CE, 113 | 0xA15486AF, 0x7C72E993, 0xB3EE1411, 0x636FBC2A, 114 | 0x2BA9C55D, 0x741831F6, 0xCE5C3E16, 0x9B87931E, 115 | 0xAFD6BA33, 0x6C24CF5C, 0x7A325381, 0x28958677, 116 | 0x3B8F4898, 0x6B4BB9AF, 0xC4BFE81B, 0x66282193, 117 | 0x61D809CC, 0xFB21A991, 0x487CAC60, 0x5DEC8032, 118 | 0xEF845D5D, 0xE98575B1, 0xDC262302, 0xEB651B88, 119 | 0x23893E81, 0xD396ACC5, 0x0F6D6FF3, 0x83F44239, 120 | 0x2E0B4482, 0xA4842004, 0x69C8F04A, 0x9E1F9B5E, 121 | 0x21C66842, 0xF6E96C9A, 0x670C9C61, 0xABD388F0, 122 | 0x6A51A0D2, 0xD8542F68, 0x960FA728, 0xAB5133A3, 123 | 0x6EEF0B6C, 0x137A3BE4, 0xBA3BF050, 0x7EFB2A98, 124 | 0xA1F1651D, 0x39AF0176, 0x66CA593E, 0x82430E88, 125 | 0x8CEE8619, 0x456F9FB4, 0x7D84A5C3, 0x3B8B5EBE, 126 | 0xE06F75D8, 0x85C12073, 0x401A449F, 0x56C16AA6, 127 | 0x4ED3AA62, 0x363F7706, 0x1BFEDF72, 0x429B023D, 128 | 0x37D0D724, 0xD00A1248, 0xDB0FEAD3, 0x49F1C09B, 129 | 0x075372C9, 0x80991B7B, 0x25D479D8, 0xF6E8DEF7, 130 | 0xE3FE501A, 0xB6794C3B, 0x976CE0BD, 0x04C006BA, 131 | 0xC1A94FB6, 0x409F60C4, 0x5E5C9EC2, 0x196A2463, 132 | 0x68FB6FAF, 0x3E6C53B5, 0x1339B2EB, 0x3B52EC6F, 133 | 0x6DFC511F, 0x9B30952C, 0xCC814544, 0xAF5EBD09, 134 | 0xBEE3D004, 0xDE334AFD, 0x660F2807, 0x192E4BB3, 135 | 0xC0CBA857, 0x45C8740F, 0xD20B5F39, 0xB9D3FBDB, 136 | 0x5579C0BD, 0x1A60320A, 0xD6A100C6, 0x402C7279, 137 | 0x679F25FE, 0xFB1FA3CC, 0x8EA5E9F8, 0xDB3222F8, 138 | 0x3C7516DF, 0xFD616B15, 0x2F501EC8, 0xAD0552AB, 139 | 0x323DB5FA, 0xFD238760, 0x53317B48, 0x3E00DF82, 140 | 0x9E5C57BB, 0xCA6F8CA0, 0x1A87562E, 0xDF1769DB, 141 | 0xD542A8F6, 0x287EFFC3, 0xAC6732C6, 0x8C4F5573, 142 | 0x695B27B0, 0xBBCA58C8, 0xE1FFA35D, 0xB8F011A0, 143 | 0x10FA3D98, 0xFD2183B8, 0x4AFCB56C, 0x2DD1D35B, 144 | 0x9A53E479, 0xB6F84565, 0xD28E49BC, 0x4BFB9790, 145 | 0xE1DDF2DA, 0xA4CB7E33, 0x62FB1341, 0xCEE4C6E8, 146 | 0xEF20CADA, 0x36774C01, 0xD07E9EFE, 0x2BF11FB4, 147 | 0x95DBDA4D, 0xAE909198, 0xEAAD8E71, 0x6B93D5A0, 148 | 0xD08ED1D0, 0xAFC725E0, 0x8E3C5B2F, 0x8E7594B7, 149 | 0x8FF6E2FB, 0xF2122B64, 0x8888B812, 0x900DF01C, 150 | 0x4FAD5EA0, 0x688FC31C, 0xD1CFF191, 0xB3A8C1AD, 151 | 0x2F2F2218, 0xBE0E1777, 0xEA752DFE, 0x8B021FA1, 152 | 0xE5A0CC0F, 0xB56F74E8, 0x18ACF3D6, 0xCE89E299, 153 | 0xB4A84FE0, 0xFD13E0B7, 0x7CC43B81, 0xD2ADA8D9, 154 | 0x165FA266, 0x80957705, 0x93CC7314, 0x211A1477, 155 | 0xE6AD2065, 0x77B5FA86, 0xC75442F5, 0xFB9D35CF, 156 | 0xEBCDAF0C, 0x7B3E89A0, 0xD6411BD3, 0xAE1E7E49, 157 | 0x00250E2D, 0x2071B35E, 0x226800BB, 0x57B8E0AF, 158 | 0x2464369B, 0xF009B91E, 0x5563911D, 0x59DFA6AA, 159 | 0x78C14389, 0xD95A537F, 0x207D5BA2, 0x02E5B9C5, 160 | 0x83260376, 0x6295CFA9, 0x11C81968, 0x4E734A41, 161 | 0xB3472DCA, 0x7B14A94A, 0x1B510052, 0x9A532915, 162 | 0xD60F573F, 0xBC9BC6E4, 0x2B60A476, 0x81E67400, 163 | 0x08BA6FB5, 0x571BE91F, 0xF296EC6B, 0x2A0DD915, 164 | 0xB6636521, 0xE7B9F9B6, 0xFF34052E, 0xC5855664, 165 | 0x53B02D5D, 0xA99F8FA1, 0x08BA4799, 0x6E85076A 166 | ], 167 | [ 168 | 0x4B7A70E9, 0xB5B32944, 0xDB75092E, 0xC4192623, 169 | 0xAD6EA6B0, 0x49A7DF7D, 0x9CEE60B8, 0x8FEDB266, 170 | 0xECAA8C71, 0x699A17FF, 0x5664526C, 0xC2B19EE1, 171 | 0x193602A5, 0x75094C29, 0xA0591340, 0xE4183A3E, 172 | 0x3F54989A, 0x5B429D65, 0x6B8FE4D6, 0x99F73FD6, 173 | 0xA1D29C07, 0xEFE830F5, 0x4D2D38E6, 0xF0255DC1, 174 | 0x4CDD2086, 0x8470EB26, 0x6382E9C6, 0x021ECC5E, 175 | 0x09686B3F, 0x3EBAEFC9, 0x3C971814, 0x6B6A70A1, 176 | 0x687F3584, 0x52A0E286, 0xB79C5305, 0xAA500737, 177 | 0x3E07841C, 0x7FDEAE5C, 0x8E7D44EC, 0x5716F2B8, 178 | 0xB03ADA37, 0xF0500C0D, 0xF01C1F04, 0x0200B3FF, 179 | 0xAE0CF51A, 0x3CB574B2, 0x25837A58, 0xDC0921BD, 180 | 0xD19113F9, 0x7CA92FF6, 0x94324773, 0x22F54701, 181 | 0x3AE5E581, 0x37C2DADC, 0xC8B57634, 0x9AF3DDA7, 182 | 0xA9446146, 0x0FD0030E, 0xECC8C73E, 0xA4751E41, 183 | 0xE238CD99, 0x3BEA0E2F, 0x3280BBA1, 0x183EB331, 184 | 0x4E548B38, 0x4F6DB908, 0x6F420D03, 0xF60A04BF, 185 | 0x2CB81290, 0x24977C79, 0x5679B072, 0xBCAF89AF, 186 | 0xDE9A771F, 0xD9930810, 0xB38BAE12, 0xDCCF3F2E, 187 | 0x5512721F, 0x2E6B7124, 0x501ADDE6, 0x9F84CD87, 188 | 0x7A584718, 0x7408DA17, 0xBC9F9ABC, 0xE94B7D8C, 189 | 0xEC7AEC3A, 0xDB851DFA, 0x63094366, 0xC464C3D2, 190 | 0xEF1C1847, 0x3215D908, 0xDD433B37, 0x24C2BA16, 191 | 0x12A14D43, 0x2A65C451, 0x50940002, 0x133AE4DD, 192 | 0x71DFF89E, 0x10314E55, 0x81AC77D6, 0x5F11199B, 193 | 0x043556F1, 0xD7A3C76B, 0x3C11183B, 0x5924A509, 194 | 0xF28FE6ED, 0x97F1FBFA, 0x9EBABF2C, 0x1E153C6E, 195 | 0x86E34570, 0xEAE96FB1, 0x860E5E0A, 0x5A3E2AB3, 196 | 0x771FE71C, 0x4E3D06FA, 0x2965DCB9, 0x99E71D0F, 197 | 0x803E89D6, 0x5266C825, 0x2E4CC978, 0x9C10B36A, 198 | 0xC6150EBA, 0x94E2EA78, 0xA5FC3C53, 0x1E0A2DF4, 199 | 0xF2F74EA7, 0x361D2B3D, 0x1939260F, 0x19C27960, 200 | 0x5223A708, 0xF71312B6, 0xEBADFE6E, 0xEAC31F66, 201 | 0xE3BC4595, 0xA67BC883, 0xB17F37D1, 0x018CFF28, 202 | 0xC332DDEF, 0xBE6C5AA5, 0x65582185, 0x68AB9802, 203 | 0xEECEA50F, 0xDB2F953B, 0x2AEF7DAD, 0x5B6E2F84, 204 | 0x1521B628, 0x29076170, 0xECDD4775, 0x619F1510, 205 | 0x13CCA830, 0xEB61BD96, 0x0334FE1E, 0xAA0363CF, 206 | 0xB5735C90, 0x4C70A239, 0xD59E9E0B, 0xCBAADE14, 207 | 0xEECC86BC, 0x60622CA7, 0x9CAB5CAB, 0xB2F3846E, 208 | 0x648B1EAF, 0x19BDF0CA, 0xA02369B9, 0x655ABB50, 209 | 0x40685A32, 0x3C2AB4B3, 0x319EE9D5, 0xC021B8F7, 210 | 0x9B540B19, 0x875FA099, 0x95F7997E, 0x623D7DA8, 211 | 0xF837889A, 0x97E32D77, 0x11ED935F, 0x16681281, 212 | 0x0E358829, 0xC7E61FD6, 0x96DEDFA1, 0x7858BA99, 213 | 0x57F584A5, 0x1B227263, 0x9B83C3FF, 0x1AC24696, 214 | 0xCDB30AEB, 0x532E3054, 0x8FD948E4, 0x6DBC3128, 215 | 0x58EBF2EF, 0x34C6FFEA, 0xFE28ED61, 0xEE7C3C73, 216 | 0x5D4A14D9, 0xE864B7E3, 0x42105D14, 0x203E13E0, 217 | 0x45EEE2B6, 0xA3AAABEA, 0xDB6C4F15, 0xFACB4FD0, 218 | 0xC742F442, 0xEF6ABBB5, 0x654F3B1D, 0x41CD2105, 219 | 0xD81E799E, 0x86854DC7, 0xE44B476A, 0x3D816250, 220 | 0xCF62A1F2, 0x5B8D2646, 0xFC8883A0, 0xC1C7B6A3, 221 | 0x7F1524C3, 0x69CB7492, 0x47848A0B, 0x5692B285, 222 | 0x095BBF00, 0xAD19489D, 0x1462B174, 0x23820E00, 223 | 0x58428D2A, 0x0C55F5EA, 0x1DADF43E, 0x233F7061, 224 | 0x3372F092, 0x8D937E41, 0xD65FECF1, 0x6C223BDB, 225 | 0x7CDE3759, 0xCBEE7460, 0x4085F2A7, 0xCE77326E, 226 | 0xA6078084, 0x19F8509E, 0xE8EFD855, 0x61D99735, 227 | 0xA969A7AA, 0xC50C06C2, 0x5A04ABFC, 0x800BCADC, 228 | 0x9E447A2E, 0xC3453484, 0xFDD56705, 0x0E1E9EC9, 229 | 0xDB73DBD3, 0x105588CD, 0x675FDA79, 0xE3674340, 230 | 0xC5C43465, 0x713E38D8, 0x3D28F89E, 0xF16DFF20, 231 | 0x153E21E7, 0x8FB03D4A, 0xE6E39F2B, 0xDB83ADF7 232 | ], 233 | [ 234 | 0xE93D5A68, 0x948140F7, 0xF64C261C, 0x94692934, 235 | 0x411520F7, 0x7602D4F7, 0xBCF46B2E, 0xD4A20068, 236 | 0xD4082471, 0x3320F46A, 0x43B7D4B7, 0x500061AF, 237 | 0x1E39F62E, 0x97244546, 0x14214F74, 0xBF8B8840, 238 | 0x4D95FC1D, 0x96B591AF, 0x70F4DDD3, 0x66A02F45, 239 | 0xBFBC09EC, 0x03BD9785, 0x7FAC6DD0, 0x31CB8504, 240 | 0x96EB27B3, 0x55FD3941, 0xDA2547E6, 0xABCA0A9A, 241 | 0x28507825, 0x530429F4, 0x0A2C86DA, 0xE9B66DFB, 242 | 0x68DC1462, 0xD7486900, 0x680EC0A4, 0x27A18DEE, 243 | 0x4F3FFEA2, 0xE887AD8C, 0xB58CE006, 0x7AF4D6B6, 244 | 0xAACE1E7C, 0xD3375FEC, 0xCE78A399, 0x406B2A42, 245 | 0x20FE9E35, 0xD9F385B9, 0xEE39D7AB, 0x3B124E8B, 246 | 0x1DC9FAF7, 0x4B6D1856, 0x26A36631, 0xEAE397B2, 247 | 0x3A6EFA74, 0xDD5B4332, 0x6841E7F7, 0xCA7820FB, 248 | 0xFB0AF54E, 0xD8FEB397, 0x454056AC, 0xBA489527, 249 | 0x55533A3A, 0x20838D87, 0xFE6BA9B7, 0xD096954B, 250 | 0x55A867BC, 0xA1159A58, 0xCCA92963, 0x99E1DB33, 251 | 0xA62A4A56, 0x3F3125F9, 0x5EF47E1C, 0x9029317C, 252 | 0xFDF8E802, 0x04272F70, 0x80BB155C, 0x05282CE3, 253 | 0x95C11548, 0xE4C66D22, 0x48C1133F, 0xC70F86DC, 254 | 0x07F9C9EE, 0x41041F0F, 0x404779A4, 0x5D886E17, 255 | 0x325F51EB, 0xD59BC0D1, 0xF2BCC18F, 0x41113564, 256 | 0x257B7834, 0x602A9C60, 0xDFF8E8A3, 0x1F636C1B, 257 | 0x0E12B4C2, 0x02E1329E, 0xAF664FD1, 0xCAD18115, 258 | 0x6B2395E0, 0x333E92E1, 0x3B240B62, 0xEEBEB922, 259 | 0x85B2A20E, 0xE6BA0D99, 0xDE720C8C, 0x2DA2F728, 260 | 0xD0127845, 0x95B794FD, 0x647D0862, 0xE7CCF5F0, 261 | 0x5449A36F, 0x877D48FA, 0xC39DFD27, 0xF33E8D1E, 262 | 0x0A476341, 0x992EFF74, 0x3A6F6EAB, 0xF4F8FD37, 263 | 0xA812DC60, 0xA1EBDDF8, 0x991BE14C, 0xDB6E6B0D, 264 | 0xC67B5510, 0x6D672C37, 0x2765D43B, 0xDCD0E804, 265 | 0xF1290DC7, 0xCC00FFA3, 0xB5390F92, 0x690FED0B, 266 | 0x667B9FFB, 0xCEDB7D9C, 0xA091CF0B, 0xD9155EA3, 267 | 0xBB132F88, 0x515BAD24, 0x7B9479BF, 0x763BD6EB, 268 | 0x37392EB3, 0xCC115979, 0x8026E297, 0xF42E312D, 269 | 0x6842ADA7, 0xC66A2B3B, 0x12754CCC, 0x782EF11C, 270 | 0x6A124237, 0xB79251E7, 0x06A1BBE6, 0x4BFB6350, 271 | 0x1A6B1018, 0x11CAEDFA, 0x3D25BDD8, 0xE2E1C3C9, 272 | 0x44421659, 0x0A121386, 0xD90CEC6E, 0xD5ABEA2A, 273 | 0x64AF674E, 0xDA86A85F, 0xBEBFE988, 0x64E4C3FE, 274 | 0x9DBC8057, 0xF0F7C086, 0x60787BF8, 0x6003604D, 275 | 0xD1FD8346, 0xF6381FB0, 0x7745AE04, 0xD736FCCC, 276 | 0x83426B33, 0xF01EAB71, 0xB0804187, 0x3C005E5F, 277 | 0x77A057BE, 0xBDE8AE24, 0x55464299, 0xBF582E61, 278 | 0x4E58F48F, 0xF2DDFDA2, 0xF474EF38, 0x8789BDC2, 279 | 0x5366F9C3, 0xC8B38E74, 0xB475F255, 0x46FCD9B9, 280 | 0x7AEB2661, 0x8B1DDF84, 0x846A0E79, 0x915F95E2, 281 | 0x466E598E, 0x20B45770, 0x8CD55591, 0xC902DE4C, 282 | 0xB90BACE1, 0xBB8205D0, 0x11A86248, 0x7574A99E, 283 | 0xB77F19B6, 0xE0A9DC09, 0x662D09A1, 0xC4324633, 284 | 0xE85A1F02, 0x09F0BE8C, 0x4A99A025, 0x1D6EFE10, 285 | 0x1AB93D1D, 0x0BA5A4DF, 0xA186F20F, 0x2868F169, 286 | 0xDCB7DA83, 0x573906FE, 0xA1E2CE9B, 0x4FCD7F52, 287 | 0x50115E01, 0xA70683FA, 0xA002B5C4, 0x0DE6D027, 288 | 0x9AF88C27, 0x773F8641, 0xC3604C06, 0x61A806B5, 289 | 0xF0177A28, 0xC0F586E0, 0x006058AA, 0x30DC7D62, 290 | 0x11E69ED7, 0x2338EA63, 0x53C2DD94, 0xC2C21634, 291 | 0xBBCBEE56, 0x90BCB6DE, 0xEBFC7DA1, 0xCE591D76, 292 | 0x6F05E409, 0x4B7C0188, 0x39720A3D, 0x7C927C24, 293 | 0x86E3725F, 0x724D9DB9, 0x1AC15BB4, 0xD39EB8FC, 294 | 0xED545578, 0x08FCA5B5, 0xD83D7CD3, 0x4DAD0FC4, 295 | 0x1E50EF5E, 0xB161E6F8, 0xA28514D9, 0x6C51133C, 296 | 0x6FD5C7E7, 0x56E14EC4, 0x362ABFCE, 0xDDC6C837, 297 | 0xD79A3234, 0x92638212, 0x670EFA8E, 0x406000E0 298 | ], 299 | [ 300 | 0x3A39CE37, 0xD3FAF5CF, 0xABC27737, 0x5AC52D1B, 301 | 0x5CB0679E, 0x4FA33742, 0xD3822740, 0x99BC9BBE, 302 | 0xD5118E9D, 0xBF0F7315, 0xD62D1C7E, 0xC700C47B, 303 | 0xB78C1B6B, 0x21A19045, 0xB26EB1BE, 0x6A366EB4, 304 | 0x5748AB2F, 0xBC946E79, 0xC6A376D2, 0x6549C2C8, 305 | 0x530FF8EE, 0x468DDE7D, 0xD5730A1D, 0x4CD04DC6, 306 | 0x2939BBDB, 0xA9BA4650, 0xAC9526E8, 0xBE5EE304, 307 | 0xA1FAD5F0, 0x6A2D519A, 0x63EF8CE2, 0x9A86EE22, 308 | 0xC089C2B8, 0x43242EF6, 0xA51E03AA, 0x9CF2D0A4, 309 | 0x83C061BA, 0x9BE96A4D, 0x8FE51550, 0xBA645BD6, 310 | 0x2826A2F9, 0xA73A3AE1, 0x4BA99586, 0xEF5562E9, 311 | 0xC72FEFD3, 0xF752F7DA, 0x3F046F69, 0x77FA0A59, 312 | 0x80E4A915, 0x87B08601, 0x9B09E6AD, 0x3B3EE593, 313 | 0xE990FD5A, 0x9E34D797, 0x2CF0B7D9, 0x022B8B51, 314 | 0x96D5AC3A, 0x017DA67D, 0xD1CF3ED6, 0x7C7D2D28, 315 | 0x1F9F25CF, 0xADF2B89B, 0x5AD6B472, 0x5A88F54C, 316 | 0xE029AC71, 0xE019A5E6, 0x47B0ACFD, 0xED93FA9B, 317 | 0xE8D3C48D, 0x283B57CC, 0xF8D56629, 0x79132E28, 318 | 0x785F0191, 0xED756055, 0xF7960E44, 0xE3D35E8C, 319 | 0x15056DD4, 0x88F46DBA, 0x03A16125, 0x0564F0BD, 320 | 0xC3EB9E15, 0x3C9057A2, 0x97271AEC, 0xA93A072A, 321 | 0x1B3F6D9B, 0x1E6321F5, 0xF59C66FB, 0x26DCF319, 322 | 0x7533D928, 0xB155FDF5, 0x03563482, 0x8ABA3CBB, 323 | 0x28517711, 0xC20AD9F8, 0xABCC5167, 0xCCAD925F, 324 | 0x4DE81751, 0x3830DC8E, 0x379D5862, 0x9320F991, 325 | 0xEA7A90C2, 0xFB3E7BCE, 0x5121CE64, 0x774FBE32, 326 | 0xA8B6E37E, 0xC3293D46, 0x48DE5369, 0x6413E680, 327 | 0xA2AE0810, 0xDD6DB224, 0x69852DFD, 0x09072166, 328 | 0xB39A460A, 0x6445C0DD, 0x586CDECF, 0x1C20C8AE, 329 | 0x5BBEF7DD, 0x1B588D40, 0xCCD2017F, 0x6BB4E3BB, 330 | 0xDDA26A7E, 0x3A59FF45, 0x3E350A44, 0xBCB4CDD5, 331 | 0x72EACEA8, 0xFA6484BB, 0x8D6612AE, 0xBF3C6F47, 332 | 0xD29BE463, 0x542F5D9E, 0xAEC2771B, 0xF64E6370, 333 | 0x740E0D8D, 0xE75B1357, 0xF8721671, 0xAF537D5D, 334 | 0x4040CB08, 0x4EB4E2CC, 0x34D2466A, 0x0115AF84, 335 | 0xE1B00428, 0x95983A1D, 0x06B89FB4, 0xCE6EA048, 336 | 0x6F3F3B82, 0x3520AB82, 0x011A1D4B, 0x277227F8, 337 | 0x611560B1, 0xE7933FDC, 0xBB3A792B, 0x344525BD, 338 | 0xA08839E1, 0x51CE794B, 0x2F32C9B7, 0xA01FBAC9, 339 | 0xE01CC87E, 0xBCC7D1F6, 0xCF0111C3, 0xA1E8AAC7, 340 | 0x1A908749, 0xD44FBD9A, 0xD0DADECB, 0xD50ADA38, 341 | 0x0339C32A, 0xC6913667, 0x8DF9317C, 0xE0B12B4F, 342 | 0xF79E59B7, 0x43F5BB3A, 0xF2D519FF, 0x27D9459C, 343 | 0xBF97222C, 0x15E6FC2A, 0x0F91FC71, 0x9B941525, 344 | 0xFAE59361, 0xCEB69CEB, 0xC2A86459, 0x12BAA8D1, 345 | 0xB6C1075E, 0xE3056A0C, 0x10D25065, 0xCB03A442, 346 | 0xE0EC6E0E, 0x1698DB3B, 0x4C98A0BE, 0x3278E964, 347 | 0x9F1F9532, 0xE0D392DF, 0xD3A0342B, 0x8971F21E, 348 | 0x1B0A7441, 0x4BA3348C, 0xC5BE7120, 0xC37632D8, 349 | 0xDF359F8D, 0x9B992F2E, 0xE60B6F47, 0x0FE3F11D, 350 | 0xE54CDA54, 0x1EDAD891, 0xCE6279CF, 0xCD3E7E6F, 351 | 0x1618B166, 0xFD2C1D05, 0x848FD2C5, 0xF6FB2299, 352 | 0xF523F357, 0xA6327623, 0x93A83531, 0x56CCCD02, 353 | 0xACF08162, 0x5A75EBB5, 0x6E163697, 0x88D273CC, 354 | 0xDE966292, 0x81B949D0, 0x4C50901B, 0x71C65614, 355 | 0xE6C6C7BD, 0x327A140A, 0x45E1D006, 0xC3F27B9A, 356 | 0xC9AA53FD, 0x62A80F00, 0xBB25BFE2, 0x35BDD2F6, 357 | 0x71126905, 0xB2040222, 0xB6CBCF7C, 0xCD769C2B, 358 | 0x53113EC0, 0x1640E3D3, 0x38ABBD60, 0x2547ADF0, 359 | 0xBA38209C, 0xF746CE76, 0x77AFA1C5, 0x20756060, 360 | 0x85CBFE4E, 0x8AE88DD8, 0x7AAAF9B0, 0x4CF9AA7E, 361 | 0x1948C25C, 0x02FB8A8C, 0x01C36AE4, 0xD6EBE1F9, 362 | 0x90D4F869, 0xA65CDEA0, 0x3F09252D, 0xC208E69F, 363 | 0xB74E6132, 0xCE77E25B, 0x578FDFE3, 0x3AC372E6 364 | ] 365 | ] 366 | 367 | # Cycle through the p-boxes and round-robin XOR the 368 | # key with the p-boxes 369 | key_len = len (key) 370 | index = 0 371 | for i in range (len (self.p_boxes)): 372 | val = (ord (key[index % key_len]) << 24) + \ 373 | (ord (key[(index + 1) % key_len]) << 16) + \ 374 | (ord (key[(index + 2) % key_len]) << 8) + \ 375 | ord (key[(index + 3) % key_len]) 376 | self.p_boxes[i] = self.p_boxes[i] ^ val 377 | index = index + 4 378 | 379 | # For the chaining process 380 | l, r = 0, 0 381 | 382 | # Begin chain replacing the p-boxes 383 | for i in range (0, len (self.p_boxes), 2): 384 | l, r = self.cipher (l, r, self.ENCRYPT) 385 | self.p_boxes[i] = l 386 | self.p_boxes[i + 1] = r 387 | 388 | # Chain replace the s-boxes 389 | for i in range (len (self.s_boxes)): 390 | for j in range (0, len (self.s_boxes[i]), 2): 391 | l, r = self.cipher (l, r, self.ENCRYPT) 392 | self.s_boxes[i][j] = l 393 | self.s_boxes[i][j + 1] = r 394 | 395 | def cipher (self, xl, xr, direction): 396 | 397 | if direction == self.ENCRYPT: 398 | for i in range (16): 399 | xl = xl ^ self.p_boxes[i] 400 | xr = self.__round_func (xl) ^ xr 401 | xl, xr = xr, xl 402 | xl, xr = xr, xl 403 | xr = xr ^ self.p_boxes[16] 404 | xl = xl ^ self.p_boxes[17] 405 | else: 406 | for i in range (17, 1, -1): 407 | xl = xl ^ self.p_boxes[i] 408 | xr = self.__round_func (xl) ^ xr 409 | xl, xr = xr, xl 410 | xl, xr = xr, xl 411 | xr = xr ^ self.p_boxes[1] 412 | xl = xl ^ self.p_boxes[0] 413 | return xl, xr 414 | 415 | def __round_func (self, xl): 416 | a = (xl & 0xFF000000) >> 24 417 | b = (xl & 0x00FF0000) >> 16 418 | c = (xl & 0x0000FF00) >> 8 419 | d = xl & 0x000000FF 420 | 421 | # Perform all ops as longs then and out the last 32-bits to 422 | # obtain the integer 423 | f = (long (self.s_boxes[0][a]) + long (self.s_boxes[1][b])) % self.modulus 424 | f = f ^ long (self.s_boxes[2][c]) 425 | f = f + long (self.s_boxes[3][d]) 426 | f = (f % self.modulus) & 0xFFFFFFFF 427 | 428 | return f 429 | 430 | def encrypt (self, data): 431 | 432 | if not len (data) == 8: 433 | raise RuntimeError, "Attempted to encrypt data of invalid block length: %s" %len (data) 434 | 435 | # Use big endianess since that's what everyone else uses 436 | xl = ord (data[3]) | (ord (data[2]) << 8) | (ord (data[1]) << 16) | (ord (data[0]) << 24) 437 | xr = ord (data[7]) | (ord (data[6]) << 8) | (ord (data[5]) << 16) | (ord (data[4]) << 24) 438 | 439 | cl, cr = self.cipher (xl, xr, self.ENCRYPT) 440 | chars = ''.join ([ 441 | chr ((cl >> 24) & 0xFF), chr ((cl >> 16) & 0xFF), chr ((cl >> 8) & 0xFF), chr (cl & 0xFF), 442 | chr ((cr >> 24) & 0xFF), chr ((cr >> 16) & 0xFF), chr ((cr >> 8) & 0xFF), chr (cr & 0xFF) 443 | ]) 444 | return chars 445 | 446 | def decrypt (self, data): 447 | 448 | if not len (data) == 8: 449 | raise RuntimeError, "Attempted to encrypt data of invalid block length: %s" %len (data) 450 | 451 | # Use big endianess since that's what everyone else uses 452 | cl = ord (data[3]) | (ord (data[2]) << 8) | (ord (data[1]) << 16) | (ord (data[0]) << 24) 453 | cr = ord (data[7]) | (ord (data[6]) << 8) | (ord (data[5]) << 16) | (ord (data[4]) << 24) 454 | 455 | xl, xr = self.cipher (cl, cr, self.DECRYPT) 456 | chars = ''.join ([ 457 | chr ((xl >> 24) & 0xFF), chr ((xl >> 16) & 0xFF), chr ((xl >> 8) & 0xFF), chr (xl & 0xFF), 458 | chr ((xr >> 24) & 0xFF), chr ((xr >> 16) & 0xFF), chr ((xr >> 8) & 0xFF), chr (xr & 0xFF) 459 | ]) 460 | return chars 461 | 462 | def blocksize (self): 463 | return 8 464 | 465 | def key_length (self): 466 | return 56 467 | 468 | def key_bits (self): 469 | return 56 * 8 470 | 471 | 472 | -------------------------------------------------------------------------------- /pithos/pandora/fake.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*- 2 | ### BEGIN LICENSE 3 | # Copyright (C) 2010 Kevin Mehall 4 | #This program is free software: you can redistribute it and/or modify it 5 | #under the terms of the GNU General Public License version 3, as published 6 | #by the Free Software Foundation. 7 | # 8 | #This program is distributed in the hope that it will be useful, but 9 | #WITHOUT ANY WARRANTY; without even the implied warranties of 10 | #MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 11 | #PURPOSE. See the GNU General Public License for more details. 12 | # 13 | #You should have received a copy of the GNU General Public License along 14 | #with this program. If not, see . 15 | ### END LICENSE 16 | 17 | from pithos.pandora.pandora import * 18 | import gtk 19 | import logging 20 | 21 | class FakePandora(Pandora): 22 | def __init__(self): 23 | super(FakePandora, self).__init__() 24 | self.counter = 0 25 | self.show_fail_window() 26 | logging.info("Using test mode") 27 | 28 | def count(self): 29 | self.counter +=1 30 | return self.counter 31 | 32 | def show_fail_window(self): 33 | self.window = gtk.Window() 34 | self.window.set_size_request(200, 100) 35 | self.window.set_title("Pithos failure tester") 36 | self.window.set_opacity(0.7) 37 | self.auth_check = gtk.CheckButton("Authenticated") 38 | self.time_check = gtk.CheckButton("Be really slow") 39 | vbox = gtk.VBox() 40 | self.window.add(vbox) 41 | vbox.pack_start(self.auth_check) 42 | vbox.pack_start(self.time_check) 43 | self.window.show_all() 44 | 45 | def maybe_fail(self): 46 | if self.time_check.get_active(): 47 | logging.info("fake: Going to sleep for 10s") 48 | time.sleep(10) 49 | if not self.auth_check.get_active(): 50 | logging.info("fake: We're deauthenticated...") 51 | raise PandoraAuthTokenInvalid("Auth token invalid", "AUTH_INVALID_TOKEN") 52 | 53 | def set_authenticated(self): 54 | self.auth_check.set_active(True) 55 | 56 | def json_call(self, method, args={}, https=False, blowfish=True): 57 | time.sleep(1) 58 | self.maybe_fail() 59 | 60 | if method == 'user.getStationList': 61 | return {'stations': [ 62 | {'stationId':'987', 'stationToken':'345434', 'isShared':False, 'isQuickMix':False, 'stationName':"Test Station 1"}, 63 | {'stationId':'321', 'stationToken':'453544', 'isShared':False, 'isQuickMix':True, 'stationName':"Fake's QuickMix", 64 | 'quickMixStationIds':['987', '343']}, 65 | {'stationId':'432', 'stationToken':'345485', 'isShared':False, 'isQuickMix':False, 'stationName':"Test Station 2"}, 66 | {'stationId':'254', 'stationToken':'345415', 'isShared':False, 'isQuickMix':False, 'stationName':"Test Station 4 - Out of Order"}, 67 | {'stationId':'343', 'stationToken':'345435', 'isShared':False, 'isQuickMix':False, 'stationName':"Test Station 3"}, 68 | ]} 69 | elif method == 'station.getPlaylist': 70 | stationId = self.get_station_by_token(args['stationToken']).id 71 | return {'items': [self.makeFakeSong(stationId) for i in range(4)]} 72 | elif method == 'music.search': 73 | return {'artists': [ 74 | {'score':90, 'musicToken':'988', 'artistName':"artistName"}, 75 | ], 76 | 'songs':[ 77 | {'score':80, 'musicToken':'238', 'songName':"SongName", 'artistName':"ArtistName"}, 78 | ], 79 | } 80 | elif method == 'station.createStation': 81 | return {'stationId':'999', 'stationToken':'345433', 'isShared':False, 'isQuickMix':False, 'stationName':"Added Station"} 82 | elif method == 'station.addFeedback': 83 | return {'feedbackId': '1234'} 84 | elif method in ('user.setQuickMix', 85 | 'station.deleteFeedback', 86 | 'station.transformSharedStation', 87 | 'station.renameStation', 88 | 'station.deleteStation', 89 | 'user.sleepSong', 90 | 'bookmark.addSongBookmark', 91 | 'bookmark.addArtistBookmark', 92 | ): 93 | return 1 94 | else: 95 | logging.error("Invalid method %s" % method) 96 | 97 | def connect(self, user, password): 98 | self.set_authenticated() 99 | self.get_stations() 100 | 101 | def get_station_by_token(self, token): 102 | for i in self.stations: 103 | if i.idToken == token: 104 | return i 105 | 106 | def makeFakeSong(self, stationId): 107 | c = self.count() 108 | return { 109 | 'albumName':"AlbumName", 110 | 'artistName':"ArtistName", 111 | 'additionalAudioUrl':'http://kevinmehall.net/p/pithos/testfile.aac?val='+'0'*48, 112 | 'trackGain':0, 113 | 'trackToken':'5908540384', 114 | 'songRating': 1 if c%3 == 0 else 0, 115 | 'stationId': stationId, 116 | 'songName': 'Test song %i'%c, 117 | 'songDetailUrl': 'http://kevinmehall.net/p/pithos/', 118 | 'albumDetailUrl':'http://kevinmehall.net/p/pithos/', 119 | 'albumArtUrl':'http://i.imgur.com/H4Z8x.jpg', 120 | } 121 | 122 | -------------------------------------------------------------------------------- /pithos/pandora/pandora.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*- 2 | ### BEGIN LICENSE 3 | # Copyright (C) 2010 Kevin Mehall 4 | # Copyright (C) 2012 Christopher Eby 5 | #This program is free software: you can redistribute it and/or modify it 6 | #under the terms of the GNU General Public License version 3, as published 7 | #by the Free Software Foundation. 8 | # 9 | #This program is distributed in the hope that it will be useful, but 10 | #WITHOUT ANY WARRANTY; without even the implied warranties of 11 | #MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 12 | #PURPOSE. See the GNU General Public License for more details. 13 | # 14 | #You should have received a copy of the GNU General Public License along 15 | #with this program. If not, see . 16 | ### END LICENSE 17 | 18 | from pithos.pandora.blowfish import Blowfish 19 | import json 20 | import logging 21 | import time 22 | import urllib 23 | import urllib2 24 | 25 | # This is an implementation of the Pandora JSON API using Android partner 26 | # credentials. 27 | # See http://pan-do-ra-api.wikia.com/wiki/Json/5 for API documentation. 28 | 29 | PROTOCOL_VERSION = '5' 30 | 31 | CLIENTS = {'android-generic': {'deviceModel': 'android-generic', 32 | 'partner_username': 'android', 33 | 'partner_password': 'AC7IBG09A3DTSYM4R41UJWL07VLN8JI7', 34 | 'rpcUrl': '://tuner.pandora.com/services/json/?', 35 | 'encryptKey': '6#26FRL$ZWD', 36 | 'decryptKey': 'R=U!LH$O2B#', 37 | }, 38 | 'pandora one': {'deviceModel': 'D01', 39 | 'partner_username': 'pandora one', 40 | 'partner_password': 'TVCKIBGS9AO9TSYLNNFUML0743LH82D', 41 | 'rpcUrl': '://internal-tuner.pandora.com/services/json/?', 42 | 'encryptKey': '2%3WCL*JU$MP]4', 43 | 'decryptKey': 'U#IO$RZPAB%VX2', 44 | } 45 | } 46 | DEFAULT_CLIENT = CLIENTS['android-generic'] 47 | 48 | HTTP_TIMEOUT = 30 49 | USER_AGENT = 'pithos' 50 | 51 | RATE_BAN = 'ban' 52 | RATE_LOVE = 'love' 53 | RATE_NONE = None 54 | 55 | API_ERROR_API_VERSION_NOT_SUPPORTED = 11 56 | API_ERROR_COUNTRY_NOT_SUPPORTED = 12 57 | API_ERROR_INSUFFICIENT_CONNECTIVITY = 13 58 | API_ERROR_READ_ONLY_MODE = 1000 59 | API_ERROR_INVALID_AUTH_TOKEN = 1001 60 | API_ERROR_INVALID_LOGIN = 1002 61 | API_ERROR_LISTENER_NOT_AUTHORIZED = 1003 62 | API_ERROR_PARTNER_NOT_AUTHORIZED = 1010 63 | 64 | PLAYLIST_VALIDITY_TIME = 60*60*3 65 | 66 | class PandoraError(IOError): 67 | def __init__(self, message, status=None, submsg=None): 68 | self.status = status 69 | self.message = message 70 | self.submsg = submsg 71 | 72 | class PandoraAuthTokenInvalid(PandoraError): pass 73 | class PandoraNetError(PandoraError): pass 74 | class PandoraAPIVersionError(PandoraError): pass 75 | class PandoraTimeout(PandoraNetError): pass 76 | 77 | 78 | def pad(s, l): 79 | return s + "\0" * (l - len(s)) 80 | 81 | 82 | class Pandora(object): 83 | def pandora_encrypt(self, s): 84 | return "".join([self.blowfish_encode.encrypt(pad(s[i:i+8], 8)).encode('hex') for i in xrange(0, len(s), 8)]) 85 | 86 | def pandora_decrypt(self, s): 87 | return "".join([self.blowfish_decode.decrypt(pad(s[i:i+16].decode('hex'), 8)) for i in xrange(0, len(s), 16)]).rstrip('\x08') 88 | 89 | def json_call(self, method, args={}, https=False, blowfish=True): 90 | url_arg_strings = [] 91 | if self.partnerId: 92 | url_arg_strings.append('partner_id=%s'%self.partnerId) 93 | if self.userId: 94 | url_arg_strings.append('user_id=%s'%self.userId) 95 | if self.userAuthToken: 96 | url_arg_strings.append('auth_token=%s'%urllib.quote_plus(self.userAuthToken)) 97 | elif self.partnerAuthToken: 98 | url_arg_strings.append('auth_token=%s'%urllib.quote_plus(self.partnerAuthToken)) 99 | 100 | url_arg_strings.append('method=%s'%method) 101 | protocol = 'https' if https else 'http' 102 | url = protocol + self.rpc_url + '&'.join(url_arg_strings) 103 | 104 | if self.time_offset: 105 | args['syncTime'] = int(time.time()+self.time_offset) 106 | if self.userAuthToken: 107 | args['userAuthToken'] = self.userAuthToken 108 | elif self.partnerAuthToken: 109 | args['partnerAuthToken'] = self.partnerAuthToken 110 | data = json.dumps(args) 111 | 112 | logging.debug(url) 113 | logging.debug(data) 114 | 115 | if blowfish: 116 | data = self.pandora_encrypt(data) 117 | 118 | try: 119 | req = urllib2.Request(url, data, {'User-agent': USER_AGENT, 'Content-type': 'text/plain'}) 120 | response = self.opener.open(req, timeout=HTTP_TIMEOUT) 121 | text = response.read() 122 | except urllib2.HTTPError as e: 123 | logging.error("HTTP error: %s", e) 124 | raise PandoraNetError(str(e)) 125 | except urllib2.URLError as e: 126 | logging.error("Network error: %s", e) 127 | if e.reason[0] == 'timed out': 128 | raise PandoraTimeout("Network error", submsg="Timeout") 129 | else: 130 | raise PandoraNetError("Network error", submsg=e.reason[1]) 131 | 132 | logging.debug(text) 133 | 134 | tree = json.loads(text) 135 | 136 | if tree['stat'] == 'fail': 137 | code = tree['code'] 138 | msg = tree['message'] 139 | logging.error('fault code: ' + str(code) + ' message: ' + msg) 140 | 141 | if code == API_ERROR_INVALID_AUTH_TOKEN: 142 | raise PandoraAuthTokenInvalid(msg) 143 | elif code == API_ERROR_COUNTRY_NOT_SUPPORTED: 144 | raise PandoraError("Pandora not available", code, 145 | submsg="Pandora is not available outside the United States.") 146 | elif code == API_ERROR_API_VERSION_NOT_SUPPORTED: 147 | raise PandoraAPIVersionError(msg) 148 | elif code == API_ERROR_INSUFFICIENT_CONNECTIVITY: 149 | raise PandoraError("Out of sync", code, 150 | submsg="Correct your system's clock. If the problem persists, a Pithos update may be required") 151 | elif code == API_ERROR_READ_ONLY_MODE: 152 | raise PandoraError("Pandora maintenance", code, 153 | submsg="Pandora is in read-only mode as it is performing maintenance. Try again later.") 154 | elif code == API_ERROR_INVALID_LOGIN: 155 | raise PandoraError("Login Error", code, submsg="Invalid username or password") 156 | elif code == API_ERROR_LISTENER_NOT_AUTHORIZED: 157 | raise PandoraError("Login Error", code, submsg='User is not a Pandora One subscriber.\nPlease uncheck "Pandora One Subscriber" in preferences.') 158 | elif code == API_ERROR_PARTNER_NOT_AUTHORIZED: 159 | raise PandoraError("Login Error", code, 160 | submsg="Invalid Pandora partner keys. A Pithos update may be required.") 161 | else: 162 | raise PandoraError("Pandora returned an error", code, "%s (code %d)"%(msg, code)) 163 | 164 | if 'result' in tree: 165 | return tree['result'] 166 | 167 | def set_audio_quality(self, fmt): 168 | self.audio_quality = fmt 169 | 170 | def set_proxy(self, proxy): 171 | if proxy: 172 | proxy_handler = urllib2.ProxyHandler({'http': proxy}) 173 | self.opener = urllib2.build_opener(proxy_handler) 174 | else: 175 | self.opener = urllib2.build_opener() 176 | 177 | def connect(self, prefs): 178 | client = DEFAULT_CLIENT 179 | if prefs['pandora_one']: 180 | client = CLIENTS['pandora one'] 181 | self.partner_username = prefs.get('partner_username', client['partner_username']) 182 | self.partner_password = prefs.get('partner_password', client['partner_password']) 183 | self.device_model = prefs.get('device_model', client['deviceModel']) 184 | self.blowfish_encode = Blowfish(prefs.get('encrypt_key', client['encryptKey'])) 185 | self.blowfish_decode = Blowfish(prefs.get('decrypt_key', client['decryptKey'])) 186 | self.rpc_url = prefs.get('rpc_url', client['rpcUrl']) 187 | 188 | self.partnerId = self.userId = self.partnerAuthToken = self.userAuthToken = self.time_offset = None 189 | 190 | partner = self.json_call('auth.partnerLogin', {'deviceModel': self.device_model, 191 | 'username': self.partner_username, 192 | 'password': self.partner_password, 193 | 'version': PROTOCOL_VERSION}, 194 | https=True, blowfish=False) 195 | self.partnerId = partner['partnerId'] 196 | self.partnerAuthToken = partner['partnerAuthToken'] 197 | 198 | pandora_time = int(self.pandora_decrypt(partner['syncTime'])[4:14]) 199 | self.time_offset = pandora_time - time.time() 200 | logging.info("Time offset is %s", self.time_offset) 201 | 202 | user = self.json_call('auth.userLogin', {'username': prefs['username'], 203 | 'password': prefs['password'], 'loginType': 'user'}, 204 | https=True) 205 | self.userId = user['userId'] 206 | self.userAuthToken = user['userAuthToken'] 207 | 208 | self.get_stations(self) 209 | 210 | def get_stations(self, *ignore): 211 | stations = self.json_call('user.getStationList')['stations'] 212 | self.quickMixStationIds = None 213 | self.stations = [Station(self, i) for i in stations] 214 | 215 | if self.quickMixStationIds: 216 | for i in self.stations: 217 | if i.id in self.quickMixStationIds: 218 | i.useQuickMix = True 219 | 220 | def save_quick_mix(self): 221 | stationIds = [] 222 | for i in self.stations: 223 | if i.useQuickMix: 224 | stationIds.append(i.id) 225 | self.json_call('user.setQuickMix', {'quickMixStationIds': stationIds}) 226 | 227 | def search(self, query): 228 | results = self.json_call('music.search', {'searchText': query}) 229 | 230 | l = [SearchResult('artist', i) for i in results['artists']] 231 | l += [SearchResult('song', i) for i in results['songs']] 232 | l.sort(key=lambda i: i.score, reverse=True) 233 | 234 | return l 235 | 236 | def add_station_by_music_id(self, musicid): 237 | d = self.json_call('station.createStation', {'musicToken': musicid}) 238 | station = Station(self, d) 239 | self.stations.append(station) 240 | return station 241 | 242 | def get_station_by_id(self, id): 243 | for i in self.stations: 244 | if i.id == id: 245 | return i 246 | 247 | def add_feedback(self, trackToken, rating): 248 | logging.info("pandora: addFeedback") 249 | rating_bool = True if rating == RATE_LOVE else False 250 | feedback = self.json_call('station.addFeedback', {'trackToken': trackToken, 'isPositive': rating_bool}) 251 | return feedback['feedbackId'] 252 | 253 | def delete_feedback(self, stationToken, feedbackId): 254 | self.json_call('station.deleteFeedback', {'feedbackId': feedbackId, 'stationToken': stationToken}) 255 | 256 | class Station(object): 257 | def __init__(self, pandora, d): 258 | self.pandora = pandora 259 | 260 | self.id = d['stationId'] 261 | self.idToken = d['stationToken'] 262 | self.isCreator = not d['isShared'] 263 | self.isQuickMix = d['isQuickMix'] 264 | self.name = d['stationName'] 265 | self.useQuickMix = False 266 | 267 | if self.isQuickMix: 268 | self.pandora.quickMixStationIds = d.get('quickMixStationIds', []) 269 | 270 | def transformIfShared(self): 271 | if not self.isCreator: 272 | logging.info("pandora: transforming station") 273 | self.pandora.json_call('station.transformSharedStation', {'stationToken': self.idToken}) 274 | self.isCreator = True 275 | 276 | def get_playlist(self): 277 | logging.info("pandora: Get Playlist") 278 | playlist = self.pandora.json_call('station.getPlaylist', 279 | {'stationToken': self.idToken}, https=True) 280 | songs = [] 281 | for i in playlist['items']: 282 | if 'songName' in i: # check for ads 283 | songs.append(Song(self.pandora, i)) 284 | return songs 285 | 286 | @property 287 | def info_url(self): 288 | return 'http://www.pandora.com/stations/'+self.idToken 289 | 290 | def rename(self, new_name): 291 | if new_name != self.name: 292 | self.transformIfShared() 293 | logging.info("pandora: Renaming station") 294 | self.pandora.json_call('station.renameStation', {'stationToken': self.idToken, 'stationName': new_name}) 295 | self.name = new_name 296 | 297 | def delete(self): 298 | logging.info("pandora: Deleting Station") 299 | self.pandora.json_call('station.deleteStation', {'stationToken': self.idToken}) 300 | 301 | class Song(object): 302 | def __init__(self, pandora, d): 303 | self.pandora = pandora 304 | 305 | self.album = d['albumName'] 306 | self.artist = d['artistName'] 307 | if self.pandora.audio_quality in d['audioUrlMap']: 308 | self.audioUrl = d['audioUrlMap'][self.pandora.audio_quality]['audioUrl'] 309 | else: 310 | # Just pick the first one 311 | self.audioUrl = d['audioUrlMap'].values()[0]['audioUrl'] 312 | self.trackToken = d['trackToken'] 313 | self.rating = RATE_LOVE if d['songRating'] == 1 else RATE_NONE # banned songs won't play, so we don't care about them 314 | self.stationId = d['stationId'] 315 | self.title = d['songName'] 316 | self.songDetailURL = d['songDetailUrl'] 317 | self.artRadio = d['albumArtUrl'] 318 | 319 | self.tired=False 320 | self.message='' 321 | self.start_time = None 322 | self.finished = False 323 | self.playlist_time = time.time() 324 | self.feedbackId = None 325 | 326 | @property 327 | def station(self): 328 | return self.pandora.get_station_by_id(self.stationId) 329 | 330 | def rate(self, rating): 331 | if self.rating != rating: 332 | self.station.transformIfShared() 333 | if rating == RATE_NONE: 334 | if not self.feedbackId: 335 | # We need a feedbackId, get one by re-rating the song. We 336 | # could also get one by calling station.getStation, but 337 | # that requires transferring a lot of data (all feedback, 338 | # seeds, etc for the station). 339 | opposite = RATE_BAN if self.rating == RATE_LOVE else RATE_LOVE 340 | self.feedbackId = self.pandora.add_feedback(self.trackToken, opposite) 341 | self.pandora.delete_feedback(self.station.idToken, self.feedbackId) 342 | else: 343 | self.feedbackId = self.pandora.add_feedback(self.trackToken, rating) 344 | self.rating = rating 345 | 346 | def set_tired(self): 347 | if not self.tired: 348 | self.pandora.json_call('user.sleepSong', {'trackToken': self.trackToken}) 349 | self.tired = True 350 | 351 | def bookmark(self): 352 | self.pandora.json_call('bookmark.addSongBookmark', {'trackToken': self.trackToken}) 353 | 354 | def bookmark_artist(self): 355 | self.pandora.json_call('bookmark.addArtistBookmark', {'trackToken': self.trackToken}) 356 | 357 | @property 358 | def rating_str(self): 359 | return self.rating 360 | 361 | def is_still_valid(self): 362 | return (time.time() - self.playlist_time) < PLAYLIST_VALIDITY_TIME 363 | 364 | class SearchResult(object): 365 | def __init__(self, resultType, d): 366 | self.resultType = resultType 367 | self.score = d['score'] 368 | self.musicId = d['musicToken'] 369 | 370 | if resultType == 'song': 371 | self.title = d['songName'] 372 | self.artist = d['artistName'] 373 | elif resultType == 'artist': 374 | self.name = d['artistName'] 375 | 376 | -------------------------------------------------------------------------------- /pithos/pithosconfig.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*- 2 | ### BEGIN LICENSE 3 | # Copyright (C) 2010-2012 Kevin Mehall 4 | #This program is free software: you can redistribute it and/or modify it 5 | #under the terms of the GNU General Public License version 3, as published 6 | #by the Free Software Foundation. 7 | # 8 | #This program is distributed in the hope that it will be useful, but 9 | #WITHOUT ANY WARRANTY; without even the implied warranties of 10 | #MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 11 | #PURPOSE. See the GNU General Public License for more details. 12 | # 13 | #You should have received a copy of the GNU General Public License along 14 | #with this program. If not, see . 15 | ### END LICENSE 16 | 17 | # where your project will head for your data (for instance, images and ui files) 18 | # by default, this is ../data, relative your trunk layout 19 | __pithos_data_directory__ = '../data/' 20 | __license__ = 'GPL-3' 21 | 22 | VERSION = '0.3.17' 23 | 24 | import os 25 | 26 | class project_path_not_found(Exception): 27 | pass 28 | 29 | valid_audio_formats = [ 30 | ('highQuality', 'High'), 31 | ('mediumQuality', 'Medium'), 32 | ('lowQuality', 'Low'), 33 | ] 34 | 35 | def get_data_file(*path_segments): 36 | """Get the full path to a data file. 37 | 38 | Returns the path to a file underneath the data directory (as defined by 39 | `get_data_path`). Equivalent to os.path.join(get_data_path(), 40 | *path_segments). 41 | """ 42 | return os.path.join(getdatapath(), *path_segments) 43 | 44 | def getdatapath(): 45 | """Retrieve pithos data path 46 | 47 | This path is by default /../data/ in trunk 48 | and /usr/share/pithos in an installed version but this path 49 | is specified at installation time. 50 | """ 51 | 52 | # get pathname absolute or relative 53 | if __pithos_data_directory__.startswith('/'): 54 | pathname = __pithos_data_directory__ 55 | else: 56 | pathname = os.path.dirname(__file__) + '/' + __pithos_data_directory__ 57 | 58 | abs_data_path = os.path.abspath(pathname) 59 | if os.path.exists(abs_data_path): 60 | return abs_data_path 61 | else: 62 | raise project_path_not_found 63 | 64 | if __name__=='__main__': 65 | print VERSION 66 | -------------------------------------------------------------------------------- /pithos/plugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*- 2 | ### BEGIN LICENSE 3 | # Copyright (C) 2010-2012 Kevin Mehall 4 | #This program is free software: you can redistribute it and/or modify it 5 | #under the terms of the GNU General Public License version 3, as published 6 | #by the Free Software Foundation. 7 | # 8 | #This program is distributed in the hope that it will be useful, but 9 | #WITHOUT ANY WARRANTY; without even the implied warranties of 10 | #MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 11 | #PURPOSE. See the GNU General Public License for more details. 12 | # 13 | #You should have received a copy of the GNU General Public License along 14 | #with this program. If not, see . 15 | ### END LICENSE 16 | 17 | import logging 18 | import glob 19 | import os 20 | 21 | class PithosPlugin(object): 22 | _PITHOS_PLUGIN = True # used to find the plugin class in a module 23 | preference = None 24 | def __init__(self, name, window): 25 | self.name = name 26 | self.window = window 27 | self.prepared = False 28 | self.enabled = False 29 | 30 | def enable(self): 31 | if not self.prepared: 32 | self.error = self.on_prepare() 33 | self.prepared = True 34 | if not self.error and not self.enabled: 35 | logging.info("Enabling module %s"%(self.name)) 36 | self.on_enable() 37 | self.enabled = True 38 | 39 | def disable(self): 40 | if self.enabled: 41 | logging.info("Disabling module %s"%(self.name)) 42 | self.on_disable() 43 | self.enabled = False 44 | 45 | def on_prepare(self): 46 | pass 47 | 48 | def on_enable(self): 49 | pass 50 | 51 | def on_disable(self): 52 | pass 53 | 54 | class ErrorPlugin(PithosPlugin): 55 | def __init__(self, name, error): 56 | logging.error("Error loading plugin %s: %s"%(name, error)) 57 | self.prepared = True 58 | self.error = error 59 | self.name = name 60 | self.enabled = False 61 | 62 | def load_plugin(name, window): 63 | try: 64 | module = __import__('pithos.plugins.'+name) 65 | module = getattr(module.plugins, name) 66 | 67 | except ImportError as e: 68 | return ErrorPlugin(name, e.message) 69 | 70 | # find the class object for the actual plugin 71 | for key, item in module.__dict__.iteritems(): 72 | if hasattr(item, '_PITHOS_PLUGIN') and key != "PithosPlugin": 73 | plugin_class = item 74 | break 75 | else: 76 | return ErrorPlugin(name, "Could not find module class") 77 | 78 | return plugin_class(name, window) 79 | 80 | def load_plugins(window): 81 | plugins = window.plugins 82 | prefs = window.preferences 83 | 84 | plugins_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "plugins") 85 | discovered_plugins = [ fname.replace(".py", "") for fname in glob.glob1(plugins_dir, "*.py") if not fname.startswith("__") ] 86 | 87 | for name in discovered_plugins: 88 | if not name in plugins: 89 | plugin = plugins[name] = load_plugin(name, window) 90 | else: 91 | plugin = plugins[name] 92 | 93 | if plugin.preference and prefs.get(plugin.preference, False): 94 | plugin.enable() 95 | else: 96 | plugin.disable() 97 | 98 | -------------------------------------------------------------------------------- /pithos/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TingPing/pithos-for-windows/02a93e76ff6b616e14f10dfc04d1532bd7d98940/pithos/plugins/__init__.py -------------------------------------------------------------------------------- /pithos/plugins/growl.py: -------------------------------------------------------------------------------- 1 | from pithos.plugin import PithosPlugin 2 | from pithos.pithosconfig import get_data_file 3 | import logging 4 | 5 | class GrowlPlugin(PithosPlugin): 6 | preference = 'growl' 7 | 8 | def on_prepare(self): 9 | pass 10 | 11 | def on_enable(self): 12 | try: import gntp.notifier 13 | except(ImportError): 14 | logging.error('Growl Error: gntp not installed: https://github.com/kfdm/gntp') 15 | return False 16 | #temp hosting of icon 17 | self.pithosicon = 'http://puu.sh/xnMA' 18 | self.growl = gntp.notifier.GrowlNotifier( 19 | applicationName='Pithos', 20 | notifications=['Song Changed'], 21 | defaultNotifications=['Song Changed'], 22 | applicationIcon=self.pithosicon, 23 | # change for over the network notifications 24 | #hostname='localhost', 25 | #password='' 26 | ) 27 | try: self.growl.register() 28 | except: logging.warning('Failed to register Growl') 29 | self.song_callback_handle = self.window.connect("song-changed", self.song_changed) 30 | 31 | def song_changed(self, window, song): 32 | try: 33 | self.growl.notify( 34 | noteType='Song Changed', 35 | title=song.title, 36 | description='by %s on %s' %(song.artist, song.album), 37 | icon=self.pithosicon, 38 | sticky=False, 39 | priority=1 40 | ) 41 | except: 42 | logging.warning('Growl not running') 43 | 44 | def on_disable(self): 45 | self.window.disconnect(self.song_callback_handle) 46 | -------------------------------------------------------------------------------- /pithos/plugins/mediakeys.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*- 2 | ### BEGIN LICENSE 3 | # Copyright (C) 2010-2012 Kevin Mehall 4 | #This program is free software: you can redistribute it and/or modify it 5 | #under the terms of the GNU General Public License version 3, as published 6 | #by the Free Software Foundation. 7 | # 8 | #This program is distributed in the hope that it will be useful, but 9 | #WITHOUT ANY WARRANTY; without even the implied warranties of 10 | #MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 11 | #PURPOSE. See the GNU General Public License for more details. 12 | # 13 | #You should have received a copy of the GNU General Public License along 14 | #with this program. If not, see . 15 | ### END LICENSE 16 | 17 | from pithos.plugin import PithosPlugin 18 | import logging 19 | 20 | class MediaKeyPlugin(PithosPlugin): 21 | preference = 'mediakeys' 22 | 23 | def kbevent(self, event): 24 | if event.KeyID == 179 or event.Key == 'Media_Play_Pause': 25 | self.window.playpause_notify() 26 | if event.KeyID == 176 or event.Key == 'Media_Next_Track': 27 | self.window.next_song() 28 | return True 29 | 30 | def on_enable(self): 31 | try: import pyHook 32 | except ImportError: 33 | logging.warning('Please install PyHook(http://sourceforge.net/projects/pyhook/files/') 34 | return False 35 | self.hookman = pyHook.HookManager() 36 | self.hookman.KeyDown = self.kbevent 37 | self.hookman.HookKeyboard() 38 | 39 | def on_disable(self): 40 | self.hookman.UnhookKeyboard() 41 | -------------------------------------------------------------------------------- /pithos/plugins/notification_icon.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*- 2 | ### BEGIN LICENSE 3 | # Copyright (C) 2010-2012 Kevin Mehall 4 | #This program is free software: you can redistribute it and/or modify it 5 | #under the terms of the GNU General Public License version 3, as published 6 | #by the Free Software Foundation. 7 | # 8 | #This program is distributed in the hope that it will be useful, but 9 | #WITHOUT ANY WARRANTY; without even the implied warranties of 10 | #MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 11 | #PURPOSE. See the GNU General Public License for more details. 12 | # 13 | #You should have received a copy of the GNU General Public License along 14 | #with this program. If not, see . 15 | ### END LICENSE 16 | 17 | import gtk 18 | from pithos.pithosconfig import get_data_file 19 | from pithos.plugin import PithosPlugin 20 | 21 | # Check if appindicator is available on the system 22 | try: 23 | import appindicator 24 | indicator_capable = True 25 | except: 26 | indicator_capable = False 27 | 28 | class PithosNotificationIcon(PithosPlugin): 29 | preference = 'show_icon' 30 | 31 | def on_prepare(self): 32 | if indicator_capable: 33 | self.ind = appindicator.Indicator("pithos", \ 34 | "pithos-mono", \ 35 | appindicator.CATEGORY_APPLICATION_STATUS, \ 36 | get_data_file('media')) 37 | 38 | def on_enable(self): 39 | self.visible = True 40 | self.delete_callback_handle = self.window.connect("delete-event", self.toggle_visible) 41 | self.state_callback_handle = self.window.connect("play-state-changed", self.play_state_changed) 42 | self.song_callback_handle = self.window.connect("song-changed", self.song_changed) 43 | 44 | if indicator_capable: 45 | self.ind.set_status(appindicator.STATUS_ACTIVE) 46 | else: 47 | self.statusicon = gtk.status_icon_new_from_file(get_data_file('media', 'icon.png')) 48 | self.statusicon.connect('activate', self.toggle_visible) 49 | 50 | self.build_context_menu() 51 | 52 | def build_context_menu(self): 53 | menu = gtk.Menu() 54 | 55 | def button(text, action, icon=None): 56 | if icon == 'check': 57 | item = gtk.CheckMenuItem(text) 58 | item.set_active(True) 59 | elif icon: 60 | item = gtk.ImageMenuItem(text) 61 | item.set_image(gtk.image_new_from_stock(icon, gtk.ICON_SIZE_MENU)) 62 | else: 63 | item = gtk.MenuItem(text) 64 | item.connect('activate', action) 65 | item.show() 66 | menu.append(item) 67 | return item 68 | 69 | if indicator_capable: 70 | # We have to add another entry for show / hide Pithos window 71 | self.visible_check = button("Show Pithos", self._toggle_visible, 'check') 72 | 73 | self.playpausebtn = button("Pause", self.window.playpause, gtk.STOCK_MEDIA_PAUSE) 74 | button("Skip", self.window.next_song, gtk.STOCK_MEDIA_NEXT) 75 | button("Love", (lambda *i: self.window.love_song()), gtk.STOCK_ABOUT) 76 | button("Ban", (lambda *i: self.window.ban_song()), gtk.STOCK_CANCEL) 77 | button("Tired", (lambda *i: self.window.tired_song()), gtk.STOCK_JUMP_TO) 78 | button("Quit", self.window.quit, gtk.STOCK_QUIT ) 79 | 80 | # connect our new menu to the statusicon or the appindicator 81 | if indicator_capable: 82 | self.ind.set_menu(menu) 83 | else: 84 | self.statusicon.connect('popup-menu', self.context_menu, menu) 85 | 86 | self.menu = menu 87 | 88 | 89 | def play_state_changed(self, window, playing): 90 | """ play or pause and rotate the text """ 91 | 92 | button = self.playpausebtn 93 | if not playing: 94 | button.set_label("Play") 95 | button.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU)) 96 | 97 | else: 98 | button.set_label("Pause") 99 | button.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_MENU)) 100 | 101 | if indicator_capable: # menu needs to be reset to get updated icon 102 | self.ind.set_menu(self.menu) 103 | 104 | def song_changed(self, window, song): 105 | if not indicator_capable: 106 | self.statusicon.set_tooltip("%s by %s"%(song.title, song.artist)) 107 | 108 | def _toggle_visible(self, *args): 109 | if self.visible: 110 | self.window.hide() 111 | else: 112 | self.window.bring_to_top() 113 | 114 | self.visible = not self.visible 115 | 116 | def toggle_visible(self, *args): 117 | if hasattr(self, 'visible_check'): 118 | self.visible_check.set_active(not self.visible) 119 | else: 120 | self._toggle_visible() 121 | 122 | return True 123 | 124 | def context_menu(self, widget, button, time, data=None): 125 | if button == 3: 126 | if data: 127 | data.show_all() 128 | data.popup(None, None, None, 3, time) 129 | 130 | def on_disable(self): 131 | if indicator_capable: 132 | self.ind.set_status(appindicator.STATUS_PASSIVE) 133 | else: 134 | self.statusicon.set_visible(False) 135 | 136 | self.window.disconnect(self.delete_callback_handle) 137 | self.window.disconnect(self.state_callback_handle) 138 | self.window.disconnect(self.song_callback_handle) 139 | 140 | # Pithos window needs to be reconnected to on_destro() 141 | self.window.connect('delete-event',self.window.on_destroy) 142 | 143 | -------------------------------------------------------------------------------- /pithos/plugins/scrobble.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*- 2 | ### BEGIN LICENSE 3 | # Copyright (C) 2010-2012 Kevin Mehall 4 | #This program is free software: you can redistribute it and/or modify it 5 | #under the terms of the GNU General Public License version 3, as published 6 | #by the Free Software Foundation. 7 | # 8 | #This program is distributed in the hope that it will be useful, but 9 | #WITHOUT ANY WARRANTY; without even the implied warranties of 10 | #MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR 11 | #PURPOSE. See the GNU General Public License for more details. 12 | # 13 | #You should have received a copy of the GNU General Public License along 14 | #with this program. If not, see . 15 | ### END LICENSE 16 | 17 | from pithos import pylast 18 | import webbrowser 19 | import logging 20 | from pithos.gobject_worker import GObjectWorker 21 | from pithos.plugin import PithosPlugin 22 | 23 | #getting an API account: http://www.last.fm/api/account 24 | API_KEY = '997f635176130d5d6fe3a7387de601a8' 25 | API_SECRET = '3243b876f6bf880b923a3c9fb955720c' 26 | 27 | #client id, client version info: http://www.last.fm/api/submissions#1.1 28 | CLIENT_ID = 'pth' 29 | CLIENT_VERSION = '1.0' 30 | 31 | _worker = None 32 | def get_worker(): 33 | # so it can be shared between the plugin and the authorizer 34 | global _worker 35 | if not _worker: 36 | _worker = GObjectWorker() 37 | return _worker 38 | 39 | class LastfmPlugin(PithosPlugin): 40 | preference='lastfm_key' 41 | 42 | def on_prepare(self): 43 | self.worker = get_worker() 44 | 45 | def on_enable(self): 46 | self.connect(self.window.preferences['lastfm_key']) 47 | self.song_ended_handle = self.window.connect('song-ended', self.song_ended) 48 | self.song_changed_handle = self.window.connect('song-changed', self.song_changed) 49 | 50 | def on_disable(self): 51 | self.window.disconnect(self.song_ended_handle) 52 | self.window.disconnect(self.song_rating_changed_handle) 53 | self.window.disconnect(self.song_changed_handle) 54 | 55 | def song_ended(self, window, song): 56 | self.scrobble(song) 57 | 58 | def connect(self, session_key): 59 | self.network = pylast.get_lastfm_network( 60 | api_key=API_KEY, api_secret=API_SECRET, 61 | session_key = session_key 62 | ) 63 | self.scrobbler = self.network.get_scrobbler(CLIENT_ID, CLIENT_VERSION) 64 | 65 | def song_changed(self, window, song): 66 | self.worker.send(self.scrobbler.report_now_playing, (song.artist, song.title, song.album)) 67 | 68 | def send_rating(self, song, rating): 69 | if song.rating: 70 | track = self.network.get_track(song.artist, song.title) 71 | if rating == 'love': 72 | self.worker.send(track.love) 73 | elif rating == 'ban': 74 | self.worker.send(track.ban) 75 | logging.info("Sending song rating to last.fm") 76 | 77 | def scrobble(self, song): 78 | if song.duration > 30 and (song.position > 240 or song.position > song.duration/2): 79 | logging.info("Scrobbling song") 80 | mode = pylast.SCROBBLE_MODE_PLAYED 81 | source = pylast.SCROBBLE_SOURCE_PERSONALIZED_BROADCAST 82 | self.worker.send(self.scrobbler.scrobble, (song.artist, song.title, int(song.start_time), source, mode, song.duration, song.album)) 83 | 84 | 85 | class LastFmAuth: 86 | def __init__(self, d, prefname, button): 87 | self.button = button 88 | self.dict = d 89 | self.prefname = prefname 90 | 91 | self.auth_url= False 92 | self.set_button_text() 93 | self.button.connect('clicked', self.clicked) 94 | 95 | @property 96 | def enabled(self): 97 | return self.dict[self.prefname] 98 | 99 | def setkey(self, key): 100 | self.dict[self.prefname] = key 101 | self.set_button_text() 102 | 103 | def set_button_text(self): 104 | self.button.set_sensitive(True) 105 | if self.auth_url: 106 | self.button.set_label("Click once authorized on web site") 107 | elif self.enabled: 108 | self.button.set_label("Disable") 109 | else: 110 | self.button.set_label("Authorize") 111 | 112 | def clicked(self, *ignore): 113 | if self.auth_url: 114 | def err(e): 115 | logging.error(e) 116 | self.set_button_text() 117 | 118 | get_worker().send(self.sg.get_web_auth_session_key, (self.auth_url,), self.setkey, err) 119 | self.button.set_label("Checking...") 120 | self.button.set_sensitive(False) 121 | self.auth_url = False 122 | 123 | elif self.enabled: 124 | self.setkey(False) 125 | else: 126 | self.network = pylast.get_lastfm_network(api_key=API_KEY, api_secret=API_SECRET) 127 | self.sg = pylast.SessionKeyGenerator(self.network) 128 | 129 | def callback(url): 130 | self.auth_url = url 131 | self.set_button_text() 132 | webbrowser.open(self.auth_url) 133 | 134 | get_worker().send(self.sg.get_web_auth_url, (), callback) 135 | self.button.set_label("Connecting...") 136 | self.button.set_sensitive(False) 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /windows/MoveFileFolder.nsh: -------------------------------------------------------------------------------- 1 | ;%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | ; MoveFile and MoveFolder macros 3 | ; 4 | ; Author: theblazingangel@aol.com (for the AutoPatcher project - www.autopatcher.com) 5 | ; Created: June 2007 6 | ;%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 7 | 8 | ;================== 9 | ; MoveFile macro 10 | ;================== 11 | 12 | !macro MoveFile sourceFile destinationFile 13 | 14 | !define MOVEFILE_JUMP ${__LINE__} 15 | 16 | ; Check source actually exists 17 | 18 | IfFileExists "${sourceFile}" +3 0 19 | SetErrors 20 | goto done_${MOVEFILE_JUMP} 21 | 22 | ; Add message to details-view/install-log 23 | 24 | DetailPrint "Moving/renaming file: ${sourceFile} to ${destinationFile}" 25 | 26 | ; If destination does not already exists simply move file 27 | 28 | IfFileExists "${destinationFile}" +3 0 29 | rename "${sourceFile}" "${destinationFile}" 30 | goto done_${MOVEFILE_JUMP} 31 | 32 | ; If overwriting without 'ifnewer' check 33 | 34 | ${If} $switch_overwrite == 1 35 | delete "${destinationFile}" 36 | rename "${sourceFile}" "${destinationFile}" 37 | delete "${sourceFile}" 38 | goto done_${MOVEFILE_JUMP} 39 | ${EndIf} 40 | 41 | ; If destination already exists 42 | 43 | Push $R0 44 | Push $R1 45 | Push $R2 46 | push $R3 47 | 48 | GetFileTime "${sourceFile}" $R0 $R1 49 | GetFileTime "${destinationFile}" $R2 $R3 50 | 51 | IntCmp $R0 $R2 0 older_${MOVEFILE_JUMP} newer_${MOVEFILE_JUMP} 52 | IntCmp $R1 $R3 older_${MOVEFILE_JUMP} older_${MOVEFILE_JUMP} newer_${MOVEFILE_JUMP} 53 | 54 | older_${MOVEFILE_JUMP}: 55 | delete "${sourceFile}" 56 | goto time_check_done_${MOVEFILE_JUMP} 57 | 58 | newer_${MOVEFILE_JUMP}: 59 | delete "${destinationFile}" 60 | rename "${sourceFile}" "${destinationFile}" 61 | delete "${sourceFile}" ;incase above failed! 62 | 63 | time_check_done_${MOVEFILE_JUMP}: 64 | 65 | Pop $R3 66 | Pop $R2 67 | Pop $R1 68 | Pop $R0 69 | 70 | done_${MOVEFILE_JUMP}: 71 | 72 | !undef MOVEFILE_JUMP 73 | 74 | !macroend 75 | 76 | ;================== 77 | ; MoveFolder macro 78 | ;================== 79 | 80 | !macro MoveFolder source destination mask 81 | 82 | !define MOVEFOLDER_JUMP ${__LINE__} 83 | 84 | Push $R0 85 | Push $R1 86 | 87 | ; Move path parameters into registers so they can be altered if necessary 88 | 89 | StrCpy $R0 "${source}" 90 | StrCpy $R1 "${destination}" 91 | 92 | ; Sort out paths - remove final backslash if supplied 93 | 94 | Push $0 95 | 96 | ; Source 97 | StrCpy $0 "$R0" 1 -1 98 | StrCmp $0 '\' 0 +2 99 | StrCpy $R0 "$R0" -1 100 | 101 | ; Destination 102 | StrCpy $0 "$R1" 1 -1 103 | StrCmp $0 '\' 0 +2 104 | StrCpy $R1 "$R1" -1 105 | 106 | Pop $0 107 | 108 | ; Create destination dir 109 | 110 | CreateDirectory "$R1\" 111 | 112 | ; Add message to details-view/install-log 113 | 114 | DetailPrint "Moving files: $R0\${mask} to $R1\" 115 | 116 | ; Push registers used by ${Locate} onto stack 117 | 118 | Push $R6 119 | Push $R7 120 | Push $R8 121 | Push $R9 122 | 123 | ; Duplicate dir structure (to preserve empty folders and such) 124 | 125 | ${Locate} "$R0" "/L=D" ".MoveFolder_Locate_createDir" 126 | 127 | ; Locate files and move (via callback function) 128 | 129 | ${Locate} "$R0" "/L=F /M=${mask} /S= /G=1" ".MoveFolder_Locate_moveFile" 130 | 131 | ; Delete subfolders left over after move 132 | 133 | Push $R2 134 | deldir_loop_${MOVEFOLDER_JUMP}: 135 | StrCpy $R2 0 136 | ${Locate} "$R0" "/L=DE" ".MoveFolder_Locate_deleteDir" 137 | StrCmp $R2 0 0 deldir_loop_${MOVEFOLDER_JUMP} 138 | Pop $R2 139 | 140 | ; Delete empty subfolders moved - say the user just wanted to move *.apm files, they now also have a load of empty dir's from dir structure duplication! 141 | 142 | Push $R2 143 | delnewdir_loop_${MOVEFOLDER_JUMP}: 144 | StrCpy $R2 0 145 | ${Locate} "$R1" "/L=DE" ".MoveFolder_Locate_deleteDir" 146 | StrCmp $R2 0 0 delnewdir_loop_${MOVEFOLDER_JUMP} 147 | Pop $R2 148 | 149 | ; Pop registers used by ${Locate} off the stack again 150 | 151 | Pop $R9 152 | Pop $R8 153 | Pop $R7 154 | Pop $R6 155 | 156 | ; Delete source folder if empty 157 | 158 | rmdir "$R0" 159 | 160 | Pop $R1 161 | Pop $R0 162 | 163 | !undef MOVEFOLDER_JUMP 164 | 165 | !macroend 166 | 167 | ;================== 168 | ; MoveFolder macro's ${Locate} callback functions 169 | ;================== 170 | 171 | Function .MoveFolder_Locate_createDir 172 | 173 | ${If} $R6 == "" 174 | Push $R2 175 | StrLen $R2 "$R0" 176 | StrCpy $R2 $R9 '' $R2 177 | CreateDirectory "$R1$R2" 178 | Pop $R2 179 | ${EndIf} 180 | 181 | Push $R1 182 | 183 | FunctionEnd 184 | 185 | Function .MoveFolder_Locate_moveFile 186 | 187 | Push $R2 188 | 189 | ; Get path to file 190 | 191 | StrLen $R2 "$R0" 192 | StrCpy $R2 $R9 '' $R2 193 | StrCpy $R2 "$R1$R2" 194 | 195 | ; If destination does not already exists simply move file 196 | 197 | IfFileExists "$R2" +3 0 198 | rename "$R9" "$R2" 199 | goto done 200 | 201 | ; If overwriting without 'ifnewer' check 202 | 203 | ${If} $switch_overwrite == 1 204 | delete "$R2" 205 | rename "$R9" "$R2" 206 | delete "$R9" 207 | goto done 208 | ${EndIf} 209 | 210 | ; If destination already exists 211 | 212 | Push $0 213 | Push $1 214 | Push $2 215 | push $3 216 | 217 | GetFileTime "$R9" $0 $1 218 | GetFileTime "$R2" $2 $3 219 | 220 | IntCmp $0 $2 0 older newer 221 | IntCmp $1 $3 older older newer 222 | 223 | older: 224 | delete "$R9" 225 | goto time_check_done 226 | 227 | newer: 228 | delete "$R2" 229 | rename "$R9" "$R2" 230 | delete "$R9" ;incase above failed! 231 | 232 | time_check_done: 233 | 234 | Pop $3 235 | Pop $2 236 | Pop $1 237 | Pop $0 238 | 239 | done: 240 | 241 | Pop $R2 242 | 243 | Push $R1 244 | 245 | FunctionEnd 246 | 247 | Function .MoveFolder_Locate_deleteDir 248 | 249 | ${If} $R6 == "" 250 | RMDir $R9 251 | IntOp $R2 $R2 + 1 252 | ${EndIf} 253 | 254 | Push $R1 255 | 256 | FunctionEnd -------------------------------------------------------------------------------- /windows/dependencies.nsi: -------------------------------------------------------------------------------- 1 | ;--------------------------------------------------------------------- 2 | ; CUSTOM PAGE to DOWNLOAD REQUIRED DEPENDENCIES 3 | ; - Modified from ASCEND NSIS installer (http://www.ascend4.org/) 4 | 5 | Var CHECKPY 6 | Var CHECKGSTCOMSDK 7 | 8 | !macro setCheckboxChecked CB 9 | SendMessage ${CB} ${BM_SETCHECK} 0x0001 0 10 | Pop $0 11 | !macroend 12 | 13 | Function dependenciesCreate 14 | 15 | ${If} $HAVE_PYTHON == 'OK' 16 | ${AndIf} $HAVE_GSTCOMSDK == 'OK' 17 | ; do nothing in this page 18 | ${Else} 19 | nsDialogs::Create /NOUNLOAD 1018 20 | Pop $0 21 | 22 | ${NSD_CreateLabel} 0% 0 100% 48% "The following additional packages are required for Pithos to function correctly. Checked items will be downloaded and installed (some of the installers may require you to click 'next' a few times). If you already have the components installed you can unckeck them and they will not be downloaded. If any of these packages are not installed, Pithos will not work." 23 | Pop $0 24 | 25 | ${If} $HAVE_PYTHON == 'NOK' 26 | ${NSD_CreateCheckbox} 10% 48% 100% 8u "Python ${PYTHON_VERSION} (${PYTHON_FSIZE})" 27 | Pop $CHECKPY 28 | !insertmacro setCheckboxChecked $CHECKPY 29 | ${Else} 30 | ${NSD_CreateLabel} 10% 48% 100% 8u "--- Python ${PYTHON_VERSION} already installed" 31 | ${EndIf} 32 | 33 | ; GStreamer.com SDK 34 | 35 | ${If} $HAVE_GSTCOMSDK == 'NOK' 36 | ${NSD_CreateCheckbox} 10% 64% 100% 8u "GStreamer.com SDK ${GSTCOMSDK_VERSION} (${GSTCOMSDK_FSIZE})" 37 | Pop $CHECKGSTCOMSDK 38 | !insertmacro setCheckboxChecked $CHECKGSTCOMSDK 39 | ${Else} 40 | ${NSD_CreateLabel} 10% 64% 100% 8u "--- GStreamer.com SDK already installed" 41 | ${EndIf} 42 | 43 | nsDialogs::Show 44 | ${EndIf} 45 | 46 | FunctionEnd 47 | 48 | Function DependenciesLeave 49 | SendMessage $CHECKPY ${BM_GETCHECK} 0 0 $NEED_PYTHON 50 | SendMessage $CHECKGSTCOMSDK ${BM_GETCHECK} 0 0 $NEED_GSTCOMSDK 51 | FunctionEnd 52 | -------------------------------------------------------------------------------- /windows/detect.nsi: -------------------------------------------------------------------------------- 1 | ;--------------------------------------------------------------------- 2 | ; ROUTINES TO DETECT PYTHON, PYGTK, PYGOBJECT, PYCAIRO and TCL/TK. 3 | ; - Taken from ASCEND NSIS installer (http://www.ascend4.org/) 4 | 5 | ;--------------------------------------------------------------------- 6 | ; Look for Python in specific directory 7 | 8 | Function DetectPython 9 | 10 | ; TODO: Really, we should be supporting more than one version here, but 11 | ; given the python support from GST only supports 2.6... nope. 12 | 13 | ; TODO: Really, assuming that the python path is a fixed location is 14 | ; a bit broken, but this installer is really geared towards non-technical 15 | ; users anyways, so the chances of them having it installed in a 16 | ; non-default location is.. low, right? *hides from bugreports* 17 | 18 | ${If} ${FileExists} "${PYTHON_PATH}\python.exe" 19 | StrCpy $HAVE_PYTHON "OK" 20 | ${Else} 21 | ;MessageBox MB_OK "No python.exe in $R6" 22 | StrCpy $HAVE_PYTHON "NOK" 23 | ${EndIf} 24 | FunctionEnd 25 | 26 | ;-------------------------------------------------------------------- 27 | ; GStreamer.com SDK package detection 28 | 29 | Function DetectGstreamerComSDK 30 | ReadEnvStr $0 GSTREAMER_SDK_ROOT_X86 31 | ${IfNot} $0 == "" 32 | StrCpy $HAVE_GSTCOMSDK "OK" 33 | ${Else} 34 | StrCpy $HAVE_GSTCOMSDK "NOK" 35 | ${EndIf} 36 | 37 | ; TODO: What if they don't select the correct options? 38 | FunctionEnd 39 | 40 | -------------------------------------------------------------------------------- /windows/download.nsi: -------------------------------------------------------------------------------- 1 | ;--------------------------------------------------------------------- 2 | ; - Taken from ASCEND NSIS installer (http://www.ascend4.org/) 3 | 4 | Var DAI_RET 5 | Var DAI_MSG 6 | Var DAI_TMPFILE 7 | Var DAI_REMOVE 8 | 9 | !macro downloadAndInstall DAI_NAME DAI_URL DAI_FN DAI_CMD 10 | Push $0 11 | Push $1 12 | 13 | StrCpy $DAI_RET "" 14 | StrCpy $DAI_REMOVE "" 15 | 16 | ${If} ${FileExists} "${DAI_FN}" 17 | DetailPrint "Found local file ${DAI_FN}..." 18 | ${If} ${Cmd} `MessageBox MB_ICONQUESTION|MB_YESNO "File ${DAI_FN} was found in the current directory, so it may not be necessary to download it now.$\n$\nWould you like to run this local copy of the installer?" IDYES ` 19 | StrCpy $DAI_RET "success" 20 | StrCpy $DAI_TMPFILE "${DAI_FN}" 21 | ${EndIf} 22 | ${EndIf} 23 | 24 | ${If} $DAI_RET != "success" 25 | DetailPrint "Downloading file ${DAI_FN}..." 26 | DetailPrint "URL: ${DAI_URL}" 27 | StrCpy $DAI_TMPFILE "$TEMP\${DAI_FN}" 28 | 29 | ; Download files using the INETC plugin for NSIS, available from 30 | ; http://nsis.sourceforge.net/Inetc_plug-in 31 | inetc::get /CAPTION "${DAI_FN}" /USERAGENT "Mozilla/5.0 NSIS_Inetc (Pithos Installer)" "${DAI_URL}" "$DAI_TMPFILE" /END 32 | Pop $DAI_RET ; return value = exit code, "OK" means OK 33 | 34 | ${DoWhile} $DAI_RET != "OK" 35 | ${If} $DAI_RET == "cancel" 36 | StrCpy $DAI_MSG "cancelled" 37 | ${Else} 38 | StrCpy $DAI_MSG "failed (return '$DAI_RET')" 39 | ${EndIf} 40 | 41 | DetailPrint "Download of ${DAI_FN} $DAI_MSG." 42 | ${IfNot} ${Cmd} `MessageBox MB_ICONEXCLAMATION|MB_YESNO "${DAI_NAME} download $DAI_MSG. URL was:$\n$\n${DAI_URL}$\n$\nDo you wish to re-attempt the download?" IDYES ` 43 | ; response was no 44 | ;MessageBox MB_OK "File ${DAI_NAME} will not be installed..." 45 | Pop $1 46 | Pop $0 47 | Push 1 ; error code 48 | Return 49 | ${EndIf} 50 | 51 | ;MessageBox MB_OK "Will re-attempt download of ${DAI_NAME}" 52 | ${If} ${FileExists} "$DAI_TMPFILE" 53 | Delete "$DAI_TMPFILE" 54 | ${EndIf} 55 | ${Loop} 56 | 57 | StrCpy $DAI_REMOVE "1" 58 | ${EndIf} 59 | 60 | 61 | ;MessageBox MB_OK "Installing ${DAI_NAME}...$\n$\nCommand: ${DAI_CMD}" 62 | DetailPrint "Installing ${DAI_NAME} (${DAI_FN})" 63 | ExecWait "${DAI_CMD}" $0 64 | DetailPrint "Installer return code = $0" 65 | ${If} $0 != "0" 66 | MessageBox MB_ICONEXCLAMATION|MB_OK "${DAI_NAME} installer returned a non-zero error code '$0'" 67 | Pop $1 68 | Pop $0 69 | Push 1 ; error code 70 | ${Else} 71 | Pop $1 72 | Pop $0 73 | Push 0 ; error code 74 | ${EndIf} 75 | 76 | ${If} $DAI_REMOVE != "" 77 | ;MessageBox MB_OK "Deleting $DAI_TMPFILE..." 78 | Delete "$DAI_TMPFILE" 79 | ${EndIf} 80 | 81 | !macroend -------------------------------------------------------------------------------- /windows/install.nsi: -------------------------------------------------------------------------------- 1 | ;Pithos for Windows installer script 2 | ;Modified by TingPing 3 | ;Based on Exaile Windows installer script 4 | ;Modified by Dustin Spicuzza 5 | ;Based on the Quod Libet / Ex Falso Windows installer script 6 | ;Modified by Steven Robertson 7 | ;Based on the NSIS Modern User Interface Start Menu Folder Example Script 8 | ;Written by Joost Verburg 9 | 10 | ;compression 11 | SetCompressor /SOLID LZMA 12 | 13 | !define MULTIUSER_EXECUTIONLEVEL Highest 14 | !define MULTIUSER_MUI 15 | !define MULTIUSER_INSTALLMODE_COMMANDLINE 16 | 17 | !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\Pithos" 18 | !define INSTDIR_KEY "Software\Pithos" 19 | !define INSTDIR_SUBKEY "InstDir" 20 | 21 | ;-------------------------------- 22 | ;Include Modern UI and other libs 23 | 24 | !include "MUI2.nsh" 25 | !include "LogicLib.nsh" 26 | 27 | ;-------------------------------- 28 | ;General 29 | 30 | ;Name and file 31 | Name "Pithos" 32 | OutFile "pithos_installer.exe" 33 | 34 | ;Default installation folder 35 | InstallDir "$PROGRAMFILES\Pithos" 36 | 37 | ;Get installation folder from registry if available 38 | ;InstallDirRegKey HKCU "${INSTDIR_KEY}" "" 39 | ;doesn't work with multi user -> see onInit.. 40 | 41 | ;Request application privileges for Windows Vista+ 42 | RequestExecutionLevel admin 43 | 44 | ;-------------------------------- 45 | ;Variables 46 | 47 | Var StartMenuFolder 48 | Var instdir_temp 49 | 50 | Var HAVE_PYTHON 51 | Var HAVE_GSTCOMSDK 52 | 53 | Var NEED_PYTHON 54 | Var NEED_GSTCOMSDK 55 | 56 | 57 | ;-------------------------------- 58 | ;Interface Settings 59 | 60 | !define MUI_ABORTWARNING 61 | !define MUI_ICON "..\data\icons\pithos-small.ico" 62 | 63 | ;-------------------------------- 64 | ;Pages 65 | 66 | !insertmacro MUI_PAGE_LICENSE "..\LICENSE" 67 | !insertmacro MUI_PAGE_DIRECTORY 68 | 69 | Page custom dependenciesCreate dependenciesLeave 70 | 71 | ;Start Menu Folder Page Configuration 72 | !define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKCU" 73 | !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\Pithos" 74 | !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder" 75 | 76 | !insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder 77 | 78 | !insertmacro MUI_PAGE_INSTFILES 79 | 80 | !insertmacro MUI_UNPAGE_CONFIRM 81 | !insertmacro MUI_UNPAGE_INSTFILES 82 | 83 | ;------------------------------------------------------------ 84 | ; DOWNLOAD AND INSTALL DEPENDENCIES FIRST 85 | ;!define TEST_URL "" 86 | 87 | ; Use the official python.org Python packages 88 | !define PYTHON_VERSION "2.7" 89 | !define PYTHON_FULL_VERSION "2.7.3" 90 | !define PYTHON_PATH "C:\Python27" 91 | !define PYTHON_FN "python-${PYTHON_FULL_VERSION}.msi" 92 | !define PYTHON_FSIZE "15MB" 93 | !define PYTHON_URL "http://python.org/ftp/python/${PYTHON_FULL_VERSION}/${PYTHON_FN}" 94 | ;!define PYTHON_URL "${TEST_URL}/${PYTHON_FN}" 95 | !define PYTHON_CMD "msiexec /i $DAI_TMPFILE /passive ALLUSERS=1" 96 | 97 | ; Use the GStreamer.com SDK 98 | !define GSTCOMSDK_VERSION "2012.9" 99 | !define GSTCOMSDK_FN "gstreamer-sdk-x86-${GSTCOMSDK_VERSION}.msi" 100 | !define GSTCOMSDK_FSIZE "97MB" 101 | !define GSTCOMSDK_URL "http://www.freedesktop.org/software/gstreamer-sdk/data/packages/windows/x86/${GSTCOMSDK_FN}" 102 | ;!define GSTCOMSDK_URL "${TEST_URL}/${GSTCOMSDK_FN}" 103 | !define GSTCOMSDK_FEATURES "_gstreamer_core,_gstreamer_system,_gstreamer_playback,_gstreamer_codecs,_gstreamer_networking,_gstreamer_python,_gtk__2.0,_gtk__2.0_python,_gstreamer_codecs_restricted,_gstreamer_networking_restricted" 104 | !define GSTCOMSDK_CMD "msiexec /i $DAI_TMPFILE /passive ALLUSERS=1 ADDLOCAL=${GSTCOMSDK_FEATURES}" 105 | 106 | !include "download.nsi" 107 | 108 | Section "-python" 109 | ${If} $NEED_PYTHON == '1' 110 | DetailPrint "--- DOWNLOAD PYTHON ---" 111 | !insertmacro downloadAndInstall "Python" "${PYTHON_URL}" "${PYTHON_FN}" "${PYTHON_CMD}" 112 | Call DetectPython 113 | ${If} $HAVE_PYTHON == 'NOK' 114 | MessageBox MB_OK "Python installation appears to have failed. You may need to retry manually." 115 | ${EndIf} 116 | ${EndIf} 117 | SectionEnd 118 | 119 | Section "-gstcomsdk" 120 | ${If} $NEED_GSTCOMSDK == '1' 121 | DetailPrint "--- DOWNLOAD GSTREAMER.COM SDK ---" 122 | !insertmacro downloadAndInstall "GStreamer.com SDK" "${GSTCOMSDK_URL}" "${GSTCOMSDK_FN}" "${GSTCOMSDK_CMD}" 123 | Pop $0 124 | ${If} $0 != "0" 125 | MessageBox MB_OK "GStreamer.com SDK installation appears to have failed. You may need to retry manually." 126 | ${EndIf} 127 | ${EndIf} 128 | 129 | SectionEnd 130 | 131 | ;------------------------------------------------------------ 132 | ; Install Pithos last 133 | 134 | Section "-Pithos" SecPithos 135 | 136 | SetOutPath "$INSTDIR" 137 | 138 | File /r "..\data" 139 | File /r "..\pithos" 140 | File /r "..\pithos.pyw" 141 | File /r "..\pithos.bat" 142 | 143 | ;Store installation folder 144 | WriteRegStr SHCTX "${INSTDIR_KEY}" "${INSTDIR_SUBKEY}" $INSTDIR 145 | 146 | ;Multi user uninstaller stuff 147 | ;WriteRegStr SHCTX "${UNINST_KEY}" \ 148 | ;"DisplayName" "Pithos - Pandora desktop client." 149 | ;WriteRegStr SHCTX "${UNINST_KEY}" "DisplayIcon" "$\"$INSTDIR\data\icons\pithos-small.ico$\"" 150 | ;WriteRegStr SHCTX "${UNINST_KEY}" "UninstallString" \ 151 | ;"$\"$INSTDIR\uninstall.exe$\" /$MultiUser.InstallMode" 152 | ;WriteRegStr SHCTX "${UNINST_KEY}" "QuietUninstallString" \ 153 | ;"$\"$INSTDIR\uninstall.exe$\" /$MultiUser.InstallMode /S" 154 | 155 | ;Create uninstaller 156 | WriteUninstaller "$INSTDIR\uninstall.exe" 157 | 158 | !insertmacro MUI_STARTMENU_WRITE_BEGIN Application 159 | 160 | ;Create shortcuts 161 | CreateDirectory "$SMPROGRAMS\$StartMenuFolder" 162 | CreateShortCut "$SMPROGRAMS\$StartMenuFolder\Pithos.lnk" "$INSTDIR\pithos.bat" "" "$INSTDIR\data\icons\pithos-large.ico" 163 | 164 | !insertmacro MUI_STARTMENU_WRITE_END 165 | 166 | SectionEnd 167 | 168 | 169 | !include "dependencies.nsi" 170 | 171 | !include "detect.nsi" 172 | 173 | ;-------------------------------- 174 | ;Uninstaller Section 175 | 176 | Section "Uninstall" 177 | 178 | RMDir /r "$INSTDIR" 179 | 180 | Delete "$INSTDIR\uninstall.exe" 181 | 182 | !insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuFolder 183 | 184 | Delete "$SMPROGRAMS\$StartMenuFolder\Pithos.lnk" 185 | RMDir "$SMPROGRAMS\$StartMenuFolder" 186 | 187 | DeleteRegKey SHCTX "${UNINST_KEY}" 188 | DeleteRegKey SHCTX "${INSTDIR_KEY}" 189 | 190 | SectionEnd 191 | 192 | -------------------------------------------------------------------------------- /windows/redist.txt: -------------------------------------------------------------------------------- 1 | -- for plugins (mediakeys, growl) -- 2 | http://sourceforge.net/projects/pyhook/files/ 3 | http://sourceforge.net/projects/pywin32/files/pywin32/ 4 | https://github.com/kfdm/gntp/ 5 | --------------------------------------------------------------------------------