2017-03-20 14:49:11 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
2017-03-26 10:26:37 +02:00
|
|
|
# comp - Curses Online Media Player
|
|
|
|
# Copyright (C) 2017 Raphael McSinyx
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as
|
|
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
|
|
# License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
2017-03-20 14:49:11 +01:00
|
|
|
import curses
|
|
|
|
import json
|
|
|
|
from argparse import ArgumentParser
|
2017-03-23 15:37:52 +01:00
|
|
|
from configparser import ConfigParser
|
|
|
|
from os.path import expanduser
|
2017-03-30 06:14:51 +02:00
|
|
|
from time import gmtime, strftime
|
2017-03-20 14:49:11 +01:00
|
|
|
|
2017-03-30 06:14:51 +02:00
|
|
|
from mpv import MPV
|
2017-03-20 14:49:11 +01:00
|
|
|
|
|
|
|
|
2017-03-30 06:14:51 +02:00
|
|
|
def initmpv(ytdl_format, video):
|
2017-03-20 14:49:11 +01:00
|
|
|
if video:
|
2017-03-30 06:14:51 +02:00
|
|
|
return MPV(input_default_bindings=True, input_vo_keyboard=True,
|
|
|
|
ytdl=True, ytdl_format=ytdl_format)
|
2017-03-20 14:49:11 +01:00
|
|
|
else:
|
2017-03-30 06:14:51 +02:00
|
|
|
return MPV(input_default_bindings=True, input_vo_keyboard=True,
|
|
|
|
ytdl=True, ytdl_format=ytdl_format, vid=False)
|
2017-03-24 15:57:19 +01:00
|
|
|
|
|
|
|
|
2017-03-30 06:14:51 +02:00
|
|
|
def setno(data, keys):
|
|
|
|
"""Set all keys of each track in data to False."""
|
|
|
|
for key in keys:
|
|
|
|
for track in data:
|
|
|
|
track[key] = False
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
if pos is None:
|
|
|
|
return ''
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def updatestatusline(stdscr):
|
|
|
|
stdscr.addstr(curses.LINES - 2, 1, '{} {}'.format(
|
|
|
|
'|' if mp._get_property('pause', bool) else '>',
|
|
|
|
secpair2hhmmss(mp._get_property('time-pos', int),
|
|
|
|
mp._get_property('duration', int))
|
|
|
|
))
|
2017-03-24 15:57:19 +01:00
|
|
|
stdscr.addstr(
|
|
|
|
curses.LINES - 2,
|
|
|
|
curses.COLS - 16,
|
|
|
|
'{:7} {:8}'.format(mode, 'selected' if selected else 'all')
|
|
|
|
)
|
|
|
|
stdscr.chgat(curses.LINES - 2, 0, curses.color_pair(8))
|
|
|
|
stdscr.refresh()
|
|
|
|
|
|
|
|
|
2017-03-30 06:14:51 +02:00
|
|
|
def reprint(stdscr, data2print):
|
|
|
|
stdscr.clear()
|
|
|
|
stdscr.addstr(0, curses.COLS-12, 'URL')
|
|
|
|
stdscr.addstr(0, 1, 'Title')
|
|
|
|
stdscr.chgat(0, 0, curses.color_pair(10) | curses.A_BOLD)
|
|
|
|
for i, track in enumerate(data2print):
|
|
|
|
y = i + 1
|
|
|
|
stdscr.addstr(y, 0, track['url'].rjust(curses.COLS - 1))
|
|
|
|
stdscr.addstr(y, 1, track['title'][:curses.COLS-12])
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
2017-03-26 10:26:37 +02:00
|
|
|
def move(stdscr, data, y, delta):
|
2017-03-24 15:57:19 +01:00
|
|
|
global start
|
|
|
|
if start + y + delta < 1:
|
|
|
|
start = 0
|
2017-03-30 06:14:51 +02:00
|
|
|
setno(data, ['highlight'])
|
2017-03-26 10:26:37 +02:00
|
|
|
data[0]['highlight'] = True
|
2017-03-30 06:14:51 +02:00
|
|
|
reprint(stdscr, data[:curses.LINES-3])
|
2017-03-24 15:57:19 +01:00
|
|
|
return 1
|
2017-03-26 10:26:37 +02:00
|
|
|
elif start + y + delta > len(data):
|
|
|
|
start = len(data) - curses.LINES + 3
|
2017-03-24 15:57:19 +01:00
|
|
|
y = curses.LINES - 3
|
2017-03-30 06:14:51 +02:00
|
|
|
setno(data, ['highlight'])
|
2017-03-26 10:26:37 +02:00
|
|
|
data[-1]['highlight'] = True
|
2017-03-30 06:14:51 +02:00
|
|
|
reprint(stdscr, data[-curses.LINES+3:])
|
2017-03-20 14:49:11 +01:00
|
|
|
return y
|
|
|
|
|
2017-03-24 15:57:19 +01:00
|
|
|
if 0 < y + delta < curses.LINES - 2:
|
|
|
|
y = y + delta
|
|
|
|
elif y + delta < 1:
|
|
|
|
start += y + delta - 1
|
|
|
|
y = 1
|
|
|
|
else:
|
|
|
|
start += y + delta - curses.LINES + 3
|
|
|
|
y = curses.LINES - 3
|
2017-03-30 06:14:51 +02:00
|
|
|
setno(data, ['highlight'])
|
2017-03-26 10:26:37 +02:00
|
|
|
data[start + y - 1]['highlight'] = True
|
2017-03-30 06:14:51 +02:00
|
|
|
reprint(stdscr, data[start : start+curses.LINES-3])
|
2017-03-24 15:57:19 +01:00
|
|
|
stdscr.refresh()
|
|
|
|
return y
|
2017-03-20 14:49:11 +01:00
|
|
|
|
|
|
|
|
2017-03-30 06:14:51 +02:00
|
|
|
parser = ArgumentParser(description="console/curses online media mp")
|
2017-03-20 14:49:11 +01:00
|
|
|
parser.add_argument('-j', '--json-playlist', required=False,
|
|
|
|
help='path to playlist in JSON format')
|
|
|
|
args = parser.parse_args()
|
2017-03-23 15:37:52 +01:00
|
|
|
|
|
|
|
config = ConfigParser()
|
|
|
|
config.read(expanduser('~/.config/comp/settings.ini'))
|
|
|
|
ytdl_format = config.get('Init', 'ytdl-format', fallback='best')
|
|
|
|
mode = config.get('Runtime', 'play-mode', fallback='normal')
|
|
|
|
selected = config.getboolean('Runtime', 'play-selected-only', fallback=False)
|
|
|
|
video = config.getboolean('Runtime', 'video', fallback=True)
|
|
|
|
|
2017-03-20 14:49:11 +01:00
|
|
|
with open(args.json_playlist) as f:
|
2017-03-26 10:26:37 +02:00
|
|
|
data = json.load(f)
|
2017-03-30 06:14:51 +02:00
|
|
|
setno(data, ['error', 'playing', 'selected', 'highlight'])
|
2017-03-24 15:57:19 +01:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2017-03-30 06:14:51 +02:00
|
|
|
mp = initmpv(ytdl_format, video)
|
|
|
|
mp.observe_property('time-pos', lambda pos: updatestatusline(stdscr))
|
|
|
|
|
2017-03-24 15:57:19 +01:00
|
|
|
# Print initial content
|
|
|
|
start = 0
|
|
|
|
y = 1
|
2017-03-26 10:26:37 +02:00
|
|
|
data[0]['highlight'] = True
|
2017-03-30 06:14:51 +02:00
|
|
|
reprint(stdscr, data[:curses.LINES-3])
|
2017-03-24 15:57:19 +01:00
|
|
|
|
2017-03-30 06:14:51 +02:00
|
|
|
# mpv keys: []{}<>.,qQ/*90m-#fTweoPOvjJxzlLVrtsSIdA
|
|
|
|
# yuighkcbn
|
2017-03-24 15:57:19 +01:00
|
|
|
c = stdscr.getch()
|
|
|
|
while c != 113: # letter q
|
|
|
|
if c == curses.KEY_RESIZE:
|
|
|
|
curses.update_lines_cols()
|
2017-03-30 06:14:51 +02:00
|
|
|
move(stdscr, data, y, 1 - y)
|
|
|
|
y = move(stdscr, data, 1, y - 1)
|
|
|
|
elif c in (106, curses.KEY_DOWN): # letter j or down arrow
|
2017-03-26 10:26:37 +02:00
|
|
|
y = move(stdscr, data, y, 1)
|
2017-03-30 06:14:51 +02:00
|
|
|
elif c in (107, curses.KEY_UP): # letter k or up arrow
|
2017-03-26 10:26:37 +02:00
|
|
|
y = move(stdscr, data, y, -1)
|
2017-03-30 06:14:51 +02:00
|
|
|
elif c == curses.KEY_PPAGE: # page up
|
2017-03-26 10:26:37 +02:00
|
|
|
y = move(stdscr, data, y, -curses.LINES)
|
2017-03-30 06:14:51 +02:00
|
|
|
elif c == curses.KEY_NPAGE: # page down
|
2017-03-26 10:26:37 +02:00
|
|
|
y = move(stdscr, data, y, curses.LINES)
|
2017-03-30 06:14:51 +02:00
|
|
|
elif c == curses.KEY_HOME: # home
|
2017-03-26 10:26:37 +02:00
|
|
|
y = move(stdscr, data, y, -len(data))
|
2017-03-30 06:14:51 +02:00
|
|
|
elif c == curses.KEY_END: # end
|
2017-03-26 10:26:37 +02:00
|
|
|
y = move(stdscr, data, y, len(data))
|
2017-03-30 06:14:51 +02:00
|
|
|
elif c == 32: # space
|
|
|
|
setno(data, ['playing'])
|
|
|
|
mp.play('https://youtu.be/' + data[start + y - 1]['url'])
|
|
|
|
data[start + y - 1]['playing'] = True
|
|
|
|
reprint(stdscr, data[start : start+curses.LINES-3])
|
|
|
|
elif c == 112: # letter p
|
|
|
|
mp._set_property('pause', not mp._get_property('pause', bool), bool)
|
|
|
|
elif c == 99: # letter c
|
2017-03-26 10:26:37 +02:00
|
|
|
data[start + y - 1]['selected'] = not data[start + y - 1]['selected']
|
|
|
|
y = move(stdscr, data, y, 1)
|
2017-03-24 15:57:19 +01:00
|
|
|
c = stdscr.getch()
|
|
|
|
|
|
|
|
curses.nocbreak()
|
|
|
|
stdscr.keypad(False)
|
|
|
|
curses.echo()
|
|
|
|
curses.endwin()
|
2017-03-30 06:14:51 +02:00
|
|
|
|
|
|
|
del mp
|