Add repeat and shuffle mode

This commit is contained in:
Nguyễn Gia Phong 2017-04-03 20:52:59 +07:00
parent fefa352ae1
commit 8a79cafddd
3 changed files with 122 additions and 75 deletions

184
comp.py
View File

@ -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()

9
settings.ini Normal file
View File

@ -0,0 +1,9 @@
[comp]
play-mode = shuffle-selected
[mpv]
video = no
video-output = xv
[youtube-dl]
format = best

View File

@ -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',