Refactor partially (now more things are broken)

This commit is contained in:
Nguyễn Gia Phong 2017-04-27 21:36:10 +07:00 committed by Nguyễn Gia Phong
parent cf9cfd3c01
commit df98e90366
1 changed files with 204 additions and 194 deletions

398
comp Normal file → Executable file
View File

@ -22,7 +22,7 @@ from argparse import ArgumentParser
from configparser import ConfigParser from configparser import ConfigParser
from curses.ascii import ctrl from curses.ascii import ctrl
from datetime import datetime from datetime import datetime
from gettext import bindtextdomain, gettext, textdomain from gettext import gettext as _, textdomain
from itertools import cycle from itertools import cycle
from os import linesep, makedirs from os import linesep, makedirs
from os.path import abspath, dirname, expanduser, isfile from os.path import abspath, dirname, expanduser, isfile
@ -36,8 +36,8 @@ from mpv import MPV
# Init gettext # Init gettext
textdomain('comp') textdomain('comp')
_ = gettext
# Global constants
SYSTEM_CONFIG = '/etc/comp/settings.ini' SYSTEM_CONFIG = '/etc/comp/settings.ini'
USER_CONFIG = expanduser('~/.config/comp/settings.ini') USER_CONFIG = expanduser('~/.config/comp/settings.ini')
MPV_LOG = expanduser('~/.cache/comp/mpv.log') MPV_LOG = expanduser('~/.cache/comp/mpv.log')
@ -53,52 +53,49 @@ def mpv_logger(loglevel, component, message):
f.write(mpv_log) f.write(mpv_log)
def setno(entries, keys):
"""Set all keys of each entry in entries to False."""
for key in keys:
for entry in entries:
entry[key] = False
def getlink(entry):
"""Return an URL from the given entry."""
with YoutubeDL({'quiet': True}) as ytdl:
return ytdl.extract_info(entry['url'], download=False,
ie_key=entry.get('ie_key')).get('webpage_url')
def _secpair2hhmmss(pos, duration): def _secpair2hhmmss(pos, duration):
"""Quick hack to convert a pair of seconds to HHMMSS / HHMMSS """Quick hack to convert a pair of seconds to HHMMSS / HHMMSS
string as MPV.get_property_osd_string isn't available. string as MPV.get_property_osd_string isn't available.
""" """
if pos is None: return ''
postime, durationtime = gmtime(pos), gmtime(duration) postime, durationtime = gmtime(pos), gmtime(duration)
# Let's hope media durations are shorter than a day # Let's hope media durations are shorter than a day
timestr = '%M:%S' if duration < 3600 else '%H:%M:%S' timestr = '%M:%S' if duration < 3600 else '%H:%M:%S'
return ' {} / {}'.format(strftime(timestr, postime), return '{} / {}'.format(strftime(timestr, postime),
strftime(timestr, durationtime)) strftime(timestr, durationtime))
class Comp(object): class Comp:
"""Meta object for drawing and playing.""" """Meta object for drawing and playing.
def __new__(cls, entries, mode, mpv_vo, mpv_vid, ytdlf):
self = object.__new__(cls) Attributes:
self.start = 0 entries
self.y = 1 mode (str)
self.mode = mode mp (MPV)
self.entries = entries start (int)
self.playlist = (i for i in []) # an empty generator? stdscr
self.stdscr = curses.initscr() y (int)
"""
def idx(self): return self.start + self.y - 1
def current(self): return self.entries[self.idx()]
def __init__(self, entries, mode, mpv_vo, mpv_vid, ytdlf):
self.mode, self.reading, self.start, self.y = mode, False, 0, 1
self.entries, self.playlist = entries, iter(())
self.setno('current', 'error', 'playing', 'selected')
if self.entries: self.current()['current'] = True
self.mp = MPV(input_default_bindings=True, input_vo_keyboard=True, self.mp = MPV(input_default_bindings=True, input_vo_keyboard=True,
log_handler=mpv_logger, ytdl=True, ytdl_format=ytdlf) log_handler=mpv_logger, ytdl=True, ytdl_format=ytdlf)
if mpv_vo: self.mp['vo'] = mpv_vo if mpv_vo: self.mp['vo'] = mpv_vo
self.mp._set_property('vid', mpv_vid) self.mp._set_property('vid', mpv_vid)
return self self.mp.observe_property('mute', self.generic_event_handler)
self.mp.observe_property('pause', self.generic_event_handler)
def __init__(self): self.mp.observe_property('time-pos', self.generic_event_handler)
setno(entries, ['current', 'error', 'playing', 'selected']) self.mp.observe_property('vid', self.generic_event_handler)
entries[0]['current'] = True
self.stdscr = curses.initscr()
curses.noecho() curses.noecho()
curses.cbreak() curses.cbreak()
self.stdscr.keypad(True) self.stdscr.keypad(True)
@ -120,125 +117,149 @@ class Comp(object):
curses.init_pair(13, -1, 5) curses.init_pair(13, -1, 5)
curses.init_pair(14, -1, 6) curses.init_pair(14, -1, 6)
self.mp._set_property('vid', mpv_vid) self.reprint()
def updatestt(void): self.update_status()
self.mp.observe_property('mute', updatestt) def setno(self, *keys):
self.mp.observe_property('pause', updatestt) """Set all keys of each entry in entries to False."""
self.mp.observe_property('time-pos', updatestt) for entry in self.entries:
self.mp.observe_property('vid', updatestt) for key in keys:
entry[key] = False
def update_status(self, message='', msgattr=curses.A_NORMAL): def update_status(self, message='', msgattr=curses.A_NORMAL):
if self.reading: return False
right = ' {} {}{} '.format( right = ' {} {}{} '.format(
_(mode), ' ' if self.mp._get_property('mute', bool) else 'A', _(self.mode), ' ' if self.mp._get_property('mute', bool) else 'A',
' ' if mp._get_property('vid') == 'no' else 'V') ' ' if self.mp._get_property('vid') == 'no' else 'V')
self.stdscr.addstr(curses.LINES - 2, 0, right.rjust(curses.COLS),
curses.color_pair(14))
try: try:
left = _secpair2hhmmss(self.mp._get_property('time-pos', int), left = ' {} {} '.format(
self.mp._get_property('duration', int)) _secpair2hhmmss(self.mp._get_property('time-pos', int),
except: self.mp._get_property('duration', int)),
left += ' | ' if self.mp._get_property('pause', bool) else ' > ' '|' if self.mp._get_property('pause', bool) else '>')
self.stdscr.addstr(curses.LINES - 2, 0, right.rjust(curses.COLS), self.stdscr.addstr(curses.LINES - 2, 0, left, curses.color_pair(14))
curses.color_pair(14))
else:
stdscr.addstr(curses.LINES - 2, 0, left, curses.color_pair(14))
title_len = curses.COLS - len(left + right) title_len = curses.COLS - len(left + right)
center = self.entries.setdefault('title', self.mp._get_property('media-title')).ljust(title_len)[:title_len] center = self.mp._get_property('media-title').ljust(title_len)[:title_len]
self.stdscr.addstr(curses.LINES - 2, len(left), center, self.stdscr.addstr(curses.LINES - 2, len(left), center,
curses.color_pair(14) | curses.A_BOLD) curses.color_pair(14) | curses.A_BOLD)
self.stdscr.addstr(curses.LINES - 2, len(left + center), right, except:
curses.color_pair(14)) return True
finally: self.stdscr.move(curses.LINES - 1, 0)
self.stdscr.move(curses.LINES - 1, 0) self.stdscr.clrtoeol()
self.stdscr.clrtoeol() self.stdscr.addstr(curses.LINES - 1, 0,
self.stdscr.addstr(curses.LINES - 1, 0, message, msgattr) message[:curses.COLS], msgattr)
self.stdscr.refresh() self.stdscr.refresh()
return False
def choose_from(mode1):
if mode1 == 'all': return entries
else: return [entry for entry in entries if entry.setdefault(mode1, False)]
def update_playlist(self): def update_playlist(self):
action = self.mode.split('-')[0] action, pick = self.mode.split('-')
entries2play = choose_from(mode.split('-')[1]) if pick == 'current':
# Somehow yield have to be used instead of returning a generator entries = [self.current()]
elif pick == 'all':
entries = self.entries
else:
entries = [entry for entry in self.entries if entry['selected']]
if action == 'play': if action == 'play':
for entry in entries2play: yield entry self.playlist = iter(entries)
elif action == 'repeat': elif action == 'repeat':
for entry in cycle(entries2play): yield entry self.playlist = cycle(entries)
elif entries2play: else:
while True: yield choice(entries2play) self.playlist = iter(lambda: choice(entries), None)
def getlink(self, entry):
"""Return an URL from the given entry."""
with YoutubeDL({'quiet': True}) as ytdl:
return entry.setdefault('webpage_url', ytdl.extract_info(
entry['url'], download=False, ie_key=entry.get('ie_key')
).get('webpage_url'))
def play(self): def play(self):
for entry in playlist(self.mode): def mpv_play(entry):
setno(entries, ['playing']) self.mp.play(self.getlink(entry))
reprint(stdscr, entries[start : start+curses.LINES-3]) self.mp.wait_for_playback()
entries[entries.index(entry)]['playing'] = True self.setno('playing')
reprint(stdscr, entries[start : start+curses.LINES-3]) self.reprint()
mp.play(entry.setdefault('webpage_url', getlink(entry)))
mp.wait_for_playback()
def reattr(self, entry): try:
entry = next(self.playlist)
except StopIteration:
pass
else:
play_thread = Thread(target=mpv_play, args=(entry,), daemon=True)
play_thread.start()
self.mp._set_property('pause', False, bool)
entry['playing'] = True
self.reprint()
def reattr(self, y=None):
if not y: y = self.y
entry = self.entries[self.start + y - 1]
invert = 8 if entry.setdefault('current', False) else 0 invert = 8 if entry.setdefault('current', False) else 0
if entry.setdefault('error', False): if entry.setdefault('error', False):
self.stdscr.chgat(self.y, 0, curses.color_pair(1 + invert) | curses.A_BOLD) self.stdscr.chgat(y, 0, curses.color_pair(1 + invert) | curses.A_BOLD)
elif entry.setdefault('playing', False): elif entry.setdefault('playing', False):
self.stdscr.chgat(self.y, 0, curses.color_pair(3 + invert) | curses.A_BOLD) self.stdscr.chgat(y, 0, curses.color_pair(3 + invert) | curses.A_BOLD)
elif entry.setdefault('selected', False): elif entry.setdefault('selected', False):
self.stdscr.chgat(self.y, 0, curses.color_pair(5 + invert) | curses.A_BOLD) self.stdscr.chgat(y, 0, curses.color_pair(5 + invert) | curses.A_BOLD)
elif invert: elif invert:
self.stdscr.chgat(self.y, 0, curses.color_pair(12) | curses.A_BOLD) self.stdscr.chgat(y, 0, curses.color_pair(12) | curses.A_BOLD)
else: else:
self.stdscr.chgat(self.y, 0, curses.color_pair(0)) self.stdscr.chgat(y, 0, curses.color_pair(0))
def reprint(stdscr, entries2print): def reprint(self):
stdscr.clear() self.stdscr.clear()
stdscr.addstr(0, 1, _('Title')) self.stdscr.addstr(0, 1, _('Title'))
sitenamelen = max(max(len(entry['ie_key']) for entry in entries), 6) sitenamelen = max(max(len(entry['ie_key']) for entry in self.entries), 6)
stdscr.addstr(0, curses.COLS - sitenamelen - 1, _('Source')) self.stdscr.addstr(0, curses.COLS - sitenamelen - 1, _('Source'))
stdscr.chgat(0, 0, curses.color_pair(10) | curses.A_BOLD) self.stdscr.chgat(0, 0, curses.color_pair(10) | curses.A_BOLD)
for i, entry in enumerate(entries2print): for i, entry in enumerate(self.entries[self.start:][:curses.LINES-3]):
y = i + 1 self.stdscr.addstr(i + 1, 0, entry['ie_key'].rjust(curses.COLS - 1))
stdscr.addstr(y, 0, entry['ie_key'].rjust(curses.COLS - 1)) self.stdscr.addstr(i + 1, 1, entry.get('title', '')[:curses.COLS-sitenamelen-3])
stdscr.addstr(y, 1, entry.get('title', '')[:curses.COLS-sitenamelen-3]) self.reattr(i + 1)
reattr(stdscr, y, entry) self.update_status()
update_status(stdscr, mp)
def move(self, delta): def move(self, delta):
if self.start + self.y + delta < 1: if not (self.entries and delta): return
if start + y == 1: self.current()['current'] = False
return 1 self.reattr()
setno(entries, ['current']) start, maxy = self.start, min(len(self.entries), curses.LINES - 3)
start = 0
entries[0]['current'] = True
reprint(stdscr, entries[:curses.LINES-3])
return 1
elif start + y + delta > len(entries):
start = max(len(entries) - curses.LINES + 3, 0)
y = min(curses.LINES - 3, len(entries))
setno(entries, ['current'])
entries[-1]['current'] = True
reprint(stdscr, entries[-curses.LINES+3:])
return y
if y + delta < 1: if self.idx() + delta <= 0:
start += y + delta - 1 self.start, self.y = 0, 1
y = 1 elif self.idx() + delta >= len(self.entries):
setno(entries, ['current']) self.start, self.y = len(self.entries) - maxy, maxy
entries[start]['current'] = True elif self.y + delta < 1:
reprint(stdscr, entries[start : start+curses.LINES-3]) self.start += self.y + delta - 1
elif y + delta > curses.LINES - 3: self.y = 1
start += y + delta - curses.LINES + 3 elif self.y + delta > curses.LINES - 3:
y = curses.LINES - 3 self.start += self.y + delta - maxy
setno(entries, ['current']) self.y = maxy
entries[start + curses.LINES - 4]['current'] = True
reprint(stdscr, entries[start : start+curses.LINES-3])
else: else:
entries[start + y - 1]['current'] = False self.y += delta
reattr(stdscr, y, entries[start + y - 1])
y = y + delta self.current()['current'] = True
entries[start + y - 1]['current'] = True if self.start == start:
reattr(stdscr, y, entries[start + y - 1]) self.reattr()
stdscr.refresh() self.stdscr.refresh()
return y else:
self.reprint()
def gets(self, prompt):
self.stdscr.addstr(curses.LINES - 1, 0, prompt)
self.reading = True
curses.curs_set(True)
curses.echo()
b = self.stdscr.getstr(curses.LINES - 1, len(prompt))
self.reading = False
curses.curs_set(False)
curses.noecho()
return b.decode()
def generic_event_handler(self, event):
"""Reprint status line and play next entry if the last one is
ended without caring about the event.
"""
if self.update_status(): self.play()
def close(self): def close(self):
curses.nocbreak() curses.nocbreak()
@ -262,125 +283,114 @@ if args.json_playlist:
json_file = args.json_playlist json_file = args.json_playlist
with open(json_file) as f: with open(json_file) as f:
entries = json.load(f) entries = json.load(f)
elif args.youtube_playlist: elif args.online_playlist:
with YoutubeDL({'extract_flat': 'in_playlist', 'quiet': True}) as ytdl: with YoutubeDL({'extract_flat': 'in_playlist', 'quiet': True}) as ytdl:
info = ytdl.extract_info(args.youtube_playlist, download=False) info = ytdl.extract_info(args.online_playlist, download=False)
entries = info.get('entries', []) entries = info.get('entries', [])
json_file = '' json_file = ''
else: else:
entries = [] entries = []
json_file = '' json_file = ''
makedirs(dirname(mpv_log), exist_ok=True) makedirs(dirname(MPV_LOG), exist_ok=True)
comp = Comp( comp = Comp(
entries=entries, entries,
mode=config.get('comp', 'play-mode', fallback='play-current'), config.get('comp', 'play-mode', fallback='play-current'),
mpv_vo=config.get('mpv', 'video-output', fallback=None), config.get('mpv', 'video-output', fallback=None),
mpv_vid=config.get('mpv', 'video', fallback='auto'), config.get('mpv', 'video', fallback='auto'),
ytdlf=config.get('youtube-dl', 'format', fallback='best')) config.get('youtube-dl', 'format', fallback='best'))
while True: while True:
c = stdscr.getch() c = comp.stdscr.getch()
if c == 10: # curses.KEY_ENTER doesn't work if c == 10: # curses.KEY_ENTER doesn't work
if not entries: continue comp.update_playlist()
mp._set_property('pause', False, bool) comp.play()
play_thread = Thread(target=play, daemon=True)
play_thread.start()
elif c == 32: # space elif c == 32: # space
mp._toggle_property('pause') comp.mp._toggle_property('pause')
elif c == 65: # letter A elif c == 65: # letter A
mp._toggle_property('mute') comp.mp._toggle_property('mute')
elif c == 77: # letter M elif c == 77: # letter M
mode = MODES[(MODES.index(mode) - 1) % 8] comp.mode = MODES[(MODES.index(comp.mode) - 1) % 8]
update_status(stdscr, mp) comp.update_status()
elif c == 86: # letter V elif c == 86: # letter V
mp._set_property('vid', comp.mp._set_property(
'auto' if mp._get_property('vid') == 'no' else 'no') 'vid', 'auto' if mp._get_property('vid') == 'no' else 'no')
elif c == 87: # letter W elif c == 87: # letter W
if not entries: continue if not entries: continue
prompt = _('Save playlist to [{}]:').format(json_file) s = comp.gets(_('Save playlist to [{}]: ').format(json_file))
stdscr.addstr(curses.LINES - 1, 0, prompt)
curses.curs_set(True)
curses.echo()
s = stdscr.getstr(curses.LINES - 1, len(prompt) + 1).decode()
if s: json_file = s if s: json_file = s
curses.curs_set(False)
curses.noecho()
try: try:
makedirs(dirname(abspath(json_file)), exist_ok=True) makedirs(dirname(abspath(json_file)), exist_ok=True)
with open(json_file, 'w') as f: with open(json_file, 'w') as f:
json.dump(entries, f) json.dump(entries, f)
except: except:
update_status(stdscr, mp, comp.update_status(
_("'{}': Can't open file for writing").format(json_file), _("'{}': Can't open file for writing").format(json_file),
curses.color_pair(1)) curses.color_pair(1))
else: else:
update_status(stdscr, mp, comp.update_status(_("'{}' written").format(json_file))
_("'{}' written").format(json_file))
elif c == 99: # letter c elif c == 99: # letter c
if not entries: continue if not entries: continue
i = start + y - 1 i = comp.start + comp.y - 1
entries[i]['selected'] = not entries[i].setdefault('selected', False) comp.entries[i]['selected'] = not entries[i].setdefault('selected', False)
y = move(stdscr, entries, y, 1) comp.move(1)
elif c == 100: # letter d elif c == 100: # letter d
if not entries: continue if not entries: continue
i = start + y - 1 i = comp.idx()
if i + 1 < len(entries): if i + 1 < len(entries):
entries.pop(i) comp.entries.pop(i)
entries[i]['current'] = True comp.entries[i]['current'] = True
elif len(entries) > 1: elif len(entries) > 1:
entries.pop(i) comp.entries.pop(i)
entries[i - 1]['current'] = True comp.entries[i - 1]['current'] = True
else: else:
entries = [] comp.entries = []
reprint(stdscr, entries[start : start+curses.LINES-3]) comp.reprint()
elif c == 109: # letter m elif c == 109: # letter m
mode = MODES[(MODES.index(mode) + 1) % 8] comp.mode = MODES[(MODES.index(comp.mode) + 1) % 8]
update_status(stdscr, mp) comp.update_status()
elif c == 113: # letter q elif c == 113: # letter q
comp.close() comp.close()
break
elif c == 119: # letter w elif c == 119: # letter w
if not entries: continue if not comp.entries: continue
with YoutubeDL({'format': ytdlf}) as ytdl: with YoutubeDL({'format': ytdlf}) as ytdl:
ytdl.download([getlink(entry) for entry in choose_from(mode)]) ytdl.download([comp.getlink(entry) for entry in choose_from(mode)])
elif c in (curses.KEY_UP, 107): # up arrow or letter k elif c in (curses.KEY_UP, 107): # up arrow or letter k
if not entries: continue if not entries: continue
y = move(stdscr, entries, y, -1) comp.move(-1)
elif c in (curses.KEY_DOWN, 106): # down arrow or letter j elif c in (curses.KEY_DOWN, 106): # down arrow or letter j
if not entries: continue if not entries: continue
y = move(stdscr, entries, y, 1) comp.move(1)
elif c in (curses.KEY_LEFT, 104): # left arrow or letter h elif c in (curses.KEY_LEFT, 104): # left arrow or letter h
if mp._get_property('duration', int): if comp.mp._get_property('duration', int):
mp.seek(-2.5) comp.mp.seek(-2.5)
elif c in (curses.KEY_RIGHT, 108): # right arrow or letter l elif c in (curses.KEY_RIGHT, 108): # right arrow or letter l
if mp._get_property('duration', int): if comp.mp._get_property('duration', int):
mp.seek(2.5) comp.mp.seek(2.5)
elif c == curses.KEY_HOME: # home elif c == curses.KEY_HOME: # home
if not entries: continue if not comp.entries: continue
y = move(stdscr, entries, y, -len(entries)) comp.move(-len(comp.entries))
elif c == curses.KEY_END: # end elif c == curses.KEY_END: # end
if not entries: continue if not entries: continue
y = move(stdscr, entries, y, len(entries)) comp.move(len(comp.entries))
elif c == curses.KEY_NPAGE: # page down elif c == curses.KEY_NPAGE: # page down
if not entries: continue if not comp.entries: continue
y = move(stdscr, entries, y, curses.LINES - 4) comp.move(curses.LINES - 4)
elif c == curses.KEY_PPAGE: # page up elif c == curses.KEY_PPAGE: # page up
if not entries: continue if not entries: continue
y = move(stdscr, entries, y, 4 - curses.LINES) comp.move(4 - curses.LINES)
elif c == curses.KEY_F5: # F5 elif c == curses.KEY_F5: # F5
reprint(stdscr, entries[start : start+curses.LINES-3]) comp.reprint()
elif c == curses.KEY_RESIZE: elif c == curses.KEY_RESIZE:
curses.update_lines_cols() curses.update_lines_cols()
if curses.COLS < MODE_STR_LEN + 42 or curses.LINES < 4: if curses.COLS < MODE_STR_LEN + 42 or curses.LINES < 4:
stdscr.clear() comp.stdscr.clear()
sizeerr = _('Current size: {}x{}. Minimum size: {}x4.').format( sizeerr = _('Current size: {}x{}. Minimum size: {}x4.').format(
curses.COLS, curses.COLS, curses.LINES, MODE_STR_LEN + 42)
curses.LINES, comp.stdscr.addstr(0, 0, sizeerr[:curses.LINES*curses.COLS])
MODE_STR_LEN + 42
)
stdscr.addstr(0, 0, sizeerr[:curses.LINES*curses.COLS])
else: else:
start += y - 1 comp.start += comp.y - 1
y = 1 comp.y = 1
reprint(stdscr, entries[start : start+curses.LINES-3]) comp.reprint()