Update documentations and clean up (?)

This commit is contained in:
Nguyễn Gia Phong 2017-06-25 17:38:19 +07:00
parent 21937f383c
commit 0066d60a4e
5 changed files with 309 additions and 133 deletions

View file

@ -2,17 +2,21 @@
comp - Curses Omni Media Player comp - Curses Omni Media Player
=============================== ===============================
This program is a curses front-end for mpv and youtube-dl. comp is a mpv front-end using curses. It has basic media player functions and
can to extract playlists from multiple sources such as media sites supported by
youtube-dl, local and direct URL to video/audio and its own JSON playlist
format.
.. image:: doc/screenshot.png .. image:: https://github.com/McSinyx/comp/raw/master/doc/screenshot.png
Installation Installation
------------ ------------
comp requires Python 3.5+ with ``curses`` module (only available on Unix-like comp requires Python 3.5+ with ``curses`` module (only available on Unix-like
OSes such as GNU/Linux and the BSDs) and ``libmpv``. It also depends on OSes such as GNU/Linux and the BSDs) and ``libmpv`` (available as ``libmpv1``
``python-mpv`` and ``youtube-dl`` but the setup program will automatically in Debian/Ubuntu, openSUSE; and as ``mpv`` in Arch Linux, Gentoo, macOS
install them if they are missing. Homebrew repository). It also depends on ``python-mpv`` and ``youtube-dl`` but
the setup program will automatically install them if they are missing.
Using pip Using pip
^^^^^^^^^ ^^^^^^^^^
@ -24,35 +28,31 @@ installation.
Using setup.py Using setup.py
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
To install the latest version or test the development branch (called To install the latest version or test development branches, you'll need to do
``bachelor``, in contrast to ``master``), you'll need to do it manually:: it manually::
git clone https://github.com/McSinyx/comp.git git clone https://github.com/McSinyx/comp.git
cd comp cd comp
git checkout bachelor # usually master is synced with the PyPI repo ./setup.py install
./setup.py install -e .
Note ``setup.py`` uses ``setuptools`` which is a third-party module and can be Note that ``setup.py`` uses ``setuptools`` which is a third-party module and
install using ``pip3``. can be install using ``pip3``.
Usage Command line options
----- --------------------
Command line arguments
^^^^^^^^^^^^^^^^^^^^^^
:: ::
$ comp --help $ comp --help
usage: comp [-h] [-e {json,mpv,youtube-dl}] [-c CONFIG] [--vid VID] usage: comp [-h] [-e {json,mpv,youtube-dl}] [-c CONFIG] [--vid VID]
[--vo DRIVER] [-f YTDL_FORMAT] [--vo DRIVER] [-f YTDL_FORMAT]
file playlist
Curses Online Media Player Curses Omni Media Player
positional arguments: positional arguments:
file path or URL to the playlist to be opened playlist path or URL to the playlist
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-e {json,mpv,youtube-dl}, --extractor {json,mpv,youtube-dl} -e {json,mpv,youtube-dl}, --extractor {json,mpv,youtube-dl}
@ -61,66 +61,75 @@ Command line arguments
path to the configuration file path to the configuration file
--vid VID initial video channel. auto selects the default, no --vid VID initial video channel. auto selects the default, no
disables video disables video
--vo DRIVER specify the video output backend to be used. See VIDEO --vo DRIVER specify the video output backend to be used. See
OUTPUT DRIVERS in mpv(1) man page for details and VIDEO OUTPUT DRIVERS in mpv(1) for details and
descriptions of available drivers descriptions of available drivers
-f YTDL_FORMAT, --format YTDL_FORMAT -f YTDL_FORMAT, --format YTDL_FORMAT
video format/quality to be passed to youtube-dl video format/quality to be passed to youtube-dl
Keyboard control Examples
^^^^^^^^^^^^^^^^ ^^^^^^^^
+--------------+---------------------------------------------+ Open a JSON playlist::
| Key | Action |
+==============+=============================================+ comp -e json test/playlist.json
| Return | Start playing |
+--------------+---------------------------------------------+ Open a Youtube playlist with video height lower than 720::
| Space | Select the current track |
+--------------+---------------------------------------------+ comp -f [height<720] https://www.youtube.com/watch?v=pqkHrdYXaTk&list=PLnk14Iku8QM7R3ARnrj1TwYSZleF-i7jT
| ``/``, ``?`` | Search forward/backward for a pattern |
+--------------+---------------------------------------------+ Keyboard control
| ``<``, ``>`` | Go forward/backward in the playlist | ----------------
+--------------+---------------------------------------------+
| ``A`` | Toggle mute | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | Key | Action |
| ``N`` | Repeat previous search in reverse direction | +==============+==============================================+
+--------------+---------------------------------------------+ | Return | Start playing |
| ``V`` | Toggle video | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | Space | Select the current track |
| ``W`` | Save the current playlist under JSON format | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | ``/``, ``?`` | Search forward/backward for a pattern |
| ``d`` | Delete current entry | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | ``<``, ``>`` | Go backward/forward in the playlist |
| ``i`` | Insert playlist | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | ``A`` | Toggle mute |
| ``m``, ``M`` | Cycle through playing modes | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | ``N`` | Repeat previous search in reverse direction |
| ``n`` | Repeat previous search | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | ``V`` | Toggle video |
| ``p`` | Toggle pause | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | ``W`` | Save the current playlist under JSON format |
| ``o`` | Open playlist | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | ``d`` | Delete current entry |
| ``w`` | Download tracks set by playing mode | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | ``i`` | Insert playlist |
| Up, ``k`` | Move a single line up | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | ``m``, ``M`` | Cycle forward/backward through playing modes |
| Down, ``j`` | Move a single line down | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | ``n`` | Repeat previous search |
| Left, ``h`` | Seek backward 5 seconds | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | ``p`` | Toggle pause |
| Right, ``l`` | Seek forward 5 seconds | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | ``o`` | Open playlist |
| Home | Move to the beginning of the playlist | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | Up, ``k`` | Move a single line up |
| End | Move to the end of the playlist | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | Down, ``j`` | Move a single line down |
| Page Up | Move a single page up | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | Left, ``h`` | Seek backward 5 seconds |
| Page Down | Move a single page down | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | Right, ``l`` | Seek forward 5 seconds |
| F5 | Redraw the screen content | +--------------+----------------------------------------------+
+--------------+---------------------------------------------+ | Home | Move to the beginning of the playlist |
+--------------+----------------------------------------------+
| End | Move to the end of the playlist |
+--------------+----------------------------------------------+
| Page Up | Move a single page up |
+--------------+----------------------------------------------+
| Page Down | Move a single page down |
+--------------+----------------------------------------------+
| F5 | Redraw the screen content |
+--------------+----------------------------------------------+
Configuration files Configuration files
------------------- -------------------
@ -146,3 +155,12 @@ are listed below::
# Video format/quality to be passed to youtube-dl. See FORMAT SELECTION in # Video format/quality to be passed to youtube-dl. See FORMAT SELECTION in
# youtube-dl(1) man page for more details and descriptions. # youtube-dl(1) man page for more details and descriptions.
format = best format = best
Bugs
----
Media durations are not extracted from online playlists as
``youtube-dl.YoutubeDL`` option ``extract_flat`` is set to ``'in_playlist'``.
This is rather a feature to save up bandwidth than a bug because a track's
duration is updated when it's played.

75
comp
View file

@ -25,7 +25,7 @@ from configparser import ConfigParser
from functools import reduce from functools import reduce
from gettext import bindtextdomain, gettext as _, textdomain from gettext import bindtextdomain, gettext as _, textdomain
from os import makedirs from os import makedirs
from os.path import abspath, dirname, expanduser from os.path import abspath, dirname, expanduser, expandvars
from threading import Thread from threading import Thread
from mpv import MPV from mpv import MPV
@ -111,7 +111,11 @@ class Comp(Omp):
add_status_str(_(self.mode), x=-5-len(_(self.mode))) add_status_str(_(self.mode), x=-5-len(_(self.mode)))
if not self.mp.mute: add_status_str('A', x=-4, X=-3) 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 self.vid != 'no': add_status_str('V', x=-2, lpad=0)
if message: self.adds(message, curses.LINES-1, attr=msgattr, lpad=0) self.scr.refresh()
def print_msg(self, message, error=False):
attributes = curses.color_pair(1) if error else curses.A_NORMAL
self.adds(message, curses.LINES-1, attr=attributes, lpad=0)
self.scr.refresh() self.scr.refresh()
def setno(self, *keys): def setno(self, *keys):
@ -182,7 +186,7 @@ class Comp(Omp):
self._writeln(y, entry['title'], entry['duration'], self._writeln(y, entry['title'], entry['duration'],
curses.A_NORMAL) curses.A_NORMAL)
def redraw(self): def refresh(self):
"""Redraw the whole screen.""" """Redraw the whole screen."""
self._writeln(0, _("Title"), _("Duration"), self._writeln(0, _("Title"), _("Duration"),
curses.color_pair(10) | curses.A_BOLD) curses.color_pair(10) | curses.A_BOLD)
@ -191,9 +195,10 @@ class Comp(Omp):
self.scr.clrtobot() self.scr.clrtobot()
self.update_status() self.update_status()
def property_handler(self, name, val): self.update_status()
def __init__(self, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf): def __init__(self, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf):
Omp.__init__(self, entries, lambda name, val: self.update_status(), Omp.__init__(self, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf)
json_file, mode, mpv_vid, mpv_vo, ytdlf)
curses.noecho() curses.noecho()
curses.cbreak() curses.cbreak()
self.scr.keypad(True) self.scr.keypad(True)
@ -203,7 +208,7 @@ class Comp(Omp):
for i in range(1, 8): curses.init_pair(i, i, -1) for i in range(1, 8): curses.init_pair(i, i, -1)
curses.init_pair(8, -1, 7) curses.init_pair(8, -1, 7)
for i in range(1, 7): curses.init_pair(i + 8, -1, i) for i in range(1, 7): curses.init_pair(i + 8, -1, i)
self.redraw() self.refresh()
def __enter__(self): return self def __enter__(self): return self
@ -220,7 +225,7 @@ class Comp(Omp):
except: except:
return {} return {}
def gets(self, prompt): def read_input(self, prompt):
"""Print the prompt string at the bottom of the screen then read """Print the prompt string at the bottom of the screen then read
from standard input. from standard input.
""" """
@ -266,11 +271,11 @@ class Comp(Omp):
self.print(prev_entry) self.print(prev_entry)
self.print() self.print()
else: else:
self.redraw() self.refresh()
def search(self, backward=False): def search(self, backward=False):
"""Prompt then search for a pattern.""" """Prompt then search for a pattern."""
p = re.compile(self.gets('/'), re.IGNORECASE) p = re.compile(self.read_input('/'), re.IGNORECASE)
entries = deque(self.entries) entries = deque(self.entries)
entries.rotate(-self.idx()) entries.rotate(-self.idx())
self.search_res = deque(filter( self.search_res = deque(filter(
@ -279,7 +284,7 @@ class Comp(Omp):
if self.search_res: if self.search_res:
self.move(self.idx(self.search_res[0]) - self.idx()) self.move(self.idx(self.search_res[0]) - self.idx())
else: else:
self.update_status(_("Pattern not found"), curses.color_pair(1)) self.print_msg(_("Pattern not found"), error=True)
def next_search(self, backward=False): def next_search(self, backward=False):
"""Repeat previous search.""" """Repeat previous search."""
@ -287,7 +292,7 @@ class Comp(Omp):
self.search_res.rotate(1 if backward else -1) self.search_res.rotate(1 if backward else -1)
self.move(self.idx(self.search_res[0]) - self.idx()) self.move(self.idx(self.search_res[0]) - self.idx())
else: else:
self.update_status(_("Pattern not found"), curses.color_pair(1)) self.print_msg(_("Pattern not found"), error=True)
def resize(self): def resize(self):
curses.update_lines_cols() curses.update_lines_cols()
@ -301,16 +306,16 @@ class Comp(Omp):
elif self.y > l: # shorter than the current entry elif self.y > l: # shorter than the current entry
self.start += self.y - l self.start += self.y - l
self.y = l self.y = l
self.redraw() self.refresh()
elif 0 < self.start > len(self.entries) - l: # longer than the list elif 0 < self.start > len(self.entries) - l: # longer than the list
idx, self.start = self.idx(), min(0, len(self.entries) - l) idx, self.start = self.idx(), min(0, len(self.entries) - l)
self.y = idx - self.start + 1 self.y = idx - self.start + 1
if self.y > l: if self.y > l:
self.start += self.y - l self.start += self.y - l
self.y = l self.y = l
self.redraw() self.refresh()
else: else:
self.redraw() self.refresh()
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, exc_type, exc_value, traceback):
curses.nocbreak() curses.nocbreak()
@ -324,7 +329,7 @@ parser = ArgumentParser(description=_("Curses Omni Media Player"))
parser.add_argument('-e', '--extractor', default='youtube-dl', parser.add_argument('-e', '--extractor', default='youtube-dl',
choices=('json', 'mpv', 'youtube-dl'), required=False, choices=('json', 'mpv', 'youtube-dl'), required=False,
help=_("playlist extractor, default is youtube-dl")) help=_("playlist extractor, default is youtube-dl"))
parser.add_argument('file', help=_("path or URL to the playlist to be opened")) parser.add_argument('playlist', help=_("path or URL to the playlist"))
parser.add_argument('-c', '--config', default=USER_CONFIG, required=False, parser.add_argument('-c', '--config', default=USER_CONFIG, required=False,
help=_("path to the configuration file")) help=_("path to the configuration file"))
parser.add_argument('--vid', required=False, parser.add_argument('--vid', required=False,
@ -332,16 +337,16 @@ parser.add_argument('--vid', required=False,
no disables video")) no disables video"))
parser.add_argument('--vo', required=False, metavar='DRIVER', parser.add_argument('--vo', required=False, metavar='DRIVER',
help=_("specify the video output backend to be used. See\ help=_("specify the video output backend to be used. See\
VIDEO OUTPUT DRIVERS in mpv(1) man page for\ VIDEO OUTPUT DRIVERS in mpv(1) for details and\
details and descriptions of available drivers")) descriptions of available drivers"))
parser.add_argument('-f', '--format', required=False, metavar='YTDL_FORMAT', parser.add_argument('-f', '--format', required=False, metavar='YTDL_FORMAT',
help=_("video format/quality to be passed to youtube-dl")) help=_("video format/quality to be passed to youtube-dl"))
args = parser.parse_args() args = parser.parse_args()
entries = extract_info(args.file, args.extractor) entries = extract_info(args.playlist, args.extractor)
if entries is None: if entries is None:
print(_("'{}': Can't extract playlist").format(args.file)) print(_("'{}': Can't extract playlist").format(args.file))
exit() exit()
json_file = args.file if args.extractor == 'json' else '' json_file = args.playlist if args.extractor == 'json' else ''
config = ConfigParser() config = ConfigParser()
config.read(args.config) config.read(args.config)
vid = args.vid or config.get('mpv', 'video', fallback='auto') vid = args.vid or config.get('mpv', 'video', fallback='auto')
@ -384,54 +389,42 @@ with Comp(entries, json_file, mode, vid, vo, ytdlf) as comp:
comp.mp.vid = comp.vid comp.mp.vid = comp.vid
comp.update_status() comp.update_status()
elif c == 87: # letter W elif c == 87: # letter W
s = comp.gets(_("Save playlist to [{}]: ").format(comp.json_file)) comp.dump_json()
if s: comp.json_file = s
try:
makedirs(dirname(abspath(comp.json_file)), exist_ok=True)
with open(comp.json_file, 'w') as f:
json.dump(comp.entries, f, ensure_ascii=False,
indent=2, sort_keys=True)
except:
errmsg = _("'{}': Can't open file for writing").format(
comp.json_file)
comp.update_status(errmsg, curses.color_pair(1))
else:
comp.update_status(_("'{}' written").format(comp.json_file))
elif c == 100: # letter d elif c == 100: # letter d
comp.entries.pop(comp.idx()) comp.entries.pop(comp.idx())
if 1 < len(comp.entries) - curses.LINES + 4 == comp.start: if 1 < len(comp.entries) - curses.LINES + 4 == comp.start:
comp.start -= 1 comp.start -= 1
elif comp.idx() == len(comp.entries): elif comp.idx() == len(comp.entries):
comp.y -= 1 comp.y -= 1
comp.redraw() comp.refresh()
elif c == 105: # letter i elif c == 105: # letter i
extractor = comp.gets(_("Playlist extractor: ")) extractor = comp.read_input(_("Playlist extractor: "))
filename = comp.gets(_("Insert: ")) filename = comp.read_input(_("Insert: "))
entries = extract_info(filename, extractor) entries = extract_info(filename, extractor)
if entries is None: if entries is None:
comp.update_status( comp.print_msg(
_("'{}': Can't extract playlist").format(filename)) _("'{}': Can't extract playlist").format(filename))
else: else:
bottom = comp.entries[comp.idx():] bottom = comp.entries[comp.idx():]
comp.entries = comp.entries[:comp.idx()] comp.entries = comp.entries[:comp.idx()]
comp.entries.extend(entries) comp.entries.extend(entries)
comp.entries.extend(bottom) comp.entries.extend(bottom)
comp.redraw() comp.refresh()
elif c == 109: # letter m elif c == 109: # letter m
comp.mode = MODES[(MODES.index(comp.mode) + 1) % 8] comp.mode = MODES[(MODES.index(comp.mode) + 1) % 8]
comp.update_status() comp.update_status()
elif c == 110: # letter n elif c == 110: # letter n
comp.next_search() comp.next_search()
elif c == 111: # letter o elif c == 111: # letter o
extractor = comp.gets(_("Playlist extractor: ")) extractor = comp.read_input(_("Playlist extractor: "))
filename = comp.gets(_("Open: ")) filename = comp.read_input(_("Open: "))
entries = extract_info(filename, extractor) entries = extract_info(filename, extractor)
if entries is None: if entries is None:
comp.update_status( comp.print_msg(
_("'{}': Can't extract playlist").format(filename)) _("'{}': Can't extract playlist").format(filename))
else: else:
comp.entries, comp.start, comp.y = entries, 0, 1 comp.entries, comp.start, comp.y = entries, 0, 1
comp.redraw() comp.refresh()
elif c == 112: # letter p elif c == 112: # letter p
comp.mp.pause ^= True comp.mp.pause ^= True
elif c in (curses.KEY_UP, 107): # up arrow or letter k elif c in (curses.KEY_UP, 107): # up arrow or letter k

142
doc/comp.1 Normal file
View file

@ -0,0 +1,142 @@
.\" Process this file with
.\" groff -man -Tutf8 comp.1
.\"
.TH COMP 1 2017-06-17 comp
.SH NAME
comp \- Curses Omni Media Player
.SH SYNOPSIS
\fBcomp\fR [\fB-h\fR] [\fB-e\fR {json,mpv,youtube-dl}] [\fB-c \fICONFIG\fR]
[\fB--vid \fIVID\fR] [\fB--vo \fIDRIVER\fR] [\fB-f \fIYTDL_FORMAT\fR]
\fIplaylist\fR
.SH DESCRIPTION
\fBcomp\fR is a
.BR mpv (1)
front-end using curses. It has basic media player functions and can to extract
playlists from multiple sources such as media sites supported by
.BR youtube-dl (1),
local and direct URL to video/audio and its own JSON playlist format.
.SH OPTIONS
.SS Positional arguments
.TP
.B playlist
path or URL to the playlist
.SS Optional arguments
.TP
.B -h, --help
show this help message and exit
.TP
.B -e \fR{json,mpv,youtube-dl}, \fB--extractor \fR{json,mpv,youtube-dl}
playlist extractor, default is \fIyoutube-dl
.TP
.B -c \fICONFIG, \fB--config \fICONFIG
path to the configuration file
.TP
.B --vid \fIVID
initial video channel. \fIauto\fR selects the default, \fIno\fR disables video
.TP
.B --vo \fIDRIVER
specify the video output backend to be used. See
.I VIDEO OUTPUT DRIVERS
in
.BR mpv (1)
for details and descriptions of available drivers
.TP
.B -f \fIYTDL_FORMAT\fR, \fB--format \fIYTDL_FORMAT
video format/quality to be passed to youtube-dl
.SH KEYBOARD CONTROL
.TP
.B Return
start playing
.TP
.B Space
select the current track
.TP
.B /, ?
search forward/backward for a pattern
.TP
.B <, >
go backward/forward in the playlist
.TP
.B A
toggle mute
.TP
.B N
repeat previous search in reverse direction
.TP
.B V
toggle video
.TP
.B W
save the current playlist under JSON format
.TP
.B d
delete current entry
.TP
.B i
insert playlist
.TP
.B m, M
cycle forward/backward through playing modes
.TP
.B n
repeat previous search
.TP
.B p
toggle pause
.TP
.B o
open playlist
.TP
.B w
download tracks set by playing mode
.TP
.B Up, k
move a single line up
.TP
.B Down, j
move a single line down
.TP
.B Left, h
seek backward 5 seconds
.TP
.B Right, l
seek forward 5 seconds
.TP
.B Home
move to the beginning of the playlist
.TP
.B End
move to the end of the playlist
.TP
.B Page Up
move a single page up
.TP
.B Page Down
move a single page down
.TP
.B F5
redraw the screen content
.SH FILES
.TP
.I ~/.config/comp/settings.ini
per user configuration file
.SH EXAMPLES
.nf R
Open a JSON playlist:
.ft B
comp -e json test/playlist.json
.ft R
Open a Youtube playlist with video height lower than 720:
.ft B
comp -f [height<720] https://www.youtube.com/list=PLnk14Iku8QM7R3ARnrj1TwYSZleF-i7jT
.SH BUGS
.PP
Media durations are not extracted from online playlists as youtube-dl
extract_flat option is used. This is rather a feature to save up bandwidth than
a bug because a track's duration is updated when it's played.
.SH AUTHOR
Written by Nguyễn Gia Phong.
.SH "SEE ALSO"
.BR mpv (1),
.BR youtube-dl (1)

View file

@ -18,6 +18,7 @@
import json import json
from collections import deque from collections import deque
from gettext import bindtextdomain, gettext as _, textdomain
from itertools import cycle from itertools import cycle
from os.path import abspath, expanduser, expandvars, isfile from os.path import abspath, expanduser, expandvars, isfile
from random import choice from random import choice
@ -25,12 +26,14 @@ from time import gmtime, sleep, strftime
from urllib import request from urllib import request
from youtube_dl import YoutubeDL from youtube_dl import YoutubeDL
from pkg_resources import resource_filename
from mpv import MPV, MpvFormat from mpv import MPV, MpvFormat
DEFAULT_ENTRY = {'filename': '', 'title': '', 'duration': '00:00:00', from .ie import extract_info
'error': False, 'playing': False, 'selected': False}
YTDL_OPTS = {'quiet': True, 'default_search': 'ytsearch', # Init gettext
'extract_flat': 'in_playlist'} bindtextdomain('omp', resource_filename('omp', 'locale'))
textdomain('omp')
class Omp(object): class Omp(object):
@ -48,8 +51,14 @@ class Omp(object):
playlist (iterator): iterator of tracks according to mode playlist (iterator): iterator of tracks according to mode
search_res (iterator): title-searched results search_res (iterator): title-searched results
vid (str): flag show if video output is enabled vid (str): flag show if video output is enabled
I/O handlers (defined by front-end):
print_msg(message, error=False): print a message
property_handler(name, val): called when a mpv property updated
read_input(prompt): prompt for user input
refresh(): update interface content
""" """
def __new__(cls, entries, handler, json_file, mode, mpv_vid, mpv_vo, ytdlf): def __new__(cls, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf):
self = super(Comp, cls).__new__(cls) self = super(Comp, cls).__new__(cls)
self.play_backward, self.reading = False, False self.play_backward, self.reading = False, False
self.playing = -1 self.playing = -1
@ -60,13 +69,12 @@ class Omp(object):
ytdl=True, ytdl_format=ytdlf) ytdl=True, ytdl_format=ytdlf)
return self return self
def __init__(self, entries, handler, json_file, mode, def __init__(self, entries, json_file, mode, mpv_vid, mpv_vo, ytdlf):
mpv_vid, mpv_vo, ytdlf):
if mpv_vo is not None: self.mp['vo'] = mpv_vo if mpv_vo is not None: self.mp['vo'] = mpv_vo
self.mp.observe_property('mute', handler) @self.mp.property_observer('mute')
self.mp.observe_property('pause', handler) @self.mp.property_observer('pause')
self.mp.observe_property('time-pos', handler, @self.mp.property_observer('time-pos', force_fmt=MpvFormat.INT64)
force_fmt=MpvFormat.INT64) def observer(name, value): self.property_handler(name, value)
def __enter__(self): return self def __enter__(self): return self
@ -130,5 +138,21 @@ class Omp(object):
else: else:
self.update_status(_("Pattern not found"), curses.color_pair(1)) self.update_status(_("Pattern not found"), curses.color_pair(1))
def dump_json(self):
s = self.read_input(
_("Save playlist to [{}]: ").format(self.json_file))
if s: self.json_file = abspath(expanduser(expandvars(s)))
try:
makedirs(dirname(comp.json_file), exist_ok=True)
with open(self.json_file, 'w') as f:
json.dump(self.entries, f, ensure_ascii=False,
indent=2, sort_keys=True)
except:
errmsg = _("'{}': Can't open file for writing").format(
self.json_file)
self.print_msg(errmsg, error=True)
else:
self.print_msg(_("'{}' written").format(comp.json_file))
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, exc_type, exc_value, traceback):
self.mp.quit() self.mp.quit()

View file

@ -1,8 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from os import listdir
from os.path import join
from setuptools import setup from setuptools import setup
with open('README.rst') as f: with open('README.rst') as f:
@ -10,7 +7,7 @@ with open('README.rst') as f:
setup( setup(
name='comp', name='comp',
version='0.3.5', version='0.3.6',
description=('Curses Omni Media Player'), description=('Curses Omni Media Player'),
long_description=long_description, long_description=long_description,
url='https://github.com/McSinyx/comp', url='https://github.com/McSinyx/comp',
@ -32,6 +29,8 @@ setup(
keywords='youtube-dl mpv-wrapper curses console-application multimedia', keywords='youtube-dl mpv-wrapper curses console-application multimedia',
packages=['omp'], packages=['omp'],
install_requires=['python-mpv', 'youtube-dl'], install_requires=['python-mpv', 'youtube-dl'],
python_requires='>=3.5',
package_data={'omp': ['locale/*/LC_MESSAGES/omp.mo']}, package_data={'omp': ['locale/*/LC_MESSAGES/omp.mo']},
data_files=[('share/man/man1', ['doc/comp.1'])],
scripts=['comp'], scripts=['comp'],
platforms=['POSIX']) platforms=['POSIX'])