pax_global_header00006660000000000000000000000064147340776720014533gustar00rootroot0000000000000052 comment=d2c663c1c67b3e5fe5d4fa055935935f7652aa8e cplay-ng-5.4.0/000077500000000000000000000000001473407767200132535ustar00rootroot00000000000000cplay-ng-5.4.0/.github/000077500000000000000000000000001473407767200146135ustar00rootroot00000000000000cplay-ng-5.4.0/.github/workflows/000077500000000000000000000000001473407767200166505ustar00rootroot00000000000000cplay-ng-5.4.0/.github/workflows/main.yml000066400000000000000000000011241473407767200203150ustar00rootroot00000000000000on: [push] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 - run: python3 -m pip install ruff - name: linters run: ruff check cplay.py publish: needs: [lint] if: startsWith(github.ref, 'refs/tags') runs-on: ubuntu-latest permissions: id-token: write steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 - run: python3 -m pip install build - name: build run: python3 -m build - name: publish uses: pypa/gh-action-pypi-publish@release/v1 cplay-ng-5.4.0/.gitignore000066400000000000000000000000421473407767200152370ustar00rootroot00000000000000*.egg-info build dist *.egg cover cplay-ng-5.4.0/AUTHORS000066400000000000000000000007451473407767200143310ustar00rootroot00000000000000cplay was originally written by Ulf Betlehem. Contributors include: Andreas van Cranenburgh Tom Adams Yoann AUBINEAU Tobias Bengfort Charl P. Botha Chmouel Boudjnah Adrian C. Ricardo Niederberger Cabral Jesus Climent Jason M. Felice Jay Felice Samium Gromoff Väinö Järvelä Georg Lehner Jean-Nicolas Kuttler Daniel Michalik Martin Michlmayr Gergely Nagy Patrice Neff Tomi Pieviläinen Antoine Reilles Peter Samuelson Gerald Stieglbauer Christian Storgaard Toni Timonen Moshe Zadka cplay-ng-5.4.0/ChangeLog000066400000000000000000000652331473407767200150360ustar00rootroot000000000000002024-12-28 Tobias Bengfort *** 5.4.0 *** - allow to refresh file list - automatically pause when playback was interrupted (e.g. because the device was suspended) - strip leading v when parsing mpv version 2024-08-03 Tobias Bengfort *** 5.3.1 *** - fix compatibility with mpv 0.38.0 - do not mess up the terminal when importing the cplay python module - make `__version__` comply with pep440 2024-05-20 Tobias Bengfort *** 5.3.0 *** - allow to start URLs with offset (see https://www.w3.org/TR/media-frags/) - fix compatibility with mpv 0.38 and above (thanks to @AlkyoneAoide) 2023-03-09 Tobias Bengfort *** 5.2.0 *** - use mpv's native volume controls (thanks to @AlkyoneAoide) - use XDG_RUNTIME_DIR for mpv socket - do not crash on invalid utf-8 from mpv 2022-11-22 Tobias Bengfort *** 5.1.0 *** - change keys for previous/next search match to [ and ] - if a stream contains metadata, display the name of the currently playing track - use @DEFAULT_SINK@ instead of hardcoded index to set volume 2021-08-30 Tobias Bengfort *** 5.0.1 *** - Fix in-app version number 2021-08-30 Tobias Bengfort *** 5.0.0 *** - changes to playlists are no longer automatically written back to their files. Instead, a * is appended to the playlist title if there are unsaved modifications. They can be written to a file with the `w` key. - cplay now uses a single instance of mpv and its IPC mechanism instead of parsing command line output. - fix: stop playback when cplay crashes - fix: do not crash on tiny screen size - fix: mpv 0.33 compatibility 2020-09-07 Tobias Bengfort *** 4.0.0 *** This is a complete rewrite which massively simplifies the code and intentionally breaks a lot of things in the process. * breaking changes: - drop support for all players except mpv - drop support for all mixers except pulse - drop support for all playlists except m3u - drop support for translations - drop support for fifo/cnq - drop support for metadata (mutagen) - rm all command line arguments - rm some key bindings, most without replacement - z for play/pause (use x or Space instead) - p for previous track - +/- for volume control (use 0..9 instead) - for horizontal scrolling - l for list mode - t/T/u/U/i/Space for tagging - o for open path - anything involving the ctrl key - Q no longer asks for confirmation * new features: - state and presentation have been decoupled, resulting in simpler code - support for unicode input - interactive recursive search - it is now possible to open a playlist file and edit it rather than just adding its contents to the internal playlist 2019-09-20 Tobias Bengfort *** 3.0.0 *** * breaking changes: - drop support for python 2 - drop support for configuration - drop support for macros - drop support for executing shell commands - drop support for bookmarks - drop support for playing videos - drop support for vlc backend - drop support for deprecated python-oss - drop "stop after each track" feature * new features: - add --autoexit option to close at the end of the playlist - automatically start playing if a file was passed - add support for webm - allow moving the current track if none is tagged 2018-04-10 Tobias Bengfort *** 2.4.1 *** - fix an issue with translation - fix outdated references in README 2018-04-10 Tobias Bengfort *** 2.4.0 *** - do not add individual files when there are playlists - add support for cue files - add mpv backend - a lot of refactoring an small fixes 2017-06-05 Tobias Bengfort *** 2.3.0 *** - fix various unicode issues - fix detection of valid song/playlist on URLs - fix: terminate backend on crash - internal restructuring to facilitate search plugins - new backend: ffplay - add http support to avplay and gst123 backends - fix: infinite loop in vlc backend 2017-01-30 Tobias Bengfort *** 2.2.0 *** - fix: space not allowed in extra requirement name - fix unicode issue with mplayer - improve sox backend - allow https in URLs - add avplay backend - wrap-around find 2016-01-03 Tobias Bengfort *** 2.1.2 *** - fix warning when using setup.py without babel installed - allow http protocol - various fixes related to mixers 2015-12-28 Tobias Bengfort *** 2.1.1 *** - add --version option - fix displaying bytestrings in python3 2015-10-30 Tobias Bengfort *** 2.1.0 *** - dropped support for ncurses - dropped support for mplayer specific features (speed, equalizer) - removed lircrc - removed cplay.list - added support for VLC - added support for playing videos - added -s flag to allow saving state on close and restoring on open - log error instead of crash on invalid cplayrc - fixed some python3 bytestring issues - translations are now managed on https://www.transifex.com/projects/p/cplay-ng/ - metadata detection has been refactored and should now be more reliable - a lot of internal restructuring to ease collaboration with Andreas van Cranenburgh's fork at https://github.com/andreasvc/cplay 2015-03-20 Tobias Bengfort *** 2.0.3 *** - test are now run with python2.7, python3.4 and pypy - some fixes to the pulseaudio volumn mixer (Andreas van Cranenburgh) - fix parsing of mpg123 output (times larger than 1h) 2014-07-13 Tobias Bengfort *** 2.0.2 *** - fix regression where cplay crashed when opened with an argument - replace getopt by argparse which provides a better command line interface - allow to select a socket for use with cnq 2014-06-18 Tobias Bengfort *** 2.0.1 *** This release brings a basic testing environment and some internal restructuring. The following bugs have been fixed: * cplay: - fix regular expressions in python3 - only add valid songs to playlist - don't crash on missing backend - don't require babel for installation * cnq: - declare missing argparse dependency 2014-06-15 Tobias Bengfort *** 2.0.0 *** cplay has been unmaintained for many years now. I tried to contact the original developers but without luck. So now I am announcing a fork of cplay: cplay-ng. My short time goal with this was to be able to install cplay from pypi. This goals has now been reached. Future plans include new features and a test suite. * cplay: - renamed to cplay-ng - dropped support for python < 2.6 - python3 compatibility - pep8 compatibility - setuptools integration - pulse mixer support (Andreas van Cranenburgh) - add gst123 backend which uses gstreamer and therefore supports many audio formats - midi support through timidity - scale key-volume mapping such that 9 is 100% * cnq: - renamed to cnq-ng - complete rework to become a full featured remote control for cplay-ng 2011-04-27 Tomi Pieviläinen *** 1.50 *** * cplay: - fix insecure /tmp handling (DB#255768, DB#324913) (Peter Samuelson) - fix shell crashing (DB#375060) - UTF-8 support (DB#279000) - debug logging - mutagen support (with ogg/flac metadata and fix to DB#413738) - file recognition with magic (based on Jesus Climent) - mplayer support with equalizer and speed support (Tom Adams, Daniel Michalik) - preliminary ALSA mixer support (Tom Adams) - bugfixes (many authors) * cnq: New executable to enque tracks to cplay (Tom Adams, fixes DB#226167) 2011-04-15 Daniel Michalik Five years after the last pre-release by Ulf Betlehem some development took place, individually distributed amongst various persons. This version of cplay collects the found fixes and improvements and provides: - proper MPlayer support, - volume control using ALSA and OSS, - fixes of all known critical bugs, more bug fixes and clean up work. Enjoy your updated cplay experience! For you information and entertainment the following list contains the "recent" development history, trying to give proper credits to the developers involved. I tried to be as complete and accurate as possible, please let me know if there is someone I forgot or if there is something that I put down incorrectly. - Peter Samuelson fixed the insecure /tmp handling (DB#255768, DB#324913) together with Martin Michlmayr. - Tom Adams collected the complete version history of cplay and published it on github. Furthermore he provided basic MPlayer support, the cnq script and ALSA mixer control. - Tomi Pieviläinen fixed the shell crashing bug (DB#375060), added logging and added mutagen support for reading meta data information from played files (DB#413738). Various other improvements include the removal of the kludge variable and clean up work/commenting. - Adrian C. fixed various small bugs and made cplay ready for ncurses 5.8. - Daniel Michalik replaced the mplayer FIFO by internal pipes to prevent permission and IO blocking problems, added speed control and equalizer support when using MPlayer and fixed some bugs (DB#387871, DB#303282, sanity check of counter values). 2006-05-09 Ulf Betlehem *** 1.50pre7 *** * cplay: - work-around backspace problem (shrizza) - shell crash work-around 2005-11-10 Ulf Betlehem *** 1.50pre6 *** * cplay: - share filedescriptors (Antoine Reilles) 2005-10-21 Ulf Betlehem *** 1.50pre5 *** Over a year since last pre-release! I will probably have broken more things than I have fixed, but here goes: * cplay: - fixed URL bug on command line (Georg Lehner) - replaced deprecated apply() - one-line scrolling - continue after errors during recursive add - added FrameOffsetPlayerMpp - handle_command a bit differently 2004-07-25 Ulf Betlehem *** 1.50pre4 *** * cplay: - ogg123 now handles .flac and .spx - require either ID3 or ogg modules for viewing metadata 2004-02-09 Ulf Betlehem *** 1.50pre3 *** * cplay: - replaced volup and voldown FIFOControl commands with one "volume set|cue|toggle N" command - removed inc_volume and dec_volume wrappers - added FIFOControl command "empty" (delete playlist) * lircrc: - volup, voldown => volume command 2004-02-07 Ulf Betlehem *** 1.50pre2 *** * po/pt_BR.po: - new file (Ricardo Niederberger Cabral) * cplay: - allow shell from playlist - user definable macros (for example MACRO['d'] = '!rm "$@"\n') - new remote control commands: macro , add * README: - documented macros and shell positional arguments 2004-02-07 Ulf Betlehem *** 1.50pre1 *** * cplay: - user definable macros (for example MACRO['d'] = '!rm "$@"\n') - new remote control commands: macro , add * README: - documented macros and shell positional arguments 2004-02-05 Ulf Betlehem * README: - mkfifo /var/tmp/cplay_control (Ricardo Niederberger Cabral) 2004-01-07 Ulf Betlehem * po/Makefile, Makefile: - SHELL = /bin/bash (Murphy) 2003-12-05 Ulf Betlehem *** 1.49 *** * README, cplay.1: - document restricted mode 2003-11-08 Ulf Betlehem *** 1.49pre4 *** * cplay: - restricted mode (suggested by Yoann AUBINEAU) - connect player stdin to a pipe - removed sleep(1) if player exec failed - combined pause/unpause -> toggle_pause - no parse_buf() if stopped or seeking - removed --no-tty-control from madplay (stdin no longer tty) - reduced codesize 2003-11-06 Ulf Betlehem * cplay: - use 'm' for bookmarking instead of 'b' - minor code clean-up - modified help page 2003-11-02 Ulf Betlehem *** 1.49pre3 *** * cplay.list: - ESP Package Manager support (http://www.easysw.com/epm/) * cplay: - removed excessive update() from get_bookmark() - rewritten delete and move commands for speed 2003-11-01 Ulf Betlehem * cplay: - move active status support from ListEntry to PlaylistEntry 2003-10-04 Ulf Betlehem * cplayrc: - removed execute permissions 2003-10-01 Ulf Betlehem * cplay: - possible bugfix for increasing CPU usage 2003-10-01 Ulf Betlehem *** 1.49pre2 *** * cplay: - possible bugfix for increasing CPU usage 2003-09-28 Ulf Betlehem * cplay: - use curses.KEY_ENTER for xwsh (wave++) 2003-09-13 Ulf Betlehem *** 1.49pre1 *** * cplay: - support and prefer ossaudiodev (dorphell) 2003-09-01 Ulf Betlehem * cplay: - fixed playlist identification for 1.48 (Jean-Nicolas Kuttler) 2003-08-28 Ulf Betlehem * Makefile: - cplayrc generation * cplay.1: - execute both /etc/cplayrc and ~/.cplayrc - ignore /etc/cplayrc if ~/.cplayrc exists - speex - xmp * README: - speex - ~/.cplayrc 2003-08-26 Ulf Betlehem *** 1.48 *** * cplay: - xmp regexp (Yuri D'Elia) - URL support in mpg123 regexp (Martin Michlmayr) - rudimentary /etc/cplayrc support (Toni Timonen) * cplay.1: - xmp, play and cplayrc references * cplayrc: - new file 2003-08-20 Ulf Betlehem * cplay(1.48pre1): - discontinue python1.5 support - mixer/volume control using python-oss module - horizontal scrolling with < and > - show tail (was head) of long input lines - import random instead of whrandom - minor progress parsing modification - NoOffsetPlayer simply counts seconds (Martin Michlmayr) - TimeOffsetPlayer with full madplay support - added partial xmp and play (sox) support 2003-08-17 Ulf Betlehem * po/hu.po: - new file (Gergely Nagy) * po/pl.po: - new file (Perry) - fixed help text not showing * po/da.po: - new file (Christian Storgaard) - specified charset/encoding 2003-05-13 Ulf Betlehem * cplay: - display "Adding tagged files" instead of a separate message for each file (Martin Michlmayr) - avoid error-messages when interrupting cplay when started via xargs (Moshe Zadka) 2003-04-13 Ulf Betlehem *** 1.47 *** * README: - mp3info and ogginfo modules are both required * TODO: *** empty log message *** * cplay.1: - mention help window - shell command and positional parameters - document control_fifo in FILES section - BUGS section 2003-04-11 Ulf Betlehem * cplay(1.47rc4): - removed "quit silently" command-line option (use Q instead) - fixed missing ": " for isearch-prompt - always add absolute paths to playlist (args and stdin) 2003-04-10 Ulf Betlehem * cplay(1.47rc3): - uses glob.glob() instead of fnmatch.filter() 2003-04-08 Ulf Betlehem * cplay(1.47rc2): - bugfix * cplay(1.47rc1): - status and title now use viewpoints (l) - hide cursor after shell command - help window updates 2003-04-07 Ulf Betlehem * cplay(1.47pre5): - '!' shell command with positional args - TAB completion - kill word/line with C-w/C-u - invert tags command 'i' - removed hide partial pathnames feature - renamed 'X' from [manual] to [stop] - bookmarks - fixed .. -> ../ - actually chdir in filelist - fixed seek/stop/pause crash - minor code cleanup 2003-03-02 Ulf Betlehem * cplay(1.47pre4): - X toggles manual/automatic playlist advancement (Väinö Järvelä) - C-s C-s now remembers previous isearch string - minor code cleanup here and there - absolute seeking with C-a and ^ for bof, C-e and $ for eof - HelpWindow includes "Undocumented commands" - seeking now yield similar results when stopped and paused - fixed byteorder issues with mixer on different architectures? 2003-02-09 Ulf Betlehem * cplay(1.47pre3): - The "Quit? (y/N)" prompt no longer requires Enter. - The number of dirs to hide can be adjusted with < and > for the pathname viewpoint. However, this might still change. - Sorting is now done according to viewpoint, which means that 'S' no longer toggles sorting methods. - Minor help window updates. 2003-01-30 Ulf Betlehem * cplay(1.47pre2): - command line option to quit silently without confirmation - isearch speedup (suggested by Eric Max Francis) - viewpoint speedup 2003-01-25 Ulf Betlehem * cplay(1.47pre1): - added os.path.exists check to get_tag() 2002-12-16 Ulf Betlehem * lircrc: - new file (Pugo) * cplay: - documented @ command - get_tag improvement (Martin Michlmayr) * cplay.1: - combined v and V options into one. 2002-12-16 Ulf Betlehem *** 1.46 *** * cplay: - documented @ command - get_tag improvement (Martin Michlmayr) * cplay.1: - combined v and V options into one. 2002-12-04 Ulf Betlehem * cplay (1.46rc1): - includes latest version of Martin's get_tag 2002-11-30 Ulf Betlehem * cplay (1.46pre9): - alternative metadata support through get_tag (Martin Michlmayr) - misc refactoring: TagListWindow, PlaylistEntry, etc. - scrollable Help Window - fixed keybinding for toggle counter mode - new @ command that jumps to the active playlist entry - removed V command and option, v toggles MASTER/PCM instead - removed custom normpath 2002-11-08 Ulf Betlehem * cplay.1: - Use minuses instead of hyphens for command line options. (Martin) 2002-10-27 Ulf Betlehem * cplay (1.46pre8) - modified keymap! - updated help window - filelist tagging support (based on a patch by Jason M. Felice) - improved status message behavior - added retry if resize failed - show cursor in input mode 2002-10-24 Ulf Betlehem * cplay (1.46pre7) - a couple of status message changes - faster delete when not in random mode - rudimentary .pls playlist support - improved streaming support - advance playlist if player not found - changed player priority order 2002-10-21 Ulf Betlehem * cplay (1.46pre6) - new and improved random mode (Radu) 2002-10-20 Ulf Betlehem * cplay: - refactoring - list mode (l = toggle viewpoints) - q = quit (y/n) and Q = Quit immediately - isearch turnaround change - input cursor position - recursive search duplicates fix - case insensitive regex marking - regex marking matches viewpoint - VALID_SONG regex matches basename - playlist sorting by filename or pathname - don't move empty list of marked entries - SIGTERM -> SIGINT (again) - updated mikmod switches 2002-10-15 Ulf Betlehem * cplay: - pad input with space for cursor position 2002-10-11 Ulf Betlehem * cplay (1.46pre5) - string.punctuation kludge for python 1.5 - recursive search in filelist! - include 669|mtm|it in mikmod regex (Samium Gromoff) 2002-08-28 Ulf Betlehem * cplay (1.46pre4) - bugfix 2002-08-28 Ulf Betlehem * cplay (1.46pre3) - LIRC support via control FIFO (Pugo) 2002-08-21 Ulf Betlehem * cplay (1.46pre2) - allow printable chars as input - alias commands: Q for q and = for + - grid bug removed from line number display - keep current position after auto filelist updates - quiet auto filelist updates (Martin Michlmayr) - select child in filelist after a parent command - parse player output only once every second - PCM/MASTER volume commands show current volume * LICENSE: new file 2002-03-31 Ulf Betlehem * cplay (1.46pre1) - remember playlist filename (Patrice Neff) 2002-03-24 Ulf Betlehem *** 1.45 *** 2002-03-19 Ulf Betlehem * cplay (1.45pre5): - emulate insstr() for python1.5 - new commands m/M = move after/before - new command D = delete current (Jay Felice) - line numbers 2002-01-19 Ulf Betlehem * cplay (1.45pre4): - added options -v and -V to control either PCM or MASTER volume - increase and decrease volume in steps of 3% (kludge) 2002-01-13 Ulf Betlehem * cplay (1.45pre3): - progressbar cosmetics - tilde expansion (Patrice Neff) 2001-12-27 Ulf Betlehem * cplay (1.45pre2): - added "--no-tty-control" option for madplay - removed "-d oss" option from ogg123 (Han) - use insstr instead of addstr to work around a classical curses- problem with writing the rightmost character without scrolling. 2001-12-01 Ulf Betlehem *** 1.44 *** * cplay: - partial support for madplay - partial support for mikmod (yason) - removed sox support - unless someone needs it - toggle counter mode: time done / time left - seek acceleration based on song length - avoid listing dot-files (Martin Michlmayr) - remove ".." entry from root (Martin Michlmayr) - show playlist upon startup if playing (Patrice Neff) - removed TERMIOS warning with recent python versions - add directories from command line (Han) - fixed x-bug (Chris Liechti) - changed write_playlist key from 'o' to 'w' - changed goto command key from 'g' to 'o' - added 'g' (home) and 'G' (end) keys - added '/' and '?' keys for searching - misc tweaks * cplay.1: - update * README: - update 2001-03-15 Ulf Betlehem *** 1.43 *** * cplay: - partial support for splay - commandline arguments: repeat/random (Gerald Stieglbauer) - volume fine tuning via +/- (Martin Michlmayr) - simplified player framework - mark/clear regexp 2001-01-18 Ulf Betlehem *** 1.42 *** * cplay: - ignore bogus gettext module - correct devfs paths - use seconds instead of frames - shuffle speedup (Martin Persson) - changed player hierarchy - improved ogg123 support 2000-12-08 Ulf Betlehem *** 1.41 *** * README: a few words about mpg123 and streaming * po/de.po, cplay.1: updated (Martin Michlmayr) * po/Makefile, Makefile: use "install -c" for compatibility * cplay: - autoplay initial playlist - is now a front-end for various audio players - ogg123 support (Martin Michlmayr) - devfs paths (Martin Michlmayr) - playlist url support (Charl P. Botha) - fixed signalling bug - minor code cleanup 2000-10-19 Ulf Betlehem *** 1.40 *** * README: added instructions on how to change player options * cplay: new versioning scheme fixed locale setting python 2.0 compatible prefers standard gettext to fintl delayed warnings for missing players and unknown fileformats fixed hline with zero length in progressline set title to xterm in cleanup better support for mpg123 buffers by signalling progress groups * README: modified usage * Makefile: install man page * cplay.1: man page (Martin Michlmayr) * ChangeLog, TODO: new entry * po/de.po: update (Martin Michlmayr) 2000-09-06 Ulf Betlehem * cplay: Python 1.6 compatible 2000-08-09 Ulf Betlehem * po/de.po: new file * po/Makefile: new Makefile * killpgmodule.c: *** empty log message *** * README: new README * Makefile: new Makefile 2000-07-31 Ulf Betlehem * cplay: added i18n support by Martin Michlmayr fixed locale support 2000-07-25 Ulf Betlehem * cplay: added support for sox to play .wav, .au and other sound formats * cplay: shows status in titlebar under X -- thanks Chmouel Boudjnah 2000-05-23 Ulf Betlehem * cplay: doesn't stat() cwd when idle * cplay: supports both pyncurses and the old cursesmodule 2000-04-24 Ulf Betlehem * cplay: - restores terminal settings on exceptions - global mp3 and m3u regexps - new and improved keymap class - removed a possible "division by zero" bug 2000-03-24 Ulf Betlehem * cplay: translate evil characters to '?' 2000-02-07 Ulf Betlehem * cplay: fixed a bug in FilelistWindow.add_dir() 2000-01-20 Ulf Betlehem * cplay: - changed the player class so that one can hold down 'n' or 'p' when changing tracks without cplay crashing ;) 2000-01-19 Ulf Betlehem * cplay: Enter now plays files bypassing the playlist Space adds files to playlist a adds recursively z toggles pause x toggles stop m3u lines beginning with '#' are now silently discarded 1999-12-22 Ulf Betlehem * cplay: - lot's of small changes 1999-12-13 Ulf Betlehem * cplay: handles SIGWINCH correctly automatically rereads current dir when modified lot's of minor changes 1999-10-26 Ulf Betlehem * cplay: Added two commands: R = random play order (keeps your playlist intact) S = sort playlist by filename Removed a seldom used (also undocumented) command: N = previous track 1999-05-10 Ulf Betlehem * cplay: catches os.error if os.listfiles() fails. 1999-02-13 Ulf Betlehem * cplay: Added error-checking to prevent manipulating empty playlists. Raised default seek speed from Pi to 4. 1999-02-07 Ulf Betlehem * cplay: Corrected a feature that caused automatic loading of playlists upon entering a directory where the cursor was over a .m3u file. 1999-01-29 Ulf Betlehem * cplay: Uses frames_done and frames_left instead of time_done and time_left. Minor code clean-up. * cplay: Now supports at least mpg123 v0.59o through v0.59q 1999-01-19 Ulf Betlehem * cplay: o Is now "pure Python", which means there is no need for the killpgmodule.so anymore. Oh, joy! o mpg123 is now automatically located in the PATH if not specified absolutely. o Moved mark() to 'space' and pause_or_unpause() to 'p' and stop_or_unstop() to 'k'. o Playlists are now always saved with the extension .m3u. 1998-12-11 Ulf Betlehem * cplay: now consumes anything written on stdout 1998-11-29 Ulf Betlehem * cplay: select() now only timeouts when necessary. 1998-11-20 Ulf Betlehem * cplay: added PlaylistWindow.command_mark_all() 1998-11-12 Ulf Betlehem * cplay: SIGTERM -> SIGINT * cplay: fixed sigchld bug added help window 1998-11-11 Ulf Betlehem * cplay: random -> whrandom * cplay: Too many changes! Reorganization Change of policy 1998-10-29 Ulf Betlehem * cplay: separated PLAYER and COMMAND checks if the PLAYER is valid before it continues 1998-10-27 Ulf Betlehem * cplay: kludged mixed case in curses constants 1998-10-12 Ulf Betlehem * cplay: support for curses module versions with different key-case. 1998-10-05 Ulf Betlehem * cplay: changed progress bar * killpgmodule.c: New file. 1998-04-29 Ulf Betlehem * cplay: remember bufptr of directories 1998-04-20 Ulf Betlehem * cplay: code cleanup 1998-04-18 Ulf Betlehem * cplay: New file. cplay-ng-5.4.0/LICENSE000066400000000000000000000431311473407767200142620ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. cplay-ng-5.4.0/README.md000066400000000000000000000014461473407767200145370ustar00rootroot00000000000000# Description `cplay` is a minimalist music player with a textual user interface written in Python. It aims to provide a power-user-friendly interface with simple filelist and playlist control. Instead of building an elaborate database of your music library, `cplay` allows you to quickly browse the filesystem and enqueue files, directories, and playlists. The original cplay was started by Ulf Betlehem in 1998 and is no longer maintained. This is a rewrite that aims to stay true to the original design while evolving with a shifting environment. ![screenshot of cplay with file browser](screenshot.png) # Requirements - [python3](http://www.python.org/) - [mpv](https://mpv.io/) # Installation $ pip install cplay-ng # Usage $ cplay-ng Press `h` to get a list of available keys. cplay-ng-5.4.0/cplay.py000066400000000000000000000605561473407767200147510ustar00rootroot00000000000000import curses import functools import json import os import random import re import selectors import signal import socket import subprocess import sys import termios import time from contextlib import contextmanager __version__ = '5.4.0' AUDIO_EXTENSIONS = [ 'mp3', 'ogg', 'oga', 'opus', 'flac', 'm4a', 'm4b', 'wav', 'mid', 'wma' ] HELP = """Global ------ Up, k : move to previous item Down, j : move to next item PageUp, K : move to previous page PageDown, J : move to next page Home, g : move to top End, G : move to bottom Enter : chdir or play Tab : switch between filelist/playlist n : next track x, Space : toggle play/pause Left, Right : seek backward/forward / : search [, ] : previous/next search match Esc : cancel 0..9 : volume control h : help q, Q : quit Filelist -------- a : add to playlist s : recursive search BS : go to parent dir r : refresh Playlist -------- d, D : delete item/all m, M : move item down/up r, R : toggle repeat/random s, S : shuffle/sort playlist w : enter filename for current playlist C : close current playlist @ : jump to current track""" def clamp(value, _min, _max): return max(_min, min(_max, value)) def space_between(a, b, n): d = n - (len(a) + len(b)) if d >= 0: return a + ' ' * d + b else: return a[:d] + b def format_time(total): h, s = divmod(total, 3600) m, s = divmod(s, 60) return '%02d:%02d:%02d' % (h, m, s) def str_match(query, s): return all(q in s.lower() for q in query.lower().split()) def resize(*_args): os.write(app.resize_out, b'.') def get_socket(path): while True: try: sock = socket.socket(family=socket.AF_UNIX) sock.connect(path) except (FileNotFoundError, ConnectionRefusedError): time.sleep(0.1) else: return sock @functools.cache def get_mpv_version(): p = subprocess.run(['mpv', '--version'], stdout=subprocess.PIPE) s = p.stdout.split(b' ', 2)[1].decode().lstrip('v') return tuple(int(i) for i in s.split('.')) @functools.lru_cache def relpath(path): if path.startswith('http'): return path elif path.startswith(filelist.path): return path[len(filelist.path):].lstrip('/') else: return os.path.relpath(path) @contextmanager def enable_ctrl_keys(): fd = sys.stdin.fileno() old = termios.tcgetattr(fd) try: tcattr = termios.tcgetattr(fd) tcattr[0] = tcattr[0] & ~(termios.IXON) termios.tcsetattr(fd, termios.TCSANOW, tcattr) yield finally: termios.tcsetattr(fd, termios.TCSADRAIN, old) def get_ext(path): return os.path.splitext(path)[1].lstrip('.') def listdir(path): with os.scandir(path) as it: for entry in sorted(it, key=lambda e: e.name): if entry.name[0] != '.': yield ( entry.path, get_ext(entry.name), entry.is_dir(follow_symlinks=False), ) class Player: def __init__(self): self.path = None self.position = 0 self.length = 0 self.metadata = None self._seek_step = 0 self._seek_timeout = None self.is_playing = False self._playing = 0 self._buffer = b'' self.socket_path = '%s/mpv-cplay-%i.sock' % ( os.getenv('XDG_RUNTIME_DIR', '/tmp'), os.getpid() ) self._proc = subprocess.Popen( [ 'mpv', f'--input-ipc-server={self.socket_path}', '--idle', '--audio-display=no', '--replaygain=track', ], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) self.socket = get_socket(self.socket_path) self._ipc('observe_property', 1, 'time-pos') self._ipc('observe_property', 2, 'duration') self._ipc('observe_property', 3, 'metadata') def _ipc(self, cmd, *args): data = json.dumps({'command': [cmd, *args]}) msg = data.encode('utf-8') + b'\n' self.socket.send(msg) def handle_ipc(self, data): if data.get('event') == 'property-change' and data['id'] == 1: if data.get('data') is not None and not self._seek_step: self.position = data['data'] elif data.get('event') == 'property-change' and data['id'] == 2: if data.get('data') is not None: self.length = data['data'] elif data.get('event') == 'property-change' and data['id'] == 3: self.metadata = data.get('data') elif data.get('event') == 'end-file': self._playing -= 1 def parse_progress(self): self._buffer += self.socket.recv(1024) msgs = self._buffer.split(b'\n') self._buffer = msgs.pop() for msg in msgs: data = json.loads(msg.decode('utf-8', errors='replace')) self.handle_ipc(data) def get_progress(self): if self.length == 0: return 0 return self.position / self.length def get_title(self): title = relpath(self.path) if self.metadata and 'icy-title' in self.metadata: title = '{} [{}]'.format(title, self.metadata['icy-title']) return title def set_volume(self, vol): self._ipc('set', 'volume', str(vol)) def stop(self): self.is_playing = False self._ipc('stop') def _play(self): if not self.path: self.is_playing = False return self.is_playing = True self._playing += 1 if get_mpv_version() >= (0, 38, 0): self._ipc('loadfile', self.path, 'replace', 0, 'start=%i' % self.position) else: self._ipc('loadfile', self.path, 'replace', 'start=%i' % self.position) def play(self, path): if path and (m := re.match(r'^(http.*)#t=([0-9]+)$', path)): self.path = m[1] self.position = float(m[2]) else: self.path = path self.position = 0 self.length = 0 self._seek_step = 0 self._play() def toggle(self): if self.is_playing: self.stop() elif self.path: self._play() def seek(self, direction): d = direction * self.length * 0.002 if self._seek_step * d > 0: # same direction self._seek_step += d else: self._seek_step = d self.position += self._seek_step self.position = min(self.length, max(0, self.position)) self._seek_timeout = time.time() + 0.5 def finish_seek(self): if self._seek_timeout and time.time() >= self._seek_timeout: self._seek_timeout = None self._seek_step = 0 if self.is_playing: self._play() @property def is_finished(self): return self.is_playing and self._playing == 0 def cleanup(self): self._proc.terminate() os.remove(self.socket_path) class Input: def __init__(self): self.active = False self.str = '' def start(self, prompt, on_input=None, on_submit=None, initial=''): self.str = initial self.prompt = prompt self.on_input = on_input self.on_submit = on_submit self.active = True if self.on_input: self.on_input(self.str) def process_key(self, key): if not self.active: return False if key == chr(27): self.str = '' self.active = False elif key == '\n': self.active = False if self.on_submit: self.on_submit(self.str) elif key == curses.KEY_BACKSPACE: self.str = self.str[:-1] elif isinstance(key, str): self.str += key else: self.active = False return False if self.on_input: self.on_input(self.str) return True class List: def __init__(self): self.items = [] self.position = 0 self.cursor = 0 self.active = -1 self.search_str = '' @property def rows(self): return app.rows - 4 def get_title(self): raise NotImplementedError def set_cursor(self, cursor): self.cursor = clamp(cursor, 0, len(self.items) - 1) self.position = clamp( self.position, self.cursor - self.rows + 1, self.cursor ) def move_cursor(self, diff): self.set_cursor(self.cursor + diff) def search(self, q, diff=1, offset=0): self.search_str = q for i in range(len(self.items)): pos = (self.cursor + (i + offset) * diff) % len(self.items) if str_match(q, self.format_item(self.items[pos])): self.set_cursor(pos) return True return False def format_item(self, item): return relpath(item) def render(self): items = self.items[self.position:self.position + self.rows] for i, item in enumerate(items): attr = 0 if self.position + i == self.cursor: attr |= curses.A_REVERSE if self.position + i == self.active: attr |= curses.A_BOLD s_item = self.format_item(item) s_item = space_between(f' {s_item}', '', app.cols) yield (s_item, attr) for _i in range(max(0, self.rows - len(items))): yield '' def process_key(self, key): # noqa: C901 if key in [curses.KEY_DOWN, 'j']: self.move_cursor(1) elif key in [curses.KEY_UP, 'k']: self.move_cursor(-1) elif key in [curses.KEY_NPAGE, 'J']: self.move_cursor(self.rows - 2) elif key in [curses.KEY_PPAGE, 'K']: self.move_cursor(-(self.rows - 2)) elif key in [curses.KEY_END, 'G']: self.set_cursor(len(self.items)) elif key in [curses.KEY_HOME, 'g']: self.set_cursor(0) elif key == '/': app.input.start('/', on_input=self.search) elif key == ']': if self.search_str: self.search(self.search_str, 1, 1) elif key == '[': if self.search_str: self.search(self.search_str, -1, 1) else: return False return True class HelpList(List): def __init__(self): super().__init__() self.items = HELP.split('\n') def get_title(self): return 'Help' def format_item(self, item): return item def process_key(self, key): if key in ['q', 'h']: app.help = False else: return super().process_key(key) return True class Filelist(List): def __init__(self): super().__init__() self.path = None self.rsearch_str = '' self.set_path(os.getcwd()) def get_title(self): title = f'Filelist: {self.path.rstrip("/")}/' if self.rsearch_str: title += f'search "{self.rsearch_str}"/' return title def format_item(self, item): s = super().format_item(item) ext = get_ext(item) if not (ext in AUDIO_EXTENSIONS or ext == 'm3u'): s += '/' return s def set_path(self, path, *, prev=None, refresh=False): if path != self.path: self.path = path os.chdir(path) relpath.cache_clear() self.search_cache = [] elif refresh: self.search_cache = [] self.all_items = [] self.rsearch_str = '' for p, ext, is_dir in listdir(path): if is_dir or ext == 'm3u' or ext in AUDIO_EXTENSIONS: self.all_items.append(p) self.items = self.all_items if prev and prev in self.items: self.set_cursor(self.items.index(prev)) else: self.position = 0 self.cursor = 0 def build_search_cache(self, root): results = [] for path, ext, is_dir in listdir(root): if is_dir: children = self.build_search_cache(path) if children: results.append(path) results += children elif ext in AUDIO_EXTENSIONS or ext == 'm3u': results.append(path) return results def filter(self, query): if not self.search_cache: self.search_cache = self.build_search_cache(self.path) if query: if self.rsearch_str and query.startswith(self.rsearch_str): base = self.items else: base = self.search_cache self.items = [] for path in base: if str_match(query, self.format_item(path)): self.items.append(path) else: self.items = self.all_items self.rsearch_str = query self.set_cursor(self.cursor) def activate(self, item): ext = item.rsplit('.', 1)[-1] if os.path.isdir(item): self.set_path(item) elif ext in AUDIO_EXTENSIONS: playlist.active = -1 player.play(item) elif ext == 'm3u': playlist.load(item) app.toggle_tabs() def process_key(self, key): if key == 'a': if self.items and playlist.add(self.items[self.cursor]): self.move_cursor(1) elif key == 's': app.input.start('search: ', on_input=self.filter) self.filter(self.rsearch_str) elif key == '\n': if self.items: self.activate(self.items[self.cursor]) elif key == 'r': self.set_path(self.path, refresh=True) elif key == curses.KEY_BACKSPACE: if self.rsearch_str: self.set_path(self.path) else: self.set_path(os.path.dirname(self.path), prev=self.path) else: return super().process_key(key) return True class Playlist(List): def __init__(self): super().__init__() self.repeat = False self.random = False self._played = set() self.path = None self.items_written = [] def get_title(self): title = 'Playlist' if self.path: title += f' {os.path.basename(self.path)}' if self.items != self.items_written: title += '*' if self.repeat: title += ' [repeat all]' if self.random: title += ' [random]' return title def clear(self): self.items = [] self.position = 0 self.cursor = 0 self.active = -1 self._played = set() def reorder(self, fn): if not self.items: return cursor_item = self.items[self.cursor] try: active_item = self.items[self.active] except IndexError: active_item = None fn() self.set_cursor(self.items.index(cursor_item)) if active_item: self.active = self.items.index(active_item) def shuffle(self): self.reorder(lambda: random.shuffle(self.items)) def sort(self): self.reorder(lambda: self.items.sort()) def remove_item(self): self.items.pop(self.cursor) if self.active == self.cursor: self.active = -1 elif self.active > self.cursor: self.active -= 1 def move_item(self, direction): new_cursor = clamp(self.cursor + direction, 0, len(self.items) - 1) if self.active == self.cursor: self.active = new_cursor elif direction < 0: if self.active >= new_cursor and self.active < self.cursor: self.active += 1 else: if self.active <= new_cursor and self.active > self.cursor: self.active -= 1 item = self.items.pop(self.cursor) self.items.insert(new_cursor, item) self.set_cursor(new_cursor) def next(self): if not self.items: return if self.random: self._played.add(self.active) left = set(range(len(self.items))).difference(self._played) if left: self.active = random.choice(list(left)) else: self._played = set() if self.repeat: self.active = random.randrange(len(self.items)) else: self.active = -1 return else: self.active += 1 if self.active >= len(self.items) and self.repeat: self.active = 0 try: return self.items[self.active] except IndexError: self.active = -1 def add_dir(self, path): count = 0 for p, _ext, _is_dir in listdir(path): count += self.add(p, recursive=True) return count def add_playlist(self, path): count = 0 dirname = os.path.dirname(path) with open(path, errors='replace') as fh: for _line in fh: line = _line.strip() if not line or line[0] == '#': continue if not re.match(r'^(/|https?://)', line): line = os.path.join(dirname, line) self.items.append(line) count += 1 return count def add(self, path, *, recursive=False): ext = path.rsplit('.', 1)[-1] if os.path.isdir(path): return self.add_dir(path) elif ext == 'm3u' and not recursive: return self.add_playlist(path) elif ext in AUDIO_EXTENSIONS: self.items.append(path) return 1 else: return 0 def load(self, path): self.clear() self.add_playlist(path) self.path = path self.items_written = self.items.copy() def write(self, path): try: with open(path, 'w') as fh: for item in self.items: fh.write(f'{item}\n') self.path = path self.items_written = self.items.copy() except OSError: pass def process_key(self, key): # noqa: C901 if key == 'm': self.move_item(1) elif key == 'M': self.move_item(-1) elif key == 'd': self.remove_item() elif key == 'D': self.clear() elif key == 'C': self.clear() self.path = None self.items_written = [] elif key == '\n': if not self.items: return True self.active = self.cursor player.play(self.items[self.active]) elif key == '@': self.set_cursor(self.active) elif key == 's': self.shuffle() elif key == 'S': self.sort() elif key == 'r': self.repeat = not self.repeat elif key == 'R': self.random = not self.random elif key == 'w': app.input.start( 'write playlist to path: ', on_submit=self.write, initial=self.path or filelist.path, ) else: return super().process_key(key) return True class Application: def __init__(self): self.tabs = [filelist, playlist] self.help = False self.input = Input() self.old_lines = [] # self-pipe to avoid concurrency issues with signal self.resize_in, self.resize_out = os.pipe2(os.O_NONBLOCK) def refresh_dimensions(self): self.rows, self.cols = self.screen.getmaxyx() def on_resize(self): curses.endwin() self.screen.refresh() self.refresh_dimensions() self.tab.set_cursor(app.tab.cursor) @property def tab(self): if self.help: return helplist else: return self.tabs[0] def toggle_tabs(self): self.tabs.append(self.tabs.pop(0)) def format_progress(self): progress = min(int(self.cols * player.get_progress()), self.cols - 1) return '=' * (progress - 1) + '|' + '-' * (self.cols - progress) def _render(self): yield (self.tab.get_title(), curses.A_BOLD) yield '-' * self.cols yield from self.tab.render() yield self.format_progress() if self.input.active: status = f'{self.input.prompt}{self.input.str}█' elif self.tab == helplist: status = f'cplay-ng {__version__}' elif player.is_playing: status = f'Playing {player.get_title()}' else: status = '' counter = ' / '.join([ format_time(player.position), format_time(player.length), ]) yield space_between(status, counter, self.cols) def render(self, *, force=False): lines = list(self._render()) try: for i, line in enumerate(lines): if ( not force and len(self.old_lines) > i and line == self.old_lines[i] ): continue self.screen.move(i, 0) self.screen.clrtoeol() if isinstance(line, str): self.screen.insstr(i, 0, line, 0) else: self.screen.insstr(i, 0, *line) # make sure cursor is in a meaningful position for a11y self.screen.move(self.tab.cursor - self.tab.position + 2, 0) self.screen.refresh() except curses.error: pass self.old_lines = lines def process_key(self, key): # noqa: C901 if self.input.process_key(key): pass elif self.tab.process_key(key): pass elif key in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']: player.set_volume(int(key, 10) * 11) elif key == curses.KEY_RIGHT: player.seek(1) elif key == curses.KEY_LEFT: player.seek(-1) elif key in ['x', ' ']: player.toggle() elif key == 'n': player.play(playlist.next()) elif key == 'h': self.help = True elif key in ['q', 'Q']: sys.exit(0) elif key == '\t': app.toggle_tabs() else: return False return True def run(self): self.refresh_dimensions() self.render() with selectors.DefaultSelector() as sel: sel.register(sys.stdin, selectors.EVENT_READ) sel.register(self.resize_in, selectors.EVENT_READ) sel.register(player.socket, selectors.EVENT_READ) prev = time.time() while True: player.finish_seek() timeout = 0.5 if player.is_playing else None for key, _mask in sel.select(timeout): # if we have skipped multiple seconds, it is probably # because the system was suspended. This heuristic is much # simpler than detecting suspend via dbus. if player.is_playing and time.time() - prev > 5: player.stop() prev = time.time() if key.fileobj is self.resize_in: os.read(self.resize_in, 8) self.on_resize() self.render(force=True) elif key.fileobj is sys.stdin: self.process_key(self.screen.get_wch()) elif key.fileobj is player.socket: player.parse_progress() if player.is_finished: player.play(playlist.next()) self.render() player = Player() playlist = Playlist() filelist = Filelist() helplist = HelpList() app = Application() def main(): app.screen = curses.initscr() app.screen.keypad(True) # noqa: FBT003 curses.cbreak() curses.noecho() curses.meta(True) # noqa: FBT003 curses.curs_set(0) signal.signal(signal.SIGWINCH, resize) try: with enable_ctrl_keys(): app.run() finally: player.cleanup() curses.endwin() if __name__ == '__main__': main() cplay-ng-5.4.0/pyproject.toml000066400000000000000000000014711473407767200161720ustar00rootroot00000000000000[build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" [project] name = "cplay-ng" version = "5.4.0" description = "A simple curses audio player" readme = "README.md" license = {text = "GPLv2+"} keywords = ["music-player", "curses"] authors = [ {name = "Ulf Betlehem", email = "flu@iki.fi"} ] maintainers = [ {name = "Tobias Bengfort", email = "tobias.bengfort@posteo.de"} ] classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console :: Curses", "Intended Audience :: End Users/Desktop", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", ] [project.urls] Homepage = "https://github.com/xi/cplay-ng" [project.scripts] cplay-ng = "cplay:main" [tool.setuptools] py-modules = ["cplay"] cplay-ng-5.4.0/screenshot.png000066400000000000000000000111121473407767200161320ustar00rootroot00000000000000PNG  IHDRVkD: pHYsȥtIME #`R"1iTXtCommentCreated with GIMPd.eIDATx[PҕeglT|-iGwB^Y^???k?R*tWQW]J%~NӔx4>aϩٴ,e3H???@i۫u?̛y#`swG:"wv89bް'a;Φw.Ӛ.tV{] jTO#qm-ijm߫2X5M>X_;Ҽ諭uͼ"&c并5=_;3 6afDz'FJ36}"ַmw޷ݕڛ)yß|ߘ޹yHeyt2kR 27wx^8| Зr^Qs]?R[۹Ld6A}ݭ)sket!ͽbs侣lA>(b4 +L'Wi&XJR"^r`~HkC[dic} [_=mPiIcPGolQ6dOz6s?a!xpH: ,g~n~S7UWm3|rku[R>SYާ*elF^夬;HIiODž*ͪp%˻dn|݊jyo͏Aٓ[h4}Lzg&XCYݺ|k~yv^CRV9Oo}̧տfxdϜPܹ==jguke$3p'cنK0)e<߸\_FxzoKE*GǟW)T{ 0uF0_[kp00w&+@̭TB(T; 2@&`BLE.p0pLV 4 `5Mq1gbޟ^d޽wOaW_m)߬{pyePhɼ?-@LBSae{fl^wROx#O^./#3{5ǢʊD5k}\^+r s~yQ9^5 F3o ۶4iL٭qwhUh1Sre^GZcZg"N8h)ɼhGpz]^Dc'\ &sA1n._Ii w f<[CYʼ"g_ }}cxY(왬1wqZ,ᄫe6AVb})GTXl9 E)YK5r<^MIg\ E 9V^c7m Q`"??? HZHd#j9b-ز%4`dFxOxǹ~~~*x!W)Zϙ~kwu,s̭3)%K7?i8}:+r\Tk^㭑՞{鬶w‡/[)9Lye#\}179lzOpRja_rK_:YSxyJ}masܸ4t]~բنW.Yw9\y%^FXNXUh1# Y}HйLg=kf4oE^6vz_9䵗{;}gI0vw+YJow5,qO3e}"Tȇ>32m~eDk1wsVm*\y]<##l?rt;&ُ+ cW9]n|_j_ľتY\z(oZvu~=)#cjy})Yw+g=*W*Fl^}ʊRUW.oTR(\30ήӾ[Srz;RcBr'{R5 >. O֙6w.}vx?C9IENDB`