2017-03-20 14:49:11 +01:00
|
|
|
#!/usr/bin/env python3
|
2017-06-10 17:14:15 +02:00
|
|
|
# comp - Curses Omni Media Player
|
2017-03-26 10:26:37 +02:00
|
|
|
#
|
2017-05-28 16:02:34 +02:00
|
|
|
# comp is free software: you can redistribute it and/or modify
|
2017-03-26 10:26:37 +02:00
|
|
|
# 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.
|
|
|
|
#
|
2017-05-28 16:02:34 +02:00
|
|
|
# comp program is distributed in the hope that it will be useful,
|
2017-03-26 10:26:37 +02:00
|
|
|
# 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
|
2017-05-28 16:02:34 +02:00
|
|
|
# along with comp. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
#
|
|
|
|
# Copyright (C) 2017 Nguyễn Gia Phong <vn.mcsinyx@gmail.com>
|
2017-03-26 10:26:37 +02:00
|
|
|
|
2017-03-20 14:49:11 +01:00
|
|
|
import curses
|
|
|
|
import json
|
2017-05-07 10:59:54 +02:00
|
|
|
import re
|
2017-03-20 14:49:11 +01:00
|
|
|
from argparse import ArgumentParser
|
2017-05-07 10:59:54 +02:00
|
|
|
from collections import deque
|
2017-03-23 15:37:52 +01:00
|
|
|
from configparser import ConfigParser
|
2017-05-01 11:19:49 +02:00
|
|
|
from functools import reduce
|
2017-06-10 17:14:15 +02:00
|
|
|
from gettext import bindtextdomain, gettext as _, textdomain
|
2017-05-29 15:38:09 +02:00
|
|
|
from os import makedirs
|
|
|
|
from os.path import abspath, dirname, expanduser, isfile
|
2017-04-03 15:52:59 +02:00
|
|
|
from threading import Thread
|
2017-03-20 14:49:11 +01:00
|
|
|
|
2017-05-29 15:38:09 +02:00
|
|
|
from mpv import MPV
|
2017-06-10 17:14:15 +02:00
|
|
|
from pkg_resources import resource_filename
|
|
|
|
from youtube_dl import YoutubeDL
|
2017-03-20 14:49:11 +01:00
|
|
|
|
2017-05-28 16:02:34 +02:00
|
|
|
from omp import extract_info, Omp
|
|
|
|
|
2017-04-05 06:39:04 +02:00
|
|
|
# Init gettext
|
2017-06-10 17:14:15 +02:00
|
|
|
bindtextdomain('omp', resource_filename('omp', 'locale'))
|
|
|
|
textdomain('omp')
|
2017-04-05 06:39:04 +02:00
|
|
|
|
2017-04-27 16:36:10 +02:00
|
|
|
# Global constants
|
2017-04-06 18:02:54 +02:00
|
|
|
SYSTEM_CONFIG = '/etc/comp/settings.ini'
|
2017-04-05 06:39:04 +02:00
|
|
|
USER_CONFIG = expanduser('~/.config/comp/settings.ini')
|
2017-05-07 10:59:54 +02:00
|
|
|
MODES = ("play-current", "play-all", "play-selected", "repeat-current",
|
|
|
|
"repeat-all", "repeat-selected", "shuffle-all", "shuffle-selected")
|
2017-04-06 18:02:54 +02:00
|
|
|
MODE_STR_LEN = max(len(_(mode)) for mode in MODES)
|
2017-05-07 10:59:54 +02:00
|
|
|
DURATION_COL_LEN = max(len(_("Duration")), 8)
|
2017-04-05 06:39:04 +02:00
|
|
|
|
|
|
|
|
2017-05-07 10:59:54 +02:00
|
|
|
def justified(s, width):
|
|
|
|
"""Return s left-justified of length width."""
|
|
|
|
return s.ljust(width)[:width]
|
|
|
|
|
|
|
|
|
2017-05-28 16:02:34 +02:00
|
|
|
class Comp(Omp):
|
2017-04-27 16:36:10 +02:00
|
|
|
"""Meta object for drawing and playing.
|
|
|
|
|
|
|
|
Attributes:
|
2017-05-01 11:19:49 +02:00
|
|
|
entries (list): list of all tracks
|
2017-05-07 10:59:54 +02:00
|
|
|
json_file (str): path to save JSON playlist
|
2017-05-01 11:19:49 +02:00
|
|
|
mode (str): the mode to pick and play tracks
|
|
|
|
mp (MPV): an mpv instance
|
|
|
|
play_backward (bool): flag show if to play the previous track
|
|
|
|
play_list (list): list of tracks according to mode
|
|
|
|
played (list): list of previously played tracks
|
|
|
|
playing (int): index of playing track in played
|
|
|
|
playlist (iterator): iterator of tracks according to mode
|
|
|
|
reading (bool): flag show if user input is being read
|
2017-05-07 10:59:54 +02:00
|
|
|
search_res (iterator): title-searched results
|
2017-05-01 11:19:49 +02:00
|
|
|
scr (curses WindowObject): curses window object
|
2017-05-07 10:59:54 +02:00
|
|
|
start (int): index of the first track to be printed on screen
|
2017-05-01 11:19:49 +02:00
|
|
|
vid (str): flag show if video output is enabled
|
|
|
|
y (int): the current y-coordinate
|
2017-04-27 16:36:10 +02:00
|
|
|
"""
|
2017-05-29 15:38:09 +02:00
|
|
|
def __new__(cls, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf):
|
2017-05-28 16:02:34 +02:00
|
|
|
self = object.__new__(cls)
|
2017-05-15 06:02:56 +02:00
|
|
|
self.play_backward, self.reading = False, False
|
2017-05-01 11:19:49 +02:00
|
|
|
self.playing, self.start, self.y = -1, 0, 1
|
2017-05-07 10:59:54 +02:00
|
|
|
self.json_file, self.mode, self.vid = json_file, mode, mpv_vid
|
|
|
|
self.entries, self.played = entries, []
|
|
|
|
self.playlist, self.search_res = iter(()), deque()
|
2017-04-24 06:13:12 +02:00
|
|
|
self.mp = MPV(input_default_bindings=True, input_vo_keyboard=True,
|
2017-05-28 16:02:34 +02:00
|
|
|
ytdl=True, ytdl_format=ytdlf)
|
2017-05-01 11:19:49 +02:00
|
|
|
self.scr = curses.initscr()
|
|
|
|
return self
|
2017-04-27 16:36:10 +02:00
|
|
|
|
2017-06-12 08:37:42 +02:00
|
|
|
def adds(self, s, y, x=0, X=-1, attr=curses.A_NORMAL, lpad=1):
|
|
|
|
"""Paint the string s, added lpad spaces to the left, from
|
|
|
|
(y, x) to (y, X) with attributes attr, overwriting anything
|
|
|
|
previously on the display.
|
|
|
|
"""
|
|
|
|
if self.reading: return
|
|
|
|
y %= curses.LINES
|
|
|
|
x %= curses.COLS
|
|
|
|
length = X % curses.COLS - x + (y != curses.LINES - 1)
|
|
|
|
self.scr.addstr(y, x, (' '*lpad + s).ljust(length)[:length], attr)
|
|
|
|
|
2017-04-24 06:13:12 +02:00
|
|
|
def update_status(self, message='', msgattr=curses.A_NORMAL):
|
2017-05-01 11:19:49 +02:00
|
|
|
"""Update the status lines at the bottom of the screen."""
|
2017-06-12 08:37:42 +02:00
|
|
|
def add_status_str(s, x=0, X=-1, attr=curses.color_pair(12), lpad=1):
|
|
|
|
self.adds(s, curses.LINES - 2, x=x, X=X, attr=attr, lpad=lpad)
|
2017-05-01 11:19:49 +02:00
|
|
|
|
2017-06-12 08:37:42 +02:00
|
|
|
if self.mp.osd.duration is not None:
|
2017-06-10 17:14:15 +02:00
|
|
|
self.played[self.playing]['duration'] = self.mp.osd.duration
|
2017-06-12 08:37:42 +02:00
|
|
|
add_status_str(self.mp.osd.time_pos or '00:00:00', X=8)
|
|
|
|
add_status_str('/', x=9, X=10)
|
|
|
|
add_status_str(self.mp.osd.duration or '00:00:00', x=11, X=19)
|
|
|
|
add_status_str('|' if self.mp.pause else '>', x=20, X=21)
|
|
|
|
add_status_str((self.mp.media_title or b'').decode(), x=22,
|
|
|
|
attr=curses.color_pair(12)|curses.A_BOLD)
|
|
|
|
add_status_str(_(self.mode), x=-5-len(_(self.mode)))
|
|
|
|
if not self.mp.mute: add_status_str('A', x=-4, X=-3)
|
|
|
|
if self.vid != 'no': add_status_str('V', x=-2, lpad=0)
|
|
|
|
if message: self.adds(message, curses.LINES-1, attr=msgattr, lpad=0)
|
2017-05-01 11:19:49 +02:00
|
|
|
self.scr.refresh()
|
2017-04-27 16:36:10 +02:00
|
|
|
|
2017-05-07 10:59:54 +02:00
|
|
|
def setno(self, *keys):
|
|
|
|
"""Set all keys of each entry in entries to False."""
|
|
|
|
for entry in self.entries:
|
|
|
|
for key in keys:
|
|
|
|
entry[key] = False
|
2017-04-24 06:13:12 +02:00
|
|
|
|
2017-05-01 11:19:49 +02:00
|
|
|
def play(self, force=False):
|
|
|
|
"""Play the next track."""
|
|
|
|
def mpv_play(entry, force):
|
2017-04-27 16:36:10 +02:00
|
|
|
self.setno('playing')
|
|
|
|
entry['playing'] = True
|
2017-05-07 10:59:54 +02:00
|
|
|
self.mp.vid = self.vid
|
2017-05-01 11:19:49 +02:00
|
|
|
try:
|
2017-05-29 15:38:09 +02:00
|
|
|
self.mp.play(entry['filename'])
|
2017-05-01 11:19:49 +02:00
|
|
|
except:
|
|
|
|
entry['error'] = True
|
2017-05-07 10:59:54 +02:00
|
|
|
self.print(entry)
|
|
|
|
if force: self.mp.pause = False
|
2017-05-01 11:19:49 +02:00
|
|
|
self.mp.wait_for_playback()
|
2017-05-15 06:02:56 +02:00
|
|
|
self.play()
|
2017-05-01 11:19:49 +02:00
|
|
|
entry['playing'] = False
|
2017-05-07 10:59:54 +02:00
|
|
|
self.print(entry)
|
2017-05-01 11:19:49 +02:00
|
|
|
|
|
|
|
if self.play_backward and -self.playing < len(self.played):
|
|
|
|
self.playing -= 1
|
|
|
|
t = self.played[self.playing], force
|
|
|
|
elif self.playing < -1:
|
|
|
|
self.playing += 1
|
|
|
|
t = self.played[self.playing], force
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
self.played.append(next(self.playlist))
|
|
|
|
except StopIteration:
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
t = self.played[-1], force
|
|
|
|
|
|
|
|
self.play_backward = False
|
|
|
|
play_thread = Thread(target=mpv_play, args=t, daemon=True)
|
|
|
|
play_thread.start()
|
|
|
|
|
2017-05-07 10:59:54 +02:00
|
|
|
def _writeln(self, y, title, duration, attr):
|
2017-06-12 08:37:42 +02:00
|
|
|
title_len = curses.COLS - DURATION_COL_LEN - 3
|
|
|
|
self.adds(title, y, attr=attr)
|
|
|
|
self.adds(duration, y, x=title_len+1, attr=attr)
|
2017-05-07 10:59:54 +02:00
|
|
|
self.scr.refresh()
|
2017-04-27 16:36:10 +02:00
|
|
|
|
2017-05-07 10:59:54 +02:00
|
|
|
def print(self, entry=None, y=None):
|
|
|
|
"""Print the entry in the line y."""
|
|
|
|
if entry is y is None:
|
|
|
|
entry = self.current()
|
|
|
|
y = self.idx() - self.start + 1
|
|
|
|
elif entry is None:
|
|
|
|
entry = self.entries[self.start + y - 1]
|
|
|
|
elif y is None:
|
|
|
|
y = self.idx(entry) - self.start + 1
|
|
|
|
if y < 1 or y > curses.LINES - 3: return
|
|
|
|
|
2017-05-01 11:19:49 +02:00
|
|
|
c = {'error': 1, 'playing': 3, 'selected': 5}
|
|
|
|
color = ((8 if entry is self.current() else 0)
|
|
|
|
| reduce(int.__xor__, (c.get(i, 0) for i in entry if entry[i])))
|
|
|
|
if color:
|
2017-05-29 15:38:09 +02:00
|
|
|
self._writeln(y, entry['title'], entry['duration'],
|
2017-05-07 10:59:54 +02:00
|
|
|
curses.color_pair(color) | curses.A_BOLD)
|
2017-04-24 06:13:12 +02:00
|
|
|
else:
|
2017-05-29 15:38:09 +02:00
|
|
|
self._writeln(y, entry['title'], entry['duration'],
|
2017-05-07 10:59:54 +02:00
|
|
|
curses.A_NORMAL)
|
2017-05-01 11:19:49 +02:00
|
|
|
|
|
|
|
def redraw(self):
|
|
|
|
"""Redraw the whole screen."""
|
2017-05-07 10:59:54 +02:00
|
|
|
self._writeln(0, _("Title"), _("Duration"),
|
|
|
|
curses.color_pair(10) | curses.A_BOLD)
|
2017-04-27 16:36:10 +02:00
|
|
|
for i, entry in enumerate(self.entries[self.start:][:curses.LINES-3]):
|
2017-05-07 10:59:54 +02:00
|
|
|
self.print(entry, i + 1)
|
|
|
|
self.scr.clrtobot()
|
2017-04-27 16:36:10 +02:00
|
|
|
self.update_status()
|
2017-04-24 06:13:12 +02:00
|
|
|
|
2017-05-29 15:38:09 +02:00
|
|
|
def __init__(self, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf):
|
|
|
|
Omp.__init__(self, entries, lambda name, val: self.update_status(),
|
2017-06-10 17:14:15 +02:00
|
|
|
json_file, mode, mpv_vid, mpv_vo, ytdlf)
|
2017-05-01 11:19:49 +02:00
|
|
|
curses.noecho()
|
|
|
|
curses.cbreak()
|
|
|
|
self.scr.keypad(True)
|
|
|
|
curses.curs_set(False)
|
|
|
|
curses.start_color()
|
|
|
|
curses.use_default_colors()
|
|
|
|
for i in range(1, 8): curses.init_pair(i, i, -1)
|
|
|
|
curses.init_pair(8, -1, 7)
|
|
|
|
for i in range(1, 7): curses.init_pair(i + 8, -1, i)
|
|
|
|
self.redraw()
|
|
|
|
|
|
|
|
def __enter__(self): return self
|
|
|
|
|
2017-05-07 10:59:54 +02:00
|
|
|
def idx(self, entry=None):
|
2017-05-01 11:19:49 +02:00
|
|
|
"""Return the index of the current entry."""
|
2017-05-07 10:59:54 +02:00
|
|
|
if entry is None:
|
|
|
|
return self.start + self.y - 1
|
|
|
|
return self.entries.index(entry)
|
2017-05-01 11:19:49 +02:00
|
|
|
|
|
|
|
def current(self):
|
|
|
|
"""Return the current entry."""
|
|
|
|
try:
|
|
|
|
return self.entries[self.idx()]
|
|
|
|
except:
|
|
|
|
return {}
|
|
|
|
|
2017-05-07 10:59:54 +02:00
|
|
|
def gets(self, prompt):
|
|
|
|
"""Print the prompt string at the bottom of the screen then read
|
|
|
|
from standard input.
|
|
|
|
"""
|
2017-06-12 08:37:42 +02:00
|
|
|
self.adds(prompt, curses.LINES - 1, lpad=0)
|
2017-05-07 10:59:54 +02:00
|
|
|
self.reading = True
|
|
|
|
curses.curs_set(True)
|
|
|
|
curses.echo()
|
|
|
|
b = self.scr.getstr(curses.LINES - 1, len(prompt))
|
|
|
|
self.reading = False
|
|
|
|
curses.curs_set(False)
|
|
|
|
curses.noecho()
|
|
|
|
return b.decode()
|
|
|
|
|
|
|
|
def seek(self, amount, reference='relative', precision='default-precise'):
|
|
|
|
"""Wrap mp.seek with a try clause to avoid crash when nothing is
|
|
|
|
being played.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
self.mp.seek(amount, reference, precision)
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
2017-04-24 06:13:12 +02:00
|
|
|
def move(self, delta):
|
2017-05-01 11:19:49 +02:00
|
|
|
"""Move to the relatively next delta entry."""
|
2017-04-27 16:36:10 +02:00
|
|
|
if not (self.entries and delta): return
|
2017-05-07 10:59:54 +02:00
|
|
|
start, prev_entry = self.start, self.current()
|
2017-05-01 11:19:49 +02:00
|
|
|
maxy = min(len(self.entries), curses.LINES - 3)
|
2017-04-27 16:36:10 +02:00
|
|
|
|
|
|
|
if self.idx() + delta <= 0:
|
|
|
|
self.start, self.y = 0, 1
|
|
|
|
elif self.idx() + delta >= len(self.entries):
|
|
|
|
self.start, self.y = len(self.entries) - maxy, maxy
|
|
|
|
elif self.y + delta < 1:
|
|
|
|
self.start += self.y + delta - 1
|
|
|
|
self.y = 1
|
|
|
|
elif self.y + delta > curses.LINES - 3:
|
|
|
|
self.start += self.y + delta - maxy
|
|
|
|
self.y = maxy
|
2017-04-24 06:13:12 +02:00
|
|
|
else:
|
2017-04-27 16:36:10 +02:00
|
|
|
self.y += delta
|
|
|
|
|
|
|
|
if self.start == start:
|
2017-05-07 10:59:54 +02:00
|
|
|
self.print(prev_entry)
|
|
|
|
self.print()
|
2017-04-27 16:36:10 +02:00
|
|
|
else:
|
2017-05-01 11:19:49 +02:00
|
|
|
self.redraw()
|
2017-04-27 16:36:10 +02:00
|
|
|
|
2017-05-07 10:59:54 +02:00
|
|
|
def search(self, backward=False):
|
|
|
|
"""Prompt then search for a pattern."""
|
|
|
|
p = re.compile(self.gets('/'), re.IGNORECASE)
|
|
|
|
entries = deque(self.entries)
|
|
|
|
entries.rotate(-self.idx())
|
|
|
|
self.search_res = deque(filter(
|
|
|
|
lambda entry: p.search(entry['title']) is not None, entries))
|
|
|
|
if backward: self.search_res.reverse()
|
|
|
|
if self.search_res:
|
|
|
|
self.move(self.idx(self.search_res[0]) - self.idx())
|
|
|
|
else:
|
|
|
|
self.update_status(_("Pattern not found"), curses.color_pair(1))
|
2017-04-27 16:36:10 +02:00
|
|
|
|
2017-05-07 10:59:54 +02:00
|
|
|
def next_search(self, backward=False):
|
|
|
|
"""Repeat previous search."""
|
|
|
|
if self.search_res:
|
|
|
|
self.search_res.rotate(1 if backward else -1)
|
|
|
|
self.move(self.idx(self.search_res[0]) - self.idx())
|
|
|
|
else:
|
|
|
|
self.update_status(_("Pattern not found"), curses.color_pair(1))
|
|
|
|
|
|
|
|
def resize(self):
|
|
|
|
curses.update_lines_cols()
|
2017-06-12 08:37:42 +02:00
|
|
|
self.scr.clear()
|
2017-05-07 10:59:54 +02:00
|
|
|
l = curses.LINES - 3
|
2017-06-12 08:37:42 +02:00
|
|
|
if curses.COLS < MODE_STR_LEN + 42 or l < 1: # too small
|
2017-05-07 10:59:54 +02:00
|
|
|
sizeerr = _("Current size: {}x{}. Minimum size: {}x4.").format(
|
|
|
|
curses.COLS, curses.LINES, MODE_STR_LEN + 42)
|
|
|
|
self.scr.addstr(0, 0, sizeerr[:curses.LINES*curses.COLS-1])
|
2017-06-12 08:37:42 +02:00
|
|
|
self.scr.refresh()
|
|
|
|
elif self.y > l: # shorter than the current entry
|
2017-05-07 10:59:54 +02:00
|
|
|
self.start += self.y - l
|
|
|
|
self.y = l
|
|
|
|
self.redraw()
|
2017-06-12 08:37:42 +02:00
|
|
|
elif 0 < self.start > len(self.entries) - l: # longer than the list
|
|
|
|
idx, self.start = self.idx(), min(0, len(self.entries) - l)
|
2017-05-07 10:59:54 +02:00
|
|
|
self.y = idx - self.start + 1
|
|
|
|
if self.y > l:
|
|
|
|
self.start += self.y - l
|
|
|
|
self.y = l
|
|
|
|
self.redraw()
|
|
|
|
else:
|
|
|
|
self.redraw()
|
2017-03-20 14:49:11 +01:00
|
|
|
|
2017-05-01 11:19:49 +02:00
|
|
|
def __exit__(self, exc_type, exc_value, traceback):
|
2017-04-24 06:13:12 +02:00
|
|
|
curses.nocbreak()
|
2017-05-01 11:19:49 +02:00
|
|
|
self.scr.keypad(False)
|
2017-04-24 06:13:12 +02:00
|
|
|
curses.echo()
|
|
|
|
curses.endwin()
|
2017-05-28 16:02:34 +02:00
|
|
|
Omp.__exit__(self, exc_type, exc_value, traceback)
|
2017-03-20 14:49:11 +01:00
|
|
|
|
|
|
|
|
2017-04-06 18:02:54 +02:00
|
|
|
parser = ArgumentParser(description=_("Curses Online Media Player"))
|
2017-05-29 15:38:09 +02:00
|
|
|
parser.add_argument('-e', '--extractor', default='youtube-dl',
|
|
|
|
choices=('json', 'mpv', 'youtube-dl'), required=False,
|
|
|
|
help=_("playlist extractor, default is youtube-dl"))
|
|
|
|
parser.add_argument('file', help=_("path or URL to the playlist to be opened"))
|
2017-06-10 17:14:15 +02:00
|
|
|
parser.add_argument('-c', '--config', default=USER_CONFIG, required=False,
|
2017-05-29 15:38:09 +02:00
|
|
|
help=_("path to the configuration file"))
|
|
|
|
parser.add_argument('--vid', required=False,
|
2017-05-07 10:59:54 +02:00
|
|
|
help=_("initial video channel. auto selects the default,\
|
|
|
|
no disables video"))
|
|
|
|
parser.add_argument('--vo', required=False, metavar='DRIVER',
|
|
|
|
help=_("specify the video output backend to be used. See\
|
|
|
|
VIDEO OUTPUT DRIVERS in mpv(1) man page for\
|
|
|
|
details and descriptions of available drivers"))
|
|
|
|
parser.add_argument('-f', '--format', required=False, metavar='YTDL_FORMAT',
|
|
|
|
help=_("video format/quality to be passed to youtube-dl"))
|
2017-03-20 14:49:11 +01:00
|
|
|
args = parser.parse_args()
|
2017-05-29 15:38:09 +02:00
|
|
|
entries = extract_info(args.file, args.extractor)
|
|
|
|
json_file = args.file if args.extractor == 'json' else ''
|
2017-03-23 15:37:52 +01:00
|
|
|
config = ConfigParser()
|
2017-06-10 17:14:15 +02:00
|
|
|
config.read(args.config)
|
|
|
|
vid = args.vid or config.get('mpv', 'video', fallback='auto')
|
|
|
|
vo = args.vo or config.get('mpv', 'video-output', fallback=None)
|
2017-05-07 10:59:54 +02:00
|
|
|
mode = config.get('comp', 'play-mode', fallback='play-current')
|
2017-06-10 17:14:15 +02:00
|
|
|
ytdlf = args.format or config.get('youtube-dl', 'format', fallback='best')
|
2017-03-24 15:57:19 +01:00
|
|
|
|
2017-05-29 15:38:09 +02:00
|
|
|
with Comp(entries, json_file, mode, vid, vo, ytdlf) as comp:
|
2017-05-01 11:19:49 +02:00
|
|
|
c = comp.scr.getch()
|
|
|
|
while c != 113: # letter q
|
|
|
|
if c == 10: # curses.KEY_ENTER doesn't work
|
|
|
|
comp.update_playlist()
|
2017-05-07 10:59:54 +02:00
|
|
|
comp.next(force=True)
|
2017-05-01 11:19:49 +02:00
|
|
|
elif c == 32: # space
|
2017-05-07 10:59:54 +02:00
|
|
|
comp.current()['selected'] = not comp.current().get('selected')
|
2017-05-01 11:19:49 +02:00
|
|
|
comp.move(1)
|
2017-05-07 10:59:54 +02:00
|
|
|
elif c == 47: # /
|
|
|
|
comp.search()
|
2017-05-01 11:19:49 +02:00
|
|
|
elif c == 60: # <
|
|
|
|
try:
|
2017-05-07 10:59:54 +02:00
|
|
|
if comp.mp._get_property('time-pos', float) < 1:
|
|
|
|
comp.next(backward=True)
|
2017-05-01 11:19:49 +02:00
|
|
|
else:
|
|
|
|
comp.seek(0, 'absolute')
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
elif c == 62: # >
|
2017-05-07 10:59:54 +02:00
|
|
|
comp.next()
|
|
|
|
elif c == 63: # ?
|
|
|
|
comp.search(backward=True)
|
2017-05-01 11:19:49 +02:00
|
|
|
elif c == 65: # letter A
|
2017-05-15 06:02:56 +02:00
|
|
|
comp.mp.mute ^= True # hack to toggle bool value
|
2017-05-01 11:19:49 +02:00
|
|
|
elif c == 77: # letter M
|
|
|
|
comp.mode = MODES[(MODES.index(comp.mode) - 1) % 8]
|
|
|
|
comp.update_status()
|
2017-05-07 10:59:54 +02:00
|
|
|
elif c == 78: # letter N
|
|
|
|
comp.next_search(backward=True)
|
2017-05-01 11:19:49 +02:00
|
|
|
elif c == 86: # letter V
|
|
|
|
comp.vid = 'auto' if comp.vid == 'no' else 'no'
|
2017-05-07 10:59:54 +02:00
|
|
|
comp.mp.vid = comp.vid
|
2017-05-01 11:19:49 +02:00
|
|
|
comp.update_status()
|
|
|
|
elif c == 87: # letter W
|
2017-05-07 10:59:54 +02:00
|
|
|
s = comp.gets(_("Save playlist to [{}]: ").format(comp.json_file))
|
|
|
|
if s: comp.json_file = s
|
2017-05-01 11:19:49 +02:00
|
|
|
try:
|
2017-05-07 10:59:54 +02:00
|
|
|
makedirs(dirname(abspath(comp.json_file)), exist_ok=True)
|
2017-06-10 17:14:15 +02:00
|
|
|
with open(comp.json_file, 'w') as f:
|
|
|
|
json.dump(comp.entries, f, ensure_ascii=False,
|
|
|
|
indent=2, sort_keys=True)
|
2017-05-01 11:19:49 +02:00
|
|
|
except:
|
2017-05-07 10:59:54 +02:00
|
|
|
errmsg = _("'{}': Can't open file for writing").format(
|
|
|
|
comp.json_file)
|
|
|
|
comp.update_status(errmsg, curses.color_pair(1))
|
2017-05-01 11:19:49 +02:00
|
|
|
else:
|
2017-05-07 10:59:54 +02:00
|
|
|
comp.update_status(_("'{}' written").format(comp.json_file))
|
2017-05-01 11:19:49 +02:00
|
|
|
elif c == 100: # letter d
|
2017-06-10 17:14:15 +02:00
|
|
|
comp.entries.pop(comp.idx())
|
|
|
|
if 1 < len(comp.entries) - curses.LINES + 4 == comp.start:
|
|
|
|
comp.start -= 1
|
|
|
|
elif comp.idx() == len(comp.entries):
|
|
|
|
comp.y -= 1
|
2017-05-01 11:19:49 +02:00
|
|
|
comp.redraw()
|
2017-06-10 17:14:15 +02:00
|
|
|
elif c == 105: # letter i
|
|
|
|
extractor = comp.gets(_("Playlist extractor: "))
|
|
|
|
filename = comp.gets(_("Insert: "))
|
|
|
|
entries = extract_info(filename, extractor)
|
|
|
|
if entries is None:
|
|
|
|
comp.update_status(
|
|
|
|
_("'{}': Can't extract playlist").format(filename))
|
|
|
|
else:
|
|
|
|
bottom = comp.entries[comp.idx():]
|
|
|
|
comp.entries = comp.entries[:comp.idx()]
|
|
|
|
comp.entries.extend(entries)
|
|
|
|
comp.entries.extend(bottom)
|
|
|
|
comp.redraw()
|
2017-05-01 11:19:49 +02:00
|
|
|
elif c == 109: # letter m
|
|
|
|
comp.mode = MODES[(MODES.index(comp.mode) + 1) % 8]
|
|
|
|
comp.update_status()
|
2017-05-07 10:59:54 +02:00
|
|
|
elif c == 110: # letter n
|
|
|
|
comp.next_search()
|
2017-05-29 15:38:09 +02:00
|
|
|
elif c == 111: # letter o
|
|
|
|
extractor = comp.gets(_("Playlist extractor: "))
|
2017-06-10 17:14:15 +02:00
|
|
|
filename = comp.gets(_("Open: "))
|
|
|
|
entries = extract_info(filename, extractor)
|
|
|
|
if entries is None:
|
|
|
|
comp.update_status(
|
|
|
|
_("'{}': Can't extract playlist").format(filename))
|
|
|
|
else:
|
|
|
|
comp.entries, comp.start, comp.y = entries, 0, 1
|
|
|
|
comp.redraw()
|
2017-05-29 15:38:09 +02:00
|
|
|
elif c == 112: # letter p
|
|
|
|
comp.mp.pause ^= True
|
2017-05-01 11:19:49 +02:00
|
|
|
elif c in (curses.KEY_UP, 107): # up arrow or letter k
|
|
|
|
comp.move(-1)
|
|
|
|
elif c in (curses.KEY_DOWN, 106): # down arrow or letter j
|
|
|
|
comp.move(1)
|
|
|
|
elif c in (curses.KEY_LEFT, 104): # left arrow or letter h
|
2017-05-07 10:59:54 +02:00
|
|
|
comp.seek(-5, precision='exact')
|
2017-05-01 11:19:49 +02:00
|
|
|
elif c in (curses.KEY_RIGHT, 108): # right arrow or letter l
|
2017-05-07 10:59:54 +02:00
|
|
|
comp.seek(5, precision='exact')
|
2017-05-01 11:19:49 +02:00
|
|
|
elif c == curses.KEY_HOME: # home
|
|
|
|
comp.move(-len(comp.entries))
|
|
|
|
elif c == curses.KEY_END: # end
|
|
|
|
comp.move(len(comp.entries))
|
|
|
|
elif c == curses.KEY_NPAGE: # page down
|
|
|
|
comp.move(curses.LINES - 4)
|
|
|
|
elif c == curses.KEY_PPAGE: # page up
|
|
|
|
comp.move(4 - curses.LINES)
|
2017-05-07 10:59:54 +02:00
|
|
|
elif c in (curses.KEY_F5, curses.KEY_RESIZE):
|
|
|
|
comp.resize()
|
2017-05-01 11:19:49 +02:00
|
|
|
c = comp.scr.getch()
|