cplay-ng

simple curses audio player
git clone https://git.ce9e.org/cplay-ng.git

commit
b322e3b98473d4eeeef5ad75f35f52bd575321cf
parent
dc24e1a79d94d6cf1b1c90b2e6bcef9f5c619c7b
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2021-06-17 21:15
use mpv IPC

see https://mpv.io/manual/master/#using-mpv-from-other-programs-or-scripts

Diffstat

M cplay.py 112 +++++++++++++++++++++++++++++++++++++++----------------------

1 files changed, 71 insertions, 41 deletions


diff --git a/cplay.py b/cplay.py

@@ -1,10 +1,12 @@
    1     1 import curses
    2     2 import functools
   -1     3 import json
    3     4 import os
    4     5 import random
    5     6 import re
    6     7 import selectors
    7     8 import signal
   -1     9 import socket
    8    10 import subprocess
    9    11 import sys
   10    12 import termios
@@ -86,6 +88,16 @@ def resize(*args):
   86    88     os.write(app.resize_out, b'.')
   87    89 
   88    90 
   -1    91 def get_socket(path):
   -1    92     while True:
   -1    93         try:
   -1    94             sock = socket.socket(family=socket.AF_UNIX)
   -1    95             sock.connect(path)
   -1    96             return sock
   -1    97         except (FileNotFoundError, ConnectionRefusedError):
   -1    98             time.sleep(0.1)
   -1    99 
   -1   100 
   89   101 @functools.lru_cache()
   90   102 def relpath(path):
   91   103     if path.startswith(filelist.path):
@@ -123,26 +135,56 @@ def listdir(path):
  123   135 
  124   136 
  125   137 class Player:
  126    -1     re_progress = re.compile(br'AV?: (\d+):(\d+):(\d+) / (\d+):(\d+):(\d+)')
  127    -1 
  128   138     def __init__(self):
  129    -1         self._proc = None
  130   139         self.path = None
  131   140         self.position = 0
  132   141         self.length = 0
  133   142         self._seek_step = 0
  134   143         self._seek_timeout = None
   -1   144         self.is_playing = False
   -1   145         self._playing = 0
   -1   146         self._buffer = b''
  135   147 
  136    -1         self.stdin_r, self.stdin_w = os.pipe()
  137    -1         self.stdout_r, self.stdout_w = os.pipe()
  138    -1         self.stderr_r, self.stderr_w = os.pipe()
  139    -1 
  140    -1     def parse_progress(self, fd):
  141    -1         match = self.re_progress.search(os.read(fd, 512))
  142    -1         if match and not self._seek_step:
  143    -1             ph, pm, ps, lh, lm, ls = map(int, match.groups())
  144    -1             self.position = ph * 3600 + pm * 60 + ps
  145    -1             self.length = lh * 3600 + lm * 60 + ls
   -1   148         self.socket_path = '/tmp/mpv-cplay-%i.sock' % os.getpid()
   -1   149         self._proc = subprocess.Popen(
   -1   150             [
   -1   151                 'mpv',
   -1   152                 '--input-ipc-server=%s' % self.socket_path,
   -1   153                 '--idle',
   -1   154                 '--audio-display=no',
   -1   155                 '--replaygain=track',
   -1   156             ],
   -1   157             stdin=subprocess.DEVNULL,
   -1   158             stdout=subprocess.DEVNULL,
   -1   159             stderr=subprocess.DEVNULL,
   -1   160         )
   -1   161         self.socket = get_socket(self.socket_path)
   -1   162 
   -1   163         self._ipc('observe_property', 1, 'time-pos')
   -1   164         self._ipc('observe_property', 2, 'duration')
   -1   165 
   -1   166     def _ipc(self, cmd, *args):
   -1   167         data = json.dumps({'command': [cmd, *args]})
   -1   168         msg = data.encode('utf-8') + b'\n'
   -1   169         self.socket.send(msg)
   -1   170 
   -1   171     def handle_ipc(self, data):
   -1   172         if data.get('event') == 'property-change' and data['id'] == 1:
   -1   173             if data['data'] is not None and not self._seek_step:
   -1   174                 self.position = data['data']
   -1   175         elif data.get('event') == 'property-change' and data['id'] == 2:
   -1   176             if data['data'] is not None:
   -1   177                 self.length = data['data']
   -1   178         elif data.get('event') == 'end-file':
   -1   179             self._playing -= 1
   -1   180 
   -1   181     def parse_progress(self):
   -1   182         self._buffer += self.socket.recv(1024)
   -1   183         msgs = self._buffer.split(b'\n')
   -1   184         self._buffer = msgs.pop()
   -1   185         for msg in msgs:
   -1   186             data = json.loads(msg.decode('utf-8'))
   -1   187             self.handle_ipc(data)
  146   188 
  147   189     def get_progress(self):
  148   190         if self.length == 0:
@@ -150,28 +192,16 @@ class Player:
  150   192         return self.position / self.length
  151   193 
  152   194     def stop(self):
  153    -1         if self._proc:
  154    -1             self._proc.terminate()
  155    -1             self._proc = None
   -1   195         self.is_playing = False
   -1   196         self._ipc('stop')
  156   197 
  157   198     def _play(self):
  158    -1         self.stop()
  159   199         if not self.path:
   -1   200             self.is_playing = False
  160   201             return
  161    -1 
  162    -1         self._proc = subprocess.Popen(
  163    -1             [
  164    -1                 'mpv',
  165    -1                 '--audio-display=no',
  166    -1                 '--start=%i' % self.position,
  167    -1                 '--volume=100',
  168    -1                 '--replaygain=track',
  169    -1                 self.path,
  170    -1             ],
  171    -1             stdout=self.stdout_w,
  172    -1             stderr=self.stderr_w,
  173    -1             stdin=self.stdin_r,
  174    -1         )
   -1   202         self.is_playing = True
   -1   203         self._playing += 1
   -1   204         self._ipc('loadfile', self.path, 'replace', 'start=%i' % self.position)
  175   205 
  176   206     def play(self, path):
  177   207         self.path = path
@@ -181,7 +211,7 @@ class Player:
  181   211         self._play()
  182   212 
  183   213     def toggle(self):
  184    -1         if self._proc:
   -1   214         if self.is_playing:
  185   215             self.stop()
  186   216         elif self.path:
  187   217             self._play()
@@ -205,12 +235,12 @@ class Player:
  205   235                 self._play()
  206   236 
  207   237     @property
  208    -1     def is_playing(self):
  209    -1         return self._proc is not None
  210    -1 
  211    -1     @property
  212   238     def is_finished(self):
  213    -1         return self._proc is not None and self._proc.poll() is not None
   -1   239         return self.is_playing and self._playing == 0
   -1   240 
   -1   241     def cleanup(self):
   -1   242         self._proc.terminate()
   -1   243         os.remove(self.socket_path)
  214   244 
  215   245 
  216   246 class Input:
@@ -730,7 +760,7 @@ class Application:
  730   760         with selectors.DefaultSelector() as sel:
  731   761             sel.register(sys.stdin, selectors.EVENT_READ)
  732   762             sel.register(self.resize_in, selectors.EVENT_READ)
  733    -1             sel.register(player.stderr_r, selectors.EVENT_READ)
   -1   763             sel.register(player.socket, selectors.EVENT_READ)
  734   764 
  735   765             while True:
  736   766                 player.finish_seek()
@@ -743,8 +773,8 @@ class Application:
  743   773                         self.render(force=True)
  744   774                     elif key.fileobj is sys.stdin:
  745   775                         self.process_key(screen.get_wch())
  746    -1                     elif key.fileobj is player.stderr_r:
  747    -1                         player.parse_progress(player.stderr_r)
   -1   776                     elif key.fileobj is player.socket:
   -1   777                         player.parse_progress()
  748   778 
  749   779                 if player.is_finished:
  750   780                     player.play(playlist.next())
@@ -774,7 +804,7 @@ def main():
  774   804         with enable_ctrl_keys():
  775   805             app.run()
  776   806     finally:
  777    -1         player.stop()
   -1   807         player.cleanup()
  778   808         curses.endwin()
  779   809 
  780   810