PageRenderTime 51ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/mps_youtube/player.py

https://gitlab.com/alidzapp/mps-youtube
Python | 299 lines | 268 code | 24 blank | 7 comment | 19 complexity | 087f5404f9c49a72a90a76847619a8ef MD5 | raw file
  1. import os
  2. import tempfile
  3. import subprocess
  4. import json
  5. import re
  6. import socket
  7. import math
  8. import time
  9. from . import g, screen
  10. from .util import dbg, xenc, F
  11. from .config import Config
  12. from .paths import get_config_dir
  13. mswin = os.name == "nt"
  14. def _get_input_file():
  15. """ Check for existence of custom input file.
  16. Return file name of temp input file with mpsyt mappings included
  17. """
  18. confpath = conf = ''
  19. if "mpv" in Config.PLAYER.get:
  20. confpath = os.path.join(get_config_dir(), "mpv-input.conf")
  21. elif "mplayer" in Config.PLAYER.get:
  22. confpath = os.path.join(get_config_dir(), "mplayer-input.conf")
  23. if os.path.isfile(confpath):
  24. dbg("using %s for input key file", confpath)
  25. with open(confpath) as conffile:
  26. conf = conffile.read() + '\n'
  27. conf = conf.replace("quit", "quit 43")
  28. conf = conf.replace("playlist_prev", "quit 42")
  29. conf = conf.replace("pt_step -1", "quit 42")
  30. conf = conf.replace("playlist_next", "quit")
  31. conf = conf.replace("pt_step 1", "quit")
  32. standard_cmds = ['q quit 43\n', '> quit\n', '< quit 42\n', 'NEXT quit\n',
  33. 'PREV quit 42\n', 'ENTER quit\n']
  34. bound_keys = [i.split()[0] for i in conf.splitlines() if i.split()]
  35. for i in standard_cmds:
  36. key = i.split()[0]
  37. if key not in bound_keys:
  38. conf += i
  39. with tempfile.NamedTemporaryFile('w', prefix='mpsyt-input',
  40. delete=False) as tmpfile:
  41. tmpfile.write(conf)
  42. return tmpfile.name
  43. def launch_player(song, songdata, cmd):
  44. """ Launch player application. """
  45. # Fix UnicodeEncodeError when title has characters
  46. # not supported by encoding
  47. cmd = [xenc(i) for i in cmd]
  48. arturl = "http://i.ytimg.com/vi/%s/default.jpg" % song.ytid
  49. input_file = _get_input_file()
  50. sockpath = None
  51. fifopath = None
  52. try:
  53. if "mplayer" in Config.PLAYER.get:
  54. cmd.append('-input')
  55. if mswin:
  56. # Mplayer does not recognize path starting with drive letter,
  57. # or with backslashes as a delimiter.
  58. input_file = input_file[2:].replace('\\', '/')
  59. cmd.append('conf=' + input_file)
  60. if g.mprisctl:
  61. fifopath = tempfile.mktemp('.fifo', 'mpsyt-mplayer')
  62. os.mkfifo(fifopath)
  63. cmd.extend(['-input', 'file=' + fifopath])
  64. g.mprisctl.send(('mplayer-fifo', fifopath))
  65. g.mprisctl.send(('metadata', (song.ytid, song.title,
  66. song.length, arturl)))
  67. p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE,
  68. stderr=subprocess.STDOUT, bufsize=1)
  69. _player_status(p, songdata + "; ", song.length)
  70. returncode = p.wait()
  71. elif "mpv" in Config.PLAYER.get:
  72. cmd.append('--input-conf=' + input_file)
  73. if g.mpv_usesock:
  74. sockpath = tempfile.mktemp('.sock', 'mpsyt-mpv')
  75. cmd.append('--input-unix-socket=' + sockpath)
  76. with open(os.devnull, "w") as devnull:
  77. p = subprocess.Popen(cmd, shell=False, stderr=devnull)
  78. if g.mprisctl:
  79. g.mprisctl.send(('socket', sockpath))
  80. g.mprisctl.send(('metadata', (song.ytid, song.title,
  81. song.length, arturl)))
  82. else:
  83. if g.mprisctl:
  84. fifopath = tempfile.mktemp('.fifo', 'mpsyt-mpv')
  85. os.mkfifo(fifopath)
  86. cmd.append('--input-file=' + fifopath)
  87. g.mprisctl.send(('mpv-fifo', fifopath))
  88. g.mprisctl.send(('metadata', (song.ytid, song.title,
  89. song.length, arturl)))
  90. p = subprocess.Popen(cmd, shell=False, stderr=subprocess.PIPE,
  91. bufsize=1)
  92. _player_status(p, songdata + "; ", song.length, mpv=True,
  93. sockpath=sockpath)
  94. returncode = p.wait()
  95. else:
  96. with open(os.devnull, "w") as devnull:
  97. returncode = subprocess.call(cmd, stderr=devnull)
  98. p = None
  99. return returncode
  100. except OSError:
  101. g.message = F('no player') % Config.PLAYER.get
  102. return None
  103. finally:
  104. os.unlink(input_file)
  105. # May not exist if mpv has not yet created the file
  106. if sockpath and os.path.exists(sockpath):
  107. os.unlink(sockpath)
  108. if fifopath:
  109. os.unlink(fifopath)
  110. if g.mprisctl:
  111. g.mprisctl.send(('stop', True))
  112. if p and p.poll() is None:
  113. p.terminate() # make sure to kill mplayer if mpsyt crashes
  114. def _player_status(po_obj, prefix, songlength=0, mpv=False, sockpath=None):
  115. """ Capture time progress from player output. Write status line. """
  116. # pylint: disable=R0914, R0912
  117. re_mplayer = re.compile(r"A:\s*(?P<elapsed_s>\d+)\.\d\s*")
  118. re_mpv = re.compile(r".{,15}AV?:\s*(\d\d):(\d\d):(\d\d)")
  119. re_volume = re.compile(r"Volume:\s*(?P<volume>\d+)\s*%")
  120. re_player = re_mpv if mpv else re_mplayer
  121. last_displayed_line = None
  122. buff = ''
  123. volume_level = None
  124. last_pos = None
  125. if sockpath:
  126. s = socket.socket(socket.AF_UNIX)
  127. tries = 0
  128. while tries < 10 and po_obj.poll() is None:
  129. time.sleep(.5)
  130. try:
  131. s.connect(sockpath)
  132. break
  133. except socket.error:
  134. pass
  135. tries += 1
  136. else:
  137. return
  138. try:
  139. observe_full = False
  140. cmd = {"command": ["observe_property", 1, "time-pos"]}
  141. s.send(json.dumps(cmd).encode() + b'\n')
  142. volume_level = elapsed_s = None
  143. for line in s.makefile():
  144. resp = json.loads(line)
  145. # deals with bug in mpv 0.7 - 0.7.3
  146. if resp.get('event') == 'property-change' and not observe_full:
  147. cmd = {"command": ["observe_property", 2, "volume"]}
  148. s.send(json.dumps(cmd).encode() + b'\n')
  149. observe_full = True
  150. if resp.get('event') == 'property-change' and resp['id'] == 1:
  151. elapsed_s = int(resp['data'])
  152. elif resp.get('event') == 'property-change' and resp['id'] == 2:
  153. volume_level = int(resp['data'])
  154. if elapsed_s:
  155. line = _make_status_line(elapsed_s, prefix, songlength,
  156. volume=volume_level)
  157. if line != last_displayed_line:
  158. screen.writestatus(line)
  159. last_displayed_line = line
  160. except socket.error:
  161. pass
  162. else:
  163. elapsed_s = 0
  164. while po_obj.poll() is None:
  165. stdstream = po_obj.stderr if mpv else po_obj.stdout
  166. char = stdstream.read(1).decode("utf-8", errors="ignore")
  167. if char in '\r\n':
  168. mv = re_volume.search(buff)
  169. if mv:
  170. volume_level = int(mv.group("volume"))
  171. match_object = re_player.match(buff)
  172. if match_object:
  173. try:
  174. h, m, s = map(int, match_object.groups())
  175. elapsed_s = h * 3600 + m * 60 + s
  176. except ValueError:
  177. try:
  178. elapsed_s = int(match_object.group('elapsed_s') or
  179. '0')
  180. except ValueError:
  181. continue
  182. line = _make_status_line(elapsed_s, prefix, songlength,
  183. volume=volume_level)
  184. if line != last_displayed_line:
  185. screen.writestatus(line)
  186. last_displayed_line = line
  187. if buff.startswith('ANS_volume='):
  188. volume_level = round(float(buff.split('=')[1]))
  189. paused = ("PAUSE" in buff) or ("Paused" in buff)
  190. if (elapsed_s != last_pos or paused) and g.mprisctl:
  191. last_pos = elapsed_s
  192. g.mprisctl.send(('pause', paused))
  193. g.mprisctl.send(('volume', volume_level))
  194. g.mprisctl.send(('time-pos', elapsed_s))
  195. buff = ''
  196. else:
  197. buff += char
  198. def _make_status_line(elapsed_s, prefix, songlength=0, volume=None):
  199. """ Format progress line output. """
  200. # pylint: disable=R0914
  201. display_s = elapsed_s
  202. display_h = display_m = 0
  203. if elapsed_s >= 60:
  204. display_m = display_s // 60
  205. display_s %= 60
  206. if display_m >= 100:
  207. display_h = display_m // 60
  208. display_m %= 60
  209. pct = (float(elapsed_s) / songlength * 100) if songlength else 0
  210. status_line = "%02i:%02i:%02i %s" % (
  211. display_h, display_m, display_s,
  212. ("[%.0f%%]" % pct).ljust(6)
  213. )
  214. if volume:
  215. vol_suffix = " vol: %d%%" % volume
  216. else:
  217. vol_suffix = ""
  218. cw = screen.getxy().width
  219. prog_bar_size = cw - len(prefix) - len(status_line) - len(vol_suffix) - 7
  220. progress = int(math.ceil(pct / 100 * prog_bar_size))
  221. status_line += " [%s]" % ("=" * (progress - 1) +
  222. ">").ljust(prog_bar_size, ' ')
  223. return prefix + status_line + vol_suffix