Create an object to avoid using global variable

This commit is contained in:
Nguyễn Gia Phong 2017-04-24 11:13:12 +07:00
parent bdd2ac61cf
commit ae7618c807
1 changed files with 181 additions and 180 deletions

361
comp Executable file → Normal file
View File

@ -23,7 +23,6 @@ from configparser import ConfigParser
from curses.ascii import ctrl
from datetime import datetime
from gettext import bindtextdomain, gettext, textdomain
from io import StringIO
from itertools import cycle
from os import linesep, makedirs
from os.path import abspath, dirname, expanduser, isfile
@ -49,7 +48,7 @@ MODE_STR_LEN = max(len(_(mode)) for mode in MODES)
def mpv_logger(loglevel, component, message):
mpv_log = '{} [{}] {}: {}{}'.format(datetime.isoformat(datetime.now()),
loglevel, component, message, linesep)
loglevel, component, message, linesep)
with open(MPV_LOG, 'a') as f:
f.write(mpv_log)
@ -68,35 +67,7 @@ def getlink(entry):
ie_key=entry.get('ie_key')).get('webpage_url')
def choose_from(mode1):
if mode1 == 'all': return entries
else: return [entry for entry in entries if entry.setdefault(mode1, False)]
def playlist(mode):
"""Return a generator of entries to be played."""
action = mode.split('-')[0]
entries2play = choose_from(mode.split('-')[1])
# Somehow yield have to be used instead of returning a generator
if action == 'play':
for entry in entries2play: yield entry
elif action == 'repeat':
for entry in cycle(entries2play): yield entry
elif entries2play:
while True: yield choice(entries2play)
def play():
for entry in playlist(mode):
setno(entries, ['playing'])
reprint(stdscr, entries[start : start+curses.LINES-3])
entries[entries.index(entry)]['playing'] = True
reprint(stdscr, entries[start : start+curses.LINES-3])
mp.play(getlink(entry))
mp.wait_for_playback()
def secpair2hhmmss(pos, duration):
def _secpair2hhmmss(pos, duration):
"""Quick hack to convert a pair of seconds to HHMMSS / HHMMSS
string as MPV.get_property_osd_string isn't available.
"""
@ -104,126 +75,188 @@ def secpair2hhmmss(pos, duration):
postime, durationtime = gmtime(pos), gmtime(duration)
# Let's hope media durations are shorter than a day
timestr = '%M:%S' if duration < 3600 else '%H:%M:%S'
return '{} / {}'.format(strftime(timestr, postime),
strftime(timestr, durationtime))
return ' {} / {}'.format(strftime(timestr, postime),
strftime(timestr, durationtime))
def update_status(stdscr, mp, message='', msgattr=curses.A_NORMAL):
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(14))
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(14) | curses.A_BOLD)
stdscr.addstr(curses.LINES - 2, len(left + center), right,
curses.color_pair(14))
else:
stdscr.addstr(curses.LINES - 2, 0, right.rjust(curses.COLS),
curses.color_pair(14))
stdscr.move(curses.LINES - 1, 0)
stdscr.clrtoeol()
stdscr.addstr(curses.LINES - 1, 0, message, msgattr)
stdscr.refresh()
class Comp(object):
"""Meta object for drawing and playing."""
def __new__(cls, entries, mode, mpv_vo, mpv_vid, ytdlf):
self = object.__new__(cls)
self.start = 0
self.y = 1
self.mode = mode
self.entries = entries
self.playlist = (i for i in []) # an empty generator?
self.stdscr = curses.initscr()
self.mp = MPV(input_default_bindings=True, input_vo_keyboard=True,
log_handler=mpv_logger, ytdl=True, ytdl_format=ytdlf)
if mpv_vo: self.mp['vo'] = mpv_vo
self.mp._set_property('vid', mpv_vid)
return self
def reattr(stdscr, y, entry):
invert = 8 if entry.setdefault('current', False) else 0
if entry.setdefault('error', False):
stdscr.chgat(y, 0, curses.color_pair(1 + invert) | curses.A_BOLD)
elif entry.setdefault('playing', False):
stdscr.chgat(y, 0, curses.color_pair(3 + invert) | curses.A_BOLD)
elif entry.setdefault('selected', False):
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))
def reprint(stdscr, entries2print):
stdscr.clear()
stdscr.addstr(0, 1, _('Title'))
sitenamelen = max(max(len(entry['ie_key']) for entry in entries), 6)
stdscr.addstr(0, curses.COLS - sitenamelen - 1, _('Source'))
stdscr.chgat(0, 0, curses.color_pair(10) | curses.A_BOLD)
for i, entry in enumerate(entries2print):
y = i + 1
stdscr.addstr(y, 0, entry['ie_key'].rjust(curses.COLS - 1))
stdscr.addstr(y, 1, entry['title'][:curses.COLS-sitenamelen-3])
reattr(stdscr, y, entry)
update_status(stdscr, mp)
def initprint(stdscr, entries):
"""Print initial content."""
global start, y
start, y = 0, 1
if not entries:
return
setno(entries, ['current', 'error', 'playing', 'selected'])
entries[0]['current'] = True
reprint(stdscr, entries[:curses.LINES-3])
def move(stdscr, entries, y, delta):
global start
if start + y + delta < 1:
if start + y == 1:
return 1
setno(entries, ['current'])
start = 0
def __init__(self):
setno(entries, ['current', 'error', 'playing', 'selected'])
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:])
curses.noecho()
curses.cbreak()
self.stdscr.keypad(True)
curses.curs_set(False)
curses.start_color()
curses.use_default_colors()
curses.init_pair(1, 1, -1)
curses.init_pair(2, 2, -1)
curses.init_pair(3, 3, -1)
curses.init_pair(4, 4, -1)
curses.init_pair(5, 5, -1)
curses.init_pair(6, 6, -1)
curses.init_pair(7, 7, -1)
curses.init_pair(8, -1, 7)
curses.init_pair(9, -1, 1)
curses.init_pair(10, -1, 2)
curses.init_pair(11, -1, 3)
curses.init_pair(12, -1, 4)
curses.init_pair(13, -1, 5)
curses.init_pair(14, -1, 6)
self.mp._set_property('vid', mpv_vid)
def updatestt(void): self.update_status()
self.mp.observe_property('mute', updatestt)
self.mp.observe_property('pause', updatestt)
self.mp.observe_property('time-pos', updatestt)
self.mp.observe_property('vid', updatestt)
def update_status(self, message='', msgattr=curses.A_NORMAL):
right = ' {} {}{} '.format(
_(mode), ' ' if self.mp._get_property('mute', bool) else 'A',
' ' if mp._get_property('vid') == 'no' else 'V')
try:
left = _secpair2hhmmss(self.mp._get_property('time-pos', int),
self.mp._get_property('duration', int))
except:
left += ' | ' if self.mp._get_property('pause', bool) else ' > '
self.stdscr.addstr(curses.LINES - 2, 0, right.rjust(curses.COLS),
curses.color_pair(14))
else:
stdscr.addstr(curses.LINES - 2, 0, left, curses.color_pair(14))
title_len = curses.COLS - len(left + right)
center = self.entries.setdefault('title', self.mp._get_property('media-title')).ljust(title_len)[:title_len]
self.stdscr.addstr(curses.LINES - 2, len(left), center,
curses.color_pair(14) | curses.A_BOLD)
self.stdscr.addstr(curses.LINES - 2, len(left + center), right,
curses.color_pair(14))
finally:
self.stdscr.move(curses.LINES - 1, 0)
self.stdscr.clrtoeol()
self.stdscr.addstr(curses.LINES - 1, 0, message, msgattr)
self.stdscr.refresh()
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):
action = self.mode.split('-')[0]
entries2play = choose_from(mode.split('-')[1])
# Somehow yield have to be used instead of returning a generator
if action == 'play':
for entry in entries2play: yield entry
elif action == 'repeat':
for entry in cycle(entries2play): yield entry
elif entries2play:
while True: yield choice(entries2play)
def play(self):
for entry in playlist(self.mode):
setno(entries, ['playing'])
reprint(stdscr, entries[start : start+curses.LINES-3])
entries[entries.index(entry)]['playing'] = True
reprint(stdscr, entries[start : start+curses.LINES-3])
mp.play(entry.setdefault('webpage_url', getlink(entry)))
mp.wait_for_playback()
def reattr(self, entry):
invert = 8 if entry.setdefault('current', False) else 0
if entry.setdefault('error', False):
self.stdscr.chgat(self.y, 0, curses.color_pair(1 + invert) | curses.A_BOLD)
elif entry.setdefault('playing', False):
self.stdscr.chgat(self.y, 0, curses.color_pair(3 + invert) | curses.A_BOLD)
elif entry.setdefault('selected', False):
self.stdscr.chgat(self.y, 0, curses.color_pair(5 + invert) | curses.A_BOLD)
elif invert:
self.stdscr.chgat(self.y, 0, curses.color_pair(12) | curses.A_BOLD)
else:
self.stdscr.chgat(self.y, 0, curses.color_pair(0))
def reprint(stdscr, entries2print):
stdscr.clear()
stdscr.addstr(0, 1, _('Title'))
sitenamelen = max(max(len(entry['ie_key']) for entry in entries), 6)
stdscr.addstr(0, curses.COLS - sitenamelen - 1, _('Source'))
stdscr.chgat(0, 0, curses.color_pair(10) | curses.A_BOLD)
for i, entry in enumerate(entries2print):
y = i + 1
stdscr.addstr(y, 0, entry['ie_key'].rjust(curses.COLS - 1))
stdscr.addstr(y, 1, entry.get('title', '')[:curses.COLS-sitenamelen-3])
reattr(stdscr, y, entry)
update_status(stdscr, mp)
def move(self, delta):
if self.start + self.y + delta < 1:
if start + y == 1:
return 1
setno(entries, ['current'])
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:
start += y + delta - 1
y = 1
setno(entries, ['current'])
entries[start]['current'] = True
reprint(stdscr, entries[start : start+curses.LINES-3])
elif y + delta > curses.LINES - 3:
start += y + delta - curses.LINES + 3
y = curses.LINES - 3
setno(entries, ['current'])
entries[start + curses.LINES - 4]['current'] = True
reprint(stdscr, entries[start : start+curses.LINES-3])
else:
entries[start + y - 1]['current'] = False
reattr(stdscr, y, entries[start + y - 1])
y = y + delta
entries[start + y - 1]['current'] = True
reattr(stdscr, y, entries[start + y - 1])
stdscr.refresh()
return y
if y + delta < 1:
start += y + delta - 1
y = 1
setno(entries, ['current'])
entries[start]['current'] = True
reprint(stdscr, entries[start : start+curses.LINES-3])
elif y + delta > curses.LINES - 3:
start += y + delta - curses.LINES + 3
y = curses.LINES - 3
setno(entries, ['current'])
entries[start + curses.LINES - 4]['current'] = True
reprint(stdscr, entries[start : start+curses.LINES-3])
else:
entries[start + y - 1]['current'] = False
reattr(stdscr, y, entries[start + y - 1])
y = y + delta
entries[start + y - 1]['current'] = True
reattr(stdscr, y, entries[start + y - 1])
stdscr.refresh()
return y
def close(self):
curses.nocbreak()
self.stdscr.keypad(False)
curses.echo()
curses.endwin()
self.mp.terminate()
parser = ArgumentParser(description=_("Curses Online Media Player"))
parser.add_argument('-j', '--json-playlist', required=False,
parser.add_argument('-j', '--json-playlist', required=False, metavar='path',
help=_('path to playlist in JSON format'))
parser.add_argument('-y', '--youtube-playlist', required=False,
parser.add_argument('-u', '--online-playlist', required=False, metavar='URL',
help=_('URL to an playlist on Youtube'))
args = parser.parse_args()
config = ConfigParser()
config.read(USER_CONFIG if isfile(USER_CONFIG) else SYSTEM_CONFIG)
mode = config.get('comp', 'play-mode', fallback='play-current')
video = config.get('mpv', 'video', fallback='auto')
video_output = config.get('mpv', 'video-output', fallback=None)
ytdl_opts = {'format': config.get('youtube-dl', 'format', fallback='best')}
if args.json_playlist:
json_file = args.json_playlist
@ -232,49 +265,23 @@ if args.json_playlist:
elif args.youtube_playlist:
with YoutubeDL({'extract_flat': 'in_playlist', 'quiet': True}) as ytdl:
info = ytdl.extract_info(args.youtube_playlist, download=False)
entries = info.get('entries', {})
entries = info.get('entries', [])
json_file = ''
else:
entries = []
json_file = ''
# Init curses screen
stdscr = curses.initscr()
curses.noecho()
curses.cbreak()
stdscr.keypad(True)
curses.curs_set(False)
curses.start_color()
curses.use_default_colors()
curses.init_pair(1, 1, -1)
curses.init_pair(2, 2, -1)
curses.init_pair(3, 3, -1)
curses.init_pair(4, 4, -1)
curses.init_pair(5, 5, -1)
curses.init_pair(6, 6, -1)
curses.init_pair(7, 7, -1)
curses.init_pair(8, -1, 7)
curses.init_pair(9, -1, 1)
curses.init_pair(10, -1, 2)
curses.init_pair(11, -1, 3)
curses.init_pair(12, -1, 4)
curses.init_pair(13, -1, 5)
curses.init_pair(14, -1, 6)
makedirs(dirname(mpv_log), exist_ok=True)
# Init mpv
makedirs(expanduser(dirname(MPV_LOG)), exist_ok=True)
mp = MPV(input_default_bindings=True, input_vo_keyboard=True,
log_handler=mpv_logger, ytdl=True, ytdl_format=ytdl_opts['format'])
if video_output: mp['vo'] = video_output
mp._set_property('vid', video)
mp.observe_property('mute', lambda foo: update_status(stdscr, mp))
mp.observe_property('pause', lambda foo: update_status(stdscr, mp))
mp.observe_property('time-pos', lambda foo: update_status(stdscr, mp))
mp.observe_property('vid', lambda foo: update_status(stdscr, mp))
comp = Comp(
entries=entries,
mode=config.get('comp', 'play-mode', fallback='play-current'),
mpv_vo=config.get('mpv', 'video-output', fallback=None),
mpv_vid=config.get('mpv', 'video', fallback='auto'),
ytdlf=config.get('youtube-dl', 'format', fallback='best'))
initprint(stdscr, entries)
c = stdscr.getch()
while c != 113: # letter q
while True:
c = stdscr.getch()
if c == 10: # curses.KEY_ENTER doesn't work
if not entries: continue
mp._set_property('pause', False, bool)
@ -331,6 +338,8 @@ while c != 113: # letter q
elif c == 109: # letter m
mode = MODES[(MODES.index(mode) + 1) % 8]
update_status(stdscr, mp)
elif c == 113: # letter q
comp.close()
elif c == 119: # letter w
if not entries: continue
with YoutubeDL({'format': ytdlf}) as ytdl:
@ -375,11 +384,3 @@ while c != 113: # letter q
start += y - 1
y = 1
reprint(stdscr, entries[start : start+curses.LINES-3])
c = stdscr.getch()
curses.nocbreak()
stdscr.keypad(False)
curses.echo()
curses.endwin()
del mp