Add repeat and shuffle mode
This commit is contained in:
parent
fefa352ae1
commit
8a79cafddd
184
comp.py
184
comp.py
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
# comp - Curses Online Media Player
|
||||
# Copyright (C) 2017 Raphael McSinyx
|
||||
#
|
||||
|
@ -20,11 +20,17 @@ import curses
|
|||
import json
|
||||
from argparse import ArgumentParser
|
||||
from configparser import ConfigParser
|
||||
from itertools import cycle
|
||||
from os.path import expanduser
|
||||
from random import choice
|
||||
from time import gmtime, strftime
|
||||
from threading import Thread
|
||||
|
||||
from mpv import MPV
|
||||
|
||||
MODES = ('play-current', 'play-all', 'play-selected', 'repeat-current',
|
||||
'repeat-all', 'repeat-selected', 'shuffle-all', 'shuffle-selected')
|
||||
|
||||
|
||||
def setno(data, keys):
|
||||
"""Set all keys of each track in data to False."""
|
||||
|
@ -33,30 +39,27 @@ def setno(data, keys):
|
|||
track[key] = False
|
||||
|
||||
|
||||
def find(data, key):
|
||||
"""Return the list tracks with key set to True."""
|
||||
return [track for track in data if track[key]]
|
||||
def playlist(mode):
|
||||
"""Return a generator of tracks to be played."""
|
||||
action, choose_from = mode.split('-')
|
||||
if choose_from == 'all': tracks = data
|
||||
else: tracks = [track for track in data if track[choose_from]]
|
||||
# Somehow yield have to be used instead of returning a generator
|
||||
if action == 'play':
|
||||
for track in tracks: yield track
|
||||
elif action == 'repeat':
|
||||
for track in cycle(tracks): yield track
|
||||
elif tracks:
|
||||
while True: yield choice(tracks)
|
||||
|
||||
|
||||
def initmpv(vf, af):
|
||||
"""Return a mpv object with youtube-dl video format set to vf+af."""
|
||||
ytdlf = '{}+{}'.format(vf, af) if vf and af else vf + af
|
||||
mp = MPV(input_default_bindings=True, input_vo_keyboard=True,
|
||||
ytdl=True, ytdl_format=ytdlf)
|
||||
if video_output: mp['vo'] = video_output
|
||||
mp.observe_property('pause', lambda foo: updatestatusline(stdscr, mp))
|
||||
mp.observe_property('time-pos', lambda foo: updatestatusline(stdscr, mp))
|
||||
mp.observe_property('aid', lambda foo: updatestatusline(stdscr, mp))
|
||||
mp.observe_property('vid', lambda foo: updatestatusline(stdscr, mp))
|
||||
return mp
|
||||
|
||||
|
||||
def play(track):
|
||||
setno(data, ['playing'])
|
||||
mp._set_property('pause', False, bool)
|
||||
mp.play('https://youtu.be/' + track['url'])
|
||||
track['playing'] = True
|
||||
reprint(stdscr, data[start : start+curses.LINES-3])
|
||||
def play():
|
||||
for track in playlist(mode):
|
||||
setno(data, ['playing'])
|
||||
data[data.index(track)]['playing'] = True
|
||||
reprint(stdscr, data[start : start+curses.LINES-3])
|
||||
mp.play('https://youtu.be/' + track['url'])
|
||||
mp.wait_for_playback()
|
||||
|
||||
|
||||
def secpair2hhmmss(pos, duration):
|
||||
|
@ -71,23 +74,41 @@ def secpair2hhmmss(pos, duration):
|
|||
strftime(timestr, durationtime))
|
||||
|
||||
|
||||
def updatestatusline(stdscr, mp):
|
||||
playmode = ' {}{} {}'.format('A' if mp._get_property('aid') != 'no' else ' ',
|
||||
'V' if mp._get_property('vid') != 'no' else ' ',
|
||||
mode)
|
||||
time = secpair2hhmmss(mp._get_property('time-pos', int),
|
||||
mp._get_property('duration', int))
|
||||
if time:
|
||||
stdscr.addstr(curses.LINES - 2, 1, '{} {} {}'.format(
|
||||
time,
|
||||
'|' if mp._get_property('pause', bool) else '>',
|
||||
mp._get_property('media-title')
|
||||
)[:curses.COLS-len(playmode)])
|
||||
stdscr.addstr(curses.LINES - 2, curses.COLS - 1 - len(playmode), playmode)
|
||||
stdscr.chgat(curses.LINES - 2, 0, curses.color_pair(8))
|
||||
def update_status_line(stdscr, mp):
|
||||
left = ' ' + secpair2hhmmss(mp._get_property('time-pos', int),
|
||||
mp._get_property('duration', int))
|
||||
right = ' {} {}{} '.format(mode,
|
||||
' ' if mp._get_property('mute', bool) else 'A',
|
||||
' ' if mp._get_property('vid') == 'no' else 'V')
|
||||
if left != ' ':
|
||||
left += ' | ' if mp._get_property('pause', bool) else ' > '
|
||||
stdscr.addstr(curses.LINES - 2, 0, left, curses.color_pair(8))
|
||||
title_len = curses.COLS - len(left + right)
|
||||
center = mp._get_property('media-title').ljust(title_len)[:title_len]
|
||||
stdscr.addstr(curses.LINES - 2, len(left), center,
|
||||
curses.color_pair(8) | curses.A_BOLD)
|
||||
stdscr.addstr(curses.LINES - 2, len(left + center), right,
|
||||
curses.color_pair(8))
|
||||
else:
|
||||
stdscr.addstr(curses.LINES - 2, 0, right.rjust(curses.COLS),
|
||||
curses.color_pair(8))
|
||||
stdscr.refresh()
|
||||
|
||||
|
||||
def reattr(stdscr, y, track):
|
||||
invert = 8 if track['current'] else 0
|
||||
if track['error']:
|
||||
stdscr.chgat(y, 0, curses.color_pair(1 + invert) | curses.A_BOLD)
|
||||
elif track['playing']:
|
||||
stdscr.chgat(y, 0, curses.color_pair(3 + invert) | curses.A_BOLD)
|
||||
elif track['selected']:
|
||||
stdscr.chgat(y, 0, curses.color_pair(5 + invert) | curses.A_BOLD)
|
||||
elif invert:
|
||||
stdscr.chgat(y, 0, curses.color_pair(12) | curses.A_BOLD)
|
||||
else:
|
||||
stdscr.chgat(y, 0, curses.color_pair(0) | curses.A_NORMAL)
|
||||
|
||||
|
||||
def reprint(stdscr, data2print):
|
||||
stdscr.clear()
|
||||
stdscr.addstr(0, curses.COLS-12, 'URL')
|
||||
|
@ -97,48 +118,49 @@ def reprint(stdscr, data2print):
|
|||
y = i + 1
|
||||
stdscr.addstr(y, 0, track['url'].rjust(curses.COLS - 1))
|
||||
stdscr.addstr(y, 1, track['title'][:curses.COLS-14])
|
||||
invert = 8 if track['highlight'] else 0
|
||||
if track['error']:
|
||||
stdscr.chgat(y, 0, curses.color_pair(1 + invert) | curses.A_BOLD)
|
||||
elif track['playing']:
|
||||
stdscr.chgat(y, 0, curses.color_pair(3 + invert) | curses.A_BOLD)
|
||||
elif track['selected']:
|
||||
stdscr.chgat(y, 0, curses.color_pair(5 + invert) | curses.A_BOLD)
|
||||
elif invert:
|
||||
stdscr.chgat(y, 0, curses.color_pair(12) | curses.A_BOLD)
|
||||
else:
|
||||
stdscr.chgat(y, 0, curses.color_pair(0) | curses.A_NORMAL)
|
||||
updatestatusline(stdscr, mp)
|
||||
reattr(stdscr, y, track)
|
||||
update_status_line(stdscr, mp)
|
||||
|
||||
|
||||
def move(stdscr, data, y, delta):
|
||||
global start
|
||||
if start + y + delta < 1:
|
||||
if start + y == 1:
|
||||
return 1
|
||||
start = 0
|
||||
setno(data, ['highlight'])
|
||||
data[0]['highlight'] = True
|
||||
setno(data, ['current'])
|
||||
data[0]['current'] = True
|
||||
reprint(stdscr, data[:curses.LINES-3])
|
||||
return 1
|
||||
elif start + y + delta > len(data):
|
||||
if start + y == len(data):
|
||||
return curses.LINES - 3
|
||||
start = len(data) - curses.LINES + 3
|
||||
y = curses.LINES - 3
|
||||
setno(data, ['highlight'])
|
||||
data[-1]['highlight'] = True
|
||||
setno(data, ['current'])
|
||||
data[-1]['current'] = True
|
||||
reprint(stdscr, data[-curses.LINES+3:])
|
||||
return y
|
||||
|
||||
if 0 < y + delta < curses.LINES - 2:
|
||||
y = y + delta
|
||||
elif y + delta < 1:
|
||||
if y + delta < 1:
|
||||
start += y + delta - 1
|
||||
y = 1
|
||||
else:
|
||||
setno(data, ['current'])
|
||||
data[start]['current'] = True
|
||||
reprint(stdscr, data[start : start+curses.LINES-3])
|
||||
elif y + delta > curses.LINES - 3:
|
||||
start += y + delta - curses.LINES + 3
|
||||
y = curses.LINES - 3
|
||||
setno(data, ['highlight'])
|
||||
data[start + y - 1]['highlight'] = True
|
||||
reprint(stdscr, data[start : start+curses.LINES-3])
|
||||
stdscr.refresh()
|
||||
setno(data, ['current'])
|
||||
data[start + curses.LINES - 4]['current'] = True
|
||||
reprint(stdscr, data[start : start+curses.LINES-3])
|
||||
else:
|
||||
data[start + y - 1]['current'] = False
|
||||
reattr(stdscr, y, data[start + y - 1])
|
||||
y = y + delta
|
||||
data[start + y - 1]['current'] = True
|
||||
reattr(stdscr, y, data[start + y - 1])
|
||||
stdscr.refresh()
|
||||
return y
|
||||
|
||||
|
||||
|
@ -150,14 +172,13 @@ args = parser.parse_args()
|
|||
config = ConfigParser()
|
||||
config.read(expanduser('~/.config/comp/settings.ini'))
|
||||
mode = config.get('comp', 'play-mode', fallback='play-all')
|
||||
video = config.get('mpv', 'video', fallback='auto')
|
||||
video_output = config.get('mpv', 'video-output', fallback='')
|
||||
audio_format = config.get('youtube-dl', 'audio-format', fallback='bestaudio')
|
||||
video_format = config.get('youtube-dl', 'video-format', fallback='bestvideo')
|
||||
audio, video = audio_format, video_format
|
||||
ytdlf = config.get('youtube-dl', 'format', fallback='best')
|
||||
|
||||
with open(args.json_playlist) as f:
|
||||
data = json.load(f)
|
||||
setno(data, ['error', 'playing', 'selected', 'highlight'])
|
||||
setno(data, ['error', 'playing', 'selected', 'current'])
|
||||
|
||||
stdscr = curses.initscr()
|
||||
curses.noecho()
|
||||
|
@ -181,12 +202,19 @@ curses.init_pair(12, -1, 4)
|
|||
curses.init_pair(13, -1, 5)
|
||||
curses.init_pair(14, -1, 6)
|
||||
|
||||
mp = initmpv(video, audio)
|
||||
mp = MPV(input_default_bindings=True, input_vo_keyboard=True,
|
||||
ytdl=True, ytdl_format=ytdlf)
|
||||
if video_output: mp['vo'] = video_output
|
||||
mp._set_property('vid', video)
|
||||
mp.observe_property('mute', lambda foo: update_status_line(stdscr, mp))
|
||||
mp.observe_property('pause', lambda foo: update_status_line(stdscr, mp))
|
||||
mp.observe_property('time-pos', lambda foo: update_status_line(stdscr, mp))
|
||||
mp.observe_property('vid', lambda foo: update_status_line(stdscr, mp))
|
||||
|
||||
# Print initial content
|
||||
start = 0
|
||||
y = 1
|
||||
data[0]['highlight'] = True
|
||||
data[0]['current'] = True
|
||||
reprint(stdscr, data[:curses.LINES-3])
|
||||
|
||||
# mpv keys: []{}<>.,qQ/*90m-#fTweoPOvjJxzlLVrtsSIdA
|
||||
|
@ -195,29 +223,39 @@ c = stdscr.getch()
|
|||
while c != 113: # letter q
|
||||
if c == curses.KEY_RESIZE:
|
||||
curses.update_lines_cols()
|
||||
move(stdscr, data, y, 1 - y)
|
||||
y = move(stdscr, data, 1, y - 1)
|
||||
start += y - 1
|
||||
y = 1
|
||||
reprint(stdscr, data[start : start+curses.LINES-3])
|
||||
elif c in (106, curses.KEY_DOWN): # letter j or down arrow
|
||||
y = move(stdscr, data, y, 1)
|
||||
elif c in (107, curses.KEY_UP): # letter k or up arrow
|
||||
y = move(stdscr, data, y, -1)
|
||||
elif c == curses.KEY_PPAGE: # page up
|
||||
y = move(stdscr, data, y, -curses.LINES)
|
||||
y = move(stdscr, data, y, 4 - curses.LINES)
|
||||
elif c == curses.KEY_NPAGE: # page down
|
||||
y = move(stdscr, data, y, curses.LINES)
|
||||
y = move(stdscr, data, y, curses.LINES - 4)
|
||||
elif c == curses.KEY_HOME: # home
|
||||
y = move(stdscr, data, y, -len(data))
|
||||
elif c == curses.KEY_END: # end
|
||||
y = move(stdscr, data, y, len(data))
|
||||
elif c == 109: # letter m
|
||||
mode = MODES[(MODES.index(mode) + 1) % 8]
|
||||
update_status_line(stdscr, mp)
|
||||
elif c == 77: # letter M
|
||||
mode = MODES[(MODES.index(mode) - 1) % 8]
|
||||
update_status_line(stdscr, mp)
|
||||
elif c == 112: # letter p
|
||||
play(data[start + y - 1])
|
||||
mp._set_property('pause', False, bool)
|
||||
play_thread = Thread(target=play)
|
||||
play_thread.daemon = True
|
||||
play_thread.start()
|
||||
elif c == 32: # space
|
||||
mp._set_property('pause', not mp._get_property('pause', bool), bool)
|
||||
elif c == 99: # letter c
|
||||
data[start + y - 1]['selected'] = not data[start + y - 1]['selected']
|
||||
y = move(stdscr, data, y, 1)
|
||||
elif c == 97: # letter a
|
||||
mp._set_property('aid', 'auto' if mp._get_property('aid') == 'no' else 'no')
|
||||
mp._set_property('mute', not mp._get_property('mute', bool), bool)
|
||||
elif c == 118: # letter v
|
||||
mp._set_property('vid', 'auto' if mp._get_property('vid') == 'no' else 'no')
|
||||
c = stdscr.getch()
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
[comp]
|
||||
play-mode = shuffle-selected
|
||||
|
||||
[mpv]
|
||||
video = no
|
||||
video-output = xv
|
||||
|
||||
[youtube-dl]
|
||||
format = best
|
4
setup.py
4
setup.py
|
@ -5,14 +5,14 @@ from distutils.core import setup
|
|||
with open('README.rst') as f:
|
||||
long_description = f.read()
|
||||
|
||||
setup(name = 'comp', version = '0.1.0a2',
|
||||
setup(name = 'comp', version = '0.1.0a3',
|
||||
url = 'https://github.com/McSinyx/comp',
|
||||
description = ('Curses Online Media Player'),
|
||||
long_description=long_description,
|
||||
author = 'McSinyx', author_email = 'vn.mcsinyx@gmail.com',
|
||||
py_modules = ['mpv'], scripts=['comp.py'],
|
||||
classifiers = [
|
||||
'Development Status :: 2 - Pre-alpha',
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Console :: Curses',
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
'License :: OSI Approved :: GNU Affero General Public License v3',
|
||||
|
|
Loading…
Reference in New Issue