PyTone-3.0.3/000755 000765 000765 00000000000 11406223507 013243 5ustar00ringoringo000000 000000 PyTone-3.0.3/._AUTHORS000644 000765 000765 00000000122 10266220577 014532 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/AUTHORS000644 000765 000765 00000000034 10266220577 014317 0ustar00ringoringo000000 000000 Jörg Lehmann PyTone-3.0.3/ChangeLog000644 000765 000765 00000241237 11406223457 015032 0ustar00ringoringo000000 000000 2010-06-16 Jörg Lehmann * Version 3.0.3 released. 2010-02-27 Jörg Lehmann * Do not hide windows in focuschanged events in order to not trigger further focuschanged events. 2009-10-11 Jörg Lehmann * Internal mixer implemented. 2009-09-15 Jörg Lehmann * services/songdbs/sqlite.py: Delete song from database when querying it and it is no longer present on disk. 2009-08-23 Jörg Lehmann * conf/pytonerc: Add notify-send as an example for the song change command which seems to be much more common nowadays (thanks to Michael Hartmann for pointing this out). 2009-08-23 Jörg Lehmann * mixerwin.py: Encode unicode string (thanks to Michael Hartmann for reporting this issue). 2009-08-22 Jörg Lehmann * Create .pytone directory earlier in the initilization phase (thanks to Jean Jordaan for pointing this out). 2009-08-12 Jörg Lehmann * filelist.py: Use updatedir() instead of readdir() when deleting songs to prevent that the current selection gets lost. * playerwin.py: Fix error when trying to rate the currently playing * song while there is no such one. 2009-05-31 Jörg Lehmann * Version 3.0.2 released. 2009-03-04 Jörg Lehmann * services/songdbs/sqlite.py: Do not force metadata reread when querying songs (as suggested by Tom Diedrich ). * services/songdbs/sqlite.py: Raise DenyRequest in autoregisterer_queryregistersong method if request is not addressed at this database (thanks to Tom Diedrich for the patch). 2008-12-13 Jörg Lehmann * eventy.py: Fix a couple of __repr__ methods (fixing the skip song functionality). * config.py, pytone.py: Make switching on of mouse support a config option (as suggested by Dominik Bruhn ). 2008-11-27 Jörg Lehmann * pytonectl.py: Encode output (Thanks to Dominik Bruhn for the patch). 2008-08-16 Jörg Lehmann * services/player.py, services/players/*.py, services/playlist.py: Make sending of song_played and song_skipped events more local. * statswin.py: Encode output. * Use %r in __repr__ and replace __str__ by __repr__ at various places fixing an error with Python 2.5 and above, reported by Eliot Blennerhassett . 2008-08-07 Jörg Lehmann * config.py: Also allow "alsa05" driver. * bufferedao.c: Adapt to PEP 353 (Thanks to Philipp Jocham for the patch). * services/players/mpg123.py: Make player working again after previous internal API changes (Thanks to Philipp Jocham for the patch). 2008-07-16 Jörg Lehmann * config.py: Allow "pulse" in driver arguments for players (Thanks to Eliot Blennerhassett ). 2008-02-18 Jörg Lehmann * src/encoding.py: Add missing import (Thanks to Philipp Jocham for the patch). 2008-02-18 Jörg Lehmann * services/songdbs/sqlite.py: Fix a bug in the _song_update function (Thanks to Philipp Jocham for the patch). * log.py: Open debug file in utf-8 encoding (Thanks to Philipp Jocham for the patch). 2008-01-26 Jörg Lehmann * metadata.py: Fix a bug in the Ogg Vorbis replaygain handling (Thanks to Philipp Jocham for the patch). 2007-08-15 Jörg Lehmann * services/songdb.py: Request songs from database with id == "main" instead of None to prevent a lockup (thanks to Kurt Gysin for reporting this problem). 2007-08-14 Jörg Lehmann * Version 3.0.1 released. * mainscreen.py: Reenable lyrics display. 2007-08-13 Jörg Lehmann * item.py: Implement played songs filter, which shows a virtual directory of all songs which have been played at least once. * filelistwin.py, filelist.py, item.py, config.py: Implement focus on search string, which displays a virtual directory of entries matching the given search string (i.e., artist/album/title being similar to this searchs string) 2007-08-12 Jörg Lehmann * services/songdbs/sqlite.py: Implement clearstats. 2007-08-02 Jörg Lehmann * item.py: Fix tag filtering. 2007-08-01 Jörg Lehmann * Version 3.0.0 released. * network.py, pytonectl.py, item.py: Make pytonectl work again, in particular update allowed modules for unpickling and prevent metadata fetches during unpickling. * item.py: Implement podcast and deleted songs filtering * config.py: New keybindings for (un)deletion of songs 2007-03-11 Jörg Lehmann * services/songdb.py: Introduce upper cutoff for automatic rating according to times played minus skipped. 2007-03-10 Jörg Lehmann * config.py: Remove tags_* options and replace them by postprocessors lists * metadata.py: Add postprocessors for metadata (including registry to allow plugins to register their own postprocessors) and move old tags_* code and decade adding code to the new system 2007-01-21 Jörg Lehmann * config.py: Fix check for local song databases (thanks to Alexander Wirt for reporting this bug). 2006-09-04 Jörg Lehmann * Use platform byte order for ao devices. 2006-08-28 Jörg Lehmann * Merge changes from Rothsee version: - Set correct version for new databases - Output correct versions during db upgrade 2006-08-20 Jörg Lehmann * Database version 6: Store bitrate, samplerate, vbr, size, tracknumber, trackcount, disknumber, diskccount, and replaygain information in database. * item.py: Display song information as suggested by Dag Wieers . * metadata.py: Add replaygain support to eyeD3 decoder * services/players/internal.py: Take disk number into account. 2006-08-15 Jörg Lehmann * metadata.py: Use mutagen as default ID3 reader from MP3 files. * metadata.py, dbitem.py: Store replaygain information and calculate replaygain scale factor. * pcm.c: Add function which allows scaling of the buffer by a factor. * decoder.py: Store replaygain scale factor * services/players/internal.py: Use track gain for replaygain information. 2006-08-12 Jörg Lehmann * Version 2.3.1 released. 2006-08-08 Jörg Lehmann * Make keybindings for rating of currently playing songs configurable via playerratecurrentsong1, ... playerratecurrentsong5 in the keybindings.general section of the pytonerc file (thanks to Alexander Wirt for suggesting this). 2005-12-05 Jörg Lehmann * services/player.py: Start player only after the playlist service has been started since playlist requests may already be issued during the player startup. This fixes an long-standing problem reported by several people on the mailing list. 2005-11-07 Jörg Lehmann * filelist.py: Update top after changing back into a directory as the window size may have changed in the meantime (thanks to Martin Fenelon for reporting this problem). 2005-09-19 Jörg Lehmann * mainscreen.py: Call correct getmaxyx method fixing problem with small terminals during startup (thanks to Dag Wieers for reporting this problem). * services/player.py: Move closing of audiodevice right before blocking process() call (hopefully fixing a problem reported by Dag Wieers ). 2005-09-13 Jörg Lehmann * Version 2.3.0 released. 2005-09-11 Jörg Lehmann * plugings/osdtitle.py, plugins/termtitle.py: Apply patch by Dag Wieers which adds checking for no song being played. 2005-09-08 Jörg Lehmann * services/songdbs/local.py: Rewrite db upgrade code to allow non-atomic upgrade, as well. Prompt the user before an upgrade to ask whether a non-atomic or atomic upgrade has to be performed. 2005-08-24 Jörg Lehmann * Check earlier for dbfile != "db". * item.py: Fix filters handling in songs class. 2005-08-23 Jörg Lehmann * Add error reporting to bufferedao extension module. 2005-08-22 Jörg Lehmann * Disabled automatic closing of help window by default. * Added changeable play speed support (many thanks to Richard A. Smith for providing the patch). 2005-08-21 Jörg Lehmann * Merged item-rework branch, which contains a new filter code. The new code makes the unfiltered case a special case of the filtered one. As a result, more than one filter can be applied, e.g., filtering for all songs in a certain genre of a given decade and with a given rating is now possible. Furthermore, every index now contains a (nearly) full directory hierarchy, thus allowing things like showing the top-played songs of a certain genre. 2005-08-19 Jörg Lehmann * Get rid of multi-file database layout: - convert old layout automatically - leave configuration file switches basename and dbfile intact but warn the user when they are non-empty - in a next step, we will set the default value of the basename switch to an empty value (when presumably all databases have been converted) - finally as a final step, we remove the switches and require that the users change their config files * Database version 5: do not longer index by years but by decades which removes many special code paths at various places in the db code. 2005-08-11 Jörg Lehmann * bufferedao.c: New C extension module corresponding to services/players/interal/buffereddevice with an ao device. The advantage of the C module is that it does not have to rely on holding the GIL when doing the output, which should help preventing sound dropouts. * services/players/internal.py: Make use of new extension module bufferedao. 2005-08-10 Jörg Lehmann * services/songdbs/internal.py: Checkpoint database regularly during registering of songs to prevent oversized transaction logs. * services/songdbs/internal.py: Replace quadratic in number of songs algorithm by a linear in time version to avoid huge system loads at the end of the song registering process. * services/players/internal.py: Do not open audiodevice without need. 2005-08-09 Jörg Lehmann * help.py, helpwin.py, statusbar.py: Introduced getkeyname function which has a fallback solution for unnamed keys (thanks to Troels Vognsen for reporting this problem). 2005-08-03 Jörg Lehmann * Normalize all paths in config files. * Rework dbstats system. 2005-08-02 Jörg Lehmann * services/songdb.py. Measure cache size not by number of stored requests but by the number of objects the requests refer to. * services/songdb.py: rename resultcache -> requestcache. * conf/pytonerc, config.py, services/songdb.py: Add new section database with currently only one option requestcachesize, which allows one to configure the maximal number of objects contained in the cache. * item.py: Create genres/ratings/etc. instances only once to permit the caching of the requests involving them. * statswin.py, config.py, conf/pytonerc: New window for statistical information about databases and the request cache (accessible via "%"). * Various updates in German PyTone.po. 2005-07-16 Jörg Lehmann * plugin.py: Use thread-local channel in threadedplugin to make it actually a threaded plugin. * log.py: Add time and current thread to debugging output and prune common path from module name. 2005-07-16 Jörg Lehmann * filelist.py. Update window contents in filelistjumptosong(). * plugin.py. Daemonize threaded plugins, in order to prevent a blocking of misbehaved plugins on shutdown. * Initial checkin into Subversion repository. 2005-06-30 Jörg Lehmann * plugin.py: Use init() method instead of start(), which has a different meaning for threads. The plugins currently distributed with PyTone have been changed in this regard. * plugins/audioscrobbler/scrobbler.py: Fix typos in backlog code. 2005-06-28 Jörg Lehmann * config.py: Be more liberal concerning the accepted section names (patch by Dag Wieers ). 2005-06-20 Jörg Lehmann * Version 2.2.4 released. * window.py: Include a workaround by Dag Wieers which prevents flickering when switching between the filelist and database windows. * plugins: Include audioscrobbler plugin contributed by Nicolas Évrard ). * config.py: Rename listen to bind in network section. 2005-06-06 Jörg Lehmann * Rewrite plugin loader to use imp module: Use imp module to specify plugin search path and log errors instead of aborting * mainscreen.py: Use getmaxyx() code provided by Dag Wieers . 2005-06-04 Jörg Lehmann * events.py: Renamed attribute songs -> items in playlistchanged event. 2005-05-28 Jörg Lehmann * item.py: Import _genrandomchoice function needed for random selections of filesystem directories from services.songdb, where it was moved to some time ago (thanks to Dag Wieers for reporting this issue). 2005-05-22 Jörg Lehmann * mainscreen.py: Force minimal height and width of window. 2005-05-18 Jörg Lehmann * iteminfowin.py: Fix some small bugs in the songchanged event handler. * iteminfowin.py: Allow toggeling between different states: show information about either the currently selected song or the song being played on the configured players. * config.py: Add new option toggleiteminfowindow in keybindings.general section. 2005-05-17 Jörg Lehmann * requests.py, services/songdb.py: Handle getlastplayedsongs as other requests by just supplying another wrapper function. * playerwin.py: Do not close playerinfo file every time (thanks to Dag Wieers for the patch). * item.py: Zero-pad seconds (thanks to Dag Wieers for the patch). * Add xterm plugin contributed by , which sets the title of an xterm window according to the currently playing song. 2005-05-11 Jörg Lehmann * playlistwin.py: Check for playstarttime not being None before referencing it (thanks to Stefan Wimmer for reporting this problem). * services/playlist.py: Make immediate play of song working again (thanks to Dag Wieers for reporting this problem). 2005-05-10 Jörg Lehmann * config.py: replace all occurrences of server/port by networklocation * Make remote databases work again. * Remote players should at least partially work again. 2005-04-27 Jörg Lehmann * Version 2.2.3 released. 2005-04-26 Jörg Lehmann * Deactivate playlist window before initializing the playlist to prevent issuing a wrong selectionchanged event (fixing a problem reported by Stuart Pook ). 2005-04-25 Jörg Lehmann * services/players/internal.py: Prevent sound device from being reopened when an already stopped player is being stopped again (thanks to Stuart Pook for pointing out this problem). * services/players/internal.py: Daemonize bufferedaudiodevice again. 2005-04-21 Jörg Lehmann * network.py: Ignore empty lines in _receiveobject instead of reporting an error. * services/playlist.py: Make going back to previous song work again (thanks to Johannes Segitz for reporting this bug). 2005-04-20 Jörg Lehmann * Ignore invalid database definitions unless there is at least one valid database. 2005-03-30 Jörg Lehmann * iteminfowin.py: Fix secondary player for songs in the playlist window (thanks to Stefan Wimmer for reporting this problem). 2005-03-23 Jörg Lehmann * iteminfowin.py, item.py: Fix wrong window size of item info window. * slist.py: Fix crash occuring when deleting the contents of the playlist (thanks to Stefan Wimmer for reporting this problem). 2005-03-16 Jörg Lehmann * Version 2.2.2 released. 2005-03-12 Jörg Lehmann * services/players/mpg123.py: Implement seeking. 2005-03-08 Jörg Lehmann * events.py: Make checkpointdb a dbevent in order to let the songdbmanager pass it to the databases (many thanks to Tomas Menzl for pointing out this problem). * services/songdbs/local.py: Send sendeventin event to global hub instead of to songdbhub to make it actually work. Use repeat interval instead of sending every time a new sendeventin event. * item.py: Add new item method getid, which returns a unique id for the item in the current context (used in slist.set method) * services/playlist.py: Add getid method to playlistitem (for use in slist.set method). * slist.py: Use getid method to identify the previously selected item in set method. * services/playlist.py: Cleanup variable names: song -> item. 2005-03-07 Jörg Lehmann * services/player.py: Process incoming events twice to be able to rely on block argument of channel. Also block when waiting for a new song to appear in the playlist, since any arbitrary events unblocks the player. * services/songdbs/local.py: Use active transaction as an indication for a busy db. * filelist.py: Also react on new playlists. 2005-03-06 Jörg Lehmann * services/songdbs/local.py: Delete log files when checkpointing dbenv preventing huge disk space usage during song registering (Ochsenschlegel bugfix). 2005-03-03 Jörg Lehmann * services/players/internal.py. Also request new song when not crossfading to make autoplay == False work in this case (thanks to Tomas Menzl for providing a patch). * events, services/playlist.py: New event playlistplaysong which allows one to tell the playlist to immediately play a specific song. * playlist.py: Use playlistplaysong instead of playlistaddsongstop when the user requests the immediate playback of a song in the playlist (as suggested by Tomas Menzl ), leading to a more mainstream behaviour of PyTone in that regard. ;-) * playlist.py: playlistaddsongstop now correctly updates the information about the currently playing song. * playlist.py: Keep information about played songs after restarting PyTone. * helper.py: Write exception to stdout since we have closed stderr. * services/timer.py: Do not daemonize and switch to new timeout option of channel.process. * services/players/internal.py: Do not daemonize bufferedaudiodev buf shut it down properly. 2005-03-01 Jörg Lehmann * hub.py: Add optional timeout to process method of channel. * services/player.py: Make use of new timeout option. * event.py: Renamed playerforward -> playernext * decoder.py, services/player.py, services/players/internal.py: Enable seeking in songs (many thanks to Tomas Menzl for providing a patch). * requests.py: Add sort member variable to getsongsinplaylists request, fixing a crash when rescanning, querying a random selection, etc. of all songs in playlists (thanks to Tomas Menzl for reporting this problem). 2005-02-27 Jörg Lehmann * filelist.py: Some cleanups. * playlist.py: Ditto. * events.py: New event filelistjumptosong, which directs the filelist to jump to a given song in the directory hierarchy. * config.py. New keybinding "filelistjumptoselectedsong" for playlistwindow (by default KEY_RIGHT is used). * config.py: New option "skipsinglealbums" in filelistwindow section, which when turned on, tells PyTone to skip the album and go directly to the songs when there is only one album of a given artist. 2005-02-26 Jörg Lehmann * dbitem.py: Remove __class__ comparison from __cmp__ method of dbitem class * services/songdb.py: Avoid repeated songs when randomly selecting songs out of a short list. 2005-02-12 Jörg Lehmann * services/playlist.py: Cleanup and fix some minor bugs. 2005-02-09 Jörg Lehmann * setup.py: (Ab-)use scripts directive for installing pytone and pytonectl shell scripts. * log.py: Make debugfile initialization manual to allow the use of the log module in the config module (thanks to Brian Lenihan for proposing this). * config.py: Fix --rebuild command line switch. * services/players/mpg123.py: Be more relaxed when initializing the player and when parsing the "@F" lines to make mpg123 work again (thanks to Brian Lenihan for providing a patch). * conf/pytonerc: Add sample command line for mpg123 player. * dbitem.py: Do not assume that all exceptions derive from the Exception base class (fixing the scanning problems reported by Alexander Wirt and Jack Bakeman ). 2005-02-08 Jörg Lehmann * config.py: Use correct copyright date when printing usage summary. 2005-02-07 Jörg Lehmann * Version 2.2.1 released. * services/playlist.py: Also notify database of not fully played song when stopping the player manually. * services/songdb.py: Improve random choice logic. * services/songdb.py: Store copy of request and not request itself in cache. * config.py: Add more checks for database options: prevent sharing of basenames or dbenvdirs of several databases. 2005-02-06 Jörg Lehmann * log.py: New function debug_traceback which records a traceback when in debugging mode. * dbitem.py: Ignore (but report when in debugging mode) errors when parsing the song metadata (thanks to Jack Bakeman , Sascha , and Alexander Wirt for reporting that problem). * metadata.py: fix fallback code for length calculation. 2005-02-03 Jörg Lehmann * service.py: New module containing a base class for services which contains a main loop doing exception handling and error reporting. * plugin.py: Use new service class. * services/songdb.py: Use new service class. * services/songdbs/local.py: Use new service class. * services/player.py: Use new service class. * services/playlist.py: Use new service class. 2005-02-01 Jörg Lehmann * Version 2.2.0 released. 2005-01-25 Jörg Lehmann * filelistwin.py: Clear searchstring when search has been aborted by pressing ESC. 2005-01-24 Jörg Lehmann * metadata.py: Also import MP3Info module when using the eyeD3 module, since one helper function is also needed when using the latter module. 2005-01-23 Jörg Lehmann * services/songdb.py: Do not sort the intermediate results when querying multiple databases. While this yields an improved caching behaviour, we are not allowed to do that, because the caller supplied compare function cannot be used for unwrapped items. * pytone.py: Redirect stdout to /dev/null in order to prevent ALSA buffer underrun messages from spoiling the user interface. 2005-01-22 Jörg Lehmann * item.py: Remove bogus cmpitem method of rating class. * metadata.py: New module containing the song metadata interfaces. * Add _some_ initial support for FLAC files via pyflac (not stable yet and thus currently unsupported!) 2005-01-17 Jörg Lehmann * pcm/pcm.c: add new function upsample which allows to create a pseudo-stereo stream from a mono stream * decoder.py: Handle mono ogg files correctly (fixing an error reported by Uwe Bielz ). * item.py: Be more robust when displaying playing time of song. 2005-01-16 Jörg Lehmann * events.py: registerplaylists now expects lists of playlists instead of list of paths (analogous to registersongs). * events.py, services/songdbs/local.py. new events delplaylist and updateplaylist for deletion and update of playlists * services/songdbs/local.py: Also update (and delete non longer existing) playlists when reregistering all songs (as suggested by Stefan Wimmer ). 2005-01-09 Jörg Lehmann * Plugin architecture (based upon the prototype by Nicolas Évrard ). 2004-12-31 Jörg Lehmann * item.py: Case-insensitive sort in filelist window. 2004-12-19 Jörg Lehmann * Include path of song in detailed information about currently selected item. * services/players/internal.py: Flush song queue and audio device when a new song is requested while the player is paused. 2004-12-12 Jörg Lehmann * config.py: Add support for config subsection templates. It is now possible to define an arbitrary number of databases. 2004-11-29 Jörg Lehmann * Version 2.1.3 released. * item.py: Replace superfluous database requests, which led to an unnecessarily sluggish UI during the database rebuild. * filelist.py: Ensure that only dbitem.song and not item.song instances are sent to the database when rescanning songs. Also send songs to the correct database (if the selected directory contains songs from various databases). 2004-11-27 Jörg Lehmann * Add French translation (many thanks to Nicolas Évrard ). * services/playlist.py: Convert songs to playlistitems, when loading a playlist. Fix appending of these items, as well. (Thanks to Stefan Wimmer for reporting this bug). * change all helper.debug calls to log.debug and move the corresponding debug file code into the log module. 2004-11-24 Jörg Lehmann * item.py: Reduce precision for longer last played times. 2004-11-20 Jörg Lehmann * hub.py: renamed hub-> _defaulthub, _hub -> hub. * hub.py: Add module level functions notify, request and newchannel to provide easy access to the default hub. * Replace all calls of hub methods with new hub module functions making the code more readable. 2004-11-18 Jörg Lehmann * services/songdbs/local.py: Make playlists work again (thanks to Maurizio Panniello for reporting and locating this problem). 2004-11-15 Jörg Lehmann * Renamed services/players/xmms.py in services/players/xmmsplayer.py to prevent a name conflict with the pyxmms module. * services/players/xmmsplayer.py: Replace xmms by xmms.control to make this module work with the latest versions of pyxmms (thanks to Mario Rodríguez for reporting this problem). * services/songdbs/local.py: When reregistering a song which is already in the database, do not replace it completely (thereby deleting its rating, etc.) but only update the song metadata. (closing Debian bug #269711). 2004-11-08 Jörg Lehmann * Version 2.1.2 released. * pytonerc: Fix typo in addsongtoplaylist and showiteminfolong options (thanks to Toma¸ Ficko for reporting these bugs). * services/songdbs/local.py: Fix error occuring when requesting artists, albums or songs with unknown decade (thanks to Toma¸ Ficko for sending a patch). * services/songdbs/local.py: Manually remove unneeded log files because automatic removal is only supported by the newest bsddb versions, fixing Debian bug #273370 (thanks to Toma¸ Ficko for sending a patch). 2004-11-07 Jörg Lehmann * Version 2.1.1 released. * services/songdbs/local.py: Enable the songautoregisterer to do the rescanning of songs by itself in order to take load of the database thread. * services/songdbs/local.py: When registering songs, also update information of song which have alread been in database. In particular, delete songs which are no longer existent (Implementing a suggestion by some anonymous user). * filelist.py: Call the song registerer when updating the songs in the basedir of a database to prevent blocking of the UI thread. 2004-09-25 Jörg Lehmann * item.py: Fix calculating of hours and minutes of last played time (thanks to Zoltan Szalontai for finding this bug). 2004-09-12 Jörg Lehmann * dbitem.py: Update path information when rescanning song. When updating the songs in a database, a relocation in a different musicbasedir is now taken into amount correctly. 2004-08-20 Jörg Lehmann * Never issue a requestnextsong request for second player. (Fixing a problem occured during the last Rothsee-Party). * Associate a playlist (or explicitly none) to every player. * requests.py: requestnextsong now requires a playlistid instead of a playerid. * services/songdbs/local.py: Checkpoint database after each database update step to prevent even more oversized log files. * dbitem.py: Only store last 10 playing times. 2004-08-16 Jörg Lehmann * services/songdbs/local.py: Treat case of musicbasedir ending with a slash correctly (fixing a problem reported by Alexander Wirt ). 2004-08-11 Jörg Lehmann * services/playlist.py: Fix wrong playlistitem constructor signature in playlistaddsongtop (thanks to Andreas Poisel for reporting this bug). 2004-08-09 Jörg Lehmann * Also accept "MP3" etc. as extension of MP3 files. * New window which show more detailed information about currently selected item. 2004-08-03 Jörg Lehmann * Version 2.1.0 released. * dbitem.py: Really use length of MP3 file if no ID3 tag is present (when using eyeD3 module). * services/playlist.py: Reset playingsong to None only using the playbackinfochanged event. * services/playlist.py: React on changes in the song database. 2004-08-02 Jörg Lehmann * dbitem.py: Try harder to get reasonable length information for MP3 file. * dbitem.py: Add adddict and safe options to format method of song. * dbitem.py: Store list of last played times. * Consider song as not having been played if it has been aborted very early (currently during the first 10 seconds) (closing Debian bug #218283). * playlist.py: Do not set playingsong by using the playbackinfochanged event of the player since this leads to race conditions. 2004-07-27 Jörg Lehmann * item.py : Show songs in filtered artist. * filelistwin.py: Implement incremental searching (as suggested by Stuart Pook ). * config.py: New key binding "repeatsearch" in filelist window which allows the user to specify a key for the repetition of the last search (as suggested by Falko Rütten ). * config.py: New option songchangecommand in general section which allows to specify a command executed when the playback of a new song starts. 2004-07-26 Jörg Lehmann * New configuration options in database sections which allow the user to turn on and off various tag transformation and to specify the regular expression used for obtaining track nr and title from the song filename. * services/songdbs/local.py: Even further simplify artist and album index machinery now that ratings are no longer stored directly in this items but only in songs. 2004-07-25 Jörg Lehmann * Implemented play previous song (as requested by Sebastian Schwerdhoefer and Han Boetes, George J. De Bruin, and Sam Rowe) * pytonerc: New config option stepsize in mixer section, which allows the user to change the step size (in percent) of the mixer (requested by Krzysztof Zych ). * mixerwin.py: Make volume bar as wide as possible when type=statusbar (requested by Krzysztof Zych ). 2004-07-24 Jörg Lehmann * dbitem.py: Remove genres and years attributes of artist and album. * dbitem.py: New index rating. * services/songdbs/local.py: Generalize index machinery to enable simplified addition of new index. * item.py: Generalize filtereditem for a simplified addition of a new index. * Do no longer store artist and album rating but instead a rating source in the song. * Allow the user to filter songs by their rating (as suggested by Thomas Klein-Hitpass ). * Database version 4. 2004-07-22 Jörg Lehmann * Version 2.0.14 released. * Make player progress bar more readable on mono devices (thanks to Krzysztof Zych ). 2004-07-19 Jörg Lehmann * Replace "PyX" by "PyTone" in license headers (thanks to Krzysztof Zych for pointing this out). * Add polish translation (many thanks to Krzysztof Zych ). * dbitem.py: Update eyeD3 integration based on contributions by Krzysztof Zych . * item.py: Make last played song list sorted again. * services/songdbs/locale.py: Some fixes in database upgrade code. 2004-07-18 Jörg Lehmann * services/songdbs/locale.py: Really make use of transactional subsystem. * services/songdbs/locale.py: Move dbitem.song creation to songautoregisterer thread to greatly improve the usability during the song registering process. * dbitem.py: Added support for the eyeD3 module, which parses MP3 files much faster as the current version of the MP3Info module. 2004-07-15 Jörg Lehmann * Handle SIGTERM gracefully by sending a quit signal to all running threads. 2004-07-13 Jörg Lehmann * hub.py: Explicitly use list as underlying queue to make the PriorityQueue class work with Python 2.4 (thanks to Krzysztof Zych for reporting this problem). 2004-07-08 Jörg Lehmann * pytone.py: Catch errors during service creation to be able to shut down all already running services * services/playlist.py: Be careful when loading a dumped playlist which has ids incompatible with the global _counter variable. * playlist.py: Issue selectionchanged event in _recenter and in playlistchanged method if necessary. This should fix a longstanding bug where the item info window showed the wrong playlist item. 2004-07-07 Jörg Lehmann * Allow only songs located under basedir in databases. * Do not delete currently playing song when backspace is pressed. * decoder.py: Replace samplerate by outrate in calculation of playing time since the data has already been resampled to outrate. 2004-07-06 Jörg Lehmann * Removed some unnecessary selectionchanged notifications. * dbitem.py: Finally switched from path as id to relative path as id. This should facilitate moving songs from one base directory to the other. * Database version 3. * Use artist and album from relative path as fallback if no id3 information is present and the relative path (with respect to the musicbasedir) consists of exactly two directories. 2004-07-01 Jörg Lehmann * services/songdbs/local.py: Don't index song in _queryregistersong, if it is already registered in database. * services/songdbs/local.py: Use transaction system when writing to the database (addressing the problems of Debian bug #245503). * pytone.py: Set locale to setting defined by the user's environment variables. * inputwin.py: Allow the user to input all printable characters according to the locale set (as suggested by Stuart Pook ). * filelistwin.py: Empty search string repeats last search (as suggested by Stuart Pook ). * slist.py: New method for regular expression search. * filelistwin.py: Search strings are now interpreted as regular expressions. 2004-06-29 Jörg Lehmann * Make timer a service, which runs in an independent thread * services/songdbs/local.py: Turn on transaction and log subsystem and checkpoint log regularly. 2004-06-25 Jörg Lehmann * mixerwin.py: Do not fail if neither ossaudiodev nor oss module is present (thanks to Linus Sjöberg ) 2004-06-21 Jörg Lehmann * dbitem.py: Catch error when no id3 tag is present in an MP3 file (fix contributed by Toma¸ Ficko ). * MP3Info.py: New version (thanks to Toma¸ Ficko for providing me with the update). Included is a patch which should fix the length detection for VBR MP3 files (hopefully fixing a problem reported some time ago by George J. De Bruin ). 2004-06-13 Jörg Lehmann * Version 2.0.13 released. 2004-06-10 Jörg Lehmann * conf/pytonerc: Update description of autoregisterer option in the database sections (fixing Debian bug #245528). * conf/pytonerc: New option dbfile in database sections which allows to store the database in one single file (instead of multiple ones, when using the old basename option) * services/songdbs/local.py: support storing of databases in a single file. 2004-04-22 Jörg Lehmann * Do not process global keybindings when input window is active (thanks to Andreas Poisel for reporting this problem). * pytonectl.py: Rewrite to allow for requests and not only events. * pytonectl.py: Add getplayerinfo function, which prints a string with information about the currently playing song (as suggested by Alexander Wirt ). 2004-04-21 Jörg Lehmann * pytonectl: Print error message instead of traceback when connection to server fails (as suggested by Alexander Wirt ). 2004-04-17 Jörg Lehmann * Version 2.0.12 released. * Fixed selection by mouse in playlist window. 2004-04-12 Jörg Lehmann * When dumpfile is set, we use it to store the playlist state not only when a crash occurs but also on PyTone's exit. * pytonerc: Added option songformat to playerwindow and playlistwindow sections of config file which allow the user to specify an arbitrary format strings for title of player window and entries in playlist window. 2004-04-11 Jörg Lehmann * Added new layout which consists only of one column. * pytonerc: New option togglelayout in keybindings.general section. * Added example configuration files for different layouts. * pytonerc: New option layout in general section, new option activetitle in filelistwindow and playlistwindow sections. * services/players/internal.py: Fix problem preventing automatic crossfading. 2004-04-09 Jörg Lehmann * decoder.py: Use length information of mad decoder instead of the one stored in the database to further improve the behaviour when using VBR files. * Window borders are know configurable. * pytonerc: new option border in filelistwindow, playlistwindow, iteminfowindow, and playerwindow sections. 2004-04-05 Jörg Lehmann * dbitem.py: Use the TLEN id3 tag when existent for the length of an MP3 file. Hopefully, this improves the behaviour when using VBR files. * pytonerc: Added vi-like navigation keys to standard config (as suggested by Andreas Poisel ). 2004-04-04 Jörg Lehmann * Version 2.0.11 released. 2004-04-03 Jörg Lehmann * config.py: Add aooptions configuration option string that allows the user to change all options provided by the ao library (as suggested by Stuart Pook ). 2003-03-28 Jörg Lehmann * services/songdbs/local.py: automatically update to new database version if necessary. * Close audio device when no songs are left in playlist (fixing a bug reported by Stuart Pook ). * Remove alsa_buf_size option again as it was not working as expected. 2003-03-22 Jörg Lehmann * services/songdbs/local.py: store version number of database schema in database. * Do no longer identify albums by artist+album but only by album name (as suggested by Stuart Pook ). Albums with the same name thus get automatically merged in the albums list. 2003-03-18 Jörg Lehmann * Prevent race condition by starting song auto registerer only after the database thread has been started. 2003-03-17 Jörg Lehmann * Version 2.0.10 released. * Added alsa_buf_size option to player sections of config file, which allows one to specify the internal buffer size of the alsa device (partly implementing a suggestion by Stuart Pook ). * Added sections logwindow and colors.logwindow to config file. * Mark song as unplayed again when it is stopped (as suggested by George J. De Bruin ). 2003-03-15 Jörg Lehmann * Keep audio device only open, when it is really needed, i.e., close it when the player is stopped or paused (as suggested by ). * Log problems during audio device initialisation to message log. 2003-03-14 Jörg Lehmann * Disable mixer functionality when an error occured during mixer initialisation. * Add message log functionality. * Do not use "=" for the top border of an active windows anymore. 2003-03-13 Jörg Lehmann * pytone.py: Initialize players after databases to prevent a race condition when autoplaymode=random (thanks to Niels Drost for reporting this bug). * Handle virtual directories at top of filelist window correctly when inserting new artists in the database (fixing a bug reported by Niels Drost ). * config.py: Fix wrong behaviour with foreground colour black (thanks to Stuart Pook for reporting this problem). 2003-02-18 Jörg Lehmann * Check for old database type did require Python 2.2 and above. * Include cursext extension module in distribution. * Check path names in m3u files for zero-bytes to handle a (very rare) corner case (reported by Douglas Bagnall . 2003-02-18 Jörg Lehmann * Include po files in distribution. 2003-02-15 Jörg Lehmann * Version 2.0.9 released. * filelistwin.py and playlistwin.py: Move cursor to the right position in order to make it more easy for users of Braille displays to track the current position/selection (thanks to Stéphane Doyon ). * config.py and services/players/internal.py: New config option crossfading in [players.*] sections, which allows the user to turn off the crossfading of songs (suggested by Niels Drost ). * services/songdbs/local.py: Store playing time with each song in lastplayed list to later on permit merging of lists coming from different databases. Upon upgrade, the last played song list will be deleted! 2003-02-14 Jörg Lehmann * services/playlist.py: Upon deletion of played songs, the played and total times info of the playlist have not been adapted. * playlistwin.py: When given the focus to the playlist window, we now recenter the displayed list and select the currently playing song. * Disable logging functionality of bsddb. * When updating the song database, we delete songs which are no longer accesible. * services/songdbs/local.py: Be more aggresive, when scanning for new songs. 2003-02-12 Jörg Lehmann * dbitem.py: Try to determine the "correct" character set for the reencoding of the unicode strings contained in the tags of Ogg Vorbis files (thanks to Michal Cihar for the patch). 2003-02-08 Jörg Lehmann * Better curses initialisation (thanks to Johannes Mockenhaupt ) * Add an extension module which provides support for transparent terminals also for "older" Python versions (thanks to Johannes Mockenhaupt ). * pytone.py: Initialize players before everything else to prevent a hang of PyTone when a problem occured at this stage. * services/player.py: Be more verbose on player initialization problems. 2003-02-07 Jörg Lehmann * services/playlist.py: Remove extraneous _checksong call in _addsongs method. * services/playlist.py: Check whether song is already contained in a local database. If yes, do not create a new entry in the primary database. The disadvantage of this behaviour is that the playing information is no longer collected centrally. * Rescanning songs proceeds now in a separate thread. 2003-01-31 Jörg Lehmann * services/players/internal.py: Check for endianness of platform to set correct output format when using the ossaudiodev module, fixing the internal player on PowerPC (thanks to Peter Poeml for the patch). * setup.py: install italian message catalog 2003-01-18 Jörg Lehmann * Version 2.0.8 released. * Include Italian localization (many thanks to Davide Alessio ). 2003-01-11 Jörg Lehmann * network.py: Do not bail out when pytonectl socket does not exist (thanks to Alexander Wirt for reporting this problem). 2003-01-06 Jörg Lehmann * playlist.py: Fix bug occuring when rating a song and no song is selected in playlist. Furthermore, the song info window was not updated. * Added new command rescan in filelist and playlist mode, which allows the user to rescan/update the id3 information of the selected song(s). 2003-01-05 Jörg Lehmann * Version 2.0.7 released. * Fix incorrect title and sort order when displaying genre and decade filtered items. 2003-01-04 Jörg Lehmann * MP3Info.py: use new version of Vivake Gupta. * dbitem.py: adapt to new version of MP3Info module. * dbitem.py: Extract track number from file names like "03 Songtitle.mp3". 2003-01-03 Jörg Lehmann * config.py: Added new option cachesize in [database.*] sections, which allows to specify the cache size used for the database when using Python 2.3 and above. * services/songdbs/songdb.py: Renamed in services/songdbs/local.py to prevent collision with global bsddb.py module. * services/songdbs/local.py: Enable use of new bsddb module from Python 2.3 and above. 2003-12-13 Jörg Lehmann * config.py: Do not process command line and reading of configuration file at module initialisation time, in order to be usable for pytonectl as well (fixing a problem reported by Han Boetes ). * services/player.py: autoplay now also works after having stopped and restarted the player manually (thanks to Han Boetes for pointing me to this long standing annoyance). * events.py: New event playertogglepause which allows to pause the player, if it is playing, or start playing, if it is paused. * pytonectl.py: Add support for the playertogglepause function (as suggested by Han Boetes ). 2003-12-12 Jörg Lehmann * Version 2.0.6 released. * event.py, player.py: Add new event playerratecurrentsong: rating of the song currently being played on the player. 2003-12-11 Jörg Lehmann * pytonectl.py: Add support for song addition and immediate song play. * config.py: New option playerinfofile in the general section, which allows to specify a file where the song currently being played on the main player will be written (as suggested by Han Boetes ). 2003-12-09 Jörg Lehmann * config.py: Prune removed -n switch from usage/help output. * pytonectl.py: New script for the remote control of PyTone (as suggested by Han Boetes ). * config.py: New option in the network section: socketfile. Specifies the name of a UNIX domain socket for the remote control of PyTone, if set to a non-empty value. 2003-11-25 Jörg Lehmann * Version 2.0.5 released. * Added missing help texts for functions introduced in version 2.0.4 (fixing a bug spotted by Han Boetes ). * Fix a small bug in services.players (thanks to Andreas Poisel for the patch). * services/playlist.py: Notify upon playlist changes due to a song being played. * Cleanup README file: purge news section. 2003-11-25 Jörg Lehmann * Version 2.0.4 released. 2003-11-24 Jörg Lehmann * Added repeat function for playlist. * The playlist mode can now be set initially with the variable initialplaylistmode in the [general] section (instead of autorandomplay) and toggled during runtime by pressing CTRL-T. 2003-11-23 Jörg Lehmann * More safety checks with respect to Ogg Vorbis support. * Do not show Ogg Vorbis files in filesystem directory view, if no Ogg Vorbis support is present. * Move update of song played information to services/player.py to be consistent with song.unplay, which will be introduced later. * Rename _nextsong->_playsong in player modules. 2003-11-18 Jörg Lehmann * services/playlist.py: Replace incorrect use of dbitem.song by item.song in _checksong method. * add autorandomplay to [general] section of pytonerc, which enables choosing a random song for playing, when playlist is empty * Allow replaying of the current playlist. The corresponding key can be configured in the [keybindings] section of pytonerc via the variable general.playlistreplay (implements Debian wishlist bug #218282) 2003-10-18 Jörg Lehmann * Version 2.0.3 released. * pytone.py: Prevent mainscreen.dump from being called if the mainscreen constructor did not succeed (reported including a patch in Debian bug 216002 by David Kågedal ) * config.py: Remove command line option "-n/--network". Use a config file instead. * config.py: Check for empty musicbasedir in the configuration * pytonerc: Set musicbasedir to an empty value by default to remind first time users setting this option. 2003-10-13 Jörg Lehmann * item.py: Allow getcontentsrecursive (recursive insert) and implement getcontentsrecursiverandom (random insert) for filesystem directories. Thanks to Martin van Es for reporting the previous inconsistency in PyTone's behaviour in that regard). * Merge pending fix from last Rothsee party's late night hacking session: - requestnextsong gets a playerid argument to distinguish between song requests of different players 2003-08-12 Jörg Lehmann * config.py: Fix wrong behaviour in mono mode, when no mono attribute is given in config file. * inputwin.py: Re-added curses.ascii import, which got lost. * config.py: New option "colorsupport" in section "general". This allows to enable/disable colors both manually and automatically (as before). * config.py: New option "throttleoutput" in section "general", which allows to specify the number of screen updates skipped when there is still user input. 2003-07-27 Jörg Lehmann * Version 2.0.2 released. 2003-07-26 Jörg Lehmann * item.py: Fixed typo in decades.getcontentsrecursive() * Purged unneeded imports and local variables found by pychecker. * Streamlined playingsong handling by using playbackinfochanged instead of playlistchanged events. * Check for __setstate__ in __getattr__ to enable unpickling of certain wrapper classes for Python 2.3. * setup.py: Move everything (including pcm module) into pytone module. 2003-07-25 Jörg Lehmann * Version 2.0.1 released. 2003-07-24 Jörg Lehmann * Allow batching of addition of songs to playlist. * Statusbar for help window * Command line switch for database rebuilt. * Updated THANKS file. 2003-07-23 Jörg Lehmann * Code cleanups at various places. * Outstanding issues with playlist functionality have been resolved now. * Do automatic recenter around last song of playlist if no song is being played. * Add originating module to debugging output. 2003-07-20 Jörg Lehmann * Fix load and save of playlists (thanks to Alexander Wirt for spotting this bug) * Enable support for monochrome terminals again. Use curses.has_colors() to determine color capabilities, but maybe we eventually have to add a configuration variable for that. 2003-07-19 Jörg Lehmann * Fix missing os.path.expanduser around config file path (thanks to David Braaten ) 2003-07-18 Jörg Lehmann * Version 2.0.0 finally released. 2003-06-08 Jörg Lehmann * Move service setup from mainscreen.py to pytone.py * Resurrect playlist functionality. 2003-05-25 Jörg Lehmann * Renamed: aoplayer.py -> internal.py, mpg123player.py -> mpg123.py xmmsplayer.py -> xmms.py * First steps towards playback from remote db: try to access song locally. * Prevent too many register request from auto registerer. 2003-05-24 Jörg Lehmann * players/mpg123player.py: Fix bug spotted by John Plevyak : Adding a new song after all songs had been played, required to stop and restart the player, even though autoplay was enabled. 2003-05-17 Jörg Lehmann * Support default background color and try to use curses.use_default_colors, if present. This requires a new version of the Python curses module. * Support ossaudiodev instead of ao interface. Unfortunately, this doesn't work well with the current ossaudiodev module of Python 2.3beta1. On the other hand, I was not able to run the ao module with a debug build of current Python CVS. Probably, ao is buggy. * helpwin.py: Set self.items already in constructor, to prevent possible crash, if one presses a key during the PyTone initialization * songdb.py: Remove unused and dangerous __len__ method, which lead sometimes to a crash during the PyTone shutdown. 2003-05-13 Jörg Lehmann * New db index for playing statistics. Move respective code to songdb.py * Show info about songs which have been added most recently to database. 2003-05-11 Jörg Lehmann * item.py: Replaced filtereditem by two, more specialised, classes filtereddecade and filteredgenre. * iteminfowin.py, item.py: Move code for respective items into a getinfo method. * songdb.py: More index work, should be almost complete now. 2003-05-10 Jörg Lehmann * MP3Info.py: change _strip_zero function to - use faster lstrip, if Python 2.2.2 and above - cut string after first (non-leading) \0 character, thereby fixing PyTone crashes reported by a few users. * slist.py: Fix bug in selectbysearchstring and selectbyletter, reported by John Plevyak . 2003-05-06 Jörg Lehmann * mainscreen.py: Catch curses.curs_set exception (reported by roland at steeltorch.com. 2003-05-05 Jörg Lehmann * More database work and tests. Genres now contain indices to albums and songs * album -> albumid at appropriate places * introduced song.id, but not yet used 2003-05-01 Jörg Lehmann * Integrated and polished some patches by Iñigo Serna : Currently playing song gets different colour and stays centred in playlist. Furthermore, some sanity checks of the id3 tags have been included. Finally, as suggested by him, a much nicer scrollbar has been implemented. * Support ossaudiodev module (and old oss module , of course) as suggested by Bill Kearney . * Many fixes for bugs remaining from code restructuring. 2003-04-19 Jörg Lehmann * New configuration system is complete now. 2003-04-18 Jörg Lehmann * Massive rework of configuration file handling. Now it is possible to use ini-style file configuration files. Exceptions are for the moment the key bindings. 2003-04-17 Jörg Lehmann * Network connectivity now via TCP instead of XML-RPC. 2003-04-13 Jörg Lehmann * Reworked config file as first step versus a new config syntax. Import of the curses module is no longer necessary (except for the check whether the user has a color or a mono terminal, which often doesn't work, so ...) * Colors now have to be specified like in mutt "color foreground background" or "mono attribute". Foreground may contain the prefix "bright". * Accept command line options for debugging output and network functionality. * Implement first rough cut of network functionality via xmlrpc. Remote access to the database is now possible. 2003-04-12 Jörg Lehmann * Do not show playlist window of xmms (oops, forgot that). * Added list of songs for albums (and root folder) * Released 1.12.2 from the 1.12.1 codebase, fixing a Python 2.2ism. 2003-04-06 Jörg Lehmann * Add immediately played to before last played item of playlist. * Version 1.12.1 released. * New module hub, the former event hub + a simple request hub. * Major rework of database interface based upon new request hub. Database now runs as separate thread. * Requests for new songs now also use new request hub. 2003-04-05 Jörg Lehmann * Some restructuring in the player code. * Implement player pause. * Flush buffers on player stop. * Immediately play selected song via ALT+Return or ALT+Enter. * Implemented shuffle function since it seemed to be very high on the PyTone user's wishlist... * Try to save state of playlist to dump file, if PyTone crashes. During the next restart, PyTone tries to reconstruct the playlist. * Be more verbose, if config.basedir and config.songddb are set to incorrect values. 2003-03-19 Jörg Lehmann * slist.py: prevent invalid items state. * disable long traceback 2003-02-06 Jörg Lehmann * updatedb.py script. * cleanup at various places * Version 1.12.0 released. 2003-02-04 Jörg Lehmann * pcm.c: Don't free memory allocated by Python! 2003-02-03 Jörg Lehmann * All *.py files: specify encoding (for Python 2.3) * Remove #!/usr/bin/env python for modules * pcm.c: Don't forget to free temporary buffer. 2003-01-30 Jörg Lehmann * playerwin.py: Allow rating of currently playing song (via alt+1, ..., alt+5 -- better suggestions are always welcome). 2003-01-26 Jörg Lehmann * Incorporated patch by Byron Ellacott adding Ogg Vorbis support. * iteminfowin.py. Fix display of song length. * pytone.py: Work around Python 2.1 gettext problem. * THANKS: added Byron Ellacott. * window.py: Don't unnecessarily trim title. * config.py, helpwin.py, mixerwin.py: Make automatic disappearing time configurable. * inputwin.py: remove unnecessary import of oss module, which prevents PyTone from working if it is not installed. * config.py, playlist.py: Make location where playlists are stored configurable. * config.py, slist.py: Make scrolling mechanism configurable `a la Mutt. Also use page up algorithm from mutt. Adapt to window size changes, as well! * config.py, item.py: Make position of virtual directories configurable. 2003-01-21 Jörg Lehmann * Version 1.11.0 released. 2003-01-18 Jörg Lehmann * new class playbackinfo. All players should work again. 2003-01-16 Jörg Lehmann * Indices for genres and years. This gives a huge speedup for large databases. * item.py: new item method getname(). 2003-01-14 Jörg Lehmann * Implement song, album and artist rating. Choose song depending on rating upon random song insertion. 2003-01-13 Jörg Lehmann * Implement random recursive insert. Reserve key "r" for this function, giving up the old behaviour. 2003-01-12 Jörg Lehmann * config.py: New option autoregisterer, which allows to enable/disbale the song automatic searching for songs and playlists after the start of PyTone. * builddb.py: Manually populate song and playlist database. 2003-01-09 Jörg Lehmann * item.py: New classes decade and decades, which allow to show only songs from a specify decade. 2003-01-06 Jörg Lehmann * Some improvements/fixes for old songdb.py. Hopefully, this version is more stable for large databases. 2003-01-02 Jörg Lehmann * Implement bsddb3 version of songdb.py. Probably not yet ready for next version. 2002-12-30 Jörg Lehmann * players/madplayer.py: Set sample size to 4096 (instead of 4806, i.e. the size of an mp3 frame). This is much more friendlier to the sound device drivers! Thanks to Damjan Georgeivski for pointing this out. 2002-12-22 Jörg Lehmann * Use MP3Info.py from http://www.omniscia.org/~vivake/python instead of old, modified mp3info.py from http://www.dotfunk.com/projects/mp3/. As a consequence, id3v2 tags are now supported. 2002-12-21 Jörg Lehmann * Version 1.10.0 released. * madplayer.py: Manual song forward works again. 2002-12-19 Jörg Lehmann * players/madplayer.py: Do not crossfade, if two songs follow each other on an album. Kill the gap between the songs instead. 2002-12-15 Jörg Lehmann * Implement genre list: New database schema, permitting efficient search for items belonging to a given genre. New items: genres, genre and filtereditem. The latter one can be used to display only a subset of the directory hierarchy. 2002-12-14 Jörg Lehmann * More refactoring: merge the two filelist classes. The filesystem view is now provided by a virtual folder in item.py. * filelist.py: Implemented jump functions to the random list and the filesystem view. * filelist.py: remove generaterandomlist. * item.py: Refactoring. New method cmpitem of diritem classes. Everything should work now properly. * window.py, mainscreen.py: Reimplemented window resizing, which is now much more robust. However, there are still some crashes of xterm. * config.py: Renamed "selectedsong" -> "selected song", etc. in colors. Added new field "artist/album" and "selected artist/album". * iteminfo.py: Recognise new items. 2002-12-12 Jörg Lehmann * Refactor filelist code: remove filelistitem, move logic to item, etc. * Allow resizing of terminal (not fully complete yet). 2002-12-11 Jörg Lehmann * New module purgedb.py: This module allows to delete no longer existent songs from the database. 2002-12-10 Jörg Lehmann * Better formatting of last played time. * Playlists are now also stored and displayed in database and database view, respectively. * Random songs are now also accesible via the main database view. 2002-12-08 Jörg Lehmann * Fix recursive insertion of top and last played songs. * Directory like items now are able to return their content. 2002-12-06 Jörg Lehmann * Sort songs by tracknr if possible. Otherwise use name. * First implementation of top and last played lists. * Fill up caches after song auto registering. 2002-11-30 Jörg Lehmann * Version 1.9.7 released. * More color work. * Limit size of help window (therby preventing a crash occuring for small terminal sizes) and make it scrollable. 2002-11-27 Jörg Lehmann * Initial support for colors. Customizable via config.py. * Enter and insert a directory only if the click has hit the actual string. Otherwise only select the corresponding line. I hope, this is more intuitive then the old behaviour. 2002-11-25 Jörg Lehmann * Customize appearance of mixerwin and inputwin: one can now choose between a popup and a status bar variant. * inputwin.py: popup variant adjusts statusbar. * mixerwin.py: popup variant adjusts statusbar. * pytone.py: Use pytone base dir as base dir for locales. This has to be changed, if we/someone else install the .mo files under their proper location. 2002-11-25 Jörg Lehmann * New module inputwin for a generic input window (for search strings, filenames, etc). 2002-11-24 Jörg Lehmann * More mouse work: Hide mixer and help win on mouse clicks. * window.py: walk through all panels in enclose() method * slist.py: Fix erroneous use of window width instead of height which prevented correct scrolling in list windows. * playlist.py: Localize load and save prompts. 2002-11-23 Jörg Lehmann * Initial work for mouse support. 2002-11-03 Jörg Lehmann * slist.py: Cleanup: remove active flag; use focus of corresponding window instead. * filelistwin.py: Activate file list window upon end of search. Search should work again (Thanks to Andy Bourges for reporting this bug). * player.py: Also sleep in STOP state to prevent unnecessary CPU utilization. * Code cleanup at various places. * Added MANIFEST.in and updated setup.py: Include AUTHORS, COPYING, ChangeLOG, TODO, and *.mo files. 2002-11-02 Jörg Lehmann * mixerwin.py: Localize mixer text. * iteminfowin.py: Fix small bug, occurring if no ID3 tag is present. * helper.py: New function from Python Cookbook: extended traceback. * pytone.py: Print extended traceback when an uncaught exception is raised. * iteminfowin.py: Make geometry calculations more clean. Use of spacer now dependent on window width. * pytone.py: Removed obsolete reference to hipplayer module. 2002-11-01 Jörg Lehmann * Localization work all over the place. * filelist.py: Fix bug occurring when entering an empty directory (Thanks to Andy Bourges for the bug report). 2002-10-27 Jörg Lehmann * Version 1.9.6 * New handling of window focus: New event focuschanged instead of old activewinchanged. The actual is granted to the panel which is on top of the panel stack. * Help and mixer windows now disappear upon keypress events. * Moved function descriptions from config.py to new help.py. Later on, we shall localize them via the gettext module. * timer.py: Return immediately, if no alarms are pending. 2002-10-17 Jörg Lehmann * timer.py: New timer service, which sends events at specified times. * events.py: New hidewindow event. * window.py: Allow hiding of window. Also window.top() * helpwin.py, mixerwin.py: Automatically hide after 5 seconds. 2002-10-13 Jörg Lehmann * songdb.py: Implemented caching of list of all artists (and already existent caching of all songs). * filelist.py: Don't store list content in shistory, anymore. * Implement context sensitivity of help window. 2002-10-12 Jörg Lehmann * Implemented automatic adjustment of statusbar to keybinding. * Use curses.panel. * First version of help window. 2002-10-12 Jörg Lehmann * item.py: Prevent empty artist names 2002-09-20 Jörg Lehmann * Version 1.9.5 * item.py: Try to be a little more intelligent during the capitalization of song, album and artist names. For instance, only force the first character of a word to be a capital one. Prevent empty album and artist names, as well. * filelist.py and songdb.py: Check for access permission of directories and files. * madplayer.py: Catch exception during mp3file object creation. 2002-09-20 Jörg Lehmann * All players are now running in a separate thread. * Factored out most of player control logic to player.py. 2002-09-10 Jörg Lehmann * Implement write lock for song database. * Automatic registering of songs proceeds now in separate thread. * slist.py: Fix incorrect insertion of new items leading to unsorted lists. * Remove timer events, leading to notable decrease of idle CPU usage. * Add ALT+RightArrow to default key bindings for recursive insertion of songs. 2002-09-08 Jörg Lehmann * New event handling: Queue generated events and process them not until an explicit call of a new process method. This makes event handling in a multithreaded environment much simpler. The new class eventhub collects the eventchannels from the different threads. * madplayer.py: Integrate madplayer class into player class, which is made possible owing to new event handling. * madplayer.py: Generate playbackinfochaned events only if there was really a change. 2002-09-05 Jörg Lehmann * Version 1.9.4 * Stopping works again in autoplay mode. 2002-08-31 Jörg Lehmann * filelistwin.py: Corrected quick search key logic. * Moved handling of global key bindings to pytone.py. Introduced new events to signalize playlist changes. * playlist.py: Don't save playlist, if name is empty. * Implemented autoplay player option permitting automatic start of playing, if playlist is not empty. * More comments in config.db. * players/mpg123player.py: Now also works with Python 2.1. Also fixed error, where playback info was not displayed correctly. * helper.py: Q&D solution to make debug output configurable. 2002-08-30 Jörg Lehmann * Made key bindings configurable wherever possible. * Added python modules to setup.py to ease creation of distribution. * pcm.c: Pad with zeros for crossfading of two buffers with unequal lengths. * players/madplayer.py: Adapted crossfading logic to change in pcm.c. * playerwin.py: Corrected minor error, where title of song was show although playing has already finished. 2002-08-29 Jörg Lehmann * Fixed the specification of the libao device. It is now also possible to specify arbitrary options. Added aRts based player sample config to config.py. * Added setup.py for building of pcm extension module. * RightArrow in playlist window now also moves to database window. 2002-08-28 Jörg Lehmann * Version 1.9.3, aka ready for release * Added license headers to all python files except config.py * Renamed PyJuke -> PyTone following a suggestion by Harald Görl (goerl at luga.de) 2002-08-25 Jörg Lehmann * Version 1.9.2 * Added scrollbars and allow to turn them on/off via config.py * Corrected error in madplayer.py, where too many new songs were requested * madplayer.py: return artist + song title as window title * madplayer.py: indicate crossfading in title * madplayer.py: made use of sound device configurable. Still need to abstract device options! * Minor fixes to documentation in index.html * Added simple style.css for index.html 2002-08-22 Jörg Lehmann * Version 1.9 * Crossfading for internal libmad based player now works * Restructured and documented config.py * Added README with documentation * Added ChangeLog, AUTHORS and COPYING * Added some licence headers 2002-08-08 Jörg Lehmann * Released 1.0 for Rothsee party PyTone-3.0.3/conf/000755 000765 000765 00000000000 11406223507 014170 5ustar00ringoringo000000 000000 PyTone-3.0.3/._COPYING000644 000765 000765 00000000122 10302650534 014504 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/COPYING000644 000765 000765 00000035430 10302650534 014301 0ustar00ringoringo000000 000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS PyTone-3.0.3/locale/000755 000765 000765 00000000000 11406223507 014502 5ustar00ringoringo000000 000000 PyTone-3.0.3/PKG-INFO000644 000765 000765 00000001073 11406223507 014341 0ustar00ringoringo000000 000000 Metadata-Version: 1.0 Name: PyTone Version: 3.0.3 Summary: Powerful music jukebox with a curses based GUI. Home-page: http://www.luga.de/pytone/ Author: Jörg Lehmann Author-email: joerg@luga.de License: GPL Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console :: Curses Classifier: Intended Audience :: End Users/Desktop Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Programming Language :: Python Classifier: Topic :: Multimedia :: Sound/Audio :: Players PyTone-3.0.3/._pytone000755 000765 000765 00000000122 10732276545 014732 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/pytone000755 000765 000765 00000000044 10732276545 014520 0ustar00ringoringo000000 000000 #!/bin/sh python src/pytone.py "$@" PyTone-3.0.3/._pytonectl000755 000765 000765 00000000122 10732276550 015431 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/pytonectl000755 000765 000765 00000000047 10732276550 015222 0ustar00ringoringo000000 000000 #!/bin/sh python src/pytonectl.py "$@" PyTone-3.0.3/README000644 000765 000765 00000027064 11406223145 014132 0ustar00ringoringo000000 000000 PyTone MP3 Jukebox redux _________________________________________________________________ Summary PyTone is a music jukebox written in Python with a curses based GUI. While providing advanced features like crossfading and multiple players, special emphasis is put on ease of use, turning PyTone into an ideal jukebox system for use at parties. Features * concise curses based GUI * simple song selection + using an arbitrary number of music databases with hierarchical (artist/album/songs, some tags/artist/album/songs) navigation, + from list of top and last played songs, + from list of most recently added songs, + random song list, + stored playlists, or + alternatively from file system * editable playlist: + deletion + move song up/down + delete played songs + shuffle + repetition and automatic addition of random songs, when the playlist is empty + save to and load from .m3u file * pluggable players, currently + internal MP3/Ogg Vorbis player with crossfading and/or + xmms based external player and/or + mpg321 or (the non-free) mpg123 based external player * display of information for currently selected song: + ID3 tag + length, bitrate, sample rate, BPM, ReplayGain information, part of a compilation, podcast + times played and skipped + last played + song rating (1 to 5 stars) * plays currently selected song on second player (if your computer has a second sound card or one card with more than one line out) * search functionality: + quick search by first letter + incremental search by regular expression * random song selection taking into account song rating and time at which song was last played * description of important key bindings in status bar and context sensitive help * random song suggestion * logging of played songs * execution of arbitrary command when playback of new song starts * basic mixer functionality * customizable key bindings * customizable look * English, French, German, Italian and Polish user interface * external control, e.g. from the shell * plugin system; currently plugins for the AudioScrobbler service and for displaying the title in the terminal window and using xosd are included Software prerequisites * Python 2.3 (available from [2]here); * for the mad based internal player (optional): + Python header files, + pymad (available from [3]here) and + pyvorbis (optional, available from [4]here), + pyao (available from [4]here, please use version 0.82 or above) or the new Python OSS module in Python 2.3. + libao header files (available from here, if you want to compile the C version of the output ring-buffer. * for the xmms based external player (optional): + xmms 1.2.6 or higher (available [5]here) and + pyxmms (available from [6]here) * for the mpg321 or mpg123 based external player (optional): + mpg321 (available from [7]here) or + mpg123 (available from [8]here) * for the mixer interface (optional): + Python OSS module (included in Python 2.3 or available from [9] here) Download The latest version of PyTone can be downloaded as gzipped tar archive from [10]here. Installation If you want to use the internal libmad based player, you have to build one C extension module located in the pcm subdirectory. This can be done simply via $ python setup.py build_ext -i Note that by default this builds also a C extension module for the output ring-buffer, which requires the libao header files (see above). If you are happy with the Python version of the output buffer, you can disable building the C module by setting "buildbufferedaoext = False" at the top of the setup.py file before running the above command. To enable support for transparent terminals (only needed for Python 2.3.x) set "buildcursext = True" at the top of the setup.py file before running the above command. Configuration All configuration options of PyTone can be found in the sample configuration file conf/pytonerc. Side-wide configuration goes into /etc/pytonerc, user specific changes can be put into ~/.pytone/pytonerc. Note that you only have to supply options you want to change. Note that you only have to supply options you want to change. Furthermore, while most of the standard settings will probably fit your needs, you have to change the variable musicbasedir in the section [database.main] of the main database, which specifies the root of your primary MP3 collection. A minimal version of your configuration file should thus contain # minimal ~/.pytone/pytonerc defining the root of your music collection [database.main] musicbasedir=/root/of/your/music/collection Usage After having adjusted the basic configuration variables to your personal needs, just start the program with $ ./pytone and look how the database is being rebuilt. The key bindings described below should say all about the use of PyTone. A list of command line options can be obtained by $ ./pytone --help Then let it rock... The remote control of PyTone is possible using the pytonectl script. For a list of available options use: $ ./pytonectl --help In order for the remote control to work, either the socketfile or the enableserver option have to be set in the [network] section of the pytonerc file. By default, the former is the case. Key bindings In database/filelist window (left half of screen) ArrowUp move selection up ArrowDown move selection down PageUp/CTRL-P move selection one page up PageDown/CTRL-N move selection one page down Home/CTRL-A move selection to beginning End/CTRL-E move selection to end ArrowRight/Enter/Space enter directory / add song ArrowLeft exit directory i/ALT+ArrowRight add song or directory (recursively) r insert random selection of selected directory (including subdirs) u update ID3 information for song/directory D delete / undelete currently selected song/directory ALT+Enter immediately play song TAB switch to playlist window ALT+ Quicksearch: jump to next entry that begins with character CTRL-S// Search in list f Focus on songs matching approximately a search string In playlist window (lower right quarter half of screen) ArrowUp move selection up ArrowDown move selection down PageUp/CTRL-P move selection one page up PageDown/CTRL-N move selection one page down Home/CTRL-A move selection to beginning End/CTRL-E move selection to end + move selected song up - move selected song down d delete selected song ALT+Enter immediately play song r shuffle playlist TAB/ArrowLeft/h switch to database/filelist window ArrowRight/l jump to currently selected song in filelist window Always active p start/pause playing S stop playing n advance to next song in playlist b go back to previous song in playlist > fast forward in song < rewind in song BACKSPACE delete played songs CTRL-D clear playlist CTRL-W save playlist to file CTRL-R load playlist from file ( decrease output volume ) increase output volume { decrease playback speed } increase playback speed ~ reset playback speed to normal 1 - 5 change rating of selected item ALT-1 - ALT-5 change rating of currently playing song ? show help ! show message log % show statistical information about database(s) = show information about selected item L show lyrics of selected song CTRL-V toggle information shown in item info window F10 toggle UI layout (one/two column) CTRL-X CTRL-X exit program (the keypresses have to be maximally one tenth of a second apart) Mailing list For discussions on PyTone, a mailing list has been created. For more information on subscribing and for the list archive, see [11]here. History PyTone was written since my favourite MP3 Jukebox (KJukebox) wasn't maintained anymore. Its simple user interface and good usability even without a mouse combined with the crossfading ability of the player, have not been reached by any other free program. Especially for the use at a party, KJukebox was very well suited. However, after looking around on the net for sometime, I found [11]mjs, a curses based MP3 Jukebox system, which featured a really simple and efficient GUI. Unfortunately, it was written in C and already the first attempts to tailor it to my needs showed that probably a Python version of this program would be a great win. That's how the development of PyTone started... After one week of intensive programming version 1.0 of what was then called pyjuke was ready and was deployed during a three day long party. It proved to be very usable (even on a 38400 baud serial terminal) and astoundingly stable, even under extreme, Woodstock like conditions (very heavy rain + deep mud :-) ) Subsequently, the internal mad based player was written, which provides crossfading capabilities without using xmms, a new, more imaginative name was found (kudos to Harry!) and PyTone was released. Copyright and author PyTone was written by Jörg Lehmann and is free software licensed under the GNU GPL Version 2. Please send comments, wishes, bug reports and patches to the [11]mailing list or directly to me, Jörg Lehmann Of course, I always like to hear of happy users of PyTone. Links 1. http://packages.debian.org/unstable/sound/pytone.html 2. http://www.python.org/ 3. http://spacepants.org/src/pymad/ 4. http://www.andrewchatham.com/pyogg/ 5. http://www.xmms.org/ 6. http://www.via.ecp.fr/~flo/index.en.html 7. http://mpg321.sourceforge.net/ 8. http://www.mpg123.de/ 9. http://indra.com/~tim/ossmodule/ 10. http://www.luga.de/pytone/PyTone-latest.tar.gz 11. https://www.luga.de/mailman/listinfo/pytone-users/ 12. http://mjs.sourceforge.net/ PyTone-3.0.3/._setup.py000755 000765 000765 00000000122 10503277726 015201 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/setup.py000755 000765 000765 00000005177 10503277726 015003 0ustar00ringoringo000000 000000 #!/usr/bin/env python # -*- coding: ISO-8859-1 -*- from distutils.core import setup, Extension import sys; sys.path.append("src") from version import version # build extension module which adds transparency support for terminals # supporting this feature (not necessary for Python 2.4 and above) # You need the curses header files for building buildcursext = False # build extension module which replaces the Python audio output buffer # by a C version, which should help preventing audio dropouts. # You need the libao header files for building (but on the other hand, # you don't need the pyao extension module when you use bufferedao) buildbufferedaoext = True # list of supported locales locales = ["de", "it", "fr", "pl"] # # list of packages # packages = ["pytone", "pytone.services", "pytone.services.players", "pytone.services.songdbs", "pytone.plugins", "pytone.plugins.audioscrobbler"] # # list of extension modules to be built # ext_modules = [Extension("pytone.pcm", sources=["src/pcm/pcm.c"])] if buildcursext: ext_modules.append(Extension("pytone.cursext", sources=["src/cursext/cursextmodule.c"], libraries=["curses"])) if buildbufferedaoext: ext_modules.append(Extension("pytone.bufferedao", sources=["src/bufferedao.c"], libraries=["ao"])) # # list of data files to be installed # mo_files = ["locale/%s/LC_MESSAGES/PyTone.mo" % locale for locale in locales] data_files=[('share/locale/de/LC_MESSAGES', mo_files)] # # list of scripts to be installed # # Note that we (ab-)use distutils scripts option to install our wrapper # files (hopefully) at the correct location. scripts=['pytone', 'pytonectl'] # # additional package metadata # classifiers = ["Development Status :: 5 - Production/Stable", "Environment :: Console :: Curses", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: GNU General Public License (GPL)", "Programming Language :: Python", "Topic :: Multimedia :: Sound/Audio :: Players"] if sys.version_info >= (2, 3): addargs = {"classifiers": classifiers} else: addargs = {} setup(name="PyTone", version=version, description="Powerful music jukebox with a curses based GUI.", author="Jörg Lehmann", author_email="joerg@luga.de", url="http://www.luga.de/pytone/", license="GPL", package_dir={"pytone": "src"}, packages=packages, ext_modules=ext_modules, data_files=data_files, scripts=scripts, **addargs) PyTone-3.0.3/src/000755 000765 000765 00000000000 11406223507 014032 5ustar00ringoringo000000 000000 PyTone-3.0.3/._THANKS000644 000765 000765 00000000122 11153553606 014373 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/THANKS000644 000765 000765 00000006355 11153553606 014174 0ustar00ringoringo000000 000000 Thanks to all who have contributed to PyTone by reporting problems, suggesting improvements, submitting bug fixes, or even new code. Here is an alphabetically sorted list of these people: Davide Alessio for translating the PyTone user interface into Italian Jack Bakeman for his patient testing Dominik Bruhn for feedback and patches Han Boetes for providing Mandrake RPMs of PyTone Andreas Bourges for all the testing David Braaten for contributing a small fix Michal Cihar for improving the unicode decoding in the tags of Ogg Vorbis files Tom Diedrich for very constructive feedback Stéphane Doyon for improving the behaviour of PyTone when using Braille displays Byron Ellacott for adding Ogg Vorbis support to PyTone Nicolas Évrard for translating the PyTone user interface into French, for writing a first prototype of the plugin system and for contribution an AudioScrobbler plugin Toma¸ Ficko for some bug fixes and for providing me with a new version of the MP3Info module Damjan Georgeivski for locating a bug that I was not able to find Harald Görl for suggesting the name PyTone Philipp Jocham for a small fix in the Ogg Vorbis metadata handling and a patch for a nasty bug in the song update function ... and various other patches here and there David Kågedal for a suggesting a simple workaround I could not come up with Bill Kearney for many feature requests Thomas Klein-Hitpaß for reporting some problems with the Ogg Vorbis support detection Brian Lenihan for various small patches and ideas Tomas Menzl for implementing player seeking, various bugfixes and suggestions Johannes Mockenhaupt for providing an extension module which adds transparency support also for older Python versions Maurizio Panniello for making playlists work again John Plevyak for various bug reports and patches Stuart Pook for various suggestions and help with the ALSA driver Iñigo Serna for feedback and sundry patches Linus Sjöberg for a small patch Richard A. Smith for adding changeable play speed support Zoltan Szalontai for contributing a small fix Dag Wieers for writing the first plugins and contribution various fixes Alexander Wirt for packaging and maintaining PyTone for Debian, my favourite distribution Krzysztof Zych for some hints and patches and for translating PyTone's user interface into Polish PyTone-3.0.3/._TODO000644 000765 000765 00000000122 11046515510 014141 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/TODO000644 000765 000765 00000003701 11046515510 013732 0ustar00ringoringo000000 000000 - FLAC: finish decoder when not using it any more - check David Bowie Let's Dance - make removeaccents work again: howto use unicode.translate - allow tagging of no artists dir - check collation (Á vs A, etc.) - check XMMS and remote player (do they still work?) TOCHECK: - what happens to mpg123 when pytone crashes - update playingtime when updating song! - secondary player doesn't seem to work with bufferedao module. The version Rothsee-2006/PyTone-2.4.0-Rothsee/src/services/players/internal.py contains a workaround USER WISHLIST: - allow user to turn the secondary player on and off (or alternatively to stop it) (requested by Stuart Pook) - streaming sources - Fish: Song der auf zweitem Player kommt, anzeigen - Tomas Menzl : o show number of albums etc o handle collections better - start not at beginning but a few seconds later when prehearing song - fade out at exit (Dag Wieers ) - enhance crossfading such that pauses between songs can be inserted (Rene Maurer ) - put number of artists in title (Dag Wieers) - use mutagen also for Ogg Vorbis (David E. Thiel) - General: o timer interface o user authentication and permissions o allow up- and download of songs o make automatic backups of database o database reconstruction o undo for playlist changes o take metadata in song hash o can we get rid of setDaemon o do not use database "main" in pytonectl o check whether we can merge dbrequestsongs and dbrequestlist in songdb.py - Performance improvements: o move number of decades, genres, playlists (do it like for songs) o make length calculation of filtereddecade and filteredgenre smarter - UI Improvements: o improve behaviour on scroll bar mouse clicks o scrollbars are sometimes too short o list of seldomly played songs o make showing of last added list configurable (similarly for rest) o visual indicator that PyTone is busy PyTone-3.0.3/src/.___init__.py000644 000765 000765 00000000122 10266220577 016362 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/__init__.py000644 000765 000765 00000000167 10266220577 016156 0ustar00ringoringo000000 000000 # this is required to convert this directory into a package """ this package contains the main modules of PyTone """ PyTone-3.0.3/src/._bufferedao.c000644 000765 000765 00000000122 11046515056 016513 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/bufferedao.c000644 000765 000765 00000044223 11046515056 016310 0ustar00ringoringo000000 000000 /* bufferedao.c: Copyright 2005 Joerg Lehmann * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, * USA. */ /* written using the pyao wrapper by Andrew Chatham */ #include #include #include #include #include #include #include #if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN) typedef int Py_ssize_t; #define PY_SSIZE_T_MAX INT_MAX #define PY_SSIZE_T_MIN INT_MIN #endif #define NRITEMS() ((self->in >= self->out) ? self->in-self->out : self->in+self->buffersize-self->out) /* debug and error log functions */ PyObject *log_debug; /* currently not used */ PyObject *log_error; static PyObject *bufferedaoerror; typedef struct { char* buff; int bytes; } bufitem; typedef struct { PyObject_HEAD /* properties of the ao device */ int driver_id; ao_sample_format format; ao_option *options; ao_device *dev; /* pointer to the ao_device if open, NULL otherwise */ int ispaused; int done; /* ring buffer */ int SIZE; /* size in bytes of one item in the buffer */ int buffersize; /* number of items in the buffer */ bufitem *buffer; int in; /* position of next item put in the buffer */ int out; /* position of next item read from buffer */ pthread_mutex_t buffermutex; /* mutex protecting the ring buffer */ pthread_cond_t notempty; /* condition variable signalizing that the ring buffer is not empty */ pthread_cond_t notfull; /* ... and not full */ pthread_mutex_t restartmutex; /* mutex protecting the restart condition variable */ pthread_cond_t restart; /* condition variable signalizing that we should restart after being paused */ pthread_mutex_t devmutex; /* mutex protecting dev */ } bufferedao; /* helper methods */ static ao_option * py_options_to_ao_options(PyObject *py_options) { Py_ssize_t pos = 0; PyObject *key, *val; ao_option *head = NULL; int ret; if ( !PyDict_Check(py_options) ) { PyErr_SetString(PyExc_TypeError, "options has to be a dictionary"); return NULL; } while ( PyDict_Next(py_options, &pos, &key, &val) ) { if (!PyString_Check(key) || !PyString_Check(val)) { PyErr_SetString(PyExc_TypeError, "keys in options may only be strings"); ao_free_options(head); return NULL; } } ret = ao_append_option(&head, PyString_AsString(key), PyString_AsString(val)); if ( ret == 0 ) { PyErr_SetString(bufferedaoerror, "Error appending options"); ao_free_options(head); return NULL; } return head; } /* type methods for bufferedao type */ static void bufferedao_dealloc(bufferedao* self) { ao_close(self->dev); ao_free_options(self->options); if (self->buffer) { int i; for (i=0; ibuffersize; i++) free(self->buffer[i].buff); free(self->buffer); } pthread_mutex_destroy(&self->buffermutex); pthread_cond_destroy(&self->notempty); pthread_cond_destroy(&self->notfull); pthread_mutex_destroy(&self->restartmutex); pthread_cond_destroy(&self->restart); pthread_mutex_destroy(&self->devmutex); self->ob_type->tp_free((PyObject*)self); } static PyObject * bufferedao_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { bufferedao *self; int bufsize; char *driver_name; int i; PyObject *py_options = NULL; static char *kwlist[] = {"bufsize", "SIZE", "driver_name", "bits", "rate", "channels", "byte_format", "options", NULL}; self = (bufferedao *)type->tp_alloc(type, 0); if ( !self ) return NULL; /* default values for sample format */ self->format.bits = 16; self->format.rate = 44100; self->format.channels = 2; self->format.byte_format = 4; /* platform byte order */ /* parse parameters... */ if ( !PyArg_ParseTupleAndKeywords(args, kwds, "iis|iiiiO!", kwlist, &bufsize, &self->SIZE, &driver_name, &self->format.bits, &self->format.rate, &self->format.channels, &self->format.byte_format, &PyDict_Type, &py_options) ) { Py_DECREF(self); return NULL; } if ( (self->driver_id = ao_driver_id(driver_name)) == -1 ) { PyErr_SetString(bufferedaoerror, "unknown driver_name"); Py_DECREF(self); return NULL; } /* ... and possibly contained options */ self->options = NULL; if (py_options && PyDict_Size(py_options) ) { /* In the case of an empty dictionary, py_options_to_ao_options would return NULL. * Thus, we should (and need) not call it in this case */ if ( !(self->options = py_options_to_ao_options(py_options)) ) { Py_DECREF(self); return NULL; } } /* calculate number of items in the ring buffer from bufsize which is in kB and SIZE in bytes */ self->buffersize = 1024*bufsize/self->SIZE + 1; if ( !( self->buffer = (bufitem *) malloc(sizeof(bufitem) * self->buffersize) ) ) { Py_DECREF(self); return NULL; } for (i=0; ibuffersize; i++) { if ( !( self->buffer[i].buff = (char *) malloc(sizeof(char) * self->SIZE) ) ) { /* deallocate everything already allocated if we run out of memory */ int j; for (j=0; jbuffer[j].buff); free(self->buffer); Py_DECREF(self); return NULL; } } self->in = 0; self->out = 0; pthread_mutex_init(&self->buffermutex, 0); pthread_cond_init(&self->notempty, 0); pthread_cond_init(&self->notfull, 0); self->ispaused = 0; self->done = 0; pthread_mutex_init(&self->restartmutex, 0); pthread_cond_init(&self->restart, 0); pthread_mutex_init(&self->devmutex, 0); return (PyObject *)self; } /* own methods */ static PyObject * bufferedao_start(bufferedao *self) { char *buff; int bytes; int errorlogged; Py_BEGIN_ALLOW_THREADS while ( !self->done ) { pthread_mutex_lock(&self->restartmutex); while (self->ispaused) pthread_cond_wait(&self->restart, &self->restartmutex); pthread_mutex_unlock(&self->restartmutex); /* ring-buffer get code */ pthread_mutex_lock(&self->buffermutex); while ( self->in == self->out ) pthread_cond_wait(&self->notempty, &self->buffermutex); /* we can safely drop the mutex here, assuming that we are the only reader, and thus * the only one modyfing self->in and the corresponding buffer item */ pthread_mutex_unlock(&self->buffermutex); buff = self->buffer[self->out].buff; bytes = self->buffer[self->out].bytes; if (bytes) { pthread_mutex_lock(&self->devmutex); /* try to open audiodevice, if this has not yet happened. * This corresponds to the opendevice method in Python code. However, we have to be more careful here * since we have to guarantee, that the pointer we get has not been modified (i.e. set to NULL) by * the closedevice method */ errorlogged = 0; while (self->dev == NULL ) { self->dev = ao_open_live(self->driver_id, &self->format, self->options); if ( self->dev == NULL ) { int errsv = errno; char *ao_errorstring=""; char errorstring[128]; pthread_mutex_unlock(&self->devmutex); if (!errorlogged) { Py_BLOCK_THREADS /* XXX report details of error */ switch (errsv) { case AO_ENODRIVER: ao_errorstring = "No driver corresponds to driver_id."; break; case AO_ENOTLIVE: ao_errorstring = "This driver is not a live output device."; break; case AO_EBADOPTION: ao_errorstring = "A valid option key has an invalid value."; break; case AO_EOPENDEVICE: ao_errorstring ="Cannot open the device."; break; case AO_EFAIL: ao_errorstring = "Unknown failure"; break; } snprintf(errorstring, 128, "cannot open audio device: %s", ao_errorstring); PyObject *result = PyObject_CallFunction(log_error, "s", errorstring); Py_XDECREF(result); Py_UNBLOCK_THREADS errorlogged = 1; } sleep(1); pthread_mutex_lock(&self->devmutex); } } ao_play(self->dev, buff, bytes); pthread_mutex_unlock(&self->devmutex); } /* we have to reacquire the mutex before sending the signal */ pthread_mutex_lock(&self->buffermutex); self->out = (self->out + 1) % self->buffersize; pthread_mutex_unlock(&self->buffermutex); pthread_cond_signal(&self->notfull); } Py_END_ALLOW_THREADS Py_INCREF(Py_None); return Py_None; } static PyObject * bufferedao_play(bufferedao *self, PyObject *args) { char *buff; int bytes; int len; if ( !PyArg_ParseTuple(args, "s#i", &buff, &len, &bytes) ) return NULL; if ( len>self->SIZE ) { PyErr_SetString(bufferedaoerror, "buff too long"); return NULL; } Py_BEGIN_ALLOW_THREADS /* ring-buffer put code */ pthread_mutex_lock(&self->buffermutex); /* note that we can store actually only one item less then buffersize, because * otherwise we are not able to detect whether the ring buffer is empty or full */ while ( NRITEMS() == self->buffersize-1 ) pthread_cond_wait(&self->notfull, &self->buffermutex); /* we can safely drop the mutex here, assuming that we are the only writer, and thus * the only one modyfing self->in and the corresponding buffer item */ pthread_mutex_unlock(&self->buffermutex); memcpy(self->buffer[self->in].buff, buff, len); self->buffer[self->in].bytes = bytes; /* we have to reacquire the mutex before sending the signal */ pthread_mutex_lock(&self->buffermutex); self->in = (self->in + 1) % self->buffersize; pthread_mutex_unlock(&self->buffermutex); pthread_cond_signal(&self->notempty); Py_END_ALLOW_THREADS Py_INCREF(Py_None); return Py_None; } static PyObject * bufferedao_closedevice(bufferedao *self) { Py_BEGIN_ALLOW_THREADS pthread_mutex_lock(&self->devmutex); if (self->dev) { ao_close(self->dev); /* we use self->dev == NULL as a marker for a closed audio device */ self->dev = NULL; } pthread_mutex_unlock(&self->devmutex); Py_END_ALLOW_THREADS Py_INCREF(Py_None); return Py_None; } static PyObject * bufferedao_queuelen(bufferedao *self) { return PyFloat_FromDouble(1.0/(self->format.channels * self->format.bits / 8) * self->SIZE / self->format.rate * NRITEMS()); } static PyObject * bufferedao_flush(bufferedao *self) { Py_BEGIN_ALLOW_THREADS pthread_mutex_lock(&self->buffermutex); self->in = 0; self->out = 0; pthread_cond_signal(&self->notfull); pthread_mutex_unlock(&self->buffermutex); Py_END_ALLOW_THREADS Py_INCREF(Py_None); return Py_None; } static PyObject * bufferedao_pause(bufferedao *self) { PyObject *retval; self->ispaused = 1; if ( !(retval = PyObject_CallMethod((PyObject *) self, "closedevice", NULL)) ) { return NULL; } Py_DECREF(retval); Py_INCREF(Py_None); return Py_None; } static PyObject * bufferedao_unpause(bufferedao *self) { if ( self->ispaused ) { Py_BEGIN_ALLOW_THREADS pthread_mutex_lock(&self->restartmutex); self->ispaused = 0; pthread_mutex_unlock(&self->restartmutex); pthread_cond_signal(&self->restart); Py_END_ALLOW_THREADS } Py_INCREF(Py_None); return Py_None; } static PyObject * bufferedao_quit(bufferedao *self) { PyObject *retval; self->done = 1; if ( !(retval = PyObject_CallMethod((PyObject *) self, "flush", NULL)) ) { return NULL; } Py_DECREF(retval); if ( !(retval = PyObject_CallMethod((PyObject *) self, "closedevice", NULL)) ) { return NULL; } Py_DECREF(retval); pthread_mutex_lock(&self->restartmutex); self->ispaused = 0; pthread_mutex_unlock(&self->restartmutex); pthread_cond_signal(&self->restart); Py_INCREF(Py_None); return Py_None; } static PyMethodDef bufferedao_methods[] = { {"start", (PyCFunction) bufferedao_start, METH_VARARGS, "start main processing routine (blocks and thus has to be called from a new thread)" }, {"play", (PyCFunction) bufferedao_play, METH_VARARGS, "put buff, bytes on buffer" }, {"closedevice", (PyCFunction) bufferedao_closedevice, METH_NOARGS, "Close audio device until it is needed again" }, {"queuelen", (PyCFunction) bufferedao_queuelen, METH_NOARGS, "Return approximate length of currently buffered PCM data in seconds" }, {"flush", (PyCFunction) bufferedao_flush, METH_NOARGS, "flush currently buffered PCM data" }, {"pause", (PyCFunction) bufferedao_pause, METH_NOARGS, "Pause output" }, {"unpause", (PyCFunction) bufferedao_unpause, METH_NOARGS, "Pause output" }, {"quit", (PyCFunction) bufferedao_quit, METH_NOARGS, "Stop buffered output thread" }, {NULL} /* Sentinel */ }; static PyTypeObject bufferedaoType = { PyObject_HEAD_INIT(NULL) 0, /* ob_size */ "bufferedao.buferredao", /* tp_name */ sizeof(bufferedao), /* tp_basicsize */ 0, /* tp_itemsize */ (destructor)bufferedao_dealloc, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_compare */ 0, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ 0, /* tp_hash */ 0, /* tp_call */ 0, /* tp_str */ 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ "bufferedao objects", /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ 0, /* tp_iter */ 0, /* tp_iternext */ bufferedao_methods, /* tp_methods */ 0, /* tp_members */ 0, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */ 0, /* tp_descr_get */ 0, /* tp_descr_set */ 0, /* tp_dictoffset */ 0, /* tp_init */ 0, /* tp_alloc */ bufferedao_new, /* tp_new */ }; static PyMethodDef module_methods[] = { {NULL} /* Sentinel */ }; #ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ #define PyMODINIT_FUNC void #endif PyMODINIT_FUNC initbufferedao(void) { PyObject* log_module; PyObject *m; PyObject *d; /* import log module and fetch debug and error functions */ if ( !(log_module = PyImport_ImportModule("log")) ) return; d = PyModule_GetDict(log_module); if ( !(log_debug = PyDict_GetItemString(d, "debug")) ) { Py_DECREF(log_module); return; } if ( !(log_error = PyDict_GetItemString(d, "error")) ) { Py_DECREF(log_module); return; } Py_DECREF(log_module); /* initialize the ao library */ ao_initialize(); /* finalize and add extension type to module */ if (PyType_Ready(&bufferedaoType) < 0) return; m = Py_InitModule3("bufferedao", module_methods, "The bufferedao module contains the bufferedao class."); Py_INCREF(&bufferedaoType); PyModule_AddObject(m, "bufferedao", (PyObject *)&bufferedaoType); d = PyModule_GetDict(m); bufferedaoerror = PyErr_NewException("bufferedao.error", NULL, NULL); PyDict_SetItemString(d, "error", bufferedaoerror); Py_DECREF(bufferedaoerror); } PyTone-3.0.3/src/config.py000644 000765 000765 00000100512 11251267120 015645 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2003, 2007 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import ConfigParser, copy, curses, sys, getopt, exceptions, os.path, types, re, types import log, encoding, version class ConfigError(Exception): pass class configsection: """section of the config file""" # we store our configuration items in a separate dictionary _configitems # after instantiation, we only allow reading and writing to this # dictionary (via the corresponding configitems get and set method) def __getattr__(self, name): try: return self._configitems[name].get() except KeyError: raise AttributeError try: return self._configsections[name] except KeyError: raise AttributeError def __setattr__(self, name, value): self._configitems[name].set(value) def __delattr__(self, name): del self._configitems[name] def __getitem__(self, name): try: return self._configitems[name].get() except KeyError: try: return self._configsections[name] except KeyError: raise IndexError def asdict(self): d = {} for n, v in self._configitems.items(): d[n] = v return d def getsubsections(self): return self._configsections.keys() # # define different types of configuration variables # class configitem: def __init__(self, default): self._check(default) self.default = default self.value = None # cached effective output value self._cachedoutput = None def _check(self, s): """ check whether string conforms with expected format If not, this method should raise a ConfigError exception. """ pass def _convert(self, s): """ convert from string to item value """ return s def set(self, s): self._check(s) self.value = s self._cachedoutput = None def get(self): if self._cachedoutput is None: if self.value is None: self._cachedoutput = self._convert(self.default) else: self._cachedoutput = self._convert(self.value) return self._cachedoutput class configstring(configitem): pass class configint(configitem): def _check(self, s): try: int(s) except: raise ConfigError("Expecting float, got '%s'" % s) def _convert(self, s): return int(s) class configfloat(configitem): def _check(self, s): try: float(s) except: raise ConfigError("Expecting float, got '%s'" % s) def _convert(self, s): return float(s) class configlist(configitem): def _check(self, s): try: items = s.split() except: raise ConfigError("Expecting list, got '%s'" % s) def _convert(self, s): return s.split() class configcolor(configitem): # dict of available curses colors # # The entry for "default" is set to -1 (in mainscreen.py) if the # curses.use_default_colors() call succeeds. It then represents a # (possibly transpert) default color. _colors = { "white": curses.COLOR_WHITE, "black": curses.COLOR_BLACK, "green": curses.COLOR_GREEN, "magenta": curses.COLOR_MAGENTA, "blue": curses.COLOR_BLUE, "cyan": curses.COLOR_CYAN, "yellow": curses.COLOR_YELLOW, "red": curses.COLOR_RED, "default" : 0 } _mono = { "none": curses.A_NORMAL, "bold": curses.A_BOLD, "underline": curses.A_UNDERLINE, "reverse": curses.A_REVERSE, "standout": curses.A_STANDOUT } _defaultbg = "default" # disable color support by default. Reenable it in mainscreen.py # if supported by terminal _colorenabled = 0 _colorpairs = [] # two helper methods which parse a color or a mono definition # and return the rest of the line def _parsecolor(self, cdef): if cdef[1].startswith("bright"): fg = self._colors[cdef[1][6:]] bright = 1 else: fg = self._colors[cdef[1]] bright = 0 if len(cdef)>2 and cdef[2]!="mono": bg = self._colors[cdef[2]] return ((fg, bg, bright), cdef[3:]) else: bg = self._colors[self._defaultbg] return ((fg, bg, bright), cdef[2:]) def _parsemono(self, cdef): attr = cdef[1] return (self._mono[attr], cdef[2:]) def _parsecolormono(self, cdef): # parse combined color and mono definition fg = bg = bright = attr = None if cdef[0] == "color": (fg, bg, bright), cdef = self._parsecolor(cdef) if cdef: attr, cdef = self._parsemono(cdef) else: attr = self._mono["none"] elif cdef[0] == "mono": attr, cdef = self._parsemono(cdef) if cdef: (fg, bg, bright), cdef = self._parsecolor(cdef) if cdef: raise ConfigError("color definition too long") return fg, bg, bright, attr def _check(self, s): try: self._parsecolormono(s.split()) except: raise ConfigError("wrong color definition '%s'" %s ) def _convert(self, s): fg, bg, bright, attr = self._parsecolormono(s.split()) if fg is not None and self._colorenabled or attr is None: try: colorindex = self._colorpairs.index((fg, bg))+1 except ValueError: self._colorpairs.append((fg, bg)) colorindex = len(self._colorpairs) curses.init_pair(colorindex, fg, bg) color = curses.color_pair(colorindex) if bright: color |= curses.A_BOLD return color else: return attr class configkeys(configitem): def _check(self, s): if not s: return for key in s.split(" "): keyorig = key if key[:5].lower()=="ctrl-": key = key[5:].upper() elif key[:4].lower()=="alt-": key = key[4:] if key=="KEY_SPACE": pass elif key.startswith("KEY_") and key[4:].isalnum(): try: eval("curses.%s" % key) except: raise ConfigError("wrong key specification '%s'" % keyorig) elif key.startswith("\\") and len(key)==2 and key[1] in ("n", "r", "t"): pass elif len(key)!=1: raise ConfigError("wrong key specification '%s'" % keyorig) def _convert(self, s): keys = [] for key in s.split(" "): modifier = 0 if key[:5].lower()=="ctrl-": key = key[5:].upper() modifier = -64 elif key[:4].lower()=="alt-": key = key[4:] modifier = 1024 if key=="KEY_SPACE": keyvalue = 32 elif key.startswith("KEY_") and key[4:].isalnum(): keyvalue = eval("curses.%s" % key) elif key.startswith("\\") and len(key)==2: keyvalue = ord({"n": "\n", "r": "\r", "t": "\t"}[key[1]]) elif len(key)==1: keyvalue = ord(key) keys.append(keyvalue+modifier) return keys class configboolean(configitem): def _check(self, s): if s not in ("0", "1", "on", "off", "true", "false"): raise ConfigError("Excepting boolean, got '%s'" % s) def _convert(self, s): return s in ("1", "on", "true") class configalternatives(configitem): def __init__(self, default, alternatives): self.alternatives = alternatives configitem.__init__(self, default) def _check(self, s): if s not in self.alternatives: raise ConfigError("Expecting one of %s, got %s" % (str(self.alternatives), s)) class configpath(configitem): def _convert(self, s): if s != "": return os.path.normpath(os.path.expanduser(s)) else: return "" class confignetworklocation(configitem): def _parselocation(self, s): if ":" in s: # address:port name, port = s.split(":") port = int(port) return name, port else: return os.path.expanduser(s) def _check(self, s): try: self._parselocation(s) except: raise ConfigError("Excepting address:port or filename, got '%s'" % s) def _convert(self, s): return self._parselocation(s) class configre(configitem): def _convert(self, s): return re.compile(s) BORDER_TOP = 1 BORDER_BOTTOM = 2 BORDER_LEFT = 4 BORDER_RIGHT = 8 BORDER_COMPACT = 16 BORDER_ULTRACOMPACT = 32 class configborder(configitem): def _check(self, s): if s == "all" or s == "compact" or s == "off" or s == "ultracompact": return for b in s.split(): if b not in ("top", "bottom", "left", "right", "compact"): raise ConfigError("Expecting one of 'top', 'bottom', 'left', or 'right', got '%s'" % b) def _convert(self, s): result = 0 if s == "all": return BORDER_TOP | BORDER_BOTTOM | BORDER_LEFT | BORDER_RIGHT if s == "compact": return BORDER_COMPACT if s == "off": return 0 if s == "ultracompact": return BORDER_ULTRACOMPACT for b in s.split(): if b == "top": result |= BORDER_TOP elif b == "bottom": result |= BORDER_BOTTOM elif b == "left": result |= BORDER_LEFT elif b == "right": result |= BORDER_RIGHT return result ############################################################################## # configuration tree ############################################################################## class general(configsection): logfile = configpath("~/.pytone/pytone.log") songchangecommand = configstring("") playerinfofile = configpath("~/.pytone/playerinfo") dumpfile = configpath("~/.pytone/pytone.dump") debugfile = configpath("") randominsertlength = configfloat("3600") colorsupport = configalternatives("auto", ["auto", "on", "off"]) mousesupport = configboolean("on") layout = configalternatives("twocolumn", ["onecolumn", "twocolumn"]) throttleoutput = configint("0") autoplaymode = configalternatives("off", ["off", "repeat", "random"]) plugins = configlist("") class database(configsection): requestcachesize = configint("50000") class __template__(configsection): type = configalternatives("local", ["local", "remote"]) dbfile = configpath("~/.pytone/main.db") cachesize = configint("1000") musicbasedir = configpath("") tracknrandtitlere = configre(r"^\[?(\d+)\]? ?[- ] ?(.*)\.(mp3|ogg)$") postprocessors = configlist("capitalize strip_leading_article add_decade_tag") autoregisterer = configboolean("on") playingstatslength = configint("100") networklocation = confignetworklocation("localhost:1972") class tag(configsection): class __template__(configsection): name = configstring("") key = configkeys("") class mixer(configsection): type = configalternatives("external", ["external", "internal", "off"]) device = configpath("/dev/mixer") channel = configstring("SOUND_MIXER_PCM") stepsize = configint("5") class network(configsection): socketfile = configstring("~/.pytone/pytonectl") enableserver = configboolean("false") bind = configstring("") port = configint("1972") class player(configsection): class main(configsection): type = configalternatives("internal", ["internal", "xmms", "mpg123", "remote", "off"]) autoplay = configboolean("true") # only for internal player driver = configalternatives("oss", ["alsa", "alsa09", "alsa05", "arts", "esd", "oss", "sun", "macosx", "macosxau", "pulse"]) device = configstring("/dev/dsp") bufsize = configint(100) crossfading = configboolean("true") crossfadingstart = configfloat(5) crossfadingduration = configfloat(6) aooptions = configstring("") # only for xmms player session = configint("0") noqueue = configboolean("false") # only for mpg123 player cmdline = configstring("/usr/bin/mpg321 --skip-printing-frames=5 -a /dev/dsp") # only for remote player networklocation = confignetworklocation("localhost:1972") class secondary(configsection): type = configalternatives("off", ["internal", "xmms", "mpg123", "off"]) autoplay = configboolean("true") # only for internal player driver = configalternatives("oss", ["alsa", "alsa09", "alsa05", "arts", "esd", "oss", "sun", "macosx", "pulse"]) device = configstring("/dev/dsp1") bufsize = configint(100) crossfading = configboolean("true") crossfadingstart = configfloat(5) crossfadingduration = configfloat(6) aooptions = configstring("") # only for xmms player session = configint("0") noqueue = configboolean("false") # only for mpg123 player cmdline = configstring("/usr/bin/mpg321 --skip-printing-frames=5 -a /dev/dsp1") class filelistwindow(configsection): border = configborder("all") scrollbar = configboolean("true") scrollmode = configalternatives("page", ["page", "line"]) virtualdirectoriesattop = configboolean("false") skipsinglealbums = configboolean("true") class playerwindow(configsection): border = configborder("all") songformat = configstring("%(artist)s - %(title)s") class iteminfowindow(configsection): border = configborder("all") class playlistwindow(configsection): border = configborder("all") scrollbar = configboolean("true") scrollmode = configalternatives("page", ["page", "line"]) songformat = configstring("%(artist)s - %(title)s") class mixerwindow(configsection): type = configalternatives("popup", ["popup", "statusbar"]) autoclosetime = configfloat("5") class helpwindow(configsection): autoclosetime = configfloat("0") class logwindow(configsection): autoclosetime = configfloat("10") class statswindow(configsection): autoclosetime = configfloat("10") class iteminfolongwindow(configsection): autoclosetime = configfloat("30") class lyricswindow(configsection): autoclosetime = configfloat("0") class inputwindow(configsection): type = configalternatives("popup", ["popup", "statusbar"]) class colors(configsection): class filelistwindow(configsection): title = configcolor("color brightgreen mono bold") activetitle = configcolor("color brightgreen mono bold") background = configcolor("color white") selected_song = configcolor("color white red mono reverse") artist_album = configcolor("color brightblue mono bold") directory = configcolor("color brightcyan mono bold") border = configcolor("color green") activeborder = configcolor("color brightgreen mono bold") scrollbar = configcolor("color green") scrollbarhigh = configcolor("color brightgreen mono bold") scrollbararrow = configcolor("color brightgreen mono bold") song = configcolor("color white") selected_directory = configcolor("color brightcyan red mono reverse") selected_artist_album = configcolor("color brightblue red mono reverse") class playlistwindow(configsection): title = configcolor("color brightgreen mono bold") activetitle = configcolor("color brightgreen mono bold") background = configcolor("color white") unplayedsong = configcolor("color brightwhite mono bold") selected_unplayedsong = configcolor("color brightwhite red mono reverse") playedsong = configcolor("color white") selected_playedsong = configcolor("color white red mono reverse") playingsong = configcolor("color yellow mono underline") selected_playingsong = configcolor("color yellow red mono reverse") border = configcolor("color green") activeborder = configcolor("color brightgreen mono bold") scrollbar = configcolor("color green") scrollbarhigh = configcolor("color brightgreen mono bold") scrollbararrow = configcolor("color brightgreen mono bold") class playerwindow(configsection): title = configcolor("color brightgreen mono bold") content = configcolor("color white") background = configcolor("color white") description = configcolor("color brightcyan mono bold") activeborder = configcolor("color brightgreen mono bold") progressbar = configcolor("color cyan cyan") border = configcolor("color green") progressbarhigh = configcolor("color red red mono bold") class iteminfowindow(configsection): title = configcolor("color brightgreen mono bold") content = configcolor("color white") background = configcolor("color white") description = configcolor("color brightcyan mono bold") activeborder = configcolor("color brightgreen mono bold") border = configcolor("color green") class iteminfolongwindow(configsection): title = configcolor("color brightgreen mono bold") content = configcolor("color white") background = configcolor("color white") description = configcolor("color brightcyan mono bold") activeborder = configcolor("color brightgreen mono bold") border = configcolor("color green") class lyricswindow(configsection): title = configcolor("color brightgreen mono bold") content = configcolor("color white") background = configcolor("color white") activeborder = configcolor("color brightgreen mono bold") border = configcolor("color green") class inputwindow(configsection): title = configcolor("color brightgreen mono bold") content = configcolor("color white") background = configcolor("color white") description = configcolor("color brightcyan mono bold") activeborder = configcolor("color brightgreen mono bold") border = configcolor("color green") class mixerwindow(configsection): title = configcolor("color brightgreen mono bold") content = configcolor("color white") bar = configcolor("color cyan cyan") description = configcolor("color brightcyan mono bold") border = configcolor("color green") background = configcolor("color white") activeborder = configcolor("color brightgreen mono bold") barhigh = configcolor("color red red mono bold") class helpwindow(configsection): title = configcolor("color brightgreen mono bold") background = configcolor("color white") key = configcolor("color brightcyan mono bold") description = configcolor("color white") activeborder = configcolor("color brightgreen mono bold") border = configcolor("color green") class logwindow(configsection): title = configcolor("color brightgreen mono bold") background = configcolor("color white") time = configcolor("color brightcyan mono bold") debug = configcolor("color white") info = configcolor("color white") warning = configcolor("color cyan") error = configcolor("color red mono bold") activeborder = configcolor("color brightgreen mono bold") border = configcolor("color green") class statswindow(configsection): title = configcolor("color brightgreen mono bold") content = configcolor("color white") background = configcolor("color white") description = configcolor("color brightcyan mono bold") activeborder = configcolor("color brightgreen mono bold") border = configcolor("color green") class statusbar(configsection): key = configcolor("color brightcyan mono bold") background = configcolor("color white") description = configcolor("color white") class keybindings(configsection): class general(configsection): refresh = configkeys("ctrl-l") exit = configkeys("ctrl-x") playerstart = configkeys("p P") playerpause = configkeys("p P") playernextsong = configkeys("n N") playerprevioussong = configkeys("b B") playerforward = configkeys(">") playerrewind = configkeys("<") playerstop = configkeys("S") playlistdeleteplayedsongs = configkeys("KEY_BACKSPACE") playlistclear = configkeys("ctrl-d") playlistsave = configkeys("ctrl-w") playlistreplay = configkeys("ctrl-u") playlisttoggleautoplaymode = configkeys("ctrl-t") togglelayout = configkeys("KEY_F10") showhelp = configkeys("?") showlog = configkeys("!") showstats = configkeys("%") showiteminfolong = configkeys("=") showlyrics = configkeys("L") toggleiteminfowindow = configkeys("ctrl-v") volumeup = configkeys(")") volumedown = configkeys("(") playerplayfaster = configkeys("}") playerplayslower = configkeys("{") playerspeedreset = configkeys("~") playerratecurrentsong1 = configkeys("alt-1") playerratecurrentsong2 = configkeys("alt-2") playerratecurrentsong3 = configkeys("alt-3") playerratecurrentsong4 = configkeys("alt-4") playerratecurrentsong5 = configkeys("alt-5") class filelistwindow(configsection): selectnext = configkeys("KEY_DOWN j") selectprev = configkeys("KEY_UP k") selectnextpage = configkeys("ctrl-n KEY_NPAGE") selectprevpage = configkeys("ctrl-p KEY_PPAGE") selectfirst = configkeys("ctrl-a KEY_HOME") selectlast = configkeys("ctrl-e KEY_END") dirdown = configkeys("KEY_RIGHT KEY_SPACE \n KEY_ENTER l") dirup = configkeys("KEY_LEFT h") addsongtoplaylist = configkeys("KEY_SPACE \n KEY_ENTER KEY_RIGHT") adddirtoplaylist = configkeys("i I KEY_IC alt-KEY_RIGHT") playselectedsong = configkeys("alt-\n alt-KEY_ENTER") activateplaylist = configkeys("\t") insertrandomlist = configkeys("r R") rescan = configkeys("u U") toggledelete = configkeys("D") search = configkeys("/ ctrl-s") repeatsearch = configkeys("ctrl-g") focus = configkeys("f") class playlistwindow(configsection): selectnext = configkeys("KEY_DOWN j") selectprev = configkeys("KEY_UP k") selectnextpage = configkeys("ctrl-n KEY_NPAGE") selectprevpage = configkeys("ctrl-p KEY_PPAGE") selectfirst = configkeys("ctrl-a KEY_HOME") selectlast = configkeys("ctrl-e KEY_END") moveitemup = configkeys("+") moveitemdown = configkeys("-") deleteitem = configkeys("d D KEY_DC") activatefilelist = configkeys("\t KEY_LEFT h") playselectedsong = configkeys("alt-\n alt-KEY_ENTER") rescan = configkeys("u U") shuffle = configkeys("r R") filelistjumptoselectedsong = configkeys("KEY_RIGHT l") # # register known configuration sections # sections = ['mixerwindow', 'helpwindow', 'filelistwindow', 'database', 'tag', 'iteminfowindow', 'logwindow', 'statswindow', 'iteminfolongwindow', 'lyricswindow', 'mixer', 'colors', 'playerwindow', 'playlistwindow', 'general', 'inputwindow', 'network', 'player', 'keybindings'] ############################################################################## # end configuration tree ############################################################################## # options which can be overwritten via the command line userconfigfile = os.path.expanduser("~/.pytone/pytonerc") forcedatabaserebuild = False forcedebugfile = None # # helper functions # # configparser used for the config files configparser = None def setupconfigparser(): """ initialize ConfigParser.RawConfigParser for the standard configuration files """ global configparser cflist = ["/etc/pytonerc", userconfigfile] s = ", ".join(map(os.path.realpath, cflist)) log.info("Using configuration from file(s) %s" % s) configparser = ConfigParser.RawConfigParser() configparser.read(cflist) def readconfigsection(section, clsection): """ fill configsection subclass clsection with entries stored under the name section in configfile and return a corresponding clsection instance""" try: for option in configparser.options(section): if not clsection.__dict__.has_key(option): raise ConfigError("Unkown configuration option '%s' in section '%s'" % (option, section)) value = configparser.get(section, option) clsection.__dict__[option].set(value) except ConfigParser.NoSectionError: pass def finishconfigsection(clsection): """ finish a configsection subclass clsection """ # move configuration items from class __dict__ into _configitems and store # config subsections clsection._configitems = {} clsection._configsections = {} for n, v in clsection.__dict__.items(): if isinstance(v, configitem): clsection._configitems[n] = v del clsection.__dict__[n] elif type(v) == types.ClassType and issubclass(v, configsection) and n != "__template__": finishconfigsection(v) # instantiate class to make __getattr__ and __setattr__ work and add the instance to # a _configsections dictionary clsection.__dict__[n] = clsection._configsections[n] = v() def processstandardconfig(): for section in configparser.sections(): if not section.startswith("plugin."): if "." not in section: # not a subsection if section not in sections: raise ConfigError("Unkown configuration section '%s'" % section) else: # check for valid subsection # first check the form (section.subsection) of the config section try: mainsection, subsection = section.split(".") except: raise ConfigError("Unkown configuration section '%s'" % section) if mainsection not in sections or not subsection.isalnum(): raise ConfigError("Unkown configuration section '%s'" % section) # get corresponding config class try: clsection = eval(section) if not issubclass(clsection, configsection): raise ConfigError("Unkown configuration section '%s'" % section) except AttributeError: # if it does not exist, use a __template__ class in # mainsection if possible try: # We copy the __template__ class by explicitely # creating a new class with a deeply copied class # dictionary (maybe there is an easier solution) # Any templateclassdict = eval("%s.__template__.__dict__" % mainsection) newclass = types.ClassType(mainsection+subsection, (configsection,), copy.deepcopy(templateclassdict)) exec("%s.%s = newclass" % (mainsection, subsection)) except AttributeError: raise ConfigError("Unkown configuration section '%s'" % section) readconfigsection(section, eval(section)) def finishconfig(): """ prepare all configuration sections for later use and read command line arguments """ for section in sections: finishconfigsection(eval(section)) # convert the configsection class into an instance of the same class # to make __getattr__ and __setattr__ work exec("%s = %s()" % (section, section)) in globals(), globals() # apply command line options if forcedatabaserebuild: for databasename in database.getsubsections(): exec("database.%s.autoregisterer = 'on'" % databasename) in globals(), globals() if forcedebugfile: general.debugfile = forcedebugfile def gendefault(): cp = ConfigParser() cp.read("/home/ringo/PyTone/config") for section in cp.sections(): print "class %s(configsection):" % section for option in cp.options(section): default = cp.get(section, option) if option.startswith("color_"): itemname = "configcolor" elif default in ("on", "off", "true", "false"): itemname = "configboolean" else: try: float(default) itemname = "configfloat" except: itemname = "configstring" print ' %s = %s("%s")' % (option, itemname, default) print print "sections =", cp.sections() # # parse command line options # def usage(): print "PyTone %s" % version.version print "Copyright %s" % encoding.encode(version.copyright) print "usage: pytone.py [options]" print "-h, --help: show this help" print "-c, --config : read config from filename" print "-d, --debug : enable debugging output (into filename)" print "-r, --rebuild: rebuild all databases" def processcommandline(): # we pass the information on the command line options via # global variables global userconfigfile global forcedatabaserebuild global forcedebugfile try: # keep rest of arguments for other use global args opts, args = getopt.getopt(sys.argv[1:], "hc:d:r", ["help", "config=", "debug=", "rebuild"]) except getopt.GetoptError: usage() sys.exit(2) for o, a in opts: if o in ("-h", "--help"): usage() sys.exit() if o in ("-d", "--debug"): forcedebugfile = a if o in ("-c", "--config"): userconfigfile = a if o in ("-r", "--rebuild"): forcedatabaserebuild = True def checkoptions(): #if network.enableserver and network.server: # usage() # print "Error: cannot run both as server and as client" # sys.exit(2) import metadata # check database options dbfiles = [] if not database.getsubsections(): print "Please define at least one song database in your configuration file." sys.exit(2) for databasename in database.getsubsections(): songdb = database[databasename] if songdb.type == "local" and songdb.musicbasedir == "": print ( "Please set musicbasedir in the [database.%s] section of the config file ~/.pytone/pytonerc\n" "to the location of your MP3/Ogg Vorbis files." % databasename ) sys.exit(2) if songdb.dbfile != "": if songdb.dbfile in dbfiles: print "dbfile '%s' of database '%s' already in use." % (songdb.dbfile, databasename) sys.exit(2) dbfiles.append(songdb.dbfile) # check ao options for aooption in player.main.aooptions.split() + player.secondary.aooptions.split(): if aooption.count("=")!=1: raise RuntimeError("invalid format for alsa option '%s'" % aooption) def processconfig(): setupconfigparser() processstandardconfig() finishconfig() checkoptions() PyTone-3.0.3/src/cursext/000755 000765 000765 00000000000 11406223507 015527 5ustar00ringoringo000000 000000 PyTone-3.0.3/src/._decoder.py000644 000765 000765 00000000122 11051526156 016223 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/decoder.py000644 000765 000765 00000022430 11051526156 016014 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002, 2003, 2004, 2005 Jörg Lehmann # Ogg Vorbis decoder interface by Byron Ellacott . # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os.path import hub, requests import log import pcm import encoding # # decoder class and simple decoder registry # class decoder: def __init__(self, path): pass def samplerate(self): """ return samplerate in samples per second """ pass def ttime(self): """ return total length of song in seconds """ pass def ptime(self): """ return the current position in the song in seconds """ pass def read(self): """ return pcm stream (16bit, 2 channels) """ pass def seekrelative(self, seconds): """ seek in stream by the given number of seconds (relative to current position) """ pass # mapping: file type -> decoder class _decoders = {} def registerdecoder(type, decoderclass): _decoders[type] = decoderclass def getdecoder(type): return _decoders[type] # # MP3 decoder using libmad # class mp3decoder(decoder): def __init__(self, path): assert isinstance(path, str), "path has to be a string" self.file = mad.MadFile(path) def samplerate(self): return self.file.samplerate() def ttime(self): return self.file.total_time()//1000 def ptime(self): return self.file.current_time()//1000 def read(self): return self.file.read() def seekrelative(self, seconds): time = min(max(self.file.current_time() + seconds*1000, 0), self.file.total_time()) self.file.seek_time(time) try: import mad registerdecoder("mp3", mp3decoder) except ImportError: pass # # Ogg Vorbis decoder # class oggvorbisdecoder(decoder): def __init__(self, path): self.file = ogg.vorbis.VorbisFile(path) def samplerate(self): return self.file.info().rate def ttime(self): return self.file.time_total(0) def ptime(self): return self.file.time_tell() def read(self): buff, bytes, bit = self.file.read() if self.file.info().channels == 2: return buffer(buff, 0, bytes) else: # for mono files, libvorbis really returns a mono stream # (as opposed to libmad) so that we have to "double" the # stream before we return it return pcm.upsample(buffer(buff, 0, bytes)) def seekrelative(self, seconds): time = min(max(self.file.time_tell() + seconds, 0), self.file.time_total(0)) self.file.time_seek(time) try: import ogg.vorbis registerdecoder("ogg", oggvorbisdecoder) except ImportError: pass # # FLAC decoder # class flacdecoder(decoder): def __init__(self, path): self.filedecoder = flac.decoder.FileDecoder() self.filedecoder.set_filename(path) # register callbacks self.filedecoder.set_write_callback(self._write_callback) self.filedecoder.set_error_callback(self._error_callback) self.filedecoder.set_metadata_callback(self._metadata_callback) # init decoder and process (here: ignore) metadata self.filedecoder.init() self.filedecoder.process_until_end_of_metadata() self._ptime = 0 # position in file in seconds # to be able to return the sample rate, we have to decode # some data self.buff = None self.filedecoder.process_single() def _metadata_callback(self, dec, block): if block.type == flac.metadata.STREAMINFO: streaminfo = block.data.stream_info self._samplerate = streaminfo.sample_rate self._channels = streaminfo.channels self._bits_per_sample = streaminfo.bits_per_sample self._ttime = streaminfo.total_samples // self._samplerate def _error_callback(self, dec, block): pass def _write_callback(self, dec, buff, size): self.buff = buff def samplerate(self): return self._samplerate def ttime(self): return self._ttime def ptime(self): return int(self._ptime) def read(self): if self.buff is None: self.filedecoder.process_single() if self.buff is not None: result = self.buff[:] self._ptime += 1.0*len(result)/self._channels/self._bits_per_sample*8/self._samplerate # ok, here it becomes very weird. There seems to be a problem with # the pyflac module, which does not occur (for me!) when # I insert the following code try: for i in range(100): pass except: pass self.buff = None return result def seekrelative(self, seconds): self._ptime += seconds self.filedecoder.seek_absolute(self._ptime * self._samplerate) try: import flac.decoder import flac.metadata registerdecoder("flac", flacdecoder) except ImportError: pass # # main class # class decodedsong: """ song decoder and rate converter This class is for decoding of a song and the conversion of the resulting pcm stream to a defined sample rate. Besides the constructor, there is only one method, namely read, which returns a pcm frame of or less than a given arbitrary size. """ def __init__(self, song, outrate): self.outrate = outrate self.default_rate = outrate try: decoder = getdecoder(song.type) except: log.error("No decoder for song type '%r' registered "% song.type) raise RuntimeError("No decoder for song type '%r' registered "% song.type) url = encoding.encode_path(song.url) if url.startswith("file://"): dbstats = hub.request(requests.getdatabasestats(song.songdbid)) if not dbstats.basedir: log.error("Currently only support for locally stored songs available") raise RuntimeError("Currently only support for locally stored songs available") path = os.path.join(dbstats.basedir, url[7:]) self.decodedfile = decoder(path) else: log.error("Currently only support for locally stored songs available") raise RuntimeError("Currently only support for locally stored songs available") # Use the total time given by the decoder library and not the one # stored in the database. The former one turns out to be more precise # for some VBR songs. self.ttime = max(self.decodedfile.ttime(), song.length) # sometimes the mad library seems to report a wrong sample rate, # so use the one stored in the database if song.samplerate: self.samplerate = song.samplerate else: self.samplerate = self.decodedfile.samplerate() self.buff = self.last_l = self.last_r = None self.buffpos = 0 self.ptime = 0 def read(self, size): if self.buff is not None: bytesleft = len(self.buff) - self.buffpos else: bytesleft = 0 # fill buffer, if necessary while bytesleft < size: newbuff = self.decodedfile.read() if newbuff: self.buff, self.last_l, self.last_r = \ pcm.rate_convert(newbuff, self.samplerate, self.buff, self.buffpos, self.outrate, self.last_l, self.last_r) # the new self.buff contains only new data self.buffpos = 0 bytesleft = len(self.buff) else: size = bytesleft break oldpos = self.buffpos self.buffpos += size self.ptime = self.decodedfile.ptime() if self.buff: return self.buff[oldpos:self.buffpos] else: return [] def seekrelative(self, seconds): self.decodedfile.seekrelative(seconds) self.buff = self.last_l = self.last_r = None self.buffpos = 0 self.ptime = self.decodedfile.ptime() def playslower(self, speed_adj = 441): self.outrate += speed_adj def playfaster(self, speed_adj = 441): # Its absurd that someone would try this # but we better check for it. if (self.outrate - speed_adj) < 1: self.outrate = 1 else: self.outrate -= speed_adj def resetplayspeed(self): self.outrate = self.default_rate PyTone-3.0.3/src/._encoding.py000644 000765 000765 00000000122 10756653223 016413 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/encoding.py000644 000765 000765 00000001610 10756653223 016201 0ustar00ringoringo000000 000000 import sys import locale _fallbacklocalecharset = "iso-8859-1" try: # works only in python > 2.3 _localecharset = locale.getpreferredencoding() except: try: _localecharset = locale.getdefaultlocale()[1] except: try: _localecharset = sys.getdefaultencoding() except: _localecharset = _fallbacklocalecharset if _localecharset in [None, 'ascii', 'ANSI_X3.4-1968']: _localecharset = _fallbacklocalecharset _fs_encoding = sys.getfilesystemencoding() if _fs_encoding in [None, 'ascii', 'ANSI_X3.4-1968']: _fs_encoding = _fallbacklocalecharset # exported functions def encode(ustring): return ustring.encode(_localecharset, "replace") def decode(string): return string.decode(_localecharset, "replace") def decode_path(path): return path.decode(_fs_encoding) def encode_path(path): return path.encode(_fs_encoding) PyTone-3.0.3/src/._errors.py000644 000765 000765 00000000122 10266220577 016137 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/errors.py000644 000765 000765 00000002447 10266220577 015736 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import exceptions class pytoneerror(exceptions.Exception): def __init__(self, value): self.value = value def __str__(self): return "PyTone error: %s" % `self.value` class configurationerror(pytoneerror): def __str__(self): return "PyTone configuration error: %s" % `self.value` class databaseerror(pytoneerror): def __str__(self): return "PyTone database error: %s" % `self.value` class playererror(pytoneerror): def __str__(self): return "PyTone player error: %s" % `self.value` PyTone-3.0.3/src/events.py000644 000765 000765 00000034014 11264371660 015720 0ustar00ringoringo000000 000000 ## -*- coding: ISO-8859-1 -*- # Copyright (C) 2002, 2003, 2004, 2007 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA class event: def __repr__(self): return self.__class__.__name__ class dbevent(event): """ base class for all database service events """ def __init__(self, songdbid): self.songdbid = songdbid def __repr__(self): return "%r(%r)" % (self.__class__.__name__, self.songdbid) class quit(event): """ request end of thread """ pass class keypressed(event): def __init__(self, key): self.key = key def __repr__(self): return "%r(%d)" % (self.__class__.__name__, self.key) class mouseevent(event): def __init__(self, y, x, state): self.y, self.x, self.state = y, x, state def __repr__(self): return "%r(%d, %d, %d)" % (self.__class__.__name__, self.y, self.x, self.state) class selectionchanged(event): def __init__(self, item): self.item = item def __repr__(self): return "%r(%r)" % (self.__class__.__name__, self.item) class focuschanged(event): pass class activateplaylist(event): pass class activatefilelist(event): pass class sendeventat(event): """ send event at alarmtime and every repeat seconds after (if nonzero) or replace the given event""" def __init__(self, event, alarmtime, replace=0): self.event = event self.alarmtime = alarmtime self.repeat = repeat self.replace = replace def __repr__(self): return "%r(%r, %r, %r, %r)" % (self.__class__.__name__, self.event, self.alarmtime, self.repeat, self.replace) class sendeventin(event): """ send event in alartimediff seconds and every repeat seconds after (if nonzero), or replace the given event""" def __init__(self, event, alarmtimediff, repeat=0, replace=0): self.event = event self.alarmtimediff = alarmtimediff self.repeat = repeat self.replace = replace def __repr__(self): return "%r(%r, %r, %r, %r)" % (self.__class__.__name__, self.event, self.alarmtimediff, self.repeat, self.replace) class checkpointdb(dbevent): """flush memory pool, write checkpoint record to log and flush flog of songdbid""" class add_song(dbevent): """ add song to database """ def __init__(self, songdbid, song): self.songdbid = songdbid self.song = song def __repr__(self): return "%r(%r)->%r" % (self.__class__.__name__, self.song, self.songdbid) class update_song(dbevent): """ update song in database """ def __init__(self, songdbid, song): self.songdbid = songdbid self.song = song def __repr__(self): return "%r(%r)->%r" % (self.__class__.__name__, self.song, self.songdbid) class song_played(dbevent): """ register playing of song in database """ def __init__(self, songdbid, song, date_played): self.songdbid = songdbid self.song = song self.date_played = date_played def __repr__(self): return "%r(%r, %r)->%r" % (self.__class__.__name__, self.song, self.date_played, self.songdbid) class song_skipped(dbevent): """ register skipping of song in database """ def __init__(self, songdbid, song): self.songdbid = songdbid self.song = song def __repr__(self): return "%r(%r)->%r" % (self.__class__.__name__, self.song, self.songdbid) class delete_song(dbevent): """ delete song from database """ def __init__(self, songdbid, song): self.songdbid = songdbid self.song = song def __repr__(self): return "%r(%r)->%r" % (self.__class__.__name__, self.song, self.songdbid) class add_playlist(dbevent): """ add playlist to database """ def __init__(self, songdbid, name, songs): self.songdbid = songdbid self.name = name self.songs = songs def __repr__(self): return "%r(%r,%r)->%r" % (self.__class__.__name__, self.name, self.songs, self.songdbid) class update_playlist(dbevent): """ update playlist in database """ def __init__(self, songdbid, name, songs): self.songdbid = songdbid self.name = name self.songs = songs def __repr__(self): return "%r(%r)->%r" % (self.__class__.__name__, self.name, self.songs, self.songdbid) class delete_playlist(dbevent): """ delete playlist from database """ def __init__(self, songdbid, name): self.songdbid = songdbid self.name = name def __repr__(self): return "%r(%r)->%r" % (self.__class__.__name__, self.name, self.songdbid) class autoregistersongs(dbevent): """ start autoregisterer for database If force is set, the m_time of songs is ignored and they are always rescanned. """ def __init__(self, songdbid, force=False): self.songdbid = songdbid self.force = force def __repr__(self): return "%r(%r)->%r" % (self.__class__.__name__, self.force, self.songdbid) class autoregisterer_rescansongs(dbevent): """ rescan songs in given database If force is set, the m_time of songs is ignored and they are always rescanned """ def __init__(self, songdbid, songs, force=False): self.songdbid = songdbid self.songs = songs self.force = force def __repr__(self): return "%r(%r, %r)->%r" % (self.__class__.__name__, self.songs, self.force, self.songdbid) class clearstats(dbevent): """ clear playing and added information of all songs (be carefull!) """ def __init__(self, songdbid): self.songdbid = songdbid def __repr__(self): return "%r->%r" % (self.__class__.__name__, self.songdbid) class songchanged(event): """ song information changed """ def __init__(self, songdbid, song): self.songdbid = songdbid self.song = song def __repr__(self): return "%r(%r)->%r" % (self.__class__.__name__, self.song, self.songdbid) class songschanged(event): "list of songs in database changed" def __init__(self, songdbid): self.songdbid = songdbid def __repr__(self): return "%r->%r" % (self.__class__.__name__, self.songdbid) class artistschanged(event): "list of artists in database changed" def __init__(self, songdbid): self.songdbid = songdbid def __repr__(self): return "%r->%r" % (self.__class__.__name__, self.songdbid) class albumschanged(event): "list of albums in database changed" def __init__(self, songdbid): self.songdbid = songdbid def __repr__(self): return "%r->%r" % (self.__class__.__name__, self.songdbid) class tagschanged(event): "list of tags in database changed" def __init__(self, songdbid): self.songdbid = songdbid def __repr__(self): return "%r->%r" % (self.__class__.__name__, self.songdbid) class dbplaylistchanged(event): def __init__(self, songdbid, playlist): self.songdbid = songdbid self.playlist = playlist def __repr__(self): return "%r(%r)->%r" % (self.__class__.__name__, self.playlist, self.songdbid) class playerevent(event): """ event for the player control """ def __init__(self, playerid): self.playerid = playerid def __repr__(self): return "%r(%r)" % (self.__class__.__name__, self.playerid) class playerstart(playerevent): """ start player """ pass class playerpause(playerevent): """ pause player """ pass class playertogglepause(playerevent): """ pause player, if playing, or start playing, if paused """ pass class playernext(playerevent): """ play next song on player """ pass class playerprevious(playerevent): """ play previous song on player""" pass class playerseekrelative(playerevent): """ seek relative in song by the given number of seconds """ def __init__(self, playerid, seconds): self.playerid = playerid self.seconds = seconds def __repr__(self): return "%r(%f->%r)" % (self.__class__.__name__, self.seconds, self.playerid) class player_change_volume_relative(playerevent): """ change volume of internal player by volume_adj percent""" def __init__(self, playerid, volume_adj): self.playerid = playerid self.volume_adj = volume_adj class playerplayfaster(playerevent): """ increase play speed of song on player""" def __init__(self,playerid): self.playerid = playerid self.speed_adj = 441 class playerplayslower(playerevent): """ decrease play speed of song on player""" def __init__(self,playerid): self.playerid = playerid self.speed_adj = 441 class playerspeedreset(playerevent): """ Reset play speed of song on player back to its original rate""" def __init__(self,playerid): self.playerid = playerid class playerstop(playerevent): """ stop player """ pass class playerplaysong(playerevent): """ play song or playlistitem on player """ def __init__(self, playerid, playlistitemorsong): self.playerid = playerid self.playlistitemorsong = playlistitemorsong def __repr__(self): return "%r(%r->%r)" % (self.__class__.__name__, self.playlistitemorsong, self.playerid) class playerratecurrentsong(playerevent): """ rate song currently being played """ def __init__(self, playerid, rating): playerevent.__init__(self, playerid) self.rating = rating def __repr__(self): return "%r(%r,%d)" % (self.__class__.__name__, self.playerid, self.rating) class playbackinfochanged(event): def __init__(self, playbackinfo): self.playbackinfo = playbackinfo def __repr__(self): return "%r(%r)" % (self.__class__.__name__, self.playbackinfo) class player_volume_changed(event): """ volume of player has been changed """ def __init__(self, playerid, volume): self.playerid = playerid self.volume = volume def __repr__(self): return "%r(%r,%r)" % (self.__class__.__name__, self.playerid, self.volume) class statusbar_update(event): """ update status bar pos = 0: info for currently selected window pos = 1: player info pos = 2: global info """ def __init__(self, pos, content): self.pos = pos self.content = content def __repr__(self): return "%r(%r, %r)" % (self.__class__.__name__, self.pos, self.content) class statusbar_showmessage(event): """ show a message (which automatically disappears after some time in the statusbar""" def __init__(self, message): self.message = message def __repr__(self): return "%r(%r)" % (self.__class__.__name__, self.message) class requestinput(event): def __init__(self, title, prompt, handler): self.title = title self.prompt = prompt self.handler = handler def __repr__(self): return "%r(%r,%r,%r)" % (self.__class__.__name__, self.title, self.prompt, self.handler) class playlistevent(event): pass class playlistaddsongs(playlistevent): """ add songs to playlist """ def __init__(self, songs): self.songs = songs def __repr__(self): return "%r(%r)" % (self.__class__.__name__, self.songs) class playlistaddsongtop(playlistevent): """ add song to top of playlist """ def __init__(self, song): self.song = song def __repr__(self): return "%r(%r)" % (self.__class__.__name__, self.song) class playlistdeletesong(playlistevent): def __init__(self, id): self.id = id def __repr__(self): return "%r(%r)" % (self.__class__.__name__, self.id) class playlistmovesongup(playlistevent): def __init__(self, id): self.id = id def __repr__(self): return "%r(%r)" % (self.__class__.__name__, self.id) class playlistmovesongdown(playlistevent): def __init__(self, id): self.id = id def __repr__(self): return "%r(%r)" % (self.__class__.__name__, self.id) class playlistsave(playlistevent): pass class playlistclear(playlistevent): pass class playlistdeleteplayedsongs(playlistevent): pass class playlistreplay(playlistevent): """mark all songs of playlist unplayed again""" pass class playlistshuffle(playlistevent): pass class playlisttoggleautoplaymode(playlistevent): pass class playlistplaysong(playlistevent): """ immediately play song in playlist """ def __init__(self, id): self.id = id def __repr__(self): return "%r(%r)" % (self.__class__.__name__, self.id) class playlistchanged(event): def __init__(self, items, ptime, ttime, autoplaymode, playingitem): self.items = items self.ptime = ptime self.ttime = ttime self.autoplaymode = autoplaymode self.playingitem = playingitem def __repr__(self): return "%r(%r,%r/%r,%r,%r)" % (self.__class__.__name__, self.items, self.ptime, self.ttime, self.autoplaymode, self.playingitem) class filelistjumptosong(event): """ jump to specific song in filelist window """ def __init__(self, song): self.song = song def __repr__(self): return "%r(%r)" % (self.__class__.__name__, self.song) class hidewindow(event): def __init__(self, window): self.window = window def __repr__(self): return "%r(%r)" % (self.__class__.__name__, self.window) PyTone-3.0.3/src/filelist.py000644 000765 000765 00000023406 11240612755 016227 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002, 2003, 2007 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import config import events, requests, hub import item import slist import log class filelist(slist.slist): def __init__(self, win, songdbids): slist.slist.__init__(self, win, config.filelistwindow.scrollmode == "page") self.basedir = item.basedir(songdbids, rootdir=True) # self.basedir = item.basedir(songdbids) self.dir = [self.basedir] self.shistory = [] self.readdir() self.win.channel.subscribe(events.songschanged, self.songschanged) self.win.channel.subscribe(events.artistschanged, self.artistschanged) self.win.channel.subscribe(events.albumschanged, self.albumschanged) self.win.channel.subscribe(events.tagschanged, self.tagschanged) self.win.channel.subscribe(events.songchanged, self.songchanged) self.win.channel.subscribe(events.dbplaylistchanged, self.dbplaylistchanged) self.win.channel.subscribe(events.filelistjumptosong, self.filelistjumptosong) def isdirselected(self): return isinstance(self.getselected(), item.diritem) def issongselected(self): return isinstance(self.getselected(), item.song) def getselectedsubdir(self): return self.dir + [self.getselected()] def readdir(self): self.set(self.dir[-1].getcontents()) def updatedir(self): """ reread directory trying to keep the current selection """ self.set(self.dir[-1].getcontents(), keepselection=True) def dirdown(self): self.shistory.append((self.dir, self.selected, self.top)) self.dir = self.getselectedsubdir() self.readdir() # In the case of the selected item having been an artist check # whether only one album is present. If yes directly jump to # this album. if config.filelistwindow.skipsinglealbums and isinstance(self.dir[-1], item.artist) and len(self) <= 2: self.dir = self.getselectedsubdir() self.readdir() def dirup(self): if len(self.shistory)>0: dir, selected, top = self.shistory.pop() self.dir = dir self.readdir() self.selected = selected self.top = top # the window size could have changed in the meantime, so we have to update top self._updatetop() self._notifyselectionchanged() def focus_on(self, searchstring): # remove any previous focus if isinstance(self.dir[-1], item.focus_on): self.dirup() self.shistory.append((self.dir, self.selected, self.top)) songdbid = self.dir[-1].songdbid # if filters apply, use them try: filters = self.dir[-1].filters except AttributeError: filters = None self.dir = self.dir + [item.focus_on(songdbid, searchstring, filters)] self.readdir() def selectionpath(self): return self.dir[-1].getheader(self.getselected()) def insertrecursiveselection(self): if self.isdirselected(): songs = self.getselected().getcontentsrecursivesorted() hub.notify(events.playlistaddsongs(songs)) elif self.issongselected(): hub.notify(events.playlistaddsongs([self.getselected()])) def randominsertrecursiveselection(self): if self.isdirselected(): songs = self.getselected().getcontentsrecursiverandom() hub.notify(events.playlistaddsongs(songs)) elif self.issongselected(): hub.notify(events.playlistaddsongs([self.getselected()])) def rateselection(self, rating): if self.isdirselected(): if not isinstance(self.getselected(), (item.artist, item.album)): self.win.sendmessage(_("Not rating virtual directories!")) return False songs = self.getselected().getcontentsrecursive() if rating: self.win.sendmessage(_("Rating %d song(s) with %d star(s)...") % (len(songs), rating)) else: self.win.sendmessage(_("Removing rating of %d song(s)...") % len(songs)) elif self.issongselected(): songs = [self.getselected()] for song in songs: song.rate(rating) return True def addtagselection(self, tag): if self.isdirselected(): if not isinstance(self.getselected(), (item.artist, item.album)): self.win.sendmessage(_("Not tagging virtual directories!")) return False songs = self.getselected().getcontentsrecursive() self.win.sendmessage(_("Tagging %d song(s) with tag '%s'...") % (len(songs), tag)) elif self.issongselected(): songs = [self.getselected()] for song in songs: song.addtag(tag) return True def removetagselection(self, tag): if self.isdirselected(): if not isinstance(self.getselected(), (item.artist, item.album)): self.win.sendmessage(_("Not untagging virtual directories!")) return False songs = self.getselected().getcontentsrecursive() self.win.sendmessage(_("Removing tag '%s' from %d song(s)...") % (tag, len(songs))) elif self.issongselected(): songs = [self.getselected()] for song in songs: song.removetag(tag) return True def toggledeleteselection(self): if self.isdirselected(): if not isinstance(self.getselected(), (item.artist, item.album)): self.win.sendmessage(_("Not (un)deleting virtual directories!")) return False songs = self.getselected().getcontentsrecursive() self.win.sendmessage(_("(Un)deleting %d song(s)...") % len(songs)) elif self.issongselected(): songs = [self.getselected()] for song in songs: song.toggledelete() self.updatedir() return True def rescanselection(self, force): if ( isinstance(self.getselected(), item.basedir) or ( isinstance(self.getselected(), item.filesystemdir) and self.getselected().isbasedir()) ): # instead of rescanning of a whole filesystem we start the autoregisterer self.win.sendmessage(_("Scanning for songs in database '%s'...") % self.getselected().songdbid) hub.notify(events.autoregistersongs(self.getselected().songdbid)) else: if self.isdirselected(): # distribute songs over songdbs # Note that we have to ensure that only dbitem.song (and not item.song) instances # are sent to the db songs = self.getselected().getcontentsrecursive() else: songs = [self.getselected()] self.win.sendmessage(_("Rescanning %d song(s)...") % len(songs)) dsongs = {} for song in songs: dsongs.setdefault(song.songdbid, []).append(song) for songdbid, songs in dsongs.items(): if songs: hub.notify(events.autoregisterer_rescansongs(songdbid, songs, force)) # event handler def songschanged(self, event): if isinstance( self.dir[-1], (item.songs, item.album)): self.updatedir() self.win.update() def artistschanged(self, event): if isinstance( self.dir[-1], item.basedir): self.updatedir() self.win.update() def albumschanged(self, event): if isinstance(self.dir[-1], (item.albums, item.artist, item.compilations)): self.updatedir() self.win.update() def tagschanged(self, event): if isinstance(self.dir[-1], item.tags): self.updatedir() self.win.update() def songchanged(self, event): if isinstance( self.dir[-1], (item.songs, item.album, item.topplayedsongs, item.lastplayedsongs)): self.updatedir() self.win.update() def dbplaylistchanged(self, event): #if (isinstance(self.dir[-1], item.artist) and # self.dir[-1].songdbid==event.songdbid and # self.dir[-1].name in event.album.artists): self.updatedir() self.win.update() def filelistjumptosong(self, event): """ directly jump to given song """ # In order to get the correct shistory, we more or less simulate # a walk through the directory hierarchy, starting from the basedir. self.shistory = [] self.dir = [self.basedir] self.readdir() # either we are able to locate the artist or we should look under compilations if ( (event.song.artist_id and (self.selectbyid(event.song.artist_id) or self.selectbyid("compilations"))) or self.selectbyid("noartist") ): self.dirdown() # We might have skipped the album when there is only a single one of # the given artist. if not isinstance(self.dir[-1], item.album) and self.selectbyid(event.song.album_id): self.dirdown() self.selectbyid(event.song.id) self.win.update() PyTone-3.0.3/src/._filelistwin.py000644 000765 000765 00000000122 10660305371 017146 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/filelistwin.py000644 000765 000765 00000027504 10660305371 016746 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002, 2007 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import curses import config import events, hub import item import filelist import statusbar import window import encoding class filelistwin(window.window): def __init__(self, screen, layout, channel, songdbids): self.channel = channel self.keybindings = config.keybindings.filelistwindow self.songdbids = songdbids # last search string self.searchstring = None # list of selections during incremental search self.searchpositions = [] # last song added to playlist self.lastadded = None h, w, y, x, border = layout window.window.__init__(self, screen, h, w, y, x, config.colors.filelistwindow, "MP3s", border, config.filelistwindow.scrollbar) self.items = filelist.filelist(self, self.songdbids) self.channel.subscribe(events.keypressed, self.keypressed) self.channel.subscribe(events.mouseevent, self.mouseevent) self.channel.subscribe(events.focuschanged, self.focuschanged) def sendmessage(self, message): hub.notify(events.statusbar_showmessage(message)) # allow message to be processed self.channel.process() def updatestatusbar(self): sbar = [] if len(self.items.shistory)>0: sbar += statusbar.generatedescription("filelistwindow", "dirup") sbar += statusbar.separator if self.items.isdirselected(): sbar += statusbar.generatedescription("filelistwindow", "dirdown") sbar += statusbar.separator sbar += statusbar.generatedescription("filelistwindow", "adddirtoplaylist") sbar += statusbar.separator elif self.items.issongselected(): sbar += statusbar.generatedescription("filelistwindow", "addsongtoplaylist") sbar += statusbar.separator sbar += statusbar.generatedescription("filelistwindow", "activateplaylist") hub.notify(events.statusbar_update(0, sbar)) def updatescrollbar(self): self.drawscrollbar(self.items.top, len(self.items)) def searchhandler(self, searchstring, key): if key == curses.KEY_BACKSPACE: if self.searchpositions: self.items.selectbynr(self.searchpositions.pop()) elif key in self.keybindings["repeatsearch"]: self.items.selectbyregexp(searchstring, includeselected=False) elif key == ord("\n"): self.searchpositions = [] self.searchstring = searchstring hub.notify(events.activatefilelist()) elif key == 1023: if self.searchpositions: self.items.selectbynr(self.searchpositions.pop()) self.searchpositions = [] self.searchstring = searchstring hub.notify(events.activatefilelist()) else: self.searchpositions.append(self.items.selected) self.items.selectbyregexp(searchstring) # We explicitely issue a selectionchanged event because the # selectbyregexp doesn't do this due to the focus being on the # searchstring input window hub.notify(events.selectionchanged(self.items.getselected())) self.update() def focus_on_handler(self, searchstring, key): if key == ord("\n") and searchstring: self.items.focus_on(searchstring) def isclickonstring(self, y, x): """ check whether a click was on a string or not """ while x < self.ix+self.iw: if self.win.inch(y, x) & 0xFF!=32: return 1 x += 1 return 0 def resize(self, layout): h, w, y, x, self.border = layout window.window.resize(self, h, w, y, x) self.items._updatetop() # event handler def keypressed(self, event): if self.hasfocus(): key = event.key if key in self.keybindings["selectnext"]: self.items.selectnext() elif key in self.keybindings["selectprev"]: self.items.selectprev() elif key in self.keybindings["selectnextpage"]: self.items.selectnextpage() elif key in self.keybindings["selectprevpage"]: self.items.selectprevpage() elif key in self.keybindings["selectfirst"]: self.items.selectfirst() elif key in self.keybindings["selectlast"]: self.items.selectlast() elif key in self.keybindings["dirdown"] and \ self.items.isdirselected(): self.items.dirdown() elif key in self.keybindings["dirup"]: self.items.dirup() elif key in self.keybindings["addsongtoplaylist"] and \ self.items.issongselected(): songtoadd = self.items.getselected() if self.items.selected is not self.lastadded: self.lastadded = self.items.selected hub.notify(events.playlistaddsongs([songtoadd])) self.items.selectrelative(+1) elif key in self.keybindings["adddirtoplaylist"] and \ self.items.isdirselected(): itemtoadd = self.items.getselected() if self.items.selected is not self.lastadded: self.lastadded = self.items.selected self.items.insertrecursiveselection() self.items.selectrelative(+1) elif key in self.keybindings["playselectedsong"] and \ self.items.issongselected(): songtoplay = self.items.getselected() hub.notify(events.playlistaddsongtop(songtoplay)) elif key in self.keybindings["activateplaylist"]: hub.notify(events.activateplaylist()) elif key in self.keybindings["insertrandomlist"] and self.items.isdirselected(): self.items.randominsertrecursiveselection() elif key in self.keybindings["repeatsearch"]: if self.searchstring: self.items.selectbyregexp(self.searchstring, includeselected=False) elif key in self.keybindings["search"]: hub.notify(events.requestinput(_("Search"), "", self.searchhandler)) elif key in self.keybindings["focus"]: hub.notify(events.requestinput(_("Focus on"), "", self.focus_on_handler)) elif key in self.keybindings["rescan"]: self.items.rescanselection(force=True) self.items.selectrelative(+1) elif key in self.keybindings["toggledelete"]: self.items.toggledeleteselection() elif ord("a")<=key-1024<=ord("z") or ord("A")<=key-1024<=ord("Z") : self.items.selectbyletter(chr(key-1024)) elif ord("0")<=key<=ord("5"): if self.items.rateselection(key-ord("1")+1): self.items.selectrelative(+1) else: return if self.items.selected != self.lastadded: self.lastadded = None self.update() raise hub.TerminateEventProcessing def mouseevent(self, event): if self.enclose(event.y, event.x): y, x = self.stdscrtowin(event.y, event.x) self.top() if event.state & curses.BUTTON1_CLICKED: if x==self.ix+self.iw and self.hasscrollbar: scrollbarbegin, scrollbarheight = self.scrollbardimensions(self.items.top, len(self.items)) if y==self.iy+1: self.items.selectprev() elif y==self.iy+self.ih-2: self.items.selectnext() elif self.iy # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import curses ############################################################################## # descriptions for functions and keys # # descriptions: dictionary with entries # "general": global function descriptions # "filelist": filelist function descriptions # "playlist": playlistlist function descriptions # which map from # name of function to # 2-tuple of very short (for statusbar) and short description of # corresponding function # # keyname: dictionary which maps from keycodes to the names of the keys ############################################################################## descriptions = { "general": { "refresh": (_("refresh"), _("refresh display")), "exit": (_("exit"), _("exit PyTone (press twice)")), "playerstart": (_("play"), _("start main player")), "playerpause": (_("pause"), _("pause main player")), "playernextsong": (_("next song"), _("advance to next song")), "playerprevioussong":(_("previous song"), _("go back to previous song")), "playerrewind": (_("rewind"), _("rewind main player")), "playerforward": (_("forward"), _("forward main player")), "playerstop": (_("stop"), _("stop main player")), "playlistdeleteplayedsongs": (_("delete played"), _("delete played songs from playlist")), "playlistreplay": (_("replay songs"), _("mark all songs in playlist as unplayed")), "playlisttoggleautoplaymode": (_("toggle playlist mode"), _("toggle the playlist mode")), "playlistclear": (_("clear"), _("clear playlist")), "playlistsave": (_("save"), _("save playlist")), "showhelp": (_("help"), _("show help")), "showlog": (_("log"), _("show log messages")), "showstats": (_("statistics"), _("show statistical information about database(s)")), "showiteminfolong": (_("item info"), _("show information about selected item")), "showlyrics": (_("lyrics"), _("show lyrics of selected song")), "toggleiteminfowindow": (_("toggle item info"), _("toggle information shown in item info window")), "togglelayout": (_("toggle layout"), _("toggle layout")), "volumeup": (_("volume up"), _("increase output volume")), "volumedown": (_("volume down"), _("decrease output volume")), "playerplayfaster": (_("play faster"), _("increase the play speed")), "playerplayslower": (_("play slower"), _("decrease the play speed")), "playerspeedreset": (_("default play speed"), _("reset the play speed to normal")), "playerratecurrentsong1": (_("rate current song 1"), _("rate currently playing song with 1 star")), "playerratecurrentsong2": (_("rate current song 2"), _("rate currently playing song with 2 stars")), "playerratecurrentsong3": (_("rate current song 3"), _("rate currently playing song with 3 stars")), "playerratecurrentsong4": (_("rate current song 4"), _("rate currently playing song with 4 stars")), "playerratecurrentsong5": (_("rate current song 5"), _("rate currently playing song with 5 stars")), }, "filelistwindow": { "selectnext": (_("down"), _("move to the next entry")), "selectprev": (_("up"), _("move to the previous entry")), "selectnextpage": (_("page down"), _("move to the next page")), "selectprevpage": (_("page up"), _("move to previous page")), "selectfirst": (_("first"), _("move to the first entry")), "selectlast": (_("last"), _("move to the last entry")), "dirdown": (_("enter dir"), _("enter selected directory")), "dirup": (_("exit dir"), _("go directory up")), "addsongtoplaylist": (_("add song"), _("add song to playlist")), "adddirtoplaylist": (_("add dir"), _("add directory recursively to playlist")), "playselectedsong": (_("immediate play"), _("play selected song immediately")), "activateplaylist": (_("switch to playlist"), _("switch to playlist window")), # "generaterandomlist":(_("random suggestion"), _("generate random song list")), "insertrandomlist": (_("random add dir"), _("add random contents of dir to playlist")), "search": (_("search"), _("search entry")), "repeatsearch": (_("repeat search"), _("repeat last search")), "focus": (_("focus"), _("focus by search string")), "rescan": (_("rescan"), _("rescan/update id3 info for selection")), "toggledelete": (_("(un)delete"), _("delete/undelete selection ")), }, "playlistwindow": { "selectnext": (_("down"), _("move to the next entry")), "selectprev": (_("up"), _("move to the previous entry")), "selectnextpage": (_("page down"), _("move to the next page")), "selectprevpage": (_("page up"), _("move to previous page")), "selectfirst": (_("first"), _("move to the first entry")), "selectlast": (_("last"), _("move to the last entry")), "moveitemup": (_("move song up"), _("move song up")), "moveitemdown": (_("move song down"), _("move song down")), "shuffle": (_("shuffle"), _("shuffle playlist")), "deleteitem": (_("delete"), _("delete entry")), "playselectedsong": (_("immediate play"), _("play selected song immediately")), "activatefilelist": (_("switch to database"), _("switch to database window")), "rescan": (_("rescan"), _("rescan/update id3 info for selection")), "filelistjumptoselectedsong": (_("jump to selected"), _("jump to selected song in filelist window")), } } # prefill keynames keynames = {} for c in range(32): keynames[c] = _("CTRL")+"-"+chr(c+64) for c in range(33, 128): keynames[c] = chr(c) # special keys (incomplete, but sufficient list) keynames[ord("\t")] = _("") keynames[ord("\n")] = _("") keynames[27] = _("") keynames[32] = _("") keynames[curses.KEY_BACKSPACE] = _("") keynames[curses.KEY_DC] = _("") keynames[curses.KEY_DOWN] = _("") keynames[curses.KEY_END] = _("") keynames[curses.KEY_ENTER] = _("") keynames[curses.KEY_HOME] = _("") keynames[curses.KEY_IC] = _("") keynames[curses.KEY_LEFT] = _("") keynames[curses.KEY_SLEFT] = _("") keynames[curses.KEY_NPAGE] = _("") keynames[curses.KEY_PPAGE] = _("") keynames[curses.KEY_RIGHT] = _("") keynames[curses.KEY_SRIGHT] = _("") keynames[curses.KEY_UP] = _("") # function keys for nr in range(1, 23): keynames[eval("curses.KEY_F%d" % nr)] = "" %nr # alt+key for key in keynames.keys(): keynames[key+1024] = "%s-%s"% (_("Alt"), keynames[key]) def getkeyname(key): try: return keynames[key] except KeyError: return "Key_%d" % key PyTone-3.0.3/src/._helper.py000644 000765 000765 00000000122 10266220577 016102 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/helper.py000644 000765 000765 00000003212 10266220577 015670 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002, 2004 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, traceback def formattime(t): return "%2d:%02d" % divmod(t, 60) # Recipe for extended traceback from Python Cookbook def print_exc_plus(): tb = sys.exc_info()[2] while 1: if not tb.tb_next: break tb = tb.tb_next stack = [] f = tb.tb_frame while f: stack.append(f) f = f.f_back stack.reverse() traceback.print_exc(file=sys.stdout) print "Locals by frame, innermost last" for frame in stack: print print "Frame %s in %s at line %s" % (frame.f_code.co_name, frame.f_code.co_filename, frame.f_lineno) for key, value in frame.f_locals.items(): print "\t%20s = " % key, try: print value except: print "" PyTone-3.0.3/src/._helpwin.py000644 000765 000765 00000000122 10557445614 016275 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/helpwin.py000644 000765 000765 00000004636 10557445614 016076 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002, 2003, 2004 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import config import help import events, hub import messagewin import statusbar import encoding def getitems(section): items = [] if section: for function in config.keybindings[section].asdict().keys(): keys = list(config.keybindings[section][function]) keys.sort() keynames = [help.getkeyname(key) for key in keys] descr = help.descriptions[section][function][1] items += [(keynames, descr)] return items class helpwin(messagewin.messagewin): def __init__(self, screen, maxh, maxw, channel): messagewin.messagewin.__init__(self, screen, maxh, maxw, channel, config.colors.helpwindow, _("PyTone Help"), [], config.helpwindow.autoclosetime) sbar = statusbar.generatedescription("general", "showhelp") hub.notify(events.statusbar_update(2, sbar)) def showitems(self): y = self.iy for item in self.items[self.first:]: self.addstr(y, 1, " "*self.iw, self.colors.background) self.move(y, 1) for keyname in item[0][:-1]: self.addstr(encoding.encode(keyname), self.colors.key) self.addstr("/", self.colors.description) self.addstr(encoding.encode(item[0][-1]), self.colors.key) self.addnstr(y, 35, encoding.encode(item[1]), self.iw-35) y += 1 if y>=self.iy+self.ih: break def showhelp(self, context): self.items = getitems("general")+getitems(context) self.items.sort() self.show() PyTone-3.0.3/src/._hub.py000644 000765 000765 00000000122 11051530057 015367 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/hub.py000644 000765 000765 00000012756 11051530057 015172 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2003, 2004 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import exceptions import threading import Queue import events, log class TerminateEventProcessing(exceptions.Exception): pass class DenyRequest(exceptions.Exception): """ deny processing of a request """ pass # from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/87369 class PriorityQueue(Queue.Queue): def _init(self, maxsize): # we need to be sure to have a list as underlying queue self.maxsize = maxsize self.queue = [] def _put(self, item): data, priority = item self._insort_right((priority, data)) def _get(self): return self.queue.pop(0)[1] def _insort_right(self, x): """Insert item x in list, and keep it sorted assuming a is sorted. If x is already in list, insert it to the right of the rightmost x. """ a = self.queue lo = 0 hi = len(a) while lo < hi: mid = (lo+hi)/2 if x[0] < a[mid][0]: hi = mid else: lo = mid+1 a.insert(lo, x) # # request response class # class requestresponse: """ structure containing request + response upon request """ def __init__(self, request): self.request = request self.result = None self.ready = threading.Event() def __repr__(self): return "requestresponse(%r -> %r)" % (self.request, self.result) def waitforcompletion(self): self.ready.wait() def hascompleted(self): return self.ready.isSet() # # event and request dispatcher classes # class hub: """ collects event channels from different threads """ def __init__(self): self.channels = [] def connect(self, channel): self.channels.append(channel) def disconnect(self, channel): self.channels.remove(channel) def newchannel(self): achannel = channel(self) self.connect(achannel) return achannel def notify(self, item, priority=0): """ notify all channels belonging to hub of item (event or request) """ log.debug("event: %s (priority %d)" % (repr(item), priority)) for channel in self.channels: channel._notify(item, priority) def request(self, request, priority=0): """ submit a request (blocking) this method submits a request, waits for the result and returns it. Requests with a high priority are treated first. """ # generate a request response object for the request, # send it to hub and wait for result log.debug("request: %s (priority %d)" % (repr(request), priority)) rr = requestresponse(request) self.notify(rr, priority) rr.waitforcompletion() return rr.result # def requestnoblocking(self, request, priority=0): # """ submit a request (nonblocking) # # this method submits a request and returns a requestresponse # structure. Requests with a high priority are treated first. # """ # rr = requestresponse(request) # self.notify(rr, priority) # return rr class channel: """ collects event handlers for one thread """ def __init__(self, hub): self.hub = hub self.subscriptions = [] self.suppliers = [] self.queue = PriorityQueue(-1) # self.queue = Queue.Queue(-1) def process(self, block=False, timeout=None): """ process queued events and request If block is set, we wait for incoming events and requests. In this case, a timeout in seconds can be specified, as well. """ while True: try: item = self.queue.get(block=block, timeout=timeout) except Queue.Empty: break # after having get the first event, we do no longer block block = False timeout = None if isinstance(item, events.event): try: for subscribedevent, handler in self.subscriptions: if isinstance(item, subscribedevent): handler(item) except TerminateEventProcessing: pass else: for suppliedrequest, handler in self.suppliers: if isinstance(item.request, suppliedrequest): # compute result and signalise that # request has been processed try: item.result = handler(item.request) log.debug(u"got result %r for %r" % (item.result, item.request)) item.ready.set() break except DenyRequest: pass def subscribe(self, eventtype, handler): self.subscriptions.append((eventtype, handler)) def unsubscribe(self, eventtype, handler): self.subscriptions.remove((eventtype, handler)) def supply(self, requesttype, handler): self.suppliers.append((requesttype, handler)) def unsupply(self, requesttype, handler): self.suppliers.remove((requesttype, handler)) def _notify(self, item, priority=0): """ notify channel of item (event or request) """ self.queue.put((item, -priority)) # set up default hub and provide easy access to its externally used methods _defaulthub = hub() newchannel = _defaulthub.newchannel notify = _defaulthub.notify request = _defaulthub.request PyTone-3.0.3/src/inputwin.py000644 000765 000765 00000012231 11342234645 016264 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import config import curses import string import events, hub import statusbar import window # string.printable is not updated when locale is changed (this is a known bug, which # however is not planed to be fixed), so we just do this by ourselves printable = string.digits + string.letters + string.punctuation + string.whitespace class inputwin(window.window): """ generic input window """ def __init__(self, screen, maxh, maxw, channel): self.channel = channel self.keybindings = config.keybindings.general self.inputstring = "" self.inputprompt = "" self.hide() self.channel.subscribe(events.keypressed, self.keypressed) self.channel.subscribe(events.mouseevent, self.mouseevent) self.channel.subscribe(events.requestinput, self.requestinput) # we also need a blinking cursor, whenever we have the focus def hide(self): try: curses.curs_set(0) except: pass window.window.hide(self) def top(self): try: curses.curs_set(1) except: pass window.window.top(self) # event handler def keypressed(self, event): if self.hasfocus(): key = event.key if 32 <= key <= 255 and chr(key) in printable: if len(self.inputstring)+len(self.inputprompt) # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os.path, string, time import config, metadata import events, hub, requests import encoding import helper # # filters # class filter: def __init__(self, name, indexname, indexid): self.name = name self.indexname = indexname self.indexid = indexid def __repr__(self): # for dbrequest cache return "%r=%r" % (self.indexname, self.indexid) def SQL_JOIN_string(self): return "" def SQL_WHERE_string(self): return "" def SQL_args(self): return [] class hiddenfilter(filter): " a filter which does not show up in the UI " def __init__(self, indexname, indexid): filter.__init__(self, None, indexname, indexid) class urlfilter(hiddenfilter): def __init__(self, url): self.url = url hiddenfilter.__init__(self, "url", url) def SQL_WHERE_string(self): return "songs.url = ?" def SQL_args(self): return [self.url] class compilationfilter(hiddenfilter): def __init__(self, iscompilation): self.iscompilation = iscompilation hiddenfilter.__init__(self, "compilation", iscompilation) def SQL_WHERE_string(self): return "%s songs.compilation" % (not self.iscompilation and "NOT" or "") # return "(songs.compilation = %s)" % (self.iscompilation and "1" or "0") class artistfilter(hiddenfilter): def __init__(self, artist_id): self.artist_id = artist_id hiddenfilter.__init__(self, "artist_id", artist_id) def SQL_WHERE_string(self): return "artists.id = ? OR songs.album_artist_id = ?" def SQL_args(self): return [self.artist_id, self.artist_id] class noartistfilter(hiddenfilter): def __init__(self): hiddenfilter.__init__(self, "artist_id", None) def SQL_WHERE_string(self): return "songs.artist_id IS NULL" class albumfilter(hiddenfilter): def __init__(self, album_id): self.album_id = album_id hiddenfilter.__init__(self, "album_id", album_id) def SQL_WHERE_string(self): return "albums.id = ?" def SQL_args(self): return [self.album_id] class playlistfilter(hiddenfilter): def __init__(self, playlist_id): self.playlist_id = playlist_id hiddenfilter.__init__(self, "playlist_id", playlist_id) def SQL_JOIN_string(self): return "JOIN playlistcontents ON playlistcontents.song_id = songs.id" def SQL_WHERE_string(self): return "playlistcontents.playlist_id = ?" def SQL_args(self): return [self.playlist_id] class playedsongsfilter(filter): def __init__(self): filter.__init__(self, _("Played songs"), "playedsongs", "true") def SQL_WHERE_string(self): return "songs.playcount > 0" class searchfilter(filter): def __init__(self, searchstring): self.searchstring = searchstring filter.__init__(self, "Search: %s" % searchstring, None, searchstring) def SQL_WHERE_string(self): #return "(songs.title LIKE ?)" return "(songs.title LIKE ?) OR (albums.name LIKE ?) OR (artists.name LIKE ?)" def SQL_args(self): return ["%%%s%%" % self.searchstring] * 3 class tagfilter(filter): """ filters only items of given tag, if tag_id is not None, use this id to query the database, otherwise use tag_name """ def __init__(self, tag_name, tag_id=None, inverted=False): self.tag_name = tag_name self.tag_id = tag_id self.inverted = inverted name = "%s%s=%s" % (_("Tag"), inverted and "!" or "", tag_name) filter.__init__(self, name, indexname="tag", indexid=tag_name) def __repr__(self): return "tag%r=%r" % (self.inverted and "!" or "", self.tag_name) def SQL_WHERE_string(self): if self.tag_id: return ( "songs.id %sIN (SELECT taggings.song_id FROM taggings WHERE taggings.tag_id = %d)" % (self.inverted and "NOT " or "", self.tag_id) ) else: return ( "songs.id %sIN (SELECT taggings.song_id FROM taggings, tags WHERE taggings.tag_id = tags.id and tags.name=?)" % (self.inverted and "NOT " or "") ) def SQL_args(self): if not self.tag_id: return [self.tag_name] else: return [] class podcastfilter(tagfilter): def __init__(self, inverted=False): tagfilter.__init__(self, "G:Podcast", inverted=inverted) # hide filter self.name = None class deletedfilter(tagfilter): def __init__(self, inverted=False): tagfilter.__init__(self, "S:Deleted", inverted=inverted) # hide filter self.name = None class ratingfilter(filter): """ filters only items of given rating """ def __init__(self, rating): if rating is not None: name = "%s=%s" % (_("Rating"), "*" * rating) else: name = "%s=%s" % (_("Rating"), _("Not rated")) self.rating = rating filter.__init__(self, name, indexname="rating", indexid=rating) def SQL_WHERE_string(self): if self.rating: return "songs.rating = ?" else: return "songs.rating IS NULL" def SQL_args(self): if self.rating: return [self.rating] else: return [] class filters(tuple): def getname(self): s = ", ".join([filter.name for filter in self if filter.name]) if s: return " <%s>" % s else: return "" def added(self, filter): return filters(self + (filter,)) def removed(self, filterclass): return filters(tuple([f for f in self if not isinstance(f, filterclass)])) def contains(self, filterclass): for f in self: if isinstance(f, filterclass): return True return False def SQL_JOIN_string(self): return "\n".join([filter.SQL_JOIN_string() for filter in self]) def SQL_WHERE_string(self): wheres = [filter.SQL_WHERE_string() for filter in self] wheres = ["(%s)" % s for s in wheres if s] filterstring = " AND ".join(wheres) if filterstring: filterstring = "WHERE (%s)" % filterstring return filterstring def SQL_args(self): result = [] for filter in self: result.extend(filter.SQL_args()) return result # helper function for usage in getinfo methods, which merges information about # filters in third and forth columns of lines def _mergefilters(lines, filters): # filter out filters which are to be shown filters = [filter for filter in filters if filter.name] if filters: for nr, filter in enumerate(filters[:4]): if len(lines) > nr: lines[nr][2:3] = [_("Filter:"), filter.name] else: lines.append(["", "", _("Filter:"), filter.name]) return lines class item(object): """ base class for various items presentend in the database and playlist windows.""" def __init__(self, songdbid, id): """ each item has to be bound to a specific database identified by songdbid """ self.songdbid = songdbid self.id = id def getid(self): """ return unique id of item in context """ raise NotImplementedError("has to be implemented by sub classes") def getname(self): """ short name used for item in lists """ raise NotImplementedError("has to be implemented by sub classes") def getinfo(self): """ 4x4 array containing rows and columns used for display of item in iteminfowin""" return [["", "", "", ""]] def getinfolong(self): """ nx4 array containing rows and columns used for display of item in iteminfowin2""" return self.getinfo() class diritem(item): """ item containing other items """ def getname(self): return "[%s]/" % self.name def getid(self): return self.name def getcontents(self): """ return items contained in self """ pass def getcontentsrecursive(self): """ return items contained in self including subdirs (in arbitrary order)""" result = [] for aitem in self.getcontents(): if isinstance(aitem, diritem): result.extend(aitem.getcontentsrecursive()) else: result.append(aitem) return result def getcontentsrecursivesorted(self): """ return items contained in self including subdirs (sorted)""" result = [] for aitem in self.getcontents(): if isinstance(aitem, diritem): result.extend(aitem.getcontentsrecursivesorted()) else: result.append(aitem) return result def getcontentsrecursiverandom(self): """ return random list of items contained in self including subdirs """ # this should be implemented by subclasses return [] def getheader(self, item): """ return header (used for title bar in filelistwin) of item in self. Note that item can be None! """ if item and item.artist and item.album: s = "%s - %s" % (item.artist, item.album) else: s = self.name return s + self.filters.getname() def getinfo(self): return _mergefilters([[self.name, "", "", ""]], self.filters) # # specialized classes # def _formatnumbertotal(number, total): """ return string for number and total number """ if number and total: return "%d/%d" % (number, total) elif number: return "%d" % number else: return "-" class song(item): __slots__ = ["songdbid", "id", "album_id", "artist_id", "song", "playingtime"] def __init__(self, songdbid, id, album_id, artist_id, album_artist_id, date_played=None): """ create song with given id together with its database.""" self.songdbid = songdbid self.id = id self.album_id = album_id self.artist_id = artist_id self.album_artist_id = album_artist_id self.date_played = date_played self.song_metadata = None def __repr__(self): return "song(%r) in %r database" % (self.id, self.songdbid) # the following two methods have to be defined because we use song as a # member of a set in the autoregisterer def __hash__(self): return hash("%r-%d" % (self.songdbid, self.id)) def __eq__(self, other): return isinstance(other, song) and self.songdbid == other.songdbid and self.id == other.id def __getstate__(self): return (self.songdbid, self.id, self.album_id, self.artist_id, self.album_artist_id, self.date_played) def __setstate__(self, tuple): self.songdbid, self.id, self.album_id, self.artist_id, self.album_artist_id, self.date_played = tuple self.song_metadata = None def __getattr__(self, attr): # we refuse to fetch the song metadata if an "internal" method name is queried. # Thus, we do not interfere with pickling of song instances, etc. if attr.startswith("__"): raise AttributeError if not self.song_metadata: self.song_metadata = hub.request(requests.getsong_metadata(self.songdbid, self.id)) # return metadata if we have been able to fetch it, otherwise return None if self.song_metadata: return getattr(self.song_metadata, attr) else: return None def _updatesong_metadata(self): """ notify database of song changes """ hub.notify(events.update_song(self.songdbid, self)) def getid(self): return self.id def getname(self): if self.title: return self.title else: return "DELETED" def getinfo(self): l = [["", "", "", ""]]*4 # if we are unable to fetch the title, the song has been deleted in the meantime if self.title is None: return l l[0] = [_("Title:"), self.title] if self.tracknumber: l[0] += [_("Nr:"), _formatnumbertotal(self.tracknumber, self.trackcount)] else: l[0] += ["", ""] if self.album: l[1] = [_("Album:"), self.album] else: l[1] = [_("URL:"), self.url] if self.year: l[1] += [_("Year:"), str(self.year)] else: l[1] += ["", ""] if self.artist: l[2] = [_("Artist:"), self.artist] else: l[2] = ["", ""] if self.length: l[2] += [_("Time:"), helper.formattime(self.length)] else: l[2] += ["", ""] if self.tags: l[3] = [_("Tags:"), u" | ".join(self.tags)] if self.getplayingtime() is not None: seconds = int((time.time()-self.getplayingtime())/60) days, rest = divmod(seconds, 24*60) hours, minutes = divmod(rest, 60) if days>=10: played = "%dd" % days elif days>0: played = "%dd %dh" % (days, hours) elif hours>0: played = "%dh %dm" % (hours, minutes) else: played = "%dm" % minutes if self.rating: played = played + " (%s)" % ("*"*self.rating) l[3] += [_("Played:"), _("#%d, %s ago") % (self.playcount, played)] else: if self.rating: l[3] += [_("Rating:"), "*" * self.rating] else: l[3] += ["", ""] return l def getinfolong(self): l = [] # if we are unable to fetch the title, the song has been deleted in the meantime if self.title is None: return l l.append([_("Title:"), self.title, "", ""]) l.append([_("Album:"), self.album or "-", "", ""]) l.append([_("Artist:"), self.artist or "-", "", ""]) if self.year: year = str(self.year) else: year = "-" l.append([_("Time:"), "%d:%02d" % divmod(self.length, 60), _("Year:"), year]) l.append([_("Track No:"), _formatnumbertotal(self.tracknumber, self.trackcount), _("Disk No:"), _formatnumbertotal(self.disknumber, self.diskcount)]) l.append([_("Tags:"), u" | ".join(self.tags), _("Rating:"), self.rating and ("*" * self.rating) or "-"]) if self.size: if self.size > 1024*1024: sizestring = "%.1f MB" % (self.size / 1024.0 / 1024) elif self.size > 1024: sizestring = "%.1f kB" % (self.size / 1024.0) else: sizestring = "%d B" % self.size else: sizestring = "" typestring = self.type.upper() if self.bitrate is not None: typestring = "%s %dkbps" % (typestring, self.bitrate/1000) if self.is_vbr: typestring = typestring + "VBR" if self.samplerate: typestring = "%s (%.1f kHz)" % (typestring, self.samplerate/1000.) l.append([_("File type:"), typestring, _("Size:"), sizestring]) replaygain = "" if self.replaygain_track_gain is not None and self.replaygain_track_peak is not None: replaygain = replaygain + "%s: %+f dB (peak: %f) " % (_("track"), self.replaygain_track_gain, self.replaygain_track_peak) if self.replaygain_album_gain is not None and self.replaygain_album_peak is not None: replaygain = replaygain + "%s: %+f dB (peak: %f)" % (_("album"), self.replaygain_album_gain, self.replaygain_album_peak) l.append([_("Replaygain:"), replaygain or "-", _("Beats per minute:"), self.bpm and str(self.bpm) or "-"]) l.append([_("Times played:"), str(self.playcount),_("Times skipped:"), str(self.skipcount)]) l.append([_("Comment:"), self.comments and self.comments[0][2] or "-", _("Lyrics:"), self.lyrics and _("%d lines") % len(self.lyrics[0][2].split("\n")) or "-"]) l.append([_("URL:"), self.url, "", ""]) for played in self.dates_played[-1:-6:-1]: last = int((time.time()-played)/60) days, rest = divmod(last, 24*60) hours, minutes = divmod(rest, 60) if days>0: lastplayed = "%dd %dh %dm" % (days, hours, minutes) elif hours>0: lastplayed = "%dh %dm" % (hours, minutes) else: lastplayed = "%dm" % minutes l.append([_("Played:"), "%s (%s)" % (time.ctime(played), _("%s ago") % lastplayed), "", ""]) return l def format(self, formatstring, adddict={}, safe=False): """format song info using formatstring. Further song information in adddict is added. If safe is True, all values are cleaned of characters which are neither letters, digits, a blank or a colon. """ if self.title is None: return "DELETED" d = {} d.update(self.song_metadata.__dict__) d.update(adddict) d["minutes"], d["seconds"] = divmod(d["length"], 60) d["length"] = "%d:%02d" % (d["minutes"], d["seconds"]) if safe: allowedchars = encoding.decode(string.letters + string.digits + " :") for key, value in d.items(): try: l = [] for c in value: if c in allowedchars: l.append(c) d[key] = "".join(l) except TypeError: pass return unicode(formatstring) % d def rate(self, rating): # just to fetch song metadata oldrating = self.rating # if this was sucessful we can rate the song if self.song_metadata: if rating: self.song_metadata.rating = rating else: self.song_metadata.rating = None self._updatesong_metadata() def addtag(self, tag): tags = self.tags if tags is not None and tag not in tags: tags.append(tag) self.tags = tags self._updatesong_metadata() def removetag(self, tag): tags = self.tags if tags is not None and tag in tags: tags.remove(tag) self.tags = tags self._updatesong_metadata() def toggledelete(self): if self.tags is None: return if "S:Deleted" in self.tags: self.tags.remove("S:Deleted") else: self.tags.append("S:Deleted") self._updatesong_metadata() def getplayingtime(self): """ return time at which this particular song instance has been played or the last playing time, if no such time has been specified at instance creation time """ return self.date_played or self.date_lastplayed class artist(diritem): """ artist bound to specific songdb """ def __init__(self, songdbid, id, name, filters): self.songdbid = songdbid self.id = id self.name = name self.filters = filters.removed(compilationfilter).added(artistfilter(id)) def __repr__(self): return "artist(%r) in %r (filtered: %r)" % (self.name, self.songdbid, self.filters) def getname(self): return "%s/" % self.name def getcontents(self): albums = hub.request(requests.getalbums(self.songdbid, filters=self.filters)) return albums + [songs(self.songdbid, self.name, self.filters)] def getcontentsrecursive(self): return hub.request(requests.getsongs(self.songdbid, filters=self.filters)) def getcontentsrecursivesorted(self): albums = hub.request(requests.getalbums(self.songdbid, filters=self.filters)) result = [] for aalbum in albums: result.extend(aalbum.getcontentsrecursivesorted()) return result def getcontentsrecursiverandom(self): return hub.request(requests.getsongs(self.songdbid, filters=self.filters, random=True)) def getheader(self, item): return self.name + self.filters.getname() def getinfo(self): if self.name == metadata.VARIOUS: # this should not happen, actually artistname = _("Various") else: artistname = self.name return _mergefilters([[_("Artist:"), artistname, "", ""]], self.filters) class album(diritem): """ album bound to specific songdb """ def __init__(self, songdbid, id, artist, name, filters): self.songdbid = songdbid self.id = id self.artist = artist self.name = name self.filters = filters.added(albumfilter(id)) def __repr__(self): return "album(%r) in %r" % (self.id, self.songdbid) class _orderclass: def cmpitem(self, x, y): return ( x.disknumber and y.disknumber and cmp(x.disknumber, y.disknumber) or x.tracknumber and y.tracknumber and cmp(x.tracknumber, y.tracknumber) or cmp(x.title, y.title) ) def SQL_string(self): return "ORDER BY songs.disknumber, songs.tracknumber, songs.title" order = _orderclass() def getid(self): return self.id def getname(self): return "%s/" % self.name def getcontents(self): songs = hub.request(requests.getsongs(self.songdbid, sort=self.order, filters=self.filters)) return songs def getcontentsrecursive(self): return hub.request(requests.getsongs(self.songdbid, filters=self.filters)) def getcontentsrecursiverandom(self): return hub.request(requests.getsongs(self.songdbid, filters=self.filters, random=True)) def getinfo(self): if self.artist == metadata.VARIOUS: artistname = _("Various") else: artistname = self.artist albumname = self.name l = [[_("Artist:"), artistname, "", ""], [_("Album:"), albumname, "", ""]] return _mergefilters(l, self.filters) class playlist(diritem): """ songs in a playlist in the corresponding database """ def __init__(self, songdbid, id, name, nfilters): self.songdbid = songdbid self.id = id self.name = name if nfilters is not None: self.filters = nfilters.added(playlistfilter(id)) else: self.filters = filters((playlistfilter(id),)) def getname(self): return "%s/" % self.name class _orderclass: def SQL_string(self): return "ORDER BY playlistcontents.position" order = _orderclass() def getcontents(self): return hub.request(requests.getsongs(self.songdbid, filters=self.filters, sort=self.order)) getcontentsrecursive = getcontents def getcontentsrecursiverandom(self): return hub.request(requests.getsongs(self.songdbid, filters=self.filters, random=True)) def getheader(self, item): if item and item.artist and item.album: return item.artist + " - " + item.album else: return self.name def getinfo(self): return [["%s:" % _("Playlist"), self.name, "", ""]] class totaldiritem(diritem): """ diritem which contains the total database(s) as its contents """ def getcontentsrecursive(self): return hub.request(requests.getsongs(self.songdbid, filters=self.filters)) getcontentsrecursivesorted = getcontentsrecursive def getcontentsrecursiverandom(self): return hub.request(requests.getsongs(self.songdbid, filters=self.filters, random=True)) class songs(totaldiritem): """ all songs in the corresponding database """ def __init__(self, songdbid, artist=None, filters=None): self.songdbid = songdbid self.id = "songs" self.name = _("Songs") self.artist = artist self.filters = filters self.nrsongs = None def getname(self): if self.nrsongs is None: self.nrsongs = hub.request(requests.getnumberofsongs(self.songdbid, filters=self.filters)) return "[%s (%d)]/" % (self.name, self.nrsongs) class _orderclass: def cmpitem(self, x, y): return ( cmp(x.title, y.title) or cmp(x.album, y.album) or cmp(x.path, y.path) ) def SQL_string(self): return "ORDER BY songs.title, albums.name, songs.url" order = _orderclass() def getcontents(self): songs = hub.request(requests.getsongs(self.songdbid, filters=self.filters, sort=self.order)) self.nrsongs = len(songs) return songs def getinfo(self): if self.artist is not None: l = [[_("Artist:"), self.artist, "", ""], [self.name, "", "", ""]] else: l = [[self.name, "", "", ""]] return _mergefilters(l, self.filters) class noartist(songs): """ list of songs without artist information """ def __init__(self, songdbid, filters): self.songdbid = songdbid self.id = "noartist" self.name = _("No artist") self.filters = filters.added(noartistfilter()) self.nrsongs = None def getinfo(self): return _mergefilters([[self.name, "", "", ""]], self.filters) class randomsongs(totaldiritem): """ random list of songs out of the corresponding database """ def __init__(self, songdbid, maxnr, filters): self.songdbid = songdbid self.id = "randomsongs" self.name = _("Random song list") self.maxnr = maxnr self.filters = filters def getcontents(self): songs = [] while len(songs) 0: songs.extend(newsongs) else: break return songs[:self.maxnr] class lastplayedsongs(diritem): """ songs last played out of the corresponding databases """ def __init__(self, songdbid, filters): self.songdbid = songdbid self.id = "lastplayedsongs" self.filters = filters.added(playedsongsfilter()) self.name = _("Last played songs") class _orderclass: def cmpitem(self, x, y): return cmp(y.getplayingtime(), x.getplayingtime()) def SQL_string(self): return "ORDER BY playstats.date_played DESC LIMIT 100" order = _orderclass() def getcontents(self): return hub.request(requests.getlastplayedsongs(self.songdbid, sort=self.order, filters=self.filters)) getcontentsrecursive = getcontentsrecursivesorted = getcontents def getcontentsrecursiverandom(self): return hub.request(requests.getlastplayedsongs(self.songdbid, filters=self.filters, random=True)) def getinfo(self): return _mergefilters([[self.name, "", "", ""]], self.filters[:-1]) class topplayedsongs(diritem): """ songs most often played of the corresponding databases """ def __init__(self, songdbid, filters): self.songdbid = songdbid self.id = "topplayedsongs" self.filters = filters.added(playedsongsfilter()) self.name = _("Top played songs") class _orderclass: def cmpitem(self, x, y): return cmp(y.playcount, x.playcount) or cmp(y.date_lastplayed, x.date_lastplayed) def SQL_string(self): return "ORDER BY songs.playcount DESC, songs.date_lastplayed DESC LIMIT 100" order = _orderclass() def getcontents(self): songs = hub.request(requests.getsongs(self.songdbid, sort=self.order, filters=self.filters)) return songs getcontentsrecursive = getcontentsrecursivesorted = getcontents def getcontentsrecursiverandom(self): return hub.request(requests.getsongs(self.songdbid, sort=self.order, filters=self.filters, random=True)) def getinfo(self): return _mergefilters([[self.name, "", "", ""]], self.filters[:-1]) class lastaddedsongs(diritem): """ songs last added to the corresponding database """ def __init__(self, songdbid, filters): self.songdbid = songdbid self.id = "lastaddedsongs" self.filters = filters self.name = _("Last added songs") class _orderclass: def cmpitem(self, x, y): return cmp(y.date_added, x.date_added) def SQL_string(self): return "ORDER BY songs.date_added DESC LIMIT 100" order = _orderclass() def getcontents(self): return hub.request(requests.getsongs(self.songdbid, sort=self.order, filters=self.filters)) getcontentsrecursive = getcontentsrecursivesorted = getcontents def getcontentsrecursiverandom(self): return hub.request(requests.getsongs(self.songdbid, sort=self.order, filters=self.filters, random=True)) class albums(totaldiritem): """ all albums in the corresponding database """ def __init__(self, songdbid, filters): self.songdbid = songdbid self.id = "albums" self.filters = filters self.name = _("Albums") self.nralbums = None def getname(self): if self.nralbums is None: self.nralbums = hub.request(requests.getnumberofalbums(self.songdbid, filters=self.filters)) return "[%s (%d)]/" % (self.name, self.nralbums) def getcontents(self): albums = hub.request(requests.getalbums(self.songdbid, filters=self.filters)) self.nralbums = len(albums) return albums def getheader(self, item): if self.nralbums is None: self.nralbums = len(self.getcontents()) return "%s (%d)" % (self.name, self.nralbums) + self.filters.getname() class compilations(albums): def __init__(self, songdbid, filters): filters = filters.added(compilationfilter(True)) albums.__init__(self, songdbid, filters) self.id = "compilations" self.name = _("Compilations") class podcasts(albums): def __init__(self, songdbid, filters): filters = filters.removed(podcastfilter) filters = filters.added(podcastfilter()) albums.__init__(self, songdbid, filters) self.id = "podcasts" self.name = _("Podcasts") class deleted(albums): def __init__(self, songdbid, filters): filters = filters.removed(deletedfilter) filters = filters.added(deletedfilter()) import log log.debug(str(filters)) albums.__init__(self, songdbid, filters) self.id = "deleted" self.name = _("Deleted songs") class tags(totaldiritem): """ all tags in the corresponding database """ def __init__(self, songdbid, songdbids, filters): self.songdbid = songdbid self.id = "tags" self.songdbids = songdbids self.filters = filters self.name = _("Tags") self.nrtags = None self.exclude_tag_ids = [] for filter in self.filters: if isinstance(filter, tagfilter): if not filter.inverted: self.exclude_tag_ids.append(filter.tag_id) def getname(self): if self.nrtags is None: self.nrtags = len(self.getcontents()) return "[%s (%d)]/" % (self.name, self.nrtags) def getcontents(self): tags = hub.request(requests.gettags(self.songdbid, filters=self.filters)) tags = [tag for tag in tags if tag.id not in self.exclude_tag_ids] self.nrtags = len(tags) return tags def getheader(self, item): if self.nrtags is None: self.nrtags = len(self.getcontents()) return "%s (%d)" % (self.name, self.nrtags) + self.filters.getname() class ratings(totaldiritem): """ all ratings in the corresponding database """ def __init__(self, songdbid, songdbids, filters): self.songdbid = songdbid self.id = "ratings" self.songdbids = songdbids self.filters = filters self.name = _("Ratings") self.nrratings = 6 def getname(self): if self.nrratings is None: self.nrratings = len(self.getcontents()) return "[%s (%d)]/" % (self.name, self.nrratings) def getcontents(self): ratings = [rating(self.songdbid, r, self.filters) for r in range(1, 6)] ratings.append(rating(self.songdbid, None, self.filters)) self.nrratings = len(ratings) return ratings def getheader(self, item): if self.nrratings is None: nrratings = len(self.getcontents()) return "%s (%d)" % (self.name, self.nrratings) + self.filters.getname() class playlists(diritem): """ all playlists in the corresponding database """ def __init__(self, songdbid, filters): self.songdbid = songdbid self.id = "playlists" self.name = _("Playlists") self.filters = filters self.nrplaylists = None def getname(self): if self.nrplaylists is None: self.nrplaylists = len(self.getcontents()) return "[%s (%d)]/" % (_("Playlists"), self.nrplaylists) def getcontents(self): playlists = hub.request(requests.getplaylists(self.songdbid, filters=self.filters)) self.nrplaylists = len(playlists) return playlists def getheader(self, item): if self.nrplaylists is None: self.nrplaylists = len(self.getcontents()) return "%s (%d)" % (_("Playlists"), self.nrplaylists) class filesystemdir(diritem): """ diritem corresponding to directory in filesystem """ def __init__(self, songdbid, basedir, dir): self.songdbid = songdbid self.id = "filesystemdir" self.basedir = basedir self.dir = dir if self.dir==self.basedir: self.name = _("Filesystem") else: self.name = encoding.decode_path(self.dir[len(self.basedir):].split("/")[-1]) def getname(self): if self.isbasedir(): return "[%s]/" % _("Filesystem") else: return "%s/" % self.name def getcontents(self): items = [] try: for name in os.listdir(self.dir): try: path = os.path.join(self.dir, name) extension = os.path.splitext(path)[1] if os.path.isdir(path) and os.access(path, os.R_OK|os.X_OK): newitem = filesystemdir(self.songdbid, self.basedir, path) items.append(newitem) elif extension in metadata.getextensions() and os.access(path, os.R_OK): song = hub.request(requests.autoregisterer_queryregistersong(self.songdbid, path)) if song: items.append(song) except (IOError, OSError): pass except OSError: return None items.sort(cmp=lambda x, y: cmp(x.getname(), y.getname())) return items def getcontentsrecursiverandom(self): return [] # songs = self.getcontentsrecursive() # return _genrandomchoice(songs) def getheader(self, item): if self.isbasedir(): return _("Filesystem") else: return self.name def getinfo(self): return [["%s:" % _("Filesystem"), encoding.decode_path(self.dir), "", ""]] def isbasedir(self): """ return whether the filesystemdir is the basedir of a song database """ return self.dir == self.basedir _dbstats = None class basedir(totaldiritem): """ base dir of database view""" def __init__(self, songdbids, afilters=None, rootdir=False): # XXX: as a really dirty hack, we cache the result of getdatabasestats for # all databases because we cannot call this request safely later on # (we might be handling another request which calls the basedir constructor) global _dbstats if _dbstats is None: _dbstats = {} for songdbid in songdbids: _dbstats[songdbid] = hub.request(requests.getdatabasestats(songdbid)) self.name = _("Song Database") self.songdbids = songdbids if len(songdbids) == 1: self.songdbid = songdbids[0] self.type = _dbstats[self.songdbid].type self.basedir = _dbstats[self.songdbid].basedir else: self.songdbid = None self.type = "virtual" self.basedir = None self.id = "basedir" if afilters is None: # add default filters self.filters = filters((podcastfilter(inverted=True), deletedfilter(inverted=True))) else: self.filters = afilters self.rootdir = rootdir self.maxnr = 100 self.nrartists = None self.nrsongs = None self._initvirtdirs() def _initvirtdirs(self): self.virtdirs = [] self.virtdirs.append(noartist(self.songdbid, filters=self.filters)) self.virtdirs.append(compilations(self.songdbid, filters=self.filters)) if self.type == "local" and self.rootdir: self.virtdirs.append(filesystemdir(self.songdbid, self.basedir, self.basedir)) self.virtdirs.append(songs(self.songdbid, filters=self.filters)) self.virtdirs.append(albums(self.songdbid, filters=self.filters)) self.virtdirs.append(podcasts(self.songdbid, filters=self.filters)) self.virtdirs.append(deleted(self.songdbid, filters=self.filters)) if not self.filters.contains(searchfilter): self.virtdirs.append(tags(self.songdbid, self.songdbids, filters=self.filters)) if not self.filters.contains(ratingfilter): self.virtdirs.append(ratings(self.songdbid, self.songdbids, filters=self.filters)) if not self.filters.contains(playedsongsfilter): self.virtdirs.append(topplayedsongs(self.songdbid, filters=self.filters)) self.virtdirs.append(lastplayedsongs(self.songdbid, filters=self.filters)) self.virtdirs.append(playedsongs(self.songdbid, nfilters=self.filters)) self.virtdirs.append(lastaddedsongs(self.songdbid, filters=self.filters)) self.virtdirs.append(randomsongs(self.songdbid, self.maxnr, filters=self.filters)) if not self.filters.contains(searchfilter): self.virtdirs.append(playlists(self.songdbid, filters=self.filters)) if len(self.songdbids) > 1: self.virtdirs.extend([basedir([songdbid], self.filters) for songdbid in self.songdbids]) def getname(self): if self.nrsongs is None: self.nrsongs = hub.request(requests.getnumberofsongs(self.songdbid, filters=self.filters)) if self.basedir: return _("[Database: %s (%d)]") % (self.basedir, self.nrsongs) else: return _("%d databases (%d)") % (len(self.songdbids), self.nrsongs) def getcontents(self): # do not show artists which only appear in compilations filters = self.filters.added(compilationfilter(False)) aartists = hub.request(requests.getartists(self.songdbid, filters=filters)) self.nrartists = len(aartists) # reset cached value self.nrsongs = None if config.filelistwindow.virtualdirectoriesattop: return self.virtdirs + aartists else: return aartists + self.virtdirs def getcontentsrecursivesorted(self): # we cannot rely on the default implementation since we don't want # to have the albums and songs included trice artists = hub.request(requests.getartists(self.songdbid, filters=self.filters)) result = [] for aartist in artists: result.extend(aartist.getcontentsrecursivesorted()) return result def getheader(self, item): if self.nrartists is not None: nrartistsstring = _("%d artists") % self.nrartists else: nrartistsstring = _("? artists") if self.basedir: maxlen = 15 dirname = self.basedir if len(dirname)>maxlen: dirname = "..."+dirname[-maxlen+3:] else: dirname = self.basedir s = _("Database (%s, %s)") % (dirname, nrartistsstring) else: s = _("%d databases (%s)") % (len(self.songdbids), nrartistsstring) return s + self.filters.getname() def getinfo(self): if self.basedir: description = _("[Database: %s (%%d)]") % (self.basedir) else: description = _("%d databases (%%d)") % (len(self.songdbids)) return _mergefilters([[self.name, description, "", ""]], self.filters) class index(basedir): def __init__(self, songdbids, name, description, filters): basedir.__init__(self, songdbids, filters) self.name = name self.description = description self.type = "index" def getname(self): # XXX make this configurable (note that showing the numbers by default is rather costly) if 1: return "%s/" % self.description else: if self.nrsongs is None: self.nrsongs = hub.request(requests.getnumberofsongs(self.songdbid, filters=self.filters)) return "%s (%d)/" % (self.description, self.nrsongs) def getinfo(self): return _mergefilters([[self.name, self.description, "", ""]], self.filters[:-1]) class tag(index): def __init__(self, songdbid, id, name, nfilters): if nfilters is not None: nfilters = nfilters.added(tagfilter(name, tag_id=id)) else: nfilters = filters((tagfilter(name, tag_id=id),)) index.__init__(self, [songdbid], _("Tag:"), name, nfilters) self.id = id class rating(index): def __init__(self, songdbid, r, nfilters): if nfilters is not None: nfilters = nfilters.added(ratingfilter(r)) else: nfilters = filters((ratingfilter(r),)) if r is None: description = _("Not rated") else: description = "*" * r index.__init__(self, [songdbid], _("Rating:"), description, nfilters) self.id = r class playedsongs(index): """ songs played at least once """ def __init__(self, songdbid, nfilters): if nfilters is not None: nfilters = nfilters.added(playedsongsfilter()) else: nfilters = filters((playedsongsfilter(),)) index.__init__(self, [songdbid], _("Played songs:"), "[%s]" % _("Played songs"), nfilters) self.name = _("Played songs") self.id = "playedsongs" def getinfo(self): return _mergefilters([[self.name, "", "", ""]], self.filters[:-1]) class focus_on(index): """ songs filtered by search string """ def __init__(self, songdbid, searchstring, nfilters): if nfilters is not None: nfilters = nfilters.added(searchfilter(searchstring)) else: nfilters = filters((searchfilter(searchstring),)) index.__init__(self, [songdbid], _("Search:"), searchstring, nfilters) self.id = "search: %s" % searchstring PyTone-3.0.3/src/._iteminfowin.py000644 000765 000765 00000000122 10557445550 017156 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/iteminfowin.py000644 000765 000765 00000020251 10557445550 016746 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002, 2003, 2005 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import config import item import services.playlist import events, hub import window import messagewin import encoding # marker class class _selection: pass selection = _selection() class iteminfowin(window.window): def __init__(self, screen, layout, channel, playerids, player): # player for pre-listening self.player = player # list of players for which information can be displayed self.playerids = [playerid for playerid in playerids if playerid is not None] # hash for items (for each view mode) self.items = {} self.items[selection] = None for playerid in self.playerids: self.items[playerid] = None # currently active view mode self.activeview = selection self.keybindings = config.keybindings.general h, w, y, x, border = layout window.window.__init__(self, screen, h, w, y, x, config.colors.iteminfowindow, _("MP3 Info"), border) channel.subscribe(events.selectionchanged, self.selectionchanged) channel.subscribe(events.songchanged, self.songchanged) channel.subscribe(events.playbackinfochanged, self.playbackinfochanged) channel.subscribe(events.keypressed, self.keypressed) def resize(self, layout): h, w, y, x, self.border = layout window.window.resize(self, h, w, y, x) def update(self): # update window title aitem = self.items[self.activeview] title = _("No song") if isinstance(aitem, (item.song, services.playlist.playlistitem)): if isinstance(aitem, item.song): atype = aitem.type else: atype = aitem.song.type if atype == "mp3": title = _("MP3 Info") elif atype == "ogg": title = _("Ogg Info") else: title = _("Song Info") elif isinstance(aitem, item.diritem): title = _("Directory Info") if self.activeview != selection: title = title + " " + _("[Player: %s]") % self.activeview self.settitle(title) window.window.update(self) # get lines to display empty= [["", "", "", ""]] if aitem is not None: info = aitem.getinfo() else: info = [] l = info + empty*(4-len(info)) colsep = self.iw > 45 # calculate width of columns wc1 = max( len(l[0][0]), len(l[1][0]), len(l[2][0]), len(l[3][0])) + colsep wc3 = max( len(l[0][2]), len(l[1][2]), len(l[2][2])) + colsep wc4 = 5 wc4 = max( len(l[0][3]), len(l[1][3]), len(l[2][3])) wc2 = self.iw-wc1-wc3-wc4-1 for lno in range(4): self.move(1+lno, self.ix) l0 = encoding.encode(l[lno][0]) l1 = encoding.encode(l[lno][1]) self.addstr(l0.ljust(wc1)[:wc1], self.colors.description) self.addstr(l1.ljust(wc2)[:wc2], self.colors.content) self.addch(" ") if lno != 3 or isinstance(aitem, item.diritem): l2 = encoding.encode(l[lno][2]) l3 = encoding.encode(l[lno][3]) self.addstr(l2.ljust(wc3)[:wc3], self.colors.description) self.addstr(l3.ljust(wc4)[:wc4], self.colors.content) else: l2 = encoding.encode(l[3][-2]) l3 = encoding.encode(l[3][-1]) # special handling of last line for songs wc3 = max(len(l2), 5) + colsep wc4 = max(len(l3), 5) self.move(1+lno, self.iw-wc3-wc4-1-self.ix) self.addch(" ") self.addstr(l2.ljust(wc3)[:wc3], self.colors.description) self.addstr(l3.ljust(wc4)[:wc4], self.colors.content) # event handler def selectionchanged(self, event): if self.player and self.items[selection] != event.item: if isinstance(event.item, item.song): hub.notify(events.playerplaysong(self.player, event.item)) elif isinstance(event.item, services.playlist.playlistitem): hub.notify(events.playerplaysong(self.player, event.item.song)) self.items[selection] = event.item self.update() def songchanged(self, event): # here we assume that a possible change actually has also affected our # item, if not we're missing it self.update() def playbackinfochanged(self, event): playerid = event.playbackinfo.playerid if playerid in self.playerids: if event.playbackinfo.song != self.items[playerid]: self.items[playerid] = event.playbackinfo.song if self.activeview == playerid: self.update() def keypressed(self, event): key = event.key if key in self.keybindings["toggleiteminfowindow"]: if self.activeview == selection: self.activeview = self.playerids[0] else: i = self.playerids.index(self.activeview) if i < len(self.playerids)-1: self.activeview = self.playerids[i+1] else: self.activeview = selection else: return self.update() raise hub.TerminateEventProcessing class iteminfowinlong(messagewin.messagewin): def __init__(self, screen, maxh, maxw, channel): messagewin.messagewin.__init__(self, screen, maxh, maxw, channel, config.colors.iteminfolongwindow, _("Item info"), [], config.iteminfolongwindow.autoclosetime) self.item = None channel.subscribe(events.selectionchanged, self.selectionchanged) def _outputlen(self, width): return 16 def showitems(self): # get lines to display empty= [["", "", "", ""]] if self.item: info = self.item.getinfolong() else: info = [] l = info + empty*(4-len(info)) colsep = self.iw > 45 # calculate width of columns wc1 = 0 wc3 = 0 wc4 = 0 for line in info: line = map(encoding.encode, line) wc1 = max(wc1, len(line[0])) wc3 = max(wc3, len(line[2])) wc4 = max(wc3, len(line[3])) wc1 += colsep wc3 += colsep wc2 = self.iw-wc1-wc3-wc4-1 self.clear() for lno in range(len(info)): line = map(encoding.encode, l[lno]) self.move(self.iy+lno, self.ix) self.addstr(line[0].ljust(wc1)[:wc1], self.colors.description) self.addstr(line[1].ljust(wc2)[:wc2], self.colors.content) self.addch(" ") if lno!=self.ih: self.addstr(line[2].ljust(wc3)[:wc3], self.colors.description) self.addstr(line[3].ljust(wc4)[:wc4], self.colors.content) else: # special handling of last line wc3 = max(len(line[-2]), 5) + colsep wc4 = max(len(line[-1]), 5) self.move(1+lno, self.iw-wc3-wc4-1-self.ix) self.addch(" ") self.addstr(line[-2].ljust(wc3)[:wc3], self.colors.description) self.addstr(line[-1].ljust(wc4)[:wc4], self.colors.content) def selectionchanged(self, event): self.item = event.item PyTone-3.0.3/src/._log.py000644 000765 000765 00000000122 10756404126 015402 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/log.py000644 000765 000765 00000005141 10756404126 015173 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2004 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import inspect import os.path import sys import threading import time import traceback import codecs _DEBUG = 0 _INFO = 1 _WARNING = 2 _ERROR = 3 _desc = { _DEBUG: "D", _INFO: "I", _WARNING: "W", _ERROR: "E" } # minimal level of log messages that should be stored in the buffer # accesible to the user in the messagewin LOGLEVEL = _INFO # LOGLEVEL = _DEBUG # open file for log output, if necessary debugfile = None # length of path prefix (used to obtain module name from path) pathprefixlen = len(os.path.dirname(__file__)) def initdebugfile(debugfilename): """ direct debugging output to debugfilename """ global debugfile if debugfilename: debugfile = codecs.open(debugfilename, "w", "utf-8") # log buffer consisting of tuples (loglevel, time, logmessage) items = [] # maximal length of log buffer maxitems = 100 def log(s, level): if debugfile: try: frame = inspect.stack() try: timestamp = time.strftime("%H:%M:%S", time.localtime()) threadname = threading.currentThread().getName() modulename = frame[2][1][pathprefixlen+1:-3] debugfile.write("%s [%s|%s|%s] %s\n" % (_desc[level], timestamp, threadname, modulename, s)) finally: del frame except: debugfile.write("%s [???] %s\n" % (_desc[level], s)) if level >= LOGLEVEL: items.append((level, time.time(), s)) if len(items) > maxitems: items.pop(0) def debug(s): log(s, _DEBUG) def info(s): log(s, _INFO) def warning(s): log(s, _WARNING) def error(s): log(s, _ERROR) def debug_traceback(): debug("Exception caught: %s " % sys.exc_info()[1]) tblist = traceback.extract_tb(sys.exc_info()[2]) for s in traceback.format_list(tblist): debug(s[:-1]) PyTone-3.0.3/src/._logwin.py000644 000765 000765 00000000122 10557445613 016125 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/logwin.py000644 000765 000765 00000005502 10557445613 015717 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2004 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import config import log import messagewin import time class logwin(messagewin.messagewin): def __init__(self, screen, maxh, maxw, channel): # column number of message string self.mc = 12 messagewin.messagewin.__init__(self, screen, maxh, maxw, channel, config.colors.logwindow, _("PyTone Messages"), log.items, config.logwindow.autoclosetime) def _outputlen(self, iw): """number of lines in window with inner widht iw""" result = 0 for item in self.items: result += len(item[2])/(iw-self.mc+2)+1 return result def showitems(self): y = self.iy for item in log.items[self.first:]: self.addstr(y, 1, " "*self.iw, self.colors.background) self.addstr(y, 1, log._desc[item[0]][0].upper(), self.colors.time) self.addstr(y, 3, time.strftime("%H:%M:%S", time.localtime(item[1])), self.colors.time) if item[0] == log._DEBUG: color = self.colors.debug elif item[0] == log._INFO: color = self.colors.info elif item[0] == log._WARNING: color = self.colors.warning else: color = self.colors.error # width of message column mw = self.iw-self.mc+1 if len(item[2])<=mw: self.addstr(y, self.mc, item[2], color) else: words = item[2].split() s = words.pop(0) while words and len(s)+len(words[0])=self.iy+self.ih: break self.addstr(y, 1, " "*self.iw, self.colors.background) s=" ".join(words) self.addnstr(y, self.mc, s, mw, color) y += 1 if y>=self.iy+self.ih: break PyTone-3.0.3/src/._lyricswin.py000644 000765 000765 00000000122 10660305312 016633 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/lyricswin.py000644 000765 000765 00000004531 10660305312 016426 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2006, 2007 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import config import item import services.playlist import events import messagewin import encoding class lyricswin(messagewin.messagewin): def __init__(self, screen, maxh, maxw, channel): messagewin.messagewin.__init__(self, screen, maxh, maxw, channel, config.colors.lyricswindow, _("Lyrics"), [], config.lyricswindow.autoclosetime) self.lyrics = _("No lyrics") channel.subscribe(events.selectionchanged, self.selectionchanged) def _outputlen(self, width): try: return len(self.lyrics[0][2].split("\n")) except IndexError: return 1 def showitems(self): self.clear() try: for lno, line in enumerate(self.lyrics[0][2].split("\n")[self.first:self.first+self.ih]): line = encoding.encode(line).strip().center(self.iw) self.addnstr(self.iy+lno, self.ix, line, self.iw, self.colors.content) except IndexError: pass def selectionchanged(self, event): if isinstance(event.item, item.song): song = event.item elif isinstance(event.item, services.playlist.playlistitem): song = event.item.song else: self.settitle(_("Lyrics")) self.lyrics = _("No song selected") return self.settitle("%s - %s - %s" % (song.artist, song.album, song.title)) if song.lyrics: self.lyrics = song.lyrics else: self.lyrics = _("No lyrics") PyTone-3.0.3/src/._mainscreen.py000644 000765 000765 00000000122 10660305326 016741 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/mainscreen.py000644 000765 000765 00000034570 10660305326 016542 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002, 2003, 2004, 2005, 2007 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # uncomment next line for pychecker # import gettext; gettext.install("PyTone", "") import curses, curses.panel, fcntl, os, struct, termios import events, hub import filelistwin import playlistwin import playerwin import iteminfowin import lyricswin import helpwin import inputwin import mixerwin import log import logwin import statswin import statusbar import config class mainscreen: def __init__(self, screen, songdbids, playerids, plugins): self.screen = screen self.layout = config.general.layout self.h, self.w = self.getmaxyx() log.debug("h=%d, w=%d" % (self.h, self.w)) self.channel = hub.newchannel() self.keybindings = config.keybindings.general self.done = False self.statusbar = statusbar.statusbar(screen, self.h-1, self.w, self.channel) # first we setup the input window in order to have it as first window # in the keypressed events lists. if config.inputwindow.type=="popup": self.inputwin = inputwin.popupinputwin(screen, self.h, self.w, self.channel) else: self.inputwin = inputwin.statusbarinputwin(screen, self.h, self.w, self.channel) # setup the four main windows windowslayout = self.calclayout() self.playerwin = playerwin.playerwin(screen, windowslayout["playerwin"], self.channel, playerids[0]) self.iteminfowin = iteminfowin.iteminfowin(screen, windowslayout["iteminfowin"], self.channel, playerids, playerids[1]) self.filelistwin = filelistwin.filelistwin(screen, windowslayout["filelistwin"], self.channel, songdbids) self.playlistwin = playlistwin.playlistwin(screen, windowslayout["playlistwin"], self.channel, "main") self.connectborders() # setup additional windows which appear on demand self.helpwin = helpwin.helpwin(screen, self.h, self.w, self.channel) self.logwin = logwin.logwin(screen, self.h, self.w, self.channel) self.statswin = statswin.statswin(screen, self.h, self.w, self.channel, len(songdbids)) self.iteminfowinlong = iteminfowin.iteminfowinlong(screen, self.h, self.w, self.channel) self.lyricswin = lyricswin.lyricswin(screen, self.h, self.w, self.channel) self.mixerwin = None if config.mixer.device: try: if config.mixerwindow.type=="popup": self.mixerwin = mixerwin.popupmixerwin(screen, self.h, self.w, self.channel) else: self.mixerwin = mixerwin.statusbarmixerwin(screen, self.h, self.w, self.channel) except IOError, e: log.warning('error "%s" during mixer init - disabling mixer' % e) else: # disbable keybindings to obtain correct help window contents del config.keybindings.general.volumeup del config.keybindings.general.volumedown # now we start the plugins for pluginmodule, pluginconfig in plugins: plugin_class = pluginmodule.plugin if plugin_class: plugin = plugin_class(self.channel, pluginconfig, self) plugin.start() self.channel.subscribe(events.keypressed, self.keypressed) self.channel.subscribe(events.activateplaylist, self.activateplaylist) self.channel.subscribe(events.activatefilelist, self.activatefilelist) self.channel.subscribe(events.quit, self.quit) hub.notify(events.activatefilelist()) def run(self): """ main loop of control thread """ skipcount = 0 while not self.done: try: key = self.screen.getch() if key==27: # handle escape sequence (e.g. alt+key) key = self.screen.getch()+1024 if key==curses.KEY_MOUSE: mouse = curses.getmouse() x, y = mouse[1:3] state = mouse[4] hub.notify(events.mouseevent(y, x, state)) elif key==curses.KEY_RESIZE: self.resizeterminal() elif key in self.keybindings["exit"]: curses.halfdelay(5) key = self.screen.getch() curses.halfdelay(1) if key in self.keybindings["exit"]: break elif key!=-1: hub.notify(events.keypressed(key), 50) self.channel.process() # update screen if key == -1 or skipcount >= config.general.throttleoutput: curses.panel.update_panels() curses.doupdate() skipcount = 0 else: skipcount += 1 except KeyboardInterrupt: pass def quit(self, event): """ cleanup """ self.done = True def getmaxyx(self): # taken from http://dag.wieers.com/home-made/dstat/ try: h, w = int(os.environ["LINES"]), int(os.environ["COLUMNS"]) except KeyError: try: h, w = curses.tigetnum('lines'), curses.tigetnum('cols') except: try: s = struct.pack('HHHH', 0, 0, 0, 0) x = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, s) h, w = struct.unpack('HHHH', x)[:2] except: h, w = 25, 80 # take into account minimal height and width according to layout if self.layout == "onecolumn": minh = 27 minw = 25 else: minh = 17 minw = 65 return max(h, minh), max(w, minw) def calclayout(self): """ calculate layout of four main windows for given height h and width w of mainscreen and layout type layout""" result = {} if self.layout == "twocolumn": leftpanelw = int(self.w/2.2) rightpanelw = self.w-leftpanelw rightpanelx = leftpanelw if config.playerwindow.border == config.BORDER_COMPACT: playerwinb = config.BORDER_LEFT | config.BORDER_TOP | config.BORDER_RIGHT elif config.playerwindow.border == config.BORDER_ULTRACOMPACT: playerwinb = config.BORDER_LEFT | config.BORDER_TOP else: playerwinb = config.playerwindow.border if playerwinb & config.BORDER_BOTTOM: playerwinh = 3 else: playerwinh = 2 result["playerwin"] = playerwinh, rightpanelw, 0, rightpanelx, playerwinb if config.iteminfowindow.border == config.BORDER_COMPACT: iteminfowinb = config.BORDER_LEFT | config.BORDER_TOP | config.BORDER_RIGHT elif config.iteminfowindow.border == config.BORDER_ULTRACOMPACT: iteminfowinb = config.BORDER_LEFT | config.BORDER_TOP else: iteminfowinb = config.iteminfowindow.border if iteminfowinb & config.BORDER_BOTTOM: iteminfowinh = 6 else: iteminfowinh = 5 result["iteminfowin"] = iteminfowinh, rightpanelw, playerwinh, rightpanelx, iteminfowinb if config.filelistwindow.border == config.BORDER_COMPACT: filelistwinb = config.BORDER_LEFT | config.BORDER_TOP | config.BORDER_BOTTOM elif config.filelistwindow.border == config.BORDER_ULTRACOMPACT: filelistwinb = config.BORDER_TOP else: filelistwinb = config.filelistwindow.border result["filelistwin"] = self.h-1, leftpanelw, 0, 0, filelistwinb if config.playlistwindow.border == config.BORDER_COMPACT: playlistwinb = config.BORDER_LEFT | config.BORDER_TOP | config.BORDER_RIGHT | config.BORDER_BOTTOM elif config.playlistwindow.border == config.BORDER_ULTRACOMPACT: playlistwinb = config.BORDER_LEFT | config.BORDER_TOP else: playlistwinb = config.playlistwindow.border result["playlistwin"] = (self.h-iteminfowinh-playerwinh-1, rightpanelw, iteminfowinh+playerwinh, rightpanelx, playlistwinb) else: # onecolumn layout if config.playerwindow.border == config.BORDER_COMPACT: playerwinb = config.BORDER_LEFT | config.BORDER_TOP | config.BORDER_RIGHT elif config.playerwindow.border == config.BORDER_ULTRACOMPACT: playerwinb = config.BORDER_TOP else: playerwinb = config.playerwindow.border if playerwinb & config.BORDER_BOTTOM: playerwinh = 3 else: playerwinh = 2 result["playerwin"] = playerwinh, self.w, 0, 0, playerwinb if config.iteminfowindow.border == config.BORDER_COMPACT: iteminfowinb = config.BORDER_LEFT | config.BORDER_TOP | config.BORDER_RIGHT elif config.iteminfowindow.border == config.BORDER_ULTRACOMPACT: iteminfowinb = config.BORDER_TOP else: iteminfowinb = config.iteminfowindow.border if iteminfowinb & config.BORDER_BOTTOM: iteminfowinh = 6 else: iteminfowinh = 5 result["iteminfowin"] = iteminfowinh, self.w, playerwinh, 0, iteminfowinb if config.filelistwindow.border == config.BORDER_COMPACT: filelistwinb = config.BORDER_LEFT | config.BORDER_TOP | config.BORDER_RIGHT elif config.filelistwindow.border == config.BORDER_ULTRACOMPACT: filelistwinb = config.BORDER_TOP else: filelistwinb = config.filelistwindow.border filelistwinh = int(self.h/2) result["filelistwin"] = filelistwinh, self.w, playerwinh+iteminfowinh, 0, filelistwinb if config.playlistwindow.border == config.BORDER_COMPACT: playlistwinb = config.BORDER_LEFT | config.BORDER_TOP | config.BORDER_RIGHT | config.BORDER_BOTTOM elif config.playlistwindow.border == config.BORDER_ULTRACOMPACT: playlistwinb = config.BORDER_TOP else: playlistwinb = config.playlistwindow.border result["playlistwin"] = (self.h-iteminfowinh-playerwinh-filelistwinh-1, self.w, playerwinh+iteminfowinh+filelistwinh, 0, playlistwinb) return result def connectborders(self): borderends = [] mainwindows = [self.filelistwin, self.playerwin, self.iteminfowin, self.playlistwin] for win in mainwindows: borderends.extend(win.getborderends()) for win in mainwindows: win.connectborderends(borderends) win.update() def refresh(self): # refresh screen (i don't know any better way...) self.screen.clear() self.filelistwin.update() self.playlistwin.update() self.playerwin.update() self.iteminfowin.update() self.statusbar.update() def resizeterminal(self): self.h, self.w = self.getmaxyx() windows = self.calclayout() self.filelistwin.resize(windows["filelistwin"]) self.playerwin.resize(windows["playerwin"]) self.iteminfowin.resize(windows["iteminfowin"]) self.playlistwin.resize(windows["playlistwin"]) self.statusbar.resize(self.h-1, self.w) self.helpwin.resize(self.h, self.w) self.logwin.resize(self.h, self.w) self.statswin.resize(self.h, self.w) self.inputwin.resize(self.h, self.w) if self.mixerwin: self.mixerwin.resize(self.h, self.w) self.connectborders() self.filelistwin.update() self.playlistwin.update() self.playerwin.update() self.iteminfowin.update() self.statusbar.update() # event handler def keypressed(self, event): key = event.key if key in self.keybindings["refresh"]: self.refresh() elif key in self.keybindings["playlistdeleteplayedsongs"]: hub.notify(events.playlistdeleteplayedsongs()) elif key in self.keybindings["playlistclear"]: hub.notify(events.playlistclear()) hub.notify(events.activatefilelist()) elif key in self.keybindings["playlistreplay"]: hub.notify(events.playlistreplay()) elif key in self.keybindings["playlistsave"]: hub.notify(events.playlistsave()) elif key in self.keybindings["playlisttoggleautoplaymode"]: hub.notify(events.playlisttoggleautoplaymode()) elif key in self.keybindings["showhelp"]: if self.filelistwin.hasfocus(): context = "filelistwindow" elif self.playlistwin.hasfocus(): context = "playlistwindow" else: context = None self.helpwin.showhelp(context) elif key in self.keybindings["showlog"]: self.logwin.show() elif key in self.keybindings["showstats"]: self.statswin.show() elif key in self.keybindings["showiteminfolong"]: self.iteminfowinlong.show() elif key in self.keybindings["showlyrics"]: self.lyricswin.show() elif key in self.keybindings["togglelayout"]: self.layout = self.layout == "onecolumn" and "twocolumn" or "onecolumn" self.resizeterminal() else: log.debug("unknown key: %d" % key) def activateplaylist(self, event): self.playlistwin.top() def activatefilelist(self, event): self.filelistwin.top() PyTone-3.0.3/src/messagewin.py000644 000765 000765 00000011175 11342234420 016546 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2004 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import config import events, hub import statusbar import version import window # a scrollable window which supports automatic closing class messagewin(window.window): """ a scrollable window which supports automatic closing """ def __init__(self, screen, maxh, maxw, channel, colors, title, items, autoclosetime): self.maxh = maxh self.maxw = maxw self.channel = channel self.autoclosetime = autoclosetime self.items = items self.first = 0 # for identification purposes, we only generate this once self.hidewindowevent = events.hidewindow(self) self.keybindings = config.keybindings.filelistwindow window.window.__init__(self, screen, 1, 1, 0, 0, colors, title) self.channel.subscribe(events.keypressed, self.keypressed) self.channel.subscribe(events.mouseevent, self.mouseevent) self.channel.subscribe(events.hidewindow, self.hidewindow) self.channel.subscribe(events.focuschanged, self.focuschanged) def _outputlen(self, width): """number of lines in window""" return len(self.items) def _resize(self): if self.maxw<=80: width = self.maxw else: width = 80 + int((self.maxw-80)*0.8) height = min(self._outputlen(width-2)+2, self.maxh-3) y = max(0, (self.maxh-height)/2) x = max(0, (self.maxw-width)/2) window.window.resize(self, height, width, y, x) def resize(self, maxh, maxw): self.maxh = maxh self.maxw = maxw self._resize() if self.hasfocus(): self.update() def show(self): self._resize() self.first = 0 self.top() self.update() if self.autoclosetime: hub.notify(events.sendeventin(self.hidewindowevent, self.autoclosetime, replace=1)) def showitems(self): """ has to be implemented by derivec classes """ pass # event handler def keypressed(self, event): if self.hasfocus(): key = event.key # XXX: do we need own keybindings for help? if key in self.keybindings["selectnext"]: self.first = min(self._outputlen(self.iw)-self.ih, self.first+1) elif key in self.keybindings["selectprev"]: self.first = max(0, self.first-1) elif key in self.keybindings["selectnextpage"]: self.first = min(self._outputlen(self.iw)-self.ih, self.first+self.ih) elif key in self.keybindings["selectprevpage"]: self.first = max(0, self.first-self.iw) elif key in self.keybindings["selectfirst"]: self.first = 0 elif key in self.keybindings["selectlast"]: self.first = self._outputlen(self.iw)-self.ih else: self.hide() raise hub.TerminateEventProcessing return self.update() if self.autoclosetime: hub.notify(events.sendeventin(self.hidewindowevent, self.autoclosetime, replace=1)) raise hub.TerminateEventProcessing def mouseevent(self, event): if self.hasfocus(): self.hide() raise hub.TerminateEventProcessing def focuschanged(self, event): if self.hasfocus(): sbar = [("PyTone %s" % version.version, config.colors.statusbar.key)] sbar += statusbar.separator sbar += [(version.copyright, config.colors.statusbar.description)] sbar += statusbar.terminate hub.notify(events.statusbar_update(0, sbar)) def update(self): window.window.update(self) self.showitems() PyTone-3.0.3/src/metadata.py000644 000765 000765 00000052164 11342227117 016174 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2005, 2006, 2007 Jörg Lehmann # # Ogg Vorbis interface by Byron Ellacott . # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os.path, re, struct, string, time import encoding import log # artist name for compilations VARIOUS = u"___VARIOUS___" tracknrandtitlere = re.compile("^\[?(\d+)\]? ?[- ] ?(.*)\.(mp3|ogg)$") ############################################################################## # song metadata class ############################################################################## class song_metadata: url = None type = None title = None album = None artist = None album_artist = None tags = None year = None comments = [] # list of tuples (language, description, text) lyrics = [] # list of tuples (language, description, text) bpm = None tracknumber = None trackcount = None disknumber = None diskcount = None compilation = False length = None size = None bitrate = None is_vbr = None samplerate = None rating = None replaygain_track_gain = None replaygain_track_peak = None replaygain_album_gain = None replaygain_album_peak = None date_added = None date_updated = None dates_played = [] # the following two items are redundant but stored for efficieny reasons date_lastplayed = None playcount = 0 # times fully played skipcount = 0 # times skipped def __init__(self): self.tags = [] self.date_updated = self.date_added = time.time() def __repr__(self): return "metadata(%r)" % (self.url) def __getitem__(self, key): return getattr(self, key) def __setitem__(self, key, value): return setattr(self, key, value) def update(self, other): " merge filesystem metadata from other into self " assert other.url == self.url, RuntimeError("song urls changed") self.title = other.title self.album = other.album self.artist = other.artist self.album_artist = other.album_artist # keep user tags usertags = [tag for tag in self.tags if tag[:2] == "U:"] self.tags = other.tags + usertags self.year = other.year self.comments = other.comments self.lyrics = other.lyrics self.bpm = other.bpm self.tracknumber = other.tracknumber self.trackcount = other.trackcount self.disknumber = other.disknumber self.diskcount = other.diskcount self.compilation = other.compilation self.length = other.length self.size = other.size self.bitrate = other.bitrate self.is_vbr = other.is_vbr self.samplerate = other.samplerate self.replaygain_track_gain = other.replaygain_track_gain self.replaygain_track_peak = other.replaygain_track_peak self.replaygain_album_gain = other.replaygain_album_gain self.replaygain_album_peak = other.replaygain_album_peak self.date_updated = other.date_updated ############################################################################## # registry for metadata decoders for various fileformats ############################################################################## # mapping: file type -> (metadata, decoder class, file extension) _fileformats = {} def registerfileformat(type, metadataclass, extension): _fileformats[type] = (metadataclass, extension) def getmetadatadecoder(type): return _fileformats[type][0] def getextensions(): result = [] for decoder_function, extension in _fileformats.values(): result.append(extension) return result def gettype(extension): for type, extensions in _fileformats.items(): if extension.lower() in extensions: return type return None ############################################################################## # registry for metadata postprocessors for metadata ############################################################################## _metadata_postprocessors = {} # mapping: metadata postprocessor name -> metadata postprocessing function def register_metadata_postprocessor(name, metadata_postprocessor): """ register a metadata postprocessor function of the given name - The name must not contain any whitespace - metadata_postprocessor has to be a callable accepting exactly one parameter which will be an instance of the metadata class. """ _metadata_postprocessors[name] = metadata_postprocessor def get_metadata_postprocessor(name): return _metadata_postprocessors[name] ############################################################################## # factory function for song metadata ############################################################################## def metadata_from_file(relpath, basedir, tracknrandtitlere, postprocessors): """ create song metadata from given file with relative (to basedir) path relpath applying the given list of postprocessors""" path = os.path.normpath(os.path.join(basedir, relpath)) if not os.access(path, os.R_OK): raise IOError("cannot read song") md = song_metadata() md.size = os.stat(path).st_size md.type = gettype(os.path.splitext(path)[1]) read_path_metadata(md, relpath, tracknrandtitlere) try: metadatadecoder = getmetadatadecoder(md.type) except: raise RuntimeError("Support for %s songs not enabled" % md.type) try: log.debug("reading metadata for %r" % path) metadatadecoder(md, path) log.debug("metadata for %r read successfully" % path) except: log.warning("could not read metadata for %r" % path) log.debug_traceback() # strip leading and trailing whitespace if md.title: md.title = md.title.strip() if md.artist: md.artist = md.artist.strip() if md.album: md.album = md.album.strip() if md.length is None: log.warning("could not read length of song %r" % path) raise RuntimeError("could not read length of song %r" % path) for postprocessor_name in postprocessors: try: get_metadata_postprocessor(postprocessor_name)(md) except: log.warning("Postprocessing of song %r metadata with '%r' failed" % (path, postprocessor_name)) log.debug_traceback() # set album_artist if not present if md.album_artist is None: if md.compilation: md.album_artist = VARIOUS else: md.album_artist = md.artist return md def read_path_metadata(md, relpath, tracknrandtitlere): relpath = os.path.normpath(relpath) md.url = u"file://" + encoding.decode_path(relpath) # guesses for title and tracknumber using the filename match = re.match(tracknrandtitlere, os.path.basename(relpath)) if match: fntracknumber = int(match.group(1)) fntitle = match.group(2) else: fntracknumber = None fntitle = os.path.basename(relpath) if fntitle.lower().endswith(".mp3") or fntitle.lower().endswith(".ogg"): fntitle = fntitle[:-4] first, second = os.path.split(os.path.dirname(relpath)) if first and second and not os.path.split(first)[0]: fnartist = first fnalbum = second else: fnartist = fnalbum = "" # now convert this to unicode strings using the standard filesystem encoding fntitle = encoding.decode_path(fntitle) fnartist = encoding.decode_path(fnartist) fnalbum = encoding.decode_path(fnalbum) fntitle = fntitle.replace("_", " ") fnalbum = fnalbum.replace("_", " ") fnartist = fnartist.replace("_", " ") if fntitle: md.title = fntitle if fnartist: md.artist = fnartist if fnalbum: md.album = fnalbum if fntracknumber: md.tracknumber = fntracknumber # accent_trans = string.maketrans('ÁÀÄÂÉÈËÊÍÌÏÎÓÒÖÔÚÙÜÛáàäâéèëêíìïîóòöôúùüû', # 'AAAAEEEEIIIIOOOOUUUUaaaaeeeeiiiioooouuuu') ############################################################################## # ID3 metadata decoder (using mutagen module) ############################################################################## def _splitnumbertotal(s): """ split string into number and total number """ r = map(int, s.split("/")) number = r[0] if len(r) == 2: count = r[1] else: count = None return number, count _mutagen_framemapping = { "TIT2": "title", "TALB": "album", "TPE1": "artist" } def read_mp3_mutagen_metadata(md, path): mp3 = mutagen.mp3.MP3(path, ID3=ID3hack) # we definitely want the MP3 header data, even if no ID3 tag is present, # so extract this info before anything goes wrong md.length = mp3.info.length md.samplerate = mp3.info.sample_rate md.bitrate = mp3.info.bitrate md.comments = [] md.lyrics = [] if mp3.tags: for frame in mp3.tags.values(): if frame.FrameID == "TCON": genre = " ".join(frame.genres) if genre: md.tags.append("G:%s" % genre) elif frame.FrameID == "RVA2": if frame.channel == 1: if frame.desc == "album": basename = "replaygain_album_" else: # for everything else, we assume it's track gain basename = "replaygain_track_" md[basename+"gain"] = frame.gain md[basename+"peak"] = frame.peak elif frame.FrameID == "TLEN": try: # we overwrite the length which maybe has been defined above md.length = int(+frame/1000) except: pass elif frame.FrameID == "TRCK": md.tracknumber, md.trackcount = _splitnumbertotal(frame.text[0]) elif frame.FrameID == "TPOS": md.disknumber, md.diskcount = _splitnumbertotal(frame.text[0]) elif frame.FrameID == "TBPM": md.bpm = int(+frame) #elif frame.FrameID == "TCMP": # self.compilation = True elif frame.FrameID == "TDRC": try: md.year = int(str(frame.text[0])) except: pass elif frame.FrameID == "USLT": md.lyrics.append((frame.lang, frame.desc, frame.text)) elif frame.FrameID == "COMM": md.comments.append((frame.lang, frame.desc, " / ".join(frame.text))) else: name = _mutagen_framemapping.get(frame.FrameID, None) if name: text = " ".join(map(unicode, frame.text)) md[name] = text else: log.debug("Could not read ID3 tags for song '%r'" % path) ############################################################################## # ID3 metadata decoder (using eyeD3 module) ############################################################################## def read_mp3_eyeD3_metadata(md, path): mp3file = eyeD3.Mp3AudioFile(path) mp3info = mp3file.getTag() # we definitely want the length of the MP3 file, even if no ID3 tag is present, # so extract this info before anything goes wrong md.length = mp3file.getPlayTime() md.is_vbr, bitrate = mp3file.getBitRate() md.bitrate = bitrate * 1000 md.samplerate = mp3file.getSampleFreq() md.comments = [] md.lyrics = [] if mp3info: md.title = mp3info.getTitle() md.album = mp3info.getAlbum() md.artist = mp3info.getArtist() try: md.year = int(mp3info.getYear()) except: pass try: genre = mp3info.getGenre() except eyeD3.tag.GenreException, e: genre = e.msg.split(':')[1].strip() if genre: md.tags.append("G:%s" % genre) md.tracknumber, md.trackcount = mp3info.getTrackNum() md.disknumber, md.diskcount = mp3info.getDiscNum() # if the playtime is also in the ID3 tag information, we # try to read it from there if mp3info.frames["TLEN"]: length = None try: length = int(int(mp3info.frames["TLEN"])/1000) except: # time in seconds (?), possibly with bad decimal separator, e.g "186,333" try: length = int(float(mp3info.frames["TLEN"].replace(",", "."))) except: pass if length: md.length = length md.lyrics = u"".join(mp3info.getLyrics()) md.comments = u"".join(map(lambda c: c.render(), mp3info.getComments())) md.bpm = mp3info.getBPM() for rva2frame in mp3info.frames["RVA2"]: # since eyeD3 currently doesn't support RVA2 frames, we have to decode # them on our own following mutagen desc, rest = rva2frame.data.split("\x00", 1) channel = ord(rest[0]) if channel == 1: gain = struct.unpack('>h', rest[1:3])[0]/512.0 # http://bugs.xmms.org/attachment.cgi?id=113&action=view rest = rest[3:] peak = 0 bits = ord(rest[0]) bytes = min(4, (bits + 7) >> 3) shift = ((8 - (bits & 7)) & 7) + (4 - bytes) * 8 for i in range(1, bytes+1): peak *= 256 peak += ord(rest[i]) peak *= 2**shift peak = (float(peak) / (2**31-1)) if desc == "album": basename = "replaygain_album_" else: # for everything else, we assume it's track gain basename = "replaygain_track_" md[basename+"gain"] = gain md[basename+"peak"] = peak try: import mutagen.mp3 import mutagen.id3 # copied from quodlibet class ID3hack(mutagen.id3.ID3): "Override 'correct' behavior with desired behavior" def loaded_frame(self, tag): if len(type(tag).__name__) == 3: tag = type(tag).__base__(tag) if tag.HashKey in self and tag.FrameID[0] == "T": self[tag.HashKey].extend(tag[:]) else: self[tag.HashKey] = tag registerfileformat("mp3", read_mp3_mutagen_metadata, ".mp3") log.info("MP3 support enabled: using mutagen module for id3 tag parsing") except ImportError: try: import eyeD3 registerfileformat("mp3", read_mp3_eyeD3_metadata, ".mp3") log.info("MP3 support enabled: using eyeD3 module for id3 tag parsing") except ImportError: log.info("MP3 support disabled: no metadata reader module found") ############################################################################## # Ogg Vorbis metadata decoder ############################################################################## def read_vorbis_metadata(md, path): vf = ogg.vorbis.VorbisFile(path) # We use the information for all streams (not stream 0). # XXX Is this correct? md.length = int(vf.time_total(-1)) md.bitrate = vf.bitrate(-1) md.samplerate = vf.info().rate md.is_vbr = vf.info().bitrate_lower != vf.info().bitrate_upper for name, value in vf.comment().as_dict().items(): value = value[0] if name == "TITLE": md.title = value if name == "ALBUM": md.album = value if name == "ARTIST": md.artist = value if name == "DATE": try: md.year = int(value) except ValueError: pass if name == "GENRE" and value: md.tags.append("G:%s" % value) if name == "COMMENT": md.comments = [value] if name == "TRACKNUMBER": try: md.tracknumber = int(value) except ValueError: pass if name == "TRACKCOUNT": try: md.trackcount = int(value) except ValueError: pass if name == "DISCNUMBER": try: md.disknumber = int(value) except ValueError: pass if name == "DISCCOUNT": try: md.diskcount = int(value) except ValueError: pass if name == "BPM": try: md.bpm = int(value) except ValueError: pass if name.startswith("REPLAYGAIN_"): # ReplayGain: # example format according to vorbisgain documentation # REPLAYGAIN_TRACK_GAIN=-7.03 dB # REPLAYGAIN_TRACK_PEAK=1.21822226 # REPLAYGAIN_ALBUM_GAIN=-6.37 dB # REPLAYGAIN_ALBUM_PEAK=1.21822226 try: profile, type = name[11:].split("_") basename = "replaygain_%s_" % profile.lower() if type == "GAIN": md[basename + "gain"] = float(value.split()[0]) try: if md[basename + "peak"] is None: md[basename + "peak"] = 1.0 except AttributeError: md[basename + "peak"] = 1.0 if type == "PEAK": md[basename + "peak"] = float(value) try: if md[basename + "gain"] is None: md[basename + "gain"] = 1.0 except AttributeError: md[basename + "gain"] = 0.0 except (ValueError, IndexError): pass # XXX how is the song lyrics stored? # md.lyrics = [] try: import ogg.vorbis registerfileformat("ogg", read_vorbis_metadata, ".ogg") log.info("Ogg Vorbis support enabled") except ImportError: log.info("Ogg Vorbis support disabled: ogg.vorbis module not found") ############################################################################## # FLAC metadata decoder ############################################################################## def read_flac_metadata(md, path): chain = flac.metadata.Chain() chain.read(path) it = flac.metadata.Iterator() it.init(chain) while 1: block = it.get_block() if block.type == flac.metadata.VORBIS_COMMENT: comment = flac.metadata.VorbisComment(block).comments id3get = lambda key, default: getattr(comment, key, default) self.title = id3get('TITLE', "") self.album = id3get('ALBUM', "") self.artist = id3get('ARTIST', "") self.year = id3get('DATE', "") self.genre = id3get('GENRE', "") self.tracknr = id3get('TRACKNUMBER', "") elif block.type == flac.metadata.STREAMINFO: streaminfo = block.data.stream_info self.length = streaminfo.total_samples / streaminfo.sample_rate if not it.next(): break try: import flac.metadata registerfileformat("flac", read_flac_metadata, ".flac") log.info("FLAC support enabled (VERY EXPERIMENTAL)") except ImportError: log.info("FLAC support disabled: flac module not found") ############################################################################## # various metadata postprocessors ############################################################################## def md_pp_capitalize(md): def capwords(s): # capitalize words also directly after a single punctuation character words = [] for word in s.split(): if word[0] in string.punctuation: words.append(word[0] + word[1:].capitalize()) else: words.append(word.capitalize()) return " ".join(words) if md.title: md.title = capwords(md.title) if md.artist: md.artist = capwords(md.artist) if md.album: md.album = capwords(md.album) def md_pp_strip_leading_article(md): # strip leading "The " in artist names, often used inconsistently if md.artist and md.artist.startswith("The ") and len(md.artist)>4: md.artist = md.artist[4:] def md_pp_remove_accents(md): # XXX disabled because I don't know how to get translate working # with unicode strings (except for encoding them first) md.artist = md.artist.translate(accent_trans) md.album = md.album.translate(accent_trans) md.title = md.title.translate(accent_trans) def md_pp_add_decade_tag(md): # automatically add decade tag if md.year: md.tags.append("D:%d" % (10*(md.year//10))) register_metadata_postprocessor("capitalize", md_pp_capitalize) register_metadata_postprocessor("strip_leading_article", md_pp_strip_leading_article) register_metadata_postprocessor("add_decade_tag", md_pp_add_decade_tag) PyTone-3.0.3/src/mixerwin.py000644 000765 000765 00000017424 11342234432 016254 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import errors import re import config import encoding import events, hub import log import statusbar import window # two simple abstraction classes for the different mixer types from the oss # and the ossaudiodev module, as well as the internal mixer class ossmixer: def __init__(self, device, channel): self.mixer = oss.open_mixer(device) self.channel = channel log.info(_("initialized oss mixer: device %s, channel %s") % (device, channel)) def get(self): return self.mixer.read_channel(self.channel) def adjust(self, level_adjust): oldlevel = self.get() self.mixer.write_channel(self.channel, (max(0, min(oldlevel[0]+level_adjust, 100)), max(0, min(oldlevel[1]+level_adjust, 100)) ) ) class ossaudiodevmixer: def __init__(self, device, channel): self.mixer = oss.openmixer(device) self.channel = channel log.info(_("initialized oss mixer: device %s, channel %s") % (device, channel)) def get(self): return self.mixer.get(self.channel) def adjust(self, level_adjust): oldlevel = self.get() self.mixer.set(self.channel, (max(0, min(oldlevel[0]+level_adjust, 100)), max(0, min(oldlevel[1]+level_adjust, 100)) ) ) class internalmixer: def __init__(self, playerid): self.playerid = playerid self.volume = 1 log.info(_("initialized internal mixer: player %s") % playerid) def get(self): return [self.volume*100, self.volume*100] def adjust(self, level_adjust): hub.notify(events.player_change_volume_relative(self.playerid, level_adjust)) # to make the update smoother, we anticipate the volume change self.volume = max(0, min(1, self.volume + level_adjust/100.)) # determine oss module to be used, if any present try: import ossaudiodev as oss externalmixer = ossaudiodevmixer except: try: import oss externalmixer = ossmixer except: externalmixer = None class mixerwin(window.window): def __init__(self, screen, maxh, maxw, channel): self.channel = channel if config.mixer.type == "external": if externalmixer is not None: mixer_device = config.mixer.device channelre = re.compile("SOUND_MIXER_[a-zA-Z0-9]") if channelre.match(config.mixer.channel): mixer_channel = eval("oss.%s" % config.mixer.channel) else: raise errors.configurationerror("Wrong mixer channel specification: %s" % config.mixer.channel) self.mixer = externalmixer(mixer_device, mixer_channel) else: self.mixer = internalmixer("main") log.warning("Could not initialize external mixer, using internal one") elif config.mixer.type == "internal": self.mixer = internalmixer("main") else: self.mixer = None self.stepsize = config.mixer.stepsize if self.mixer: self.level = self.mixer.get() else: self.level = None self.keybindings = config.keybindings.general # for identification purposes, we only generate this once self.hidewindowevent = events.hidewindow(self) self.hide() if self.mixer: self.channel.subscribe(events.keypressed, self.keypressed) self.channel.subscribe(events.mouseevent, self.mouseevent) self.channel.subscribe(events.hidewindow, self.hidewindow) self.channel.subscribe(events.focuschanged, self.focuschanged) if isinstance(self.mixer, internalmixer): self.channel.subscribe(events.player_volume_changed, self.player_volume_changed) def changevolume(self, change): if self.mixer: self.mixer.adjust(change) # event handler def keypressed(self, event): key = event.key if key in self.keybindings["volumeup"]: self.changevolume(self.stepsize) elif key in self.keybindings["volumedown"]: self.changevolume(-self.stepsize) else: if self.hasfocus(): self.hide() raise hub.TerminateEventProcessing return self.update() if config.mixerwindow.autoclosetime: hub.notify(events.sendeventin(self.hidewindowevent, config.mixerwindow.autoclosetime, replace=1)) raise hub.TerminateEventProcessing def mouseevent(self, event): if self.hasfocus(): self.hide() raise hub.TerminateEventProcessing def focuschanged(self, event): if self.hasfocus(): sbar = statusbar.generatedescription("general", "volumedown") sbar += statusbar.separator sbar += statusbar.generatedescription("general", "volumeup") sbar += statusbar.terminate hub.notify(events.statusbar_update(0, sbar)) def player_volume_changed(self, event): if isinstance(self.mixer, internalmixer) and event.playerid==self.mixer.playerid: self.mixer.volume = event.volume if self.hasfocus(): self.update() def update(self): self.level = self.mixer.get() self.top() window.window.update(self) self.addstr(self.iy, self.ix, encoding.encode(_("Volume:")), self.colors.description) self.addstr(" %3d " % round(self.level[0]), self.colors.content) percent = int(round(self.barlen*self.level[0]/100)) self.addstr("#"*percent, self.colors.barhigh) self.addstr("#"*(self.barlen-percent), self.colors.bar) class popupmixerwin(mixerwin): """ mixer which appears as a popup at the center of the screen """ def __init__(self, screen, maxh, maxw, channel): # calculate size and position self.barlen = 20 h = 3 w = len(_("Volume:")) + 7 + self.barlen y = (maxh-h)/2 x = (maxw-w)/2 window.window.__init__(self, screen, h, w, y, x, config.colors.mixerwindow, _("Mixer")) mixerwin.__init__(self, screen, maxh, maxw, channel) def resize(self, maxh, maxw): h = 3 w = len(_("Volume:")) + 7 + self.barlen y = (maxh-h)/2 x = (maxw-w)/2 window.window.resize(self, h, w, y, x) class statusbarmixerwin(mixerwin): """ mixer which appears in the statusbar """ def __init__(self, screen, maxh, maxw, channel): # calculate size and position self.barlen = max(0, maxw - len(_("Volume:")) - 5) window.window.__init__(self, screen, 1, maxw, maxh-1, 0, config.colors.mixerwindow) mixerwin.__init__(self, screen, maxh, maxw, channel) def resize(self, maxh, maxw): window.window.resize(self, 1, maxw, maxh-1, 0) PyTone-3.0.3/src/._network.py000644 000765 000765 00000000122 10660305454 016310 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/network.py000644 000765 000765 00000030620 10660305454 016101 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2003, 2007 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import SocketServer, socket import threading, tempfile, time, os.path import cPickle, cStringIO import events, hub import log # for unpickling import item, metadata, requests, services, services.player, services.playlist, copy_reg, __builtin__ _EVENT = "EVENT" _REQUEST = "REQUEST" _RESULT = "RESULT" _SUBSCRIBE = "SUBSCRIBE" _SENDFILE = "SENDFILE" ############################################################################## # restricted unpickling ############################################################################## def find_global(module, klass): if module in ("events", "requests", "metadata", "copy_reg", "__builtin__"): pass elif module=="item" and klass=="song": pass elif module=="dbitem": pass elif module=="services.player": pass elif module=="services.playlist": pass else: log.debug("refusing to unpickle %s.%s" % (module, klass)) raise cPickle.UnpicklingError, \ "cannot unpickle a %s.%s" % (module, klass) log.debug("unpickling %s.%s" % (module, klass)) return eval("%s.%s" % (module, klass)) def loads(s): unpickler = cPickle.Unpickler(cStringIO.StringIO(s)) unpickler.find_global = find_global return unpickler.load() ############################################################################## # server part ############################################################################## class servernetworkreceiver(threading.Thread): """ helper thread that processes requests coming from clients We need this, since there is no select which accepts both a socket and a queue. """ def __init__(self, socket, handler): self.socket = socket self.handler = handler self.rfile = self.socket.makefile("r") self.done = False threading.Thread.__init__(self) self.setDaemon(1) def _receiveobject(self): line = self.rfile.readline() if not line: return None, None try: log.debug("server: request type received") type, bytes = line.split() bytes = int(bytes) if type != _SENDFILE: objstring = self.rfile.read(bytes+2)[:-2] log.debug("server: object received") obj = loads(objstring) log.debug("server receive: type=%s object=%s" % (type, `obj`)) return (type, obj) else: # we handle send file requests separately filename = self.rfile.readline() tmpfilename = tempfile.mktemp() bytes = bytes-len(filename)-2 tmpfile = open(tmpfilename, "w") while bytes>2: rbytes = min(bytes, 4096) tmpfile.write(self.rfile.read(rbytes)) bytes -= rbytes self.rfile.read(2) return (type, tmpfilename) except Exception, e: log.debug("exception '%s' occured during _receiveobject" % e) return (None, None) def run(self): # process events, request and subscription requests coming from # the client while not self.done: type, obj = self._receiveobject() if type == _EVENT: log.debug("server: client sends event '%s'" % obj) hub.notify(obj, priority=-50) elif type == _REQUEST: log.debug("server: requesting %s for client" % `obj`) # extract id rid, obj = obj result = hub.request(obj, priority=-50) log.debug("server: got answer %s" % `result`) # be careful, handler may not exist anymore? try: self.handler._sendobject(_RESULT, (rid, result)) except: pass elif type == _SUBSCRIBE: log.debug("server: client requests subscription for '%s'" % `obj`) # be careful, maybe handler does not exists anymore? try: self.handler.subscribe(obj) except: pass else: log.debug("server: servernetworkreceiver exits: type=%s" % type) self.done = True self.handler.done = True class handler(SocketServer.StreamRequestHandler, SocketServer.BaseRequestHandler): """ handles requests by clients """ rbufsize = 0 def _sendobject(self, type, obj): # we have to switch to blocking mode for send # self.request.setblocking(1) objstring = cPickle.dumps(obj, 1) self.wfile.write("%s %d\r\n%s\r\n" % (type, len(objstring), objstring)) self.wfile.flush() log.debug("server send: type=%s object=%s" % (type, `obj`)) def handle(self): log.debug("starting handler") self.channel = hub.newchannel() self.done = False self.servernetworkreceiver = servernetworkreceiver(self.request, self) self.servernetworkreceiver.start() # Process events coming from the rest of the PyTone server. # This sends (via eventhandler) subscribed events to the client while not self.done: self.channel.process(block=True) log.debug("terminating handler") self.channel.hub.disconnect(self.channel) def subscribe(self, event): # clientnetworkreceiver calls this method to subscribe to certain events self.channel.subscribe(event, self.eventhandler) # # event handler # def eventhandler(self, event): # send every subscribed event to client log.debug("network event handler called") self._sendobject(_EVENT, event) # boilerplate server code class tcpserver(threading.Thread): allow_reuse_address = 1 def __init__(self, bind, port): self.bind = bind self.port = port threading.Thread.__init__(self) self.setDaemon(1) def run(self): while 1: try: self.tcpserver = SocketServer.ThreadingTCPServer((self.bind, self.port), handler) break except: log.debug("server thread is waiting for port to become free") time.sleep(1) self.tcpserver.serve_forever() class unixserver(threading.Thread): def __init__(self, filename): self.filename = filename try: os.unlink(self.filename) except OSError, e: if e.errno!=2: raise threading.Thread.__init__(self) self.setDaemon(1) def run(self): self.unixserver = SocketServer.ThreadingUnixStreamServer(self.filename, handler) self.unixserver.serve_forever() ############################################################################## # client part ############################################################################## class clientnetworkreceiver(threading.Thread): """ helper thread that receives from socket and puts result in queue We need this, since there is no select which accepts both a socket and a queue. """ def __init__(self, socket, queue): self.socket = socket self.rfile = self.socket.makefile("r") self.queue = queue self.done = False threading.Thread.__init__(self) self.setDaemon(1) def _receiveobject(self): try: line = self.rfile.readline() type, bytes = line.split() bytes = int(bytes) objstring = self.rfile.read(bytes+2)[:-2] log.debug("client receive: %s bytes" % len(objstring)) obj = loads(objstring) log.debug("client receive: type=%s object=%s" % (type, repr(obj))) return (type, obj) except: return (None, None) def run(self): while not self.done: self.queue.put((self._receiveobject(), 100)) # # bidirectional (sending + receiving) client functionality is provided by the clientchannel # and its subclasses # class clientchannel(threading.Thread): def __init__(self, networklocation): # network location is either a tuple (server adress, port) or a # filename pointing to a socket file try: server, port = networklocation family = socket.AF_INET except ValueError: filename = networklocation family = socket.AF_UNIX self.socket = socket.socket(family, socket.SOCK_STREAM) if family == socket.AF_INET: self.socket.connect((server, port)) else: self.socket.connect(filename) self.subscriptions = [] self.wfile = self.socket.makefile("wb") self.queue = hub.PriorityQueue(-1) self.clientnetworkreceiver = clientnetworkreceiver(self.socket, self.queue) self.clientnetworkreceiver.start() # hash for pending requests self.pendingrequests = {} self.done = False threading.Thread.__init__(self) self.setDaemon(1) log.debug("Network clientchannel initialized") def _sendobject(self, type, obj): log.debug("client send: type=%s object=%s" % (type, obj)) try: objstring = cPickle.dumps(obj, cPickle.HIGHEST_PROTOCOL) except Exception, e: log.debug_traceback() self.wfile.write("%s %d\r\n%s\r\n" % (type, len(objstring), objstring)) self.wfile.flush() def sendfile(self, filename): basename = os.path.basename(filename) file = open(filename, "r") f.seek(0, 2) filelen = f.tell() f.seek(0, 0) # length of request rlen = len(basename) + 2 + filelen self.wfile.write("%s %d\r\n%s\r\n" % (_SENDFILE, rlen, basename)) while filelen>0: wbytes = min(filelen, 4096) self.wfile.write(file.read(wbytes)) filelen -= wbytes self.wfile.write("\r\n") self.wfile.flush() log.debug("client send: type=%s object=file:%s" % (type, filename)) def subscribe(self, eventtype, handler): # Note that the subscription semantics is a little bit different compared # with that of hub.py. The clientchannel is a thread of its own, so # it calls the playback without being in a process method! self._sendobject(_SUBSCRIBE, eventtype) self.subscriptions.append((eventtype, handler)) def run(self): while not self.done: item = self.queue.get() if isinstance(item, events.event): self._sendobject(_EVENT, item) elif isinstance(item, hub.requestresponse): # send request including id rid = id(item) log.debug("Sending request (id=%d)" % rid) self._sendobject(_REQUEST, (rid, item.request)) self.pendingrequests[rid] = item else: # input from networkreceiver: tuple (type, obj) type, obj = item if type==_EVENT: log.debug("Received event from networkreceiver") try: for subscribedevent, handler in self.subscriptions: if isinstance(obj, subscribedevent): handler(obj) except TerminateEventProcessing: pass elif type==_RESULT: rid, obj = obj log.debug("Received request result (id=%d) from networkreceiver" % rid) item = self.pendingrequests[rid] item.result = obj item.ready.set() del self.pendingrequests[rid] def notify(self, item, priority=0): """ notify channel of item (event or request) """ self.queue.put((item, -priority)) def request(self, request, priority=0): """ submit a request (blocking) this method submits a request, waits for the result and returns it. Requests with a high priority are treated first. """ # generate a request response object for the request, # send it to hub and wait for result rr = hub.requestresponse(request) self.notify(rr, priority) rr.waitforcompletion() return rr.result def quit(self): while not self.queue.empty(): time.sleep(0.1) self.done = True # # unidirectional (only sending) client functionality is provided by the sender # and its subclasses # class sender: def __init__(self, socket): self.socket = socket self.wfile = self.socket.makefile("wb") log.debug("Network sender initialized") def sendevent(self, event): objstring = cPickle.dumps(event, 1) self.wfile.write("%s %d\r\n%s\r\n" % (_EVENT, len(objstring), objstring)) self.wfile.flush() def close(self): self.socket.close() class tcpsender(sender): def __init__(self, server, port): asocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) asocket.connect((server, port)) sender.__init__(self, asocket) class unixsender(sender): def __init__(self, filename): asocket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) asocket.connect(filename) sender.__init__(self, asocket) PyTone-3.0.3/src/pcm/000755 000765 000765 00000000000 11406223507 014611 5ustar00ringoringo000000 000000 PyTone-3.0.3/src/playerwin.py000644 000765 000765 00000016147 11342232273 016426 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002, 2003 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os import config import log import window import events, hub import statusbar import encoding from helper import formattime class playerwin(window.window): def __init__(self, screen, layout, channel, playerid): self.song = None self.time = 0 self.paused = 0 self.stopped = 1 self.playerid = playerid self.keybindings = config.keybindings.general self.songformat = config.playerwindow.songformat self.playerinfofile = config.general.playerinfofile self.songchangecommand = config.general.songchangecommand.strip() if self.songchangecommand: self.songchangecommand = "( %s )&" % self.songchangecommand h, w, y, x, border = layout window.window.__init__(self, screen, h, w, y, x, config.colors.playerwindow, _("Playback Info"), border) try: self.playerinfofd = open(self.playerinfofile, "w") except IOError, e: log.error(_("error '%s' occured during write to playerinfofile") % e) self.playerinfofd = None # we don't want to have the focus self.bottom() channel.subscribe(events.playbackinfochanged, self.playbackinfochanged) channel.subscribe(events.keypressed, self.keypressed) self.update() def resize(self, layout): h, w, y, x, self.border = layout window.window.resize(self, h, w, y, x) def updatestatusbar(self): if self.song and not self.paused and self.keybindings["playerpause"]: sbar = statusbar.generatedescription("general", "playerpause") else: sbar = statusbar.generatedescription("general", "playerstart") hub.notify(events.statusbar_update(1, sbar)) def update(self): window.window.update(self) self.updatestatusbar() self.addstr(1, self.ix, " "*self.iw) if self.song: self.move(1, self.ix) s1 = _("Time:") s2 = " %s/%s " % (formattime(self.time), formattime(self.song.length)) self.addstr(s1, self.colors.description) self.addstr(s2, self.colors.content) if not self.paused: barlen = self.iw-len(s1)-len(s2) try: percentplayed = int(barlen*self.time/self.song.length) except ZeroDivisionError: percentplayed = 0 self.addstr("#"*(percentplayed), self.colors.progressbarhigh) self.addstr("#"*(barlen-percentplayed), self.colors.progressbar) else: self.addstr(_("paused"), self.colors.description) # event handler def playbackinfochanged(self, event): if event.playbackinfo.playerid == self.playerid: if self.song != event.playbackinfo.song and event.playbackinfo.song and self.songchangecommand: os.system(event.playbackinfo.song.format(self.songchangecommand, safe=True)) self.song = event.playbackinfo.song self.paused = event.playbackinfo.ispaused() self.stopped = event.playbackinfo.isstopped() if self.song: self.settitle(u"%s%s" % (event.playbackinfo.iscrossfading() and "-> " or "", self.song.format(self.songformat))) else: self.settitle(_("Playback Info")) self.time = event.playbackinfo.time self.update() # update player info file, if configured if self.playerinfofd: try: self.playerinfofd.seek(0) if self.song: info = "%s - %s (%s/%s)\n" % ( self.song.artist, self.song.title, formattime(self.time), formattime(self.song.length)) else: info = _("Not playing") + "\n" info = encoding.encode(info) self.playerinfofd.write(info) self.playerinfofd.truncate(len(info)) except IOError, e: log.error(_("error '%s' occured during write to playerinfofile") % e) self.playerinfofd = None def keypressed(self, event): key = event.key if key in self.keybindings["playerstart"] and self.paused: hub.notify(events.playerstart(self.playerid)) elif key in self.keybindings["playerpause"] and not self.paused and not self.stopped: hub.notify(events.playerpause(self.playerid)) elif key in self.keybindings["playerstart"]: hub.notify(events.playerstart(self.playerid)) elif key in self.keybindings["playernextsong"]: hub.notify(events.playernext(self.playerid)) elif key in self.keybindings["playerprevioussong"]: hub.notify(events.playerprevious(self.playerid)) elif key in self.keybindings["playerrewind"]: hub.notify(events.playerseekrelative(self.playerid, -2)) elif key in self.keybindings["playerforward"]: hub.notify(events.playerseekrelative(self.playerid, 2)) elif key in self.keybindings["playerstop"]: hub.notify(events.playerstop(self.playerid)) elif key in self.keybindings["playerplayfaster"]: hub.notify(events.playerplayfaster(self.playerid)) elif key in self.keybindings["playerplayslower"]: hub.notify(events.playerplayslower(self.playerid)) elif key in self.keybindings["playerspeedreset"]: hub.notify(events.playerspeedreset(self.playerid)) elif key in self.keybindings["playerratecurrentsong1"]: if self.song: self.song.rate(1) elif key in self.keybindings["playerratecurrentsong2"]: if self.song: self.song.rate(2) elif key in self.keybindings["playerratecurrentsong3"]: if self.song: self.song.rate(3) elif key in self.keybindings["playerratecurrentsong4"]: if self.song: self.song.rate(4) elif key in self.keybindings["playerratecurrentsong5"]: if self.song: self.song.rate(5) else: return raise hub.TerminateEventProcessing PyTone-3.0.3/src/._playlist.py000644 000765 000765 00000000122 10557445574 016475 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/playlist.py000644 000765 000765 00000010223 10557445574 016263 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002, 2003, 2004 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import config import events, hub, requests import slist # # playlist class, which acts as glue layer between the playlist service # and the playlist window # class playlist(slist.slist): def __init__(self, win, playerid): slist.slist.__init__(self, win, config.playlistwindow.scrollmode=="page") self.playerid = playerid self.songdbid = "main" items, self.ptime, self.ttime, self.autoplaymode, self.playingitem = \ hub.request(requests.playlistgetcontents()) self.set(items) self._recenter() self.win.channel.subscribe(events.playlistchanged, self.playlistchanged) def _recenter(self): """ recenter playlist around currently playing (or alternatively last) song """ for i in range(len(self)): if self[i] is self.playingitem or i==len(self)-1: oldselected = self.selected self.selected = i if self.selected != oldselected: self._notifyselectionchanged() h2 = self.win.ih/2 if len(self)-i <= h2: self.top = max(0, len(self)-self.win.ih) elif i >= h2: self.top = i-h2 else: self.top = 0 self._updatetop() break def getselectedsong(self): """ return song corresponding to currently selected item or None """ playlistitem = self.getselected() if playlistitem: return playlistitem.song else: return None # The following three slist.slist methods are delegated to the # playlist service. Any resulting changes to the playlist will be # performed only later when a playlistchanged event comes back def deleteselected(self): "delete currently selected item" if self.selected is not None: hub.notify(events.playlistdeletesong(self.getselected().id)) def moveitemup(self): "move selected item up, if not first" if self.selected is not None and self.selected>0: hub.notify(events.playlistmovesongup(self.getselected().id)) def moveitemdown(self): "move selected item down, if not last" if self.selected is not None and self.selected # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import curses, time import config import events, hub import playlist import statusbar import window import encoding from helper import formattime class playlistwin(window.window): def __init__(self, screen, layout, channel, playerid): self.channel = channel self.keybindings = config.keybindings.playlistwindow self.songformat = config.playlistwindow.songformat h, w, y, x, border = layout window.window.__init__(self, screen, h, w, y, x, config.colors.playlistwindow, _("Playlist"), border, config.playlistwindow.scrollbar) # Immediately remove focus from playlist window in order to # prevent wrong selectionchanged events being issued, which # would lead to playing the first song in the playlist on the # second player (if configured) self.bottom() self.playlist = playlist.playlist(self, playerid) self.channel.subscribe(events.keypressed, self.keypressed) self.channel.subscribe(events.mouseevent, self.mouseevent) self.channel.subscribe(events.focuschanged, self.focuschanged) def updatestatusbar(self): sbar = [] if self.playlist.selected is not None: sbar += statusbar.generatedescription("playlistwindow", "deleteitem") sbar += statusbar.separator sbar += statusbar.generatedescription("playlistwindow", "moveitemup") sbar += statusbar.separator sbar += statusbar.generatedescription("playlistwindow", "moveitemdown") sbar += statusbar.separator sbar += statusbar.generatedescription("playlistwindow", "activatefilelist") hub.notify(events.statusbar_update(0, sbar)) def updatescrollbar(self): self.drawscrollbar(self.playlist.top, len(self.playlist)) def resize(self, layout): h, w, y, x, self.border = layout window.window.resize(self, h, w, y, x) self.playlist._updatetop() if not self.hasfocus(): self.playlist._recenter() def activatefilelist(self): # before recentering we remove the focus from the playlist in order # to prevent wrong songchanged events being issued (which would lead to # wrong songs being played on the secondary player) self.bottom() self.playlist._recenter() hub.notify(events.activatefilelist()) # event handlers def keypressed(self, event): if self.hasfocus(): key = event.key if key in self.keybindings["selectnext"]: self.playlist.selectnext() elif key in self.keybindings["selectprev"]: self.playlist.selectprev() elif key in self.keybindings["selectnextpage"]: self.playlist.selectnextpage() elif key in self.keybindings["selectprevpage"]: self.playlist.selectprevpage() elif key in self.keybindings["selectfirst"]: self.playlist.selectfirst() elif key in self.keybindings["selectlast"]: self.playlist.selectlast() elif key in self.keybindings["activatefilelist"]: self.activatefilelist() elif key in self.keybindings["moveitemup"]: self.playlist.moveitemup() elif key in self.keybindings["moveitemdown"]: self.playlist.moveitemdown() elif key in self.keybindings["deleteitem"]: self.playlist.deleteselected() elif key in self.keybindings["playselectedsong"]: self.playlist.playselected() elif key in self.keybindings["shuffle"]: hub.notify(events.playlistshuffle()) elif key in self.keybindings["rescan"]: self.playlist.rescanselection(True) elif ord("0")<=key<=ord("5"): self.playlist.rateselection(key-ord("1")+1) elif key in self.keybindings["filelistjumptoselectedsong"]: self.playlist.filelistjumptoselected() self.activatefilelist() else: return self.update() raise hub.TerminateEventProcessing def mouseevent(self, event): if self.enclose(event.y, event.x): y, x = self.stdscrtowin(event.y, event.x) self.top() if event.state & curses.BUTTON1_CLICKED: if x==self.ix+self.iw and self.hasscrollbar: scrollbarbegin, scrollbarheight = self.scrollbardimensions(self.playlist.top, len(self.playlist)) if y==self.iy+1: self.playlist.selectprev() elif y==self.iy+self.ih-2: self.playlist.selectnext() elif self.iy # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import service import config class plugin: def __init__(self, channel, config, mainscreen): """ create a plugin instance """ self.channel = channel self.config = config self.mainscreen = mainscreen def start(self): self.init() def init(self): """ initialize plugin after it has been configured """ class threadedplugin(service.service, plugin): def __init__(self, channel, config, mainscreen): service.service.__init__(self, self.__class__.__name__) # as independent thread, we have to use our own channel which has # been created by the service constructor plugin.__init__(self, self.channel, config, mainscreen) def run(self): self.init() service.service.run(self) PyTone-3.0.3/src/plugins/000755 000765 000765 00000000000 11406223507 015513 5ustar00ringoringo000000 000000 PyTone-3.0.3/src/._profile.py000644 000765 000765 00000000122 10266220577 016263 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/profile.py000644 000765 000765 00000000210 10266220577 016044 0ustar00ringoringo000000 000000 #!/usr/bin/python import hotshot def main(): import pytone prof = hotshot.Profile("PyTone.prof") prof.runcall(main) prof.close() PyTone-3.0.3/src/pytone.py000755 000765 000765 00000024216 11342542144 015732 0ustar00ringoringo000000 000000 #!/usr/bin/env python # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002, 2003, 2004, 2005, 2007 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import curses, os, os.path, signal, imp, sys ############################################################################## # gettext initialization. ############################################################################## # We have to initialize gettext very early, before importing our # modules. Assume that the locales lie in same dir as this # module. This may not be the case, if the .mo files are installed at # their proper location. try: import gettext locallocaledir = os.path.join(os.path.dirname(sys.argv[0]), "../locale") gettext.install("PyTone", locallocaledir, unicode=True) except: # Disable localization if there is any problem with the above. # This works around a problem with Python 2.1 import __builtin__ __builtin__.__dict__['_'] = lambda s: s ############################################################################## # locale initialization ############################################################################## import locale locale.setlocale(locale.LC_ALL, '') ############################################################################## # create .pytone dir in user home ############################################################################## try: os.mkdir(os.path.expanduser("~/.pytone")) log.info(_("created PyTone directory ~/.pytone")) except OSError, e: if e.errno!=17: raise ############################################################################## # process commandline options and read config file ############################################################################## import config # process the command line first, because a different location for the # config file may be given there config.processcommandline() config.processconfig() # now that the configuration has been read, we can imort the log # module and initialize the debug file if necessary import log log.initdebugfile(config.general.debugfile) # log version (but do this after the debug file has been initialized, # such that the version gets included there) import version log.info(_("PyTone %s startup") % version.version) import errors import mainscreen import helper import hub, events import services.songdb import services.player import services.timer # Uncomment the next line, if you want to experiment a little bit with # the number of bytecode instructions after which a context switch # occurs. # sys.setcheckinterval(250) ############################################################################## # start various services ############################################################################## # catch any exceptions during service startup to be able to shut down # all already running services when something goes wrong try: # timer service. We start this first so that other services can register # there periodic events with this service services.timer.timer().start() # Determine plugins specified in the config file and read their config. # The result goees into a list of tuples (pluginmodule, pluginconfig). plugins = [] userpluginpath = os.path.expanduser("~/.pytone/plugins/") cwd = os.path.abspath(os.path.dirname(sys.argv[0])) globalpluginpath = os.path.join(cwd, "plugins") pluginpath = [userpluginpath, globalpluginpath] for name in config.general.plugins: try: # We use imp.find_module to narrow down the plugin search path # to the two possible locations. Setting sys.path correspondingly # would not work, however, since then the plugin could not # import its needed modules. fp, pathname, description = imp.find_module(name, pluginpath) pluginmodule = imp.load_module(name, fp, pathname, description) # # process configuration of plugin pluginconfig = pluginmodule.config if pluginconfig is not None: config.readconfigsection("plugin.%s" % name, pluginconfig) config.finishconfigsection(pluginconfig) pluginconfig = pluginconfig() plugins.append((pluginmodule, pluginconfig)) except Exception, e: log.error(_("Cannot load plugin '%s': %s") % (name, e)) log.debug_traceback() # initialize song database manager and start it immediately so # that it can propagate quit events in case something goes wrong # when setting up the databases songdbmanager = services.songdb.songdbmanager() songdbmanager.start() # song databases songdbids = [] for songdbname in config.database.getsubsections(): try: songdbid = songdbmanager.addsongdb(songdbname, config.database[songdbname]) if songdbid: songdbids.append(songdbid) except Exception, e: log.error("cannot initialize db %s: %s" % (id, e)) if not songdbids: # raise last configuration error raise # network service if config.network.enableserver: import network network.tcpserver(config.network.bind, config.network.port).start() if config.network.socketfile: import network network.unixserver(os.path.expanduser(config.network.socketfile)).start() # Now that the basic services have been started, we can initialize # the players. This has to be done last because the players # immediately start requesting a new song playerids = [services.player.initplayer("main", config.player.main), services.player.initplayer("secondary", config.player.secondary)] except: # if something goes wrong, shutdown all already running services hub.notify(events.quit(), 100) raise ############################################################################## # basic curses library setup... ############################################################################## def cursessetup(): # Initialize curses library stdscr = curses.initscr() # Turn off echoing of keys curses.noecho() # In keypad mode, escape sequences for special keys # (like the cursor keys) will be interpreted and # a special value like curses.KEY_LEFT will be returned stdscr.keypad(1) # allow 8-bit characters to be input curses.meta(1) # enter raw mode, thus disabling interrupt, quit, suspend and flow-control keys curses.raw() # wait at maximum for 1/10th of seconds for keys pressed by user curses.halfdelay(1) if config.general.colorsupport == "auto": # Try to enable color support try: curses.start_color() except: log.warning("terminal does not support colors: disabling color support") # now check whether color support really has been enabled if curses.has_colors(): config.configcolor._colorenabled = 1 elif config.general.colorsupport == "on": curses.start_color() config.configcolor._colorenabled = 1 # Check for transparency support of terminal # use_default_colors(), which will be integrated in python 2.4. # Before that happens we try to use our own cursext c-extension try: curses.use_default_colors() config.configcolor._colors["default"] = -1 except: try: import cursext if cursext.useDefaultColors(): config.configcolor._colors["default"] = -1 else: log.warning("terminal does not support transparency") except: log.warning("transparency support disabled because cursext module is not present") # try disabling cursor try: curses.curs_set(0) except: log.warning("terminal does not support disabling of cursor") if config.general.mousesupport: # enable all mouse events curses.mousemask(curses.ALL_MOUSE_EVENTS) # redirect stderr to /dev/null (to prevent spoiling the screen # with libalsa messages). This is not really nice but at the moment there # is no other way to get rid of this nuisance. dev_null = file("/dev/null", 'w') os.dup2(dev_null.fileno(), sys.stderr.fileno()) return stdscr ############################################################################## # ... and cleanup ############################################################################## def cursescleanup(): # restore terminal settings try: stdscr.keypad(0) curses.echo() curses.nocbreak() curses.endwin() except: pass ############################################################################## # signal handler ############################################################################## def sigtermhandler(signum, frame): # shutdown all running threads hub.notify(events.quit(), 100) signal.signal(signal.SIGTERM, sigtermhandler) ############################################################################## # setup main screen (safety wrapped) ############################################################################## try: stdscr = cursessetup() # set m to None as marker in case that something goes wrong in the # mainscreen.mainscreen constructor m = None m = mainscreen.mainscreen(stdscr, songdbids, playerids, plugins) m.run() except: cursescleanup() # shutdown all other threads hub.notify(events.quit(), 100) helper.print_exc_plus() raise else: cursescleanup() # shutdown all other threads hub.notify(events.quit(), 100) PyTone-3.0.3/src/._pytonectl.py000755 000765 000765 00000000122 11113611465 016637 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/pytonectl.py000755 000765 000765 00000016544 11113611465 016441 0ustar00ringoringo000000 000000 #!/usr/bin/env python # -*- coding: ISO-8859-1 -*- # Copyright (C) 2003, 2004, 2007 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os, os.path, sys, getopt, locale ############################################################################## # gettext initialization. ############################################################################## # We have to do this very early, before importing our modules. We # assume that the locales lie in same dir as this module. This may not # be the case, if the .mo files are installed at their proper # location. try: import gettext locallocaledir = os.path.join(os.path.dirname(sys.argv[0]), "../locale") gettext.install("PyTone", locallocaledir) except: # Disable localization if there is any problem with the above. # This works around a problem with Python 2.1 import __builtin__ __builtin__.__dict__['_'] = lambda s: s import network, events, requests, version, helper # # parse command line options # server = None port = 1972 unixsocketfile = None debugmode = False def usage(): print "pytonectl %s" % version.version print "Copyright (C) 2003, 2004 Jörg Lehmann " print "usage: pytonectl.py [options] command" print print "Possible options are:" print " -h, --help: show this help" print " -s, --server : connect to PyTone server on hostname" print " -p, --port : connect to PyTone server on given port" print " -f, --file : connect to PyTone UNIX socket filename" print " -d, --debug: enable debug mode" print print "The supported commands are:" print " getplayerinfo: show information on the song currently being played" print " playerforward: play the next song in the playlist" print " playerprevious: play the previous song in the playlist" print " playerseekrelative : seek relative in the current song by the given number of seconds" print " playerpause: pause the player" print " playerstart: start/unpause the player" print " playertogglepause: pause the player, if playing, or play, if paused" print " playerstop: stop the player" print " playerratecurrentsong : rate the song currently being played (1<=rating<=5)" print " playlistaddsongs : add files to end of playlist" print " playlistaddsongtop : play file immediately" print " playlistclear: clear the playlist" print " playlistdeleteplayedsongs: remove all played songs from the playlist" print " playlistreplay: mark all songs in the playlist as unplayed" print " playlistshuffle: shuffle the playlist" try: opts, args = getopt.getopt(sys.argv[1:], "hs:p:f:d", ["help", "server=", "port=", "file=", "debug"]) except getopt.GetoptError: usage() sys.exit(2) for o, a in opts: if o in ("-h", "--help"): usage() sys.exit() if o in ("-s", "--server"): server = a if o in ("-p", "--port"): port = int(a) if o in ("-f", "--file"): unixsocketfile = a if o in ("-d", "--debug"): debugmode = True # initialize the debug file if necessary import log, sys if debugmode: log.debugfile = sys.stdout log.info("Debug mode enabled") if server is not None and unixsocketfile is not None: print "Error: cannot connect both via network and unix sockets" sys.exit(2) if server is None: if unixsocketfile is None: unixsocketfile = os.path.expanduser("~/.pytone/pytonectl") networklocation = unixsocketfile else: networklocation = server, port try: channel = network.clientchannel(networklocation) except Exception, e: print "Error: cannot connect to PyTone server: %s" % e sys.exit(2) channel.start() if len(args)==0: usage() sys.exit(2) elif len(args)==1: if args[0]=="playerforward": channel.notify(events.playernext("main")) elif args[0]=="playerprevious": channel.notify(events.playerprevious("main")) elif args[0]=="playerpause": channel.notify(events.playerpause("main")) elif args[0]=="playerstart": channel.notify(events.playerstart("main")) elif args[0]=="playertogglepause": channel.notify(events.playertogglepause("main")) elif args[0]=="playerstop": channel.notify(events.playerstop("main")) elif args[0]=="playlistclear": channel.notify(events.playlistclear()) elif args[0]=="playlistdeleteplayedsongs": channel.notify(events.playlistdeleteplayedsongs()) elif args[0]=="playlistreplay": channel.notify(events.playlistreplay()) elif args[0]=="playlistshuffle": channel.notify(events.playlistshuffle()) elif args[0]=="getplayerinfo": playbackinfo = channel.request(requests.getplaybackinfo("main")) if playbackinfo.song: # we have to manually request the song metadata because there the main event and request hub is not correctly # initialized song_metadata = channel.request(requests.getsong_metadata(playbackinfo.song.songdbid, playbackinfo.song.id)) enc = locale.getpreferredencoding() or "ascii" s=u"%s - %s (%s/%s)" % (song_metadata.artist, song_metadata.title, helper.formattime(playbackinfo.time), helper.formattime(song_metadata.length)) print s.encode(enc) else: usage() sys.exit(2) else: if args[0]=="playerratecurrentsong" and len(args)==2: try: rating = int(args[1]) if not 1<=rating<=5: raise except: usage() sys.exit(2) channel.notify(events.playerratecurrentsong("main", rating)) if args[0]=="playerseekrelative" and len(args)==2: try: seconds = float(args[1]) except: usage() sys.exit(2) channel.notify(events.playerseekrelative("main", seconds)) elif args[0]=="playlistaddsongs": songs = [channel.request(requests.autoregisterer_queryregistersong("main", path)) for path in args[1:]] channel.notify(events.playlistaddsongs(songs)) elif args[0]=="playlistaddsongtop" and len(args)==2: song = channel.request(requests.autoregisterer_queryregistersong("main", (args[1]))) channel.notify(events.playlistaddsongtop(song)) else: usage() sys.exit(2) channel.quit() PyTone-3.0.3/src/._requests.py000644 000765 000765 00000000122 11050307773 016472 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/requests.py000644 000765 000765 00000013754 11050307773 016274 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import item class request: def __repr__(self): return self.__class__.__name__ __repr__ = __repr__ # # database requests # class dbrequest: def __init__(self, songdbid): self.songdbid = songdbid def __repr__(self): return "%r->%r" % (self.__class__.__name__, self.songdbid) def __cmp__(self, other): cmp(hash(self), hash(other)) def __hash__(self): # for the cashing system every dbrequest has to be hashable # by default we rely on self.__repr__ for computing the hash value return hash(repr(self)) class dbrequestsingle(dbrequest): """ db request yielding a single result (not a list) and requiring a specific songdb to work on """ pass class dbrequestsongs(dbrequest): """ db request yielding a list of songs, which have to be merged when querying multiple databases Note that the resulting list must not be changed by the caller, with the exception that the order of the items may be changed at will (for instance by sorting) """ def __init__(self, songdbid, random=False, sort=False, filters=None): self.songdbid = songdbid self.sort = sort self.random = random self.filters = filters def __repr__(self): return "%r(%r, %r, random=%r)->%r" % (self.__class__.__name__, self.sort, self.filters, self.random, self.songdbid) class dbrequestlist(dbrequest): """ db request yielding a result list (not containing songs), which have to be merged when querying multiple databases Note that the resulting list must not be changed by the caller! """ def __init__(self, songdbid, filters=None): self.songdbid = songdbid self.filters = filters def __repr__(self): return "%r(%r)->%r" % (self.__class__.__name__, self.filters, self.songdbid) # # database requests which yield a single result # class getdatabasestats(dbrequest): """ return songdbstats instance for database """ pass class getsong_metadata(dbrequestsingle): """fetch song metadata from database songdbid corresponding to song_id""" def __init__(self, songdbid, song_id): self.songdbid = songdbid self.song_id = song_id def __repr__(self): return "%r(%r)->%r" % (self.__class__.__name__, self.song_id, self.songdbid) class gettag_id(dbrequestsingle): def __init__(self, songdbid, tag_name): self.songdbid = songdbid self.tag_name = tag_name def __repr__(self): return "%r(%r)->%r" % (self.__class__.__name__, self.tag_name, self.songdbid) # # database requests which yield a list of songs # class getsongs(dbrequestsongs): pass class getlastplayedsongs(dbrequestsongs): pass # # database requests which yield lists of other items # class getartists(dbrequestlist): pass class getalbums(dbrequestlist): pass class gettags(dbrequestlist): pass class getratings(dbrequestlist): pass class getplaylists(dbrequestlist): pass # # database request yielding the number of items of a certain kind # class getnumberofsongs(dbrequest): def __init__(self, songdbid, filters=None): self.songdbid = songdbid self.filters = filters def __repr__(self): return "%r(%r))->%r" % (self.__class__.__name__, self.filters, self.songdbid) class dbrequestnumber(dbrequest): def __init__(self, songdbid, filters=None): self.songdbid = songdbid self.filters = filters def __repr__(self): return ( "%r(%r)->%r" % (self.__class__.__name__, self.filters, self.songdbid)) class getnumberofalbums(dbrequestnumber): pass class getnumberofartists(dbrequestnumber): pass class getnumberoftags(dbrequestnumber): pass class getnumberofratings(dbrequestnumber): pass # autoregisterer request class autoregisterer_queryregistersong(dbrequest): def __init__(self, songdbid, path): self.songdbid = songdbid self.path = path def __repr__(self): return "%r(%r)->%r" % (self.__class__.__name__, self.path, self.songdbid) # songdbmanager class getsongdbmanagerstats(request): """ request statistical information about songdbs and the request cache Returns services.songdb.songdbmanagerstats instance.""" pass # # other requests for playlist and player service # class playlist_requestnextsong(request): """ request a playlistitem from playlistid. Go back in playlist if previous is set """ def __init__(self, playlistid, previous=0): self.playlistid = playlistid self.previous = previous def __repr__(self): return "%r->%r,%r" % (self.__class__.__name__, self.playlistid, self.previous) class getplaybackinfo(request): """ request info about song currently playing on player playerid """ def __init__(self, playerid): self.playerid = playerid def __repr__(self): return "%r->%r" % (self.__class__.__name__, `self.playerid`) class requestinput: def __init__(self, title, prompt, handler): self.title = title self.prompt = prompt self.handler = handler def __repr__(self): return "%r(%r,%r,%r)" % (self.__class__.__name__, self.title, self.prompt, `self.handler`) class playlistgetcontents(request): pass PyTone-3.0.3/src/._service.py000644 000765 000765 00000000122 11050307407 016251 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/service.py000644 000765 000765 00000004064 11050307407 016045 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2005 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, threading, traceback import events, hub, log class service(threading.Thread): def __init__(self, name, daemonize=False, hub=hub._defaulthub): threading.Thread.__init__(self) # as independent thread, we want our own event and request channel # and need at least respond to a quit event self.name = name self.setName("%s service" % name) self.channel = hub.newchannel() self.channel.subscribe(events.quit, self.quit) self.done = False self.setDaemon(daemonize) log.debug("started %s service" % self.name) def resetafterexception(self): """ called after an exception has occured during the event/request handling If not reset is possible, an exception can be raised which will lead to the termination of the service. """ pass def work(self): """ do the job """ self.channel.process(block=True) def run(self): # main loop of the service while not self.done: # process events and catch all unhandled exceptions try: self.work() except Exception, e: log.debug_traceback() self.resetafterexception() # event handlers def quit(self, event): self.done = True PyTone-3.0.3/src/services/000755 000765 000765 00000000000 11406223507 015655 5ustar00ringoringo000000 000000 PyTone-3.0.3/src/._slist.py000644 000765 000765 00000000122 10557445573 015771 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/slist.py000644 000765 000765 00000026233 10557445573 015567 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002, 2003, 2004 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import re import events, hub class slist: """ Generic list class with selectable items List with selectable items, out of which maximally height are being displayed at the same time. If window corresponding to list has focus, selectionchanged events are issued upon change of the currently selected item. """ def __init__(self, win, pagescroll): self.win = win self.items = [] self.selected = None # currently selected item self.top = 0 # first displayed item self.pagescroll = pagescroll # generic list methods def __getitem__(self, key): return self.items[key] def __len__(self): return len(self.items) def __delitem__(self, index): del self.items[index] if self.selected is not None: if len(self) == 0: self.selected = None else: if index < self.selected: self.selected -= 1 self.selected = min(self.selected, len(self)-1) self._notifyselectionchanged() self._updatetop() def index(self, item): return self.items.index(item) def insert(self, index, item): self.items.insert(index, item) if self.selected is None: self.selected = 0 self._notifyselectionchanged() elif self.selected>=index: self.selected += 1 self._notifyselectionchanged() self._updatetop() def append(self, item): self.items.append(item) if self.selected is None: self.selected = 0 self._notifyselectionchanged() self._updatetop() def remove(self, item): for i in range(len(self)): if self[i] is item: del self[i] return def set(self, items, keepselection=False): """set all items in slist trying to keep the current selection if keepselection is set """ if keepselection: oldselecteditem = self.getselected() oldselected = self.selected try: self.items = list(items) except: self.items = [] if keepselection and oldselected is not None and self.items: # we try to keep the current selection and search for the previously # selected item in the new list. We most probably find it around # the original position. oldselecteditemid = oldselecteditem.getid() startsearch = min(max(0, oldselected-1), len(self)) for i in range(startsearch, len(self)) + range(startsearch): if self[i].getid() == oldselecteditemid: self.selected = i break else: # if this fails (typically because the song has been # deleted from the playlist), we take the item at the # last selected position, if possible if oldselected < len(self): self.selected = oldselected elif len(self) > 0: self.selected = len(self)-1 self._updatetop() else: if self.items: self.selected = 0 else: self.selected = None self.top = 0 self._notifyselectionchanged() def sort(self, func=None): if func is None: self.items.sort() else: self.items.sort(func) self._notifyselectionchanged() # helper routines def _notifyselectionchanged(self): """ helper routine, which issues a selectionchanged event, if window corresponding to list has focus """ if self.win.hasfocus(): hub.notify(events.selectionchanged(self.getselected())) def _updatetop(self): "helper routine, which updates self.top" if len(self)<=self.win.ih: self.top = 0 return if self.selected is not None and self.selectedself.top+self.win.ih-1: if self.pagescroll: self.top = self.selected else: self.top = self.selected-self.win.ih+1 return # slist specific methods def clear(self): "clear list" self.items = [] self.selected = None self.top = 0 self._notifyselectionchanged() def insertitem(self, item, cmpfunc): "insert item at alphabetically correct position" for i in range(len(self)): if cmpfunc(self[i], item)>=0: self.insert(i, item) break else: self.append(item) def getselected(self): "return currently selected item" if self.selected is not None: try: return self.items[self.selected] except IndexError: if self.items: self.selected = len(self.items)-1 return self.items[self.selected] else: self.selected = None return None def deleteselected(self): "delete currently selected item" if self.selected is not None: del self[self.selected] def selectbynr(self, nr): "select nrth entry in list" self.selected = nr self._notifyselectionchanged() self._updatetop() def selectbylinenumber(self, nr): """select entry by line number in window Returns True if selection was valid, otherwise False. """ if len(self) > 0: if nr >= 0 and self.top+nr < len(self): self.selected = self.top+nr self._notifyselectionchanged() return True return False def selectbyid(self, id): """select entry by id Returns True if selection was valid, otherwise False.""" if len(self) > 0: for i in range(len(self)): if self[i].id == id: self.selected = i self._notifyselectionchanged() self._updatetop() return True return False def selectbysearchstring(self, searchstring): """select next entry matching searchstring. Returns True if selection was valid, otherwise False.""" if len(self) > 0: searchstring = searchstring.lower() if self.selected is None: first = 0 else: first = self.selected for i in range(first+1, len(self)) + range(first): if self[i].getname().lower().find(searchstring)!=-1: self.selected = i self._notifyselectionchanged() self._updatetop() return True return False def selectbyregexp(self, regexp, includeselected=True): """select next entry matching regexp Returns True if selection was valid, otherwise False.""" if len(self) > 0: try: cregexp = re.compile(regexp, re.IGNORECASE) except: return if self.selected is None: first = 0 else: first = self.selected for i in range(first + (not includeselected and 1 or 0), len(self)) + range(first): if cregexp.search(self[i].getname()): self.selected = i self._notifyselectionchanged() self._updatetop() return True return False def selectbyletter(self, letter): """select next entry beginning with letter Returns True if selection was valid, otherwise False.""" if len(self)>0: if self.selected is None: first = 0 else: first = self.selected letter = letter.lower() for i in range(first+1, len(self)) + range(first): if self[i].getname().lower().startswith(letter): self.selected = i self._notifyselectionchanged() self._updatetop() return True return False def selectrelative(self, dist): "change selection relatively by dist" if len(self) > 0: self.selected += dist self.selected = max(self.selected,0) self.selected = min(self.selected, len(self)-1) self._notifyselectionchanged() self._updatetop() def selectfirst(self): "select first item of list" if len(self) > 0: self.selected = 0 self._notifyselectionchanged() self._updatetop() def selectlast(self): "select last item of list" if len(self) > 0: self.selected = len(self)-1 self._notifyselectionchanged() self._updatetop() def selectnext(self): "select next item of list" self.selectrelative(1) def selectprev(self): "select previous item of list" self.selectrelative(-1) def selectnextpage(self): "select next page of list" self.selectrelative(self.win.ih) def selectprevpage(self): "select previous page of list" if len(self) > 0: if self.top-self.win.ih >= 0: self.top = self.top-self.win.ih self.selected = min(self.top+self.win.ih-1, len(self)-1) else: self.top = self.selected = 0 self._notifyselectionchanged() def moveitemup(self): "move selected item up, if not first" if self.selected is not None and self.selected > 0: self.items[self.selected-1], self.items[self.selected] = \ self.items[self.selected], self.items[self.selected-1] self.selected -= 1 self._updatetop() def moveitemdown(self): "move selected item down, if not last" if self.selected is not None and self.selected < len(self)-1: self.items[self.selected], self.items[self.selected+1] = \ self.items[self.selected+1], self.items[self.selected] self.selected += 1 self._updatetop() PyTone-3.0.3/src/._statswin.py000644 000765 000765 00000000122 11051522670 016467 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/statswin.py000644 000765 000765 00000007345 11051522670 016270 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2005 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import config import encoding import messagewin import hub, requests class statswin(messagewin.messagewin): def __init__(self, screen, maxh, maxw, channel, numberofsongdbs): # column number of message string messagewin.messagewin.__init__(self, screen, maxh, maxw, channel, config.colors.statswindow, _("PyTone Statistics"), [], config.statswindow.autoclosetime) self.numberofsongdbs = numberofsongdbs def _outputlen(self, iw): """number of lines in window with inner widht iw""" result = self.numberofsongdbs*4 + 3 return result def showitems(self): lines = [] stats = hub.request(requests.getsongdbmanagerstats()) indent = " "*3 for songdbstats in stats.songdbsstats: dbidstring = _("Database %s") % songdbstats.id + ":" dbstatstring = _("%d songs, %d albums, %d artists, %d tags") % (songdbstats.numberofsongs, songdbstats.numberofalbums, songdbstats.numberofartists, songdbstats.numberoftags) lines.append((dbidstring, dbstatstring)) if songdbstats.type == "local": dbtypestring = _("local database (db file: %s)") % (songdbstats.dbfile) else: dbtypestring = _("remote database (server: %s)") % (songdbstats.location) lines.append((indent + _("type") + ":", dbtypestring)) lines.append((indent + _("base directory") + ":", songdbstats.basedir)) dbcachesizestring = "%dkB" % songdbstats.cachesize lines.append((indent + _("cache size") + ":", dbcachesizestring)) lines.append(("", "")) cachestatsstring = _("%d requests, %d / %d objects") % (stats.requestcacherequests, stats.requestcachesize, stats.requestcachemaxsize) if stats.requestcachemaxsize != 0: cachestatsstring = cachestatsstring + " (%d%%)" % (100*stats.requestcachesize//stats.requestcachemaxsize) lines.append((_("Request cache size") + ":", cachestatsstring)) totalrequests = stats.requestcachehits + stats.requestcachemisses if totalrequests != 0: percentstring = " (%d%%)" % (100*stats.requestcachehits//totalrequests) else: percentstring = "" lines.append((_("Request cache stats") + ":", (_("%d hits / %d requests") % (stats.requestcachehits, totalrequests)) + percentstring)) wc1 = max([len(lc) for lc, rc in lines]) + 1 if wc1 > 0.6*self.iw: wc1 = int(0.6*self.iw) wc2 = self.iw - wc1 y = self.iy for lc, rc in lines: self.move(y, self.ix) self.addstr(encoding.encode(lc).ljust(wc1)[:wc1], self.colors.description) self.addstr(encoding.encode(rc).ljust(wc2)[:wc2], self.colors.content) y += 1 PyTone-3.0.3/src/._statusbar.py000644 000765 000765 00000000122 10557445600 016632 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/statusbar.py000644 000765 000765 00000007317 10557445600 016432 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import curses import config import hub, events import help import window import events import encoding # indicator which can be used to signalise that the statusbar # has to be terminated here terminate = [("TERMINATE", 0)] def generatedescription(section, name): primarykey = config.keybindings[section][name][0] keyname = help.getkeyname(primarykey) # save some space if keyname.startswith("<") and keyname.endswith(">"): keyname = keyname[1:-1] return [(keyname, config.colors.statusbar.key), (":"+help.descriptions[section][name][0], config.colors.statusbar.description)] class statusbar(window.window): def __init__(self, screen, line, width, channel): self.channel = channel # self.content[0]: local info # self.content[1]: player info # self.content[2]: global info self.content = [[], [], []] # message overriding contents self.message = None # for identification purposes, we only generate this once self.removemessageevent = events.statusbar_showmessage(None) window.window.__init__(self, screen, 1, width, line, 0, config.colors.statusbar, None) self.channel.subscribe(events.statusbar_update, self.statusbar_update) self.channel.subscribe(events.statusbar_showmessage, self.statusbar_showmessage) # hack to export some properties of the statusbar singleton into # the module namespace global separator if width<=80: separator = [(" ", self.colors.background)] else: separator = [(" ", self.colors.background)] def resize(self, line, width): window.window.resize(self, 1, width, line, 0) global separator if width<=80: separator = [(" ", self.colors.background)] else: separator = [(" ", self.colors.background)] # event handler def statusbar_update(self, event): self.content[event.pos] = event.content self.update() def statusbar_showmessage(self, event): self.message = event.message if self.message: # make message disappear after a certain time hub.notify(events.sendeventin(self.removemessageevent, 2, replace=1)) self.update() # we want to get a message out immediately, so we force its output curses.panel.update_panels() curses.doupdate() # update method def update(self): window.window.update(self) self.move(0,0) self.clrtoeol() if self.message: self.addstr(encoding.encode(self.message), config.colors.statusbar.key) else: for element in self.content[0]+separator+self.content[1]+separator+self.content[2]: if element==terminate[0]: break self.addstr(encoding.encode(element[0]), element[1]) PyTone-3.0.3/src/version.py000644 000765 000765 00000001603 11406223111 016060 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2003, 2004, 2005, 2006 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA version = "3.0.3" copyright = u"(c) 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010 Jörg Lehmann " PyTone-3.0.3/src/window.py000644 000765 000765 00000031401 11342234740 015712 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import curses import curses.panel import events, hub import config import encoding class window: def __init__(self, screen, h, w, y, x, colors, title=None, border = 15, hasscrollbar=0): self.screen = screen self.win = curses.newwin(h, w, y, x) self.panel = curses.panel.new_panel(self.win) self.panel.set_userptr(self) curses.panel.update_panels() self.colors = colors self.border = border self.hasscrollbar = hasscrollbar self._setdimensions(h, w, y, x) self.settitle(title) # list of additional border elements due to connections from other windows self.borderelements = [] def _setdimensions(self, h, w, y, x): self.h = h self.w = w self.x = x self.y = y self.win.bkgdset(0, self.colors.background) # coordinates of upper left corner and width and height of inner part of window if self.h>=2: # we always have a title line self.iy = 1 if self.hasleftborder(): self.ix = 1 else: self.ix = 0 if self.hasrightborder() or self.hasscrollbar: self.iw = self.w - self.ix - 1 else: self.iw = self.w - self.ix if self.hasbottomborder(): self.ih = h - self.iy - 1 else: self.ih = h-self.iy else: # treat very small windows, which do not have a title line, separately self.ix = 0 self.iy = 0 self.ih = h self.iw = w # some error protected versions of the standard curses library def move(self, *args): self.win.move(*args) def addch(self, *args): try: self.win.addch(*args) except curses.error: pass def addstr(self, *args): try: self.win.addstr(*args) except curses.error: pass def addnstr(self, *args): try: self.win.addnstr(*args) except curses.error: pass def clrtoeol(self): self.win.clrtoeol() def clear(self): for y in range(self.ih): self.addstr(y+1, self.ix, " "*self.iw) def hline(self, x, y, c, n, attr): try: self.win.hline(x, y, c, n, attr) except TypeError: # workaround for Python 2.1.x for y in range(y, y+n): self.addch(x, y, c, attr) def vline(self, x, y, c, n, attr): try: self.win.vline(x, y, c, n, attr) except TypeError: # workaround for Python 2.1.x for x in range(x, x+n): self.addch(x, y, c, attr) def hastopborder(self): return self.border & config.BORDER_TOP def hasbottomborder(self): return self.border & config.BORDER_BOTTOM def hasleftborder(self): return self.border & config.BORDER_LEFT def hasrightborder(self): return self.border & config.BORDER_RIGHT def getborderends(self): """ return open ends of border as list of tuples (y, x, direction) where direction is one of "left", "right" , "up" or "down" """ borderends = [] if self.hastopborder() and not self.hasleftborder(): borderends.append((self.y, self.x-1, "left")) if self.hastopborder() and not self.hasrightborder(): borderends.append((self.y, self.x+self.w+1, "right")) if self.hasbottomborder() and not self.hasleftborder(): borderends.append((self.y+self.h-1, self.x-1, "left")) if self.hasbottomborder() and not self.hasrightborder(): borderends.append((self.y+self.h-1, self.x+self.w+1, "right")) if self.hasleftborder() and not self.hastopborder(): borderends.append((self.y-1, self.x, "up")) if self.hasrightborder() and not self.hastopborder(): borderends.append((self.y-1, self.x+self.w-1, "up")) if self.hasleftborder() and not self.hasbottomborder(): borderends.append((self.y+self.h+1, self.x, "down")) if self.hasrightborder() and not self.hasbottomborder(): borderends.append((self.y+self.h+1, self.x+self.w-1, "down")) return borderends def connectborderends(self, borderends): """ update list of border connections from other windows """ self.borderelements = [] for y, x, d in borderends: if self.win.enclose(y, x): if d == "right" and self.hasleftborder(): dy = y - self.y if dy == 0 and self.hastopborder(): cel = curses.ACS_TTEE elif dy == self.h-1 and self.hasbottomborder(): cel = curses.ACS_BTEE else: cel = curses.ACS_LTEE self.borderelements.append((dy, 0, cel)) elif d == "left" and self.hasrightborder(): dy = y - self.y if dy == 0 and self.hastopborder(): cel = curses.ACS_TTEE elif dy == self.h-1 and self.hasbottomborder(): cel = curses.ACS_BTEE else: cel = curses.ACS_LTEE self.borderelements.append((dy, self.w-1, cel)) elif d == "up" and self.hasbottomborder(): dx = x - self.x if dx == 0 and self.hasleftborder(): cel = curses.ACS_LTEE elif dx == self.w-1 and self.hasrightborder(): cel = curses.ACS_RTEE else: cel = curses.ACS_TTEE self.borderelements.append((self.h, dx, cel)) elif d == "down" and self.hastopborder(): dx = x - self.x if dx == 0 and self.hasleftborder(): cel = curses.ACS_LTEE elif dx == self.w-1 and self.hasrightborder(): cel = curses.ACS_RTEE else: cel = curses.ACS_BTEE self.borderelements.append((0, dx, cel)) def resize(self, h, w, y, x): """ resize window """ try: self.win.resize(h, w) self.win.mvwin(y, x) except curses.error: pass self._setdimensions(h, w, y, x) def settitle(self, title): if title is not None: self.title = title else: self.title = None def scrollbardimensions(self, top, total): if total>0: totalheight = self.ih-4 scrollbarbegin = 3 + totalheight*top/total scrollbarheight = totalheight*self.ih/total scrollbarheight = min(max(scrollbarheight, 1), totalheight-scrollbarbegin+3) return scrollbarbegin, scrollbarheight else: return 0, 0 def drawscrollbar(self, top, total): if self.hasscrollbar: if not self.hasrightborder() and self.ih>2: self.vline(1, self.iw+self.ix, " ", self.ih, self.colors.background) if total>self.ih: xpos = self.iw+self.ix if top!=0: self.addch(2, xpos, curses.ACS_UARROW, self.colors.scrollbararrow) if top+self.ih=2: if self.hasfocus(): topborder = 0 # uncomment this to get the "thick" border of the old times # topborder = ord("=") attr = self.colors.activeborder try: titleattr = self.colors.activetitle except AttributeError: titleattr = self.colors.title else: topborder = 0 attr = self.colors.border titleattr = self.colors.title # draw configured borders if self.hastopborder(): self.hline(0, self.ix, curses.ACS_HLINE, self.iw, attr) # self.win.border(0, 0, topborder) t = encoding.encode(self.title)[:self.w-4] pos = (self.w-4-len(t))/2 self.addstr(0, pos, "[ %s ]" % t, titleattr) if self.hasleftborder(): self.addch(0, 0, curses.ACS_ULCORNER, attr) if self.hasrightborder(): self.addch(0, self.ix+self.iw, curses.ACS_URCORNER, attr) else: self.addch(0, self.ix+self.iw, curses.ACS_HLINE, attr) else: t = self.title[:self.w] self.addstr(0, 0, t.center(self.w), titleattr) if self.hasbottomborder(): self.hline(self.iy+self.ih, self.ix, curses.ACS_HLINE, self.iw, attr) if self.hasleftborder(): self.addch(self.iy+self.ih, 0, curses.ACS_LLCORNER, attr) if self.hasrightborder(): self.addch(self.iy+self.ih, self.ix+self.iw, curses.ACS_LRCORNER, attr) else: self.addch(self.iy+self.ih, self.ix+self.iw, curses.ACS_HLINE, attr) if self.hasleftborder(): if self.hastopborder(): self.vline(self.iy, 0, curses.ACS_VLINE, self.ih, attr) else: self.vline(0, 0, curses.ACS_VLINE, self.ih+1, attr) if self.hasrightborder(): if self.hastopborder(): self.vline(self.iy, self.ix+self.iw, curses.ACS_VLINE, self.ih, attr) else: self.vline(0, self.ix+self.iw, curses.ACS_VLINE, self.ih+1, attr) # draw additional border elements for y, x, c in self.borderelements: self.addch(y, x, c, attr) # event handler def hidewindow(self, event): # subclasses of window explicitely have to subscribe to this event, # if they need to if event.window==self: self.hide() PyTone-3.0.3/src/services/.___init__.py000644 000765 000765 00000000122 10266220576 020204 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/services/__init__.py000644 000765 000765 00000000310 10266220576 017766 0ustar00ringoringo000000 000000 # this is required to convert this directory into a package """ this package contains a collection of services, i.e. threads processing requests (and acting on events) of other parts of PyTone. """ PyTone-3.0.3/src/services/player.py000644 000765 000765 00000041706 11251262212 017525 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002, 2003 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import copy, time import events, hub, requests import log import services.playlist import service def initplayer(id, config): """ initialize player with id defined by config return id (or None if player is turned off) """ # only the first player has a playlist if id == "main": playlistid = "main" else: playlistid = None type = config.type if type=="off": return None elif type=="internal": import players.internal driver = config.driver if driver in ("alsa09", "alsa"): aooptions = {"dev": config.device} elif driver=="oss": aooptions = {"dsp": config.device} elif driver=="sun": aooptions = {"dev": config.device} else: aooptions = {} # add options given by user in config file for aooption in config.aooptions.split(): key, value = aooption.split("=") aooptions[key] = value try: p = players.internal.player(id, playlistid, autoplay=config.autoplay, aodevice=driver, aooptions=aooptions, bufsize=config.bufsize, crossfading=config.crossfading, crossfadingstart=config.crossfadingstart, crossfadingduration=config.crossfadingduration, ) except: log.debug_traceback() raise RuntimeError("Cannot initialize %s player: type=internal, device=%s" % (id, config.device)) elif type=="xmms": import players.xmmsplayer try: p = players.xmmsplayer.player(id, playlistid, autoplay=config.autoplay, session=config.session, noqueue=config.noqueue) except: log.debug_traceback() raise RuntimeError("Cannot initialize %s player: type=xmms, session=%d" % (id, config.session)) elif type=="mpg123": import players.mpg123 try: p = players.mpg123.player(id, playlistid, autoplay=config.autoplay, cmdline=config.cmdline) except: log.debug_traceback() raise RuntimeError("Cannot initialize %s player: type=mpg123, cmdline=%s" % (id, config.cmdline)) elif type=="remote": import players.remote try: p = players.remote.player(id, playlistid, config.networklocation) except: log.debug_traceback() raise RuntimeError("Cannot initialize %s player: type=remote, location=%s" % (id, config.networklocation)) p.setName("player thread (id=%s)" % id) if type != "remote" and id == "main": services.playlist.initplaylist(id, id, id) # start player only after the playlist service has been started since # playlist requests may already be issued during the player startup p.start() return id # player states STOP = 0 PAUSE = 1 PLAY = 2 class playbackinfo: """ class for storage of playback information This class serves as a means of communication between the actual players and the player control logic """ def __init__(self, playerid, send_played_skipped_events): """ playerid: player which this playbackinfo instance refers to state: player state (STOP, PAUSE, PLAY) song: song currently played (or None, if player is not playing) time: position in seconds in the song crossfade: crossfade in progress send_played_skipped_events: should song_played / song_skipped events (still) be send """ self.playerid = playerid self.state = STOP self.song = None self.time = 0 self.crossfade = False self.send_played_skipped_events = send_played_skipped_events def __cmp__(self, other): return (cmp(self.playerid, other.playerid) or cmp(self.state, other.state) or cmp(self.song, other.song) or cmp(self.time, other.time) or cmp(self.crossfade, other.crossfade) or cmp(self.send_played_skipped_events, other.send_played_skipped_events)) def __str__(self): s = "player %s " % `self.playerid` if self.state==STOP: s = s + "stopped" elif self.state==PAUSE: s = s + "paused" elif self.state==PLAY: s = s + "playing" s = s + " song: " if self.song: s = s + "%r at time %f" % ( self.song, self.time) else: s = s + "None" if self.crossfade: s = s+ " (crossfading)" return s def updatesong(self, song): """ update song and reset time """ self.song = song self.time = 0 def stopped(self): self.state = STOP self.song = None self.time = 0 self.crossfade = False def paused(self): self.state = PAUSE def playing(self): self.state = PLAY def updatetime(self, time): self.time = time def updatecrossfade(self, crossfade): self.crossfade = crossfade def isplaying(self): return self.state == PLAY def ispaused(self): return self.state == PAUSE def isstopped(self): return self.state == STOP def iscrossfading(self): return self.crossfade and not self.state == STOP class genericplayer(service.service): def __init__(self, id, playlistid, autoplay): """create a new player id: the player id playlistid: playlist responsible for feeding player with songs. Set to None, if there is no playlist for the player. autoplay: should the player start automatically, if a song is in the playlist and it has not been stopped explicitely by the user """ service.service.__init__(self, "player %s" % id, daemonize=True) self.id = id self.autoplay = autoplay self.playlistid = playlistid # if wantplay != autoplay, the user has requested a player stop and thus # autoplay is effectively turned off, until the player is restarted again self.wantplay = autoplay # should we notify the database that the song has been played self.sendplayedevent = False # the playbackinfo structure describes the current player state # If a playlist is attached to the player, we issue song_played and song_skipped events self.playbackinfo = playbackinfo(self.id, playlistid is not None) # old playbackinfo, used to detect changes of the player state self.oplaybackinfo = copy.copy(self.playbackinfo) self.channel.subscribe(events.playerstart, self.playerstart) self.channel.subscribe(events.playerpause, self.playerpause) self.channel.subscribe(events.playertogglepause, self.playertogglepause) self.channel.subscribe(events.playerstop, self.playerstop) self.channel.subscribe(events.playernext, self.playernext) self.channel.subscribe(events.playerprevious, self.playerprevious) self.channel.subscribe(events.playerseekrelative, self.playerseekrelative) self.channel.subscribe(events.playerplaysong, self.playerplaysong) self.channel.subscribe(events.playerratecurrentsong, self.playerratecurrentsong) self.channel.subscribe(events.player_change_volume_relative, self.player_change_volume_relative) self.channel.subscribe(events.playerplayfaster, self.playerplayfaster) self.channel.subscribe(events.playerplayslower, self.playerplayslower) self.channel.subscribe(events.playerspeedreset, self.playerspeedreset) self.channel.supply(requests.getplaybackinfo, self.getplaybackinfo) def work(self): if self.isplaying(): if ( self.playbackinfo.send_played_skipped_events and self.playbackinfo.song and (self.playbackinfo.song.length < 10 or self.playbackinfo.time > 0.8*self.playbackinfo.song.length) ): song = self.playbackinfo.song hub.notify(events.song_played(song.songdbid, song, time.time()-self.playbackinfo.time)) self.playbackinfo.send_played_skipped_events = False self.play() # request a new song, if none is playing and the player wants to play if self.isstopped() and self.wantplay: self.requestnextsong() # process incoming events self.channel.process() # and notify the rest of any changes in the playback status self.updatestatus() # Now the queue of all pending events has been # cleared. Depending on the player status we can now wait for # further incoming events. if not self.isplaying(): # We sleep a little bit to prevent being overly active # when the event channel is spilled by messages time.sleep(0.2) # In this case, we can safely block since we will be waked # up by any message on the event channel. Thus, event if # we want to request a new song, we can rely on an event # signaling the addition of a new song to the playlist. # Before doing so, we release the player device self._playerreleasedevice() self.channel.process(block=True) def play(self): """play songs this method has to be implemented by specialized classes""" pass def updatestatus(self): """notify interested parties of changes in player status""" if self.oplaybackinfo != self.playbackinfo: self.oplaybackinfo = copy.copy(self.playbackinfo) hub.notify(events.playbackinfochanged(self.playbackinfo)) def requestnextsong(self, manual=False, previous=False): """request next song from playlist and play it""" if self.playlistid is not None: nextsong = hub.request(requests.playlist_requestnextsong(self.playlistid, previous)) self.playsong(nextsong, manual) def playsong(self, song, manual): """add song to playlist and mark song played, if song is not None manual indicates whether the user has requested the song manually """ if song: self._playsong(song, manual) self.playbackinfo.playing() def isstopped(self): return self.playbackinfo.state == STOP def ispaused(self): return self.playbackinfo.state == PAUSE def isplaying(self): return self.playbackinfo.state == PLAY def _playsong(self, song, manual): """add song to playlist manual indicates whether the user has requested the song manually this method has to be implemented by specialized classes""" pass def _playerstart(self): """prepare player for playing this method has to be implemented by specialized classes""" pass def _playerpause(self): """pause player this method has to be implemented by specialized classes""" pass def _playerunpause(self): """restart player after pause this method has to be implemented by specialized classes""" pass def _playerstop(self): """stop playing this method has to be implemented by specialized classes""" pass def _playerseekrelative(self, seconds): """seek by the given number of seconds in file (relative to current position) this method has to be implemented by specialized classes""" pass def _player_change_volume_relative(self, volume_adj): """change playback volume by volume_adj percent this method has to be implemented by specialized classes""" pass def _playerplayfaster(self): """Speed up the play rate of the current song this method has to be implemented by specialized classes""" pass def _playerplayslower(self): """Slow down the play rate of the current song this method has to be implemented by specialized classes""" pass def _playerspeedreset(self): """Reset the play rate of the current song back to normal this method has to be implemented by specialized classes""" pass def _playerreleasedevice(self): """temporarily release audio device this method has to be implemented by specialized classes""" pass def _playerquit(self): """quit player this method has to be implemented by specialized classes""" pass # event handlers def playerstart(self, event): """start playing""" if event.playerid == self.id: if self.ispaused(): self._playerunpause() elif self.isstopped(): self.wantplay = self.autoplay self._playerstart() self.requestnextsong() self.playbackinfo.playing() def playerpause(self, event): """start/pause player""" if event.playerid == self.id: if self.isplaying(): self.playbackinfo.paused() self._playerpause() def playertogglepause(self, event): """start/pause player""" if event.playerid == self.id: if self.isplaying(): self.playbackinfo.paused() self._playerpause() elif self.ispaused(): self.playbackinfo.playing() self._playerunpause() def playerstop(self, event): """stop playing""" if event.playerid == self.id: self.wantplay = False self.playbackinfo.stopped() self._playerstop() def playernext(self, event): """immediately play next song""" if event.playerid == self.id: # mark playing of song as skipped if it belongs to a playlist if self.playbackinfo.send_played_skipped_events and self.playbackinfo.song: song = self.playbackinfo.song hub.notify(events.song_skipped(song.songdbid, song)) # we also prevent this song from being registered as played self.playbackinfo.send_played_skipped_events = False self.requestnextsong(manual=1) def playerprevious(self, event): """immediately play previous song""" if event.playerid == self.id: self.requestnextsong(manual=1, previous=1) def playerseekrelative(self, event): """seek by event.seconds in file (relative to current position """ if event.playerid == self.id: self._playerseekrelative(event.seconds) def player_change_volume_relative(self, event): """ change volume relative to given setting """ if event.playerid == self.id: self._player_change_volume_relative(event.volume_adj) def playerplayfaster(self, event): if event.playerid == self.id: self._playerplayfaster() def playerplayslower(self, event): if event.playerid == self.id: self._playerplayslower() def playerspeedreset(self, event): if event.playerid == self.id: self._playerspeedreset() def playerplaysong(self, event): """play event.song next""" if event.playerid == self.id: self.playsong(event.playlistitemorsong, manual=1) def playerratecurrentsong(self, event): """play event.song next""" if event.playerid == self.id and self.playbackinfo.song and 1 <= event.rating <= 5: self.playbackinfo.song.rate(event.rating) def quit(self, event): """quit player""" service.service.quit(self, event) self._playerquit() # request handlers def getplaybackinfo(self, request): if self.id != request.playerid: raise hub.DenyRequest else: return self.playbackinfo PyTone-3.0.3/src/services/players/000755 000765 000765 00000000000 11406223507 017334 5ustar00ringoringo000000 000000 PyTone-3.0.3/src/services/._playlist.py000644 000765 000765 00000000122 11051765045 020304 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/services/playlist.py000644 000765 000765 00000040422 11051765045 020076 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2003, 2004 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os.path import random import time import pickle import config import events, hub, requests import item import log import service import encoding _counter = 0 class playlistitem: """ wrapped song with the two additional attributes played and id - id: unique id for playlist item (note that the same song can be present more than once in the playlist) - played: has playlist item already been played - playstarttime: time at which song has been played or None if played is False """ def __init__(self, song, played=False, playstarttime=None): global _counter self.song = song self.played = played self.playstarttime = playstarttime self.id = _counter _counter += 1 def __repr__(self): return "playlistitem: id=%s" % `self.id` def getid(self): return self.id def getinfo(self): return self.song.getinfo() def getinfolong(self): return self.song.getinfolong() def markplayed(self): self.played = True self.playstarttime = time.time() def markunplayed(self): self.played = False def hasbeenplayed(self): return self.played def initplaylist(id, playerid, songdbid): """initialize playlist service corresponding to player with playerid """ playlist(id, playerid, songdbid).start() class playlist(service.service): """manage playlist for a single player, which can be accessed by multipled users""" def __init__(self, id, playerid, songdbid): service.service.__init__(self, "playlist") self.id = id # each playlist service is identified by the corresponding player self.playerid = playerid self.songdbid = songdbid self.items = [] self.ttime = 0 self.ptime = 0 self.playingitem = None self.logfilename = config.general.logfile self.autoplaymode = config.general.autoplaymode self.channel.subscribe(events.playbackinfochanged, self.playbackinfochanged) self.channel.subscribe(events.playerstop, self.playerstop) self.channel.subscribe(events.playlistaddsongs, self.playlistaddsongs) self.channel.subscribe(events.playlistaddsongtop, self.playlistaddsongtop) self.channel.subscribe(events.playlistdeletesong, self.playlistdeletesong) self.channel.subscribe(events.playlistmovesongup, self.playlistmovesongup) self.channel.subscribe(events.playlistmovesongdown, self.playlistmovesongdown) self.channel.subscribe(events.playlistclear, self.playlistclear) self.channel.subscribe(events.playlistdeleteplayedsongs, self.playlistdeleteplayedsongs) self.channel.subscribe(events.playlistreplay, self.playlistreplay) self.channel.subscribe(events.playlistsave, self.playlistsave) self.channel.subscribe(events.playlistshuffle, self.playlistshuffle) self.channel.subscribe(events.playlisttoggleautoplaymode, self.playlisttoggleautoplaymode) self.channel.subscribe(events.playlistplaysong, self.playlistplaysong) self.channel.subscribe(events.songchanged, self.songchanged) self.channel.supply(requests.playlist_requestnextsong, self.playlist_requestnextsong) self.channel.supply(requests.playlistgetcontents, self.playlistgetcontents) # try to load dump from prior crash, if existent if config.general.dumpfile: try: if os.path.isfile(config.general.dumpfile): self.load() os.unlink(config.general.dumpfile) except: pass def append(self, item): self.ttime += item.song.length if item.hasbeenplayed(): self.ptime += item.song.length self.items.append(item) def insert(self, index, item): self.ttime += item.song.length if item.hasbeenplayed(): self.ptime += item.song.length self.items.insert(index, item) def __delitem__(self, index): item = self.items[index] self.ttime -= item.song.length if item.hasbeenplayed(): self.ptime -= item.song.length self.items.__delitem__(index) self._updateplaystarttimes() # all methods starting with an underscore may modify the playlist but leave # it up to the caller to announce this change via an playlistchanged event def _searchnextitem(self): """return playlistitem which has to be played next or None""" for i in range(len(self.items)): if not self.items[i].hasbeenplayed(): return self.items[i] return None def _logplay(self, item): if self.logfilename: logfile = open(self.logfilename, "a") logfile.write("%s: %s\n" % (time.asctime(), encoding.encode_path(item.song.url))) logfile.close() def _updateplaystarttimes(self): # TODO: take crossfading time into account if self.playingitem: playstarttime = self.playingitem.playstarttime + self.playingitem.song.length else: playstarttime = time.time() for item in self.items: if not item.hasbeenplayed(): item.playstarttime = playstarttime playstarttime += item.song.length def _playitem(self, item): """ check for a song abortion, register song as being played and update playlist information accordingly""" if not item.hasbeenplayed(): self.ptime += item.song.length self.playingitem = item item.markplayed() self._updateplaystarttimes() self._logplay(item) def _playnext(self): """ mark next item from playlist as played and as currently playing and return corresponding song""" nextitem = self._searchnextitem() if nextitem: self._playitem(nextitem) return nextitem else: return None def _playprevious(self): """ mark next item from playlist as played & currently playing and return corresponding song""" # start either from the currently playing song, or if no song # is currently played, the first unplayed song in the playlist... # if self.playingitem: currentitem = self.playingitem else: currentitem = self._searchnextitem() if currentitem: # ... and go back one song i = self.items.index(currentitem) if i == 0: return self._markunplayed(currentitem) item = self.items[i-1] self._playitem(item) return item def _clear(self): self.items = [] self.ptime = 0 self.ttime = 0 self.playingitem = None def _deleteplayedsongs(self): for i in range(len(self.items)-1,-1,-1): if self.items[i].hasbeenplayed() and self.items[i] != self.playingitem: del self[i] def _checksong(self, song): # it is ok if the song is contained in a local song database, so we first # check whether this is the case. # XXX make this behaviour configurable? stats = hub.request(requests.getdatabasestats(song.songdbid)) if isinstance(song, item.song): if stats.type == "local": return song return song # XXX do we really need this # currently it does not work anymore if os.path.isfile(song.path): # first we try to access the song via its filesystem path return hub.request(requests.queryregistersong(self.songdbid, song.path)) if song.artist != dbitem.UNKNOWN and song.album != dbitem.UNKNOWN: # otherwise we use the artist and album tags and try to obtain the song via # the database songs = hub.request(requests.getsongs(self.songdbid, artist=song.artist, album=song.album)) for asong in songs: if asong.title == song.title: return asong # song not found # XXX start transmitting song return def _addsongs(self, songs): """add songs to end of playlist""" for song in songs: if song: song = self._checksong(song) if song: self.append(playlistitem(song)) self._updateplaystarttimes() def _markunplayed(self, item): """ mark song unplayed and adjust playlist information accordingly """ if item.hasbeenplayed(): self.ptime -= item.song.length item.markunplayed() self._updateplaystarttimes() def _markallunplayed(self): """ mark all songs in playlist as not having been played """ for item in self.items: self._markunplayed(item) # convenience method for issuing a playlistchanged event def notifyplaylistchanged(self): hub.notify(events.playlistchanged(self.items, self.ptime, self.ttime, self.autoplaymode, self.playingitem)) # statusbar input handler def saveplaylisthandler(self, name, key): name = name.strip() if key == ord("\n") and name != "" and self.items: songs = [item.song for item in self.items if item.song.songdbid == self.songdbid ] hub.notify(events.add_playlist(self.songdbid, name, songs)) def _locatesong(self, id): """ locate position of item in playlist by id """ for item, i in zip(self.items, range(len(self.items))): if item.id == id: return i else: return None def dump(self): """ write playlist to dump file """ if self.playingitem: self.playingitem.markunplayed() # self._deleteplayedsongs() self.notifyplaylistchanged() dumpfile = open(config.general.dumpfile, "w") pickle.dump(self.items, dumpfile) def load(self): """ load playlist from file """ dumpfile = open(config.general.dumpfile, "r") self._clear() for item in pickle.load(dumpfile): # We have to be careful here and not use the playlist item # stored in the dump file directly, since its id and the # global _counter variable are not in accordance. Besides that # the playstarttime information stored in the pickle is incorrect. # We thus have to create a new playlistitem. newplaylistitem = playlistitem(item.song, item.played, item.playstarttime) self.append(newplaylistitem) self._updateplaystarttimes() # event handlers def playbackinfochanged(self, event): # We are only interested in the case of the player having been stopped due to # no more song being left in the playlist. if event.playbackinfo.isstopped(): self.playingitem = None self.notifyplaylistchanged() def playerstop(self, event): # Mark the currently playing song as unplayed again when the # player has been stopped manually. Note that the handling of # this event is potentially racy with the playbackinfochanged # event, but the ordering of the events in our event channel # should prevent any problems. if event.playerid == self.playerid: if self.playingitem: self._markunplayed(self.playingitem) self.playingitem = None self.notifyplaylistchanged() def playlistaddsongs(self, event): self._addsongs(event.songs) self.notifyplaylistchanged() def playlistaddsongtop(self, event): if event.song: song = self._checksong(event.song) if song: newitem = playlistitem(song) for i in range(len(self.items)): if not self.items[i].hasbeenplayed(): self.insert(i, newitem) break else: self.append(newitem) self._playitem(newitem) self._updateplaystarttimes() hub.notify(events.playerplaysong(self.playerid, newitem)) self.notifyplaylistchanged() def playlistdeletesong(self, event): i = self._locatesong(event.id) if i is not None: del self[i] self.notifyplaylistchanged() def playlistmovesongup(self, event): i = self._locatesong(event.id) if i is not None and i > 0: self.items[i-1], self.items[i] = self.items[i], self.items[i-1] self._updateplaystarttimes() self.notifyplaylistchanged() def playlistmovesongdown(self, event): i = self._locatesong(event.id) if i is not None and i # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import copy, gc, math, random, service, time import config import events, hub, requests import metadata import item import log # helper function for the random selection of songs # # a collection of statistical information # class songdbmanagerstats: def __init__(self, songdbsstats, requestcachesize, requestcachemaxsize, requestcacherequests, requestcachehits, requestcachemisses): self.songdbsstats = songdbsstats self.requestcachesize = requestcachesize self.requestcachemaxsize = requestcachemaxsize self.requestcacherequests = requestcacherequests self.requestcachehits = requestcachehits self.requestcachemisses = requestcachemisses # # the song database manager class # class songdbmanager(service.service): """ song database manager The song database manager receives database events and requests, passes them on to the various databases, collects the results and delivers them back to the caller. - Results of database requests are cached. - dbitem.song instances are wrapped in item.song instances which also contain the id of the database where the song is stored. - Random song selections are handled. """ def __init__(self): service.service.__init__(self, "songdb manager") # hub for the various song databases self.songdbhub = hub.hub() # list of registered songdbs self.songdbids = [] # result cache containing a mapping hash(request) -> (request, result, lastaccess) self.requestcache = {} # maximal number of objects referred by request cache self.requestcachemaxsize = config.database.requestcachesize # cache use statististics self.requestcachehits = 0 self.requestcachemisses = 0 # current number of objects referred to by items in result cache self.requestcachesize = 0 # we are a database service provider... self.channel.supply(requests.dbrequestsingle, self.dbrequestsingle) self.channel.supply(requests.dbrequestsongs, self.dbrequestsongs) self.channel.supply(requests.dbrequestlist, self.dbrequestlist) self.channel.supply(requests.getdatabasestats, self.getdatabasestats) self.channel.supply(requests.getnumberofsongs, self.getnumberofsongs) self.channel.supply(requests.getnumberofalbums, self.getnumberofalbums) self.channel.supply(requests.getnumberofartists, self.getnumberofartists) self.channel.supply(requests.getnumberoftags, self.getnumberoftags) self.channel.supply(requests.getnumberofratings, self.getnumberofratings) # and need to be informed about database changes self.channel.subscribe(events.dbevent, self.dbevent) # finally, we supply some information about the databases and the cache self.channel.supply(requests.getsongdbmanagerstats, self.getsongdbmanagerstats) def resetafterexception(self): # when an exception occurs, we clear the cache self.requestcache = {} def addsongdb(self, id, config): """ add songdb with id defined by config return id (or None if player is turned off) """ type = config.type if type=="off": return None if type=="local": import songdbs.sqlite songdb = songdbs.sqlite.songdb(id, config, self.songdbhub) elif type=="remote": import songdbs.remote songdb = songdbs.remote.songdb(id, config.networklocation, self.songdbhub) for postprocessor_name in config.postprocessors: try: metadata.get_metadata_postprocessor(postprocessor_name) except: raise RuntimeError("Unkown metadata postprocesor '%s' for database '%r'" % (postprocessor_name, id)) self.songdbids.append(id) songdb.setName("song database thread (id=%s)" % id) songdb.start() if config.autoregisterer: hub.notify(events.autoregistersongs(id)) return id # method decorators for result caching and random song selection def cacheresult(requesthandler): """ method decorator which caches results of the request """ def newrequesthandler(self, request): log.debug("dbrequest cache: query for request: %r" % request) requesthash = hash(request) log.debug("dbrequest cache: sucessfully hashed request: %d" % requesthash) try: # try to get the result from the cache result = self.requestcache[requesthash][0] # update atime self.requestcache[requesthash][2] = time.time() self.requestcachehits += 1 log.debug("dbrequest cache: hit for request: %r" % request) except KeyError: # make a copy of request for later storage in cache requestcopy = copy.copy(request) result = requesthandler(self, request) resultnoobjects = len(gc.get_referents(result)) + 1 self.requestcache[requesthash] = [result, requestcopy, time.time(), resultnoobjects] self.requestcachemisses += 1 self.requestcachesize += resultnoobjects # remove least recently used items from cache if self.requestcachesize > self.requestcachemaxsize: log.debug("dbrequest cache: purging old items") cachebytime = [(item[2], key) for key, item in self.requestcache.items()] cachebytime.sort() for atime, key in cachebytime[-10:]: self.requestcachesize -= self.requestcache[key][3] del self.requestcache[key] log.debug("db request cache miss for request: %r (%d requests and %d objects cached)" % (request, len(self.requestcache), self.requestcachesize)) return result return newrequesthandler def _genrandomchoice(self, songs): """ returns random selection of songs up to the maximal length configured. Note that this method changes as a side-effect the parameter songs""" # consider trivial case separately if not songs: return [] # choose item, avoiding duplicates. Stop after a predefined # total length (in seconds). Take rating of songs/albums/artists # into account length = 0 result = [] # generate an initial random sample of large enough size samplesize # to choose from samplesize = min(100, len(songs)) sample = random.sample(songs, samplesize) currenttime = time.time() # relative percentage of songs accepted with a given rating ratingdistribution = [5, 10, 20, 30, 35] # normalize distribution normfactor = float(sum(ratingdistribution)) ratingdistribution = [x/normfactor for x in ratingdistribution] # scale for rating reduction: for playing times longer # ago than lastplayedscale seconds, the rating is not # influenced. lastplayedscale = 60.0 * 60 * 24 while length < config.general.randominsertlength: for song in sample: # we have to query the song from our databases # since otherwise this is done automatically leading to # a deadlock if song.song_metadata is None: song.song_metadata = self.songdbhub.request(requests.getsong_metadata(song.songdbid, song.id)) # if the song has been deleted in the meantime, we proceed to the next one if song.song_metadata is None: continue if song.rating: rating = song.rating else: # punish skipped songs if they have not been rated rating = min(5, max(1, 3 + max(0, 0.5*(song.playcount - song.skipcount)))) if song.date_lastplayed: # Simple heuristic algorithm to consider song ratings # for random selection. Certainly not optimal! last = max(0, (currenttime-song.date_lastplayed)/60) rating -= 2 * math.exp(-last/lastplayedscale) if rating < 1: rating = 1 if rating == 5: threshold = ratingdistribution[4] else: # get threshold by linear interpolation intpart = int(rating) rest = rating-intpart threshold = (ratingdistribution[intpart-1] + (ratingdistribution[intpart] - ratingdistribution[intpart-1])*rest) if random.random() <= threshold or len(sample) == 1: result.append(song) length += song.length if length >= config.general.randominsertlength or len(result) >= samplesize: return result # recreate sample without already chosen songs, if we ran out of songs sample = [song for song in sample if song not in result] return result def selectrandom(requesthandler): """ method decorator which returns a random selection of the request result if requested Note that the result has to be a list of songs. """ def newrequesthandler(self, request): songs = requesthandler(self, request) if request.random: return self._genrandomchoice(songs) else: return songs return newrequesthandler def sortresult(requesthandler): """ method decorator which sorts the result list if requested """ def newrequesthandler(self, request): result = requesthandler(self, request) # XXX turned off if request.sort and 0: result.sort(request.sort) return result return newrequesthandler # cache update def updatecache(self, event): """ update/clear requestcache when database event sent """ if isinstance(event, (events.checkpointdb, events.autoregistersongs)): return if isinstance(event, events.update_song): oldsong_metadata = self.songdbhub.request(requests.getsong_metadata(event.songdbid, event.song.id)) newsong_metadata = event.song.song_metadata # The following is an optimization for an update_song event which occurs rather often # Not very pretty, but for the moment enough if ( oldsong_metadata.album == newsong_metadata.album and oldsong_metadata.artist == newsong_metadata.artist and oldsong_metadata.tags == newsong_metadata.tags and oldsong_metadata.rating == newsong_metadata.rating ): # only the playing information was changed, so we just # delete the relevant cache results for key, item in self.requestcache.items(): if isinstance(item[1], (requests.getsongs)): del self.requestcache[key] return # otherwise we delete the queries for the correponding database (and all compound queries) log.debug("dbrequest cache: emptying cache for database %r" % event.songdbid) for key, item in self.requestcache.items(): songdbid = item[1].songdbid if songdbid is None or songdbid == event.songdbid: del self.requestcache[key] # event handlers def quit(self, event): service.service.quit(self, event) self.songdbhub.notify(events.quit()) def dbevent(self, event): if event.songdbid not in self.songdbids: log.error("songdbmanager: invalid songdbid '%r' for database event" % event.songdbid) return # first update result cache (to allow the updatecache method # to query the old state of the database) self.updatecache(event) # and then send the event to the database self.songdbhub.notify(event) # request handlers def dbrequestsingle(self, request): if request.songdbid not in self.songdbids: log.error("songdbmanager: invalid songdbid '%r' for database request" % request.songdbid) return return self.songdbhub.request(request) def dbrequestsongs(self, request): # make a copy of the original request, because we will subsequently modify it nrequest = copy.copy(request) # also reset the sort function as otherwise # sending over the network (which requires pickling the # request) fails # XXX we disable this at the moment if request.songdbid is None: nrequest.sort = False resulthash = {} for songdbid in self.songdbids: nrequest.songdbid = songdbid # Query the songs in the database songdbid via dbrequestsongs to cache # the result. # Note that in the case of getlastplayedsongs requests, we cheat # a little bit, since then the result of the database request is a tuple # (playingtime, dbsong). for dbsong in self.dbrequestsongs(nrequest): resulthash[dbsong] = songdbid return resulthash.values() elif request.songdbid not in self.songdbids: log.error("songdbmanager: invalid songdbid '%r' for database request" % request.songdbid) return else: return self.songdbhub.request(nrequest) dbrequestsongs = selectrandom(cacheresult(sortresult(dbrequestsongs))) def dbrequestlist(self, request): # make a copy of the original request, because we will subsequently modify it nrequest = copy.copy(request) if request.songdbid is None: resulthash = {} for songdbid in self.songdbids: nrequest.songdbid = songdbid for result in self.dbrequestlist(nrequest): resulthash[result] = songdbid # sort results return resulthash.keys() elif request.songdbid not in self.songdbids: log.error("songdbmanager: invalid songdbid '%r' for database request" % request.songdbid) else: # use nrequest here instead of request in order to not # send sort to database (this fails when # using a network channel, since we cannot pickle these # objects) return self.songdbhub.request(nrequest) dbrequestlist = cacheresult(dbrequestlist) def getdatabasestats(self, request): if request.songdbid is None: return "Virtual", "" elif request.songdbid not in self.songdbids: log.error("songdbmanager: invalid songdbid '%r' for database request" % request.songdbid) else: return self.songdbhub.request(request) # requests which return number of items of a certain kind def getnumberofsongs(self, request): if request.songdbid is not None and request.songdbid not in self.songdbids: log.error("songdbmanager: invalid songdbid '%r' for database request" % request.songdbid) # XXX use filters in sqlite instead if request.songdbid is not None and request.filters is None: return self.songdbhub.request(request) else: return len(self.dbrequestsongs(requests.getsongs(songdbid=request.songdbid, filters=request.filters))) getnumberofsongs = cacheresult(getnumberofsongs) def _requestnumbers(self, request, listrequest): """ helper method for a request which queries for the number of items. If a database is specified, the corresponding database request is executed directly. Otherwise, the length of the result of listrequest is returned. """ if request.songdbid is not None and request.songdbid not in self.songdbids: log.error("songdbmanager: invalid songdbid '%r' for database request" % request.songdbid) elif request.songdbid is None or request.filters: return len(self.dbrequestlist(listrequest(songdbid=request.songdbid, filters=request.filters))) else: return self.songdbhub.request(request) def getnumberofalbums(self, request): return self._requestnumbers(request, requests.getalbums) getnumberofalbums = cacheresult(getnumberofalbums) def getnumberoftags(self, request): return self._requestnumbers(request, requests.gettags) getnumberoftags = cacheresult(getnumberoftags) def getnumberofartists(self, request): return self._requestnumbers(request, requests.getartists) getnumberofartists = cacheresult(getnumberofartists) def getnumberofratings(self, request): return self._requestnumbers(request, requests.getratings) getnumberofratings = cacheresult(getnumberofratings) def getsongdbmanagerstats(self, request): songdbsstats = [] for songdbid in self.songdbids: songdbsstats.append(self.songdbhub.request(requests.getdatabasestats(songdbid))) return songdbmanagerstats(songdbsstats, self.requestcachesize, self.requestcachemaxsize, len(self.requestcache), self.requestcachehits, self.requestcachemisses) PyTone-3.0.3/src/services/songdbs/000755 000765 000765 00000000000 11406223507 017314 5ustar00ringoringo000000 000000 PyTone-3.0.3/src/services/._timer.py000644 000765 000765 00000000122 10266220576 017565 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/services/timer.py000644 000765 000765 00000004736 10266220576 017367 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import time import events, hub import service class timer(service.service): """ service which sends events at specified times """ def __init__(self): """ constructs timer, which sends its events through eventhub """ service.service.__init__(self, "timer") # each element in alarms is a tuple (alarmtime, event, repeat) self.alarms = [] self.channel.subscribe(events.sendeventat, self.sendeventat) self.channel.subscribe(events.sendeventin, self.sendeventin) def work(self): # TODO we could look for the next event and set the # timeout accordingly self.channel.process(block=True, timeout=0.5) acttime = time.time() for alarmtime, event, repeat in self.alarms: if alarmtime <= acttime: hub.notify(event) self.alarms.remove((alarmtime, event, repeat)) if repeat: self.alarms.append((alarmtime+repeat, event, repeat)) def _sendeventat(self, event, alarmtime, repeat, replace): if replace: for i in range(len(self.alarms)): aalarmtime, aevent, arepeat = self.alarms[i] if aevent is event and arepeat==repeat: self.alarms[i] = (alarmtime, event, repeat) return self.alarms.append((alarmtime, event, repeat)) # event handlers def sendeventat(self, event, alarmtime, repeat=False, replace=False): self._sendeventat(event.event, event.alarmtime, event.repeat, event.replace) def sendeventin(self, event): acttime = time.time() self._sendeventat(event.event, acttime+event.alarmtimediff, event.repeat, event.replace) PyTone-3.0.3/src/services/songdbs/.___init__.py000644 000765 000765 00000000122 10266220576 021643 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/services/songdbs/__init__.py000644 000765 000765 00000000075 10266220576 021435 0ustar00ringoringo000000 000000 # this is required to convert this directory into a package PyTone-3.0.3/src/services/songdbs/._remote.py000644 000765 000765 00000000122 10274140223 021364 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/services/songdbs/remote.py000644 000765 000765 00000005457 10274140223 021167 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002, 2003, 2004 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import copy import events, hub, requests import log import network import service # # songdb class # class songdb(service.service): def __init__(self, id, networklocation, songdbhub): service.service.__init__(self, "remote songdb", hub=songdbhub) self.id = id self.networklocation = networklocation self.remotesongdbid = "main" self.networkchannel = network.clientchannel(self.networklocation) #self.networkchannel.transmit(events.updatesong) #self.networkchannel.transmit(events.updatealbum) #self.networkchannel.transmit(events.updateartist) #self.networkchannel.transmit(events.playlistaddsong) self.networkchannel.start() # we need to be informed about database changes #self.channel.subscribe(events.updatesong, self.updatesong) #self.channel.subscribe(events.updatealbum, self.updatealbum) #self.channel.subscribe(events.updateartist, self.updateartist) #self.channel.subscribe(events.registersongs, self.registersongs) #self.channel.subscribe(events.registerplaylists, self.registerplaylists) # we are a database service provider... self.channel.supply(requests.dbrequest, self.dbrequest) log.info(_("database %s: type remote, location: %s") % (self.id, self.networklocation)) # request handler def dbrequest(self, request): if self.id != request.songdbid: raise hub.DenyRequest log.debug("dispatching %s" % `request`) # we have to copy the request, because another thread may also access it request = copy.copy(request) request.songdbid = self.remotesongdbid result = self.networkchannel.request(request) log.debug("result %s" % `result`) # we change the databasestats accordingly if isinstance(request, requests.getdatabasestats): result.type = "remote" result.location = str(self.networklocation) return result PyTone-3.0.3/src/services/songdbs/sqlite.py000644 000765 000765 00000137037 11254241061 021176 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002, 2003, 2004, 2006, 2007 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os import errno import math import sys import random import time try: import sqlite3 as sqlite except ImportError: from pysqlite2 import dbapi2 as sqlite import events, hub, requests import errors import log import metadata import item import service import encoding create_tables = """ CREATE TABLE artists ( id INTEGER CONSTRAINT pk_artist_id PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE ); CREATE TABLE albums ( id INTEGER CONSTRAINT pk_album_id PRIMARY KEY AUTOINCREMENT, artist_id INTEGER CONSTRAINT fk_albums_artist_id REFERENCES artists(id), name TEXT, UNIQUE (artist_id, name) ); CREATE TABLE tags ( id INTEGER CONSTRAINT pk_tag_id PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE ); CREATE TABLE taggings ( song_id INTEGER CONSTRAINT fk_song_id REFERENCES songs(id), tag_id INTEGER CONSTRAINT fk_tag_id REFERENCES tags(id) ); CREATE TABLE playstats ( song_id INTEGER CONSTRAINT fk_song_id REFERENCES songs(id), date_played TIMESTAMP ); CREATE TABLE playlists ( id INTEGER CONSTRAINT pk_playlist_id PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE ); CREATE TABLE playlistcontents ( playlist_id INTEGER CONSTRAINT fk_playlist_id REFERENCES playlists(id), song_id INTEGER CONSTRAINT fk_song_id REFERENCES songs(id), position INTEGER ); CREATE TABLE songs ( id INTEGER CONSTRAINT pk_song_id PRIMARY KEY AUTOINCREMENT, url TEXT UNIQUE, type TEXT, title TEXT, album_id INTEGER CONSTRAINT fk_song_album_id REFERENCES albums(id), artist_id INTEGER CONSTRAINT fk_song_artist_id REFERENCES artists(id), album_artist_id INTEGER CONSTRAINT fk_song_artist_id REFERENCES artists(id), year INTEGER, comments BLOB, lyrics BLOB, bpm INTEGER, length INTEGER, tracknumber INTEGER, trackcount INTEGER, disknumber INTEGER, diskcount INTEGER, compilation BOOL, bitrate INTEGER, is_vbr BOOL, samplerate INTEGER, replaygain_track_gain FLOAT, replaygain_track_peak FLOAT, replaygain_album_gain FLOAT, replaygain_album_peak FLOAT, size INTEGER, date_added TIMESTAMP, date_updated TIMESTAMP, date_lastplayed TIMESTAMP, playcount INTEGER, skipcount INTEGER, rating INTEGER ); CREATE INDEX album_id ON albums(name); CREATE INDEX artist_id ON artists(name); CREATE INDEX tag_id ON tags(name); CREATE INDEX playlist_id ON playlists(name); CREATE INDEX url_song ON songs(url); CREATE INDEX album_id_song ON songs(album_id); CREATE INDEX artist_id_song ON songs(artist_id); CREATE INDEX year_song ON songs(year); CREATE INDEX compilation_song ON songs(compilation); CREATE INDEX taggings_song_id ON taggings(song_id); CREATE INDEX taggings_tag_id ON taggings(tag_id); CREATE INDEX playlistcontents_song_id ON playlistcontents(song_id); CREATE INDEX playlistcontents_playlist_id ON playlistcontents(playlist_id); """ songcolumns_plain = ["url", "type", "title", "year", "bpm", "length", "tracknumber", "trackcount", "disknumber", "diskcount", "compilation", "bitrate", "is_vbr", "samplerate", "replaygain_track_gain", "replaygain_track_peak", "replaygain_album_gain", "replaygain_album_peak", "size", "compilation", "date_added", "date_updated", "date_lastplayed", "playcount", "skipcount", "rating"] songcolumns_indices = ["album_id", "artist_id", "album_artist_id"] songcolumns_w_indices = songcolumns_plain + songcolumns_indices songcolumns_lists = ["comments", "lyrics"] songcolumns_all = songcolumns_w_indices + songcolumns_lists # secure unpickler which does not accept any instances import cPickle, cStringIO def loads(s): unpickler = cPickle.Unpickler(cStringIO.StringIO(s)) unpickler.find_global = None return unpickler.load() def dumps(obj): return buffer(cPickle.dumps(obj)) # # statistical information about songdb # class songdbstats: def __init__(self, id, type, basedir, location, dbfile, cachesize, numberofsongs, numberofalbums, numberofartists, numberoftags): self.id = id self.type = type self.basedir = basedir self.location = location self.dbfile = dbfile self.cachesize = cachesize self.numberofsongs = numberofsongs self.numberofalbums = numberofalbums self.numberofartists = numberofartists self.numberoftags = numberoftags # # songdb class # class songdb(service.service): currentdbversion = 1 def __init__(self, id, config, songdbhub): service.service.__init__(self, "%r songdb" % id, hub=songdbhub) self.id = id self.basedir = config.musicbasedir self.dbfile = config.dbfile self.cachesize = config.cachesize self.playingstatslength = config.playingstatslength if not os.path.isdir(self.basedir): raise errors.configurationerror("musicbasedir '%r' of database %r is not a directory." % (self.basedir, self.id)) if not os.access(self.basedir, os.X_OK | os.R_OK): raise errors.configurationerror("you are not allowed to access and read config.general.musicbasedir.") # currently active cursor - initially, none self.cur = None # we need to be informed about database changes self.channel.subscribe(events.add_song, self.add_song) self.channel.subscribe(events.update_song, self.update_song) self.channel.subscribe(events.delete_song, self.delete_song) self.channel.subscribe(events.song_played, self.song_played) self.channel.subscribe(events.song_skipped, self.song_skipped) self.channel.subscribe(events.add_playlist, self.add_playlist) self.channel.subscribe(events.update_playlist, self.update_playlist) self.channel.subscribe(events.delete_playlist, self.delete_playlist) self.channel.subscribe(events.clearstats, self.clearstats) # we are a database service provider... self.channel.supply(requests.getdatabasestats, self.getdatabasestats) self.channel.supply(requests.getsong_metadata, self.getsong_metadata) self.channel.supply(requests.getartists, self.getartists) self.channel.supply(requests.getalbums, self.getalbums) self.channel.supply(requests.gettag_id, self.gettag_id) self.channel.supply(requests.getsongs, self.getsongs) self.channel.supply(requests.getnumberofsongs, self.getnumberofsongs) self.channel.supply(requests.getnumberofalbums, self.getnumberofalbums) self.channel.supply(requests.getnumberofartists, self.getnumberofartists) self.channel.supply(requests.getnumberoftags, self.getnumberoftags) self.channel.supply(requests.getnumberofratings, self.getnumberofratings) self.channel.supply(requests.gettags, self.gettags) self.channel.supply(requests.getratings, self.getratings) self.channel.supply(requests.getlastplayedsongs, self.getlastplayedsongs) self.channel.supply(requests.getplaylists, self.getplaylists) self.autoregisterer = songautoregisterer(self.basedir, self.id, self.isbusy, config.tracknrandtitlere, config.postprocessors) self.autoregisterer.start() def run(self): # self.con = sqlite.connect(":memory:") log.debug("dbfile: '%r'" % self.dbfile) self.con = sqlite.connect(self.dbfile) self.con.row_factory = sqlite.Row dbversion = self.con.execute("PRAGMA user_version").fetchone()[0] log.debug("Found on-disk db version: %d" % dbversion) if dbversion == 0: # fresh database self._txn_begin() self.con.executescript(create_tables) self._txn_commit() self.con.execute("PRAGMA user_version=%d" % self.currentdbversion) log.debug("Starting db sevice") service.service.run(self) self.close() def close(self): self.con.close() # transaction machinery def _txn_begin(self): if self.cur: raise RuntimeError("more than one transaction in parallel is not supported") # self.con.execute("BEGIN TRANSACTION") self.cur = self.con.cursor() def _txn_commit(self): # self.con.execute("COMMIT TRANSACTION") self.cur.close() self.con.commit() self.cur = None def _txn_abort(self): # self.con.execute("ROLLBACK") self.con.rollback() self.cur.close() self.cur = None # resetting db stats def _clearstats(self): self._txn_begin() try: self.cur.execute("DELETE FROM playstats") self.cur.execute("UPDATE songs SET playcount = 0, skipcount = 0, date_lastplayed = NULL ") except: self._txn_abort() raise else: self._txn_commit() # # methods for adding, updating and deleting songs # # helper methods def _queryindex(self, table, indexnames, values): " query indexnames in table and return id " newindexentry = False wheres = " AND ".join(["%s = ?" % indexname for indexname in indexnames]) self.cur.execute("SELECT id FROM %s WHERE %s" % (table, wheres), values) r = self.cur.fetchone() return r["id"] def _queryregisterindex(self, table, indexnames, values): " register in table and return if tuple (id, newentry) " newindexentry = False wheres = " AND ".join(["%s = ?" % indexname for indexname in indexnames]) self.cur.execute("SELECT id FROM %s WHERE %s" % (table, wheres), values) r = self.cur.fetchone() if r is None: self.cur.execute("INSERT INTO %s (%s) VALUES (%s)" % (table, ", ".join(indexnames), ", ".join(["?"]*len(indexnames))), values) self.cur.execute("SELECT id FROM %s WHERE %s" % (table, wheres), values) r = self.cur.fetchone() newindexentry = True return r["id"], newindexentry def _checkremoveindex(self, indextable, reftable, indexnames, value): "remove entry from indextable if no longer referenced in reftable and return whether this has happened" if value is None: return False wheres = " OR ".join(["%s = ?" % indexname for indexname in indexnames]) num = self.cur.execute("SELECT count(*) FROM %s WHERE (%s)" % (reftable, wheres), [value]*len(indexnames)).fetchone()[0] if num == 0: self.cur.execute("DELETE FROM %s WHERE id = ?" % indextable, [value]) return True else: return False _song_insert = "INSERT INTO songs (%s) VALUES (%s)" % (",".join(songcolumns_all), ",".join(["?"] * len(songcolumns_all))) def _add_song(self, song): """add song metadata to database""" log.debug("adding song: %r" % song) if not isinstance(song, metadata.song_metadata): log.error("add_song: song has to be a meta.song instance, not a %r instance" % song.__class__) return self._txn_begin() try: # query and register artist, album_artist and album if song.artist: song.artist_id, newartist = self._queryregisterindex("artists", ["name"], [song.artist]) else: song.artist_id, newartist = None, False if song.album_artist: song.album_artist_id, newartist2 = self._queryregisterindex("artists", ["name"], [song.album_artist]) newartist = newartist or newartist2 if song.album: song.album_id, newalbum = self._queryregisterindex("albums", ["artist_id", "name"], [song.album_artist_id, song.album]) else: song.album_id, newalbum = None, False else: song.album_artist_id = None song.album_id = None newalbum = False # pickle the comments and lyrics lists comments = dumps(song.comments) lyrics = dumps(song.lyrics) # register song self.cur.execute(self._song_insert, [getattr(song, columnname) for columnname in songcolumns_w_indices] + [comments, lyrics]) self.cur.execute("SELECT id FROM songs WHERE url = ?", (song.url,)) r = self.cur.fetchone() song_id = r["id"] # register song tags newtag = False for tag in song.tags: tag_id, newtag2 = self._queryregisterindex("tags", ["name"], [tag]) newtag = newtag or newtag2 self.cur.execute("INSERT INTO taggings (song_id, tag_id) VALUES (?, ?)", (song_id, tag_id)) except: self._txn_abort() raise else: self._txn_commit() if newartist: hub.notify(events.artistschanged(self.id)) if newalbum: hub.notify(events.albumschanged(self.id)) if newtag: hub.notify(events.tagschanged(self.id)) # we don't issue a songschanged event because the resulting queries put a too high load # on the database # hub.notify(events.songschanged(self.id)) #for r in cur.execute("SELECT id, name FROM artists"): # log.info("AR: %r %r" % (r["id"], r["name"])) #for r in cur.execute("SELECT id, artist_id, name FROM albums"): # log.info("AL: %r %r %r" % (r["id"], r["artist_id"], r["name"])) #for r in cur.execute("SELECT id, title FROM songs"): # log.info("S: %r %r" % (r["id"], r["title"])) def _delete_song(self, song): """delete song from database""" log.debug("delete song: %r" % song) if not isinstance(song, item.song): log.error("_delete_song: song has to be a item.song instance, not a %r instance" % song.__class__) self._txn_begin() try: # remove song self.cur.execute("DELETE FROM songs WHERE id = ?", [song.id]) # remove corresponding album and artists deletedalbum = self._checkremoveindex("albums", "songs", ["album_id"], song.album_id) deletedartist = self._checkremoveindex("artists", "songs", ["album_artist_id", "artist_id"], song.artist_id) deletedartist |= self._checkremoveindex("artists", "songs", ["album_artist_id", "artist_id"], song.album_artist_id) # query tags in order to be able to delete them (as opposed to album_id, etc., # they are not stored in item.song) tag_ids = [] for r in self.cur.execute("""SELECT DISTINCT tags.id AS tag_id FROM tags JOIN taggings ON (taggings.tag_id =tags.id) WHERE taggings.song_id = ?""", [song.id]): tag_ids.append(r["tag_id"]) # remove taggings deletedtag = False self.cur.execute("DELETE FROM taggings WHERE song_id = ?", [song.id]) for tag_id in tag_ids: deletedtag |= self._checkremoveindex("tags", "taggings", ["tag_id"], tag_id) except: self._txn_abort() raise else: self._txn_commit() if deletedartist: hub.notify(events.artistschanged(self.id)) if deletedalbum: hub.notify(events.albumschanged(self.id)) if deletedtag: hub.notify(events.tagschanged(self.id)) # XXX send event? _song_update = ( "INSERT OR REPLACE INTO songs (id, %s) VALUES (?, %s)" % (",".join(songcolumns_all), ",".join(["?"] * len(songcolumns_all))) ) def _update_song(self, song): """updates entry of song""" log.debug("updating song %r" % song) if not isinstance(song, item.song): log.error("_update_song: song has to be a item.song instance, not a %r instance" % newsong.__class__) return if not song.song_metadata: log.error("_update_song: song doesn't contain song metadata") return oldsong = self._getsong_metadata(song.id) self._txn_begin() try: # query artist, album_artist and album of oldsong (in comparison to the _add_song method, we do not have # to add entries to the indices if oldsong.artist: oldsong.artist_id = self._queryindex("artists", ["name"], [oldsong.artist]) else: oldsong.artist_id = None if oldsong.album_artist: oldsong.album_artist_id = self._queryindex("artists", ["name"], [oldsong.album_artist]) if oldsong.album: oldsong.album_id = self._queryindex("albums", ["artist_id", "name"], [oldsong.album_artist_id, oldsong.album]) else: oldsong.album_id = None else: oldsong.album_artist_id = None oldsong.album_id = None # flags for changes of corresponding tables changedartists = False changedalbums = False changedtags = False # register new artists, album_artists and albums if necessary if oldsong.artist != song.artist: if song.artist: song.artist_id, newartist = self._queryregisterindex("artists", ["name"], [song.artist]) changedartists |= newartist else: song.artist_id = None if oldsong.album_artist != song.album_artist: if song.album_artist: song.album_artist_id, newartist = self._queryregisterindex("artists", ["name"], [song.album_artist]) changedartists |= newartist if song.album: song.album_id, newalbum = self._queryregisterindex("albums", ["artist_id", "name"], [song.album_artist_id, song.album]) changedalbums |= newalbum else: song.album_id = None else: song.album_artist_id = None song.album_id = None elif oldsong.album != song.album and song.album: # only the album name changed song.album_id, newalbum = self._queryregisterindex("albums", ["artist_id", "name"], [song.album_artist_id, song.album]) changedalbums |= newalbum # encode the comments and lyrics lists comments = dumps(song.comments) lyrics = dumps(song.lyrics) # update songs table self.cur.execute(self._song_update, [song.id]+[getattr(song, columnname) for columnname in songcolumns_w_indices] + [comments, lyrics]) # delete old artists, album_artists and albums if necessary # we have to do this after the songs table has been updated, otherwise we # cannot detect whether we have to remove an album/artist or not if oldsong.album != song.album: changedalbums |= self._checkremoveindex("albums", "songs", ["album_id"], song.album_id) if oldsong.artist != song.artist: changedartists |= self._checkremoveindex("artists", "songs", ["album_artist_id", "artist_id"], oldsong.artist_id) if oldsong.album_artist != song.album_artist: changedartists |= self._checkremoveindex("artists", "songs", ["album_artist_id", "artist_id"], oldsong.album_artist_id) # update tag information if necessary if oldsong.tags != song.tags: # check for new tags for tag in song.tags: if tag not in oldsong.tags: tag_id, newtag = self._queryregisterindex("tags", ["name"], [tag]) changedtags |= newtag self.cur.execute("INSERT INTO taggings (song_id, tag_id) VALUES (?, ?)", (song.id, tag_id)) # check for removed tags for tag in oldsong.tags: if tag not in song.tags: tag_id = self._queryregisterindex("tags", ["name"], [tag])[0] self.cur.execute("DELETE FROM taggings WHERE (tag_id = ? AND song_id = ?)", [tag_id, song.id]) changedtags |= self._checkremoveindex("tags", "taggings", ["tag_id"], tag_id) except: self._txn_abort() raise else: self._txn_commit() if changedartists: hub.notify(events.artistschanged(self.id)) if changedalbums: hub.notify(events.albumschanged(self.id)) if changedtags: hub.notify(events.tagschanged(self.id)) hub.notify(events.songchanged(self.id, song)) def _song_played(self, song, date_played): """register playing of song""" log.debug("playing song: %r" % song) if not isinstance(song, item.song): log.error("_update_song: song has to be an item.song instance, not a %r instance" % song.__class__) return self._txn_begin() try: self.cur.execute("INSERT INTO playstats (song_id, date_played) VALUES (?, ?)", [song.id, date_played]) self.cur.execute("UPDATE songs SET playcount = playcount+1, date_lastplayed = ? WHERE id = ?", [date_played, song.id]) song.playcount += 1 song.date_lastplayed = date_played song.dates_played.append(date_played) except: self._txn_abort() raise else: self._txn_commit() hub.notify(events.songchanged(self.id, song)) def _song_skipped(self, song): """register skipping of song""" log.debug("skipping song: %r" % song) if not isinstance(song, item.song): log.error("_update_song: song has to be an item.song instance, not a %r instance" % song.__class__) return self._txn_begin() try: self.cur.execute("UPDATE songs SET skipcount = skipcount+1 WHERE id = ?", [song.id]) song.skipcount += 1 except: self._txn_abort() raise else: self._txn_commit() hub.notify(events.songchanged(self.id, song)) def _add_playlist(self, name, songs): log.debug("adding playlist %r" % name) if not songs: log.error("_add_playlist: cannot add empty playlist") self._txn_begin() try: self.cur.execute("INSERT INTO playlists (name) VALUES (?)", [name]) self.cur.execute("SELECT id FROM playlists WHERE name = ?", [name]) r = self.cur.fetchone() playlist_id = r["id"] for position, song in enumerate(songs): self.cur.execute("INSERT INTO playlistcontents (playlist_id, song_id, position) VALUES (?, ?, ?)", [playlist_id, song.id, position + 1]) except: self._txn_abort() raise else: self._txn_commit() def _delete_playlist(self, playlist): """delete playlist from database""" if not self.playlists.has_key(playlist.id): raise KeyError log.debug("delete playlist: %r" % playlist) self._txn_begin() try: self.playlists.delete(playlist.id, txn=self.cur) hub.notify(events.dbplaylistchanged(self.id, playlist)) except: self._txn_abort() raise else: self._txn_commit() _update_playlist = _add_playlist # read-only methods for accesing the database ########################################################################################## # !!! It is not save to call any of the following methods when a transaction is active !!! ########################################################################################## _song_select = """SELECT %s, artists.name AS artist, albums.name AS album FROM songs LEFT JOIN albums ON albums.id == album_id LEFT JOIN artists ON artists.id == songs.artist_id WHERE songs.id = ? """ % ", ".join([c for c in songcolumns_all if c!="artist_id"]) _song_tags_select = """SELECT tags.name AS name FROM tags JOIN taggings ON taggings.tag_id = tags.id WHERE taggings.song_id = ?""" _song_playstats_select = "SELECT date_played FROM playstats WHERE song_id = ?" def _getsong_metadata(self, song_id): """return song entry with given song_id""" log.debug("Querying song metadata for id=%r" % song_id) try: r = self.con.execute(self._song_select, [song_id]).fetchone() if r: # fetch album artist if r["album_artist_id"] is not None: select = """SELECT name FROM artists WHERE id = ?""" album_artist = self.con.execute(select, (r["album_artist_id"],)).fetchone()["name"] else: album_artist = None # fetch tags tags = [] for tr in self.con.execute(self._song_tags_select, [song_id]): tags.append(tr["name"]) # fetch playstats dates_played = [] for tr in self.con.execute(self._song_playstats_select, [song_id]): dates_played.append(tr["date_played"]) # generate and populate metadata md = metadata.song_metadata() for field in songcolumns_plain: md[field] = r[field] md.album = r["album"] md.artist = r["artist"] md.album_artist = album_artist md.tags = tags md.comments = loads(r["comments"]) md.lyrics = loads(r["lyrics"]) md.dates_played = dates_played return md else: log.debug("Song '%d' not found in database" % song_id) return None except: log.debug_traceback() return None def _gettag_id(self, tag_name): return self.con.execute("SELECT id FROM tags WHERE name = ?", [tag_name]).fetchone()[0] def _getsongs(self, sort=None, filters=None): """ returns songs filtered according to filters""" joinstring = filters and filters.SQL_JOIN_string() or "" wherestring = filters and filters.SQL_WHERE_string() or "" orderstring = sort and sort.SQL_string() or "" args = filters and filters.SQL_args() or [] select = """SELECT DISTINCT songs.id AS song_id, songs.album_id AS album_id, songs.artist_id AS artist_id, songs.album_artist_id AS album_artist_id FROM songs LEFT JOIN artists ON (songs.artist_id = artists.id) LEFT JOIN albums ON (songs.album_id = albums.id) %s %s %s """ % (joinstring, wherestring, orderstring) # log.debug(select) return [item.song(self.id, row["song_id"], row["album_id"], row["artist_id"], row["album_artist_id"]) for row in self.con.execute(select, args)] def _getartists(self, filters=None): """return artists filtered according to filters""" joinstring = filters and filters.SQL_JOIN_string() or "" wherestring = filters and filters.SQL_WHERE_string() or "" args = filters and filters.SQL_args() or [] select = """SELECT DISTINCT artists.id AS artist_id, artists.name AS artist_name FROM artists JOIN songs ON (songs.artist_id = artists.id) LEFT JOIN albums ON (album_id = albums.id) %s %s ORDER BY artists.name COLLATE NOCASE""" % (joinstring, wherestring) # log.debug(select) return [item.artist(self.id, row["artist_id"], row["artist_name"], filters) for row in self.con.execute(select, args)] def _getalbums(self, filters=None): """return albums filtered according to filters""" joinstring = filters and filters.SQL_JOIN_string() or "" wherestring = filters and filters.SQL_WHERE_string() or "" args = filters and filters.SQL_args() or [] # Hackish, but effective to allow collections show up in artists view if filters.contains(item.artistfilter): artist_id_column = "artist_id" else: artist_id_column = "album_artist_id" select ="""SELECT DISTINCT albums.id AS album_id, artists.name AS artist_name, albums.name AS album_name FROM albums JOIN artists ON (songs.%s = artists.id) JOIN songs ON (songs.album_id = albums.id) %s %s ORDER BY albums.name COLLATE NOCASE""" % (artist_id_column, joinstring, wherestring) # log.debug(select) return [item.album(self.id, row["album_id"], row["artist_name"], row["album_name"], filters) for row in self.con.execute(select, args)] def _gettags(self, filters=None): """return tags filtered according to filters""" joinstring = filters and filters.SQL_JOIN_string() or "" wherestring = filters and filters.SQL_WHERE_string() or "" args = filters and filters.SQL_args() or [] select ="""SELECT DISTINCT tags.id AS tag_id, tags.name AS tag_name FROM tags JOIN taggings ON (taggings.tag_id = tags.id) JOIN songs ON (songs.id = taggings.song_id) %s %s ORDER BY tags.name COLLATE NOCASE""" % (joinstring, wherestring) # log.debug(select) return [item.tag(self.id, row["tag_id"], row["tag_name"], filters) for row in self.con.execute(select, args)] def _getratings(self, filters): """return all stored ratings""" return [] def _getlastplayedsongs(self, sort=None, filters=None): """return the last played songs""" joinstring = filters and filters.SQL_JOIN_string() or "" wherestring = filters and filters.SQL_WHERE_string() or "" orderstring = sort and sort.SQL_string() or "" args = filters and filters.SQL_args() or [] select = """SELECT DISTINCT songs.id AS song_id, songs.album_id AS album_id, songs.artist_id AS artist_id, songs.album_artist_id AS album_artist_id, playstats.date_played AS date_played FROM songs LEFT JOIN artists ON (songs.artist_id = artists.id) LEFT JOIN albums ON (songs.album_id = albums.id) JOIN playstats ON (songs.id = playstats.song_id) %s %s %s """ % (joinstring, wherestring, orderstring) # log.debug(select) return [item.song(self.id, row["song_id"], row["album_id"], row["artist_id"], row["album_artist_id"], row["date_played"]) for row in self.con.execute(select, args)] def _getplaylists(self, filters=None): joinstring = filters and filters.SQL_JOIN_string() or "" wherestring = filters and filters.SQL_WHERE_string() or "" args = filters and filters.SQL_args() or [] select ="""SELECT DISTINCT playlists.id AS playlist_id, playlists.name AS playlist_name FROM playlists JOIN playlistcontents ON (playlistcontents.playlist_id = playlists.id) JOIN songs ON (songs.id = playlistcontents.song_id) %s %s ORDER BY playlists.name COLLATE NOCASE""" % (joinstring, wherestring) # JOIN taggings ON (taggings.tag_id = tags.id) # log.debug(select) return [item.playlist(self.id, row["playlist_id"], row["playlist_name"], filters) for row in self.con.execute(select, args)] def isbusy(self): """ check whether db is currently busy """ return self.cur is not None or self.channel.queue.qsize()>0 # event handlers def add_song(self, event): if event.songdbid == self.id: try: self._add_song(event.song) except KeyError: log.debug_traceback() pass def update_song(self, event): if event.songdbid == self.id: try: self._update_song(event.song) except: log.debug_traceback() pass def delete_song(self, event): if event.songdbid == self.id: try: self._delete_song(event.song) except: log.debug_traceback() pass def song_played(self, event): if event.songdbid == self.id: try: self._song_played(event.song, event.date_played) except KeyError: pass def song_skipped(self, event): if event.songdbid == self.id: try: self._song_skipped(event.song) except KeyError: pass def add_playlist(self, event): if event.songdbid == self.id: try: self._add_playlist(event.name, event.songs) except: pass def delete_playlist(self, event): if event.songdbid == self.id: try: self._delete_playlist(event.name) except KeyError: pass def update_playlist(self, event): if event.songdbid == self.id: try: self._update_playlist(event.name, event.songs) except KeyError: pass def clearstats(self, event): if event.songdbid == self.id: self._clearstats() # request handlers def getdatabasestats(self, request): if self.id != request.songdbid: raise hub.DenyRequest return songdbstats(self.id, "local", self.basedir, None, self.dbfile, self.cachesize, self.getnumberofsongs(request), self.getnumberofalbums(request), self.getnumberofartists(request), self.getnumberoftags(request)) def getnumberofsongs(self, request): if self.id != request.songdbid: raise hub.DenyRequest return self.con.execute("SELECT count(*) FROM songs").fetchone()[0] def getnumberoftags(self, request): if self.id != request.songdbid: raise hub.DenyRequest return self.con.execute("SELECT count(*) FROM tags").fetchone()[0] def getnumberofratings(self, request): if self.id != request.songdbid: raise hub.DenyRequest return 0 def getnumberofalbums(self, request): if self.id != request.songdbid: raise hub.DenyRequest return self.con.execute("SELECT count(*) FROM albums").fetchone()[0] def getnumberofartists(self, request): if self.id != request.songdbid: raise hub.DenyRequest return self.con.execute("SELECT count(*) FROM artists").fetchone()[0] def getsong_metadata(self, request): if self.id != request.songdbid: raise hub.DenyRequest try: return self._getsong_metadata(song_id=request.song_id) except KeyError: return None def getsongs(self, request): if self.id != request.songdbid: raise hub.DenyRequest try: return self._getsongs(request.sort, request.filters) except (KeyError, AttributeError, TypeError): log.debug_traceback() return [] def getartists(self, request): if self.id != request.songdbid: raise hub.DenyRequest try: return self._getartists(request.filters) except KeyError: log.debug_traceback() return [] def getalbums(self, request): if self.id != request.songdbid: raise hub.DenyRequest try: return self._getalbums(request.filters) except KeyError: log.debug_traceback() return [] def gettag_id(self, request): if self.id != request.songdbid: raise hub.DenyRequest try: return self._gettag_id(request.tag_name) except: log.debug_traceback() return None def gettags(self, request): if self.id != request.songdbid: raise hub.DenyRequest return self._gettags(request.filters) def getplaylists(self, request): if self.id != request.songdbid: raise hub.DenyRequest return self._getplaylists(request.filters) def getratings(self, request): if self.id != request.songdbid: raise hub.DenyRequest return self._getratings(request.filters) def getlastplayedsongs(self, request): if self.id != request.songdbid: raise hub.DenyRequest return self._getlastplayedsongs(request.sort, request.filters) # # thread for automatic registering and rescanning of songs in database # class songautoregisterer(service.service): def __init__(self, basedir, songdbid, dbbusymethod, tracknrandtitlere, postprocessors): service.service.__init__(self, "songautoregisterer", daemonize=True) self.basedir = basedir self.songdbid = songdbid self.dbbusymethod = dbbusymethod self.tracknrandtitlere = tracknrandtitlere self.postprocessors = postprocessors self.done = False # support file extensions self.supportedextensions = metadata.getextensions() self.channel.subscribe(events.autoregistersongs, self.autoregistersongs) self.channel.subscribe(events.autoregisterer_rescansongs, self.autoregisterer_rescansongs) self.channel.supply(requests.autoregisterer_queryregistersong, self.autoregisterer_queryregistersong) def _notify(self, event): """ wait until db is not busy and send event """ while self.dbbusymethod(): time.sleep(0.1) hub.notify(event, -100) def _request(self, request): """ wait until db is not busy and send event """ while self.dbbusymethod(): time.sleep(0.1) return hub.request(request, -100) def _registerorupdatesong(self, path, force): """ register or update song in database and return it If force is set, the mtime of the song file is ignored. """ if not path.startswith(self.basedir): log.error("Path of song not in basedir of database") return None # generate url corresponding to song if self.basedir.endswith("/"): relpath = path[len(self.basedir):] else: relpath = path[len(self.basedir)+1:] song_url = u"file://" + encoding.decode_path(relpath) urlfilter = item.filters((item.urlfilter(song_url),)) songs = self._request(requests.getsongs(self.songdbid, filters=urlfilter)) if songs: # there is exactly one resulting song song = songs[0] song.song_metadata = self._request(requests.getsong_metadata(self.songdbid, song.id)) try: if force or song.song_metadata.date_updated < os.stat(path).st_mtime: # the song has changed since the last update newsong_metadata = metadata.metadata_from_file(relpath, self.basedir, self.tracknrandtitlere, self.postprocessors) song.song_metadata.update(newsong_metadata) self._notify(events.update_song(self.songdbid, song)) log.debug("registerer: song '%r' rescanned" % song_url) else: log.debug("registerer: not scanning unchanged song '%r'" % song_url) except (IOError, OSError, RuntimeError): log.debug("registerer: song '%r' can no longer be read. deleting it from db" % song_url) self._notify(events.delete_song(self.songdbid, song)) else: # song was not stored in database newsong_metadata = metadata.metadata_from_file(relpath, self.basedir, self.tracknrandtitlere, self.postprocessors) self._notify(events.add_song(self.songdbid, newsong_metadata)) # fetch new song from database song = self._request(requests.getsongs(self.songdbid, filters=urlfilter))[0] return song def registerdirtree(self, dir, oldsongs, force): """ scan for songs in dir and its subdirectories, removing those scanned from the set oldsongs. If force is set, the m_time of a song is ignored and the song is always scanned. """ log.debug("registerer: entering %r"% dir) self.channel.process() if self.done: return songpaths = [] # scan for paths of songs and recursively call registering of subdirectories for name in os.listdir(dir): path = os.path.join(dir, name) extension = os.path.splitext(path)[1].lower() if os.access(path, os.R_OK): if os.path.isdir(path): try: self.registerdirtree(path, oldsongs, force) except (IOError, OSError), e: log.warning("songautoregisterer: could not enter dir %r: %r" % (path, e)) elif extension in self.supportedextensions: songpaths.append(path) # now register songs... songs = [] for path in songpaths: try: song = self._registerorupdatesong(path, force) # remove song from list of songs to be checked (if present) oldsongs.discard(song) except (IOError, OSError): # if the registering or update failed we do nothing and the song # will be deleted from the database later on pass except: # but in case of non-IO exceptions report them in debugging mode log.debug_traceback() log.debug("registerer: leaving %r"% dir) def run(self): # wait a little bit to not disturb the startup too much time.sleep(2) service.service.run(self) def rescansong(self, song, force): if song.songdbid != self.songdbid: log.debug("Trying to rescan song in wrong database") return if song.song_metadata is None: song.song_metadata = self._request(requests.getsong_metadata(self.songdbid, song.id)) if song.song_metadata is None: log.debug("Song not found in database") return if not song.url.startswith("file://"): log.debug("Can only rescan local files") return relpath = encoding.encode_path(song.url[7:]) path = os.path.join(self.basedir, relpath) try: if force or song_metadata.date_updated < os.stat(path).st_mtime: newsong_metadata = metadata.metadata_from_file(relpath, self.basedir, self.tracknrandtitlere, self.postprocessors) song.song_metadata.update(newsong_metadata) self._notify(events.update_song(self.songdbid, song)) except (IOError, OSError): log.debug_traceback() # if anything goes wrong, we delete the song from the database self._notify(events.delete_song(self.songdbid, song)) # # event handler # def autoregistersongs(self, event): if self.songdbid == event.songdbid: oldsongs = set(hub.request(requests.getsongs(self.songdbid))) log.info(_("database %r: scanning for songs in %r (currently %d songs registered)") % (self.songdbid, self.basedir, len(oldsongs))) # scan for all songs in the filesystem log.debug("database %r: searching for new songs" % self.songdbid) self.registerdirtree(self.basedir, oldsongs, event.force) # remove songs which have not yet been scanned and thus are not accesible anymore log.info(_("database %r: removing %d stale songs") % (self.songdbid, len(oldsongs))) for song in oldsongs: self._notify(events.delete_song(self.songdbid, song)) nrsongs = hub.request(requests.getnumberofsongs(self.songdbid)) log.info(_("database %r: rescan finished (%d songs registered)") % (self.songdbid, nrsongs)) def autoregisterer_rescansongs(self, event): if self.songdbid == event.songdbid: log.info(_("database %r: rescanning %d songs%s") % (self.songdbid, len(event.songs), event.force and _(" (full)") or "")) for song in event.songs: self.rescansong(song, event.force) log.info(_("database %r: finished rescanning %d songs") % (self.songdbid, len(event.songs))) def autoregisterer_queryregistersong(self, request): if self.songdbid != request.songdbid: raise hub.DenyRequest try: return self._registerorupdatesong(request.path, force=False) except: return None PyTone-3.0.3/src/services/players/.___init__.py000644 000765 000765 00000000122 10266220576 021663 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/services/players/__init__.py000644 000765 000765 00000000075 10266220576 021455 0ustar00ringoringo000000 000000 # this is required to convert this directory into a package PyTone-3.0.3/src/services/players/internal.py000644 000765 000765 00000041015 11264375066 021535 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002, 2003, 2004 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import Queue import sys import threading import time import hub, events import pcm import decoder from services.player import genericplayer import services.playlist import log try: import bufferedao import thread bufferedao_present = True except ImportError: bufferedao_present = False try: import ao ao_present = True except ImportError: ao_present = False try: import ossaudiodev ossaudiodev_present = False except ImportError: ossaudiodev_present = False class aoaudiodev: def __init__(self, aodevice, rate, options): self.ao = ao.AudioDevice(aodevice, rate=rate, byte_format=4, options=options) def play(self, buff, bytes): self.ao.play(buff, bytes) def close(self): # to close the ao audio device, we have to delete it... del self.ao # support for new ossaudiodev module of Python 2.3 class ossaudiodev: def __init__(self, device, rate): self.ossdevice = ossaudiodev.open(device, "w") if sys.byteorder == 'little': self.ossdevice.setfmt(ossaudiodev.AFMT_S16_LE) else: self.ossdevice.setfmt(ossaudiodev.AFMT_S16_BE) self.ossdevice.channels(2) self.ossdevice.speed(rate) def play(self, buff, bytes): self.ossdevice.write(buff) def close(self): self.ossdevice.close() class bufferedaudiodev(threading.Thread): def __init__(self, aodevice, aooptions, bufsize, rate, SIZE): self.aodevice = aodevice self.aooptions = aooptions self.rate = rate self.SIZE = SIZE # initially, we do not open the audio device self.audiodev = None # output queue queuesize = 1024*bufsize/self.SIZE + 1 self.queue = Queue.Queue(queuesize) self.done = False # wait if player thread is paused self.ispaused = False self.restart = threading.Event() threading.Thread.__init__(self) self.setDaemon(True) def opendevice(self): errorlogged = False while self.audiodev is None: try: if self.aodevice=="oss": if ossaudiodev_present: self.audiodev = ossaudiodev(self.aooptions["dsp"], self.rate) log.debug("ossaudiodev audio device opened") else: self.audiodev = aoaudiodev(self.aodevice, rate=self.rate, options=self.aooptions) log.debug("ao audio device opened") else: self.audiodev = aoaudiodev(self.aodevice, rate=self.rate, options=self.aooptions) log.debug("ao audio device opened") except Exception, e: if not errorlogged: log.debug_traceback() log.error(_('cannot open audio device: error "%s"') % e) errorlogged = True time.sleep(1) def closedevice(self): if self.audiodev is not None: # we use self.audiodev = None as a marker for a closed audio device # To avoid race conditions we thus first have to mark the device as # closed before really closing it. Note that when we try to reopen the device # later, and it has not yet been closed, the opendevice method will retry # this openaudiodev = self.audiodev self.audiodev = None openaudiodev.close() log.debug("audio device closed") def queuelen(self): """ return length of currently buffered PCM data in seconds""" return 1.0/4.0*self.queue.qsize()*self.SIZE/self.rate def run(self): while not self.done: try: if self.ispaused: self.restart.wait() self.restart.clear() buff, bytes = self.queue.get(1) if buff != 0 and bytes != 0: audiodev = self.audiodev while audiodev is None: self.opendevice() audiodev = self.audiodev audiodev.play(buff, bytes) except: log.warning("exception occured in bufferedaudiodev") log.debug_traceback() def play(self, buff, bytes): self.queue.put((buff, bytes)) def flush(self): while True: try: self.queue.get(0) except Queue.Empty: break def pause(self): self.ispaused = True self.closedevice() def unpause(self): if self.ispaused: self.ispaused = False self.restart.set() def quit(self): self.done = True self.flush() self.closedevice() self.restart.set() # # wrapper class for playlistitems or songs # class decodedsong: def __init__(self, playlistitemorsong, rate, profiles): if isinstance(playlistitemorsong, services.playlist.playlistitem): self.song = playlistitemorsong.song self.playlistitem = playlistitemorsong else: self.song = playlistitemorsong self.playlistitem = None self.decodedsong = decoder.decodedsong(self.song, rate) self.replaygain = self.calculate_replaygain(["track"]) # these method are handled by the decodedsong self.seekrelative = self.decodedsong.seekrelative self.playfaster = self.decodedsong.playfaster self.playslower = self.decodedsong.playslower self.resetplayspeed = self.decodedsong.resetplayspeed def __repr__(self): return "decodedsong(%r)" % repr(self.song) def read(self, size): # read decoded pcm stram and adjust for replaygain if necessary buff = self.decodedsong.read(size) if self.replaygain != 1: pcm.scale(buff, self.replaygain) return buff def succeedsonalbum(self, otherdecodedsong): " checks whether otherdeocedsong follows self on the same album " return (self.song.artist and self.song.artist == otherdecodedsong.song.artist and self.song.album and self.song.album == otherdecodedsong.song.album and self.song.tracknumber and otherdecodedsong.song.tracknumber and self.song.tracknumber == otherdecodedsong.song.tracknumber-1 ) def calculate_replaygain(self, profiles): # the following code is adapted from quodlibet """Return the recommended Replay Gain scale factor. profiles is a list of Replay Gain profile names ('album', 'track') to try before giving up. The special profile name 'none' will cause no scaling to occur. """ for profile in profiles: if profile is "none": return 1.0 try: db = getattr(self.song, "replaygain_%s_gain" % profile) peak = getattr(self.song, "replaygain_%s_peak" % profile) except AttributeError: continue else: if db is not None and peak is not None: scale = 10.**(db / 20) if scale * peak > 1: scale = 1.0 / peak # don't clip return min(15, scale) else: return 1.0 def rtime(self): " remaing playing time " return self.decodedsong.ttime - self.decodedsong.ptime def ptime(self): " playing time " return self.decodedsong.ptime class player(genericplayer): def __init__(self, id, playlistid, autoplay, aodevice, aooptions, bufsize, crossfading, crossfadingstart, crossfadingduration): self.rate = 44100 self.SIZE = 4096 self.volume = 1 self._volume_scale = 0.005 # factor for logarthmic volume change # use C version of buffered audio device if present if bufferedao_present: self.audiodev = bufferedao.bufferedao(bufsize, self.SIZE, aodevice, byte_format=4, rate=self.rate, options=aooptions) # we have to start a new thread for the bufferedao device thread.start_new(self.audiodev.start, ()) log.debug("bufferedao device opened") else: # create audio device thread self.audiodev = bufferedaudiodev(aodevice, aooptions, bufsize, self.rate, self.SIZE) self.audiodev.start() # songs currently playing self.decodedsongs = [] self.crossfading = crossfading if self.crossfading: # crossfading parameters (all values in seconds and change/second, resp.) self.crossfadingduration = crossfadingduration self.crossfadingstart = crossfadingstart self.crossfadingratio = 0 self.crossfadingrate = 1.0/self.rate/self.crossfadingduration # self.songtransitionmode determines the behaviour on transitions between two songs # possible values are "crossfade", "gapkill" and None (do nothing special) self.songtransitionmode = None genericplayer.__init__(self, id, playlistid, autoplay) def _flushqueue(self): """ delete internal player queue and flush audiodevice """ self.decodedsongs = [] self.audiodev.flush() self.audiodev.closedevice() def play(self): """decode songs and mix them together""" # unpause buffered ao if necessary self.audiodev.unpause() if len(self.decodedsongs) == 1: song = self.decodedsongs[0] buff = song.read(self.SIZE) if len(buff) > 0: if self.volume != 1: pcm.scale(buff, self._volume_scale**(1-self.volume)) self.audiodev.play(buff, len(buff)) else: log.debug("internal player: song ends: %r (0 songs in queue)" % self.decodedsongs[0]) del self.decodedsongs[0] # reset songtransition mode, but before possibly requesting a new song self.songtransitionmode = None if len(buff) == 0 or (self.crossfading and song.rtime() < self.crossfadingstart): self.requestnextsong() elif len(self.decodedsongs) == 2: if self.songtransitionmode == "crossfade": # perform crossfading buff1 = self.decodedsongs[0].read(self.SIZE) buff2 = self.decodedsongs[1].read(self.SIZE) if len(buff1) and len(buff2): # normal operation: no song has ended buff, self.crossfadingratio = pcm.mix(buff1, buff2, self.crossfadingratio, self.crossfadingrate) if self.crossfadingratio >= 1: self.crossfadingratio = 0 log.debug("internal player: song ends: %r (1 song in queue)" % self.decodedsongs[0]) del self.decodedsongs[0] if len(buff1) == 0: buff = buff2 self.crossfadingratio = 0 log.debug("internal player: song ends: %r (1 song in queue)" % self.decodedsongs[0]) del self.decodedsongs[0] if len(buff2) == 0: buff = buff1 self.crossfadingratio = 0 log.debug("internal player: song ends: %r" % self.decodedsongs[-1]) del self.decodedsongs[-1] log.debug("internal player: %d songs in queue" % len(self.decodedsongs)) elif self.songtransitionmode == "gapkill": # just kill gap between songs buff = self.decodedsongs[0].read(self.SIZE) if len(buff) < self.SIZE: log.debug("internal player: song ends: %r (1 song in queue)" % self.decodedsongs[0]) del self.decodedsongs[0] buff2 = self.decodedsongs[0].read(self.SIZE) if len(buff2) == 0: log.debug("internal player: song ends: %r" % self.decodedsongs[0]) del self.decodedsongs[0] log.debug("internal player: %d songs in queue" % len(self.decodedsongs)) else: buff = buff + buff2 else: # neither crossfading nor gap killing del self.decodedsongs[0] buff = self.decodedsongs[0].read(self.SIZE) if len(buff) > 0: if self.volume != 1: pcm.scale(buff, self._volume_scale**(1-self.volume)) self.audiodev.play(buff, len(buff)) # update playbackinfo if len(self.decodedsongs) > 0: # determine which song is currently played # try to take the buffer length into account if self.songtransitionmode == "crossfade" and self.decodedsongs[-1].ptime()-self.audiodev.queuelen() >= 0: playingsong = self.decodedsongs[-1] self.playbackinfo.updatecrossfade(1) else: playingsong = self.decodedsongs[0] self.playbackinfo.updatecrossfade(0) time = int(max(playingsong.ptime()-self.audiodev.queuelen(), 0)) self.playbackinfo.updatesong(playingsong.song) self.playbackinfo.updatetime(time) else: self.playbackinfo.stopped() def _playsong(self, song, manual): log.debug("internal player: new song: %r" % song) if self.ispaused(): self._flushqueue() # we want maximally 2 songs in queue if len(self.decodedsongs) == 2: del self.decodedsongs[0] try: self.decodedsongs.append(decodedsong(song, self.rate, ["track"])) if self.crossfading: self.songtransitionmode = "crossfade" # Check whether two songs come after each other on an # album In such a case, we don't want to crossfade. If, # however, the user has requested the song change (via # playerforward), we do want crossfading. if not manual and len(self.decodedsongs) == 2: if self.decodedsongs[0].succeedsonalbum(self.decodedsongs[1]): self.songtransitionmode = "gapkill" log.debug("internal player: don't crossfade successive songs.") except (IOError, RuntimeError): log.warning(_('failed to open song "%r"') % str(song.url)) log.debug("internal player: %d songs in queue" % len(self.decodedsongs)) def _playerpause(self): self.audiodev.pause() def _playerstop(self): self._flushqueue() def _playerseekrelative(self, seconds): if len(self.decodedsongs) == 0: return if len(self.decodedsongs) == 2: # we refuse to seek backward during crossfading if seconds < 0: return del self.decodedsongs[0] song = self.decodedsongs[0] song.seekrelative(seconds) self.audiodev.flush() def _player_change_volume_relative(self, volume_adj): self.volume = max(0, min(1, self.volume + volume_adj/100.0)) hub.notify(events.player_volume_changed(self.id, self.volume)) def _playerplayfaster(self): if self.decodedsongs: self.decodedsongs[0].playfaster() def _playerplayslower(self): if self.decodedsongs: self.decodedsongs[0].playslower() def _playerspeedreset(self): if self.decodedsongs: self.decodedsongs[0].resetplayspeed() def _playerreleasedevice(self): self.audiodev.closedevice() def _playerquit(self): self.audiodev.quit() PyTone-3.0.3/src/services/players/._mpg123.py000644 000765 000765 00000000122 11051512065 021123 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/services/players/mpg123.py000644 000765 000765 00000012120 11051512065 020707 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os import os.path import fcntl import re import string import time import log import services.playlist import hub,requests from services.player import genericplayer def makeNonBlocking(fd): fl = fcntl.fcntl(fd, fcntl.F_GETFL) fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) startpattern = re.compile(r"^@R MPG123 *[-0-9a-zA-Z\s_]*$") class player(genericplayer): def __init__(self, id, playlistid, autoplay, cmdline): self.cmdline = cmdline self.initmpg123() genericplayer.__init__(self, id, playlistid, autoplay) def initmpg123(self): """start new mpg123 process""" self.pstdin, self.pstdout = os.popen4(self.cmdline + " -R -") startline = self.pstdout.readline() if not re.match(startpattern, startline): raise RuntimeError("cannot initialize player") makeNonBlocking(self.pstdout.fileno()) def closempg123(self): """terminate running mpg123 process""" if self.pstdin: self.pstdin.close() if self.pstdout: self.pstdout.close() def sendmpg123(self, command): """send command to mpg123 process""" try: self.pstdin.write("%s\n" % command) self.pstdin.flush() except IOError, error: # broken pipe => restart player if error[0]==32: self.closempg123() self.initmpg123() self.playbackinfo.stopped() self.pstdin.write("%s\n" % command) else: raise def receivempg123(self): """receive command from mpg123 process""" try: return self.pstdout.readline() except (ValueError, IOError): return "" def play(self): """play songs""" r = self.receivempg123() if r=="": time.sleep(0.1) # we just want to tease mpg123 a bit to check if it is still # alive self.sendmpg123("") elif r.startswith("@F"): pframes, lframes, ptime, ltime = string.split(r[3:])[:4] ptime = int(float(ptime)) self.playbackinfo.updatetime(ptime) elif r.startswith("@P"): if self.playbackinfo.isplaying() and r[3]=="0": self.playbackinfo.stopped() self.requestnextsong() elif r.startswith("@S"): ( version, layer, samplerate, mode, modeextension, bytesperframe, channels, copyrighted, crcprotected, emphasis, bitrate ) = string.split(r[3:])[:11] self.framespersecond = 1000.0 / 8 * int(bitrate) / int(bytesperframe) def _playsong(self, playlistitemorsong, manual): """play event.song next""" path = None if isinstance( playlistitemorsong, services.playlist.playlistitem ): url = playlistitemorsong.song.url if url.startswith("file://"): dbstats = hub.request(requests.getdatabasestats(playlistitemorsong.song.songdbid)) path = os.path.join(dbstats.basedir, url[7:]) else: path = url else: log.warning("mpg123 player: song %s not a playlistitem. Not added!" % repr( playlistitemorsong) ) return self.sendmpg123("L %s" % path) self.framespersecond = None self.playbackinfo.updatesong(song) def _playerunpause(self): """unpause playing""" self.sendmpg123("P") # delete messages coming from mpg123 time.sleep(0.1) while self.receivempg123()!="": pass self.playbackinfo.playing() def _playerpause(self): """pause playing""" self.sendmpg123("P") # delete messages coming from mpg123 time.sleep(0.1) while self.receivempg123()!="": pass self.playbackinfo.paused() def _playerseekrelative(self, seconds): if self.framespersecond: self.sendmpg123("J %+d" % int(seconds * self.framespersecond)) def _playerstop(self): """stop playing""" self.sendmpg123("S") # delete messages coming from mpg123 time.sleep(0.1) while self.receivempg123()!="": pass self.playbackinfo.stopped() def _playerquit(self): self.sendmpg123("Q") self.closempg123() PyTone-3.0.3/src/services/players/._remote.py000644 000765 000765 00000000122 11051512075 021406 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/services/players/remote.py000644 000765 000765 00000005210 11051512075 021174 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2003, 2004 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import copy, service import hub, events, requests import network class player(service.service): def __init__(self, id, playlistid, networklocation): service.service.__init__(self, "remote player") self.id = id self.playlistid = playlistid self.networklocation = networklocation self.remoteplayerid = "main" self.networkchannel = network.clientchannel(self.networklocation) #self.networkchannel.transmit(events.updatesong) #self.networkchannel.transmit(events.updatealbum) #self.networkchannel.transmit(events.updateartist) #self.networkchannel.transmit(events.playlistaddsong) self.networkchannel.subscribe(events.playbackinfochanged, self.playbackinfochanged) # for playlists self.networkchannel.subscribe(events.playlistchanged, self.playlistchanged) self.networkchannel.start() # provide player service self.channel.subscribe(events.playerevent, self.playerevent) # we also provide a playlist service self.channel.subscribe(events.playlistevent, self.playlistevent) self.channel.supply(requests.playlistgetcontents, self.playlistgetcontents) # event handler def playbackinfochanged(self, event): # this is called by the networkchannel, so it should be (and is) thread safe event.playerid = self.id hub.notify(event) def playerevent(self, event): # we have to copy the event, because another thread may also access it event = copy.copy(event) event.playerid = self.remoteplayerid self.networkchannel.notify(event) def playlistevent(self, event): self.networkchannel.notify(event) def playlistchanged(self, event): hub.notify(event) # request handler def playlistgetcontents(self, request): return self.networkchannel.request(request) PyTone-3.0.3/src/services/players/._xmmsplayer.py000644 000765 000765 00000000122 11051512106 022307 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/services/players/xmmsplayer.py000644 000765 000765 00000011475 11051512106 022107 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- # Copyright (C) 2002 Jörg Lehmann # # This file is part of PyTone (http://www.luga.de/pytone/) # # PyTone is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # PyTone is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PyTone; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os import time import xmms.control from services.player import genericplayer # Note: the implementation xmms player is a bit hackish, # so don't look too close. At many places, sleeps have # been interted to get it working, so as I said... class player(genericplayer): def __init__(self, id, playlistid, autoplay, session=0, noqueue=0): self.session = session self.noqueue = noqueue # a mapping path -> song self.songs = {} self.initxmms() genericplayer.__init__(self, id, playlistid, autoplay) def initxmms(self): if not xmms.control.is_running(self.session): abs_prog_name = xmms.control._find_and_check_executable("xmms") if not abs_prog_name: raise xmms.control.ExecutableNotFound("can't find XMMS executable") os.system(abs_prog_name + " >/dev/null 2>/dev/null &") while not xmms.control.is_running(self.session): time.sleep(0.2) xmms.control.playlist_clear(self.session) xmms.control.main_win_toggle(0, self.session) xmms.control.pl_win_toggle(0, self.session) xmms.control.eq_win_toggle(0, self.session) # event handler def play(self): if xmms.control.is_playing(self.session): pos = xmms.control.get_playlist_pos(self.session) # title = xmms.control.get_playlist_title(pos, self.session) # ttime = xmms.control.get_playlist_time(pos, self.session) # ptime = xmms.control.get_output_time(self.session) # self.playbackinfo = (title, int(ptime/1000), int(ttime/1000)) path = xmms.control.get_playlist_file(pos, self.session) song = self.songs[path] ptime = xmms.control.get_output_time(self.session)/1000 self.playbackinfo.updatesong(song) self.playbackinfo.updatetime(ptime) # fill up xmms playlist if necessary if song.length-ptime<20 and xmms.control.get_playlist_length()<2: self.requestnextsong() if pos>0: path = xmms.control.get_playlist_file(0, self.session) xmms.control.playlist_delete(0, self.session) del self.songs[path] else: self.playbackinfo.updatesong(None) time.sleep(0.1) def _playsong(self, song, manual): if self.noqueue: xmms.control.playlist_clear(self.session) self.songs = {} self.songs[song.path] = song xmms.control.playlist_add((song.path,), self.session) if not xmms.control.is_playing(): xmms.control.play(self.session) # wait a little, so that xmms can start playing # and we don't request another song... time.sleep(0.5) def _playerstart(self): # before we start playing, we clear the playlist xmms.control.playlist_clear(self.session) self.songs = {} def _playerpause(self): # before we start playing, we clear the playlist xmms.control.pause(self.session) def _playerunpause(self): # before we start playing, we clear the playlist xmms.control.play(self.session) def _playerseekrelative(self, seconds): ptime = xmms.control.get_output_time(self.session) pos = xmms.control.get_playlist_pos(self.session) ttime = xmms.control.get_playlist_time(pos, self.session) time = min(max(ptime + int(seconds * 1000), 0), ttime) xmms.control.jump_to_time(time, self.session) def _playerstop(self): xmms.control.playlist_clear(self.session) self.songs = {} def playernext(self, event): if event.playerid==self.id: if xmms.control.is_playing(self.session): self.requestnextsong() # wait a little for the other threads, uuh... time.sleep(1) self.channel.process() time.sleep(0.1) xmms.control.playlist_next(self.session) def _playerquit(self): xmms.control.quit(self.session) PyTone-3.0.3/src/plugins/.___init__.py000644 000765 000765 00000000122 10266220577 020043 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/plugins/__init__.py000644 000765 000765 00000000000 10266220577 017621 0ustar00ringoringo000000 000000 PyTone-3.0.3/src/plugins/audioscrobbler/000755 000765 000765 00000000000 11406223507 020512 5ustar00ringoringo000000 000000 PyTone-3.0.3/src/plugins/._minimal.py000644 000765 000765 00000000122 10266220577 017732 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/plugins/minimal.py000644 000765 000765 00000002174 10266220577 017526 0ustar00ringoringo000000 000000 # -*- coding: ISO-8859-1 -*- import events import plugin import config import log import copy # We define a config section, which will be called [plugin.minimal] # In the plugin, we will be able to access the values as member variable # self.config class config(config.configsection): message = config.configstring("new song: ") class plugin(plugin.plugin): """ a simple plugin that logs when a new song is played on the main player A configuration option message allows the user to specify the notification string. """ def start(self): # its good practice to notify the user of the started plugin log.info("started minimal plugin") self.playbackinfo = None self.channel.subscribe(events.playbackinfochanged, self.playbackinfochanged) # event handler def playbackinfochanged(self, event): if event.playbackinfo is None: return if self.playbackinfo is None or self.playbackinfo.song != event.playbackinfo.song: log.info("%s%s" % (self.config.message, event.playbackinfo.song)) self.playbackinfo = copy.copy(event.playbackinfo) PyTone-3.0.3/src/plugins/._osdtitle.py000644 000765 000765 00000000122 10575546524 020141 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/plugins/osdtitle.py000644 000765 000765 00000004444 10575546524 017737 0ustar00ringoringo000000 000000 ### OSD title plugin for pytone ### """"""""""""""""""""""""""" ### This plugin displays on-screen song titles whenever pytone changes to ### a new song. You can install this plugin by simply copying it to your ### pytone system-wide plugin directory (eg. /usr/share/pytone/plugins ### or ~/pytone/plugins/). And enable it by adding it to the plugins list ### in the general section, like: ### ### [general] ### plugins = osdtitle ### ### You can customise the title by adding the following to /etc/pytonerc ### or ~/.pytone/pytonerc ### ### [plugin.osdtitle] ### songformat = %(artist)s - %(title)s (%(length)s) ### font = -adobe-helvetica-bold-r-*-*-*-240-*-*-*-*-*-* ### color = green ### position = top ### offset = 60 ### align = center ### shadow = 2 ### lines = 1 ### ### See the osd_cat(1) manpage for more information about these options. ### ### You can find the latest release at: ### ### http://dag.wieers.com/home-made/pytone/ ### ### If you have improvements or changes to this plugin, please ### send them to the pytone mailinglist and include me as well: ### ### pytone-users@luga.de, Dag Wieers import events, encoding, plugin, config, os, re class config(config.configsection): songformat = config.configstring('%(artist)s - %(title)s (%(length)s)') font = config.configstring('-adobe-helvetica-bold-r-*-*-*-240-*-*-*-*-*-*') color = config.configstring('green') position = config.configstring('top') offset = config.configstring('60') align = config.configstring('center') shadow = config.configstring('2') lines = config.configstring('1') class plugin(plugin.plugin): def init(self): self.channel.subscribe(events.playbackinfochanged, self.playbackinfochanged) self.command = 'osd_cat -p %(position)s -c %(color)s -o %(offset)s -A %(align)s -s %(shadow)s -l %(lines)s -f %(font)s -' % self.config self.previoussong = '' def playbackinfochanged(self, event): if event.playbackinfo.song and event.playbackinfo.song != self.previoussong: song = encoding.encode(event.playbackinfo.song.format(self.config.songformat, safe=True)) os.system('echo "%s" | %s &' % (song, self.command)) self.previoussong = event.playbackinfo.song # vim:ts=4:sw=4 PyTone-3.0.3/src/plugins/._termtitle.py000644 000765 000765 00000000122 10575546524 020323 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/plugins/termtitle.py000644 000765 000765 00000004075 10575546524 020121 0ustar00ringoringo000000 000000 ### Terminal title plugin for pytone ### """""""""""""""""""""""""""""""" ### This plugin updates terminal and screen titles whenever pytone ### changes to a new song. You can install this plugin by simply ### copying it to your pytone system-wide plugin directory ### (eg. /usr/share/pytone/plugins or ~/pytone/plugins/). And ### enable it by adding it to the plugins list in the general ### section, like: ### ### [general] ### plugins = termtitle ### ### You can customise the title by adding the following to ### /etc/pytonerc or ~/.pytone/pytonerc ### ### [plugin.termtitle] ### songformat = pytone: %(title)s - %(artist)s - %(length)s ### ### You can find the latest release at: ### ### http://dag.wieers.com/home-made/pytone/ ### ### If you have improvements or changes to this plugin, please ### send them to the pytone mailinglist and include me as well: ### ### pytone-users@luga.de, Dag Wieers import events, encoding, plugin, config, sys, os, re class config(config.configsection): songformat = config.configstring('%(artist)s - %(title)s (%(length)s)') class plugin(plugin.plugin): def init(self): if re.compile('(screen|xterm*)').match(os.getenv('TERM')): self.channel.subscribe(events.playbackinfochanged, self.playbackinfochanged) self.previoussong = None self.previouscross = False else: raise RuntimeError("Terminal type not supported") def playbackinfochanged(self, event): if event.playbackinfo.song and event.playbackinfo.song != self.previoussong or event.playbackinfo.iscrossfading() != self.previouscross: self.changetermtitle(event) self.previoussong = event.playbackinfo.song self.previouscross = event.playbackinfo.iscrossfading() def changetermtitle(self, event): prefix = (event.playbackinfo.iscrossfading() and '-> ' or '') song = encoding.encode(event.playbackinfo.song.format(self.config.songformat, safe=True)) sys.stdout.write('\033]0;' + prefix + song + '\007') # vim:ts=4:sw=4 PyTone-3.0.3/src/plugins/audioscrobbler/.___init__.py000644 000765 000765 00000000122 10266220577 023042 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/plugins/audioscrobbler/__init__.py000644 000765 000765 00000000161 10266220577 022630 0ustar00ringoringo000000 000000 import audioscrobbler plugin = audioscrobbler.audioscrobblerplugin config = audioscrobbler.audioscrobblerconfig PyTone-3.0.3/src/plugins/audioscrobbler/._audioscrobbler.py000644 000765 000765 00000000122 11060432766 024300 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/plugins/audioscrobbler/audioscrobbler.py000644 000765 000765 00000006115 11060432766 024073 0ustar00ringoringo000000 000000 # -*- coding: UTF-8 -*- ############################################################################## # # Copyright (c) 2004 Nicolas Évrard All Rights Reserved. # # WARNING: This program as such is intended to be used by professional # programmers who take the whole responsability of assessing all potential # consequences resulting from its eventual inadequacies and bugs # End users who are looking for a ready-to-use solution with commercial # garantees and support are strongly adviced to contract a Free Software # Service Company # # This program is Free Software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ############################################################################## import events import config import log import plugin import scrobbler # the configuration section [plugin.audioscrobbler] contains two options: # username and password class audioscrobblerconfig(config.configsection): username = config.configstring("") password = config.configstring("") # sending of audioscrobbler information may take time, so we make this a # separate thread by deriving from plugin.threadedplugin class audioscrobblerplugin(plugin.threadedplugin): def init(self): self.scrobbler = scrobbler.Scrobbler(self.config.username, self.config.password) self.scrobbler.handshake() self.lastSong = None self.channel.subscribe(events.playbackinfochanged, self.playbackinfochanged) log.info("started audiscrobbler plugin") def playbackinfochanged(self, event): if not event.playbackinfo.isplaying(): return song = event.playbackinfo.song # do not submit songs which are shorter than 30 seconds or longer than 30 minutes if song.length < 30 or song.length > 30*60: return # submit when the song playback is 50% complete or 240 seconds have been passed, # whatever comes first mintime = (song.length <= 480 and int(song.length/2)) or 240 if ( event.playbackinfo.time >= mintime and song != self.lastSong): log.debug("Audioscrobbler: submitting song '%s'" % song.id) try: self.scrobbler.submit(song) log.debug("Audioscrobbler: submission successful") except scrobbler.SubmissionError: log.error("Audioscrobbler: submission failed") except scrobbler.BadAuthError: log.error("Audioscrobbler: incorrect account information") self.lastSong = song PyTone-3.0.3/src/plugins/audioscrobbler/._scrobbler.py000644 000765 000765 00000000122 11060433277 023254 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/plugins/audioscrobbler/scrobbler.py000644 000765 000765 00000012746 11060433277 023056 0ustar00ringoringo000000 000000 ############################################################################## # # Copyright (c) 2004 TINY SPRL. (http://tiny.be) All Rights Reserved. # Fabien Pinckaers # # WARNING: This program as such is intended to be used by professional # programmers who take the whole responsability of assessing all potential # consequences resulting from its eventual inadequacies and bugs # End users who are looking for a ready-to-use solution with commercial # garantees and support are strongly adviced to contract a Free Software # Service Company # # This program is Free Software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ############################################################################## import os.path import urllib import md5 import time import csv class BadAuthError(Exception): pass class NotConnectedError(Exception): pass class SubmissionError(Exception): pass class Scrobbler(object): client = 'tst' version = '0.1' url = 'http://post.audioscrobbler.com/' backlogfile = os.path.expanduser('~/.audioscrobbler') def __init__(self, user, password): self.user = user self.password = password self.connected = False self.lastconnected = 0 def dohandshake(self): timestamp = str(int(time.time())) pswd = md5.new(md5.new(self.password).hexdigest() + timestamp).hexdigest() rs = urllib.urlencode( { 'hs' : 'true', 'p' : '1.1', 'c' : self.client, 'v' : self.version, 'u' : self.user, 't' : int(time.time()), 'a' : pswd }) url = self.url + "?" + rs result = urllib.urlopen(url).readlines() if result[0].startswith('UPTODATE'): return True, False, result[1:] elif result[0].startswith('UPDATE'): return True, True, result[1:] elif result[0].startswith('FAILED'): return False, True, result elif result[0].startswith('BADUSER'): return False, False, result def handshake(self): connected = False nbtries = 0 while not connected: connected, newclient, results = self.dohandshake() if connected: self.connected = True self.newclient = newclient self.md5 = results[0][:-1] self.submitURL = results[1][:-1] self.lastconnected = time.time() else: if not newclient: connected = True raise BadAuthError time.sleep(2**nbtries * 60) nbtries += 1 if nbtries > 4: connected = True def submit(self, song): self.sendInfo(song.artist, song.title, song.album, song.length, song.date_lastplayed) def sendInfo(self, artist, title, album, length, debut): if self.lastconnected - time.time() > 300: self.connected = False self.newclient = False self.md5 = '' self.submitURL = '' self.handshake() if not self.connected: raise NotConnectedError pswd = md5.new(md5.new(self.password).hexdigest() + self.md5).hexdigest() infodict = { 'u' : self.user, 's' : pswd, 'a[0]' : artist, 't[0]' : title, 'b[0]' : album, 'm[0]' : '', 'l[0]' : length, 'i[0]' : time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(debut))} rs = urllib.urlencode(infodict) try: result = urllib.urlopen(self.submitURL, rs).readlines() except IOError: self.addBacklog(infodict) return False if result[0].startswith('OK'): return True elif result[0].startswith('FAILED'): raise SubmissionError elif result[0].startswith('BADAUTH'): raise BadAuthError def addBacklog(self, info): fd = file(self.backlogfile, 'wa') writer = csv.writer(fd) writer.writerow([info["a[0]"], info["t[0]"], info["b[0]"], info["l[0]"], info["i[0]"]]) fd.close() def getBacklog(self): try: reader = csv.reader(file(self.backlogfile, 'r')) except IOError: return backlogs = list(reader) file(self.backlogfile, 'w').close() for entry in backlogs: self.sendInfo(*entry) time.sleep(1) def main(): class A: pass a = A() a.artist = raw_input('Artiste: ') a.title = raw_input('Titre: ') a.album = raw_input('Album: ') a.length = raw_input('Duree: ') a.lastplayed = [ int(time.time() - 250) ] scrobbler = Scrobbler('user', '*****') scrobbler.handshake() scrobbler.submit(a) if __name__ == '__main__': main() PyTone-3.0.3/src/pcm/._pcm.c000644 000765 000765 00000000122 10470415455 015751 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/pcm/pcm.c000644 000765 000765 00000024552 10470415455 015551 0ustar00ringoringo000000 000000 /* pcm.c: Copyright 2002 Joerg Lehmann * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, * USA. */ /* rate conversion: * based on XMMS Crossfade Plugin * Copyright (C) 2000-2001 Peter Eisenlohr * based on the original OSS Output Plugin * Copyright (C) 1998-2000 Peter Alm, Mikael Alm, Olle Hallnas, Thomas Nilsson * and 4Front Technologies * Rate conversion for 16bit stereo samples. * * The algorithm (Least Common Multiple Linear Interpolation) was * adapted from the rate conversion code used in * * sox-12.16, Copyright 1998 Fabrice Bellard, originally * Copyright 1991 Lance Norskog And Sundry Contributors. * */ #include #include #include #include /* Note: in all routines, we assume, that we deal with 16 bit stereo PCM data */ static void mix(char *b, const char *b1, const char *b2, int l, float *mixingratio, float mixingrate) { int16_t *ib = (int16_t *) b; int16_t *ib1 = (int16_t *) b1; int16_t *ib2 = (int16_t *) b2; int il = l/2; float f = *mixingratio; float df = mixingrate/2; /* we deal with stereo data */ if (df>=0) while (il--) { *ib++ = *ib1++ * (1-f) + *ib2++ * f; f += df; if (f>1) f=1; } else while (il--) { *ib++ = *ib1++ * (1-f) + *ib2++ * f; f += df; if (f<0) f=0; } *mixingratio = f; } static PyObject *py_mix(PyObject *self, PyObject *args) { PyObject *returnObj = NULL; char *b1; int l1; char *b2; int l2; char *dummy=0; /* buffer used, if l1!=l2 */ PyObject *buffobj; char *b; /* here goes the mixed stream */ int l; float mixingratio; float mixingrate; if (PyArg_ParseTuple(args, "t#t#ff", &b1, &l1, &b2, &l2, &mixingratio, &mixingrate)) { if (l1l2) { if (!(dummy = (char *) malloc(l1))) return NULL; Py_BEGIN_ALLOW_THREADS memcpy((void *) dummy, (void *) b2, l2); /* fill rest with zeros */ memset((void *) (dummy+l2), 0, l1-l2); Py_END_ALLOW_THREADS /* now proceed, as if nothing has ever happend...*/ b2 = dummy; l2 = l1; } l = l1; /* get new buffer object from python */ buffobj = PyBuffer_New(l); /* use the internal python argument parser to get b*/ PyArg_Parse(buffobj, "t#", &b, &l); /* now do the real work*/ Py_BEGIN_ALLOW_THREADS mix(b, b1, b2, l, &mixingratio, mixingrate); Py_END_ALLOW_THREADS /* build up return structure */ returnObj = Py_BuildValue("Of", buffobj, mixingratio); Py_DECREF(buffobj); if (dummy) free(dummy); } return returnObj; } /* greatest common divisor */ static long long gcd(long m, long n) { long r; while(1) { r = m % n; if (r == 0) return n; m = n; n = r; } } /* least common multiplier */ static long long lcm(int i, int j) { return ((long long ) i * j) / gcd(i, j); } static int rate_convert(char *in_c, int lin, char *out_c, int lout, int in_rate, int out_rate, int firstsample, int16_t *last_l, int16_t *last_r) { long long lcm_rate = lcm(in_rate, out_rate); int in_skip = lcm_rate / in_rate; int out_skip = lcm_rate / out_rate; int samplenr = lin/4; /* number of samples */ int16_t *in_i = (int16_t* ) in_c; int16_t *out_i = (int16_t* ) out_c; int in_ofs = 0; int out_ofs = 0; int emitted = 0; /* take last_l and last_r for first sample from input sample */ if (firstsample) { *last_l = in_i[0]; *last_r = in_i[1]; } /* interpolation loop */ for(;;) { /* advance input range to span next output ??? */ while ( (in_ofs + in_skip) <= out_ofs ) { *last_l = *in_i++; *last_r = *in_i++; in_ofs += in_skip; samplenr--; if (samplenr == 0) return emitted*4; } *out_i++ = *last_l + (((float) in_i[0] - *last_l) * (out_ofs - in_ofs) / in_skip); *out_i++ = *last_r + (((float) in_i[1] - *last_r) * (out_ofs - in_ofs) / in_skip); /* count emitted samples*/ emitted++; assert(emitted*4<=lout); /* advance to next output */ out_ofs += out_skip; /* long samples with high LCM's overrun counters! */ if(out_ofs == in_ofs) out_ofs = in_ofs = 0; } /* we never arrive here */ return 0; } static PyObject *py_rate_convert(PyObject *self, PyObject *args) { PyObject *returnObj = NULL; char *in_c; /* input data */ int lin; /* length of in_c */ int in_rate; /* input sampling rate */ char *out_c; /* output data */ int lout; /* length of out_c */ int out_rate; /* output sampling rate */ char *newout_c=0; /* dummy buffer for output data */ PyObject *py_pre_out_c; /* append output to this buffer */ PyObject *py_start_pre_out; /* and start from here */ char *pre_out_c = NULL; int lpre_out = 0; int start_pre_out = 0; PyObject *py_last_l; PyObject *py_last_r; int16_t last_l; int16_t last_r; int firstsample; /* are we dealing with the first sample */ if (PyArg_ParseTuple(args, "t#iOOiOO", &in_c, &lin, &in_rate, &py_pre_out_c, &py_start_pre_out, &out_rate, &py_last_l, &py_last_r )) { PyObject *py_out; /* python object for output buffer*/ char *b; /* temporary variable */ int l; int emitted; /* length of output stream <= out_c */ if (py_last_l!=Py_None && py_last_r!=Py_None) { int i; firstsample = 0; PyArg_Parse(py_last_l, "i", &i); last_l = i; PyArg_Parse(py_last_r, "i", &i); last_r = i; } else firstsample = 1; /* get data of prefix, if present */ if (py_pre_out_c!=Py_None && py_start_pre_out!=Py_None) { PyArg_Parse(py_pre_out_c, "t#", &pre_out_c, &lpre_out); PyArg_Parse(py_start_pre_out, "i", &start_pre_out); pre_out_c += start_pre_out; lpre_out -= start_pre_out; } if (in_rate!=out_rate) { /* allocate space for resampled output */ lout = lin*out_rate/in_rate + 4; if (!(newout_c = (char *) malloc(lout))) return NULL; out_c = newout_c; /* now do the real work*/ Py_BEGIN_ALLOW_THREADS emitted= rate_convert(in_c, lin, out_c, lout, in_rate, out_rate, firstsample, &last_l, &last_r); Py_END_ALLOW_THREADS } else { /* we only need to copy the input data */ emitted = lin; out_c = in_c; } /* get new buffer object from python for prefixed data + resampled output */ py_out = PyBuffer_New(lpre_out + emitted); /* use the internal python argument parser to get b*/ PyArg_Parse(py_out, "t#", &b, &l); /* now we copy our result */ Py_BEGIN_ALLOW_THREADS memcpy((void *) b, (void *) pre_out_c, lpre_out); memcpy((void *) (b+lpre_out), (void *) out_c, emitted); /* free space for dummy buffers if it has been allocated before */ if (newout_c) free(newout_c); Py_END_ALLOW_THREADS /* build up return structure */ returnObj = Py_BuildValue("Oii", py_out, (int) last_l, (int) last_r); Py_DECREF(py_out); } return returnObj; } /* interleave stereo channels from mono file */ static PyObject *py_upsample(PyObject *self, PyObject *args) { PyObject *returnObj = NULL; char *in_c; /* input data */ int lin; /* length of in_c */ char *out_c; /* output data */ PyObject *py_out; /* python object for output buffer*/ int16_t *in_i; int16_t *out_i; char *b; int l; int i, j; if (PyArg_ParseTuple(args, "t#", &in_c, &lin)) { Py_BEGIN_ALLOW_THREADS if (!(out_c = (char *) malloc(2*lin))) return NULL; in_i = (int16_t* ) in_c; out_i = (int16_t* ) out_c; for (i=0, j=0; i32768) b_i[i] = 32768; else if (r<-32767) b_i[i] = -32767; else b_i[i] = (int) r; } Py_END_ALLOW_THREADS Py_INCREF(Py_None); returnObj = Py_None; } return returnObj; } /* exported methods */ static PyMethodDef pcm_methods[] = { {"mix", py_mix, METH_VARARGS}, {"rate_convert", py_rate_convert, METH_VARARGS}, {"upsample", py_upsample, METH_VARARGS}, {"scale", py_scale, METH_VARARGS}, {NULL, NULL} }; void initpcm(void) { (void) Py_InitModule("pcm", pcm_methods); } PyTone-3.0.3/src/cursext/._cursextmodule.c000644 000765 000765 00000000122 10266220576 021014 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/src/cursext/cursextmodule.c000644 000765 000765 00000002664 10266220576 020614 0ustar00ringoringo000000 000000 /* cursextmodule.c: Copyright 2004 Johannes Mockenhaupt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, * USA. */ #include #include /* the function this module is about ;-) */ static PyObject *useDefaultColors(PyObject *self, PyObject *args) { if (use_default_colors() == OK) return Py_BuildValue("i",1); else return Py_BuildValue("i",0); } /* table of functions this module provides */ static PyMethodDef CursExtMethods [] = { {"useDefaultColors", useDefaultColors, METH_NOARGS, "use terminal's default colors, enabling transparency." }, {NULL, NULL, 0, NULL} }; /* module initialisation */ /* PyMODINT_FUNC */ void initcursext(void) { Py_InitModule("cursext", CursExtMethods); } PyTone-3.0.3/locale/de/000755 000765 000765 00000000000 11406223507 015072 5ustar00ringoringo000000 000000 PyTone-3.0.3/locale/fr/000755 000765 000765 00000000000 11406223507 015111 5ustar00ringoringo000000 000000 PyTone-3.0.3/locale/it/000755 000765 000765 00000000000 11406223507 015116 5ustar00ringoringo000000 000000 PyTone-3.0.3/locale/pl/000755 000765 000765 00000000000 11406223507 015115 5ustar00ringoringo000000 000000 PyTone-3.0.3/locale/._PyTone.pot000644 000765 000765 00000000122 10557445625 016671 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/locale/PyTone.pot000644 000765 000765 00000034030 10557445625 016461 0ustar00ringoringo000000 000000 # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "POT-Creation-Date: 2006-09-17 20:44+CEST\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: ENCODING\n" "Generated-By: pygettext.py 1.5\n" #: src/filelist.py:113 msgid "Not rating virtual directories!" msgstr "" #: src/filelist.py:117 msgid "Rating %d song(s) with %d star(s)..." msgstr "" #: src/filelist.py:119 msgid "Removing rating of %d song(s)..." msgstr "" #: src/filelist.py:129 msgid "Not tagging virtual directories!" msgstr "" #: src/filelist.py:132 msgid "Tagging %d song(s) with tag '%s'..." msgstr "" #: src/filelist.py:142 msgid "Not untagging virtual directories!" msgstr "" #: src/filelist.py:145 msgid "Removing tag '%s' from %d song(s)..." msgstr "" #: src/filelist.py:156 msgid "Scanning for songs in database '%s'..." msgstr "" #: src/filelist.py:166 msgid "Rescanning %d song(s)..." msgstr "" #: src/filelistwin.py:167 msgid "Search" msgstr "" #: src/help.py:39 msgid "refresh" msgstr "" #: src/help.py:39 msgid "refresh display" msgstr "" #: src/help.py:40 msgid "exit" msgstr "" #: src/help.py:40 msgid "exit PyTone (press twice)" msgstr "" #: src/help.py:41 msgid "play" msgstr "" #: src/help.py:41 msgid "start main player" msgstr "" #: src/help.py:42 msgid "pause" msgstr "" #: src/help.py:42 msgid "pause main player" msgstr "" #: src/help.py:43 msgid "advance to next song" msgstr "" #: src/help.py:43 msgid "next song" msgstr "" #: src/help.py:44 msgid "go back to previous song" msgstr "" #: src/help.py:44 msgid "previous song" msgstr "" #: src/help.py:45 msgid "rewind" msgstr "" #: src/help.py:45 msgid "rewind main player" msgstr "" #: src/help.py:46 msgid "forward" msgstr "" #: src/help.py:46 msgid "forward main player" msgstr "" #: src/help.py:47 msgid "stop" msgstr "" #: src/help.py:47 msgid "stop main player" msgstr "" #: src/help.py:49 msgid "delete played" msgstr "" #: src/help.py:49 msgid "delete played songs from playlist" msgstr "" #: src/help.py:50 msgid "mark all songs in playlist as unplayed" msgstr "" #: src/help.py:50 msgid "replay songs" msgstr "" #: src/help.py:52 msgid "toggle playlist mode" msgstr "" #: src/help.py:52 msgid "toggle the playlist mode" msgstr "" #: src/help.py:53 msgid "clear" msgstr "" #: src/help.py:53 msgid "clear playlist" msgstr "" #: src/help.py:54 msgid "save" msgstr "" #: src/help.py:54 msgid "save playlist" msgstr "" #: src/help.py:55 msgid "help" msgstr "" #: src/help.py:55 msgid "show help" msgstr "" #: src/help.py:56 msgid "log" msgstr "" #: src/help.py:56 msgid "show log messages" msgstr "" #: src/help.py:57 msgid "show statistical information about database(s)" msgstr "" #: src/help.py:57 msgid "statistics" msgstr "" #: src/help.py:58 msgid "item info" msgstr "" #: src/help.py:58 msgid "show information about selected item" msgstr "" #: src/help.py:59 msgid "lyrics" msgstr "" #: src/help.py:59 msgid "show lyrics of selected song" msgstr "" #: src/help.py:60 msgid "toggle information shown in item info window" msgstr "" #: src/help.py:60 msgid "toggle item info" msgstr "" #: src/help.py:61 msgid "toggle layout" msgstr "" #: src/help.py:62 msgid "increase output volume" msgstr "" #: src/help.py:62 msgid "volume up" msgstr "" #: src/help.py:63 msgid "decrease output volume" msgstr "" #: src/help.py:63 msgid "volume down" msgstr "" #: src/help.py:64 msgid "increase the play speed" msgstr "" #: src/help.py:64 msgid "play faster" msgstr "" #: src/help.py:65 msgid "decrease the play speed" msgstr "" #: src/help.py:65 msgid "play slower" msgstr "" #: src/help.py:66 msgid "default play speed" msgstr "" #: src/help.py:66 msgid "reset the play speed to normal" msgstr "" #: src/help.py:67 msgid "rate current song 1" msgstr "" #: src/help.py:67 msgid "rate currently playing song with 1 star" msgstr "" #: src/help.py:68 msgid "rate current song 2" msgstr "" #: src/help.py:68 msgid "rate currently playing song with 2 stars" msgstr "" #: src/help.py:69 msgid "rate current song 3" msgstr "" #: src/help.py:69 msgid "rate currently playing song with 3 stars" msgstr "" #: src/help.py:70 msgid "rate current song 4" msgstr "" #: src/help.py:70 msgid "rate currently playing song with 4 stars" msgstr "" #: src/help.py:71 msgid "rate current song 5" msgstr "" #: src/help.py:71 msgid "rate currently playing song with 5 stars" msgstr "" #: src/help.py:74 src/help.py:93 msgid "down" msgstr "" #: src/help.py:74 src/help.py:93 msgid "move to the next entry" msgstr "" #: src/help.py:75 src/help.py:94 msgid "move to the previous entry" msgstr "" #: src/help.py:75 src/help.py:94 msgid "up" msgstr "" #: src/help.py:76 src/help.py:95 msgid "move to the next page" msgstr "" #: src/help.py:76 src/help.py:95 msgid "page down" msgstr "" #: src/help.py:77 src/help.py:96 msgid "move to previous page" msgstr "" #: src/help.py:77 src/help.py:96 msgid "page up" msgstr "" #: src/help.py:78 src/help.py:97 msgid "first" msgstr "" #: src/help.py:78 src/help.py:97 msgid "move to the first entry" msgstr "" #: src/help.py:79 src/help.py:98 msgid "last" msgstr "" #: src/help.py:79 src/help.py:98 msgid "move to the last entry" msgstr "" #: src/help.py:80 msgid "enter dir" msgstr "" #: src/help.py:80 msgid "enter selected directory" msgstr "" #: src/help.py:81 msgid "exit dir" msgstr "" #: src/help.py:81 msgid "go directory up" msgstr "" #: src/help.py:82 msgid "add song" msgstr "" #: src/help.py:82 msgid "add song to playlist" msgstr "" #: src/help.py:83 msgid "add dir" msgstr "" #: src/help.py:83 msgid "add directory recursively to playlist" msgstr "" #: src/help.py:84 src/help.py:103 msgid "immediate play" msgstr "" #: src/help.py:84 src/help.py:103 msgid "play selected song immediately" msgstr "" #: src/help.py:85 msgid "switch to playlist" msgstr "" #: src/help.py:85 msgid "switch to playlist window" msgstr "" #: src/help.py:87 msgid "add random contents of dir to playlist" msgstr "" #: src/help.py:87 msgid "random add dir" msgstr "" #: src/help.py:88 msgid "search" msgstr "" #: src/help.py:88 msgid "search entry" msgstr "" #: src/help.py:89 msgid "repeat last search" msgstr "" #: src/help.py:89 msgid "repeat search" msgstr "" #: src/help.py:90 src/help.py:105 msgid "rescan" msgstr "" #: src/help.py:90 src/help.py:105 msgid "rescan/update id3 info for selection" msgstr "" #: src/help.py:99 msgid "move song up" msgstr "" #: src/help.py:100 msgid "move song down" msgstr "" #: src/help.py:101 msgid "shuffle" msgstr "" #: src/help.py:101 msgid "shuffle playlist" msgstr "" #: src/help.py:102 msgid "delete" msgstr "" #: src/help.py:102 msgid "delete entry" msgstr "" #: src/help.py:104 msgid "switch to database" msgstr "" #: src/help.py:104 msgid "switch to database window" msgstr "" #: src/help.py:107 msgid "jump to selected" msgstr "" #: src/help.py:107 msgid "jump to selected song in filelist window" msgstr "" #: src/help.py:114 msgid "CTRL" msgstr "" #: src/help.py:120 msgid "" msgstr "" #: src/help.py:121 msgid "" msgstr "" #: src/help.py:122 msgid "" msgstr "" #: src/help.py:123 msgid "" msgstr "" #: src/help.py:124 msgid "" msgstr "" #: src/help.py:125 msgid "" msgstr "" #: src/help.py:126 msgid "" msgstr "" #: src/help.py:127 msgid "" msgstr "" #: src/help.py:128 msgid "" msgstr "" #: src/help.py:129 msgid "" msgstr "" #: src/help.py:130 msgid "" msgstr "" #: src/help.py:131 msgid "" msgstr "" #: src/help.py:132 msgid "" msgstr "" #: src/help.py:133 msgid "" msgstr "" #: src/help.py:134 msgid "" msgstr "" #: src/help.py:135 msgid "" msgstr "" #: src/help.py:136 msgid "" msgstr "" #: src/help.py:137 msgid "" msgstr "" #: src/help.py:145 msgid "Alt" msgstr "" #: src/helpwin.py:45 msgid "PyTone Help" msgstr "" #: src/inputwin.py:147 msgid "ok" msgstr "" #: src/inputwin.py:150 msgid "cancel" msgstr "" #: src/item.py:149 msgid "Tag" msgstr "" #: src/item.py:168 src/item.py:170 msgid "Rating" msgstr "" #: src/item.py:170 src/item.py:1214 msgid "Not rated" msgstr "" #: src/item.py:233 src/item.py:235 msgid "Filter:" msgstr "" #: src/item.py:398 src/item.py:452 msgid "Title:" msgstr "" #: src/item.py:400 msgid "Nr:" msgstr "" #: src/item.py:404 src/item.py:453 src/item.py:664 msgid "Album:" msgstr "" #: src/item.py:406 src/item.py:496 msgid "URL:" msgstr "" #: src/item.py:409 src/item.py:460 msgid "Year:" msgstr "" #: src/item.py:414 src/item.py:454 src/item.py:615 src/item.py:663 #: src/item.py:754 msgid "Artist:" msgstr "" #: src/item.py:418 src/item.py:460 src/playerwin.py:76 msgid "Time:" msgstr "" #: src/item.py:422 src/item.py:463 msgid "Tags:" msgstr "" #: src/item.py:438 src/item.py:509 msgid "Played:" msgstr "" #: src/item.py:439 msgid "#%d, %s ago" msgstr "" #: src/item.py:442 src/item.py:463 src/item.py:1217 msgid "Rating:" msgstr "" #: src/item.py:461 msgid "Track No:" msgstr "" #: src/item.py:462 msgid "Disk No:" msgstr "" #: src/item.py:482 msgid "File type:" msgstr "" #: src/item.py:482 msgid "Size:" msgstr "" #: src/item.py:485 msgid "track" msgstr "" #: src/item.py:489 msgid "album" msgstr "" #: src/item.py:492 msgid "Beats per minute:" msgstr "" #: src/item.py:492 msgid "Replaygain:" msgstr "" #: src/item.py:493 msgid "Times played:" msgstr "" #: src/item.py:493 msgid "Times skipped:" msgstr "" #: src/item.py:494 msgid "Comment:" msgstr "" #: src/item.py:495 msgid "%d lines" msgstr "" #: src/item.py:495 msgid "Lyrics:" msgstr "" #: src/item.py:509 msgid "%s ago" msgstr "" #: src/item.py:612 src/item.py:659 msgid "Various" msgstr "" #: src/item.py:704 src/playlistwin.py:40 src/playlistwin.py:172 msgid "Playlist" msgstr "" #: src/item.py:727 msgid "Songs" msgstr "" #: src/item.py:768 msgid "No artist" msgstr "" #: src/item.py:783 msgid "Random song list" msgstr "" #: src/item.py:806 msgid "Last played songs" msgstr "" #: src/item.py:832 msgid "Top played songs" msgstr "" #: src/item.py:859 msgid "Last added songs" msgstr "" #: src/item.py:885 msgid "Albums" msgstr "" #: src/item.py:909 msgid "Compilations" msgstr "" #: src/item.py:921 msgid "Tags" msgstr "" #: src/item.py:955 msgid "Ratings" msgstr "" #: src/item.py:982 src/item.py:989 src/item.py:999 msgid "Playlists" msgstr "" #: src/item.py:1013 src/item.py:1019 src/item.py:1050 src/item.py:1055 msgid "Filesystem" msgstr "" #: src/item.py:1076 msgid "Song Database" msgstr "" #: src/item.py:1127 msgid "[Database: %s (%d)]" msgstr "" #: src/item.py:1129 msgid "%d databases (%d)" msgstr "" #: src/item.py:1154 msgid "%d artists" msgstr "" #: src/item.py:1156 msgid "? artists" msgstr "" #: src/item.py:1164 msgid "Database (%s, %s)" msgstr "" #: src/item.py:1166 msgid "%d databases (%s)" msgstr "" #: src/item.py:1171 msgid "[Database: %s (%%d)]" msgstr "" #: src/item.py:1173 msgid "%d databases (%%d)" msgstr "" #: src/item.py:1203 msgid "Tag:" msgstr "" #: src/iteminfowin.py:53 src/iteminfowin.py:74 msgid "MP3 Info" msgstr "" #: src/iteminfowin.py:67 msgid "No song" msgstr "" #: src/iteminfowin.py:76 msgid "Ogg Info" msgstr "" #: src/iteminfowin.py:78 msgid "Song Info" msgstr "" #: src/iteminfowin.py:80 msgid "Directory Info" msgstr "" #: src/iteminfowin.py:82 msgid "[Player: %s]" msgstr "" #: src/iteminfowin.py:174 msgid "Item info" msgstr "" #: src/logwin.py:32 msgid "PyTone Messages" msgstr "" #: src/lyricswin.py:32 src/lyricswin.py:55 msgid "Lyrics" msgstr "" #: src/lyricswin.py:34 src/lyricswin.py:62 msgid "No lyrics" msgstr "" #: src/lyricswin.py:56 msgid "No song selected" msgstr "" #: src/mixerwin.py:35 src/mixerwin.py:49 msgid "initialized oss mixer: device %s, channel %s" msgstr "" #: src/mixerwin.py:147 src/mixerwin.py:163 src/mixerwin.py:176 #: src/mixerwin.py:188 msgid "Volume:" msgstr "" #: src/mixerwin.py:170 msgid "Mixer" msgstr "" #: src/playerwin.py:46 src/playerwin.py:104 msgid "Playback Info" msgstr "" #: src/playerwin.py:50 src/playerwin.py:120 msgid "error '%s' occured during write to playerinfofile" msgstr "" #: src/playerwin.py:90 msgid "paused" msgstr "" #: src/playlistwin.py:167 msgid "Repeat" msgstr "" #: src/playlistwin.py:169 msgid "Random" msgstr "" #: src/pytone.py:67 msgid "PyTone %s startup" msgstr "" #: src/pytone.py:89 msgid "created PyTone directory ~/.pytone" msgstr "" #: src/pytone.py:165 msgid "Cannot load plugin '%s': %s" msgstr "" #: src/services/players/internal.py:116 msgid "cannot open audio device: error \"%s\"" msgstr "" #: src/services/players/internal.py:400 msgid "failed to open song \"%r\"" msgstr "" #: src/services/playlist.py:398 msgid "Save playlist" msgstr "" #: src/services/playlist.py:399 msgid "Name:" msgstr "" #: src/services/songdbs/remote.py:54 msgid "database %s: type remote, location: %s" msgstr "" #: src/services/songdbs/sqlite.py:1085 msgid "database %r: scanning for songs in %r" msgstr "" #: src/services/songdbs/sqlite.py:1093 msgid "database %r: removing stale songs" msgstr "" #: src/services/songdbs/sqlite.py:1097 msgid "database %r: rescan finished" msgstr "" #: src/services/songdbs/sqlite.py:1101 msgid "database %r: rescanning %d songs" msgstr "" #: src/services/songdbs/sqlite.py:1104 msgid "database %r: finished rescanning %d songs" msgstr "" #: src/statswin.py:30 msgid "PyTone Statistics" msgstr "" #: src/statswin.py:44 msgid "Database %s" msgstr "" #: src/statswin.py:45 msgid "%d songs, %d albums, %d artists, %d tags" msgstr "" #: src/statswin.py:52 msgid "local database (db file: %s)" msgstr "" #: src/statswin.py:54 msgid "remote database (server: %s)" msgstr "" #: src/statswin.py:55 msgid "type" msgstr "" #: src/statswin.py:56 msgid "base directory" msgstr "" #: src/statswin.py:58 msgid "cache size" msgstr "" #: src/statswin.py:61 msgid "%d requests, %d / %d objects" msgstr "" #: src/statswin.py:65 msgid "Request cache size" msgstr "" #: src/statswin.py:71 msgid "Request cache stats" msgstr "" #: src/statswin.py:72 msgid "%d hits / %d requests" msgstr "" PyTone-3.0.3/locale/pl/LC_MESSAGES/000755 000765 000765 00000000000 11406223507 016702 5ustar00ringoringo000000 000000 PyTone-3.0.3/locale/pl/LC_MESSAGES/._PyTone.mo000644 000765 000765 00000000122 11406223507 020665 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/locale/pl/LC_MESSAGES/PyTone.mo000644 000765 000765 00000012214 11406223507 020455 0ustar00ringoringo000000 000000 Þ•`ƒ( )5<D S^fw‰ •£« ´ ¾ÊÑâéñ ø  ! ' . ? G M %U &{ ¢ « À Õ Ü â ñ    !* L V o t Ž —  ­ ² Á Ø &Ý    6 N e | ’ ­ · ½ Ï Ö Û ú   ! . $5 Z _ m t ‹  ¥ ¶ È Í Þ ñ   8 F [ t € Š —§®· ¿ÍÔäøÿ  $18MS Z dq x…Œ’™ °º ¿"Í'ð3PW _m „ ‘ ŸÀÆÛ!á 4:P a3kŸ®¾Ðï- =^ mw “ ž"¨ ËØ àî ÿ/ : ANU dp ˆ”­ ÇÑêú'?Nb v W:UQ5 L'H6? &</4,`9 MN *8C7DYKOBR1; F(T]X$=)A_P-^0"E.G2S3VI>+![@J\ Z#%#%d, %s agoAlbumsArtist:Directory InfoFilesystemFilter:Last added songsLast played songsMixerName:Playback InfoPlayed:PlaylistPlaylistsPyTone HelpRandomRandom song listRatingRating:RepeatSave playlistSearchSong DatabaseSongsTime:Title:Top played songsVolume:Year:add diradd directory recursively to playlistadd random contents of dir to playlistadd songadd song to playlistadvance to next songcancelclearclear playlistdecrease output volumedeletedelete entrydelete playeddelete played songs from playlistenter direnter selected directoryexitexit PyTone (press twice)exit dirfirstgo directory uphelpimmediate playincrease output volumelastmark all songs in playlist as unplayedmove song downmove song upmove to previous pagemove to the first entrymove to the last entrymove to the next entrymove to the next pagemove to the previous entrynext songpausepause main playerpausedplayplay selected song immediatelyrandom add dirrefreshrefresh displayreplay songsrescanrescan/update id3 info for selectionsavesave playlistsearchsearch entryshow helpshow log messagesshuffleshuffle playliststart main playerstopstop main playerswitch to databaseswitch to database windowswitch to playlistswitch to playlist windowtoggle layouttoggle playlist modetoggle the playlist modevolume downvolume upProject-Id-Version: PyTone 2.0.13 POT-Creation-Date: 2006-09-17 20:44+CEST PO-Revision-Date: 2005-01-16 16:19+0100 Last-Translator: Krzysztof Zych MIME-Version: 1.0 Content-Type: text/plain; charset=iso-8859-2 Content-Transfer-Encoding: 8bit %d raz, %s temuAlbumyArtysta:KatalogSystem plikówFiltr:Ostatnio dodaneOstatnio odtwarzaneMikserNazwa:OdtwarzaneGrane:ListaListyPomoc PyToneLosowoLosowa lista utworówOcenaOcena:PowtarzajZapisz listêSzukajBaza utworówUtworyCzas:Tytu³:Najczê¶ciej odtwarzaneG³o¶no¶æ:Rok:dodaj katalogdodaj rekursywnie katalog do listydodaj do listy losowy fragment katalogudodajdodaj utwór do listyprzejd¼ do nastêpnego utworuanulujwyczy¶æwyczy¶æ listêzmniejsz g³o¶no¶æusuñusuñ elementusuñ odegraneusuñ z listy ju¿ odegrane utworywejd¼wejd¼ do podkataloguwyjd¼opu¶æ PyTone (wci¶nij dwukrotnie)wróæna pocz±tekprzejd¼ do katalogu nadrzêdnegopomocodtwarzaj natychmiastzwiêksz g³o¶no¶æna konieczaznacz wszystkie utwory z listy jako nieodtwarzaneprzenie¶ w dó³przenie¶ w górêpoprzednia stronaprzejd¼ do pierwszego elementuprzejd¼ do ostatniego elementuprzejd¼ do nastêpnego elementunastêpna stronaprzejd¼ do poprzedniego elementunastêpny utwórwstrzymajwstrzymaj g³ówny odtwarzaczwstrzymanyodtwarzajnatychmiast odtwarza wybrany utwórdodaj losowood¶wie¿od¶wie¿ ekranodtwórz ponownieuaktualnijodczytaj i uaktualnij info ID3 wybranej pozycjizapiszzapisz listêznajd¼znajd¼ elementpoka¿ pomocpoka¿ komunikaty z loguprzemieszajlosowo przemieszaj listêuruchom g³ówny odtwarzaczzatrzymajwy³±cz g³ówny odtwarzaczprzejd¼ do bazyprzejd¼ do okna bazy danychprzejd¼ do listyprzejd¼ do okna z list±prze³±cz uk³adprze³±cz tryb listyprze³±cz tryb listyg³o¶no¶æ -g³o¶no¶c +PyTone-3.0.3/locale/pl/LC_MESSAGES/._PyTone.po000644 000765 000765 00000000122 10654123704 020672 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/locale/pl/LC_MESSAGES/PyTone.po000644 000765 000765 00000040701 10654123704 020464 0ustar00ringoringo000000 000000 msgid "" msgstr "" "Project-Id-Version: PyTone 2.0.13\n" "POT-Creation-Date: 2006-09-17 20:44+CEST\n" "PO-Revision-Date: 2005-01-16 16:19+0100\n" "Last-Translator: Krzysztof Zych \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=iso-8859-2\n" "Content-Transfer-Encoding: 8bit\n" #: src/filelist.py:113 msgid "Not rating virtual directories!" msgstr "" #: src/filelist.py:117 msgid "Rating %d song(s) with %d star(s)..." msgstr "" #: src/filelist.py:119 msgid "Removing rating of %d song(s)..." msgstr "" #: src/filelist.py:129 msgid "Not tagging virtual directories!" msgstr "" #: src/filelist.py:132 msgid "Tagging %d song(s) with tag '%s'..." msgstr "" #: src/filelist.py:142 msgid "Not untagging virtual directories!" msgstr "" #: src/filelist.py:145 msgid "Removing tag '%s' from %d song(s)..." msgstr "" #: src/filelist.py:156 msgid "Scanning for songs in database '%s'..." msgstr "" #: src/filelist.py:166 msgid "Rescanning %d song(s)..." msgstr "" #: src/filelistwin.py:167 msgid "Search" msgstr "Szukaj" #: src/help.py:39 msgid "refresh" msgstr "od¶wie¿" #: src/help.py:39 msgid "refresh display" msgstr "od¶wie¿ ekran" #: src/help.py:40 msgid "exit" msgstr "wyjd¼" #: src/help.py:40 msgid "exit PyTone (press twice)" msgstr "opu¶æ PyTone (wci¶nij dwukrotnie)" #: src/help.py:41 msgid "play" msgstr "odtwarzaj" #: src/help.py:41 msgid "start main player" msgstr "uruchom g³ówny odtwarzacz" #: src/help.py:42 msgid "pause" msgstr "wstrzymaj" #: src/help.py:42 msgid "pause main player" msgstr "wstrzymaj g³ówny odtwarzacz" #: src/help.py:43 msgid "advance to next song" msgstr "przejd¼ do nastêpnego utworu" #: src/help.py:43 msgid "next song" msgstr "nastêpny utwór" #: src/help.py:44 #, fuzzy msgid "go back to previous song" msgstr "poprzednia strona" #: src/help.py:44 msgid "previous song" msgstr "" #: src/help.py:45 msgid "rewind" msgstr "" #: src/help.py:45 #, fuzzy msgid "rewind main player" msgstr "uruchom g³ówny odtwarzacz" #: src/help.py:46 msgid "forward" msgstr "" #: src/help.py:46 #, fuzzy msgid "forward main player" msgstr "uruchom g³ówny odtwarzacz" #: src/help.py:47 msgid "stop" msgstr "zatrzymaj" #: src/help.py:47 msgid "stop main player" msgstr "wy³±cz g³ówny odtwarzacz" #: src/help.py:49 msgid "delete played" msgstr "usuñ odegrane" #: src/help.py:49 msgid "delete played songs from playlist" msgstr "usuñ z listy ju¿ odegrane utwory" #: src/help.py:50 msgid "mark all songs in playlist as unplayed" msgstr "zaznacz wszystkie utwory z listy jako nieodtwarzane" #: src/help.py:50 msgid "replay songs" msgstr "odtwórz ponownie" #: src/help.py:52 msgid "toggle playlist mode" msgstr "prze³±cz tryb listy" #: src/help.py:52 msgid "toggle the playlist mode" msgstr "prze³±cz tryb listy" #: src/help.py:53 msgid "clear" msgstr "wyczy¶æ" #: src/help.py:53 msgid "clear playlist" msgstr "wyczy¶æ listê" #: src/help.py:54 msgid "save" msgstr "zapisz" #: src/help.py:54 msgid "save playlist" msgstr "zapisz listê" #: src/help.py:55 msgid "help" msgstr "pomoc" #: src/help.py:55 msgid "show help" msgstr "poka¿ pomoc" #: src/help.py:56 msgid "log" msgstr "" #: src/help.py:56 msgid "show log messages" msgstr "poka¿ komunikaty z logu" #: src/help.py:57 msgid "show statistical information about database(s)" msgstr "" #: src/help.py:57 msgid "statistics" msgstr "" #: src/help.py:58 msgid "item info" msgstr "" #: src/help.py:58 msgid "show information about selected item" msgstr "" #: src/help.py:59 msgid "lyrics" msgstr "" #: src/help.py:59 msgid "show lyrics of selected song" msgstr "" #: src/help.py:60 msgid "toggle information shown in item info window" msgstr "" #: src/help.py:60 #, fuzzy msgid "toggle item info" msgstr "prze³±cz tryb listy" #: src/help.py:61 msgid "toggle layout" msgstr "prze³±cz uk³ad" #: src/help.py:62 msgid "increase output volume" msgstr "zwiêksz g³o¶no¶æ" #: src/help.py:62 msgid "volume up" msgstr "g³o¶no¶c +" #: src/help.py:63 msgid "decrease output volume" msgstr "zmniejsz g³o¶no¶æ" #: src/help.py:63 msgid "volume down" msgstr "g³o¶no¶æ -" #: src/help.py:64 #, fuzzy msgid "increase the play speed" msgstr "usuñ odegrane" #: src/help.py:64 msgid "play faster" msgstr "" #: src/help.py:65 #, fuzzy msgid "decrease the play speed" msgstr "usuñ odegrane" #: src/help.py:65 #, fuzzy msgid "play slower" msgstr "odtwórz ponownie" #: src/help.py:66 #, fuzzy msgid "default play speed" msgstr "usuñ odegrane" #: src/help.py:66 msgid "reset the play speed to normal" msgstr "" #: src/help.py:67 msgid "rate current song 1" msgstr "" #: src/help.py:67 msgid "rate currently playing song with 1 star" msgstr "" #: src/help.py:68 msgid "rate current song 2" msgstr "" #: src/help.py:68 msgid "rate currently playing song with 2 stars" msgstr "" #: src/help.py:69 msgid "rate current song 3" msgstr "" #: src/help.py:69 msgid "rate currently playing song with 3 stars" msgstr "" #: src/help.py:70 msgid "rate current song 4" msgstr "" #: src/help.py:70 msgid "rate currently playing song with 4 stars" msgstr "" #: src/help.py:71 msgid "rate current song 5" msgstr "" #: src/help.py:71 msgid "rate currently playing song with 5 stars" msgstr "" #: src/help.py:74 src/help.py:93 msgid "down" msgstr "" #: src/help.py:74 src/help.py:93 msgid "move to the next entry" msgstr "przejd¼ do nastêpnego elementu" #: src/help.py:75 src/help.py:94 msgid "move to the previous entry" msgstr "przejd¼ do poprzedniego elementu" #: src/help.py:75 src/help.py:94 msgid "up" msgstr "" #: src/help.py:76 src/help.py:95 msgid "move to the next page" msgstr "nastêpna strona" #: src/help.py:76 src/help.py:95 msgid "page down" msgstr "" #: src/help.py:77 src/help.py:96 msgid "move to previous page" msgstr "poprzednia strona" #: src/help.py:77 src/help.py:96 msgid "page up" msgstr "" #: src/help.py:78 src/help.py:97 msgid "first" msgstr "na pocz±tek" #: src/help.py:78 src/help.py:97 msgid "move to the first entry" msgstr "przejd¼ do pierwszego elementu" #: src/help.py:79 src/help.py:98 msgid "last" msgstr "na koniec" #: src/help.py:79 src/help.py:98 msgid "move to the last entry" msgstr "przejd¼ do ostatniego elementu" #: src/help.py:80 msgid "enter dir" msgstr "wejd¼" #: src/help.py:80 msgid "enter selected directory" msgstr "wejd¼ do podkatalogu" #: src/help.py:81 msgid "exit dir" msgstr "wróæ" #: src/help.py:81 msgid "go directory up" msgstr "przejd¼ do katalogu nadrzêdnego" #: src/help.py:82 msgid "add song" msgstr "dodaj" #: src/help.py:82 msgid "add song to playlist" msgstr "dodaj utwór do listy" #: src/help.py:83 msgid "add dir" msgstr "dodaj katalog" #: src/help.py:83 msgid "add directory recursively to playlist" msgstr "dodaj rekursywnie katalog do listy" #: src/help.py:84 src/help.py:103 msgid "immediate play" msgstr "odtwarzaj natychmiast" #: src/help.py:84 src/help.py:103 msgid "play selected song immediately" msgstr "natychmiast odtwarza wybrany utwór" #: src/help.py:85 msgid "switch to playlist" msgstr "przejd¼ do listy" #: src/help.py:85 msgid "switch to playlist window" msgstr "przejd¼ do okna z list±" #: src/help.py:87 msgid "add random contents of dir to playlist" msgstr "dodaj do listy losowy fragment katalogu" #: src/help.py:87 msgid "random add dir" msgstr "dodaj losowo" #: src/help.py:88 msgid "search" msgstr "znajd¼" #: src/help.py:88 msgid "search entry" msgstr "znajd¼ element" #: src/help.py:89 msgid "repeat last search" msgstr "" #: src/help.py:89 #, fuzzy msgid "repeat search" msgstr "znajd¼" #: src/help.py:90 src/help.py:105 msgid "rescan" msgstr "uaktualnij" #: src/help.py:90 src/help.py:105 msgid "rescan/update id3 info for selection" msgstr "odczytaj i uaktualnij info ID3 wybranej pozycji" #: src/help.py:99 msgid "move song up" msgstr "przenie¶ w górê" #: src/help.py:100 msgid "move song down" msgstr "przenie¶ w dó³" #: src/help.py:101 msgid "shuffle" msgstr "przemieszaj" #: src/help.py:101 msgid "shuffle playlist" msgstr "losowo przemieszaj listê" #: src/help.py:102 msgid "delete" msgstr "usuñ" #: src/help.py:102 msgid "delete entry" msgstr "usuñ element" #: src/help.py:104 msgid "switch to database" msgstr "przejd¼ do bazy" #: src/help.py:104 msgid "switch to database window" msgstr "przejd¼ do okna bazy danych" #: src/help.py:107 msgid "jump to selected" msgstr "" #: src/help.py:107 msgid "jump to selected song in filelist window" msgstr "" #: src/help.py:114 msgid "CTRL" msgstr "" #: src/help.py:120 msgid "" msgstr "" #: src/help.py:121 msgid "" msgstr "" #: src/help.py:122 msgid "" msgstr "" #: src/help.py:123 msgid "" msgstr "" #: src/help.py:124 msgid "" msgstr "" #: src/help.py:125 msgid "" msgstr "" #: src/help.py:126 msgid "" msgstr "" #: src/help.py:127 msgid "" msgstr "" #: src/help.py:128 msgid "" msgstr "" #: src/help.py:129 msgid "" msgstr "" #: src/help.py:130 msgid "" msgstr "" #: src/help.py:131 msgid "" msgstr "" #: src/help.py:132 msgid "" msgstr "" #: src/help.py:133 msgid "" msgstr "" #: src/help.py:134 msgid "" msgstr "" #: src/help.py:135 msgid "" msgstr "" #: src/help.py:136 msgid "" msgstr "" #: src/help.py:137 msgid "" msgstr "" #: src/help.py:145 msgid "Alt" msgstr "" #: src/helpwin.py:45 msgid "PyTone Help" msgstr "Pomoc PyTone" #: src/inputwin.py:147 msgid "ok" msgstr "" #: src/inputwin.py:150 msgid "cancel" msgstr "anuluj" #: src/item.py:149 msgid "Tag" msgstr "" #: src/item.py:168 src/item.py:170 msgid "Rating" msgstr "Ocena" #: src/item.py:170 src/item.py:1214 msgid "Not rated" msgstr "" #: src/item.py:233 src/item.py:235 msgid "Filter:" msgstr "Filtr:" #: src/item.py:398 src/item.py:452 msgid "Title:" msgstr "Tytu³:" #: src/item.py:400 msgid "Nr:" msgstr "" #: src/item.py:404 src/item.py:453 src/item.py:664 msgid "Album:" msgstr "" #: src/item.py:406 src/item.py:496 msgid "URL:" msgstr "" #: src/item.py:409 src/item.py:460 msgid "Year:" msgstr "Rok:" #: src/item.py:414 src/item.py:454 src/item.py:615 src/item.py:663 #: src/item.py:754 msgid "Artist:" msgstr "Artysta:" #: src/item.py:418 src/item.py:460 src/playerwin.py:76 msgid "Time:" msgstr "Czas:" #: src/item.py:422 src/item.py:463 msgid "Tags:" msgstr "" #: src/item.py:438 src/item.py:509 msgid "Played:" msgstr "Grane:" #: src/item.py:439 msgid "#%d, %s ago" msgstr "%d raz, %s temu" #: src/item.py:442 src/item.py:463 src/item.py:1217 msgid "Rating:" msgstr "Ocena:" #: src/item.py:461 msgid "Track No:" msgstr "" #: src/item.py:462 msgid "Disk No:" msgstr "" #: src/item.py:482 #, fuzzy msgid "File type:" msgstr "Filtr:" #: src/item.py:482 msgid "Size:" msgstr "" #: src/item.py:485 msgid "track" msgstr "" #: src/item.py:489 #, fuzzy msgid "album" msgstr "Albumy" #: src/item.py:492 msgid "Beats per minute:" msgstr "" #: src/item.py:492 msgid "Replaygain:" msgstr "" #: src/item.py:493 #, fuzzy msgid "Times played:" msgstr "usuñ odegrane" #: src/item.py:493 #, fuzzy msgid "Times skipped:" msgstr "usuñ odegrane" #: src/item.py:494 msgid "Comment:" msgstr "" #: src/item.py:495 msgid "%d lines" msgstr "" #: src/item.py:495 msgid "Lyrics:" msgstr "" #: src/item.py:509 #, fuzzy msgid "%s ago" msgstr "%d raz, %s temu" #: src/item.py:612 src/item.py:659 #, fuzzy msgid "Various" msgstr "ró¿ni" #: src/item.py:704 src/playlistwin.py:40 src/playlistwin.py:172 msgid "Playlist" msgstr "Lista" #: src/item.py:727 msgid "Songs" msgstr "Utwory" #: src/item.py:768 #, fuzzy msgid "No artist" msgstr "Artysta:" #: src/item.py:783 msgid "Random song list" msgstr "Losowa lista utworów" #: src/item.py:806 msgid "Last played songs" msgstr "Ostatnio odtwarzane" #: src/item.py:832 msgid "Top played songs" msgstr "Najczê¶ciej odtwarzane" #: src/item.py:859 msgid "Last added songs" msgstr "Ostatnio dodane" #: src/item.py:885 msgid "Albums" msgstr "Albumy" #: src/item.py:909 msgid "Compilations" msgstr "" #: src/item.py:921 msgid "Tags" msgstr "" #: src/item.py:955 #, fuzzy msgid "Ratings" msgstr "Ocena:" #: src/item.py:982 src/item.py:989 src/item.py:999 msgid "Playlists" msgstr "Listy" #: src/item.py:1013 src/item.py:1019 src/item.py:1050 src/item.py:1055 msgid "Filesystem" msgstr "System plików" #: src/item.py:1076 msgid "Song Database" msgstr "Baza utworów" #: src/item.py:1127 #, fuzzy msgid "[Database: %s (%d)]" msgstr "Baza" #: src/item.py:1129 #, fuzzy msgid "%d databases (%d)" msgstr "%d Baza (%d utworów)" #: src/item.py:1154 msgid "%d artists" msgstr "" #: src/item.py:1156 #, fuzzy msgid "? artists" msgstr "Artysta:" #: src/item.py:1164 #, fuzzy msgid "Database (%s, %s)" msgstr "Baza (%s, %d utworów)" #: src/item.py:1166 #, fuzzy msgid "%d databases (%s)" msgstr "%d Baza (%d utworów)" #: src/item.py:1171 #, fuzzy msgid "[Database: %s (%%d)]" msgstr "Baza" #: src/item.py:1173 #, fuzzy msgid "%d databases (%%d)" msgstr "%d Baza (%d utworów)" #: src/item.py:1203 msgid "Tag:" msgstr "" #: src/iteminfowin.py:53 src/iteminfowin.py:74 msgid "MP3 Info" msgstr "" #: src/iteminfowin.py:67 #, fuzzy msgid "No song" msgstr "dodaj" #: src/iteminfowin.py:76 msgid "Ogg Info" msgstr "" #: src/iteminfowin.py:78 msgid "Song Info" msgstr "" #: src/iteminfowin.py:80 msgid "Directory Info" msgstr "Katalog" #: src/iteminfowin.py:82 #, fuzzy msgid "[Player: %s]" msgstr "Grane:" #: src/iteminfowin.py:174 msgid "Item info" msgstr "" #: src/logwin.py:32 msgid "PyTone Messages" msgstr "" #: src/lyricswin.py:32 src/lyricswin.py:55 msgid "Lyrics" msgstr "" #: src/lyricswin.py:34 src/lyricswin.py:62 msgid "No lyrics" msgstr "" #: src/lyricswin.py:56 #, fuzzy msgid "No song selected" msgstr "dodaj" #: src/mixerwin.py:35 src/mixerwin.py:49 msgid "initialized oss mixer: device %s, channel %s" msgstr "" #: src/mixerwin.py:147 src/mixerwin.py:163 src/mixerwin.py:176 #: src/mixerwin.py:188 msgid "Volume:" msgstr "G³o¶no¶æ:" #: src/mixerwin.py:170 msgid "Mixer" msgstr "Mikser" #: src/playerwin.py:46 src/playerwin.py:104 msgid "Playback Info" msgstr "Odtwarzane" #: src/playerwin.py:50 src/playerwin.py:120 msgid "error '%s' occured during write to playerinfofile" msgstr "" #: src/playerwin.py:90 msgid "paused" msgstr "wstrzymany" #: src/playlistwin.py:167 msgid "Repeat" msgstr "Powtarzaj" #: src/playlistwin.py:169 msgid "Random" msgstr "Losowo" #: src/pytone.py:67 msgid "PyTone %s startup" msgstr "" #: src/pytone.py:89 msgid "created PyTone directory ~/.pytone" msgstr "" #: src/pytone.py:165 msgid "Cannot load plugin '%s': %s" msgstr "" #: src/services/players/internal.py:116 msgid "cannot open audio device: error \"%s\"" msgstr "" #: src/services/players/internal.py:400 msgid "failed to open song \"%r\"" msgstr "" #: src/services/playlist.py:398 msgid "Save playlist" msgstr "Zapisz listê" #: src/services/playlist.py:399 msgid "Name:" msgstr "Nazwa:" #: src/services/songdbs/remote.py:54 msgid "database %s: type remote, location: %s" msgstr "" #: src/services/songdbs/sqlite.py:1085 msgid "database %r: scanning for songs in %r" msgstr "" #: src/services/songdbs/sqlite.py:1093 msgid "database %r: removing stale songs" msgstr "" #: src/services/songdbs/sqlite.py:1097 msgid "database %r: rescan finished" msgstr "" #: src/services/songdbs/sqlite.py:1101 #, fuzzy msgid "database %r: rescanning %d songs" msgstr "Baza (%s, %d utworów)" #: src/services/songdbs/sqlite.py:1104 msgid "database %r: finished rescanning %d songs" msgstr "" #: src/statswin.py:30 msgid "PyTone Statistics" msgstr "" #: src/statswin.py:44 #, fuzzy msgid "Database %s" msgstr "Baza" #: src/statswin.py:45 msgid "%d songs, %d albums, %d artists, %d tags" msgstr "" #: src/statswin.py:52 msgid "local database (db file: %s)" msgstr "" #: src/statswin.py:54 msgid "remote database (server: %s)" msgstr "" #: src/statswin.py:55 msgid "type" msgstr "" #: src/statswin.py:56 #, fuzzy msgid "base directory" msgstr "przejd¼ do katalogu nadrzêdnego" #: src/statswin.py:58 msgid "cache size" msgstr "" #: src/statswin.py:61 msgid "%d requests, %d / %d objects" msgstr "" #: src/statswin.py:65 msgid "Request cache size" msgstr "" #: src/statswin.py:71 msgid "Request cache stats" msgstr "" #: src/statswin.py:72 msgid "%d hits / %d requests" msgstr "" #~ msgid "load" #~ msgstr "wczytaj" #~ msgid "load playlist" #~ msgstr "wczytaj listê" #~ msgid "Unknown" #~ msgstr "Nieznany" #~ msgid "Decade" #~ msgstr "Dekada" #~ msgid "Genre" #~ msgstr "Gatunek" #~ msgid "Genre:" #~ msgstr "Gatunek:" #~ msgid "Genres" #~ msgstr "Gatunki" #~ msgid "Decades" #~ msgstr "Dekady" #~ msgid "%d databases (%d songs)" #~ msgstr "%d Baza (%d utworów)" #~ msgid "Load playlist" #~ msgstr "Wczytaj listê" #~ msgid "Database" #~ msgstr "Baza" #, fuzzy #~ msgid "Virtual database:" #~ msgstr "przejd¼ do bazy" PyTone-3.0.3/locale/it/LC_MESSAGES/000755 000765 000765 00000000000 11406223507 016703 5ustar00ringoringo000000 000000 PyTone-3.0.3/locale/it/LC_MESSAGES/._PyTone.mo000644 000765 000765 00000000122 11406223507 020666 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/locale/it/LC_MESSAGES/PyTone.mo000644 000765 000765 00000012452 11406223507 020462 0ustar00ringoringo000000 000000 Þ•_  "(09AH P[ct†Œ ˜ ¢®µÆ ÍÛ âðöü   %" &H o x  ¢ ¨ · Î Õ â !ð   ! : ? Y b h x } Œ £ &¨ Ï Þ ë   0 G ] x ‚ Œ ” š ¬ ³ ¸ × æ î þ    % 2 < D U g l }  ª ½ × ì     #.5<CKSgm v‰¡¸½ÁÊÓãë #:BJRgm2ƒ8¶ï!%DL`ˆ™.¹èí!% *K`f„ŠŸ»2Âõ'DZp‹¨ÃÖó#8A+Fr‘š¯ÂÈÚàï+FKfx— ©ÊâZV%A6YXL.M^_$]0[?=NE 9; 5 WBF/J@8-C H3OS'U:>)K4R*\#+(,1 G2DP"7Q&T!<I#%d, %s agoAlbumsArtist:FilesystemFilter:Last added songsLast played songsName:Nr:Played:PlaylistsPyTone HelpRandomRandom song listRepeatSave playlistSearchSong DatabaseSongsTime:Title:Top played songsYear:add diradd directory recursively to playlistadd random contents of dir to playlistadd songadd song to playlistadvance to next songclearclear playlistdecrease output volumedeletedelete entrydelete playeddelete played songs from playlistdownenter direnter selected directoryexitexit PyTone (press twice)exit dirfirstgo directory uphelpimmediate playincrease output volumelastmark all songs in playlist as unplayedmove song downmove song upmove to previous pagemove to the first entrymove to the last entrymove to the next entrymove to the next pagemove to the previous entrynext songpage downpage uppausepause main playerpausedplayplay selected song immediatelyrandom add dirrefreshrefresh displayreplay songssavesave playlistsearchsearch entryshow helpshuffleshuffle playliststart main playerstopstop main playerswitch to databaseswitch to database windowswitch to playlistswitch to playlist windowtoggle playlist modetoggle the playlist modeupvolume downvolume upProject-Id-Version: PyTone 2.0.8 POT-Creation-Date: 2006-09-17 20:44+CEST PO-Revision-Date: 2005-01-16 16:17+0100 Last-Translator: davide alessio MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit #%d, %s faAlbumArtista:FilesystemFiltro:Ultime canzoni aggiunteUltime canzoni suonateNomeN.:Suonata:PlaylistAiuto di PyToneCasualeLista di canzoni casualeRipetiSalva la playlistCercaDatabase delle canzoniCanzoniDurata:Titolo:Canzoni più suonateAnno:aggiungi la directoryaggiungi ricorsivamente la directory alla playlistaggiungi contenuto casuale della directory alla playlistaggiungi la canzoneaggiungi la canzone alla playlistavanza alla canzone successivapuliscipulisci la playlistdiminuisci il volume in uscitacancellacancella la vocerimuovi le canzoni già suonaterimuovi le canzoni già suonate dalla playlistgiùentra nella directoryentra nella direcotry selezionataescitermina PyTone (premi due volte)esci dalla directoryprimovai nella directory superioreaiutosuona immediatamenteaumenta il volume in uscitaultimosegna tutte le canzoni nella playlist come suonatesposta la canzone in giùsposta la canzone in supassa alla pagina precedentepassa alla prima vocepassa all'ultima vocepassa alla voce successivapassa alla pagina successivapassa alla voce precedentecanzone successivascorre in giù di una paginascorre in su di una paginapausemetti in pausa il player principalein pausaplaysuona la canzone selezionata immediatamenteaggiungi una directory casualeaggiornaridisegna lo schermorisuona le canzonisalvasalva la playlistcercacerca una vocemostra il file d'aiutomescola l'ordinemescola la playlistavvia il player principalestopferma il player principalepassa ai databasevai alla finestra del databasevai alle playlistvai alla finestra delle playlistcambia in modo playlistscambia le modalità playlistsudiminuisci il volumeaumenta il volumePyTone-3.0.3/locale/it/LC_MESSAGES/._PyTone.po000644 000765 000765 00000000122 10654123704 020673 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/locale/it/LC_MESSAGES/PyTone.po000644 000765 000765 00000041637 10654123704 020476 0ustar00ringoringo000000 000000 # Message catalog for PyTone # Copyright (C) 2003 Jörg Lehmann # Jörg Lehmann , 2003. # msgid "" msgstr "" "Project-Id-Version: PyTone 2.0.8\n" "POT-Creation-Date: 2006-09-17 20:44+CEST\n" "PO-Revision-Date: 2005-01-16 16:17+0100\n" "Last-Translator: davide alessio \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" #: src/filelist.py:113 msgid "Not rating virtual directories!" msgstr "" #: src/filelist.py:117 msgid "Rating %d song(s) with %d star(s)..." msgstr "" #: src/filelist.py:119 msgid "Removing rating of %d song(s)..." msgstr "" #: src/filelist.py:129 msgid "Not tagging virtual directories!" msgstr "" #: src/filelist.py:132 msgid "Tagging %d song(s) with tag '%s'..." msgstr "" #: src/filelist.py:142 msgid "Not untagging virtual directories!" msgstr "" #: src/filelist.py:145 msgid "Removing tag '%s' from %d song(s)..." msgstr "" #: src/filelist.py:156 msgid "Scanning for songs in database '%s'..." msgstr "" #: src/filelist.py:166 msgid "Rescanning %d song(s)..." msgstr "" #: src/filelistwin.py:167 msgid "Search" msgstr "Cerca" #: src/help.py:39 msgid "refresh" msgstr "aggiorna" #: src/help.py:39 msgid "refresh display" msgstr "ridisegna lo schermo" #: src/help.py:40 msgid "exit" msgstr "esci" #: src/help.py:40 msgid "exit PyTone (press twice)" msgstr "termina PyTone (premi due volte)" #: src/help.py:41 msgid "play" msgstr "play" #: src/help.py:41 msgid "start main player" msgstr "avvia il player principale" #: src/help.py:42 msgid "pause" msgstr "pause" #: src/help.py:42 msgid "pause main player" msgstr "metti in pausa il player principale" #: src/help.py:43 msgid "advance to next song" msgstr "avanza alla canzone successiva" #: src/help.py:43 msgid "next song" msgstr "canzone successiva" #: src/help.py:44 #, fuzzy msgid "go back to previous song" msgstr "passa alla pagina precedente" #: src/help.py:44 msgid "previous song" msgstr "" #: src/help.py:45 msgid "rewind" msgstr "" #: src/help.py:45 #, fuzzy msgid "rewind main player" msgstr "avvia il player principale" #: src/help.py:46 msgid "forward" msgstr "" #: src/help.py:46 #, fuzzy msgid "forward main player" msgstr "avvia il player principale" #: src/help.py:47 msgid "stop" msgstr "stop" #: src/help.py:47 msgid "stop main player" msgstr "ferma il player principale" #: src/help.py:49 msgid "delete played" msgstr "rimuovi le canzoni già suonate" #: src/help.py:49 msgid "delete played songs from playlist" msgstr "rimuovi le canzoni già suonate dalla playlist" #: src/help.py:50 msgid "mark all songs in playlist as unplayed" msgstr "segna tutte le canzoni nella playlist come suonate" #: src/help.py:50 msgid "replay songs" msgstr "risuona le canzoni" #: src/help.py:52 msgid "toggle playlist mode" msgstr "cambia in modo playlist" #: src/help.py:52 msgid "toggle the playlist mode" msgstr "scambia le modalità playlist" #: src/help.py:53 msgid "clear" msgstr "pulisci" #: src/help.py:53 msgid "clear playlist" msgstr "pulisci la playlist" #: src/help.py:54 msgid "save" msgstr "salva" #: src/help.py:54 msgid "save playlist" msgstr "salva la playlist" #: src/help.py:55 msgid "help" msgstr "aiuto" #: src/help.py:55 msgid "show help" msgstr "mostra il file d'aiuto" #: src/help.py:56 msgid "log" msgstr "" #: src/help.py:56 msgid "show log messages" msgstr "" #: src/help.py:57 msgid "show statistical information about database(s)" msgstr "" #: src/help.py:57 msgid "statistics" msgstr "" #: src/help.py:58 msgid "item info" msgstr "" #: src/help.py:58 msgid "show information about selected item" msgstr "" #: src/help.py:59 msgid "lyrics" msgstr "" #: src/help.py:59 msgid "show lyrics of selected song" msgstr "" #: src/help.py:60 msgid "toggle information shown in item info window" msgstr "" #: src/help.py:60 #, fuzzy msgid "toggle item info" msgstr "cambia in modo playlist" #: src/help.py:61 #, fuzzy msgid "toggle layout" msgstr "cambia in modo playlist" #: src/help.py:62 msgid "increase output volume" msgstr "aumenta il volume in uscita" #: src/help.py:62 msgid "volume up" msgstr "aumenta il volume" #: src/help.py:63 msgid "decrease output volume" msgstr "diminuisci il volume in uscita" #: src/help.py:63 msgid "volume down" msgstr "diminuisci il volume" #: src/help.py:64 #, fuzzy msgid "increase the play speed" msgstr "rimuovi le canzoni già suonate" #: src/help.py:64 msgid "play faster" msgstr "" #: src/help.py:65 #, fuzzy msgid "decrease the play speed" msgstr "rimuovi le canzoni già suonate" #: src/help.py:65 #, fuzzy msgid "play slower" msgstr "risuona le canzoni" #: src/help.py:66 #, fuzzy msgid "default play speed" msgstr "rimuovi le canzoni già suonate" #: src/help.py:66 msgid "reset the play speed to normal" msgstr "" #: src/help.py:67 msgid "rate current song 1" msgstr "" #: src/help.py:67 msgid "rate currently playing song with 1 star" msgstr "" #: src/help.py:68 msgid "rate current song 2" msgstr "" #: src/help.py:68 msgid "rate currently playing song with 2 stars" msgstr "" #: src/help.py:69 msgid "rate current song 3" msgstr "" #: src/help.py:69 msgid "rate currently playing song with 3 stars" msgstr "" #: src/help.py:70 msgid "rate current song 4" msgstr "" #: src/help.py:70 msgid "rate currently playing song with 4 stars" msgstr "" #: src/help.py:71 msgid "rate current song 5" msgstr "" #: src/help.py:71 msgid "rate currently playing song with 5 stars" msgstr "" #: src/help.py:74 src/help.py:93 msgid "down" msgstr "giù" #: src/help.py:74 src/help.py:93 msgid "move to the next entry" msgstr "passa alla voce successiva" #: src/help.py:75 src/help.py:94 msgid "move to the previous entry" msgstr "passa alla voce precedente" #: src/help.py:75 src/help.py:94 msgid "up" msgstr "su" #: src/help.py:76 src/help.py:95 msgid "move to the next page" msgstr "passa alla pagina successiva" #: src/help.py:76 src/help.py:95 msgid "page down" msgstr "scorre in giù di una pagina" #: src/help.py:77 src/help.py:96 msgid "move to previous page" msgstr "passa alla pagina precedente" #: src/help.py:77 src/help.py:96 msgid "page up" msgstr "scorre in su di una pagina" #: src/help.py:78 src/help.py:97 msgid "first" msgstr "primo" #: src/help.py:78 src/help.py:97 msgid "move to the first entry" msgstr "passa alla prima voce" #: src/help.py:79 src/help.py:98 msgid "last" msgstr "ultimo" #: src/help.py:79 src/help.py:98 msgid "move to the last entry" msgstr "passa all'ultima voce" #: src/help.py:80 msgid "enter dir" msgstr "entra nella directory" #: src/help.py:80 msgid "enter selected directory" msgstr "entra nella direcotry selezionata" #: src/help.py:81 msgid "exit dir" msgstr "esci dalla directory" #: src/help.py:81 msgid "go directory up" msgstr "vai nella directory superiore" #: src/help.py:82 msgid "add song" msgstr "aggiungi la canzone" #: src/help.py:82 msgid "add song to playlist" msgstr "aggiungi la canzone alla playlist" #: src/help.py:83 msgid "add dir" msgstr "aggiungi la directory" #: src/help.py:83 msgid "add directory recursively to playlist" msgstr "aggiungi ricorsivamente la directory alla playlist" #: src/help.py:84 src/help.py:103 msgid "immediate play" msgstr "suona immediatamente" #: src/help.py:84 src/help.py:103 msgid "play selected song immediately" msgstr "suona la canzone selezionata immediatamente" #: src/help.py:85 msgid "switch to playlist" msgstr "vai alle playlist" #: src/help.py:85 msgid "switch to playlist window" msgstr "vai alla finestra delle playlist" #: src/help.py:87 msgid "add random contents of dir to playlist" msgstr "aggiungi contenuto casuale della directory alla playlist" #: src/help.py:87 msgid "random add dir" msgstr "aggiungi una directory casuale" #: src/help.py:88 msgid "search" msgstr "cerca" #: src/help.py:88 msgid "search entry" msgstr "cerca una voce" #: src/help.py:89 msgid "repeat last search" msgstr "" #: src/help.py:89 #, fuzzy msgid "repeat search" msgstr "cerca" #: src/help.py:90 src/help.py:105 msgid "rescan" msgstr "" #: src/help.py:90 src/help.py:105 msgid "rescan/update id3 info for selection" msgstr "" #: src/help.py:99 msgid "move song up" msgstr "sposta la canzone in su" #: src/help.py:100 msgid "move song down" msgstr "sposta la canzone in giù" #: src/help.py:101 msgid "shuffle" msgstr "mescola l'ordine" #: src/help.py:101 msgid "shuffle playlist" msgstr "mescola la playlist" #: src/help.py:102 msgid "delete" msgstr "cancella" #: src/help.py:102 msgid "delete entry" msgstr "cancella la voce" #: src/help.py:104 msgid "switch to database" msgstr "passa ai database" #: src/help.py:104 msgid "switch to database window" msgstr "vai alla finestra del database" #: src/help.py:107 msgid "jump to selected" msgstr "" #: src/help.py:107 msgid "jump to selected song in filelist window" msgstr "" #: src/help.py:114 msgid "CTRL" msgstr "" #: src/help.py:120 msgid "" msgstr "" #: src/help.py:121 msgid "" msgstr "" #: src/help.py:122 msgid "" msgstr "" #: src/help.py:123 msgid "" msgstr "" #: src/help.py:124 msgid "" msgstr "" #: src/help.py:125 msgid "" msgstr "" #: src/help.py:126 msgid "" msgstr "" #: src/help.py:127 msgid "" msgstr "" #: src/help.py:128 msgid "" msgstr "" #: src/help.py:129 msgid "" msgstr "" #: src/help.py:130 msgid "" msgstr "" #: src/help.py:131 msgid "" msgstr "" #: src/help.py:132 msgid "" msgstr "" #: src/help.py:133 msgid "" msgstr "" #: src/help.py:134 msgid "" msgstr "" #: src/help.py:135 msgid "" msgstr "" #: src/help.py:136 msgid "" msgstr "" #: src/help.py:137 msgid "" msgstr "" #: src/help.py:145 msgid "Alt" msgstr "" #: src/helpwin.py:45 msgid "PyTone Help" msgstr "Aiuto di PyTone" #: src/inputwin.py:147 msgid "ok" msgstr "" #: src/inputwin.py:150 msgid "cancel" msgstr "" #: src/item.py:149 msgid "Tag" msgstr "" #: src/item.py:168 src/item.py:170 msgid "Rating" msgstr "" #: src/item.py:170 src/item.py:1214 msgid "Not rated" msgstr "" #: src/item.py:233 src/item.py:235 msgid "Filter:" msgstr "Filtro:" #: src/item.py:398 src/item.py:452 msgid "Title:" msgstr "Titolo:" #: src/item.py:400 msgid "Nr:" msgstr "N.:" #: src/item.py:404 src/item.py:453 src/item.py:664 msgid "Album:" msgstr "" #: src/item.py:406 src/item.py:496 msgid "URL:" msgstr "" #: src/item.py:409 src/item.py:460 msgid "Year:" msgstr "Anno:" #: src/item.py:414 src/item.py:454 src/item.py:615 src/item.py:663 #: src/item.py:754 msgid "Artist:" msgstr "Artista:" #: src/item.py:418 src/item.py:460 src/playerwin.py:76 msgid "Time:" msgstr "Durata:" #: src/item.py:422 src/item.py:463 msgid "Tags:" msgstr "" #: src/item.py:438 src/item.py:509 msgid "Played:" msgstr "Suonata:" #: src/item.py:439 msgid "#%d, %s ago" msgstr "#%d, %s fa" #: src/item.py:442 src/item.py:463 src/item.py:1217 msgid "Rating:" msgstr "" #: src/item.py:461 msgid "Track No:" msgstr "" #: src/item.py:462 msgid "Disk No:" msgstr "" #: src/item.py:482 #, fuzzy msgid "File type:" msgstr "Filtro:" #: src/item.py:482 msgid "Size:" msgstr "" #: src/item.py:485 msgid "track" msgstr "" #: src/item.py:489 #, fuzzy msgid "album" msgstr "Album" #: src/item.py:492 msgid "Beats per minute:" msgstr "" #: src/item.py:492 msgid "Replaygain:" msgstr "" #: src/item.py:493 #, fuzzy msgid "Times played:" msgstr "rimuovi le canzoni già suonate" #: src/item.py:493 #, fuzzy msgid "Times skipped:" msgstr "rimuovi le canzoni già suonate" #: src/item.py:494 msgid "Comment:" msgstr "" #: src/item.py:495 msgid "%d lines" msgstr "" #: src/item.py:495 msgid "Lyrics:" msgstr "" #: src/item.py:509 #, fuzzy msgid "%s ago" msgstr "#%d, %s fa" #: src/item.py:612 src/item.py:659 msgid "Various" msgstr "" #: src/item.py:704 src/playlistwin.py:40 src/playlistwin.py:172 msgid "Playlist" msgstr "" #: src/item.py:727 msgid "Songs" msgstr "Canzoni" #: src/item.py:768 #, fuzzy msgid "No artist" msgstr "Artista:" #: src/item.py:783 msgid "Random song list" msgstr "Lista di canzoni casuale" #: src/item.py:806 msgid "Last played songs" msgstr "Ultime canzoni suonate" #: src/item.py:832 msgid "Top played songs" msgstr "Canzoni più suonate" #: src/item.py:859 msgid "Last added songs" msgstr "Ultime canzoni aggiunte" #: src/item.py:885 msgid "Albums" msgstr "Album" #: src/item.py:909 msgid "Compilations" msgstr "" #: src/item.py:921 msgid "Tags" msgstr "" #: src/item.py:955 msgid "Ratings" msgstr "" #: src/item.py:982 src/item.py:989 src/item.py:999 msgid "Playlists" msgstr "Playlist" #: src/item.py:1013 src/item.py:1019 src/item.py:1050 src/item.py:1055 msgid "Filesystem" msgstr "Filesystem" #: src/item.py:1076 msgid "Song Database" msgstr "Database delle canzoni" #: src/item.py:1127 #, fuzzy msgid "[Database: %s (%d)]" msgstr "Database delle canzoni" #: src/item.py:1129 #, fuzzy msgid "%d databases (%d)" msgstr "%d databases (%d canzoni)" #: src/item.py:1154 msgid "%d artists" msgstr "" #: src/item.py:1156 #, fuzzy msgid "? artists" msgstr "Artista:" #: src/item.py:1164 #, fuzzy msgid "Database (%s, %s)" msgstr "Database (%s, %d canzoni)" #: src/item.py:1166 #, fuzzy msgid "%d databases (%s)" msgstr "%d databases (%d canzoni)" #: src/item.py:1171 #, fuzzy msgid "[Database: %s (%%d)]" msgstr "Database delle canzoni" #: src/item.py:1173 #, fuzzy msgid "%d databases (%%d)" msgstr "%d databases (%d canzoni)" #: src/item.py:1203 msgid "Tag:" msgstr "" #: src/iteminfowin.py:53 src/iteminfowin.py:74 msgid "MP3 Info" msgstr "" #: src/iteminfowin.py:67 #, fuzzy msgid "No song" msgstr "aggiungi la canzone" #: src/iteminfowin.py:76 msgid "Ogg Info" msgstr "" #: src/iteminfowin.py:78 msgid "Song Info" msgstr "" #: src/iteminfowin.py:80 msgid "Directory Info" msgstr "" #: src/iteminfowin.py:82 #, fuzzy msgid "[Player: %s]" msgstr "Suonata:" #: src/iteminfowin.py:174 msgid "Item info" msgstr "" #: src/logwin.py:32 msgid "PyTone Messages" msgstr "" #: src/lyricswin.py:32 src/lyricswin.py:55 msgid "Lyrics" msgstr "" #: src/lyricswin.py:34 src/lyricswin.py:62 msgid "No lyrics" msgstr "" #: src/lyricswin.py:56 #, fuzzy msgid "No song selected" msgstr "aggiungi la canzone" #: src/mixerwin.py:35 src/mixerwin.py:49 msgid "initialized oss mixer: device %s, channel %s" msgstr "" #: src/mixerwin.py:147 src/mixerwin.py:163 src/mixerwin.py:176 #: src/mixerwin.py:188 msgid "Volume:" msgstr "" #: src/mixerwin.py:170 msgid "Mixer" msgstr "" #: src/playerwin.py:46 src/playerwin.py:104 msgid "Playback Info" msgstr "" #: src/playerwin.py:50 src/playerwin.py:120 msgid "error '%s' occured during write to playerinfofile" msgstr "" #: src/playerwin.py:90 msgid "paused" msgstr "in pausa" #: src/playlistwin.py:167 msgid "Repeat" msgstr "Ripeti" #: src/playlistwin.py:169 msgid "Random" msgstr "Casuale" #: src/pytone.py:67 msgid "PyTone %s startup" msgstr "" #: src/pytone.py:89 msgid "created PyTone directory ~/.pytone" msgstr "" #: src/pytone.py:165 msgid "Cannot load plugin '%s': %s" msgstr "" #: src/services/players/internal.py:116 msgid "cannot open audio device: error \"%s\"" msgstr "" #: src/services/players/internal.py:400 msgid "failed to open song \"%r\"" msgstr "" #: src/services/playlist.py:398 msgid "Save playlist" msgstr "Salva la playlist" #: src/services/playlist.py:399 msgid "Name:" msgstr "Nome" #: src/services/songdbs/remote.py:54 msgid "database %s: type remote, location: %s" msgstr "" #: src/services/songdbs/sqlite.py:1085 msgid "database %r: scanning for songs in %r" msgstr "" #: src/services/songdbs/sqlite.py:1093 msgid "database %r: removing stale songs" msgstr "" #: src/services/songdbs/sqlite.py:1097 msgid "database %r: rescan finished" msgstr "" #: src/services/songdbs/sqlite.py:1101 #, fuzzy msgid "database %r: rescanning %d songs" msgstr "Database (%s, %d canzoni)" #: src/services/songdbs/sqlite.py:1104 msgid "database %r: finished rescanning %d songs" msgstr "" #: src/statswin.py:30 msgid "PyTone Statistics" msgstr "" #: src/statswin.py:44 #, fuzzy msgid "Database %s" msgstr "Database delle canzoni" #: src/statswin.py:45 msgid "%d songs, %d albums, %d artists, %d tags" msgstr "" #: src/statswin.py:52 msgid "local database (db file: %s)" msgstr "" #: src/statswin.py:54 msgid "remote database (server: %s)" msgstr "" #: src/statswin.py:55 msgid "type" msgstr "" #: src/statswin.py:56 #, fuzzy msgid "base directory" msgstr "vai nella directory superiore" #: src/statswin.py:58 msgid "cache size" msgstr "" #: src/statswin.py:61 msgid "%d requests, %d / %d objects" msgstr "" #: src/statswin.py:65 msgid "Request cache size" msgstr "" #: src/statswin.py:71 msgid "Request cache stats" msgstr "" #: src/statswin.py:72 msgid "%d hits / %d requests" msgstr "" #~ msgid "load" #~ msgstr "carica" #~ msgid "load playlist" #~ msgstr "carica una playlist" #~ msgid "Genre" #~ msgstr "Genere" #~ msgid "Genre:" #~ msgstr "Genere:" #~ msgid "Genres" #~ msgstr "Generi" #~ msgid "Decades" #~ msgstr "Decadi" #~ msgid "%d databases (%d songs)" #~ msgstr "%d databases (%d canzoni)" #~ msgid "Load playlist" #~ msgstr "Carica la playlist" #, fuzzy #~ msgid "Virtual database:" #~ msgstr "passa ai database" PyTone-3.0.3/locale/fr/LC_MESSAGES/000755 000765 000765 00000000000 11406223507 016676 5ustar00ringoringo000000 000000 PyTone-3.0.3/locale/fr/LC_MESSAGES/._PyTone.mo000644 000765 000765 00000000122 11406223507 020661 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/locale/fr/LC_MESSAGES/PyTone.mo000644 000765 000765 00000014316 11406223507 020456 0ustar00ringoringo000000 000000 Þ•{ô§Ìh i u  ‡ Ž ” š ¢ © ² ¹  Ê Ò Ø ß æ î ó    & 8 A G Q U ^ f o y … • œ ­ ´ ¼ Ä Ë Ù à î ô ú     %( &N u ~ “ ¨ ¯ µ Ä Û â ï !ý  $ . G L f o u Ž “ ¢ ¹ ¾ & é ø 3Jaw ’œ Ÿ©±·ÉÐÕ ô) < JW$^ƒ ˆ–  ª´ÆÎßñö4G ao„ ‡ “¢ µÁÇÍÓÙáèñú ")27Rfn†œ¯ ´¿ÂÕÛáè÷ .39 ?JYbry€‡¥¬"Á-ä"=Y`gw‰ &»âæÿ" CX`~ƒ–©±0µæ÷  $2 CQdu x†–!œ¾ÇÌèû 2FZ ny•›ª²ºÉÝåö2MhyŠ ¨­¾=^aQ:)IX{n&/Ppuf$2_+[k jgW 'JM%KDY63l9Er.;Ac,>?idB"mb1 h!4*S]z(\7Zv ` 0VUwFt8HLN< -5syexR@OGCqoT##%d, %s agoAlbum:AlbumsArtist:CTRLDirectory InfoFilesystemFilter:Last added songsLast played songsMP3 InfoName:Not ratedNr:Ogg InfoPlayed:PlaylistPlaylistsPyTone HelpPyTone MessagesRandomRandom song listRatingRating:RatingsRepeatSave playlistSearchSong DatabaseSongsTime:Title:Top played songsVolume:Year:add diradd directory recursively to playlistadd random contents of dir to playlistadd songadd song to playlistadvance to next songcancelclearclear playlistdecrease output volumedeletedelete entrydelete playeddelete played songs from playlistdownenter direnter selected directoryexitexit PyTone (press twice)exit dirfirstgo back to previous songhelpimmediate playincrease output volumelastlogmark all songs in playlist as unplayedmove song downmove song upmove to previous pagemove to the first entrymove to the last entrymove to the next entrymove to the next pagemove to the previous entrynext songokpage downpage uppausepause main playerpausedplayplay selected song immediatelyprevious songrandom add dirrefreshrefresh displayrepeat last searchrepeat searchreplay songsrescanrescan/update id3 info for selectionsavesave playlistsearchsearch entryshow helpshow log messagesshuffleshuffle playliststart main playerstopstop main playerswitch to databaseswitch to database windowswitch to playlistswitch to playlist windowtoggle layouttoggle playlist modeupvolume downvolume upProject-Id-Version: PyTone 2.1.0 POT-Creation-Date: 2006-09-17 20:44+CEST PO-Revision-Date: 2005-01-16 16:18+0100 Last-Translator: Nicolas Évrard MIME-Version: 1.0 Content-Type: text/plain; charset=ISO-8859-1 Content-Transfer-Encoding: 8bit %d fois, il y a %sAlbum:AlbumsArtiste:CtrlInformations du répertoireSystème de fichiersFiltre:Derniers titres ajoutésDerniers titres jouésInformations (mp3)Nom:Non cotées#:Informations (ogg)Joué:ListeListesAide de PyToneMessages de PyToneAu hasardListe de titres au hasardCoteCote:CotesRépétitionSauve la listeChercherBase de donnéesTitresDurée:Titre:Titres les plus jouésVolume:Année:Ajoute le répertoireAjoute récursivement le répertoireAjoute aléatoirement le contenu du répertoireAjoute le titreAjoute le titre à la listePasse à la chanson suivanteCancelEffaceEfface la listeDécroit le volumeEffaceEfface le titreEfface les chansons jouéesEfface les chansons jouées de la listeBasEntre dans le répertoireEntre dans le répertoireQuitterQuitter PyTone (appuyer deux fois)Quitte le répertoirePremierPasse à la chanson précédenteAideJoue immédiatementAugmente le volumeDernierLogMarque toute les chansons de la liste non-jouéesDescend le titreMonte le titrePage précédentePremier titreDernier titreChanson suivantePage suivanteChanson précédenteChanson suivanteOkPage SuivantePage précédentePauseMet le lecteur principal en pauseEn pausePlayJoue immédiatement le titreChanson précédenteAjoute aléatoirementRafraîchirRafraîchir l'affichageRépète la rechercheRépète la rechercheRejoue des chansonsMet à jourMet à jour les informationsSauveSauve la listeChercheChercheAffiche l'aideMontre les messagesMélangeMélange la listeLance le lecteur principalStopStoppe le lecteur principalPasse à la base de donnéesPasse à la base de donnéesPasse à la listePasse à la listeChange l'affichageChange de HautBaisse le volumeMonte le volumePyTone-3.0.3/locale/fr/LC_MESSAGES/._PyTone.po000644 000765 000765 00000000122 10654123704 020666 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/locale/fr/LC_MESSAGES/PyTone.po000644 000765 000765 00000042143 10654123704 020462 0ustar00ringoringo000000 000000 # Message catalog for PyTone # Copyright (C) 2004 Nicolas Évrard # Nicolas Évrard , 2004. # msgid "" msgstr "" "Project-Id-Version: PyTone 2.1.0\n" "POT-Creation-Date: 2006-09-17 20:44+CEST\n" "PO-Revision-Date: 2005-01-16 16:18+0100\n" "Last-Translator: Nicolas Évrard \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=ISO-8859-1\n" "Content-Transfer-Encoding: 8bit\n" #: src/filelist.py:113 msgid "Not rating virtual directories!" msgstr "" #: src/filelist.py:117 msgid "Rating %d song(s) with %d star(s)..." msgstr "" #: src/filelist.py:119 msgid "Removing rating of %d song(s)..." msgstr "" #: src/filelist.py:129 msgid "Not tagging virtual directories!" msgstr "" #: src/filelist.py:132 msgid "Tagging %d song(s) with tag '%s'..." msgstr "" #: src/filelist.py:142 msgid "Not untagging virtual directories!" msgstr "" #: src/filelist.py:145 msgid "Removing tag '%s' from %d song(s)..." msgstr "" #: src/filelist.py:156 msgid "Scanning for songs in database '%s'..." msgstr "" #: src/filelist.py:166 msgid "Rescanning %d song(s)..." msgstr "" #: src/filelistwin.py:167 msgid "Search" msgstr "Chercher" #: src/help.py:39 msgid "refresh" msgstr "Rafraîchir" #: src/help.py:39 msgid "refresh display" msgstr "Rafraîchir l'affichage" #: src/help.py:40 msgid "exit" msgstr "Quitter" #: src/help.py:40 msgid "exit PyTone (press twice)" msgstr "Quitter PyTone (appuyer deux fois)" #: src/help.py:41 msgid "play" msgstr "Play" #: src/help.py:41 msgid "start main player" msgstr "Lance le lecteur principal" #: src/help.py:42 msgid "pause" msgstr "Pause" #: src/help.py:42 msgid "pause main player" msgstr "Met le lecteur principal en pause" #: src/help.py:43 msgid "advance to next song" msgstr "Passe à la chanson suivante" #: src/help.py:43 msgid "next song" msgstr "Chanson suivante" #: src/help.py:44 msgid "go back to previous song" msgstr "Passe à la chanson précédente" #: src/help.py:44 msgid "previous song" msgstr "Chanson précédente" #: src/help.py:45 msgid "rewind" msgstr "" #: src/help.py:45 #, fuzzy msgid "rewind main player" msgstr "Lance le lecteur principal" #: src/help.py:46 msgid "forward" msgstr "" #: src/help.py:46 #, fuzzy msgid "forward main player" msgstr "Lance le lecteur principal" #: src/help.py:47 msgid "stop" msgstr "Stop" #: src/help.py:47 msgid "stop main player" msgstr "Stoppe le lecteur principal" #: src/help.py:49 msgid "delete played" msgstr "Efface les chansons jouées" #: src/help.py:49 msgid "delete played songs from playlist" msgstr "Efface les chansons jouées de la liste" #: src/help.py:50 msgid "mark all songs in playlist as unplayed" msgstr "Marque toute les chansons de la liste non-jouées" #: src/help.py:50 msgid "replay songs" msgstr "Rejoue des chansons" #: src/help.py:52 msgid "toggle playlist mode" msgstr "Change de " #: src/help.py:52 msgid "toggle the playlist mode" msgstr "" #: src/help.py:53 msgid "clear" msgstr "Efface" #: src/help.py:53 msgid "clear playlist" msgstr "Efface la liste" #: src/help.py:54 msgid "save" msgstr "Sauve" #: src/help.py:54 msgid "save playlist" msgstr "Sauve la liste" #: src/help.py:55 msgid "help" msgstr "Aide" #: src/help.py:55 msgid "show help" msgstr "Affiche l'aide" #: src/help.py:56 msgid "log" msgstr "Log" #: src/help.py:56 msgid "show log messages" msgstr "Montre les messages" #: src/help.py:57 msgid "show statistical information about database(s)" msgstr "" #: src/help.py:57 msgid "statistics" msgstr "" #: src/help.py:58 msgid "item info" msgstr "" #: src/help.py:58 msgid "show information about selected item" msgstr "" #: src/help.py:59 msgid "lyrics" msgstr "" #: src/help.py:59 msgid "show lyrics of selected song" msgstr "" #: src/help.py:60 msgid "toggle information shown in item info window" msgstr "" #: src/help.py:60 #, fuzzy msgid "toggle item info" msgstr "Change de " #: src/help.py:61 msgid "toggle layout" msgstr "Change l'affichage" #: src/help.py:62 msgid "increase output volume" msgstr "Augmente le volume" #: src/help.py:62 msgid "volume up" msgstr "Monte le volume" #: src/help.py:63 msgid "decrease output volume" msgstr "Décroit le volume" #: src/help.py:63 msgid "volume down" msgstr "Baisse le volume" #: src/help.py:64 #, fuzzy msgid "increase the play speed" msgstr "Efface les chansons jouées" #: src/help.py:64 msgid "play faster" msgstr "" #: src/help.py:65 #, fuzzy msgid "decrease the play speed" msgstr "Efface les chansons jouées" #: src/help.py:65 #, fuzzy msgid "play slower" msgstr "Rejoue des chansons" #: src/help.py:66 #, fuzzy msgid "default play speed" msgstr "Efface les chansons jouées" #: src/help.py:66 msgid "reset the play speed to normal" msgstr "" #: src/help.py:67 msgid "rate current song 1" msgstr "" #: src/help.py:67 msgid "rate currently playing song with 1 star" msgstr "" #: src/help.py:68 msgid "rate current song 2" msgstr "" #: src/help.py:68 msgid "rate currently playing song with 2 stars" msgstr "" #: src/help.py:69 msgid "rate current song 3" msgstr "" #: src/help.py:69 msgid "rate currently playing song with 3 stars" msgstr "" #: src/help.py:70 msgid "rate current song 4" msgstr "" #: src/help.py:70 msgid "rate currently playing song with 4 stars" msgstr "" #: src/help.py:71 msgid "rate current song 5" msgstr "" #: src/help.py:71 msgid "rate currently playing song with 5 stars" msgstr "" #: src/help.py:74 src/help.py:93 msgid "down" msgstr "Bas" #: src/help.py:74 src/help.py:93 msgid "move to the next entry" msgstr "Chanson suivante" #: src/help.py:75 src/help.py:94 msgid "move to the previous entry" msgstr "Chanson précédente" #: src/help.py:75 src/help.py:94 msgid "up" msgstr "Haut" #: src/help.py:76 src/help.py:95 msgid "move to the next page" msgstr "Page suivante" #: src/help.py:76 src/help.py:95 msgid "page down" msgstr "Page Suivante" #: src/help.py:77 src/help.py:96 msgid "move to previous page" msgstr "Page précédente" #: src/help.py:77 src/help.py:96 msgid "page up" msgstr "Page précédente" #: src/help.py:78 src/help.py:97 msgid "first" msgstr "Premier" #: src/help.py:78 src/help.py:97 msgid "move to the first entry" msgstr "Premier titre" #: src/help.py:79 src/help.py:98 msgid "last" msgstr "Dernier" #: src/help.py:79 src/help.py:98 msgid "move to the last entry" msgstr "Dernier titre" #: src/help.py:80 msgid "enter dir" msgstr "Entre dans le répertoire" #: src/help.py:80 msgid "enter selected directory" msgstr "Entre dans le répertoire" #: src/help.py:81 msgid "exit dir" msgstr "Quitte le répertoire" #: src/help.py:81 msgid "go directory up" msgstr "" #: src/help.py:82 msgid "add song" msgstr "Ajoute le titre" #: src/help.py:82 msgid "add song to playlist" msgstr "Ajoute le titre à la liste" #: src/help.py:83 msgid "add dir" msgstr "Ajoute le répertoire" #: src/help.py:83 msgid "add directory recursively to playlist" msgstr "Ajoute récursivement le répertoire" #: src/help.py:84 src/help.py:103 msgid "immediate play" msgstr "Joue immédiatement" #: src/help.py:84 src/help.py:103 msgid "play selected song immediately" msgstr "Joue immédiatement le titre" #: src/help.py:85 msgid "switch to playlist" msgstr "Passe à la liste" #: src/help.py:85 msgid "switch to playlist window" msgstr "Passe à la liste" #: src/help.py:87 msgid "add random contents of dir to playlist" msgstr "Ajoute aléatoirement le contenu du répertoire" #: src/help.py:87 msgid "random add dir" msgstr "Ajoute aléatoirement" #: src/help.py:88 msgid "search" msgstr "Cherche" #: src/help.py:88 msgid "search entry" msgstr "Cherche" #: src/help.py:89 msgid "repeat last search" msgstr "Répète la recherche" #: src/help.py:89 msgid "repeat search" msgstr "Répète la recherche" #: src/help.py:90 src/help.py:105 msgid "rescan" msgstr "Met à jour" #: src/help.py:90 src/help.py:105 msgid "rescan/update id3 info for selection" msgstr "Met à jour les informations" #: src/help.py:99 msgid "move song up" msgstr "Monte le titre" #: src/help.py:100 msgid "move song down" msgstr "Descend le titre" #: src/help.py:101 msgid "shuffle" msgstr "Mélange" #: src/help.py:101 msgid "shuffle playlist" msgstr "Mélange la liste" #: src/help.py:102 msgid "delete" msgstr "Efface" #: src/help.py:102 msgid "delete entry" msgstr "Efface le titre" #: src/help.py:104 msgid "switch to database" msgstr "Passe à la base de données" #: src/help.py:104 msgid "switch to database window" msgstr "Passe à la base de données" #: src/help.py:107 msgid "jump to selected" msgstr "" #: src/help.py:107 msgid "jump to selected song in filelist window" msgstr "" #: src/help.py:114 msgid "CTRL" msgstr "Ctrl" #: src/help.py:120 msgid "" msgstr "" #: src/help.py:121 msgid "" msgstr "" #: src/help.py:122 msgid "" msgstr "" #: src/help.py:123 msgid "" msgstr "" #: src/help.py:124 msgid "" msgstr "" #: src/help.py:125 msgid "" msgstr "" #: src/help.py:126 msgid "" msgstr "" #: src/help.py:127 msgid "" msgstr "" #: src/help.py:128 msgid "" msgstr "" #: src/help.py:129 msgid "" msgstr "" #: src/help.py:130 msgid "" msgstr "" #: src/help.py:131 msgid "" msgstr "" #: src/help.py:132 #, fuzzy msgid "" msgstr "" #: src/help.py:133 msgid "" msgstr "" #: src/help.py:134 msgid "" msgstr "" #: src/help.py:135 msgid "" msgstr "" #: src/help.py:136 #, fuzzy msgid "" msgstr "" #: src/help.py:137 msgid "" msgstr "" #: src/help.py:145 msgid "Alt" msgstr "" #: src/helpwin.py:45 msgid "PyTone Help" msgstr "Aide de PyTone" #: src/inputwin.py:147 msgid "ok" msgstr "Ok" #: src/inputwin.py:150 msgid "cancel" msgstr "Cancel" #: src/item.py:149 msgid "Tag" msgstr "" #: src/item.py:168 src/item.py:170 msgid "Rating" msgstr "Cote" #: src/item.py:170 src/item.py:1214 msgid "Not rated" msgstr "Non cotées" #: src/item.py:233 src/item.py:235 msgid "Filter:" msgstr "Filtre:" #: src/item.py:398 src/item.py:452 msgid "Title:" msgstr "Titre:" #: src/item.py:400 msgid "Nr:" msgstr "#:" #: src/item.py:404 src/item.py:453 src/item.py:664 msgid "Album:" msgstr "Album:" #: src/item.py:406 src/item.py:496 msgid "URL:" msgstr "" #: src/item.py:409 src/item.py:460 msgid "Year:" msgstr "Année:" #: src/item.py:414 src/item.py:454 src/item.py:615 src/item.py:663 #: src/item.py:754 msgid "Artist:" msgstr "Artiste:" #: src/item.py:418 src/item.py:460 src/playerwin.py:76 msgid "Time:" msgstr "Durée:" #: src/item.py:422 src/item.py:463 msgid "Tags:" msgstr "" #: src/item.py:438 src/item.py:509 msgid "Played:" msgstr "Joué:" #: src/item.py:439 msgid "#%d, %s ago" msgstr "%d fois, il y a %s" #: src/item.py:442 src/item.py:463 src/item.py:1217 msgid "Rating:" msgstr "Cote:" #: src/item.py:461 msgid "Track No:" msgstr "" #: src/item.py:462 msgid "Disk No:" msgstr "" #: src/item.py:482 #, fuzzy msgid "File type:" msgstr "Filtre:" #: src/item.py:482 msgid "Size:" msgstr "" #: src/item.py:485 msgid "track" msgstr "" #: src/item.py:489 #, fuzzy msgid "album" msgstr "Album:" #: src/item.py:492 msgid "Beats per minute:" msgstr "" #: src/item.py:492 msgid "Replaygain:" msgstr "" #: src/item.py:493 #, fuzzy msgid "Times played:" msgstr "Efface les chansons jouées" #: src/item.py:493 #, fuzzy msgid "Times skipped:" msgstr "Efface les chansons jouées" #: src/item.py:494 msgid "Comment:" msgstr "" #: src/item.py:495 msgid "%d lines" msgstr "" #: src/item.py:495 msgid "Lyrics:" msgstr "" #: src/item.py:509 #, fuzzy msgid "%s ago" msgstr "%d fois, il y a %s" #: src/item.py:612 src/item.py:659 #, fuzzy msgid "Various" msgstr "Varié" #: src/item.py:704 src/playlistwin.py:40 src/playlistwin.py:172 msgid "Playlist" msgstr "Liste" #: src/item.py:727 msgid "Songs" msgstr "Titres" #: src/item.py:768 #, fuzzy msgid "No artist" msgstr "Artiste:" #: src/item.py:783 msgid "Random song list" msgstr "Liste de titres au hasard" #: src/item.py:806 msgid "Last played songs" msgstr "Derniers titres joués" #: src/item.py:832 msgid "Top played songs" msgstr "Titres les plus joués" #: src/item.py:859 msgid "Last added songs" msgstr "Derniers titres ajoutés" #: src/item.py:885 msgid "Albums" msgstr "Albums" #: src/item.py:909 msgid "Compilations" msgstr "" #: src/item.py:921 msgid "Tags" msgstr "" #: src/item.py:955 msgid "Ratings" msgstr "Cotes" #: src/item.py:982 src/item.py:989 src/item.py:999 msgid "Playlists" msgstr "Listes" #: src/item.py:1013 src/item.py:1019 src/item.py:1050 src/item.py:1055 msgid "Filesystem" msgstr "Système de fichiers" #: src/item.py:1076 msgid "Song Database" msgstr "Base de données" #: src/item.py:1127 #, fuzzy msgid "[Database: %s (%d)]" msgstr "Base de données" #: src/item.py:1129 #, fuzzy msgid "%d databases (%d)" msgstr "%d bases de données (%d titres)" #: src/item.py:1154 msgid "%d artists" msgstr "" #: src/item.py:1156 #, fuzzy msgid "? artists" msgstr "Artiste:" #: src/item.py:1164 #, fuzzy msgid "Database (%s, %s)" msgstr "Base de données (%s, %d titres)" #: src/item.py:1166 #, fuzzy msgid "%d databases (%s)" msgstr "%d bases de données (%d titres)" #: src/item.py:1171 #, fuzzy msgid "[Database: %s (%%d)]" msgstr "Base de données" #: src/item.py:1173 #, fuzzy msgid "%d databases (%%d)" msgstr "%d bases de données (%d titres)" #: src/item.py:1203 msgid "Tag:" msgstr "" #: src/iteminfowin.py:53 src/iteminfowin.py:74 msgid "MP3 Info" msgstr "Informations (mp3)" #: src/iteminfowin.py:67 #, fuzzy msgid "No song" msgstr "Ajoute le titre" #: src/iteminfowin.py:76 msgid "Ogg Info" msgstr "Informations (ogg)" #: src/iteminfowin.py:78 #, fuzzy msgid "Song Info" msgstr "Informations (ogg)" #: src/iteminfowin.py:80 msgid "Directory Info" msgstr "Informations du répertoire" #: src/iteminfowin.py:82 #, fuzzy msgid "[Player: %s]" msgstr "Joué:" #: src/iteminfowin.py:174 msgid "Item info" msgstr "" #: src/logwin.py:32 msgid "PyTone Messages" msgstr "Messages de PyTone" #: src/lyricswin.py:32 src/lyricswin.py:55 msgid "Lyrics" msgstr "" #: src/lyricswin.py:34 src/lyricswin.py:62 msgid "No lyrics" msgstr "" #: src/lyricswin.py:56 #, fuzzy msgid "No song selected" msgstr "Ajoute le titre" #: src/mixerwin.py:35 src/mixerwin.py:49 msgid "initialized oss mixer: device %s, channel %s" msgstr "" #: src/mixerwin.py:147 src/mixerwin.py:163 src/mixerwin.py:176 #: src/mixerwin.py:188 msgid "Volume:" msgstr "Volume:" #: src/mixerwin.py:170 msgid "Mixer" msgstr "" #: src/playerwin.py:46 src/playerwin.py:104 msgid "Playback Info" msgstr "" #: src/playerwin.py:50 src/playerwin.py:120 msgid "error '%s' occured during write to playerinfofile" msgstr "" #: src/playerwin.py:90 msgid "paused" msgstr "En pause" #: src/playlistwin.py:167 msgid "Repeat" msgstr "Répétition" #: src/playlistwin.py:169 msgid "Random" msgstr "Au hasard" #: src/pytone.py:67 #, fuzzy msgid "PyTone %s startup" msgstr "Lancement de PyTone" #: src/pytone.py:89 msgid "created PyTone directory ~/.pytone" msgstr "" #: src/pytone.py:165 msgid "Cannot load plugin '%s': %s" msgstr "" #: src/services/players/internal.py:116 msgid "cannot open audio device: error \"%s\"" msgstr "" #: src/services/players/internal.py:400 msgid "failed to open song \"%r\"" msgstr "" #: src/services/playlist.py:398 msgid "Save playlist" msgstr "Sauve la liste" #: src/services/playlist.py:399 msgid "Name:" msgstr "Nom:" #: src/services/songdbs/remote.py:54 msgid "database %s: type remote, location: %s" msgstr "" #: src/services/songdbs/sqlite.py:1085 msgid "database %r: scanning for songs in %r" msgstr "" #: src/services/songdbs/sqlite.py:1093 msgid "database %r: removing stale songs" msgstr "" #: src/services/songdbs/sqlite.py:1097 msgid "database %r: rescan finished" msgstr "" #: src/services/songdbs/sqlite.py:1101 #, fuzzy msgid "database %r: rescanning %d songs" msgstr "Base de données (%s, %d titres)" #: src/services/songdbs/sqlite.py:1104 msgid "database %r: finished rescanning %d songs" msgstr "" #: src/statswin.py:30 #, fuzzy msgid "PyTone Statistics" msgstr "Lancement de PyTone" #: src/statswin.py:44 #, fuzzy msgid "Database %s" msgstr "Base de données" #: src/statswin.py:45 msgid "%d songs, %d albums, %d artists, %d tags" msgstr "" #: src/statswin.py:52 msgid "local database (db file: %s)" msgstr "" #: src/statswin.py:54 msgid "remote database (server: %s)" msgstr "" #: src/statswin.py:55 msgid "type" msgstr "" #: src/statswin.py:56 #, fuzzy msgid "base directory" msgstr "Entre dans le répertoire" #: src/statswin.py:58 msgid "cache size" msgstr "" #: src/statswin.py:61 msgid "%d requests, %d / %d objects" msgstr "" #: src/statswin.py:65 msgid "Request cache size" msgstr "" #: src/statswin.py:71 msgid "Request cache stats" msgstr "" #: src/statswin.py:72 msgid "%d hits / %d requests" msgstr "" #~ msgid "load" #~ msgstr "Charge" #~ msgid "load playlist" #~ msgstr "Charge la liste" #~ msgid "Unknown" #~ msgstr "Inconnu" #~ msgid "Decade" #~ msgstr "Décade:" #~ msgid "Genre" #~ msgstr "Genre" #~ msgid "Genre:" #~ msgstr "Genre:" #~ msgid "Genres" #~ msgstr "Genres" #~ msgid "Decades" #~ msgstr "Décades" #~ msgid "%d databases (%d songs)" #~ msgstr "%d bases de données (%d titres)" #~ msgid "Load playlist" #~ msgstr "Charge la liste" #~ msgid "ALT" #~ msgstr "Alt" #~ msgid "Database" #~ msgstr "Base de données" #, fuzzy #~ msgid "Virtual database:" #~ msgstr "Passe à la base de données" PyTone-3.0.3/locale/de/LC_MESSAGES/000755 000765 000765 00000000000 11406223507 016657 5ustar00ringoringo000000 000000 PyTone-3.0.3/locale/de/LC_MESSAGES/._PyTone.mo000644 000765 000765 00000000122 11406223507 020642 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/locale/de/LC_MESSAGES/PyTone.mo000644 000765 000765 00000017770 11406223507 020446 0ustar00ringoringo000000 000000 Þ•œÁ ! - C ` g s y  †  — ž ¥ ­ ² ¾ Í Ø à ñ     * . 7 E M V ` r ~ Ž   § ¸ ¿ Ç Ï Ö é ý    *0 6DK\d jw%&¥ÌÕêÿ $ E"T&wžµ ¼ É!× ù1NSmv|©¹¾Í,ä&A P]s‹¢¹Ï êô ÷ !(- LZiqž ± ¿$Ìñ   +$5Z.l›£´ ÆÑÖçú',An ¢» À Ì4Ö -JQX_fmu|ƒ ‰” ™¦ ¶ÂÊæÿ  /3 < J T ^i y†—¨± Ç Ñ Üèìÿ* 0>GNTel ‚Ž ”¡,²@ß 0Mg x ƒ+¹$Ë!ð/ 7C%[  ;°ìô#*Eav|‹.¤ÓØ1Ü  #=Unˆ  »É ÌÚêð +; UaµÆ)Ý %/C IW/f–©ÅÍ Þ ìöû  2<"Y|œ«"ÃæêñW ? bBH2,^†34~AŒ=!Z`7€_T69Vgo}cI$jQES{n)Fw8X‡O\R-5zitlƒaŠ'f+@JdK*mMsv.;h1q%Pˆ‰ C>‚ e:|„yp<[("0G ‹YDk ru]&…x/ŽULN##%d, %s ago%d hits / %d requests%d requests, %d / %d objects%s agoAlbum:AlbumsArtist:CTRLDatabase %sDirectory InfoFilesystemFilter:Last added songsLast played songsMP3 InfoMixerName:No songNot ratedNr:Ogg InfoPlayback InfoPlayed:PlaylistPlaylistsPyTone %s startupPyTone HelpPyTone MessagesPyTone StatisticsRandomRandom song listRatingRating:RatingsRepeatRequest cache sizeRequest cache statsSave playlistSearchSong DatabaseSong InfoSongsTime:Times played:Title:Top played songsVolume:Year:[Player: %s]add diradd directory recursively to playlistadd random contents of dir to playlistadd songadd song to playlistadvance to next songbase directorycache sizecancelcannot open audio device: error "%s"clear playlistcreated PyTone directory ~/.pytonedatabase %s: type remote, location: %sdecrease output volumedeletedelete entrydelete playeddelete played songs from playlistenter direnter selected directoryerror '%s' occured during write to playerinfofileexitexit PyTone (press twice)exit dirfirstforward main playergo back to previous songgo directory uphelpimmediate playincrease output volumeinitialized oss mixer: device %s, channel %slastlogmark all songs in playlist as unplayedmove song downmove song upmove to previous pagemove to the first entrymove to the last entrymove to the next entrymove to the next pagemove to the previous entrynext songokpage downpage uppausepause main playerpausedplayplay selected song immediatelyprevious songrandom add dirrefreshrefresh displayremote database (server: %s)repeat last searchrepeat searchreplay songsrescan/update id3 info for selectionrewind main playersavesave playlistsearchsearch entryshow helpshow information about selected itemshow log messagesshow statistical information about database(s)shuffleshuffle playliststart main playerstatisticsstopstop main playerswitch to databaseswitch to database windowswitch to playlistswitch to playlist windowtoggle information shown in item info windowtoggle item infotoggle layouttoggle playlist modetoggle the playlist modetypevolume downvolume upProject-Id-Version: PyTone 2.0.8 POT-Creation-Date: 2006-09-17 20:44+CEST PO-Revision-Date: 2005-08-03 15:26+0200 Last-Translator: Jörg Lehmann Language-Team: german MIME-Version: 1.0 Content-Type: text/plain; charset=ISO-8859-1 Content-Transfer-Encoding: 8bit Generated-By: pygettext.py 1.4 #%d, vor %s%d Hits / %d Anfragen%d Anfragen, %d / %d Objektevor %sAlbum:AlbenInterpret:StrgDatenbank %sVerzeichnisinfoDateisystemFilter:Zuletzt hinzugefügte LiederZuletzt gespielte LiederMP3-InfoMischerName:Kein LiedNicht bewertetNr:Ogg-InfoPlayback-InfoGespielt:PlaylistePlaylistenPyTone-%s-StartPyTone-HilfePyTone-MeldungenPyTone-StatistikZufälligZufällige LiedauswahlBewertungBewertung:BewertungenWdhAnfragencachegrößeAnfragencachestatistikSpeichere PlaylisteSucheLieddatenbankLiedinfoLiederZeit:Anzahl gespielt:Titel:Meistgespielte LiederLautstärke:Jahr:[Player: %s]Füge Verz. hinzuFüge Verzeichnis rekursiv zu Playliste hinzuFüge zufällige Auswahl des Verzeichnisinhalts zu Playliste hinzuFüge Lied hinzuFüge Lied zu Playliste hinzuSpringe zum nächsten LiedBasisverzeichnisCachegrößeAbbrechenKann Audio-Device nicht öffnen: Fehler "%s"Lösche Playliste.PyTone-Verzeichnis ~/.pytone erzeugtDatenbank %s: Typ remote, Ort: %sErniedrige AusgabelautstärkeLöschenLösche LiedLösche gespielte LiederLösche gespielte Lieder aus PlaylisteBetrete Verz.Gehe in ausgewähltes VerzeichnisFehler '%s' während Schreiben in playerinfofile aufgetretenBeendenBeende PyTone (zweimal drücken)Verlasse Verz.AnfangVorspulen des HauptplayersSpringe zum vorherigen LiedVerlasse VerzeichnisHilfesofort spielenErhöhe AusgabelautstärkeOSS-Mischer initialisiert: Device %s, Kanal %sEndeLogMarkiere alle Lieder der Playliste als ungespieltNach untenNach obenGehe zur vorherigen SeiteGehe zum ersten EintragGehe zum letzten EintragGehe zum nächsten EintragGehe zur nächsten SeiteGehe zu vorherigen EintragNächstes LiedOkNächste SeiteVorherige SeitePauseHalte Player anPausePlaySpiele ausgewähltes Lied sofortVorheriges LiedFüge zufällig Verz. hinzuAuffrischenErzeuge Bildschirmanzeige neuRemote-Datenbank (Server: %s)Wiederhole letzte Suchewiederhole SucheSpiele Lieder nochmalsLese ID3-Tags der ausgewählten Lieder neuZurückspulen des HauptplayersSpeichernSpeichere PlaylisteSucheSuche EintragZeige Hilfe anZeige Informationen zum ausgewähltem Element anZeige Meldungen anZeige Datenbankstatistik anmischenMische PlaylisteStarte PlayerStatistikStopHalte Player anDatenbankWechsele zu DatenbankfensterPlaylisteSchalte zu PlaylistenfensterSchalte Infofensteranzeigemodus umWechsle InfofensteranzeigemodusWechsle LayoutWechsle PlaylistenmodusSchalte zwischen Playlistenmodi umTypLeiserLauterPyTone-3.0.3/locale/de/LC_MESSAGES/._PyTone.po000644 000765 000765 00000000122 10654123704 020647 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/locale/de/LC_MESSAGES/PyTone.po000644 000765 000765 00000044401 10654123704 020442 0ustar00ringoringo000000 000000 # Messagekatalog für PyTone # Copyright (C) 2002 Jörg Lehmann # Jörg Lehmann , 2002. # msgid "" msgstr "" "Project-Id-Version: PyTone 2.0.8\n" "POT-Creation-Date: 2006-09-17 20:44+CEST\n" "PO-Revision-Date: 2005-08-03 15:26+0200\n" "Last-Translator: Jörg Lehmann \n" "Language-Team: german\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=ISO-8859-1\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.4\n" #: src/filelist.py:113 msgid "Not rating virtual directories!" msgstr "" #: src/filelist.py:117 msgid "Rating %d song(s) with %d star(s)..." msgstr "" #: src/filelist.py:119 msgid "Removing rating of %d song(s)..." msgstr "" #: src/filelist.py:129 msgid "Not tagging virtual directories!" msgstr "" #: src/filelist.py:132 msgid "Tagging %d song(s) with tag '%s'..." msgstr "" #: src/filelist.py:142 msgid "Not untagging virtual directories!" msgstr "" #: src/filelist.py:145 msgid "Removing tag '%s' from %d song(s)..." msgstr "" #: src/filelist.py:156 #, fuzzy msgid "Scanning for songs in database '%s'..." msgstr "Datenbank %s: suche nach Liedern in %s" #: src/filelist.py:166 #, fuzzy msgid "Rescanning %d song(s)..." msgstr "Datenbank %s: lese %d Lieder neu" #: src/filelistwin.py:167 msgid "Search" msgstr "Suche" #: src/help.py:39 msgid "refresh" msgstr "Auffrischen" #: src/help.py:39 msgid "refresh display" msgstr "Erzeuge Bildschirmanzeige neu" #: src/help.py:40 msgid "exit" msgstr "Beenden" #: src/help.py:40 msgid "exit PyTone (press twice)" msgstr "Beende PyTone (zweimal drücken)" #: src/help.py:41 msgid "play" msgstr "Play" #: src/help.py:41 msgid "start main player" msgstr "Starte Player" #: src/help.py:42 msgid "pause" msgstr "Pause" #: src/help.py:42 msgid "pause main player" msgstr "Halte Player an" #: src/help.py:43 msgid "advance to next song" msgstr "Springe zum nächsten Lied" #: src/help.py:43 msgid "next song" msgstr "Nächstes Lied" #: src/help.py:44 msgid "go back to previous song" msgstr "Springe zum vorherigen Lied" #: src/help.py:44 msgid "previous song" msgstr "Vorheriges Lied" #: src/help.py:45 msgid "rewind" msgstr "" #: src/help.py:45 msgid "rewind main player" msgstr "Zurückspulen des Hauptplayers" #: src/help.py:46 msgid "forward" msgstr "" #: src/help.py:46 msgid "forward main player" msgstr "Vorspulen des Hauptplayers" #: src/help.py:47 msgid "stop" msgstr "Stop" #: src/help.py:47 msgid "stop main player" msgstr "Halte Player an" #: src/help.py:49 msgid "delete played" msgstr "Lösche gespielte Lieder" #: src/help.py:49 msgid "delete played songs from playlist" msgstr "Lösche gespielte Lieder aus Playliste" #: src/help.py:50 msgid "mark all songs in playlist as unplayed" msgstr "Markiere alle Lieder der Playliste als ungespielt" #: src/help.py:50 msgid "replay songs" msgstr "Spiele Lieder nochmals" #: src/help.py:52 msgid "toggle playlist mode" msgstr "Wechsle Playlistenmodus" #: src/help.py:52 msgid "toggle the playlist mode" msgstr "Schalte zwischen Playlistenmodi um" #: src/help.py:53 msgid "clear" msgstr "" #: src/help.py:53 msgid "clear playlist" msgstr "Lösche Playliste." #: src/help.py:54 msgid "save" msgstr "Speichern" #: src/help.py:54 msgid "save playlist" msgstr "Speichere Playliste" #: src/help.py:55 msgid "help" msgstr "Hilfe" #: src/help.py:55 msgid "show help" msgstr "Zeige Hilfe an" #: src/help.py:56 msgid "log" msgstr "Log" #: src/help.py:56 msgid "show log messages" msgstr "Zeige Meldungen an" #: src/help.py:57 msgid "show statistical information about database(s)" msgstr "Zeige Datenbankstatistik an" #: src/help.py:57 msgid "statistics" msgstr "Statistik" #: src/help.py:58 msgid "item info" msgstr "" #: src/help.py:58 msgid "show information about selected item" msgstr "Zeige Informationen zum ausgewähltem Element an" #: src/help.py:59 msgid "lyrics" msgstr "" #: src/help.py:59 msgid "show lyrics of selected song" msgstr "" #: src/help.py:60 msgid "toggle information shown in item info window" msgstr "Schalte Infofensteranzeigemodus um" #: src/help.py:60 msgid "toggle item info" msgstr "Wechsle Infofensteranzeigemodus" #: src/help.py:61 msgid "toggle layout" msgstr "Wechsle Layout" #: src/help.py:62 msgid "increase output volume" msgstr "Erhöhe Ausgabelautstärke" #: src/help.py:62 msgid "volume up" msgstr "Lauter" #: src/help.py:63 msgid "decrease output volume" msgstr "Erniedrige Ausgabelautstärke" #: src/help.py:63 msgid "volume down" msgstr "Leiser" #: src/help.py:64 #, fuzzy msgid "increase the play speed" msgstr "Lösche gespielte Lieder" #: src/help.py:64 msgid "play faster" msgstr "" #: src/help.py:65 #, fuzzy msgid "decrease the play speed" msgstr "Lösche gespielte Lieder" #: src/help.py:65 #, fuzzy msgid "play slower" msgstr "Spiele Lieder nochmals" #: src/help.py:66 #, fuzzy msgid "default play speed" msgstr "Lösche gespielte Lieder" #: src/help.py:66 msgid "reset the play speed to normal" msgstr "" #: src/help.py:67 msgid "rate current song 1" msgstr "" #: src/help.py:67 msgid "rate currently playing song with 1 star" msgstr "" #: src/help.py:68 msgid "rate current song 2" msgstr "" #: src/help.py:68 msgid "rate currently playing song with 2 stars" msgstr "" #: src/help.py:69 msgid "rate current song 3" msgstr "" #: src/help.py:69 msgid "rate currently playing song with 3 stars" msgstr "" #: src/help.py:70 msgid "rate current song 4" msgstr "" #: src/help.py:70 msgid "rate currently playing song with 4 stars" msgstr "" #: src/help.py:71 msgid "rate current song 5" msgstr "" #: src/help.py:71 msgid "rate currently playing song with 5 stars" msgstr "" #: src/help.py:74 src/help.py:93 msgid "down" msgstr "" #: src/help.py:74 src/help.py:93 msgid "move to the next entry" msgstr "Gehe zum nächsten Eintrag" #: src/help.py:75 src/help.py:94 msgid "move to the previous entry" msgstr "Gehe zu vorherigen Eintrag" #: src/help.py:75 src/help.py:94 msgid "up" msgstr "" #: src/help.py:76 src/help.py:95 msgid "move to the next page" msgstr "Gehe zur nächsten Seite" #: src/help.py:76 src/help.py:95 msgid "page down" msgstr "Nächste Seite" #: src/help.py:77 src/help.py:96 msgid "move to previous page" msgstr "Gehe zur vorherigen Seite" #: src/help.py:77 src/help.py:96 msgid "page up" msgstr "Vorherige Seite" #: src/help.py:78 src/help.py:97 msgid "first" msgstr "Anfang" #: src/help.py:78 src/help.py:97 msgid "move to the first entry" msgstr "Gehe zum ersten Eintrag" #: src/help.py:79 src/help.py:98 msgid "last" msgstr "Ende" #: src/help.py:79 src/help.py:98 msgid "move to the last entry" msgstr "Gehe zum letzten Eintrag" #: src/help.py:80 msgid "enter dir" msgstr "Betrete Verz." #: src/help.py:80 msgid "enter selected directory" msgstr "Gehe in ausgewähltes Verzeichnis" #: src/help.py:81 msgid "exit dir" msgstr "Verlasse Verz." #: src/help.py:81 msgid "go directory up" msgstr "Verlasse Verzeichnis" #: src/help.py:82 msgid "add song" msgstr "Füge Lied hinzu" #: src/help.py:82 msgid "add song to playlist" msgstr "Füge Lied zu Playliste hinzu" #: src/help.py:83 msgid "add dir" msgstr "Füge Verz. hinzu" #: src/help.py:83 msgid "add directory recursively to playlist" msgstr "Füge Verzeichnis rekursiv zu Playliste hinzu" #: src/help.py:84 src/help.py:103 msgid "immediate play" msgstr "sofort spielen" #: src/help.py:84 src/help.py:103 msgid "play selected song immediately" msgstr "Spiele ausgewähltes Lied sofort" #: src/help.py:85 msgid "switch to playlist" msgstr "Playliste" #: src/help.py:85 msgid "switch to playlist window" msgstr "Schalte zu Playlistenfenster" #: src/help.py:87 msgid "add random contents of dir to playlist" msgstr "Füge zufällige Auswahl des Verzeichnisinhalts zu Playliste hinzu" #: src/help.py:87 msgid "random add dir" msgstr "Füge zufällig Verz. hinzu" #: src/help.py:88 msgid "search" msgstr "Suche" #: src/help.py:88 msgid "search entry" msgstr "Suche Eintrag" #: src/help.py:89 msgid "repeat last search" msgstr "Wiederhole letzte Suche" #: src/help.py:89 msgid "repeat search" msgstr "wiederhole Suche" #: src/help.py:90 src/help.py:105 msgid "rescan" msgstr "" #: src/help.py:90 src/help.py:105 msgid "rescan/update id3 info for selection" msgstr "Lese ID3-Tags der ausgewählten Lieder neu" #: src/help.py:99 msgid "move song up" msgstr "Nach oben" #: src/help.py:100 msgid "move song down" msgstr "Nach unten" #: src/help.py:101 msgid "shuffle" msgstr "mischen" #: src/help.py:101 msgid "shuffle playlist" msgstr "Mische Playliste" #: src/help.py:102 msgid "delete" msgstr "Löschen" #: src/help.py:102 msgid "delete entry" msgstr "Lösche Lied" #: src/help.py:104 msgid "switch to database" msgstr "Datenbank" #: src/help.py:104 msgid "switch to database window" msgstr "Wechsele zu Datenbankfenster" #: src/help.py:107 msgid "jump to selected" msgstr "" #: src/help.py:107 msgid "jump to selected song in filelist window" msgstr "" #: src/help.py:114 msgid "CTRL" msgstr "Strg" #: src/help.py:120 msgid "" msgstr "" #: src/help.py:121 msgid "" msgstr "" #: src/help.py:122 msgid "" msgstr "" #: src/help.py:123 msgid "" msgstr "" #: src/help.py:124 msgid "" msgstr "" #: src/help.py:125 msgid "" msgstr "" #: src/help.py:126 msgid "" msgstr "" #: src/help.py:127 msgid "" msgstr "" #: src/help.py:128 msgid "" msgstr "" #: src/help.py:129 msgid "" msgstr "" #: src/help.py:130 msgid "" msgstr "" #: src/help.py:131 msgid "" msgstr "" #: src/help.py:132 msgid "" msgstr "" #: src/help.py:133 msgid "" msgstr "" #: src/help.py:134 msgid "" msgstr "" #: src/help.py:135 msgid "" msgstr "" #: src/help.py:136 msgid "" msgstr "" #: src/help.py:137 msgid "" msgstr "" #: src/help.py:145 msgid "Alt" msgstr "" #: src/helpwin.py:45 msgid "PyTone Help" msgstr "PyTone-Hilfe" #: src/inputwin.py:147 msgid "ok" msgstr "Ok" #: src/inputwin.py:150 msgid "cancel" msgstr "Abbrechen" #: src/item.py:149 msgid "Tag" msgstr "" #: src/item.py:168 src/item.py:170 msgid "Rating" msgstr "Bewertung" #: src/item.py:170 src/item.py:1214 msgid "Not rated" msgstr "Nicht bewertet" #: src/item.py:233 src/item.py:235 msgid "Filter:" msgstr "Filter:" #: src/item.py:398 src/item.py:452 msgid "Title:" msgstr "Titel:" #: src/item.py:400 msgid "Nr:" msgstr "Nr:" #: src/item.py:404 src/item.py:453 src/item.py:664 msgid "Album:" msgstr "Album:" #: src/item.py:406 src/item.py:496 msgid "URL:" msgstr "" #: src/item.py:409 src/item.py:460 msgid "Year:" msgstr "Jahr:" #: src/item.py:414 src/item.py:454 src/item.py:615 src/item.py:663 #: src/item.py:754 msgid "Artist:" msgstr "Interpret:" #: src/item.py:418 src/item.py:460 src/playerwin.py:76 msgid "Time:" msgstr "Zeit:" #: src/item.py:422 src/item.py:463 msgid "Tags:" msgstr "" #: src/item.py:438 src/item.py:509 msgid "Played:" msgstr "Gespielt:" #: src/item.py:439 msgid "#%d, %s ago" msgstr "#%d, vor %s" #: src/item.py:442 src/item.py:463 src/item.py:1217 msgid "Rating:" msgstr "Bewertung:" #: src/item.py:461 msgid "Track No:" msgstr "" #: src/item.py:462 msgid "Disk No:" msgstr "" #: src/item.py:482 #, fuzzy msgid "File type:" msgstr "Filter:" #: src/item.py:482 msgid "Size:" msgstr "" #: src/item.py:485 msgid "track" msgstr "" #: src/item.py:489 #, fuzzy msgid "album" msgstr "Album:" #: src/item.py:492 msgid "Beats per minute:" msgstr "" #: src/item.py:492 msgid "Replaygain:" msgstr "" #: src/item.py:493 msgid "Times played:" msgstr "Anzahl gespielt:" #: src/item.py:493 #, fuzzy msgid "Times skipped:" msgstr "Anzahl gespielt:" #: src/item.py:494 msgid "Comment:" msgstr "" #: src/item.py:495 msgid "%d lines" msgstr "" #: src/item.py:495 msgid "Lyrics:" msgstr "" #: src/item.py:509 msgid "%s ago" msgstr "vor %s" #: src/item.py:612 src/item.py:659 #, fuzzy msgid "Various" msgstr "Verschiedene" #: src/item.py:704 src/playlistwin.py:40 src/playlistwin.py:172 msgid "Playlist" msgstr "Playliste" #: src/item.py:727 msgid "Songs" msgstr "Lieder" #: src/item.py:768 #, fuzzy msgid "No artist" msgstr "Interpret:" #: src/item.py:783 msgid "Random song list" msgstr "Zufällige Liedauswahl" #: src/item.py:806 msgid "Last played songs" msgstr "Zuletzt gespielte Lieder" #: src/item.py:832 msgid "Top played songs" msgstr "Meistgespielte Lieder" #: src/item.py:859 msgid "Last added songs" msgstr "Zuletzt hinzugefügte Lieder" #: src/item.py:885 msgid "Albums" msgstr "Alben" #: src/item.py:909 msgid "Compilations" msgstr "" #: src/item.py:921 msgid "Tags" msgstr "" #: src/item.py:955 msgid "Ratings" msgstr "Bewertungen" #: src/item.py:982 src/item.py:989 src/item.py:999 msgid "Playlists" msgstr "Playlisten" #: src/item.py:1013 src/item.py:1019 src/item.py:1050 src/item.py:1055 msgid "Filesystem" msgstr "Dateisystem" #: src/item.py:1076 msgid "Song Database" msgstr "Lieddatenbank" #: src/item.py:1127 #, fuzzy msgid "[Database: %s (%d)]" msgstr "Datenbank %s" #: src/item.py:1129 #, fuzzy msgid "%d databases (%d)" msgstr "%d Datenbanken (%s Lieder)" #: src/item.py:1154 msgid "%d artists" msgstr "" #: src/item.py:1156 #, fuzzy msgid "? artists" msgstr "Statistik" #: src/item.py:1164 #, fuzzy msgid "Database (%s, %s)" msgstr "Datenbank (%s, %s Lieder)" #: src/item.py:1166 #, fuzzy msgid "%d databases (%s)" msgstr "%d Datenbanken (%s Lieder)" #: src/item.py:1171 #, fuzzy msgid "[Database: %s (%%d)]" msgstr "Datenbank %s" #: src/item.py:1173 #, fuzzy msgid "%d databases (%%d)" msgstr "%d Datenbanken (%s Lieder)" #: src/item.py:1203 msgid "Tag:" msgstr "" #: src/iteminfowin.py:53 src/iteminfowin.py:74 msgid "MP3 Info" msgstr "MP3-Info" #: src/iteminfowin.py:67 msgid "No song" msgstr "Kein Lied" #: src/iteminfowin.py:76 msgid "Ogg Info" msgstr "Ogg-Info" #: src/iteminfowin.py:78 msgid "Song Info" msgstr "Liedinfo" #: src/iteminfowin.py:80 msgid "Directory Info" msgstr "Verzeichnisinfo" #: src/iteminfowin.py:82 msgid "[Player: %s]" msgstr "[Player: %s]" #: src/iteminfowin.py:174 msgid "Item info" msgstr "" #: src/logwin.py:32 msgid "PyTone Messages" msgstr "PyTone-Meldungen" #: src/lyricswin.py:32 src/lyricswin.py:55 msgid "Lyrics" msgstr "" #: src/lyricswin.py:34 src/lyricswin.py:62 msgid "No lyrics" msgstr "" #: src/lyricswin.py:56 #, fuzzy msgid "No song selected" msgstr "Kein Lied" #: src/mixerwin.py:35 src/mixerwin.py:49 msgid "initialized oss mixer: device %s, channel %s" msgstr "OSS-Mischer initialisiert: Device %s, Kanal %s" #: src/mixerwin.py:147 src/mixerwin.py:163 src/mixerwin.py:176 #: src/mixerwin.py:188 msgid "Volume:" msgstr "Lautstärke:" #: src/mixerwin.py:170 msgid "Mixer" msgstr "Mischer" #: src/playerwin.py:46 src/playerwin.py:104 msgid "Playback Info" msgstr "Playback-Info" #: src/playerwin.py:50 src/playerwin.py:120 msgid "error '%s' occured during write to playerinfofile" msgstr "Fehler '%s' während Schreiben in playerinfofile aufgetreten" #: src/playerwin.py:90 msgid "paused" msgstr "Pause" #: src/playlistwin.py:167 msgid "Repeat" msgstr "Wdh" # Zufällig #: src/playlistwin.py:169 msgid "Random" msgstr "Zufällig" #: src/pytone.py:67 msgid "PyTone %s startup" msgstr "PyTone-%s-Start" #: src/pytone.py:89 msgid "created PyTone directory ~/.pytone" msgstr "PyTone-Verzeichnis ~/.pytone erzeugt" #: src/pytone.py:165 msgid "Cannot load plugin '%s': %s" msgstr "" #: src/services/players/internal.py:116 msgid "cannot open audio device: error \"%s\"" msgstr "Kann Audio-Device nicht öffnen: Fehler \"%s\"" #: src/services/players/internal.py:400 #, fuzzy msgid "failed to open song \"%r\"" msgstr "Kann Lied \"%s\" nicht öffnen" #: src/services/playlist.py:398 msgid "Save playlist" msgstr "Speichere Playliste" #: src/services/playlist.py:399 msgid "Name:" msgstr "Name:" #: src/services/songdbs/remote.py:54 msgid "database %s: type remote, location: %s" msgstr "Datenbank %s: Typ remote, Ort: %s" #: src/services/songdbs/sqlite.py:1085 #, fuzzy msgid "database %r: scanning for songs in %r" msgstr "Datenbank %s: suche nach Liedern in %s" #: src/services/songdbs/sqlite.py:1093 #, fuzzy msgid "database %r: removing stale songs" msgstr "Datenbank %s: lese %d Lieder neu" #: src/services/songdbs/sqlite.py:1097 #, fuzzy msgid "database %r: rescan finished" msgstr "Datenbank %s: lese %d Lieder neu" #: src/services/songdbs/sqlite.py:1101 #, fuzzy msgid "database %r: rescanning %d songs" msgstr "Datenbank %s: lese %d Lieder neu" #: src/services/songdbs/sqlite.py:1104 #, fuzzy msgid "database %r: finished rescanning %d songs" msgstr "Datenbank %s: %d Lieder neugelesen" #: src/statswin.py:30 msgid "PyTone Statistics" msgstr "PyTone-Statistik" #: src/statswin.py:44 msgid "Database %s" msgstr "Datenbank %s" #: src/statswin.py:45 #, fuzzy msgid "%d songs, %d albums, %d artists, %d tags" msgstr "%d Lieder, %d Alben, %d Künstler, %d Genre, %d Jahrzehnte" #: src/statswin.py:52 #, fuzzy msgid "local database (db file: %s)" msgstr "Lokale Datenbank (DBEnv-Verzeichnis: %s)" #: src/statswin.py:54 msgid "remote database (server: %s)" msgstr "Remote-Datenbank (Server: %s)" #: src/statswin.py:55 msgid "type" msgstr "Typ" #: src/statswin.py:56 msgid "base directory" msgstr "Basisverzeichnis" #: src/statswin.py:58 msgid "cache size" msgstr "Cachegröße" #: src/statswin.py:61 msgid "%d requests, %d / %d objects" msgstr "%d Anfragen, %d / %d Objekte" #: src/statswin.py:65 msgid "Request cache size" msgstr "Anfragencachegröße" #: src/statswin.py:71 msgid "Request cache stats" msgstr "Anfragencachestatistik" #: src/statswin.py:72 msgid "%d hits / %d requests" msgstr "%d Hits / %d Anfragen" #~ msgid "load" #~ msgstr "Laden" #~ msgid "load playlist" #~ msgstr "Lade Playliste" #~ msgid "Decade" #~ msgstr "Jahrzehnt" #~ msgid "Genre" #~ msgstr "Genre" #~ msgid "Genre:" #~ msgstr "Genre:" #~ msgid "Genres" #~ msgstr "Genres" #~ msgid "Decades" #~ msgstr "Jahrzehnte" #~ msgid "%d databases (%d songs)" #~ msgstr "%d Datenbanken (%s Lieder)" #~ msgid "Load playlist" #~ msgstr "Lade Playliste " #~ msgid "" #~ "database %s: basedir %s, %d songs, %d artists, %d albums, %d genres, %d " #~ "playlists" #~ msgstr "" #~ "Datenbank %s: Basisverzeichnis %s, %d Lieder, %d Künstler, %d Alben, %d " #~ "Genres, %d Playlisten" #~ msgid "database %s: finished scanning for songs in %s" #~ msgstr "Datenbank %s: Liedsuche in %s fertig" #~ msgid "Database" #~ msgstr "Datenbank" #~ msgid "Virtual database:" #~ msgstr "Virtuelle Datenbank:" #~ msgid "entries" #~ msgstr "Einträge" #~ msgid "generate random song list" #~ msgstr "Erzeuge zufällige Liedauswahl" #~ msgid "random suggestion" #~ msgstr "Zufälliger Liedvorschlag" PyTone-3.0.3/conf/._layout.compact000644 000765 000765 00000000122 10266220576 017273 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/conf/layout.compact000644 000765 000765 00000000431 10266220576 017061 0ustar00ringoringo000000 000000 [filelistwindow] border = compact [playlistwindow] border = compact [iteminfowindow] border = compact [playerwindow] border = compact [colors.filelistwindow] title = color green activeborder = color green [colors.playlistwindow] title = color green activeborder = color green PyTone-3.0.3/conf/._layout.ultracompact000644 000765 000765 00000000122 10266220576 020343 0ustar00ringoringo000000 000000 Mac OS X  2 R@PyTone-3.0.3/conf/layout.ultracompact000644 000765 000765 00000000455 10266220576 020137 0ustar00ringoringo000000 000000 [filelistwindow] border = ultracompact [playlistwindow] border = ultracompact [iteminfowindow] border = ultracompact [playerwindow] border = ultracompact [colors.filelistwindow] title = color green activeborder = color green [colors.playlistwindow] title = color green activeborder = color green PyTone-3.0.3/conf/pytonerc000644 000765 000765 00000055634 11250772161 015775 0ustar00ringoringo000000 000000 # PyTone configuration file ############################################################################## # general configuration settings ############################################################################## [general] # randominsertlength: # # Total length of songs in seconds after which the random song insert # algorithm stops. The following means, that you get at least one # hour of your favourite music upon pressing "r" when a folder # is selected. :-) randominsertlength = 3600 # logfile: Name of the file, where the played songs are logged. # # Set this to an empty value to disable this feature logfile = ~/.pytone/pytone.log # songchangecommand: command executed in shell when the playback of a new song starts # # Set this to an empty value to disable this feature. String interpolation like for # the songformat option (see playlistwindow section below with the exception that # playstarthours & Co cannot be used here) is performed. songchangecommand = # use the following (or a variant thereof) if you have installed the notiy-send binary # songchangecommand = notify-send PyTone "%(artist)s - %(title)s (%(length)s)" # use the following (or a variant thereof) if you have installed the osd_cat binary # songchangecommand = echo "%(artist)s - %(title)s (%(length)s)" | osd_cat -p middle -A center -f "-adobe-helvetica-bold-r-normal-*-*-320-*-*-p-*-iso8859-1" -l 1 - # playerinfofile: Name of the file, where information on the song currently # being played on the main player is recorded # # Set this to an empty value to disable this feature playerinfofile = ~/.pytone/playerinfo # dumpfile: Name of PyTone dump file # # PyTone can try to save it's state in a dump file upon a crash. # During the following restart, PyTone then tries to reconstruct the # playlist. # Set this to an empty value to disable this feature dumpfile = ~/.pytone/pytone.dump # debugfile: Name of PyTone deug file # # You can specify a file to which PyTone outputs some # debugging information # Set this to an empty value to disable this feature debugfile = # colorsupport: either auto, on or off # # Enable terminal colors. If you set this to auto, PyTone tries # to check automatically whether your terminal supports colors, # which not always works reliably. colorsupport = auto # mousesupport: either on or off # # Enable mouse support (if available in terminal). mousesupport = on # layout: onecolumn or twocolumn # # Layout of main PyTone windows layout = twocolumn # throttleoutput: limit screen updates # # Set this to an integer larger than zero, if your terminal # is slow and you experience problems with the sound output (skips). # This setting specifies, how many screen updates are being skipped # if there is still user input. throttleoutput = 0 # autoplaymode: initial play mode of playlist: off, repeat, random # # off: stop playing, when playlist is empty # repeat: repeat playlist, when end of playlist has been reached # random: choose a random song for playing when playlist is empty # # During runtime, this setting can be changed by pressing a key # specified in [keybindings.general] playlisttoggleautoplaymode (see below). autoplaymode = off # plugins: whitespace separated list of PyTone plugins to be loaded # # Plugins may have a config section [plugin.pluginname], where pluginname is # the name of the plugin. plugins = ############################################################################## # database configuration ############################################################################## # The song database stores all relevant informations of your songs. [database] # requestcachesize: number of objects stored in the database request cache # # In between the various databases and the rest of PyTone sits a database # management layer which dispatches, merges and caches the requests to # the various song databases. The maximal size of its cache (in number of # objects) can be configured via this option. The optimal setting depends # on the amount of RAM you have available. Note that there are also # cachesize settings for the local bsddb databases. requestcachesize = 50000 # For each song database, there has to be a corresponding [database.name] # section, where name is the name used to identify the database. # type: type of database: "local", "remote", "off" # the different database types have different options, summarized in the following # type = local: local bsddb database # # dbfile: Name of the database file # Note that every database must have its own database database file! # musicbasedir: basedir for filesystem based navigation and auto # registering of songs in database. # !!! You have to adjust this setting !!! # tracknrandtitlere Regular expression used for obtaining track nr and title # from the song filename. # postprocessors: List of metadata postprocessors to be used. By default the following # postprocesors are used: # - capitalize: Capitalize title, album and artist # - strip_leading_article: strip leading "The " from artists names # - add_decace_tag: Add decade tag automatically # autoregisterer: start song and playlist autoregisterer? # If yes, PyTone tries to find new songs and playlists # in musicbasedir when it is started. # You may want to disable the automatic song registering # after you have once populated your database. Then, you # register new songs using the -r (--rebuild) command # line option. Alternatively, you can press "u" to update # a selected directory in the filelist window. # playingstatslength: how many songs show PyTone take into account # for the lists of last and top played songs # cachesize: size of cache in kBytes used for database # (only available when using Python 2.3 and above) # # # type = remote: remote song database # # networklocation: either server_address:port or socket filename of remote database # type = off: this database is not used [database.main] type = local dbfile = ~/.pytone/main.db musicbasedir = tracknrandtitlere = ^\[?(\d+)\]? ?[- ] ?(.*)\.(mp3|ogg)$ postprocessors = capitalize strip_leading_article add_decade_tag autoregisterer = on playingstatslength = 100 cachesize = 1000 #[database.secondary] #type = remote #networklocation = localhost:1972 #networklocation = ~/.pytone/pytonectl ############################################################################## # mixer configuration ############################################################################## [mixer] # set sevice to "" to turn the mixer off device = /dev/mixer # mixer names (see the oss module for all possibilities) # for instance: # control PCM output level... channel = SOUND_MIXER_PCM # ... or control master output level # channel = SOUND_MIXER_VOLUME # step size (in percent) used when turning volume up and down stepsize = 5 ############################################################################## # network configuration ############################################################################## [network] # if socketfile is a non-empty string, it is opened as a UNIX domain socket # for the remote control of pytone socketfile = ~/.pytone/pytonectl # additionally, PyTone can repond to connections made via client connected by # a network. If you want PyTone to do this, set this config option to a true value # Note that you should only do this on a TRUSTED NETWORK! enableserver = false # port on which server listens for connections (if enableserver is true) port = 1972 # here you can specify the domain name on which the server will listen # (if enableserver is true) bind = localhost ############################################################################## # player configuration ############################################################################## # common option: # # autoplay: enable automatic start of playing, if songs are in playlist # type: type of player: "internal", "xmms", "mpg123", "remote", or "off" # the different players have different options, summarized in the following # type = internal: internal player (output with libao) # # options: # driver: "alsa", "alsa09", "alsa05", "arts", "esd", "oss", "sun", "macosx", "macosxau" or "pulse" # device: path of dsp used for output (for oss, sun and alsa drivers) # bufsize: size of audio buffer used by PyTone itself in kBytes # Note: A large buffer may prevent clicking but # leads to a delayed response for instance to play next # song requests # crossfading: on/off # crossfadingstart: start of crossfading in seconds before the end of # the currently playing song (only used when crossfading is on) # crossfadingduration: duration of crossfading in seconds (only used when # crossfading is on) # aooptions: additional options passed to the ao library. Format: # name=value ... # type = mpg123: external player using mpg123/mpg321 # # option: # cmdline: command line for player start # type = xmms: external player using xmms # # options: # session: xmms session number (usually 0) # noqueue: 0=no internal queue for song # 1=internal queue for songs, such that crossfading (via xmms-crossfade) # is possible # type = remote: remote player # # options: # # networklocation: either server_address:port or socket filename of remote player [player.main] # This player will be used for playing of the songs in the playlist. autoplay = true type = internal driver = oss device = /dev/dsp bufsize = 100 crossfading = on crossfadingstart = 5 crossfadingduration = 6 #aooptions=period_time=100 use_mmap=1 # example for an internal player using alsa #type = internal #driver = alsa09 #device = default #bufsize = 100 #crossfading = on #crossfadingstart = 5 #crossfadingduration = 6 # example for an internal player which outputs with a small buffer to aRts #type = internal #driver = arts #bufsize = 100 #crossfading = on #crossfadingstart = 5 #crossfadingduration = 6 # example for an internal player which outputs with a small buffer to esd #type = internal #driver = esd #bufsize = 100 #crossfading = on #crossfadingstart = 5 #crossfadingduration = 6 # example for an external player using xmms #type = xmms #session = 0 #noqueue = false # example for an external player using mpg321 #type = mpg123 #cmdline = /usr/bin/mpg321 --skip-printing-frames=5 -a /dev/dsp #here the command line for mpg123 #cmdline = /usr/bin/mpg123 -a /dev/dsp" # examle for remote players #type = remote #networklocation = localhost:1972 #networklocation = ~/.pytone/pytonectl [player.secondary] # the secondary player always plays the currently selected song. # To be able to use it, you need either need two outputs on your soundcard # (or two soundcards) or a remote main player on another computer autoplay = false type = off #type = internal #driver = oss #device = /dev/dsp1 #bufsize = 0 #crossfading = on #crossfadingstart = 1 #crossfadingduration = 2 #aooptions=period_time=100 use_mmap=1 #type = xmms #session = 1 #noqueue = false #type = mpg123 #cmdline = /usr/bin/mpg321 --skip-printing-frames=5 -a /dev/dsp1 ############################################################################## # window configuration ############################################################################## [filelistwindow] # the filelist window allows the selection of the songs in your # database # border: define borders of filelist window # # Either you can use the predefined settings "off", "all", "compact" # or "ultracompact" or specify the border manual using a combination # of "left", "right", "top", and "bottom". An empty string means no # border border = all # scrollbar: should the filelist window have a scrollbar scrollbar = true # scrollmode: either "line" or "page" # # scrolled up or down one line or one page when you attempt to move across a # screen boundary. Setting this to "page" is useful for slow links to # avoid many redraws). scrollmode = page # virtualdirectoriesattop: # # should the virtual directories be displayed at the top of the # filelist window? virtualdirectoriesattop = false # skipsinglealbums: # # should one directly enter the album of an artist with a single album only skipsinglealbums = true [playlistwindow] # the playlist window shows the currently scheduled songs for playing # border: define borders of playlist window # # The format is described in the filelistwindow section. border = all # scrollbar: should the playlist window have a scrollbar scrollbar = true # scrollmode: either "line" or "page" # # scrolled up or down one line or one page when you attempt to move across a # screen boundary. Setting this to "page" is useful for slow links to # avoid many redraws). scrollmode = page # songformat: format string used for playlist entries # # A normal python format string can be used, whereby access to # a dict with the following keys is available: # title: title of song (string) # album: album of song(string) # artist: artist of song (string) # path: path of song (string) # name: basename of song (string) # length: length of song formated as mm:ss (string) # minutes: minutes of song length (int) # seconds: seconds of song length (int) # year: year of song (int) # genre: genre of song (string) # tracknr: track number of song (string) # playstarthours, playstartminutes, playstartseconds: scheduled start time for song playback (int) songformat = %(artist)s - %(title)s #songformat = %(artist)s - %(album)s - %(title)s #songformat = [%(playstarthours)2d:%(playstartminutes)02d:%(playstartseconds)02d] %(artist)s - %(album)s - %(title)s [playerwindow] # the player window shows the song currently being played on the main player # border: define borders of player window # # The format is described in the filelistwindow section. border = all # songformat: format string used for player window title # # The format is the same as above. songformat = %(artist)s - %(title)s #songformat = %(artist)s - %(album)s - %(title)s [iteminfowindow] # the item info window shows information about the item currently selected # border: define borders of item info window # # The format is described in the filelistwindow section. border = all [inputwindow] # window appearing, when PyTone expects an input from the user's side # (for searching, file names, etc.) # type: either "popup" or "statusbar" # # If type="popup", PyTone opens a new window on top of the other windows # If type="statusbar", PyTone expects the input in the status bar type = popup [mixerwindow] # window for built-in mixer (if present) # type: either "popup" or "statusbar" # # If type="popup", PyTone opens a new mixer window on top of the other windows # If type="statusbar", the mixer appears in the statusbar of PyTone. type = popup # times in seconds after which mixer windows disappears # automatically. Use 0 to disable the automatic closing. autoclosetime = 5 [helpwindow] # popup window for online help # times in seconds after which the help window disappears # automatically. Use 0 to disable the automatic closing. autoclosetime = 0 [logwindow] # popup window for message log # times in seconds after which the message log disappears # automatically. Use 0 to disable the automatic closing. autoclosetime = 10 [statswindow] # popup window for database statistics # times in seconds after which the database statistics window disappears # automatically. Use 0 to disable the automatic closing. autoclosetime = 10 ############################################################################## # color configuration ############################################################################## # specification of colors goes á la mutt: # use for colors: color fgcolor bgcolor # - fgcolor may be prefixed by bright # - bgcolor can be left away, resulting in the default background color # use for mono terminals: mono attr # - attr=none, bold, underline, reverse, standout # you may also specify a color and a mono directive to support both # types of colors. If only a color is given, "mono none" is implicitely assumed [colors.filelistwindow] background = color white border = color green activeborder = color brightgreen mono bold title = color brightgreen mono bold activetitle = color brightgreen mono bold scrollbar = color green scrollbarhigh = color brightgreen mono bold scrollbararrow = color brightgreen mono bold song = color white artist_album = color brightblue mono bold directory = color brightcyan mono bold selected_song = color white red mono reverse selected_artist_album = color brightblue red mono reverse selected_directory = color brightcyan red mono reverse [colors.playlistwindow] background = color white border = color green activeborder = color brightgreen mono bold title = color brightgreen mono bold activetitle = color brightgreen mono bold scrollbar = color green scrollbarhigh = color brightgreen mono bold scrollbararrow = color brightgreen mono bold unplayedsong = color brightwhite mono bold playingsong = color yellow mono underline playedsong = color white selected_unplayedsong = color brightwhite red mono reverse selected_playingsong = color yellow red mono reverse selected_playedsong = color white red mono reverse [colors.playerwindow] # the player window shows informations about the song which is # currently being played on the main player background = color white border = color green activeborder = color brightgreen mono bold title = color brightgreen mono bold description = color brightcyan mono bold content = color white progressbar = color cyan cyan progressbarhigh = color red red mono bold [colors.iteminfowindow] # the iteminfo window shows informations about the currently selected item # (song, artist, album, playlist, etc.) background = color white border = color green activeborder = color brightgreen mono bold title = color brightgreen mono bold description = color brightcyan mono bold content = color white [colors.statusbar] background = color white key = color brightcyan mono bold description = color white [colors.inputwindow] # colors (the border and title colors are only relevant if inputwindow.type="popup") background = color white border = color green activeborder = color brightgreen mono bold title = color brightgreen mono bold description = color brightcyan mono bold content = color white [colors.mixerwindow] # colors (the border and title colors are only relevant, if type="popup") background = color white border = color green activeborder = color brightgreen mono bold title = color brightgreen mono bold description = color brightcyan mono bold content = color white bar = color cyan cyan barhigh = color red red mono bold [colors.helpwindow] background = color white border = color green activeborder = color brightgreen mono bold title = color brightgreen mono bold key = color brightcyan mono bold description = color white [colors.logwindow] background = color white border = color green activeborder = color brightgreen mono bold time = color brightgreen mono bold debug = color white info = color white warning = color cyan error = color red mono bold [colors.statswindow] background = color white border = color green activeborder = color brightgreen mono bold title = color brightgreen mono bold description = color brightcyan mono bold content = color white ############################################################################## # keybindings ############################################################################## # each entry is a space separated list of keys, where for each key: # - the prefixes alt- and ctrl- may be used # - case is important # - KEY_... identifiers from the curses library (+KEY_SPACE for the space key) # may be used [keybindings.general] # keybindings independent of filelist and playlist window refresh = ctrl-l # note that the exit key has to be pressed two times in fast succession exit = ctrl-x playerstart = p P playerpause = p P playernextsong = n N playerprevioussong = b B playerforward = > playerrewind = < playerstop = S playlistdeleteplayedsongs = KEY_BACKSPACE playlistclear = ctrl-d playlistsave = ctrl-w playlistreplay = ctrl-u playlisttoggleautoplaymode= ctrl-t togglelayout = KEY_F10 showhelp = ? showlog = ! showstats = % showiteminfolong = = showlyrics = L toggleiteminfowindow = ctrl-v volumeup = ) volumedown = ( # Play speed controls playerplayfaster = } playerplayslower = { playerspeedreset = ~ # rating of currently playing song playerratecurrentsong1 = alt-1 playerratecurrentsong2 = alt-2 playerratecurrentsong3 = alt-3 playerratecurrentsong4 = alt-4 playerratecurrentsong5 = alt-5 [keybindings.filelistwindow] # additional keybindings when filelist window is active selectnext = KEY_DOWN j selectprev = KEY_UP k selectnextpage = ctrl-n KEY_NPAGE selectprevpage = ctrl-p KEY_PPAGE selectfirst = ctrl-a KEY_HOME selectlast = ctrl-e KEY_END dirdown = KEY_RIGHT KEY_SPACE \n KEY_ENTER l dirup = KEY_LEFT h addsongtoplaylist = KEY_SPACE \n KEY_ENTER KEY_RIGHT l adddirtoplaylist = i I KEY_IC alt-KEY_RIGHT playselectedsong = alt-\n alt-KEY_ENTER activateplaylist = \t insertrandomlist = r R rescan = u U toggledelete = D search = / ctrl-s repeatsearch = ctrl-g focus = f [keybindings.playlistwindow] # additional keybindings when playlist window is active selectnext = KEY_DOWN j selectprev = KEY_UP k selectnextpage = ctrl-n KEY_NPAGE selectprevpage = ctrl-p KEY_PPAGE selectfirst = ctrl-a KEY_HOME selectlast = ctrl-e KEY_END moveitemup = + moveitemdown = - deleteitem = d D KEY_DC activatefilelist = \t KEY_LEFT h playselectedsong = alt-\n alt-KEY_ENTER shuffle = r R rescan = u U filelistjumptoselectedsong = KEY_RIGHT l