pax_global_header 0000666 0000000 0000000 00000000064 15173102562 0014514 g ustar 00root root 0000000 0000000 52 comment=739e1b7c90c288cc837494f68b9b6766f1f37aac
cplay-ng-5.5.0/ 0000775 0000000 0000000 00000000000 15173102562 0013235 5 ustar 00root root 0000000 0000000 cplay-ng-5.5.0/.github/ 0000775 0000000 0000000 00000000000 15173102562 0014575 5 ustar 00root root 0000000 0000000 cplay-ng-5.5.0/.github/workflows/ 0000775 0000000 0000000 00000000000 15173102562 0016632 5 ustar 00root root 0000000 0000000 cplay-ng-5.5.0/.github/workflows/main.yml 0000664 0000000 0000000 00000001124 15173102562 0020277 0 ustar 00root root 0000000 0000000 on: [push]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
- 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@v6
- uses: actions/setup-python@v6
- 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.5.0/.gitignore 0000664 0000000 0000000 00000000042 15173102562 0015221 0 ustar 00root root 0000000 0000000 *.egg-info
build
dist
*.egg
cover
cplay-ng-5.5.0/AUTHORS 0000664 0000000 0000000 00000000745 15173102562 0014313 0 ustar 00root root 0000000 0000000 cplay 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.5.0/CHANGES.md 0000664 0000000 0000000 00000015164 15173102562 0014636 0 ustar 00root root 0000000 0000000 # 5.5.0 (2026-04-25)
- include symlink dirs in directory listings
- ignore symlink loops when adding a directory
- shift scroll position if space is available at the bottom
- move cursor to top on recursive search
- fix: move cursor when deleting the last item
- use casefold for string comparison
- pyproject.toml: update license format
- handle file system errors (e.g. PermissionError)
- convert changelog to markdown
# 5.4.0 (2024-12-28)
- 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
# 5.3.1 (2024-08-03)
- fix compatibility with mpv 0.38.0
- do not mess up the terminal when importing the cplay python module
- make `__version__` comply with pep440
# 5.3.0 (2024-05-20)
- allow to start URLs with offset (see )
- fix compatibility with mpv 0.38 and above (thanks to @AlkyoneAoide)
# 5.2.0 (2023-03-09)
- 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
# 5.1.0 (2022-11-22)
- 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
# 5.0.1 (2021-08-30)
- Fix in-app version number
# 5.0.0 (2021-08-30)
- 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
# 4.0.0 (2020-09-07)
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
# 3.0.0 (2019-09-20)
- 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
# 2.4.1 (2018-04-10)
- fix an issue with translation
- fix outdated references in README
# 2.4.0 (2018-04-10)
- do not add individual files when there are playlists
- add support for cue files
- add mpv backend
- a lot of refactoring and small fixes
# 2.3.0 (2017-06-05)
- 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
# 2.2.0 (2017-01-30)
- 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
# 2.1.2 (2016-01-03)
- fix warning when using setup.py without babel installed
- allow http protocol
- various fixes related to mixers
# 2.1.1 (2015-12-28)
- add `--version` option
- fix displaying bytestrings in python3
# 2.1.0 (2015-10-30)
- 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
- 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
# 2.0.3 (2015-03-20)
- 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)
# 2.0.2 (2014-07-13)
- 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
# 2.0.1 (2014-06-18)
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
# 2.0.0 (2014-06-15)
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
# older releases
releases before 2.0.0 are listed in ChangeLog.old
cplay-ng-5.5.0/ChangeLog.old 0000664 0000000 0000000 00000047012 15173102562 0015570 0 ustar 00root root 0000000 0000000 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.5.0/LICENSE 0000664 0000000 0000000 00000043131 15173102562 0014244 0 ustar 00root root 0000000 0000000 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.5.0/README.md 0000664 0000000 0000000 00000001446 15173102562 0014521 0 ustar 00root root 0000000 0000000 # 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.

# 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.5.0/cplay.py 0000664 0000000 0000000 00000061506 15173102562 0014727 0 ustar 00root root 0000000 0000000 import 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.5.0'
XDG_RUNTIME_DIR = os.getenv('XDG_RUNTIME_DIR', '/tmp')
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(int(total), 3600)
m, s = divmod(s, 60)
return f'{h:02d}:{m:02d}:{s:02d}'
def str_match(query, s):
return all(q in s.casefold() for q in query.casefold().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
else:
return os.path.relpath(path, filelist.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):
try:
with os.scandir(path) as it:
for entry in sorted(it, key=lambda e: e.name):
if entry.name.startswith('.'):
continue
ext = get_ext(entry.name)
if entry.is_dir() or ext in AUDIO_EXTENSIONS or ext == 'm3u':
yield entry
except OSError:
pass
def walk(path, seen=None):
realpath = os.path.realpath(path)
seen = seen or set()
if realpath in seen:
return []
seen.add(realpath)
result = []
for entry in listdir(path):
if entry.is_dir():
children = walk(entry.path, seen)
if children:
result.append(entry.path)
result += children
else:
result.append(entry.path)
return result
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 = f'{XDG_RUNTIME_DIR}/mpv-cplay-{os.getpid()}.sock'
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, f'start={self.position}')
else:
self._ipc('loadfile', self.path, 'replace', f'start={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)
if len(self.items) < self.rows:
self.position = 0
else:
self.position = clamp(
self.position, self.cursor - self.rows + 1, self.cursor
)
if len(self.items) < self.rows + self.position:
self.position = len(self.items) - self.rows
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 = os.getcwd()
self.all_items = []
self.items = []
self.search_cache = []
self.position = 0
self.cursor = 0
self.rsearch_str = ''
self.set_path(self.path, fail_silently=False)
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, fail_silently=True):
if path != self.path:
try:
os.chdir(path)
except Exception:
if fail_silently:
return
raise
self.path = path
relpath.cache_clear()
self.search_cache = []
elif refresh:
self.search_cache = []
self.all_items = []
self.rsearch_str = ''
self.all_items = [entry.path for entry in listdir(path)]
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 filter(self, query):
if not self.search_cache:
self.search_cache = walk(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)
self.set_cursor(0)
else:
self.items = self.all_items
self.rsearch_str = query
def activate(self, item):
ext = get_ext(item)
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
self.move_cursor(0)
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, root):
count = 0
for path in walk(root):
if get_ext(path) in AUDIO_EXTENSIONS:
count += self.add(path)
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):
ext = get_ext(path)
if os.path.isdir(path):
return self.add_dir(path)
elif ext == 'm3u':
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.screen: curses.window
# 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(line, 0)
else:
self.screen.insstr(*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.5.0/pyproject.toml 0000664 0000000 0000000 00000001472 15173102562 0016155 0 ustar 00root root 0000000 0000000 [build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "cplay-ng"
version = "5.5.0"
description = "A simple curses audio player"
readme = "README.md"
license = "GPL-2.0-or-later"
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.5.0/screenshot.png 0000664 0000000 0000000 00000011112 15173102562 0016114 0 ustar 00root root 0000000 0000000 PNG
IHDR V kD: pHYs ȥ tIME #`R"1 iTXtComment Created with GIMPd.e IDATx[ PҕeglT|-iGwB^Y^ ???k?R*tWQW]J%~NӔx4>aϩٴ,e3H???@i۫u?̛y#`swG:"wv89bް'a;Φw.Ӛ. tV{] jTO#q