minirok-2.1/0000755000175000017500000000000011265736104011471 5ustar datodatominirok-2.1/config/0000755000175000017500000000000011265733622012740 5ustar datodatominirok-2.1/config/minirok_append.desktop0000644000175000017500000000032011265733622017325 0ustar datodato[Desktop Entry] Type=Service Actions=appendToPlaylist; X-KDE-ServiceTypes=KonqPopupMenu/Plugin;audio/* [Desktop Action appendToPlaylist] Name=Append to Minirok playlist Exec=minirok --append %U Icon=minirok minirok-2.1/config/minirok.desktop0000644000175000017500000000036411265733622016006 0ustar datodato[Desktop Entry] Type=Application Name=Minirok Exec=minirok %U Icon=minirok Encoding=UTF-8 Terminal=false Categories=Qt;KDE;AudioVideo;Audio;Player; MimeType=audio/vorbis;audio/x-flac;audio/x-mp3;audio/x-musepack;audio/x-oggflac;audio/x-vorbis; minirok-2.1/config/minirokui.rc0000644000175000017500000000121111265733622015267 0ustar datodato Playlist Toolbar minirok-2.1/minirok.xml0000644000175000017500000001377511265733622013702 0ustar datodato Adeodato Simó
dato@net.com.org.es
2007-09-04
minirok 1 minirok a small music player written in Python minirok options file DESCRIPTION Minirok is a small music player written in Python for the K Desktop Environment. To start it, select it from the applications menu, or type minirok in a terminal. To reproduce music, first type in the combo box at the left the directory where music is located, and press enter, or select "Open directory" from the File menu. Available audio files will be shown in a tree structure. Locate the files you want to play, and use drag and drop to append them to the playlist in the right. You can also press double click in a file or a folder to append it to the existing playlist. Both the tree view and the playlist have search line widgets above them. Typing text in them will reduce the shown items to those matching the introduced words. If you press enter after a search in the tree view completes, the visible items will be appended to the playlist. If you press enter in the playlist search line, the first visible track will be played. You can modify the order in which the tracks are played by enqueueing them in a different order. For this, press right button click on a track, and select "Enqueue track". Or press Control + RightButtonClick on the track to enqueue. Similarly, you can signal that playing should stop after a certain track. To do this, select "Stop playing after this track" in the contextual menu as above, or press Control + MiddleButtonClick. If you make changes to the filesystem, you can quickly refresh the tree view by clicking on the refresh button next to the combo box with the directory name. A key can be also configurated to do this, F5 by default. DBUS INTERFACE Minirok offers a DBus interface to control the player and various other bits. At the moment a single object /Player is provided, under the org.kde.minirok service. To invoke a DBus method, run from a terminal qdbus org.kde.minirok /Player methodName. dbus-send(1) should also work, but then you'll need to fully qualify the method name. Here's a list of available methods: Play Pause PlayPause Stop Next Previous NowPlaying AppendToPlaylist StopAfterCurrent See the README.Usage file for details. Note that this interface will only be available if the required dependencies are installed. See the README file a list of these. LAST.FM Minirok can submit played tracks to Last.fm, or any other Last.fm-compatible service (such as Libre.fm). You will just need to configure your username and password in the preferences dialog. Starting with Minirok 2.1, no external software is needed any more. OPTIONS Try to append the files given as arguments to an existing Minirok instance first. If that fails, start a new Minirok instance as usual. (This is done via DBus, see the README file for required dependencies.) Minirok also accepts many other options for using the Qt and KDE libraries. Run minirok --help-all for a comprehensive list. REPORTING BUGS Please report bug to the Debian Bug Tracking System. See the README.Bugs file for instructions. A list of reported issues is kept at http://bugs.debian.org/minirok. COPYRIGHT Minirok is Copyright (c) 2007-2009 Adeodato Simó, and licensed under the terms of the MIT license. SEE ALSO /usr/share/doc/minirok/NEWS /usr/share/doc/minirok/FAQ /usr/share/doc/minirok/README /usr/share/doc/minirok/README.Bugs /usr/share/doc/minirok/README.Lastfm /usr/share/doc/minirok/README.Usage /usr/share/doc/minirok/TODO
minirok-2.1/src0000777000175000017500000000000011265736104013573 2minirokustar datodatominirok-2.1/FAQ0000644000175000017500000000062411265733622012027 0ustar datodatoPreemptive list of Frequently Asked Questions --------------------------------------------- Q: Why does the tree view not show all subdirectories? A: The tree view does not show files which are not in a recognized playable format. Because of this, all directories which contain no audio files result in empty directories. Having them there only clutters the view, so they are removed as well. minirok-2.1/README.Bugs0000644000175000017500000000332511265733622013255 0ustar datodatoReporting bugs ============== I use Debian's Bug Tracking System to manage bugs in Minirok, so I'd appreciate if you could report directly there, following the instructions below. If you really can't, you can mail me directly, but I will probably forward your report to the public BTS. The list of already reported bugs is at http://bugs.debian.org/minirok. You may want to check it before submitting a new report. If you'd like to make a comment to an existing bug, just send mail to , where nnn is obviously the bug number. To report a new bug, just send a mail to , with the following format for the body: Package: minirok Version: x.y.z Here the rest of the body. That is, a mail whose first line is a line starting with "Package", then a second line specifying the "Version" you're using, then a *blank* line (don't forget this one), and then whatever you want, most likely an explanation of the problem. You can find more detailed instructions here: http://www.debian.org/Bugs/Reporting If you're a Debian user, you can just use the `reportbug` program. If you're an Ubuntu user, please don't use reportbug, because it will send the bug report elsewhere and I probably won't see it. Known bugs ========== Here is a list of known bugs or quirks that I don't plan on addressing, most likely because I wouldn't know how -- help or patches welcome. Known bugs that can/should get fixed someday are documented in the TODO file. * One can't configure multi-key global shortcuts. This is because KDE 4 no longer supports multi-key shortcuts AFAIK. Upstream says there is a bit of hope, but not much, since they don't think the usefulness/cost ratio is worth it. minirok-2.1/minirok.10000644000175000017500000001007111265736104013222 0ustar datodato'\" -*- coding: us-ascii -*- .if \n(.g .ds T< \\FC .if \n(.g .ds T> \\F[\n[.fam]] .de URL \\$2 \(la\\$1\(ra\\$3 .. .if \n(.g .mso www.tmac .TH minirok 1 2007-09-04 "" "" .SH NAME minirok \- a small music player written in Python .SH SYNOPSIS 'nh .fi .ad l \fBminirok\fR \kx .if (\nx>(\n(.l/2)) .nr x (\n(.l/5) 'in \n(.iu+\nxu [options] [file]\&... 'in \n(.iu-\nxu .ad b 'hy .SH DESCRIPTION Minirok is a small music player written in Python for the K Desktop Environment. To start it, select it from the applications menu, or type \fBminirok\fR in a terminal. .PP To reproduce music, first type in the combo box at the left the directory where music is located, and press enter, or select "Open directory" from the File menu. Available audio files will be shown in a tree structure. Locate the files you want to play, and use drag and drop to append them to the playlist in the right. You can also press double click in a file or a folder to append it to the existing playlist. .PP Both the tree view and the playlist have search line widgets above them. Typing text in them will reduce the shown items to those matching the introduced words. If you press enter after a search in the tree view completes, the visible items will be appended to the playlist. If you press enter in the playlist search line, the first visible track will be played. .PP You can modify the order in which the tracks are played by enqueueing them in a different order. For this, press right button click on a track, and select "Enqueue track". Or press Control + RightButtonClick on the track to enqueue. .PP Similarly, you can signal that playing should stop after a certain track. To do this, select "Stop playing after this track" in the contextual menu as above, or press Control + MiddleButtonClick. .PP If you make changes to the filesystem, you can quickly refresh the tree view by clicking on the refresh button next to the combo box with the directory name. A key can be also configurated to do this, F5 by default. .SH "DBUS INTERFACE" Minirok offers a DBus interface to control the player and various other bits. At the moment a single object /Player is provided, under the org.kde.minirok service. To invoke a DBus method, run from a terminal \fBqdbus org.kde.minirok /Player \fImethodName\fB\fR. \fBdbus-send\fR(1) should also work, but then you'll need to fully qualify the method name. .PP Here's a list of available methods: .PP .nf \*(T< Play Pause PlayPause Stop Next Previous NowPlaying AppendToPlaylist StopAfterCurrent\*(T> .fi .PP See the \*(T<\fIREADME.Usage\fR\*(T> file for details. Note that this interface will only be available if the required dependencies are installed. See the \*(T<\fIREADME\fR\*(T> file a list of these. .SH LAST.FM Minirok can submit played tracks to Last.fm, or any other Last.fm-compatible service (such as Libre.fm). You will just need to configure your username and password in the preferences dialog. Starting with Minirok 2.1, no external software is needed any more. .SH OPTIONS .TP \*(T<\fB\-a\fR\*(T>, \*(T<\fB\-\-append\fR\*(T> Try to append the files given as arguments to an existing Minirok instance first. If that fails, start a new Minirok instance as usual. (This is done via DBus, see the \*(T<\fIREADME\fR\*(T> file for required dependencies.) .PP Minirok also accepts many other options for using the Qt and KDE libraries. Run \fBminirok --help-all\fR for a comprehensive list. .SH "REPORTING BUGS" Please report bug to the Debian Bug Tracking System. See the \*(T<\fIREADME.Bugs\fR\*(T> file for instructions. .PP A list of reported issues is kept at http://bugs.debian.org/minirok. .SH COPYRIGHT Minirok is Copyright (c) 2007-2009 Adeodato Sim\('o, and licensed under the terms of the MIT license. .SH "SEE ALSO" \*(T<\fI/usr/share/doc/minirok/NEWS\fR\*(T> .PP \*(T<\fI/usr/share/doc/minirok/FAQ\fR\*(T> .PP \*(T<\fI/usr/share/doc/minirok/README\fR\*(T> .PP \*(T<\fI/usr/share/doc/minirok/README.Bugs\fR\*(T> .PP \*(T<\fI/usr/share/doc/minirok/README.Lastfm\fR\*(T> .PP \*(T<\fI/usr/share/doc/minirok/README.Usage\fR\*(T> .PP \*(T<\fI/usr/share/doc/minirok/TODO\fR\*(T> minirok-2.1/README0000644000175000017500000001252411265733622012357 0ustar datodatoDescription =========== Minirok is a small music player written in Python for the K Desktop Environment. As its name hints, it's modelled after Amarok (1.4), but with a reduced set of features. In particular, it is designed to cover all the needs and wishes of the author, leaving everything else out. The look and feel is almost identical to the old Amarok, though. The main interface is a *tree view of the filesystem*, with a playlist that can only be populated via drag and drop. There is no collection built from tags, so it's targeted at people whose collection is structured in a tree already at the filesystem level. Searches can be performed both in the tree view and the playlist. Other features include: * DBus interface for controlling the player and retrieving the currently played track, among other things * alter the playing order in the playlist by queueing tracks; stop after a certain track; repeat track or playlist; random mode; undo and redo * reading of tags when adding to the playlist can be disabled by specifying a regular expression to extract them from the filename * submission of played songs to Last.fm * global shortcuts Audio formats ============= Minirok supports playing and reading tags from MP3, Ogg Vorbis, FLAC and Musepack, which are the formats I use. If you need some other format, please send me a polite e-mail and I will add them if GStreamer and Mutagen support them (which they probably will). Motivation ========== Let me start by saying that I was a very happy Amarok user for more than three years, and that I think at least version 1.4 is a terrific player which deserved all the success it got (I haven't tried version 2 at all). Anyway, I always wished for some things to be different. In particular, I always missed a tree view of the filesystem, because my music collection is highly structured, and it would've been enough. The collection seemed an overkill to me, though it worked very nicely for me until: (1) they "fixed" Bug#116127. Read the bug log for details, and my comment #10 in particular. The bottom line is that my collection became uselessly cluttered, because I have a lot of different artists in my collection, but a high percentage with only one song coming from compilation albums. (2) I got a laptop. I discovered that despite mounting desktop:/mnt/mp3 via sshfs at laptop:/mnt/mp3, I could not share the collection.db file (Debian's Bug#437873, I think). Plus I could not have two collections, one eg. for the mounted /mnt/mp3, and another for a local ~/mp3 archive. So on summer 2007 I found myself with much spare time, and one night the idea of writing a player tailored to my needs popped up: I'd get a personalized player, get rid of my free time, and learn PyKDE as a bonus. A win-win-win situation! I also like having Python projects to hack on, so that too. Requirements ============ Minirok is written in Python, version 2.5 or later is required. If it's available from your distribution, I recommend you install that package, since it'll take care of installing everything necessary. For Debian and derivatives, you should be able to install directly with apt-get, and you can check if there's a more recent version here: http://chistera.yi.org/~adeodato/code/minirok/packages For installing from source, here's a list of the *required* libraries for the program to run: * PyQt and PyKDE (version 4) Debian and Ubuntu: python-qt4, python-kde4 Source: http://www.riverbankcomputing.co.uk/pyqt/download.php http://www.riverbankcomputing.co.uk/pykde/download.php * Mutagen (audio metadata library) Debian and Ubuntu: python-mutagen Source: http://www.sacredchao.net/quodlibet/wiki/Development/Mutagen * The json or simplejson modules (shipped with Python 2.6 already) Debian and Ubuntu: python-simplejson | python (>= 2.6) Source: http://undefined.org/python/#simplejson * The GStreamer media framework, in particular: + The GStreamer Python bindings Debian and Ubuntu: python-gst0.10 + Plugins, in all the flavours needed to cover your audio formats: - MP3: gst-plugins-ugly *and* gst-plugins-good - Ogg Vorbis: gst-plugins-base - FLAC: gst-plugins-good - Musepack (MPC): gst-plugins-bad Debian and Ubuntu: gstreamer0.10-plugins-ugly, gstreamer0.10-plugins-base, gstreamer0.10-plugins-good, gstreamer0.10-plugins-bad + A suitable audiosink, for example ALSA. Debian and Ubuntu: gstreamer0.10-alsa Source: http://gstreamer.freedesktop.org The following dependencies are optional and enhance Minirok in some way: * dbus-python - enables controlling the player from scripts, à-la-DCOP Debian and Ubuntu: python-dbus Source: http://www.freedesktop.org/wiki/Software/DBusBindings#python (You'll also need DBus support compiled-in in the PyQt bindings above; in Debian and Ubuntu, just install python-qt4-dbus in addition to python-dbus. Additionally, you must be running Qt 4.4.0 or later for the DBus interface to work.) * python-psutil - makes the scrobble lock handling more robust Debian and Ubuntu: python-psutil Source: http://code.google.com/p/psutil/ Author and license ================== Minirok is written by Adeodato Simó (dato@net.com.org.es), and licensed under the terms of the MIT license. vi: sw=2 minirok-2.1/LICENSE0000644000175000017500000000220411265733622012476 0ustar datodatoMinirok is Copyright (c) 2007-2009 Adeodato Simó, and licensed under the terms of the MIT license: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. minirok-2.1/NEWS0000644000175000017500000002073611265733622012202 0ustar datodato2.1 2009-10-15 IMPROVEMENTS * Submissions to Last.fm or a Last.fm-compatible service are performed directly by Minirok now, and the lastfmsubmitd daemon is no longer a dependency. If you were previously using lastfmsubmitd, you will need to provide Minirok with your username and password in the preferences dialog now. If listening to music offline, or if the Last.fm server is not responding, submissions are stored on disk by Minirok and submitted later on. For this functionality, the "simplejson" module has been added as a dependency. This module comes already with Python 2.6, see the README file for details. * Allow to enqueue/dequeue selected tracks with a shortcut (Ctrl+E by default). * Allow for a global shortcut to be configured to the Stop, Next and Previous actions. (No default shortcut is provided, but one can be set by the user.) BUGFIXES * When running from source, correctly show the playlist controls toolbar (was not being displayed at all). * Fix crash when using File->Open directory if no directory has ever been opened in the tree view. * Fix the following warning when running with PyQt 4.5 (which becomes a crash under PyQt 4.6): WARNING: skipping invalid entry in column config: '' Also, if you had experienced this warning and saw the columns in the playlist come up in a weird default order, that's been fixed too. OTHER NEWS * Python 2.5 or later is required now. 2.0 2009-06-01 PORTED TO KDE 4 Minirok now uses PyQt4 and PyKDE4. As a result: * global keybindings work natively, without needing KHotKeys. * the set of buttons in the toolbar can be configured, as in other KDE applications. * the DCOP interface is gone, replaced by DBus. See README.Usage for details. IMPROVEMENTS * The playlist now supports Undo/Redo functionality. * If you don't use the search in the tree view, it is now possible to disable it, saving Minirok from having to recurse your entire filesystem tree each time it starts. * Command line arguments that are directories will not be discarded. Instead, all playable files underneath them will be added to the playlist. * When changing directories in the tree view, if the new directory had already been loaded, it won't be scanned again and search will be available instantly. OTHER NEWS * Support for Amarok's classic "funky-monkey" theme has been dropped. Sorry! 0.9.2 2008-11-29 BUGFIXES * Fix crashes that randomly happened while moving from one track to another. * Minirok no longer hangs on exit, which had started happening with Python 2.5. 0.9.1 2008-05-21 BUGFIXES * Don't die when setting the tooltip for tracks with no artist tag. This manifested in the slider not moving for those tracks, and the player not jumping to the next track when reaching the end. 0.9 2008-03-22 NEW FEATURES * The slider in the statusbar can now be used to seek within a track. 0.8.1 2008-01-29 BUGFIXES * Unbreak saving the list of paths from the tree view combo box. 0.8 2008-01-27 NEW FEATURES * The tree view can now quickly scan for changes in the filesystem (via a new Refresh button), instead of having to re-read all directory contents. * New action "Open directory" in the File menu to select with a dialog the directory to load in the tree view, instead of having to type it. * Dropping tracks while holding down the Control key will always append them at the end of the playlist, independently of the position they were dropped at. IMPROVEMENTS * Key shortcuts for toggling random mode (Ctrl+R by default) and cycling through possible repeat modes (Ctrl+T). * The labels in the statusbar that tell the position in the track will now blink while the player is paused. * Completion of directory names works in the path combo. (The code was there, but wasn't working due to a small PyKDE oddity.) * Make the creation of the tree view faster by avoding lots of useless calls to slot_populate_one() when iterator.current() is a FileItem and not a DirectoryItem. * Read ID3 tags in a separate thread, to improve UI responsiveness when the audio files live in a network filesystem over a slow network link, eg. sshfs over wireless. (The same is planned for reading directory contents for the tree view, probably once ported to Qt4, because I'm having trouble with the main thread blocking when reading directory contents in a separate thread, that seem solved in PyQt4.) BUGFIXES * Does not discard length information for MP3 files without any ID3 tags; formerly, the length for such files would always be reported as 0:00. OTHER CHANGES * Improve the handling of non existing directories in the tree view combo box. 0.7 2007-11-21 NEW FEATURES * Calculate the length of tracks when loading them into the playlist, instead of just when starting to play them, unless reading of tags is disabled. * The systray icon will show the currently playing track as a tooltip. * New function in the context menu to crop selected tracks, that is, to remove from the playlist all tracks except those selected. * The context menu can handle enqueueing several tracks at once. * Compatible with lastfmsubmitd 0.36, which introduced a new API; compatibility with older versions (0.35) is maintained. BUGFIXES * When adding tracks to the queue while playing the last track in the playlist, the Next button would not get enabled. * Then length of tracks is calculated with Mutagen instead of GStreamer, which fixes several cases where GStreamer would get the length wrong. * Ctrl+LeftButtonClick works to select several items in the playlist. * Exit the engine thread cleanly, so that there are no unhandled exceptions when quitting. 0.6 2007-09-04 NEW FEATURES * Repeat mode: repeat track or repeat playlist. * Random mode. * Handle the return key in the playlist search line, starting to play the first item that matched the search; and in the tree view search line, appending matching items to the playlist and starting playback if the player was stopped. * Minirok accepts files to load into the playlist as arguments. * New command line option -a/--append and accompanying DCOP function appendToPlaylist to append given files to an existing Minirok instance. For --append, if no instance is running, a new one will be started. BUGFIXES * Searching in the tree view handles non-ASCII characters case insensitivity. Formerly case insensitiveness only coped with ASCII. * "Stop after current" works even if the currently played item is not present in the playlist (i.e., has been removed). * The Next button is enabled when at the last item in the playlist, but with items still left in the queue. * Items don't get added more than once to the playlist even if they're present multiple times in the drag object (eg. when doing Ctrl-A). * Minirok does not cancel logging out when the main window is visible. OTHER CHANGES * When reaching the end of the track marked as "stop after this track", playing stops but the current track jumps to the next track. Formerly it would stay at the just played track. * The playlist scrolls automatically to ensure the currently playing item is always visible. This is particularly handy for random mode. * If the list gets cleared while playing a certain track, and then that track gets added to the playlist while still being played, the playlist will mark it the current track. * If Minirok is docked in the systray when logging out of KDE, it will be there as well when restoring the session. Formerly the main window would always be shown. * Empty directories are not shown in the tree view. This includes directories that contain no playable files. Directories which only contain empty subdirectories are not shown as well, recursively. 0.5.1 2007-08-25 BUGFIXES * Handle lastfmsubmitd being installed but not configured. 0.5 2007-08-25 * First public release. vi: sw=2:comments+=fb\:* minirok-2.1/setup.sh0000755000175000017500000000365611265733622013204 0ustar datodato#! /bin/bash # If $DEBIAN_PREFIX is set, it will be prepended to all locations. # This is used when building the Debian package. set -e ## if ! kde4-config 2>/dev/null; then echo >&2 "ERROR: could not find kde4-config." exit 1 fi ## PREFIX=`kde4-config --prefix` BIN=`kde4-config --install exe` APPS=`kde4-config --install data` ICONS=`kde4-config --install icon` DESKTOP=`kde4-config --install xdgdata-apps` SERVICE_MENUS=`kde4-config --install services`/ServiceMenus MINIROK="$APPS/minirok" ## install_file () { # path/file path/dir -> path/dir/file install_file2 "$1" "$2/`basename $1`" } install_file2 () { # path/file path/dir/file2 -> path/dir/file2 install -D -m `mode $1` "$1" "${DEBIAN_PREFIX%%/}/${2##/}" } install_symlink () { DEST="${DEBIAN_PREFIX%%/}/${2##/}" mkdir -p "`dirname $DEST`" ln -sf "$1" "$DEST" } ## install_icons () { ( cd "$1" && find -maxdepth 1 -name '*.png' ) | while read file; do install_file2 "$1/$file" "$2/`echo $file | tr = /`" done } install_images () { for img in images/*.png; do install_file "$img" "$MINIROK/images" done } install_package () { for p in minirok.py minirok/*.py minirok/ui/*.py; do install_file "$p" "$PREFIX/share/minirok/`dirname $p`" done } install_manpage () { if make -s minirok.1; then install_file minirok.1 /usr/share/man/man1 fi } ## mode () { if [ -x "$1" ]; then echo 755 else echo 644 fi } ## case "$1" in install) install_images install_package install_manpage install_icons images/icons "$ICONS" install_icons images/icons/private "$MINIROK/icons" install_file config/minirokui.rc "$MINIROK" install_file config/minirok.desktop "$DESKTOP" install_file config/minirok_append.desktop "$SERVICE_MENUS" install_symlink "$PREFIX/share/minirok/minirok.py" "$BIN/minirok" ;; *) echo "Doing nothing, please pass 'install' as the first argument." ;; esac minirok-2.1/INSTALL0000644000175000017500000000106011265733622012521 0ustar datodatoInstalling instructions ======================= If you want to install Minirok from source (instead of installing packages from your distribution), run `./setup.sh install`. That will install files under the appropriate locations as determined by kde4-config. If the kde4-config binary can't be found, installation will be aborted. However, Minirok should run just fine from the unpacked source directory, with just `./minirok.py` (you can make a symlink to that from eg. ~/bin/minirok). Be sure to run `make ui` if you want the Preferences dialog to work. minirok-2.1/Makefile0000644000175000017500000000024211265733622013131 0ustar datodatoall: ui minirok.1 ui: $(MAKE) -C $(CURDIR)/minirok/ui clean: rm -f minirok.1 $(MAKE) -C $(CURDIR)/minirok/ui clean minirok.1: minirok.xml docbook2x-man $< minirok-2.1/images/0000755000175000017500000000000011265733622012740 5ustar datodatominirok-2.1/images/repeat_track_small.png0000644000175000017500000000036211265733622017303 0ustar datodatoPNG  IHDR7gAMA7tEXtSoftwareAdobe ImageReadyqe<IDAT(ϵA 0 EI$L& 8@$ &aPc[ ȿ'm }QaV P̴#PBR'@OXo/'816RVJ(q7gUsBqA&L F֡hs`-5{>*ֶ>$<j^=Z!Y)dw1bxv_#XstK?@űw?6_y. W9)L,,N̴F~h4PiKg/R~`Z3ا wag8ay^2eD'1Xfi*]j* d#2Vz{qP *ŚG׆_ u`IENDB`minirok-2.1/images/repeat_playlist_small.png0000644000175000017500000000125711265733622020044 0ustar datodatoPNG  IHDRasBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<,IDAT8Kafe B#(" v-ݶ(VPZmE6I͜8P)y;ֲC 'q !~80"r]RZX4o 숎dw<8EL咮Ƨٝ0$(Ц.dzgS6hL#ߒ|3ۉ6uPMq1pg4ޤϪG$I'}iQ1qp]xO'x\?qf58nh/D 3'JleV8~c N{/*u%(E` -[r9Cc.M+~-TCL VEgC˺cdl+ծɗ򞢯 oT4YќR8J8v=xzD27. d e (DxpT>;p>薋ւ"jӕ@U_1x2qqV K%k!W^8;U5!bzx;vݣvIENDB`minirok-2.1/images/icons/0000755000175000017500000000000011265733622014053 5ustar datodatominirok-2.1/images/icons/hicolor=16x16=apps=minirok.png0000644000175000017500000000061111265733622021470 0ustar datodatoPNG  IHDRabKGDC pHYs  )IDAT8˕=/DQgjѩ~DH$JR^- ;Wf̙/2_%~(GP(Q ϱg,a/ b] XGofa8-bu<἖psx-&~@5aK<+98k@ZNx ;j;f N4JYTR.N#B +Gfe%V;u,c, S&ؑM$߇ՏR"xEo[FFBPPY|IENDB`minirok-2.1/images/icons/hicolor=128x128=apps=minirok.png0000644000175000017500000000365411265733622021652 0ustar datodatoPNG  IHDR>abKGD pHYs  LIDATxmhVe5.sNVDXf&A&:f}"LI#fR "EDfi"2 +ߘo3ݦsnO=Zgsγ}<!B!Bf@!pN J2u|s?O?K1 TTۀNuF90EXM^`'  ybঐm5].l?_v b:dulQ'gݹ[SL,W&d?J̗C" py 51eOZ^ǁxcy0,N!Z^Vdp!T%$8S馚I[LKsP[K7&8`z<d up`T RW앃9.A^.]w۴ד(3} ]}8%ةNÀ|lPM["UD|m鿣躶1{vD/"8i_yै;UIzٞOpfF⽩`S% e+ƨOv:(0 VRS7<["feۇ=lo]yof2Ms%:LfoDJl%ʋ(\x62;)|m&)y?3̷ȇq#Mdnj4[~cE궳F7Cl7%LFIƉjV[b%6mEp7S-PZnb KШ8 |nQ А-DFt (#oa! z:YF}k`Q;l΄ ,=b4t|5a0Sj~b~Ǽ3ѪbQ+=2hB4e@A%3hcH65{}=.l#TnHo\⋗ B!B!B;Dđ!,IENDB`minirok-2.1/images/icons/hicolor=64x64=apps=minirok.png0000644000175000017500000000253411265733622021504 0ustar datodatoPNG  IHDR@@iqbKGDC pHYs  IDATxYVuϬ.ӌ ՔFiXTXQtP7$]FWvdPP]E{QI{68v?9,43h1ihJٙ܏6Qt;{f%Wq{08^>\+svT)@sFg'lHr+ Wr|x˒;Fpb#1]ƵX-j:6*ɋ7kkqÈvaـ~CB$VXSIvwqwBtq6e!iw l<iN~Iy/&z3*X6c7OX+2oH9%XQa0( a*5lx \@zf5H˖D kaWM}X,KOJxK?'xsD8b' 0(wIa"|V֦ćR%]yByxBEb QJ{w 9KeV9YB~Ckru2"5KzˢKsacGCmiw),@}l΢SXa'~i)[8B]52 pt.iwca~2-޵Zr;'0E(N݉3*E7d1M!O^Pm EhÙJLhwONYi>w?wW{RxG@}:nΰIVd`U|DcLO=]U|onP/+cE{LP)-@*S 4])S^h&V"`hZ:Ml>H QdOJu ͥvӔ,j'1Y?!Tu>.~0B=%Mv%??$IeP8T)Nzy1yc:ݲ ො6 ?u]F pPc[ExG-FH+ \kLByu<^{1Gr q>,⎎%5?⫝̸6]B]O_/b\d~sD5ڍpGM0EYepmB(#{pG//pM۱AW:oo2%! (>?(raHހU,syByHQQ;G߂/jfхy[)pE+KТǃBs3& 痍-ZhY =IENDB`minirok-2.1/images/icons/private/0000755000175000017500000000000011265733622015525 5ustar datodatominirok-2.1/images/icons/private/hicolor=22x22=actions=minirok_playlist-clear.png0000644000175000017500000000135711265733622026646 0ustar datodatoPNG  IHDRĴl;sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<lIDAT8ՕkA?ovr{{0H2ثX`gFRQQDhnwvY>X|{73+a9* \ f EZP r~$'9 V6GFzq9N6wp$oUw-eǻkѹz=J ѽxM50bs LC]$C㟘~] OP#IENDB`minirok-2.1/images/icons/private/hicolor=48x48=actions=minirok_playlist-clear.png0000644000175000017500000000272711265733622026670 0ustar datodatoPNG  IHDR00WsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<TIDATh홿oE?of81XAI4 CDIEAAR_@AAT!B( P1H!N{?QޮsClKyoߙcV+n* lz.&fFS֔K^F3\}ؤ||qV́^HKe_WQxstfZ.ݻ@7I-2usIĞmB4܆z?8gtݕ<3:꾅e㤝FѨsrv篥aȰ@NGY րW^7uXۃ7{ InWYn%X;+'zmF+|W'x/pdz! sWO.g0xϴ6~[E`CT ǡ0{dlb1{d}nQzL݂,=U[DrZ 8ǚH` k7\#?jRIYkC}ZVh*M=)~(~%|^^ ^^CXtRNS 4[z̩ticoN\IDATX[KAǧ7(QnXThb.f7"KhL&Z/iw*}؇,TЇJm-K)d9grch"WrwΓnj'ӝw#^xwUnwxv*fKޫIχU𡇸>}O.R!UtW.5PLY.>xeƺmuc㨛 H '*:// x$K(*@ᙓlKNdjs1Pd$H{$,2k]}VK$0;c'c,5̮@Ljn5{0M/V*j޾Zj#C7\.hVU6Y$? Uc7)0Cq  ֈ+0ZHVγQP8R`uA x^+`(WQ@/lai wU@_d[f4<^n9`&P5_#o Pp.(FWli_F.y\N/L& ,..,g# `<`{R+ B4á&up`0G"Ѩ b@p b(<i|Ҋo2(3ѡT `>SOjohPp LG0cl\0HALKqXEs!8Ơ y-4 G7wޘX)8;;s|yJWfI;QP.N)٣ۉlwnh@MNrkbT/\B_Z(a7ͻmk/;޺ꦦ8lܾWPLηc8l{n-#Jʒ N!$vbo\ 2~&;l";O\Wg~g?sL?u~NEg>qH+3 #iV7LAu*p΄w})y)sBTT?dPx$$A!N9S30lO29D8f78Yiǁ~1`qٳ ) :<.O'ez.8@<UD\8ʿ7LL9Y &̚w֒z@TՍ2jHi8xxzuTi3%1riR+גXXfj"Srw\hUoYYwvf#h!uFZp^՟IEʛ}3IENDB`minirok-2.1/images/icons/private/hicolor=16x16=actions=minirok_playlist-clear.png0000644000175000017500000000107711265733622026653 0ustar datodatoPNG  IHDRasBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDAT8MKAgf/z_BrUH+jU }ZmZԢuT$C-HKfN׮)>0p^yy`FT= \1yXG&+E&sv+[M,yU_;ˮicrmzw. ]hj\0Y\V6}hS--̑>mŒAC iFn \&O_R>lp&CZ$-PԔ$0(j!H8zsS[1 ߁ڄCn#PpĨ(JGygvrb0FF$_JӮ'*AN?*귖2cdrU"IA7ޚq^gro,`7H{?u\<я8[v$U$NZ1M:Ϣ\0~op>XT9|_;LԸR0\{fᏅc!=K"p:%/vͰ".jֺىRa'5)nbjR LjRD!񾉧/: Nf'̑$\3/GWq!;[ʼnxQ5Wv} l(~\$krS5X[aЊJha7Vg qLIENDB`minirok-2.1/images/icons/hicolor=48x48=apps=minirok.png0000644000175000017500000000177211265733622021513 0ustar datodatoPNG  IHDR00WbKGDC pHYs  IDAThkdEIf"cPDAtaDAr.Ņݹp "2"*Ȉ8 >fpDLjb2D{EUcsvw9U眪sN271vl=H<XlMLጮ6318~8Œ400 bK8pcոgagljq6ZX!}nqQO/AZ _!LDݗA7>t x>uƭu h54ea]. fGzqCSXG;GX/otMdy; į12#걌wBvKq!>2C" E*<)d)2Z7t=S3nFcxT Am%:8RƅtRO:wwK㹬@6_-Em :LrGq݂0XZ+2%LJʼSH8y7!H9bweM?HGq*~)vE=C5!UU7wBKsx̿{*~E)!)Z%3OU/|MA8vlE!LDت $b 6#To,-5ImdK@dh "0%cnn;9;^-,Ycn[ By),FQw!X4,(Hx`4lPV}$pw6?!0y׌#) 6KJpx.)VFI9!U9;*}ɿEp9pq\ kdP<6݅u0= Lƾ\RSIENDB`minirok-2.1/minirok.py0000755000175000017500000000037711265733622013527 0ustar datodato#! /usr/bin/env python ## vim: fileencoding=utf-8 # # Copyright (c) 2007 Adeodato Simó (dato@net.com.org.es) # Licensed under the terms of the MIT license. import minirok # ensures dependency modules are in place import minirok.main minirok.main.main() minirok-2.1/TODO0000644000175000017500000000327211265733622012167 0ustar datodato(This list is obviosly not complete, just here to not forget of these.) IMPORTANT: * Using util.playable_from_untrusted in load_saved_playlist() eliminates duplicates. Tree View: * Use a regex as well to control the appearance of file names? Playlist: * Tabs?? * Previous button behaves more like a "Back" button: does not go to the item above the current item, but to the item played right before (think queues and random mode). * After clearing a search, the playlist scrolls to the top, which may not show the current item as visible. * Allow "read tags when item visible", à-la-xmms? * Make Undo preserve stuff like the "stop after". * Make Undo/Redo preserve the selection? * Bug: Load album A; start song X; stop; clear playlist; load album B; start song Y; stop; hit Undo twice; play; hit redo twice... song Y shows as playing. UI: * connect track change / playlist change with statusbar and caption Statusbar: * is it possible to get a "you have XXX songs in this playlist" info somewhere in the display? maybe with an added "this is 5 hours of noise" Options: * First radio button properly vertically aligned Other: * Save window position?, maybe use a meaningful size the first time * It'd be nice to have module and line numbers in logging output, but it doesn't seem to work. * Read directory contents in a separate thread, as we do with tags. But probably when ported to Qt4, see: http://www.riverbankcomputing.com/pipermail/pyqt/2008-January/018103.html * Use __slots__ for TreeViewItems? * Last.fm submission should report any errors, eg. failure to write to the spool directory. vi: sw=2 et minirok-2.1/minirok/0000755000175000017500000000000011265733622013143 5ustar datodatominirok-2.1/minirok/proxy.py0000644000175000017500000000706111265733622014702 0ustar datodato#! /usr/bin/env python ## vim: fileencoding=utf-8 # # Copyright (c) 2008 Adeodato Simó (dato@net.com.org.es) # Licensed under the terms of the MIT license. import re from PyQt4 import QtGui from minirok import util ## class Model(QtGui.QSortFilterProxyModel): """A proxy model that makes multi-word match. Can be nicely used with a LineWidget below. Matches will be done like this: * patterns will be matched against sourceModel.data(self.filterRole()) (for the column given with filterKeyColumn(), or all columns if < 0) * patterns will be split into words, and an item will match if it matches (either as full words or subwords) *all* the words in pattern, in any order. """ def __init__(self, parent=None): QtGui.QSortFilterProxyModel.__init__(self, parent) self.pattern = None self.regexes = [] def setPattern(self, pattern): pystring = unicode(pattern).strip() if pystring: if pystring != self.pattern: self.pattern = pystring self.regexes = [ re.compile(re.escape(pat), re.I | re.U) for pat in pystring.split() ] else: self.pattern = None self.invalidateFilter() def filterAcceptsRow(self, row, parent): if self.pattern is None: return True else: text = u'' role = self.filterRole() model = self.sourceModel() c = self.filterKeyColumn() if c >= 0: columns = [ c ] else: columns = range(model.columnCount(parent)) for c in columns: index = model.index(row, c, parent) data = index.data(role) text += unicode(data.toString()) for regex in self.regexes: if not regex.search(text): return False else: return True ## def _map(method): """Decorator to invoke a method in sourceModel(), mapping one index.""" def wrapper(self, index): index = self.mapToSource(index) return getattr(self.sourceModel(), method.func_name)(index) return wrapper def _map_many(method): """Decorator to invoke a method in sourceModel(), mapping a list of indexes.""" def wrapper(self, indexes): indexes = map(self.mapToSource, indexes) return getattr(self.sourceModel(), method.func_name)(indexes) return wrapper ## class LineWidget(QtGui.QWidget): def __init__(self, parent=None): QtGui.QWidget.__init__(self, parent) self._label = QtGui.QLabel('S&earch: ', self) self._searchLine = self.createSearchLine() self._searchLine.show() self._label.setBuddy(self._searchLine) self._label.show() layout = QtGui.QHBoxLayout(self) layout.setSpacing(0) layout.setMargin(0) layout.addWidget(self._label) layout.addWidget(self._searchLine) self.setFocusProxy(self._searchLine) def searchLine(self): return self._searchLine def createSearchLine(self): return Line(self) ## class Line(util.DelayedLineEdit): def __init__(self, parent=None): self._model = None util.DelayedLineEdit.__init__(self, parent) self.connect(self, self.SIGNAL, lambda text: self._model.setPattern(text)) def setProxyModel(self, model): assert isinstance(model, Model), 'proxy.Line only works with proxy.Model' self._model = model minirok-2.1/minirok/tag_reader.py0000644000175000017500000000470211265733622015615 0ustar datodato#! /usr/bin/env python ## vim: fileencoding=utf-8 # # Copyright (c) 2007-2008 Adeodato Simó (dato@net.com.org.es) # Licensed under the terms of the MIT license. import mutagen import mutagen.id3 import mutagen.mp3 import mutagen.easyid3 import minirok from minirok import util ## class TagReader(util.ThreadedWorker): """Worker to read tags from files.""" def __init__(self): util.ThreadedWorker.__init__(self, lambda item: self.tags(item.path)) ## @staticmethod def tags(path): """Return a dict with the tags read from the given path. Tags that will be read: Track, Artist, Album, Title, Length. Any of these may be not present in the returned dict. """ try: info = mutagen.File(path) if isinstance(info, mutagen.mp3.MP3): # We really want an EasyID3 object, so we re-read the tags now. # Alas, EasyID3 does not include the .info part, which contains # the length, so we save it from the MP3 object. dot_info = info.info try: info = mutagen.easyid3.EasyID3(path) except mutagen.id3.ID3NoHeaderError: info = mutagen.easyid3.EasyID3() info.info = dot_info elif info is None: raise Exception, 'mutagen.File() returned None' except Exception, e: # Er, note that not only the above raise is catched here, since # mutagen.File() can raise exceptios as well. Wasn't obvious when I # revisited this code. if path in str(e): # mutagen normally includes the path itself msg = 'could not read tags: %s' % e else: msg = 'could not read tags from %s: %s' % (path, e) minirok.logger.warning(msg) return {} tags = {} for column in [ 'Track', 'Artist', 'Album', 'Title' ]: if column == 'Track': tag = 'tracknumber' else: tag = column.lower() try: tags[column] = info[tag][0] except ValueError: minirok.logger.warn('invalid tag %r for %s', tag, type(info)) except KeyError: # tag is not present pass try: tags['Length'] = int(info.info.length) except AttributeError: pass return tags minirok-2.1/minirok/engine.py0000644000175000017500000000772511265733622014775 0ustar datodato#! /usr/bin/env python ## vim: fileencoding=utf-8 # # Copyright (c) 2007-2008 Adeodato Simó (dato@net.com.org.es) # Licensed under the terms of the MIT license. import os import gst import gobject import minirok from PyQt4 import QtCore gobject.threads_init() ## class State: """This class holds the possible values for engine status.""" PLAYING = object() STOPPED = object() PAUSED = object() ## class GStreamerEngine(QtCore.QObject): SINK = 'alsasink' PLUGINS = { 'flac': [ '.flac' ], 'mad': [ '.mp3', ], 'musepack': [ '.mpc', '.mp+', ], 'vorbis': [ '.ogg' ], } def __init__(self): QtCore.QObject.__init__(self) self._supported_extensions = [] for plugin, extensions in self.PLUGINS.items(): if gst.registry_get_default().find_plugin(plugin) is not None: self._supported_extensions.extend(extensions) self.uri = None self._status = State.STOPPED self.bin = gst.element_factory_make('playbin') self.bin.set_property('video-sink', None) try: device = gst.parse_launch(self.SINK) except gobject.GError: pass else: self.bin.set_property('audio-sink', device) bus = self.bin.get_bus() bus.add_signal_watch() bus.connect('message::eos', self._message_eos) bus.connect('message::error', self._message_error) bus.connect('message::async-done', self._message_async_done) self.time_fmt = gst.Format(gst.FORMAT_TIME) self.seek_pending = False ## def _set_status(self, value): if value != self._status: self._status = value self.emit(QtCore.SIGNAL('status_changed'), value) status = property(lambda self: self._status, _set_status) ## def can_play_path(self, path): """Return True if the engine can play the given file. This is done by looking at the extension of the file. """ prefix, extension = os.path.splitext(path) return extension.lower() in self._supported_extensions ## def play(self, path): self.uri = 'file://' + os.path.abspath(path) self.bin.set_property('uri', self.uri) self.bin.set_state(gst.STATE_NULL) self.bin.set_state(gst.STATE_PLAYING) self.status = State.PLAYING def pause(self, paused=True): if paused: self.bin.set_state(gst.STATE_PAUSED) self.status = State.PAUSED else: self.bin.set_state(gst.STATE_PLAYING) self.status = State.PLAYING def stop(self): self.bin.set_state(gst.STATE_NULL) self.status = State.STOPPED def get_position(self): """Returns the current position as an int in seconds.""" try: return int(round(self.bin.query_position(self.time_fmt)[0] / gst.SECOND)) except gst.QueryError: return 0 def set_position(self, seconds): """Seek to the given position in the current track. This method does not block; "seek_finished" will be emitted after the seek has been performed. """ self.seek_pending = True self.bin.seek_simple(self.time_fmt, gst.SEEK_FLAG_FLUSH | gst.SEEK_FLAG_KEY_UNIT, seconds * gst.SECOND) # self.bin.get_state(gst.CLOCK_TIME_NONE) # block until done ## def _message_eos(self, bus, message): self.bin.set_state(gst.STATE_NULL) self.status = State.STOPPED self.emit(QtCore.SIGNAL('end_of_stream')) def _message_error(self, bus, message): error, debug_info = message.parse_error() minirok.logger.warning('engine error: %s (%s)', error, self.uri) self._message_eos(bus, message) def _message_async_done(self, bus, message): if self.seek_pending: self.seek_pending = False self.emit(QtCore.SIGNAL('seek_finished')) ## Engine = GStreamerEngine minirok-2.1/minirok/drag.py0000644000175000017500000000265111265733622014436 0ustar datodato#! /usr/bin/env python ## vim: fileencoding=utf-8 # # Copyright (c) 2007-2008 Adeodato Simó (dato@net.com.org.es) # Licensed under the terms of the MIT license. import os import stat from PyKDE4 import kdecore from PyQt4 import QtGui, QtCore import minirok from minirok import util ## class FileListDrag(QtGui.QDrag): MIME_TYPE = 'text/x-minirok-track-list' def __init__(self, files, parent): """Constructs a QDrag object from a python list of str paths.""" nfiles = len(files) QtGui.QDrag.__init__(self, parent) if nfiles > 0: mimedata = QtCore.QMimeData() kurllist = kdecore.KUrl.List(map(util.unicode_from_path, files)) kurllist.populateMimeData(mimedata) # flag to signal that the QMimeData comes from ourselves mimedata.setData(self.MIME_TYPE, 'True') # display a "tooltip" with the number of tracks text = '%d track%s' % (nfiles, nfiles > 1 and 's' or '') metrics = parent.fontMetrics() width = metrics.width(text) height = metrics.height() ascent = metrics.ascent() self.pixmap = QtGui.QPixmap(width+4, height) # self needed self.pixmap.fill(parent, 0, 0) painter = QtGui.QPainter(self.pixmap) painter.drawText(2, ascent+1, text) self.setMimeData(mimedata) self.setPixmap(self.pixmap) minirok-2.1/minirok/preferences.py0000644000175000017500000001220311265733622016014 0ustar datodato#! /usr/bin/env python ## vim: fileencoding=utf-8 # # Copyright (c) 2007-2009 Adeodato Simó (dato@net.com.org.es) # Licensed under the terms of the MIT license. import re from PyKDE4 import kdeui from PyQt4 import QtGui, QtCore import minirok from minirok import scrobble, util try: from minirok.ui import options1 except ImportError: from minirok.ui.error import options1 minirok.logger.warn('compiled files under ui/ missing') ## class Preferences(kdeui.KConfigSkeleton): def __init__(self, *args): kdeui.KConfigSkeleton.__init__(self, *args) self.setCurrentGroup('Playlist') self._tag_regex_value = QtCore.QString() self._tags_from_regex = self.addItemBool('TagsFromRegex', False, False) self._tag_regex = self.addItemString('TagRegex', self._tag_regex_value, '') self._tag_regex_mode = self.addItemInt('TagRegexMode', 0, 0) self.lastfm = LastfmPreferences(self) self.readConfig() @property def tags_from_regex(self): return self._tags_from_regex.value() @property def tag_regex(self): return util.kurl_to_path(self._tag_regex_value) @property def tag_regex_mode(self): _dict = { 0: 'Always', 1: 'OnRegexFail', 2: 'Never', } key = self._tag_regex_mode.value() try: return _dict[key] except KeyError: minirok.logger.error('invalid value for TagRegexMode: %s', self._tag_regex_mode.property().toString()) return _dict[0] ## class LastfmPreferences(object): def __init__(self, skel): skel.setCurrentGroup('Last.fm') self._user = QtCore.QString() self._pass = QtCore.QString() self._hs_url = QtCore.QString() self._enable = skel.addItemBool('EnableLastfm', False, False) self._user_item = skel.addItemString('LastfmUser', self._user, '') self._pass_item = skel.addItemString('LastfmPassword', self._pass, '') self._server = skel.addItemInt('LastfmServer', 0, 0) self._hs_url_item = skel.addItemString('LastfmURL', self._hs_url, '') @property def enable(self): return self._enable.value() @property def user(self): return str(self._user) @property def password(self): return str(self._pass) @property def server(self): index = self._server.value() return scrobble.Server.get_all_values()[index] @property def handshake_url(self): return str(self._hs_url) ## class Dialog(kdeui.KConfigDialog): def __init__(self, parent, name, preferences): kdeui.KConfigDialog.__init__(self, parent, name, preferences) self.setButtons(kdeui.KDialog.ButtonCode(kdeui.KDialog.Ok | kdeui.KDialog.Apply | kdeui.KDialog.Cancel)) self.general_page = GeneralPage(self, preferences) self.general_page_item = self.addPage(self.general_page, 'General') self.general_page_item.setIcon(kdeui.KIcon('minirok')) def check_valid_regex(self): regex = util.kurl_to_path(self.general_page.kcfg_TagRegex.text()) try: re.compile(regex) except re.error, e: msg = 'The introduced regular expression is not valid:\n%s' % e kdeui.KMessageBox.error(self, msg, 'Invalid regular expression') return False else: return True ## def slotButtonClicked(self, button): if (button in (kdeui.KDialog.Ok, kdeui.KDialog.Apply) and not hasattr(options1.Ui_Page, 'NO_UI') and not self.check_valid_regex()): pass # Don't let the button close the dialog. else: kdeui.KConfigDialog.slotButtonClicked(self, button) ## class GeneralPage(QtGui.QWidget, options1.Ui_Page): def __init__(self, parent, preferences): QtGui.QWidget.__init__(self, parent) self.setupUi(self) if getattr(self, 'NO_UI', False): # This Ui_Page comes from ui/error.py. return self.kcfg_LastfmServer.addItems(scrobble.Server.get_all_values()) self.connect(self.kcfg_TagsFromRegex, QtCore.SIGNAL('toggled(bool)'), self.slot_tags_from_regex_toggled) self.connect(self.kcfg_EnableLastfm, QtCore.SIGNAL('toggled(bool)'), self.slot_enable_lastfm_toggled) self.connect(self.kcfg_LastfmServer, QtCore.SIGNAL('currentIndexChanged(const QString &)'), self.slot_lastfm_server_changed) self.slot_enable_lastfm_toggled(preferences.lastfm.enable) self.slot_tags_from_regex_toggled(preferences.tags_from_regex) self.slot_lastfm_server_changed(preferences.lastfm.server) def slot_enable_lastfm_toggled(self, checked): self.lastfmFrame.setEnabled(checked) def slot_tags_from_regex_toggled(self, checked): self.regexInfoGroup.setEnabled(checked) def slot_lastfm_server_changed(self, server): # TODO: do something fancy and show the URL that'll be used for !Other? self.kcfg_LastfmURL.setEnabled(str(server) == scrobble.Server.Other) minirok-2.1/minirok/main.py0000644000175000017500000000720411265733622014444 0ustar datodato#! /usr/bin/env python ## vim: fileencoding=utf-8 # # Copyright (c) 2007-2008 Adeodato Simó (dato@net.com.org.es) # Licensed under the terms of the MIT license. import sys import errno import minirok from PyQt4 import QtGui from PyKDE4 import kdeui, kdecore ## def main(): _ = kdecore.ki18n emptyloc = kdecore.KLocalizedString() about_data = kdecore.KAboutData( minirok.__appname__, "", # catalog name _(minirok.__progname__), minirok.__version__, _(minirok.__description__), kdecore.KAboutData.License_Custom, _(minirok.__copyright__), emptyloc, # extra text minirok.__homepage__, minirok.__bts__) about_data.setLicenseText(_(minirok.__license__)) about_data.setCustomAuthorText(emptyloc, _('Please report bugs to %s.
' 'See README.Bugs for instructions.' % (minirok.__bts__, minirok.__bts__))) for name, task, email in minirok.__authors__: about_data.addAuthor(_(name), _(task), email) for name, task, email, webpage in minirok.__thanksto__: about_data.addCredit(_(name), _(task), email, webpage) options = kdecore.KCmdLineOptions() options.add('a') options.add('append', _('Try to append files to an existing Minirok instance')) options.add('+[files]', _('Files to load into the playlist')) kdecore.KCmdLineArgs.init(sys.argv, about_data) kdecore.KCmdLineArgs.addCmdLineOptions(options) args = kdecore.KCmdLineArgs.parsedArgs() count = args.count() files = [] if count > 0: from minirok import util for i in range(count): files.append(util.kurl_to_path(args.url(i))) if (args.isSet('append') and append_to_remote_minirok_successful(files)): sys.exit(0) ## # These imports happen here rather than at the top level because if gst # gets imported before the above KCmdLineArgs.init() call, it steals our # --help option from minirok import engine, main_window as mw, scrobble minirok.Globals.engine = engine.Engine() application = kdeui.KApplication() main_window = mw.MainWindow() scrobbler = scrobble.Scrobbler() scrobbler.start() if minirok._has_dbus: import dbus import dbus.service import dbus.mainloop.qt from minirok import dbusface dbus.mainloop.qt.DBusQtMainLoop(set_as_default=True) name = dbus.service.BusName('org.kde.minirok', dbus.SessionBus()) player = dbusface.Player() if files: minirok.Globals.playlist.add_files_untrusted(files, clear_playlist=True) ## if main_window.canBeRestored(1): main_window.restore(1, False) # False: do not force show() else: main_window.show() application.exec_() ## def append_to_remote_minirok_successful(files): # TODO Rewrite this function using the dbus module? from subprocess import Popen, PIPE cmdline = [ 'qdbus', 'org.kde.minirok' ] try: p = Popen(cmdline, stdout=PIPE, stderr=PIPE) except OSError, e: if e.errno == errno.ENOENT: minirok.logger.warn('could not exec %s', cmdline[0]) return False else: raise else: status = p.wait() if status != 0: minirok.logger.warn( 'could not contact with an existing Minirok instance') return False else: cmdline.extend(['/Player', 'org.kde.minirok.AppendToPlaylist', '('] + files + [')']) return not Popen(cmdline, stdout=PIPE).wait() minirok-2.1/minirok/statusbar.py0000644000175000017500000002222111265733622015524 0ustar datodato#! /usr/bin/env python ## vim: fileencoding=utf-8 # # Copyright (c) 2007-2008 Adeodato Simó (dato@net.com.org.es) # Licensed under the terms of the MIT license. from PyQt4 import QtGui, QtCore from PyKDE4 import kdeui, kdecore import minirok from minirok import engine, util from minirok.playlist import RepeatMode ## class StatusBar(kdeui.KStatusBar): SLIDER_PRESSED = object() SLIDER_MOVED = object() SLIDER_RELEASED = object() def __init__(self, *args): kdeui.KStatusBar.__init__(self, *args) self.seek_to = None self.timer = util.QTimerWithPause(self) self.blink_timer = QtCore.QTimer(self) self.blink_timer_flag = True # used in slot_blink() self.repeat = RepeatLabel(self) self.random = RandomLabel(self) self.slider = QtGui.QSlider(QtCore.Qt.Horizontal, self) self.label1 = TimeLabel(self) self.label2 = NegativeTimeLabel(self) self.slider.setTracking(False) self.slider.setMaximumWidth(150) self.slider.setFocusPolicy(QtCore.Qt.NoFocus) self.setContentsMargins(0, 0, 4, 0) self.addPermanentWidget(self.repeat, 0) self.addPermanentWidget(self.random, 0) self.addPermanentWidget(self.label1, 0) self.addPermanentWidget(self.slider, 0) self.addPermanentWidget(self.label2, 0) self.slot_stop() self._connect_timer() # this has a method 'cause we do it several times self.connect(self.blink_timer, QtCore.SIGNAL('timeout()'), self.slot_blink) self.connect(self.slider, QtCore.SIGNAL('sliderPressed()'), lambda: self.handle_slider_event(self.SLIDER_PRESSED)) self.connect(self.slider, QtCore.SIGNAL('sliderMoved(int)'), lambda x: self.handle_slider_event(self.SLIDER_MOVED, x)) self.connect(self.slider, QtCore.SIGNAL('sliderReleased()'), lambda: self.handle_slider_event(self.SLIDER_RELEASED)) self.connect(minirok.Globals.playlist, QtCore.SIGNAL('new_track'), self.slot_start) self.connect(minirok.Globals.engine, QtCore.SIGNAL('status_changed'), self.slot_engine_status_changed) self.connect(minirok.Globals.engine, QtCore.SIGNAL('seek_finished'), self.slot_engine_seek_finished) # Actions self.action_next_repeat_mode = util.create_action('action_next_repeat_mode', 'Change repeat mode', self.repeat.mousePressEvent, QtGui.QIcon(util.get_png('repeat_track_small')), 'Ctrl+T') self.action_toggle_random_mode = util.create_action('action_toggle_random_mode', 'Toggle random mode', self.random.mousePressEvent, QtGui.QIcon(util.get_png('random_small')), 'Ctrl+R') def slot_update(self): self.elapsed = minirok.Globals.engine.get_position() self.remaining = self.length - self.elapsed self.slider.setValue(self.elapsed) self.label1.set_time(self.elapsed) self.label2.set_time(self.remaining) # XXX what if length was unset def slot_start(self): tags = minirok.Globals.playlist.get_current_tags() self.length = tags.get('Length', 0) self.slider.setRange(0, self.length) self.timer.start(1000) self.slider.setEnabled(True) self.slot_update() def slot_stop(self): self.timer.stop() self.blink_timer.stop() self.slider.setEnabled(False) self.length = self.elapsed = self.remaining = 0 self.slot_update() def slot_engine_status_changed(self, new_status): if new_status == engine.State.PAUSED: self.timer.pause() self.blink_timer.start(750) elif new_status == engine.State.PLAYING: self.timer.resume() self.blink_timer.stop() elif new_status == engine.State.STOPPED: self.slot_stop() def slot_blink(self): self.blink_timer_flag = not self.blink_timer_flag if self.blink_timer_flag: self.label1.set_time(self.elapsed) self.label2.set_time(self.remaining) else: self.label1.clear() self.label2.clear() def slot_engine_seek_finished(self): self._connect_timer() def handle_slider_event(self, what, value=None): if what is self.SLIDER_PRESSED: # I'm using a disconnect/connect pair here because using # pause/resume resulted in slot_update() getting called many # seconds after resume(). Weird. self._connect_timer(disconnect=True) elif what is self.SLIDER_MOVED: self.seek_to = value self.label1.set_time(value) self.label2.set_time(self.length - value) elif what is self.SLIDER_RELEASED: if self.seek_to is not None: minirok.Globals.engine.set_position(self.seek_to) self.seek_to = None else: minirok.logger.warn('unknown slider event %r', what) def _connect_timer(self, disconnect=False): f = disconnect and self.disconnect or self.connect f(self.timer, QtCore.SIGNAL('timeout()'), self.slot_update) ## class TimeLabel(QtGui.QLabel): PREFIX = ' ' def __init__(self, *args): QtGui.QLabel.__init__(self, *args) self.setFont(kdeui.KGlobalSettings.fixedFont()) self.setSizePolicy(QtGui.QSizePolicy.Maximum, QtGui.QSizePolicy.Fixed) def set_time(self, seconds): self.setText('%s%s' % (self.PREFIX, util.fmt_seconds(seconds))) self.setFixedSize(self.sizeHint()) # make the label.clear() above DTRT class NegativeTimeLabel(TimeLabel): PREFIX = '-' ## class MultiIconLabel(QtGui.QLabel): """A clickable label that shows a series of icons. The label automatically changes the icon on click, and then emits a QtCore.SIGNAL('clicked(int)'). """ CONFIG_SECTION = 'Statusbar' CONFIG_OPTION = None def __init__(self, parent, icons=None, tooltips=[]): """Initialize the label. :param icons: a list of QPixmaps over which to iterate. :param tooltips: tooltips associated with each icon/state. """ QtGui.QLabel.__init__(self, parent) util.CallbackRegistry.register_save_config(self.save_config) self.connect(self, QtCore.SIGNAL('clicked(int)'), self.slot_clicked) if icons is not None: self.icons = list(icons) else: self.icons = [ QtGui.QPixmap() ] self.tooltips = list(tooltips) self.tooltips += [ None ] * (len(self.icons) - len(self.tooltips)) if self.CONFIG_OPTION is not None: config = kdecore.KGlobal.config().group(self.CONFIG_SECTION) value = config.readEntry(self.CONFIG_OPTION, QtCore.QVariant('0')).toString() try: self.state = int(value) - 1 except ValueError: minirok.logger.warning('invalid value %r for %s', value, self.CONFIG_OPTION) self.state = -1 else: self.state = -1 self.mousePressEvent(None) def mousePressEvent(self, event=None): self.state += 1 if self.state >= len(self.icons): self.state = 0 self.setPixmap(self.icons[self.state]) tooltip = self.tooltips[self.state] if tooltip is not None: self.setToolTip(tooltip) else: self.setToolTip('') self.emit(QtCore.SIGNAL('clicked(int)'), self.state) def slot_clicked(self, state): raise NotImplementedError, \ 'MultiIconLabel.slot_clicked must be reimplemented in subclasses.' def save_config(self): if self.CONFIG_OPTION is not None: config = kdecore.KGlobal.config().group(self.CONFIG_SECTION) config.writeEntry(self.CONFIG_OPTION, QtCore.QVariant(self.state)) class RepeatLabel(MultiIconLabel): CONFIG_OPTION = 'RepeatMode' STATES = { 0: RepeatMode.NONE, 1: RepeatMode.TRACK, 2: RepeatMode.PLAYLIST, } def __init__(self, parent): icons = [ kdeui.KIcon('go-bottom').pixmap(16, QtGui.QIcon.Active, QtGui.QIcon.Off), util.get_png('repeat_track_small'), util.get_png('repeat_playlist_small'), ] tooltips = [ 'Repeat: Off', 'Repeat: Track', 'Repeat: Playlist', ] MultiIconLabel.__init__(self, parent, icons, tooltips) def slot_clicked(self, state): minirok.Globals.playlist.repeat_mode = self.STATES[state] class RandomLabel(MultiIconLabel): CONFIG_OPTION = 'RandomMode' def __init__(self, parent): icons = [ kdeui.KIcon('go-next').pixmap(16, QtGui.QIcon.Active, QtGui.QIcon.Off), util.get_png('random_small'), ] tooltips = [ 'Random mode: Off', 'Random mode: On', ] MultiIconLabel.__init__(self, parent, icons, tooltips) def slot_clicked(self, state): minirok.Globals.playlist.random_mode = bool(state) minirok-2.1/minirok/main_window.py0000644000175000017500000001643111265733622016035 0ustar datodato#! /usr/bin/env python ## vim: fileencoding=utf-8 # # Copyright (c) 2007-2009 Adeodato Simó (dato@net.com.org.es) # Licensed under the terms of the MIT license. import os from PyQt4 import QtGui, QtCore from PyKDE4 import kio, kdeui, kdecore import minirok from minirok import left_side, preferences, right_side, statusbar, util ## class MainWindow(kdeui.KXmlGuiWindow): CONFIG_SECTION = 'MainWindow' CONFIG_OPTION_SPLITTER_STATE = 'splitterState' def __init__ (self, *args): kdeui.KXmlGuiWindow.__init__(self, *args) util.CallbackRegistry.register_save_config(self.save_config) minirok.Globals.action_collection = self.actionCollection() minirok.Globals.preferences = preferences.Preferences() self.main_view = QtGui.QSplitter(self) self.left_side = left_side.LeftSide(self.main_view) self.right_side = right_side.RightSide(self.main_view, main_window=self) policy = QtGui.QSizePolicy() policy.setHorizontalStretch(1) self.left_side.setSizePolicy(policy) policy = QtGui.QSizePolicy() policy.setHorizontalStretch(3) self.right_side.setSizePolicy(policy) self.statusbar = statusbar.StatusBar(self) self.setStatusBar(self.statusbar) # Restore splitter state config = kdecore.KGlobal.config().group(self.CONFIG_SECTION) sstate = config.readEntry(self.CONFIG_OPTION_SPLITTER_STATE, QtCore.QVariant(QtCore.QByteArray())).toByteArray() if not sstate.isEmpty(): self.main_view.restoreState(sstate) self.init_systray() self.init_actions() self.setHelpMenuEnabled(False) self.setCentralWidget(self.main_view) # self.setAutoSaveSettings() # XXX-KDE4 I don't think this is needed anymore: Check setupGUI_args = [ QtCore.QSize(900, 540), self.StandardWindowOption( # Skip StatusBar self.ToolBar | self.Keys | self.Save | self.Create) ] # If a minirokui.rc file exists in the standard location, do # not specify one for setupGUI(), else specify one if available. if kdecore.KStandardDirs.locate('appdata', 'minirokui.rc').isEmpty(): local_rc = os.path.join(os.path.dirname(minirok.__path__[0]), 'config/minirokui.rc') if os.path.isfile(local_rc): setupGUI_args.append(local_rc) self.setupGUI(*setupGUI_args) # We only want the app to exit if Quit was called from the systray icon # or from the File menu, not if the main window was closed. Use a flag # so that slot_really_quit() and queryClose() know what to do. self._flag_really_quit = False ## def init_actions(self): actionCollection = self.actionCollection() # File menu self.action_open_directory = util.create_action('action_open_directory', 'Open directory...', self.slot_open_directory, 'document-open-folder', 'Ctrl+F') self.action_quit = kdeui.KStandardAction.quit(self.slot_really_quit, actionCollection) # Help menu self.action_about = kdeui.KStandardAction.aboutApp( kdeui.KAboutApplicationDialog(None, self).show, actionCollection) self.action_about.setShortcutConfigurable(False) # Other self.action_toggle_window = util.create_action('action_toggle_window', 'Show/Hide window', self.systray.toggleActive, global_shortcut='Ctrl+Alt+M') self.action_preferences = kdeui.KStandardAction.preferences( self.slot_preferences, actionCollection) self.action_preferences.setShortcutConfigurable(False) def init_systray(self): self.systray = Systray(self) self.systray.connect(self.systray, QtCore.SIGNAL('quitSelected()'), self.slot_really_quit) self.systray.show() ## def slot_open_directory(self): """Open a dialog to select a directory, and set it in the tree view.""" # NOTE: Not using KFileDialog.getExistingDirectory() here, because # it pops up just a tree view which I don't find very useable. urls = self.left_side.path_combo.urls() current = urls.first() if not urls.isEmpty() else '~' dialog = kio.KFileDialog(kdecore.KUrl(current), 'Directories', self) dialog.setCaption('Open directory') dialog.setMode(kio.KFile.Directory) dialog.exec_() directory = dialog.selectedFile() if directory: self.left_side.path_combo.slot_set_url(directory) def slot_really_quit(self): self._flag_really_quit = True self.close() def slot_preferences(self): if kdeui.KConfigDialog.showDialog('preferences dialog'): return else: dialog = preferences.Dialog(self, 'preferences dialog', minirok.Globals.preferences) self.connect(dialog, QtCore.SIGNAL('settingsChanged(const QString &)'), util.CallbackRegistry.apply_preferences_all) dialog.show() def save_config(self): config = kdecore.KGlobal.config().group(self.CONFIG_SECTION) config.writeEntry(self.CONFIG_OPTION_SPLITTER_STATE, self.main_view.saveState()) ## def queryClose(self): finishing_session = kdeui.KApplication.kApplication().sessionSaving() if not finishing_session: # We want to save the shown/hidden status on session quit self.hide() return self._flag_really_quit or finishing_session def queryExit(self): util.CallbackRegistry.save_config_all() kdecore.KGlobal.config().sync() return True def saveProperties(self, config): config.writeEntry('docked', bool(self.isHidden())) def readProperties(self, config): self.setShown(not config.readBoolEntry('docked', False)) ## class Systray(kdeui.KSystemTrayIcon): """A KSystemTrayIcon class that calls Play/Pause on middle button clicks. It will also show the currently played track in its tooltip. """ def __init__(self, *args): kdeui.KSystemTrayIcon.__init__(self, *args) self.setIcon(self.loadIcon('minirok')) self.installEventFilter(self) self.connect(self, QtCore.SIGNAL('activated(QSystemTrayIcon::ActivationReason)'), self.slot_activated) def slot_activated(self, reason): # NB: Filtering for middle button clicks in eventFilter() below does # not seem to work. if reason == QtGui.QSystemTrayIcon.MiddleClick: minirok.Globals.action_collection.action('action_play_pause').trigger() def eventFilter(self, object_, event): if (object_ == self and event.type() == QtCore.QEvent.ToolTip): tags = minirok.Globals.playlist.get_current_tags() if tags: title = tags.get('Title') or '' artist = tags.get('Artist') or '' if artist and title: artist += ' - ' if artist or title: QtGui.QToolTip.showText(event.globalPos(), artist + title) return True return kdeui.KSystemTrayIcon.eventFilter(self, object_, event) minirok-2.1/minirok/util.py0000644000175000017500000003371411265733622014502 0ustar datodato#! /usr/bin/env python ## vim: fileencoding=utf-8 # # Copyright (c) 2007-2008 Adeodato Simó (dato@net.com.org.es) # Licensed under the terms of the MIT license. import os import re import stat import time import random from PyQt4 import QtGui, QtCore from PyKDE4 import kdeui, kdecore import minirok ## def kurl_to_path(kurl): """Convert a KURL or QString to a str in the local filesystem encoding. For KURLs, the leading file:// prefix will be stripped if present. """ if isinstance(kurl, kdecore.KUrl): kurl = kurl.pathOrUrl() return unicode(kurl).encode(minirok.filesystem_encoding) def unicode_from_path(path): """Convert from the filesystem encoding to unicode.""" if isinstance(path, unicode): return path else: try: return unicode(path, minirok.filesystem_encoding) except UnicodeDecodeError: minirok.logger.warning('cannot convert %r to %s', path, minirok.filesystem_encoding) return unicode(path, minirok.filesystem_encoding, 'replace') def fmt_seconds(seconds): """Convert a number of seconds to m:ss or h:mm:ss notation.""" try: seconds = int(seconds) except ValueError: minirok.logger.warn('invalid int passed to fmt_seconds(): %r', seconds) return seconds if seconds < 3600: return '%d:%02d' % (seconds//60, seconds%60) else: return '%d:%02d:%02d' % (seconds//3600, (seconds%3600)//60, seconds%60) def get_png(name): """Return a QPixmap of the named PNG file under $APPDATA/images. If it does not exist in $APPDATA/images, it will be assumed Minirok is running from source, and it'll be searched in `dirname __file__`/../images. Pixmaps are cached. """ if not re.search(r'\.png$', name): name += '.png' try: return _png_cache[name] except KeyError: pass for path in [ str(kdecore.KStandardDirs.locate('appdata', os.path.join('images', name))), os.path.join(os.path.dirname(__file__), '..', 'images', name) ]: if os.path.exists(path): break else: minirok.logger.warn('could not find %s', name) return _png_cache.setdefault(name, QtGui.QPixmap(path, 'PNG')) _png_cache = {} def create_action(name, text, slot, icon=None, shortcut=None, global_shortcut=None, factory=kdeui.KAction): """Helper to create KAction objects.""" action = factory(None) action.setText(text) QtCore.QObject.connect(action, QtCore.SIGNAL('triggered(bool)'), slot) minirok.Globals.action_collection.addAction(name, action) if icon is not None: action.setIcon(kdeui.KIcon(icon)) if shortcut is not None: action.setShortcut(kdeui.KShortcut(shortcut)) if global_shortcut is not None: action.setGlobalShortcut(kdeui.KShortcut(global_shortcut)) return action def playable_from_untrusted(files, warn=False): """Filter a list of untrusted paths to only include playable files. This method takes a list of paths, and drops from it files that do not exist or the engine can't play. Directories will be read and all its files included as appropriate. :param warn: If True, emit a warning for each skipped file, stating the reason; if False, debug() statements will be emitted instead. """ result = [] if warn: warn = minirok.logger.warn else: warn = minirok.logger.debug def append_path(path): try: mode = os.stat(path).st_mode except OSError, e: warn('skipping %r: %s', path, e.strerror) return if stat.S_ISDIR(mode): try: contents = sorted(os.listdir(path)) except OSError, e: warn('skipping %r: %s', path, e.strerror) else: for entry in contents: append_path(os.path.join(path, entry)) elif stat.S_ISREG(mode): if minirok.Globals.engine.can_play_path(path): if path not in result: result.append(path) else: warn('skipping %r: not a playable format', path) else: warn('skipping %r: not a regular file', path) for f in files: append_path(f) return result def contiguous_chunks(intlist): """Calculate a list of contiguous areas in a possibly unsorted list. >>> contiguous_chunks([2, 9, 3, 5, 8, 1]) [ (1, 3), (5, 1), (8, 2) ] """ if len(intlist) == 0: return [] mylist = sorted(intlist) result = [ [mylist[0], 1] ] for x in mylist[1:]: if x == sum(result[-1]): result[-1][1] += 1 else: result.append([x, 1]) return map(tuple, result) def ensure_utf8(string): """Return an UTF-8 string out of the passed string. If string is already in UTF-8, it will be returned unmodified; if string is an unicode object, it'll be encoded to UTF-8 and returned; else, it'll be assumed to be in ISO-8859-1 and returned as UTF-8. Additionally, None can be passed, in which case the empty string will be returned. """ if string is None: return '' if isinstance(string, unicode): return string.encode('utf-8') else: try: string.decode('utf-8') except UnicodeDecodeError: return string.decode('iso-8859-1').encode('utf-8') else: return string def creat_excl(path, mode=0644): """Return a write-only file object created with O_EXCL.""" fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, mode) return os.fdopen(fd, 'w') ## class CallbackRegistry(object): # TODO: rename "save_config" to something else, eg. "at_exit". SAVE_CONFIG = object() APPLY_PREFS = object() def __init__(self): self._callbacks = {} def register(self, type, callback): self._callbacks.setdefault(type, []).append(callback) def invoke_callbacks(self, type): assert type in self._callbacks for callback in self._callbacks[type]: callback() ## def register_save_config(self, callback): self.register(self.SAVE_CONFIG, callback) def register_apply_prefs(self, callback): self.register(self.APPLY_PREFS, callback) ## def save_config_all(self): self.invoke_callbacks(self.SAVE_CONFIG) def apply_preferences_all(self): self.invoke_callbacks(self.APPLY_PREFS) CallbackRegistry = CallbackRegistry() ## class QTimerWithPause(QtCore.QObject): """A QTimer with pause() and resume() methods. Note that we don't inherit from QTimer, and we just offer a limited interface: start(msecs), stop(), pause(), resume(), and setSingleShot(). The timeout() signal is emitted as normal. """ def __init__(self, *args): QtCore.QObject.__init__(self, *args) self._timer = QtCore.QTimer(self) self._timer.setSingleShot(True) self.connect(self._timer, QtCore.SIGNAL('timeout()'), self._slot_timeout) self._running = False self._duration = None # duration as given to start() self._remaining = None # what's pending, considering pauses() self._start_time = None # time we last _started() self._single_shot = False ## def start(self, msecs): self._duration = self._remaining = msecs self._start() def stop(self): self._remaining = self._duration self._timer.stop() def pause(self): if self._timer.isActive(): self._timer.stop() elapsed = time.time() - self._start_time self._remaining -= int(elapsed*1000) def resume(self): if (self._running and not self._timer.isActive()): self._start() def setSingleShot(self, value): self._single_shot = bool(value) ## def _start(self): self._running = True self._start_time = time.time() self._timer.start(self._remaining) def _slot_timeout(self): self._running = False self._remaining = self._duration self.emit(QtCore.SIGNAL('timeout()')) if not self._single_shot: self._start() ## class RandomOrderedList(list): """A list where append() inserts items at a random position.""" def append(self, item): self.insert(random.randrange(len(self)+1), item) def extend(self, seq): list.extend(self, seq) random.shuffle(self) ## class Enum(object): """An attribute-based implementation of enumerations. >>> Color = Enum('White', 'Black', 'Gray.50') >>> Color.White 'White' >>> Color.Gray50 'Gray.50' >>> Color.Red Traceback (most recent call last): ... ValueError: unknown value for enum: 'Red' >>> Color.is_valid('Blue') False >>> Color.is_valid('Gray.50') True >>> Color.is_valid('Gray50') True >>> Color.get_all_values() ['White', 'Black', 'Gray.50'] """ def __init__(self, *values): self._values = list(values) self._map = dict((re.sub(r'\W+', '', x), x) for x in self._values) def __getattr__(self, name): try: return self._map[name] except KeyError: raise ValueError('unknown value for enum: %r' % (name,)) def is_valid(self, name): return name in self._map or name in self._values def get_all_values(self): return self._values[:] ## def needs_lock(mutex_name): """Helper decorator for ThreadedWorker.""" def decorator(function): def wrapper(self, *args): mutex = getattr(self, mutex_name) mutex.lock() try: return function(self, *args) finally: mutex.unlock() return wrapper return decorator ## class ThreadedWorker(QtCore.QThread): """A thread that performs a given action on items in a queue. The thread consumes items from a queue, and stores pairs (item, result) in a "done" queue. Whenever there are done items, the thread emits a "items_ready" signal. """ def __init__(self, function): """Create a worker. :param function: The function to invoke on each item. """ QtCore.QThread.__init__(self) self._done = [] self._queue = [] self._mutex = QtCore.QMutex() # for _queue self._mutex2 = QtCore.QMutex() # for _done self._pending = QtCore.QWaitCondition() self.function = function ## @needs_lock('_mutex') def queue(self, item): self._queue.append(item) self._pending.wakeAll() @needs_lock('_mutex') def queue_many(self, items): self._queue.extend(items) if len(self._queue) > 0: self._pending.wakeAll() @needs_lock('_mutex') @needs_lock('_mutex2') def is_empty(self): """Returns True if both queues are empty.""" return len(self._queue) == 0 and len(self._done) == 0 @needs_lock('_mutex') def dequeue(self, item): try: self._queue.remove(item) except ValueError: pass @needs_lock('_mutex') @needs_lock('_mutex2') def clear_queue(self): self._done[:] = [] self._queue[:] = [] @needs_lock('_mutex2') def pop_done(self): done = self._done[:] self._done[:] = [] return done ## def run(self): while True: self._mutex.lock() try: while True: try: # We just don't pop() the item here, because after # calling self.function(), we'll want to check that the # item is still in the queue (that is, that the queue # was not cleared in the meantime). item = self._queue[0] except IndexError: self._pending.wait(self._mutex) # unlocks and re-locks else: break finally: self._mutex.unlock() result = self.function(item) self._mutex.lock() try: try: self._queue.remove(item) except ValueError: continue finally: self._mutex.unlock() self._mutex2.lock() try: self._done.append((item, result)) finally: self._mutex2.unlock() self.emit(QtCore.SIGNAL('items_ready')) ## class SearchLineWithReturnKey(kdeui.KTreeWidgetSearchLine): """A search line that doesn't forward Return key to its QTreeWidget.""" def event(self, event): # Do not let KTreeWidgetSearchLine eat our return key if (event.type() == QtCore.QEvent.KeyPress and (event.key() == QtCore.Qt.Key_Enter or event.key() == QtCore.Qt.Key_Return)): return kdeui.KLineEdit.event(self, event) else: return kdeui.KTreeWidgetSearchLine.event(self, event) ## class DelayedLineEdit(kdeui.KLineEdit): """Emits a text changed signal once the user is done writing. This class emits a "delayed_text_changed" signal once some time has passed since the last key press from the user. """ DELAY = 400 # ms SIGNAL = QtCore.SIGNAL('delayed_text_changed') def __init__(self, parent=None): kdeui.KLineEdit.__init__(self, parent) self._queued_signals = 0 self.setClearButtonShown(True) self.connect(self, QtCore.SIGNAL('textChanged(const QString &)'), self._queue_signal) def _queue_signal(self, text): self._queued_signals += 1 QtCore.QTimer.singleShot(self.DELAY, self._emit_signal) def _emit_signal(self): if self._queued_signals > 1: self._queued_signals -= 1 else: self._queued_signals = 0 self.emit(self.SIGNAL, QtCore.QString(self.text())) minirok-2.1/minirok/scrobble.py0000644000175000017500000004567111265733622015325 0ustar datodato#! /usr/bin/env python ## vim: fileencoding=utf-8 # # Copyright (c) 2007-2009 Adeodato Simó (dato@net.com.org.es) # Licensed under the terms of the MIT license. from __future__ import with_statement import os import re import time import errno import socket import string import urllib import hashlib import httplib import urlparse import threading try: import json except ImportError: import simplejson as json try: import psutil except ImportError: _has_psutil = False else: _has_psutil = True from PyQt4 import QtCore from PyKDE4 import kdecore import minirok from minirok import engine, util # TODO: Quitting Minirok while playing will not submit the current track until # the next time Minirok starts (via the spool). # the required playing time has passed. # TODO: Use KWallet? ## Server = util.Enum('Last.fm', 'Libre.fm', 'Other') ServerURL = { Server.Lastfm: 'http://post.audioscrobbler.com:80/', Server.Librefm: 'http://turtle.libre.fm:80/', } PROTOCOL_VERSION = '1.2.1' CLIENT_IDENTIFIER = 'mrk' TRACK_MIN_LENGTH = 30 TRACK_SUBMIT_SECONDS = 240 TRACK_SUBMIT_PERCENT = 0.5 SOURCE_USER = 'P' SOURCE_BROADCAST = 'R' SOURCE_PERSONALIZED = 'E' SOURCE_LASTFM = 'L' SOURCE_UNKNOWN = 'U' MAX_FAILURES = 3 MAX_SLEEP_MINUTES = 120 MAX_TRACKS_AT_ONCE = 50 APPDATA_SCROBBLE = 'scrobble' APPDATA_SCROBBLE_LOCK = 'scrobble.lock' ## class Submission(object): class RequiredDataMissing(Exception): pass class TrackTooShort(Exception): pass def __init__(self, tag_dict): """Create a Submission object from a dict of tags. tag_dict should be a dictionary as returned by get_current_tags() in the global Playlist object, i.e. contain 'Title', 'Artist', etc. """ if not all(tag_dict[x] for x in ['Title', 'Artist', 'Length']): raise self.RequiredDataMissing() elif tag_dict['Length'] < TRACK_MIN_LENGTH: raise self.TrackTooShort() self.path = None self.length = tag_dict['Length'] self.start_time = int(time.time()) self.param = { 'm': '', 'r': '', 'o': SOURCE_USER, 'l': str(self.length), 'i': str(self.start_time), 'n': util.ensure_utf8(tag_dict['Track']), 't': util.ensure_utf8(tag_dict['Title']), 'b': util.ensure_utf8(tag_dict['Album']), 'a': util.ensure_utf8(tag_dict['Artist']), } def get_params(self, i=0): return dict(('%s[%d]' % (k, i), v) for k, v in self.param.items()) def get_now_playing_params(self): return dict((k, self.param[k]) for k in list('atblnm')) def serialize(self): return json.dumps(self.param, indent=4) @classmethod def load_from_file(cls, path): with open(path) as f: try: param = json.load(f) except ValueError: return None else: # TODO: could it be possible to get json to give us str objects? param = dict((k, util.ensure_utf8(param[k])) for k in param) if set(param.keys()) == set('mrolintba'): obj = cls.__new__(cls) obj.path = path obj.param = param obj.length = int(param['l']) obj.start_time = int(param['i']) return obj else: return None ## class HandshakeFatalError(Exception): pass ## class Request(object): def __init__(self, url, params): self.body = [] self.error = None self.failed = False url = urlparse.urlparse(url) conn = httplib.HTTPConnection(url.netloc) try: conn.request('POST', url.path, urllib.urlencode(params), { 'Content-Type': 'application/x-www-form-urlencoded' }) except socket.error, e: self.failed = True self.error = e.args[1] # Thank you for not providing e.message else: resp = conn.getresponse() if resp.status != httplib.OK: self.failed = True self.error = resp.reason else: self.body = resp.read().rstrip('\n').split('\n') if not self.body: self.failed = True self.error = 'no response received from server' elif self.body[0].split()[0] != 'OK': self.failed = True self.error = re.sub(r'^FAILED\s+', '', self.body[0]) class HandshakeRequest(Request): def __init__(self, url, params): super(HandshakeRequest, self).__init__(url, params) if self.failed: if re.search(r'^(BANNED|BADTIME)', self.error): raise HandshakeFatalError(self.error) elif len(self.body) != 4: self.failed = True self.error = 'unexpected response from scrobbler server:\n%r' % ( self.body,) ## class ProcInfo(object): def __init__(self, pid=None): if pid is None: pid = os.getpid() d = self.data = {} if not _has_psutil: d['pid'] = pid d['version'] = '1.0' else: d['pid'] = pid d['version'] = '1.1' try: d['cmdline'] = psutil.Process(pid).cmdline except psutil.error: d['version'] = '1.0' def serialize(self): return json.dumps(self.data, indent=4) def isRunning(self): if self.data['version'] == '1.0': try: os.kill(self.data['pid'], 0) except OSError, e: return (e.errno != errno.ESRCH) # ESRCH: No such PID else: return True elif self.data['version'] == '1.1': try: proc = psutil.Process(self.data['pid']) except psutil.error.NoSuchProcess: return False else: return proc.cmdline == self.data['cmdline'] @classmethod def load_from_fileobj(cls, fileobj): try: param = json.load(fileobj) except ValueError: return None else: version = param.get('version', None) if version == '1.0': keys = ['version', 'pid'] elif version == '1.1': if _has_psutil: keys = ['version', 'pid', 'cmdline'] else: # Downgrade format param['version'] = '1.0' keys = ['version', 'pid'] else: return None obj = cls.__new__(cls) try: obj.data = dict((k, param[k]) for k in keys) except KeyError: return None else: return obj ## class Scrobbler(QtCore.QObject, threading.Thread): def __init__(self): QtCore.QObject.__init__(self) threading.Thread.__init__(self) self.setDaemon(True) self.user = None self.password_hash = None self.handshake_url = None self.session_key = None self.scrobble_url = None self.now_playing_url = None self.failure_count = 0 self.pause_duration = 1 # minutes self.scrobble_queue = [] self.current_track = None self.mutex = threading.Lock() self.event = threading.Event() self.timer = util.QTimerWithPause() self.configured = threading.Condition() self.timer.setSingleShot(True) util.CallbackRegistry.register_apply_prefs(self.apply_preferences) self.apply_preferences() # Connect signals/slots, read user/passwd appdata = str(kdecore.KGlobal.dirs().saveLocation('appdata')) do_queue = False self.spool = os.path.join(appdata, APPDATA_SCROBBLE) # Spool directory handling: create it if it doesn't exist... if not os.path.isdir(self.spool): try: os.mkdir(self.spool) except OSError, e: minirok.logger.error('could not create scrobbling spool: %s', e) self.spool = None # ... else ensure it is readable and writable elif not os.access(self.spool, os.R_OK | os.W_OK): minirok.logger.error('scrobbling spool is not readable/writable') self.spool = None # If not, we try to assess whether this Minirok instance should try to # submit the existing entries, if any. Supposedly, the Last.fm server # has some support for detecting duplicate submissions, but we're # adviced not to rely on it (<4A7FECF7.5030100@last.fm>), so we use a # lock file to signal that some Minirok process is taking care of the # submissions from the spool directory. (This scheme, I realize, # doesn't get all corner cases right, but will have to suffice for now. # For example, if Minirok A starts, then Minirok B starts, and finally # Minirok A quits and Minirok C starts, Minirok B and C will end up # both trying to submit B's entries that haven't been able to be # submitted yet. There's also the race-condition-du-jour, of course.) else: scrobble_lock = os.path.join(appdata, APPDATA_SCROBBLE_LOCK) try: lockfile = open(scrobble_lock) except IOError, e: if e.errno == errno.ENOENT: do_queue = True else: raise else: proc = ProcInfo.load_from_fileobj(lockfile) if proc and proc.isRunning(): minirok.logger.info( 'Minirok already running (pid=%d), ' 'not scrobbling existing items', proc.data['pid']) else: do_queue = True if do_queue: self.lock_file = scrobble_lock with open(self.lock_file, 'w') as lock: lock.write(ProcInfo().serialize()) files = [ os.path.join(self.spool, x) for x in os.listdir(self.spool) ] tracks = sorted( [ t for t in map(Submission.load_from_file, files) if t is not None ], key=lambda t: t.start_time) if tracks: self.scrobble_queue.extend(tracks) else: self.lock_file = None util.CallbackRegistry.register_save_config(self.cleanup) def cleanup(self): if self.lock_file is not None: try: os.unlink(self.lock_file) except: pass def slot_new_track(self): self.timer.stop() self.current_track = None tags = minirok.Globals.playlist.get_current_tags() try: self.current_track = Submission(tags) except Submission.RequiredDataMissing, e: minirok.logger.info('track missing required tags, not scrobbling') except Submission.TrackTooShort, e: minirok.logger.info('track shorter than %d seconds, ' 'not scrobbling', TRACK_MIN_LENGTH) else: runtime = min(TRACK_SUBMIT_SECONDS, self.current_track.length * TRACK_SUBMIT_PERCENT) self.timer.start(runtime * 1000) with self.mutex: self.event.set() def slot_engine_status_changed(self, new_status): if new_status == engine.State.PAUSED: self.timer.pause() elif new_status == engine.State.PLAYING: self.timer.resume() elif new_status == engine.State.STOPPED: self.timer.stop() self.current_track = None with self.mutex: self.event.set() def slot_timer_timeout(self): if not self.isAlive(): # Abort this function if the scrobbling thread is not running; this # happens if we received a BANNED or BADTIME from the server. In # such cases, it's probably not a bad idea not to write anything to # disk. (Well, supposedly there's precious data we could save in # the case of BANNED, and submit it again with a fixed version, hm.) return with self.mutex: self.scrobble_queue.append(self.current_track) if self.spool is not None: path = self.write_track_to_spool(self.current_track) if path is None: minirok.logger.warn( 'could not create file in scrobbling spool') else: self.current_track.path = path self.current_track = None minirok.logger.debug('track queued for scrobbling') # XXX def write_track_to_spool(self, track): path = os.path.join(self.spool, str(track.start_time)) for x in [''] + list(string.ascii_lowercase): try: f = util.creat_excl(path + x) except OSError, e: if e.errno != errno.EEXIST: raise else: f.write(track.serialize()) f.flush() # Otherwise the write() syscall happens after fsync() os.fsync(f.fileno()) f.close() return path + x ## def run(self): if self.user is None: # We're not configured to run, so we hang on here. with self.configured: self.configured.wait() if self.scrobble_queue: # Any tracks loaded from spool? with self.mutex: self.event.set() while True: if self.session_key is None: try: self.do_handshake() except HandshakeFatalError, e: minirok.logger.error('aborting scrobbler: %s', e) return self.event.wait() with self.mutex: self.event.clear() current_track = self.current_track ## while self.scrobble_queue: params = { 's': self.session_key } with self.mutex: tracks = self.scrobble_queue[0:MAX_TRACKS_AT_ONCE] for i, track in enumerate(tracks): params.update(track.get_params(i)) req = Request(self.scrobble_url, params) if req.failed: if req.error.startswith('BADSESSION'): self.session_key = None # Trigger re-handshake else: minirok.logger.info('scrobbling %d track(s) failed: %s', len(tracks), req.error) self.failure_count += 1 if self.failure_count >= MAX_FAILURES: self.session_key = None break else: minirok.logger.debug('scrobbled %d track(s) successfully', len(tracks)) # XXX for t in tracks: if t.path is not None: try: os.unlink(t.path) except OSError, e: if e.errno != errno.ENOENT: raise with self.mutex: self.scrobble_queue[0:len(tracks)] = [] ## if current_track is not None and self.session_key is not None: params = { 's': self.session_key } params.update(current_track.get_now_playing_params()) req = Request(self.now_playing_url, params) if req.failed: minirok.logger.info( 'could not send "now playing" information: %s', req.error) if req.error.startswith('BADSESSION'): self.session_key = None # Trigger re-handshake else: self.failure_count += 1 if self.failure_count >= MAX_FAILURES: self.session_key = None else: minirok.logger.debug('sent "now playing" information successfully') # XXX ## if self.session_key is None: # Ensure we retry pending actions as soon # as we've successfully handshaked again. with self.mutex: self.event.set() def do_handshake(self): while True: now = str(int(time.time())) params = { 'hs': 'true', 'p': PROTOCOL_VERSION, 'c': CLIENT_IDENTIFIER, 'v': minirok.__version__, 'u': self.user, 't': now, 'a': hashlib.md5(self.password_hash + now).hexdigest(), } req = HandshakeRequest(self.handshake_url, params) if req.failed: if re.search(r'^BADAUTH', req.error): minirok.logger.warn( 'scrobbler handshake failed: bad password') with self.configured: self.configured.wait() else: minirok.logger.info('scrobbler handshake failed (%s), ' 'retrying in %d minute(s)', req.error, self.pause_duration) time.sleep(self.pause_duration * 60) if self.pause_duration < MAX_SLEEP_MINUTES: self.pause_duration = min(MAX_SLEEP_MINUTES, self.pause_duration * 2) else: self.failure_count = 0 self.pause_duration = 1 self.session_key = req.body[1] self.scrobble_url = req.body[3] self.now_playing_url = req.body[2] minirok.logger.debug('scrobbling handshake successful') # XXX break ## def apply_preferences(self): # TODO: what if there's a queue and we get disabled? prefs = minirok.Globals.preferences.lastfm if prefs.enable: func = self.connect self.user = prefs.user # TODO: The password is stored in plain in the configuration file.. self.password_hash = hashlib.md5(prefs.password).hexdigest() try: self.handshake_url = ServerURL[prefs.server] except KeyError: self.handshake_url = prefs.handshake_url self.session_key = None with self.configured: self.configured.notify() else: func = self.disconnect func(minirok.Globals.playlist, QtCore.SIGNAL('new_track'), self.slot_new_track) func(minirok.Globals.engine, QtCore.SIGNAL('status_changed'), self.slot_engine_status_changed) func(self.timer, QtCore.SIGNAL('timeout()'), self.slot_timer_timeout) minirok-2.1/minirok/right_side.py0000644000175000017500000000377611265733622015653 0ustar datodato#! /usr/bin/env python ## vim: fileencoding=utf-8 # # Copyright (c) 2007-2008 Adeodato Simó (dato@net.com.org.es) # Licensed under the terms of the MIT license. from PyKDE4 import kdeui from PyQt4 import QtGui, QtCore import minirok from minirok import playlist, proxy ## class RightSide(QtGui.QWidget): def __init__(self, parent, main_window): QtGui.QWidget.__init__(self, parent) self.proxy = playlist.Proxy() self.playlist = playlist.Playlist() self.playlist_view = playlist.PlaylistView(self) self.stretchtoolbar = QtGui.QWidget() self.playlist_search = proxy.LineWidget() self.toolbar = kdeui.KToolBar('playlistToolBar', main_window, QtCore.Qt.BottomToolBarArea) self.proxy.setFilterKeyColumn(-1) # all self.proxy.setSourceModel(self.playlist) self.playlist_view.setModel(self.proxy) self.playlist_search.searchLine().setProxyModel(self.proxy) self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) self.playlist.selection_model = self.playlist_view.selectionModel() # ... vlayout = QtGui.QVBoxLayout() vlayout.setSpacing(0) vlayout.setContentsMargins(4, 4, 4, 0) vlayout.addWidget(self.playlist_search) vlayout.addWidget(self.playlist_view) vlayout.addWidget(self.stretchtoolbar) self.setLayout(vlayout) hlayout = QtGui.QHBoxLayout() hlayout.addStretch() hlayout.addWidget(self.toolbar) hlayout.setContentsMargins(0, 0, 0, 0) self.stretchtoolbar.setLayout(hlayout) self.connect(self.playlist_search.searchLine(), QtCore.SIGNAL('returnPressed(const QString &)'), self.slot_play_first_visible) minirok.Globals.playlist = self.playlist def slot_play_first_visible(self, string): if len(unicode(string).strip()) > 0: index = self.proxy.index(0, 0) self.proxy.slot_activate_index(index) minirok-2.1/minirok/ui/0000755000175000017500000000000011265736104013556 5ustar datodatominirok-2.1/minirok/ui/options1.py0000644000175000017500000001651511265736104015714 0ustar datodato#!/usr/bin/env python # coding=UTF-8 # # Generated by pykdeuic4 from options1.ui on Fri Oct 16 01:20:20 2009 # # WARNING! All changes to this file will be lost. from PyKDE4 import kdecore from PyKDE4 import kdeui from PyQt4 import QtCore, QtGui class Ui_Page(object): def setupUi(self, Page): Page.setObjectName("Page") Page.resize(502, 673) self.verticalLayout_3 = QtGui.QVBoxLayout(Page) self.verticalLayout_3.setObjectName("verticalLayout_3") self.playlistGroup = QtGui.QGroupBox(Page) self.playlistGroup.setObjectName("playlistGroup") self.vboxlayout = QtGui.QVBoxLayout(self.playlistGroup) self.vboxlayout.setObjectName("vboxlayout") self.kcfg_TagsFromRegex = QtGui.QCheckBox(self.playlistGroup) self.kcfg_TagsFromRegex.setObjectName("kcfg_TagsFromRegex") self.vboxlayout.addWidget(self.kcfg_TagsFromRegex) self.regexInfoGroup = QtGui.QGroupBox(self.playlistGroup) self.regexInfoGroup.setFlat(True) self.regexInfoGroup.setObjectName("regexInfoGroup") self.vboxlayout1 = QtGui.QVBoxLayout(self.regexInfoGroup) self.vboxlayout1.setObjectName("vboxlayout1") self.kcfg_TagRegex = QtGui.QLineEdit(self.regexInfoGroup) self.kcfg_TagRegex.setObjectName("kcfg_TagRegex") self.vboxlayout1.addWidget(self.kcfg_TagRegex) self.kcfg_TagRegexMode = KButtonGroup(self.regexInfoGroup) self.kcfg_TagRegexMode.setFlat(True) self.kcfg_TagRegexMode.setObjectName("kcfg_TagRegexMode") self.vboxlayout2 = QtGui.QVBoxLayout(self.kcfg_TagRegexMode) self.vboxlayout2.setObjectName("vboxlayout2") self.radio1 = QtGui.QRadioButton(self.kcfg_TagRegexMode) self.radio1.setObjectName("radio1") self.vboxlayout2.addWidget(self.radio1) self.radio2 = QtGui.QRadioButton(self.kcfg_TagRegexMode) self.radio2.setObjectName("radio2") self.vboxlayout2.addWidget(self.radio2) self.radio3 = QtGui.QRadioButton(self.kcfg_TagRegexMode) self.radio3.setObjectName("radio3") self.vboxlayout2.addWidget(self.radio3) self.vboxlayout1.addWidget(self.kcfg_TagRegexMode) self.vboxlayout.addWidget(self.regexInfoGroup) self.verticalLayout_3.addWidget(self.playlistGroup) self.lastfmGroup = QtGui.QGroupBox(Page) self.lastfmGroup.setObjectName("lastfmGroup") self.verticalLayout_2 = QtGui.QVBoxLayout(self.lastfmGroup) self.verticalLayout_2.setObjectName("verticalLayout_2") self.kcfg_EnableLastfm = QtGui.QCheckBox(self.lastfmGroup) self.kcfg_EnableLastfm.setObjectName("kcfg_EnableLastfm") self.verticalLayout_2.addWidget(self.kcfg_EnableLastfm) self.lastfmFrame = QtGui.QFrame(self.lastfmGroup) self.lastfmFrame.setFrameShape(QtGui.QFrame.NoFrame) self.lastfmFrame.setFrameShadow(QtGui.QFrame.Raised) self.lastfmFrame.setObjectName("lastfmFrame") self.verticalLayout = QtGui.QVBoxLayout(self.lastfmFrame) self.verticalLayout.setObjectName("verticalLayout") self.gridLayout = QtGui.QGridLayout() self.gridLayout.setObjectName("gridLayout") self.label = QtGui.QLabel(self.lastfmFrame) self.label.setObjectName("label") self.gridLayout.addWidget(self.label, 0, 0, 1, 1) self.kcfg_LastfmUser = QtGui.QLineEdit(self.lastfmFrame) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.kcfg_LastfmUser.sizePolicy().hasHeightForWidth()) self.kcfg_LastfmUser.setSizePolicy(sizePolicy) self.kcfg_LastfmUser.setObjectName("kcfg_LastfmUser") self.gridLayout.addWidget(self.kcfg_LastfmUser, 0, 1, 1, 1) spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) self.gridLayout.addItem(spacerItem, 0, 2, 1, 1) self.label_2 = QtGui.QLabel(self.lastfmFrame) self.label_2.setObjectName("label_2") self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1) self.kcfg_LastfmPassword = QtGui.QLineEdit(self.lastfmFrame) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.kcfg_LastfmPassword.sizePolicy().hasHeightForWidth()) self.kcfg_LastfmPassword.setSizePolicy(sizePolicy) self.kcfg_LastfmPassword.setEchoMode(QtGui.QLineEdit.Password) self.kcfg_LastfmPassword.setObjectName("kcfg_LastfmPassword") self.gridLayout.addWidget(self.kcfg_LastfmPassword, 1, 1, 1, 1) spacerItem1 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) self.gridLayout.addItem(spacerItem1, 1, 2, 1, 1) self.label_3 = QtGui.QLabel(self.lastfmFrame) self.label_3.setObjectName("label_3") self.gridLayout.addWidget(self.label_3, 2, 0, 1, 1) self.kcfg_LastfmServer = QtGui.QComboBox(self.lastfmFrame) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.kcfg_LastfmServer.sizePolicy().hasHeightForWidth()) self.kcfg_LastfmServer.setSizePolicy(sizePolicy) self.kcfg_LastfmServer.setObjectName("kcfg_LastfmServer") self.gridLayout.addWidget(self.kcfg_LastfmServer, 2, 1, 1, 1) spacerItem2 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) self.gridLayout.addItem(spacerItem2, 2, 2, 1, 1) spacerItem3 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Minimum) self.gridLayout.addItem(spacerItem3, 3, 0, 1, 1) self.kcfg_LastfmURL = QtGui.QLineEdit(self.lastfmFrame) self.kcfg_LastfmURL.setObjectName("kcfg_LastfmURL") self.gridLayout.addWidget(self.kcfg_LastfmURL, 3, 1, 1, 2) self.verticalLayout.addLayout(self.gridLayout) self.verticalLayout_2.addWidget(self.lastfmFrame) self.verticalLayout_3.addWidget(self.lastfmGroup) spacerItem4 = QtGui.QSpacerItem(17, 198, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) self.verticalLayout_3.addItem(spacerItem4) self.retranslateUi(Page) QtCore.QMetaObject.connectSlotsByName(Page) def retranslateUi(self, Page): Page.setWindowTitle(kdecore.i18n("Configuring Minirok")) self.playlistGroup.setTitle(kdecore.i18n("Playlist")) self.kcfg_TagsFromRegex.setText(kdecore.i18n("Use a ®ular expression to guess tags from the filename")) self.radio1.setText(kdecore.i18n("Use this regex to populate the playlist initially,\n" "but still read the tags in the background")) self.radio2.setText(kdecore.i18n("Do not read tags if the regex matches")) self.radio3.setText(kdecore.i18n("Never read tags from files")) self.lastfmGroup.setTitle(kdecore.i18n("Last.fm")) self.kcfg_EnableLastfm.setText(kdecore.i18n("Submit played tracks to &Last.fm")) self.label.setText(kdecore.i18n("User")) self.label_2.setText(kdecore.i18n("Password")) self.label_3.setText(kdecore.i18n("Server")) from PyKDE4.kdeui import KButtonGroup minirok-2.1/minirok/ui/Makefile0000644000175000017500000000023611265733622015221 0ustar datodatoPYFILES = $(patsubst %.ui,%.py,$(wildcard *.ui)) all: $(PYFILES) clean: rm -f $(PYFILES) $(patsubst %.py,%.pyc,$(PYFILES)) %.py: %.ui pykdeuic4 -o $@ $^ minirok-2.1/minirok/ui/error.py0000644000175000017500000000111511265733622015261 0ustar datodato#! /usr/bin/env python ## vim: fileencoding=utf-8 # # Copyright (c) 2008 Adeodato Simó (dato@net.com.org.es) # Licensed under the terms of the MIT license. from PyQt4 import QtGui class Ui(object): NO_UI = True def setupUi(self, widget): self.vboxlayout = QtGui.QVBoxLayout(widget) self.vboxlayout.addWidget(QtGui.QLabel('''\ You are running Minirok from the source branch without having compiled the UI files. Please run `make ui` in the top level directory. You will need pykdeuic4, from the python-kde4-dev package. ''')) class options1: Ui_Page = Ui minirok-2.1/minirok/ui/options1.ui0000644000175000017500000001735211265733622015703 0ustar datodato Page 0 0 502 673 Configuring Minirok Playlist Use a &regular expression to guess tags from the filename true true Use this regex to populate the playlist initially, but still read the tags in the background Do not read tags if the regex matches Never read tags from files Last.fm Submit played tracks to &Last.fm QFrame::NoFrame QFrame::Raised User 0 0 Qt::Horizontal 40 20 Password 0 0 QLineEdit::Password Qt::Horizontal 40 20 Server 0 0 Qt::Horizontal 40 20 Qt::Horizontal QSizePolicy::Minimum 40 20 kcfg_EnableLastfm lastfmFrame Qt::Vertical 17 198 KButtonGroup QGroupBox
kbuttongroup.h
1
minirok-2.1/minirok/ui/__init__.py0000644000175000017500000000000011265733622015657 0ustar datodatominirok-2.1/minirok/playlist.py0000644000175000017500000015636611265733622015377 0ustar datodato#! /usr/bin/env python ## vim: fileencoding=utf-8 # # Copyright (c) 2007-2008 Adeodato Simó (dato@net.com.org.es) # Licensed under the terms of the MIT license. import os import re import errno from PyQt4.QtCore import Qt from PyQt4 import QtGui, QtCore from PyKDE4 import kdeui, kdecore import minirok from minirok import drag, engine, proxy, tag_reader, util ## class Playlist(QtCore.QAbstractTableModel): # This is the value self.current_item has whenver just the first item on # the playlist should be used. Only set to this value when the playlist # contains items! FIRST_ITEM = object() RoleQueuePosition = Qt.UserRole + 1 RoleQueryIsCurrent = Qt.UserRole + 2 RoleQueryIsPlaying = Qt.UserRole + 3 RoleQueryIsStopAfter = Qt.UserRole + 4 def __init__(self, parent=None): QtCore.QAbstractTableModel.__init__(self, parent) util.CallbackRegistry.register_save_config(self.save_config) util.CallbackRegistry.register_apply_prefs(self.apply_preferences) # Core model stuff self._itemlist = [] self._row_count = 0 self._empty_model_index = QtCore.QModelIndex() self._column_count = len(PlaylistItem.ALLOWED_TAGS) self.queue = [] self.visualizer_rect = None self.stop_mode = StopMode.NONE self.tag_reader = tag_reader.TagReader() self.random_queue = util.RandomOrderedList() self.tag_reader.start() # these have a property() below self._stop_after = None self._repeat_mode = RepeatMode.NONE self._random_mode = False self._current_item = None self._currently_playing = None self.connect(self, QtCore.SIGNAL('list_changed'), self.slot_list_changed) self.connect(self.tag_reader, QtCore.SIGNAL('items_ready'), self.slot_update_tags) self.connect(minirok.Globals.engine, QtCore.SIGNAL('status_changed'), self.slot_engine_status_changed) self.connect(minirok.Globals.engine, QtCore.SIGNAL('end_of_stream'), self.slot_engine_end_of_stream) self.init_actions() self.init_undo_stack() self.apply_preferences() self.load_saved_playlist() # XXX This is dataChanged() abuse: there are a bunch of places in which # the model wants to say: "my state (but not my data) changed somehow, # you may want to redraw your visible parts if you're paying attention # to state". I don't know of a method in the view that will do that # (redisplay the visible part calling with the appropriate drawRow() # and Delegate.paint() calls, without needing to refetch data()), so # I'm abusing dataChanged(0, 1) for this purpose, which seems to work! self.connect(self, QtCore.SIGNAL('repaint_needed'), lambda: self.my_emit_dataChanged(0, 1)) ## """Model functions.""" def rowCount(self, parent=None): if parent is None or parent == self._empty_model_index: return self._row_count else: return 0 # as per QAbstractItemModel::rowCount docs def columnCount(self, parent=None): return self._column_count def data(self, index, role): ret = None row = index.row() column = index.column() if (not index.isValid() or row > self._row_count or column > self._column_count): ret = None elif role == Qt.DisplayRole: ret = QtCore.QString(self._itemlist[row].tag_by_index(column) or '') elif role == Qt.TextAlignmentRole: c = PlaylistItem.ALLOWED_TAGS[column] if c == 'Track': ret = Qt.AlignHCenter elif c == 'Length': ret = Qt.AlignRight elif role == self.RoleQueuePosition: ret = self._itemlist[row].queue_position or 0 elif role == self.RoleQueryIsCurrent: ret = self._itemlist[row] is self.current_item elif role == self.RoleQueryIsPlaying: ret = self._itemlist[row] is self.currently_playing elif role == self.RoleQueryIsStopAfter: ret = self._itemlist[row] is self.stop_after if ret is not None: return QtCore.QVariant(ret) else: return QtCore.QVariant() def headerData(self, section, orientation, role): if (role == Qt.DisplayRole and orientation == Qt.Horizontal): return QtCore.QVariant( QtCore.QString(PlaylistItem.ALLOWED_TAGS[section])) else: return QtCore.QVariant() ## """Drag and drop functions.""" PLAYLIST_DND_MIME_TYPE = 'application/x-minirok-playlist-dnd' def supportedDropActions(self): return Qt.CopyAction | Qt.MoveAction def mimeTypes(self): types = QtCore.QStringList() types.append('text/uri-list') types.append(self.PLAYLIST_DND_MIME_TYPE) return types def flags(self, index): if index.isValid(): return (Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled) else: return Qt.ItemIsDropEnabled def mimeData(self, indexes): """Encodes a list of the rows in indexes.""" mimedata = QtCore.QMimeData() bytearray = QtCore.QByteArray() datastream = QtCore.QDataStream(bytearray, QtCore.QIODevice.WriteOnly) rows = set(x.row() for x in indexes) datastream.writeUInt32(len(rows)) for row in rows: datastream.writeUInt32(row) mimedata.setData(self.PLAYLIST_DND_MIME_TYPE, bytearray) return mimedata def dropMimeData(self, mimedata, action, row, column, index): if mimedata.hasUrls(): files = map(util.kurl_to_path, kdecore.KUrl.List.fromMimeData(mimedata)) if not mimedata.hasFormat(drag.FileListDrag.MIME_TYPE): # Drop does not come from ourselves, so: files = util.playable_from_untrusted(files, warn=False) if (QtGui.QApplication.keyboardModifiers() & Qt.ControlModifier): row = -1 self.add_files(files, position=row) return True elif mimedata.hasFormat(self.PLAYLIST_DND_MIME_TYPE): bytearray = mimedata.data(self.PLAYLIST_DND_MIME_TYPE) datastream = QtCore.QDataStream(bytearray, QtCore.QIODevice.ReadOnly) rows = set(datastream.readUInt32() for x in range(datastream.readUInt32())) if row < 0: row = self._row_count # now, we remove items after the drop, so... row -= len(filter(lambda r: r <= row, rows)) self.undo_stack.beginMacro('move ' + _n_tracks_str(len(rows))) try: removecmd = RemoveItemsCmd(self, rows, do_queue=False) InsertItemsCmd(self, row, removecmd.get_items(), do_queue=False) finally: self.undo_stack.endMacro() # restore the selection: better UI experience top = self.index(row, 0, QtCore.QModelIndex()) bottom = self.index(row + len(rows) - 1, 0, QtCore.QModelIndex()) self.selection_model.select(QtGui.QItemSelection(top, bottom), QtGui.QItemSelectionModel.Rows | QtGui.QItemSelectionModel.ClearAndSelect) self.selection_model.setCurrentIndex( min(rows) > row and top or bottom, # \o/ QtGui.QItemSelectionModel.Rows | QtGui.QItemSelectionModel.NoUpdate) return True else: return False ## """Adding and removing items to _itemlist. (NB: No other function should modify _itemlist directly.) """ def insert_items(self, position, items): # if currently_playing is absent, we'll check whether # it's getting re-added in this call current_item = None if (self.current_item in (None, self.FIRST_ITEM) and self.currently_playing is not None): playing_path = self.currently_playing.path else: playing_path = None try: nitems = len(items) for item in self._itemlist[position:]: item.position += nitems for i, item in enumerate(items): item.position = position + i if (playing_path is not None and playing_path == item.path): current_item = item playing_path = None self.beginInsertRows(QtCore.QModelIndex(), position, position + nitems - 1) self._itemlist[position:0] = items self._row_count += nitems finally: self.endInsertRows() self.random_queue.extend(x for x in items if not x.already_played) self.tag_reader.queue_many(x for x in items if x.needs_tag_reader) self.emit(QtCore.SIGNAL('list_changed')) if current_item is not None: self.current_item = self.currently_playing = current_item def remove_items(self, position, amount): items = self._itemlist[position:position+amount] for item in items: if item.needs_tag_reader: self.tag_reader.dequeue(item) if not item.already_played: try: self.random_queue.remove(item) except ValueError: pass if item is self.current_item: self.current_item = self.FIRST_ITEM item.position = None for item in self._itemlist[position+amount:]: item.position -= amount try: self.beginRemoveRows(QtCore.QModelIndex(), position, position + amount - 1) self._itemlist[position:position+amount] = [] self._row_count -= amount finally: self.endRemoveRows() self.emit(QtCore.SIGNAL('list_changed')) return items def clear_itemlist(self): self.current_item = None self.random_queue[:] = [] self.tag_reader.clear_queue() items = self._itemlist[:] self._row_count = 0 self._itemlist[:] = [] self.reset() self.emit(QtCore.SIGNAL('list_changed')) return items ## """Initialization.""" def init_actions(self): self.action_play = util.create_action('action_play', 'Play', self.slot_play, 'media-playback-start') self.action_pause = util.create_action('action_pause', 'Pause', self.slot_pause, 'media-playback-pause', factory=kdeui.KToggleAction) self.action_play_pause = util.create_action('action_play_pause', 'Play/Pause', self.slot_play_pause, 'media-playback-start', 'Ctrl+P', 'Ctrl+Alt+P', factory=kdeui.KToggleAction) # The following actions have their global shortcut set to the empty # string. This allows the user to configure a global shortcut for these # actions, without us having to provide a default one (and polluting # the global shortcut namespace). self.action_stop = util.create_action('action_stop', 'Stop', self.slot_stop, 'media-playback-stop', 'Ctrl+O', '', factory=StopAction) self.action_next = util.create_action('action_next', 'Next', self.slot_next, 'media-skip-forward', 'Ctrl+N', '') self.action_previous = util.create_action('action_previous', 'Previous', self.slot_previous, 'media-skip-backward', 'Ctrl+I', '') # Note: the icon here is named minirok_foo-bar and not minirok-foo-bar, # because if it isn't found, minirok-* seems to select the minirok.png # icon automatically. And I'd rather have the "unknown icon" icon instead. self.action_clear = util.create_action('action_clear_playlist', 'Clear playlist', self.slot_clear, 'minirok_playlist-clear', 'Ctrl+L') self.action_toggle_stop_after_current = util.create_action( 'action_toggle_stop_after_current', 'Stop after current', self.slot_toggle_stop_after_current, 'media-playback-stop', 'Ctrl+K')#, 'Ctrl+I+K') def init_undo_stack(self): self.undo_stack = QtGui.QUndoStack(self) # Undo/Redo action handling: this is tricky. We would want for the # actions in the toolbar to be QAction objects as returned by # createUndoAction() and createRedoAction(), because these do enable # and disable themselves as appropriate depending on where there's # stuff to undo or redo, which is nice. However, placing these in the # toolbar directly makes kdelibs emit a warning, so we do with a couple # KActions that watch the QActions and enable and disable themselves # when necessary. The KActions are needed anyway to have configurable # shortcuts for these actions, since the KDE shortcuts dialog doesn't # support configuring QAction shortcuts. self.undo_kaction = util.create_action('action_playlist_undo', 'Undo', self.undo_stack.undo, 'edit-undo', kdeui.KStandardShortcut.shortcut(kdeui.KStandardShortcut.Undo)) self.redo_kaction = util.create_action('action_playlist_redo', 'Redo', self.undo_stack.redo, 'edit-redo', kdeui.KStandardShortcut.shortcut(kdeui.KStandardShortcut.Redo)) self.undo_qaction = self.undo_stack.createUndoAction(self) self.redo_qaction = self.undo_stack.createRedoAction(self) self.connect(self.undo_qaction, QtCore.SIGNAL('changed()'), lambda: self.adjust_kaction_from_qaction(self.undo_kaction, self.undo_qaction)) self.connect(self.redo_qaction, QtCore.SIGNAL('changed()'), lambda: self.adjust_kaction_from_qaction(self.redo_kaction, self.redo_qaction)) self.adjust_kaction_from_qaction(self.undo_kaction, self.undo_qaction) self.adjust_kaction_from_qaction(self.redo_kaction, self.redo_qaction) ## """Properties.""" def _set_stop_after(self, value): self._stop_after = value if value is None: self.stop_mode = StopMode.NONE self.emit(QtCore.SIGNAL('repaint_needed')) stop_after = property(lambda self: self._stop_after, _set_stop_after) def _set_repeat_mode(self, value): self._repeat_mode = value # TODO Check it's a valid value? self.emit(QtCore.SIGNAL('list_changed')) repeat_mode = property(lambda self: self._repeat_mode, _set_repeat_mode) def _set_random_mode(self, value): self._random_mode = bool(value) self.emit(QtCore.SIGNAL('list_changed')) random_mode = property(lambda self: self._random_mode, _set_random_mode) def _set_current_item(self, value): if not (value is self.FIRST_ITEM and self._row_count == 0): self._current_item = value try: self.random_queue.remove(value) except ValueError: pass else: self._current_item = None self.emit(QtCore.SIGNAL('list_changed')) self.emit(QtCore.SIGNAL('repaint_needed')) if self.current_item not in (self.FIRST_ITEM, None): self.emit(QtCore.SIGNAL('scroll_needed'), self.index(self.current_item.position, 0)) current_item = property(lambda self: self._current_item, _set_current_item) def _set_currently_playing(self, item): self._currently_playing = item self.emit(QtCore.SIGNAL('repaint_needed')) currently_playing = property(lambda self: self._currently_playing, _set_currently_playing) ## """Maintain the state of actions current.""" def slot_list_changed(self): if self._row_count == 0: self._current_item = None # can't use the property here self.action_next.setEnabled(False) self.action_clear.setEnabled(False) self.action_previous.setEnabled(False) else: if self.current_item is None: self._current_item = self.FIRST_ITEM if self.current_item is self.FIRST_ITEM: current = 0 else: current = self.current_item.position self.action_clear.setEnabled(True) self.action_previous.setEnabled(current > 0) self.action_next.setEnabled(bool(self.queue or self.repeat_mode == RepeatMode.PLAYLIST or (self.random_mode and self.random_queue) or (not self.random_mode and current+1 < self._row_count))) self.slot_engine_status_changed(minirok.Globals.engine.status) def slot_engine_status_changed(self, new_status): if new_status == engine.State.STOPPED: self.action_stop.setEnabled(False) self.action_pause.setEnabled(False) self.action_pause.setChecked(False) self.action_play.setEnabled(self._row_count > 0) self.action_play_pause.setChecked(False) self.action_play_pause.setEnabled(self._row_count > 0) self.action_play_pause.setIcon(kdeui.KIcon('media-playback-start')) elif new_status == engine.State.PLAYING: self.action_stop.setEnabled(True) self.action_pause.setEnabled(True) self.action_pause.setChecked(False) self.action_play_pause.setChecked(False) self.action_play_pause.setIcon(kdeui.KIcon('media-playback-pause')) elif new_status == engine.State.PAUSED: self.action_pause.setChecked(True) self.action_play_pause.setChecked(True) ## """Other slots.""" def slot_clear(self): ClearItemlistCmd(self) def slot_activate_index(self, index): # proxy reimplements this too self.maybe_populate_random_queue() self.current_item = self._itemlist[index.row()] self.slot_play() def slot_toggle_stop_after_current(self): current = self.currently_playing or self.current_item if current not in (self.FIRST_ITEM, None): self.toggle_stop_after_item(current) def slot_update_tags(self): rows = [] for item, tags in self.tag_reader.pop_done(): item.update_tags(tags) item.needs_tag_reader = False rows.append(item.position) if rows: self.my_emit_dataChanged(min(rows), max(rows)) ## """Actions.""" def slot_play(self): if self.current_item is not None: if self.current_item is self.FIRST_ITEM: if self.queue: self.current_item = self.queue_popfront() else: self.current_item = self.my_first_child() self.currently_playing = self.current_item self.currently_playing.already_played = True minirok.Globals.engine.play(self.current_item.path) if self.current_item.tags()['Length'] is None: tags = tag_reader.TagReader.tags(self.current_item.path) self.current_item.update_tags({'Length': tags.get('Length', 0)}) self.my_emit_dataChanged(self.current_item.position) self.emit(QtCore.SIGNAL('new_track')) def slot_pause(self): e = minirok.Globals.engine if e.status == engine.State.PLAYING: e.pause(True) elif e.status == engine.State.PAUSED: e.pause(False) def slot_play_pause(self): if minirok.Globals.engine.status == engine.State.STOPPED: self.slot_play() else: self.slot_pause() def slot_stop(self): if minirok.Globals.engine.status != engine.State.STOPPED: self.currently_playing = None minirok.Globals.engine.stop() def slot_next(self, force_play=False): if self.current_item is not None: if self.queue: next = self.queue_popfront() elif self.random_mode: try: next = self.random_queue.pop(0) except IndexError: next = None self.maybe_populate_random_queue() elif self.current_item is self.FIRST_ITEM: next = self.my_first_child() else: index = self.current_item.position + 1 if index < self._row_count: next = self._itemlist[index] else: next = None if next is None and self.repeat_mode is RepeatMode.PLAYLIST: next = self.my_first_child() if next is None: if self.random_mode: self.current_item = self.random_queue[0] else: self.current_item = self.FIRST_ITEM else: self.current_item = next if (force_play or minirok.Globals.engine.status != engine.State.STOPPED): self.slot_play() def slot_previous(self): if self.current_item not in (self.FIRST_ITEM, None): index = self.current_item.position - 1 if index >= 0: self.current_item = self._itemlist[index] if minirok.Globals.engine.status != engine.State.STOPPED: self.slot_play() def slot_engine_end_of_stream(self): finished_item = self.currently_playing self.currently_playing = None if finished_item is self.stop_after: self.stop_after = None self.slot_next(force_play=False) elif self.repeat_mode == RepeatMode.TRACK: # This can't be in slot_next() because the next button should move # to the next track *even* with repeat_mode == TRACK. self.slot_play() else: self.slot_next(force_play=True) ## """Methods for the view (proxy model reimplements these).""" def toggle_stop_after(self, index): assert index.isValid() self.toggle_stop_after_item(self._itemlist[index.row()]) def toggle_enqueued(self, index): assert index.isValid() self.toggle_enqueued_many_items([ self._itemlist[index.row()] ]) def toggle_enqueued_many(self, indexes): self.toggle_enqueued_many_items([ self._itemlist[x.row()] for x in indexes ]) def removeItemsCmd(self, indexes): RemoveItemsCmd(self, [ x.row() for x in indexes ]) ## def toggle_stop_after_item(self, item): if item == self.stop_after: self.stop_after = None else: self.stop_after = item self.stop_mode = StopMode.AFTER_ONE def toggle_enqueued_many_items(self, items, preserve_stop_after=False): """Toggle a list of items from being in the queue. If :param preserve_stop_after: is True, stop_after will not be touched. (This is mostly useful when dequeueing for playing what may be the last item in the queue, see queue_popfront() below.) """ # items to queue, and items to dequeue enqueue = [ item for item in items if not item.queue_position ] dequeue = [ item for item in items if item.queue_position ] if dequeue: indexes = sorted(item.queue_position - 1 for item in dequeue) chunks = util.contiguous_chunks(indexes) chunks.append((len(self.queue), 0)) # fake chunk at the end # Now this is simple (at least compared to what was here before): # starting after each removal chunk, and until the beginning of the # next one, we substract the cumulative amount of removed items. accum = 0 for i, (index, amount) in enumerate(chunks[:-1]): accum += amount until = sum(chunks[i+1]) for item in self.queue[index+amount:until]: item.queue_position -= accum for index in reversed(indexes): item = self.queue.pop(index) item.queue_position = None if enqueue: size = len(self.queue) self.queue.extend(enqueue) for i, item in enumerate(enqueue): item.queue_position = size+i+1 if (not preserve_stop_after and self.stop_mode == StopMode.AFTER_QUEUE): if not self.queue: self.stop_after = None self.stop_mode = StopMode.AFTER_QUEUE elif self.queue[-1] is not self.stop_after: self.stop_after = self.queue[-1] self.emit(QtCore.SIGNAL('list_changed')) self.emit(QtCore.SIGNAL('repaint_needed')) def queue_popfront(self): """Convenience function to dequeue and return the first item from the queue.""" try: popped = self.queue[0] except IndexError: minirok.logger.warn('queue_popfront() called on an empty queue') else: self.toggle_enqueued_many_items([ popped ], preserve_stop_after=True) return popped def my_first_child(self): """Return the first item to be played, honouring random_mode.""" if self.random_mode: self.maybe_populate_random_queue() return self.random_queue.pop(0) else: return self._itemlist[0] def maybe_populate_random_queue(self): if not self.random_queue: self.random_queue.extend(self._itemlist) for item in self._itemlist: self.already_played = False ## def apply_preferences(self): prefs = minirok.Globals.preferences if prefs.tags_from_regex: try: self._regex = re.compile(prefs.tag_regex) except re.error, e: minirok.logger.error('invalid regular expresion %s: %s', prefs.tag_regex, e) self._regex = None self._regex_mode = 'Always' else: self._regex_mode = prefs.tag_regex_mode assert self._regex_mode in ['Always', 'OnRegexFail', 'Never'] else: self._regex = None self._regex_mode = 'Always' ## def save_config(self): """Saves the current playlist.""" paths = (item.path for item in self._itemlist) try: playlist = file(self.saved_playlist_path(), 'w') except IOError, e: minirok.logger.error('could not save playlist: %s', e) else: playlist.write('\0'.join(paths)) playlist.close() def load_saved_playlist(self): try: playlist = file(self.saved_playlist_path()) except IOError, e: if e.errno == errno.ENOENT: pass else: minirok.logger.warning('error opening saved playlist: %s', e) else: files = re.split(r'\0+', playlist.read()) if files != ['']: # empty saved playlist # add_files_untrusted() will use InsertItemsCmd, and here # that wouldn't be appropriate: cook up the code ourselves. self.insert_items(0, map(self.create_item, util.playable_from_untrusted(files, warn=True))) self.slot_list_changed() @staticmethod def saved_playlist_path(): appdata = str(kdecore.KGlobal.dirs().saveLocation('appdata')) return os.path.join(appdata, 'saved_playlist.txt') ## def add_files(self, files, position=-1): """Add the given files to the playlist at a given position. If position is < 0, files will be added at the end of the playlist. """ if position < 0: position = self._row_count if files: items = map(self.create_item, files) InsertItemsCmd(self, position, items) def add_files_untrusted(self, files, clear_playlist=False): """Add to the playlist those files that exist and are playable.""" if clear_playlist: self.slot_clear() self.add_files(util.playable_from_untrusted(files, warn=True)) def create_item(self, path): tags = self.tags_from_filename(path) if len(tags) == 0 or tags.get('Title', None) is None: regex_failed = True dirname, filename = os.path.split(path) tags['Title'] = util.unicode_from_path(filename) else: regex_failed = False item = PlaylistItem(path, tags) if self._regex_mode == 'Always' or (regex_failed and self._regex_mode == 'OnRegexFail'): item.needs_tag_reader = True else: item.needs_tag_reader = False return item def tags_from_filename(self, path): if self._regex is None: return {} else: match = self._regex.search(path) if match is None: return {} tags = {} for group, match in match.groupdict().items(): group = group.capitalize() if group in PlaylistItem.ALLOWED_TAGS and match is not None: tags[group] = util.unicode_from_path(match) return tags ## # XXX-KDE4 TODO def contentsDragMoveEvent(self, event): if (not (kdecore.KApplication.kApplication().keyboardMouseState() & qt.Qt.ControlButton) or not drag.FileListDrag.canDecode(event)): if self.visualizer_rect is not None: self.viewport().repaint(self.visualizer_rect, True) self.visualizer_rect = None return kdeui.KListView.contentsDragMoveEvent(self, event) else: try: self.cleanDropVisualizer() self.setDropVisualizer(False) rect = self.drawDropVisualizer(None, None, self.lastChild()) if rect != self.visualizer_rect: self.visualizer_rect = rect brush = qt.QBrush(qt.Qt.Dense4Pattern) painter = qt.QPainter(self.viewport()) painter.fillRect(self.visualizer_rect, brush) return kdeui.KListView.contentsDragMoveEvent(self, event) finally: self.setDropVisualizer(True) ## """Misc. helpers.""" def my_emit_dataChanged(self, row1, row2=None, column=None): """Emit dataChanged() between sorted([row1, row2]). If :param row2: is None, it will default to row1. If :param column: is not None, only include that column in the signal. """ if row2 is None: row2 = row1 elif row1 > row2: row1, row2 = row2, row1 if column is None: col1 = 0 col2 = self.columnCount() - 1 else: col1 = col2 = column self.emit(QtCore.SIGNAL( 'dataChanged(const QModelIndex &, const QModelIndex &)'), self.index(row1, col1), self.index(row2, col2)) ## def get_current_tags(self): """Return the tags of the currently played item, if any.""" if self.currently_playing is not None: return self.currently_playing.tags() else: return {} ## def adjust_kaction_from_qaction(self, kaction, qaction): kaction.setToolTip(qaction.text()) kaction.setEnabled(qaction.isEnabled()) ## class Proxy(proxy.Model): def __init__(self, parent=None): proxy.Model.__init__(self, parent) self.scroll_index = None def setSourceModel(self, model): proxy.Model.setSourceModel(self, model) self.connect(model, QtCore.SIGNAL('scroll_needed'), self.slot_scroll_needed) # XXX dataChanged() abuse here too... self.connect(model, QtCore.SIGNAL('repaint_needed'), lambda: self.emit(QtCore.SIGNAL( 'dataChanged(const QModelIndex &, const QModelIndex &)'), self.index(0, 0), self.index(1, self.columnCount()))) def setPattern(self, pattern): proxy.Model.setPattern(self, pattern) self.emit_scroll_needed() def slot_scroll_needed(self, index): self.scroll_index = index self.emit_scroll_needed() def emit_scroll_needed(self): if self.scroll_index is not None: mapped = self.mapFromSource(self.scroll_index) if mapped.isValid(): self.emit(QtCore.SIGNAL('scroll_needed'), mapped) ## @proxy._map def toggle_enqueued(self, index): pass @proxy._map def toggle_stop_after(self, index): pass @proxy._map def slot_activate_index(self, index): pass @proxy._map_many def removeItemsCmd(self, indexes): pass @proxy._map_many def toggle_enqueued_many(self, indexes): pass ## class RepeatMode: NONE = object() TRACK = object() PLAYLIST = object() class StopMode: NONE = object() AFTER_ONE = object() AFTER_QUEUE = object() class StopAction(kdeui.KToolBarPopupAction): def __init__(self, *args): kdeui.KToolBarPopupAction.__init__(self, kdeui.KIcon(), "", None) menu = self.menu() menu.addTitle('Stop') self.action_now = menu.addAction('Now') self.action_after_current = menu.addAction('After current') self.action_after_queue = menu.addAction('After queue') self.connect(menu, QtCore.SIGNAL('aboutToShow()'), self.slot_prepare) self.connect(menu, QtCore.SIGNAL('triggered(QAction *)'), self.slot_activated) def slot_prepare(self): playlist = minirok.Globals.playlist if (playlist.stop_mode == StopMode.AFTER_ONE and playlist.stop_after == playlist.currently_playing): self.action_after_current.setCheckable(True) self.action_after_current.setChecked(True) else: self.action_after_current.setCheckable(False) if playlist.stop_mode == StopMode.AFTER_QUEUE: self.action_after_queue.setCheckable(True) self.action_after_queue.setChecked(True) else: self.action_after_queue.setCheckable(False) def slot_activated(self, action): playlist = minirok.Globals.playlist if action is self.action_now: self.trigger() elif action is self.action_after_current: playlist.slot_toggle_stop_after_current() elif action is self.action_after_queue: if playlist.stop_mode == StopMode.AFTER_QUEUE: playlist.stop_after = None else: playlist.stop_after = None # clear possible AFTER_ONE mode playlist.stop_mode = StopMode.AFTER_QUEUE if playlist.queue: playlist.stop_after = playlist.queue[-1] ## class PlaylistView(QtGui.QTreeView): def __init__(self, parent=None): QtGui.QTreeView.__init__(self, parent) self.setHeader(Columns(self)) self.setRootIsDecorated(False) self.setDropIndicatorShown(True) self.setAllColumnsShowFocus(True) self.setDragDropMode(self.DragDrop) self.setSelectionBehavior(self.SelectRows) self.setSelectionMode(self.ExtendedSelection) self.action_queue_tracks = util.create_action( 'action_enqueue_dequeue_selected', 'Enqueue/dequeue selection', self.slot_enqueue_dequeue_selected, None, 'Ctrl+E') def setModel(self, playlist): QtGui.QTreeView.setModel(self, playlist) self.header().setup_from_config() for c in range(playlist.columnCount()): if playlist.headerData(c, Qt.Horizontal, Qt.DisplayRole).toString() == 'Track': self.track_delegate = PlaylistTrackDelegate() self.setItemDelegateForColumn(c, self.track_delegate) break else: minirok.logger.error('index for Track column not found :-?') self.connect(self, QtCore.SIGNAL('activated(const QModelIndex &)'), playlist.slot_activate_index) self.connect(playlist, QtCore.SIGNAL('scroll_needed'), lambda index: self.scrollTo(index)) ## def uniqSelectedIndexes(self): return set(x for x in self.selectedIndexes() if x.column() == 0) def unselectedIndexes(self): model = self.model() all = set(range(model.rowCount())) selected = set(x.row() for x in self.selectedIndexes()) return [ model.index(x, 0) for x in all - selected ] ## def drawRow(self, painter, styleopt, index): if index.data(Playlist.RoleQueryIsPlaying).toBool(): styleopt = QtGui.QStyleOptionViewItem(styleopt) # make a copy styleopt.font.setItalic(True) QtGui.QTreeView.drawRow(self, painter, styleopt, index) if index.data(Playlist.RoleQueryIsCurrent).toBool(): painter.save() r = styleopt.rect w = sum(self.columnWidth(x) for x in range(self.header().count())) painter.setPen(styleopt.palette.highlight().color()) painter.drawRect(r.x(), r.y(), w-1, r.height()-1) painter.restore() def startDrag(self, actions): # Override this function to loose the ugly pixmap provided by Qt indexes = self.selectedIndexes() if len(indexes) > 0: mimedata = self.model().mimeData(indexes) drag = QtGui.QDrag(self) drag.setMimeData(mimedata) drag.setPixmap(QtGui.QPixmap(1, 1)) drag.exec_(actions) def keyPressEvent(self, event): if event.key() == Qt.Key_Delete: event.accept() self.model().removeItemsCmd(self.uniqSelectedIndexes()) else: return QtGui.QTreeView.keyPressEvent(self, event) def mousePressEvent(self, event): button = event.button() keymod = QtGui.QApplication.keyboardModifiers() index = self.indexAt(event.pos()) # TODO: accept event? if not index.isValid(): # click on viewport self.clearSelection() elif keymod & Qt.ControlModifier: if button & Qt.RightButton: self.model().toggle_enqueued(index) elif button & Qt.MidButton: self.model().toggle_stop_after(index) else: return QtGui.QTreeView.mousePressEvent(self, event) elif keymod != Qt.NoModifier: # Eat them up return QtGui.QTreeView.mousePressEvent(self, event) elif button & Qt.MidButton: if index.data(Playlist.RoleQueryIsPlaying).toBool(): minirok.Globals.action_collection.action('action_pause').trigger() elif button & Qt.RightButton: QtGui.QTreeView.mousePressEvent(self, event) menu = kdeui.KMenu(self) selected_indexes = self.uniqSelectedIndexes() assert len(selected_indexes) > 0 # or maybe use itemAt() if len(selected_indexes) == 1: enqueue_action = menu.addAction('Enqueue track') if index.data(Playlist.RoleQueuePosition).toInt()[0] > 0: enqueue_action.setCheckable(True) enqueue_action.setChecked(True) else: enqueue_action = menu.addAction('Enqueue/Dequeue tracks') stop_after_action = menu.addAction('Stop playing after this track') if index.data(Playlist.RoleQueryIsStopAfter).toBool(): stop_after_action.setCheckable(True) stop_after_action.setChecked(True) crop_action = menu.addAction('Crop tracks') ## selected_action = menu.exec_(event.globalPos()) if selected_action == enqueue_action: self.model().toggle_enqueued_many(sorted(selected_indexes)) elif selected_action == stop_after_action: self.model().toggle_stop_after(index) elif selected_action == crop_action: self.model().removeItemsCmd(self.unselectedIndexes()) else: return QtGui.QTreeView.mousePressEvent(self, event) ## def slot_enqueue_dequeue_selected(self): self.model().toggle_enqueued_many(sorted(self.uniqSelectedIndexes())) ## class PlaylistItem(object): # This class should be considered sort of private to the model ALLOWED_TAGS = [ 'Track', 'Artist', 'Album', 'Title', 'Length' ] def __init__(self, path, tags=None): self.path = path self._tags = dict((tag, None) for tag in self.ALLOWED_TAGS) if tags is not None: self.update_tags(tags) # these are maintained up to date by the model self.position = None self.queue_position = None self.already_played = False self.needs_tag_reader = True ## def tags(self): return self._tags.copy() def tag_text(self, tag): value = self._tags[tag] if tag == 'Length' and value is not None: return util.fmt_seconds(value) else: return value def tag_by_index(self, index): return self.tag_text(self.ALLOWED_TAGS[index]) def update_tags(self, tags): for tag, value in tags.items(): if tag not in self._tags: minirok.logger.warn('unknown tag %s', tag) continue if tag == 'Track': try: # remove leading zeroes value = str(int(value)) except ValueError: pass elif tag == 'Length': try: value = int(value) except ValueError: minirok.logger.warn('invalid length: %r', value) continue self._tags[tag] = value ## # XXX-KDE4 TODO def paintFocus(self, painter, colorgrp, qrect): """Only allows focus to be painted in the current item.""" if not self._is_current: return else: kdeui.KListViewItem.paintFocus(self, painter, colorgrp, qrect) ## class PlaylistTrackDelegate(QtGui.QItemDelegate): """Paints the track number and the "stop after/queue pos" ellipse. Code originally comes from PlaylistItem::paintCell() in Amarok 1.4. """ def paint(self, painter, option, index): QtGui.QItemDelegate.paint(self, painter, option, index) queue_pos = index.data(Playlist.RoleQueuePosition).toInt()[0] draw_stop = index.data(Playlist.RoleQueryIsStopAfter).toBool() if draw_stop or queue_pos: painter.save() painter.translate(option.rect.x(), option.rect.y()) width = option.rect.width() height = option.rect.height() e_width = 16 e_margin = 2 e_height = height - e_margin*2 if draw_stop: s_width = 8 s_height = 8 else: s_width = 1 # Seems to prevent an artifact s_height = 0 if queue_pos: queue_pos = str(queue_pos) q_width = painter.fontMetrics().width(queue_pos) q_height = painter.fontMetrics().height() else: q_width = q_height = 0 items_width = s_width + q_width painter.setBrush(option.palette.highlight()) painter.setPen(option.palette.highlight().color().dark()) painter.drawEllipse(width - items_width - e_width/2, e_margin, e_width, e_height) painter.drawRect(width - items_width, e_margin, items_width+1, e_height) painter.setPen(option.palette.highlight().color()) painter.drawLine(width - items_width, e_margin+1, width - items_width, e_height+1) x = width - items_width - e_margin if draw_stop: y = e_height / 2 - s_height / 2 + e_margin painter.setBrush(QtGui.QColor(0, 0, 0)) painter.drawRect(x, y, s_width, s_height) x += s_width + e_margin/2 if queue_pos: painter.setPen(option.palette.highlightedText().color()) painter.drawText(x, 0, width-x, q_height, Qt.AlignCenter, queue_pos) painter.restore() ## class Columns(QtGui.QHeaderView): # We use a single configuration option, which contains the order in which # columns are to be displayed, their width, and whether they are hidden or # not. CONFIG_SECTION = 'Playlist' CONFIG_OPTION = 'Columns' DEFAULT_COLUMNS = [ ('Track', 60, 1), ('Artist', 200, 1), ('Album', 200, 0), ('Title', 275, 1), ('Length', 60, 1) ] def __init__(self, parent): QtGui.QHeaderView.__init__(self, Qt.Horizontal, parent) util.CallbackRegistry.register_save_config(self.save_config) self.setMovable(True) self.setStretchLastSection(False) self.setDefaultAlignment(Qt.AlignLeft) self.setContextMenuPolicy(Qt.CustomContextMenu) self.connect(self, QtCore.SIGNAL('customContextMenuRequested(const QPoint&)'), self.exec_popup) def sorted_column_names(self): names = [] model = self.model() for c in range(model.columnCount()): names.append(model.headerData(c, Qt.Horizontal, Qt.DisplayRole).toString()) return map(str, names) def setup_from_config(self): """Read config, sanitize it, and apply. NOTE: this code can't be in __init__, because at that time there is not a model/view associated with the object. """ config = kdecore.KGlobal.config().group(self.CONFIG_SECTION) columns = [] model_columns = self.sorted_column_names() unseen_columns = set(model_columns) if not config.hasKey(self.CONFIG_OPTION): columns.extend(self.DEFAULT_COLUMNS) else: warn = minirok.logger.warn entries = map(str, config.readEntry( self.CONFIG_OPTION, QtCore.QVariant(QtCore.QStringList())).toStringList()) for entry in entries: try: name, width, visible = entry.split(':', 2) except ValueError: warn('skipping invalid entry in column config: %r', entry) continue try: width = int(width) except ValueError: warn('invalid column width for %s: %r', name, width) continue # TODO Maybe this one ought to be more flexible try: visible = bool(int(visible)) except ValueError: warn('invalid visibility value for %s: %r', name, visible) continue try: unseen_columns.remove(name) except KeyError: warn('skipping unknown or duplicated column: %r', name) continue columns.append((name, width, visible)) if unseen_columns: missing = [ d for d in self.DEFAULT_COLUMNS if d[0] in unseen_columns ] warn('these columns could not be found in config, creating from' ' defaults: %s', ', '.join(d[0] for d in missing)) columns.extend(missing) ## for visual, (name, width, visible) in enumerate(columns): logical = model_columns.index(name) current = self.visualIndex(logical) if current != visual: self.moveSection(current, visual) self.resizeSection(logical, width) self.setSectionHidden(logical, not visible) ## def exec_popup(self, position): model = self.model() menu = kdeui.KMenu(self) menu.addTitle('Columns') for i in range(model.columnCount()): logindex = self.logicalIndex(i) name = model.headerData(logindex, Qt.Horizontal, Qt.DisplayRole).toString() action = menu.addAction(name) action.setCheckable(True) action.setData(QtCore.QVariant(logindex)) action.setChecked(not self.isSectionHidden(logindex)) selected_action = menu.exec_(self.mapToGlobal(position)) if selected_action is not None: hide = not selected_action.isChecked() column = selected_action.data().toInt()[0] self.setSectionHidden(column, hide) ## def save_config(self): entries = [None] * self.count() for logical, name in enumerate(self.sorted_column_names()): visible = int(not self.isSectionHidden(logical)) if not visible: # gross, but sectionSize() would return 0 otherwise :-( self.setSectionHidden(logical, False) width = self.sectionSize(logical) entry = '%s:%d:%d' % (name, width, visible) entries[self.visualIndex(logical)] = entry config = kdecore.KGlobal.config().group(self.CONFIG_SECTION) config.writeEntry(self.CONFIG_OPTION, entries) ## """Undoable commands to modify the contents of the playlist. Note that they will add themselves to the model's QUndoStack. """ class AlterItemlistMixin(object): """Common functionality to make changes to the item list. This class offers methods to insert and remove items from the list. Each operation saves state, so that calling the reverse operation without any arguments just undoes it. In both cases, there is housekeeping of the playlist's queue, removing items from it when removing, and restoring the previous state on insertion. This can be disabled by passing "do_queue=False" to __init__. """ def __init__(self, model, do_queue=True): self.items = {} self.chunks = [] self.queuepos = {} self.current_item = None self.model = model self.do_queue = do_queue ## def insert_items(self, items=None): """Insert items into the playlist. :param items: should be a dict like: { pos1: itemlist1, pos2: itemlist2, ... } The items will be inserted in *ascending* order by position. If items is None, self.items will be used. """ if items is None: items = self.items for position, items in sorted(items.iteritems()): self.model.insert_items(position, items) # Restore the current item, if we have one *and* the playlist doesn't if (self.current_item is not None and self.model.current_item in (None, Playlist.FIRST_ITEM)): self.model.current_item = self.current_item if self.do_queue: # TODO Think whether to invalidate these queue positions if the # queue changes between a removal and its undo. for pos, amount in util.contiguous_chunks(self.queuepos.keys()): items = [ self.queuepos[x] for x in range(pos, pos+amount) ] tail = self.model.queue[pos-1:] self.model.toggle_enqueued_many_items(tail + items) self.model.toggle_enqueued_many_items(tail) def remove_items(self, chunks=None): """Remove items from the playlist. :param chunks: should be a list like: [ (pos1, amount1), (pos2, amount2), ... ] The items will be removed in *descending* order by position. If chunks is None, self.chunks will be used, and if empty, it will be calculated first from self.items. This method will fills self.items in the format explained in insert_items() above. """ if chunks is None: if self.chunks: chunks = self.chunks else: chunks = self.chunks = sorted((row, len(items)) for row, items in self.items.iteritems()) self.items.clear() self.queuepos.clear() if self.model.current_item is not Playlist.FIRST_ITEM: self.current_item = self.model.current_item for position, amount in reversed(chunks): self.items[position] = self.model.remove_items(position, amount) if self.do_queue: for itemlist in self.items.itervalues(): self.queuepos.update((item.queue_position, item) for item in itemlist if item.queue_position) if self.queuepos: self.model.toggle_enqueued_many_items(self.queuepos.values()) ## def get_items(self): """Return an ordered list of all items belonging to this command.""" result = [] for position, items in sorted(self.items.iteritems()): result.extend(items) return result class InsertItemsCmd(QtGui.QUndoCommand, AlterItemlistMixin): """Command to insert a list of items at a certain position.""" def __init__(self, model, position, items, do_queue=True): QtGui.QUndoCommand.__init__(self, 'insert ' + _n_tracks_str(len(items))) AlterItemlistMixin.__init__(self, model, do_queue) if items: self.items = { position: items } self.model.undo_stack.push(self) def undo(self): self.remove_items() def redo(self): self.insert_items() class RemoveItemsCmd(QtGui.QUndoCommand, AlterItemlistMixin): """Command to remove a list of rows from the playlist.""" def __init__(self, model, rows, do_queue=True): """Create the command. :param rows: A possibly unsorted/non-contiguous list of rows to remove. """ QtGui.QUndoCommand.__init__(self, 'remove ' + _n_tracks_str(len(rows))) AlterItemlistMixin.__init__(self, model, do_queue) if rows: self.chunks = util.contiguous_chunks(rows) self.model.undo_stack.push(self) def undo(self): self.insert_items() def redo(self): self.remove_items() class ClearItemlistCmd(QtGui.QUndoCommand, AlterItemlistMixin): """Command to completely clear the playlist. This command offers a more efficient implementation of remove_items than the mixin (uses the model's clear_itemlist), and handles the queue more efficiently. """ def __init__(self, model): QtGui.QUndoCommand.__init__(self, 'clear playlist') AlterItemlistMixin.__init__(self, model) self.model.undo_stack.push(self) def remove_items(self): self.items.clear() self.queuepos.clear() if self.model.current_item is not Playlist.FIRST_ITEM: self.current_item = self.model.current_item self.items[0] = self.model.clear_itemlist() if self.do_queue: # iterate over model's queue directly, since we are # dequeueing *everything* self.queuepos.update((item.queue_position, item) for item in self.model.queue) if self.queuepos: self.model.toggle_enqueued_many_items(self.queuepos.values()) def undo(self): self.insert_items() def redo(self): self.remove_items() ## def _n_tracks_str(amount): """Return '1 track' if amount is 1 else '$amount tracks'.""" if amount == 1: return '1 track' else: return '%d tracks' % (amount,) minirok-2.1/minirok/left_side.py0000644000175000017500000001324411265733622015457 0ustar datodato#! /usr/bin/env python ## vim: fileencoding=utf-8 # # Copyright (c) 2007-2009 Adeodato Simó (dato@net.com.org.es) # Licensed under the terms of the MIT license. import os from PyQt4 import QtGui, QtCore from PyKDE4 import kio, kdeui, kdecore import minirok from minirok import tree_view, util ## class LeftSide(QtGui.QWidget): def __init__(self, *args): QtGui.QWidget.__init__(self, *args) self.tree_view = tree_view.TreeView() self.tree_search = QtGui.QWidget(self) self.combo_toolbar = kdeui.KToolBar(None) layout = QtGui.QVBoxLayout() layout.setSpacing(0) layout.setContentsMargins(4, 4, 4, 0) layout.addWidget(self.tree_search) layout.addWidget(self.combo_toolbar) layout.addWidget(self.tree_view) self.setLayout(layout) self.button_action = 'Enable' self.search_button = QtGui.QPushButton(self.button_action) self.search_widget = tree_view.TreeViewSearchLineWidget() self.search_widget.setEnabled(False) layout2 = QtGui.QHBoxLayout() layout2.setSpacing(0) layout2.setContentsMargins(0, 0, 0, 0) layout2.addWidget(self.search_widget) layout2.addWidget(self.search_button) self.tree_search.setLayout(layout2) self.path_combo = MyComboBox(self.combo_toolbar) self.combo_toolbar.addWidget(self.path_combo) self.combo_toolbar.setIconDimensions(16) self.combo_toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) # self.combo_toolbar.setItemAutoSized(0) # XXX-KDE4 should be stretchabe or however it's called self.action_refresh = util.create_action('action_refresh_tree_view', 'Refresh tree view', self.tree_view.slot_refresh, 'view-refresh', 'F5') self.combo_toolbar.addAction(self.action_refresh) self.action_focus_path_combo = util.create_action('action_path_combo_focus', 'Focus path combobox', self.path_combo.slot_focus, shortcut='Alt+O') ## self.search_widget.searchLine().setTreeWidget(self.tree_view) self.connect(self.tree_view, QtCore.SIGNAL('scan_in_progress'), self.slot_tree_view_does_scan) self.connect(self.search_button, QtCore.SIGNAL('clicked(bool)'), self.slot_do_button) self.connect(self.search_widget.searchLine(), QtCore.SIGNAL('search_finished'), self.tree_view.slot_search_finished) self.connect(self.search_widget.searchLine(), QtCore.SIGNAL('returnPressed(const QString &)'), self.tree_view.slot_append_visible) self.connect(self.path_combo, QtCore.SIGNAL('new_directory_selected'), self.tree_view.slot_show_directory) ## if self.path_combo.currentText(): # This can't go in the MyComboBox constructor because the signals # are not connected yet at that time. self.path_combo.slot_set_url(self.path_combo.currentText()) else: text = 'Enter a directory here' width = self.path_combo.fontMetrics().width(text) self.path_combo.setEditText(text) self.path_combo.setMinimumWidth(width + 30) # add pixels for arrow ## def slot_tree_view_does_scan(self, scanning): negated = not scanning self.search_button.setHidden(negated) self.search_widget.setEnabled(negated) self.search_button.setEnabled(scanning) self.button_action = self.tree_view.recurse and 'Stop scan' or 'Enable' self.search_button.setText(self.button_action) if scanning: self.search_widget.setToolTip('Search disabled while reading directory contents') else: self.search_widget.setToolTip('') def slot_do_button(self): enable = (self.button_action == 'Enable') self.button_action = enable and 'Stop scan' or 'Enable' self.search_button.setText(self.button_action) if not enable: self.search_widget.setToolTip('') self.tree_view.recurse = enable ## class MyComboBox(kio.KUrlComboBox): """A KURLComboBox that saves the introduced directories in the config.""" CONFIG_SECTION = 'Tree View' CONFIG_HISTORY_OPTION = 'History' def __init__(self, parent): kio.KUrlComboBox.__init__(self, kio.KUrlComboBox.Directories, True, parent) util.CallbackRegistry.register_save_config(self.save_config) self.completion_object = kio.KUrlCompletion(kio.KUrlCompletion.DirCompletion) self.setCompletionObject(self.completion_object) config = kdecore.KGlobal.config().group(self.CONFIG_SECTION) urls = config.readPathEntry(self.CONFIG_HISTORY_OPTION, QtCore.QStringList()) self.setUrls(urls) self.connect(self, QtCore.SIGNAL('urlActivated(const KUrl &)'), self.slot_set_url) self.connect(self, QtCore.SIGNAL('returnPressed(const QString &)'), self.slot_set_url) def slot_focus(self): self.setFocus() self.lineEdit().selectAll() def slot_set_url(self, url): if isinstance(url, kdecore.KUrl): # We can only store QStrings url = url.pathOrUrl() directory = util.kurl_to_path(url) if os.path.isdir(directory): urls = self.urls() urls.removeAll(url) urls.prepend(url) self.setUrls(urls, kio.KUrlComboBox.RemoveBottom) self.emit(QtCore.SIGNAL('new_directory_selected'), directory) def save_config(self): config = kdecore.KGlobal.config().group(self.CONFIG_SECTION) config.writePathEntry(self.CONFIG_HISTORY_OPTION, self.urls()) minirok-2.1/minirok/tree_view.py0000644000175000017500000003607211265733622015516 0ustar datodato#! /usr/bin/env python ## vim: fileencoding=utf-8 # # Copyright (c) 2007-2009 Adeodato Simó (dato@net.com.org.es) # Licensed under the terms of the MIT license. import os import re import stat from PyQt4 import QtGui, QtCore from PyKDE4 import kdeui, kdecore import minirok from minirok import drag, engine, util ## class TreeView(QtGui.QTreeWidget): CONFIG_SECTION = 'Tree View' CONFIG_RECURSE_OPTION = 'RecurseScan' def __init__(self, *args): QtGui.QTreeWidget.__init__(self, *args) self.root = None self.populating = False self.populate_pending = [] self.empty_directories = set() self.automatically_opened = set() self.timer = QtCore.QTimer(self) # Recursing the tree to enable the search widget is configurable; # the LeftSide communicates with us via the "recurse" property. config = kdecore.KGlobal.config().group(self.CONFIG_SECTION) self._recurse = config.readEntry( self.CONFIG_RECURSE_OPTION, QtCore.QVariant(False)).toBool() util.CallbackRegistry.register_save_config(self.save_config) ## self.header().hide() self.setDragDropMode(self.DragOnly) self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) self.connect(self.timer, QtCore.SIGNAL('timeout()'), self.slot_populate_done) self.connect(self, QtCore.SIGNAL('itemActivated(QTreeWidgetItem *, int)'), self.slot_append_selected) self.connect(self, QtCore.SIGNAL('itemExpanded(QTreeWidgetItem *)'), lambda item: item.repopulate()) ## def _set_recurse(self, value): scanning = self.timer.isActive() if scanning ^ value: self._recurse = bool(value) if self._recurse: self.emit(QtCore.SIGNAL('scan_in_progress'), True) self.timer.start(0) else: self.timer.stop() recurse = property(lambda self: self._recurse, _set_recurse) ## def selected_files(self): """Returns a list of the selected files (reading directory contents).""" def _add_item(item): if item.IS_DIR: item.repopulate() # meh, I don't like stat()'ing from here for child in _get_children(item): _add_item(child) else: if item.path not in files: files.append(item.path) files = [] for item in self.selectedItems(): _add_item(item) return files def visible_files(self): files = [] iterator = QtGui.QTreeWidgetItemIterator(self, QtGui.QTreeWidgetItemIterator.NotHidden) item = iterator.value() while item: if not item.IS_DIR: files.append(item.path) iterator += 1 item = iterator.value() return files ## def slot_append_selected(self, item): if item is not None: # maybe overzealous here minirok.Globals.playlist.add_files(self.selected_files()) def slot_append_visible(self, search_string): if not unicode(search_string).strip(): return playlist_was_empty = bool(minirok.Globals.playlist.rowCount() == 0) minirok.Globals.playlist.add_files(self.visible_files()) if (playlist_was_empty and minirok.Globals.engine.status == engine.State.STOPPED): minirok.Globals.action_collection.action('action_play').trigger() ## def slot_show_directory(self, directory): """Changes the TreeView root to the specified directory. If directory is the current root and there is no ongoing scan, a simple refresh will be performed instead. """ if directory != self.root or self.populating: # Not refreshing self.clear() self.populate_pending = [] self.setSortingEnabled(False) # dog slow otherwise self.empty_directories.clear() self.automatically_opened.clear() self.root = directory def _directory_children(parent): return _get_children(parent, lambda x: x.IS_DIR) self.populating = True _populate_tree(self.invisibleRootItem(), self.root) self.sortItems(0, QtCore.Qt.AscendingOrder) # (¹) self.populate_pending = _directory_children(self.invisibleRootItem()) if self._recurse: self.timer.start(0) self.emit(QtCore.SIGNAL('scan_in_progress'), True) # (¹) There seems to be a bug somewhere, that if setSortingEnabled(True) # is called, without calling some function like sortItems() where the # SortOrder is specified, you get Descending by default. Beware. def slot_refresh(self): self.slot_show_directory(self.root) def slot_populate_done(self): def _directory_children(parent): return _get_children(parent, lambda x: x.IS_DIR) try: item = self.populate_pending.pop(0) except IndexError: self.timer.stop() self.populating = False self.setSortingEnabled(True) for item in self.empty_directories: (item.parent() or self.invisibleRootItem()).removeChild(item) del item # necessary? self.empty_directories.clear() self.emit(QtCore.SIGNAL('scan_in_progress'), False) else: _populate_tree(item, item.path) self.populate_pending.extend(_directory_children(item)) def slot_search_finished(self, null_search): """Open the visible items, closing items opened in the previous search. Non-toplevel items with more than 5 children will not be opened. If null_search is True, no items will be opened at all. """ # make a list of selected and its parents, in order not to close them selected = set() iterator = QtGui.QTreeWidgetItemIterator(self, QtGui.QTreeWidgetItemIterator.Selected) item = first_selected = iterator.value() while item: selected.add(item) parent = item.parent() while parent: selected.add(parent) parent = parent.parent() iterator += 1 item = iterator.value() for item in self.automatically_opened - selected: item.setExpanded(False) self.automatically_opened &= selected # keep them to close later if null_search: self.scrollToItem(first_selected) return ## is_visible = lambda x: not x.isHidden() pending = _get_children(self.invisibleRootItem(), is_visible) toplevel_count = len(pending) i = 0 while pending: i += 1 item = pending.pop(0) visible_children = _get_children(item, is_visible) if ((i <= toplevel_count or len(visible_children) <= 5) and not item.isExpanded()): item.setExpanded(True) self.automatically_opened.add(item) pending.extend(x for x in visible_children if x.IS_DIR) ## def startDrag(self, action): dragobj = drag.FileListDrag(self.selected_files(), self) dragobj.exec_(action) ## def save_config(self): config = kdecore.KGlobal.config().group(self.CONFIG_SECTION) config.writeEntry(self.CONFIG_RECURSE_OPTION, QtCore.QVariant(self._recurse)) ## class TreeViewItem(QtGui.QTreeWidgetItem): IS_DIR = 0 # TODO Use QTreeWidgetItem::type() instead? def __init__(self, path, root): self.path = path dirname, self.filename = os.path.split(path) # Note that we don't pass a parent here, because I've found that # to be slow. Instead, we always construct parentless items, and # add them to the parent with addChildren() in _populate_tree(). QtGui.QTreeWidgetItem.__init__(self, [ util.unicode_from_path(self.filename) ]) # optimization for TreeViewSearchLine.itemMatches() below rel_path = re.sub('^%s/*' % re.escape(root), '', path) self.unicode_rel_path = util.unicode_from_path(rel_path) def __lt__(self, other): """Sorts directories before files, and by filename after that.""" return (other.IS_DIR, self.filename) < (self.IS_DIR, other.filename) class FileItem(TreeViewItem): pass class DirectoryItem(TreeViewItem): IS_DIR = 1 def __init__(self, path, root): TreeViewItem.__init__(self, path, root) self.setChildIndicatorPolicy(QtGui.QTreeWidgetItem.ShowIndicator) def repopulate(self): """Force a repopulation of this item.""" _populate_tree(self, self.path, force_refresh=True) if not self.childCount(): self.setChildIndicatorPolicy(QtGui.QTreeWidgetItem.DontShowIndicator) else: self.setChildIndicatorPolicy(QtGui.QTreeWidgetItem.ShowIndicator) self.sortChildren(0, QtCore.Qt.AscendingOrder) ## class TreeViewSearchLine(util.SearchLineWithReturnKey): """Class to perform matches against a TreeViewItem. The itemMatches() method is overriden to make a match against the full relative path (with respect to the TreeView root directory) of the items, plus the pattern is split in words and all have to match (instead of having to match *in the same order*, as happens in the standard KListViewSearchLine. When the user stops typing, a search_finished(bool empty_search) signal is emitted. """ def __init__(self, *args): util.SearchLineWithReturnKey.__init__(self, *args) self.string = None self.regexes = [] self.timer = QtCore.QTimer(self) self.timer.setSingleShot(True) self.connect(self.timer, QtCore.SIGNAL('timeout()'), self.slot_emit_search_finished) def slot_emit_search_finished(self): self.emit(QtCore.SIGNAL('search_finished'), self.string is None) ## def updateSearch(self, string): # http://www.riverbankcomputing.com/pipermail/pyqt/2008-January/018314.html if not isinstance(string, QtCore.QString): return kdeui.KTreeWidgetSearchLine.updateSearch(self, string) pystring = unicode(string).strip() if pystring: if pystring != self.string: self.string = pystring self.regexes = [ re.compile(re.escape(pat), re.I | re.U) for pat in pystring.split() ] else: self.string = None kdeui.KTreeWidgetSearchLine.updateSearch(self, string) self.timer.start(400) def itemMatches(self, item, string): # We don't need to do anything with the string parameter here because # self.string and self.regexes are always set in updateSearch() above. if self.string is None: return True try: item_text = item.unicode_rel_path except AttributeError: item_text = unicode(item.text(0)) for regex in self.regexes: if not regex.search(item_text): return False else: return True class TreeViewSearchLineWidget(kdeui.KTreeWidgetSearchLineWidget): """Same as super class, but with a TreeViewSearchLine widget.""" def createSearchLine(self, qtreewidget): return TreeViewSearchLine(self, qtreewidget) ## def _get_children(toplevel, filter_func=None): """Returns a filtered list of all direct children of toplevel. :param filter_func: Only include children for which this function returns true. If None, all children will be returned. """ return [ item for item in map(toplevel.child, range(toplevel.childCount())) if filter_func is None or filter_func(item) ] def _populate_tree(parent, directory, force_refresh=False): """A helper function to populate either a TreeView or a DirectoryItem. When populating, this function sets parent.mtime, and when invoked later on the same parent, it will return immediately if the mtime of directory is not different. If it is different, a refresh will be performed, keeping as many existing children as possible. It updates TreeView's empty_directories set as appropriate. """ _my_listdir(directory) mtime, contents = _my_listdir_cache[directory] if mtime == getattr(parent, 'mtime', None): return else: parent.mtime = mtime prune_this_parent = True files = set(contents.keys()) # Check filesystem contents against existing children # TODO What's up with prune_this_parent when refreshing. children = _get_children(parent) if children: # map { basename: item }, to compare with files mapping = dict((i.filename, i) for i in children) keys = set(mapping.keys()) common = files & keys # Remove items no longer found in the filesystem for k in keys - common: parent.removeChild(mapping[k]) # Do not re-add items already in the tree view files -= common # Pointer to the parent QTreeWidget, for empty_directories treewidget = parent.treeWidget() items = [] for filename in files: path = os.path.join(directory, filename) if stat.S_ISDIR(contents[filename].st_mode): item = DirectoryItem(path, treewidget.root) treewidget.empty_directories.add(item) elif minirok.Globals.engine.can_play_path(path): item = FileItem(path, treewidget.root) prune_this_parent = False else: continue items.append(item) if items: parent.addChildren(items) if not prune_this_parent: while parent: treewidget.empty_directories.discard(parent) parent = parent.parent() # This is a dict like: # { path: (mtime, { entry1: stat_struct, entry2: stat_struct, ... }), ... } _my_listdir_cache = {} def _my_listdir(path): """Read directory contents, storing results in a dictionary cache. When invoked over a previously read directory, its contents will only be re-read from the filesystem if the mtime is different to the mtime the last time the contents were read. """ try: mtime = os.stat(path).st_mtime except OSError, e: minirok.logger.warn('could not stat %r: %s', path, e.strerror) _my_listdir_cache.setdefault(path, (None, {})) return if mtime == _my_listdir_cache.get(path, (None, None))[0]: return try: contents = os.listdir(path) except OSError, e: minirok.logger.warn('could not listdir %r: %s', path, e.strerror) _my_listdir_cache.setdefault(path, (None, {})) return d = {} for entry in contents: try: entryp = os.path.join(path, entry) d[entry] = os.stat(entryp) except OSError, e: minirok.logger.warn('could not access %r: %s', entryp, e.strerror) _my_listdir_cache[path] = (mtime, d) minirok-2.1/minirok/__init__.py0000644000175000017500000001171311265733622015257 0ustar datodato#! /usr/bin/env python ## vim: fileencoding=utf-8 # # Copyright (c) 2007-2008 Adeodato Simó (dato@net.com.org.es) # Licensed under the terms of the MIT license. import os import re import sys import logging ## filesystem_encoding = sys.getfilesystemencoding() ## __appname__ = 'minirok' __progname__ = 'Minirok' __version__ = '2.1' __description__ = 'A small music player written in Python' __copyright__ = 'Copyright (c) 2007-2009 Adeodato Simó' __homepage__ = 'http://chistera.yi.org/~adeodato/code/minirok' __bts__ = 'http://bugs.debian.org' __authors__ = [ ('Adeodato Simó', '', 'dato@net.com.org.es'), ] __thanksto__ = [ # ('Name', 'Task', 'Email', 'Webpage'), ('The Amarok developers', 'For their design and ideas, which I copied.\n' 'And their code, which I frequently also copied.', '', 'http://amarok.kde.org'), ('Pino Toscano', 'For saving me from KConfigDialogManager + QButtonGroup misery.', 'pino@kde.org', ''), ] __license__ = '''\ Minirok is Copyright (c) 2007-2009 Adeodato Simó, and licensed under the terms of the MIT license: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''' ## def _minirok_logger(): levelname = os.environ.get('MINIROK_DEBUG_LEVEL', 'warning') level = getattr(logging, levelname.upper(), None) if not isinstance(level, int): bogus_debug_level = True level = logging.WARNING else: bogus_debug_level = False fmt = 'minirok: %(levelname)s: %(message)s' stderr = logging.StreamHandler(sys.stderr) stderr.setFormatter(logging.Formatter(fmt)) logger = logging.getLogger('minirok') logger.setLevel(level) logger.addHandler(stderr) if bogus_debug_level: logger.warn('invalid value for MINIROK_DEBUG_LEVEL: %r', levelname) return logger logger = _minirok_logger() del _minirok_logger ## _do_exit = False _not_found = [] try: from PyQt4 import ( QtGui, QtCore, # used below ) except ImportError: _do_exit = True _not_found.append('PyQt') try: from PyKDE4 import ( kio, kdeui, # used below kdecore, ) except ImportError, e: _do_exit = True _not_found.append('PyKDE (error was: %s)' % e) try: import mutagen except ImportError: _do_exit = True _not_found.append('Mutagen') try: # Do not import gst instead of pygst here, or gst will eat our --help import pygst pygst.require('0.10') except ImportError: _do_exit = True _not_found.append('GStreamer Python bindings') except pygst.RequiredVersionError: _do_exit = True _not_found.append('GStreamer Python bindings (>= 0.10)') try: import json except ImportError: try: import simplejson except ImportError: _do_exit = True _not_found.append('json or simplejson module') try: import dbus import dbus.mainloop.qt except ImportError: _has_dbus = False else: match = re.match(r'(\d+)\.(\d+).(\d+)', str(QtCore.qVersion())) version = tuple(map(int, match.groups())) # XXX match could be None? if version >= (4, 4, 0): _has_dbus = True else: logger.warn('disabling DBus interface: ' \ 'Qt version is %s, but 4.4.0 is needed', QtCore.qVersion()) _has_dbus = False if _not_found: print >>sys.stderr, ('''\ The following required libraries could not be found on your system: %s See the "Requirements" section in the README file for details about where to obtain these dependencies, or how to install them from your distribution.''' % ('\n'.join(' * %s' % s for s in _not_found))) if _do_exit: sys.exit(1) del _do_exit del _not_found ## class Globals(object): """Singleton object to hold pointers to various pieces of the program. See the __slots__ variable for a list of available attributes. """ __slots__ = [ 'engine', 'playlist', 'preferences', 'action_collection', ] Globals = Globals() minirok-2.1/minirok/dbusface.py0000644000175000017500000000457511265733622015304 0ustar datodato#! /usr/bin/env python ## vim: fileencoding=utf-8 # # Copyright (c) 2007-2008 Adeodato Simó (dato@net.com.org.es) # Licensed under the terms of the MIT license. import dbus import dbus.service import minirok from minirok import util ## DBUS_SERVICE_NAME = 'org.kde.minirok' ## class Player(dbus.service.Object): def __init__(self): dbus.service.Object.__init__(self, dbus.SessionBus(), '/Player') @staticmethod def get_action(action_name): """Returns the trigger method of a named action.""" action = minirok.Globals.action_collection.action(action_name) if action is None: minirok.logger.error('action %r not found', action_name) return lambda: None else: return action.trigger ## decorator = dbus.service.method(DBUS_SERVICE_NAME) decorator_as = dbus.service.method(DBUS_SERVICE_NAME, 'as') decorator_s_s = dbus.service.method(DBUS_SERVICE_NAME, 's', 's') @decorator def Play(self): self.get_action('action_play')() @decorator def Pause(self): self.get_action('action_pause')() @decorator def PlayPause(self): self.get_action('action_play_pause')() @decorator def Stop(self): self.get_action('action_stop')() @decorator def Next(self): self.get_action('action_next')() @decorator def Previous(self): self.get_action('action_previous')() @decorator def StopAfterCurrent(self): self.get_action('action_toggle_stop_after_current')() @decorator_as def AppendToPlaylist(self, paths): files = map(util.kurl_to_path, paths) minirok.Globals.playlist.add_files_untrusted(files) @decorator_s_s def NowPlaying(self, format=None): tags = minirok.Globals.playlist.get_current_tags() if not tags: formatted = '' else: if format is not None: try: formatted = format % tags except (KeyError, ValueError, TypeError), e: formatted = '>> Error when formatting string: %s' % e else: title = tags['Title'] artist = tags['Artist'] if artist is not None: formatted = u'%s - %s' % (artist, title) else: formatted = title return formatted minirok-2.1/.gitignore0000644000175000017500000000017111265733622013462 0ustar datodato*.pyc /minirok.1 /minirok/tags /minirok/ui/*.py /debian/files /debian/minirok /debian/*.substvars /debian/*.debhelper* minirok-2.1/README.Usage0000644000175000017500000000564611265733622013431 0ustar datodatoUI hints ======== * Enqueue tracks in the playlist with Ctrl+RightButtonClick, like in Amarok. * Mark a track as "stop after this track" with Ctrl+MiddleButtonClick. * Press return in the tree view search line to append all the search results to the playlist, starting playback if the playlist was empty and the player stopped. Press return as well in the playlist search line to start playing the first track in the search result. DBus interface ============== Minirok exports a DBus interface; the object path is /Player, and the service org.kde.minirok. There are functions to perform playlist actions (Play, Pause, PlayPause, Stop, Next, Previous, StopAfterCurrent), and a function to retrieve the currently playing track, "NowPlaying". This last functions comes in two flavours: without an argument, it will return a string like "Artist - Title", or just "Title" if there's no known artist. However, you can pass a string argument that will be formatted against a dict of the tags with the Python % operator. For example: % qdbus org.kde.minirok \ /Player NowPlaying "%(Artist)s - %(Title)s [%(Album)s]" Do not forget the "s" after the brackets, it's needed by Python. There is also an AppendToPlaylist function, which does the same as `minirok --append`. The function takes a list of paths as argument, so to invoke it you must place the arguments between brackets, like this: % qdbus org.kde.minirok \ /Player AppendToPlaylist '(' /path/to/file.mp3 ... ')' Note that you have to specify the full paths of files, or it won't work. Regular expressions =================== Instead of reading tags from audio files, a Python regular expression can be used to guess them from the filename. The full patch will be searched, but the regular expression does not need to match the full path (for pythonistas, it'll be a re.search, not a re.match). The tags will be extracted from the named groups of the match, namely: "title", "artist", "album", and "track". Even if a regular expression is configured, tags will still be read from the files in the background. This can be configured in the Preferences dialog so that they are never read, or only if the regular expression did not match. A regular expression match with an empty "title" group is considered as failure to match. An example of a simple regular expression that matches "Artist - Title.mp3" would be: '/((?P.+?) - )?(?P.+)\.[^.]+$' A more elaborated one, the one I use: '(?i).*?/(\(\d+\) )?(?P<album>[^/]+(/(CD|vol|disco) *\d+)?)/((?P<track>\d+)_)?((?P<artist>[^/]+?) - )?(?P<title>[^/]+)\.[^.]+$' This matches, case insensitively: .../Album/Artist - Title.mp3 .../Album/07_Artist - Title.mp3 .../(year) Album/07_Artist - Title.mp3 .../(year) Album/cd 1/07_Artist - Title.mp3 For more information on Python regular expression: http://docs.python.org/lib/module-re.html http://docs.python.org/lib/re-syntax.html ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������